ChatGPT-Next-Web/app/components/ui-lib.tsx

243 lines
5.5 KiB
TypeScript
Raw Normal View History

2023-03-12 19:06:21 +00:00
import styles from "./ui-lib.module.scss";
2023-03-15 05:50:11 +00:00
import LoadingIcon from "../icons/three-dots.svg";
2023-03-15 17:24:03 +00:00
import CloseIcon from "../icons/close.svg";
2023-04-21 17:13:23 +00:00
import EyeIcon from "../icons/eye.svg";
import EyeOffIcon from "../icons/eye-off.svg";
2023-03-21 14:56:27 +00:00
import { createRoot } from "react-dom/client";
2023-04-21 17:13:23 +00:00
import React, { HTMLProps, useEffect, useState } from "react";
import { IconButton } from "./button";
2023-03-11 17:14:07 +00:00
export function Popover(props: {
children: JSX.Element;
content: JSX.Element;
open?: boolean;
onClose?: () => void;
}) {
return (
<div className={styles.popover}>
{props.children}
{props.open && (
<div className={styles["popover-content"]}>
<div className={styles["popover-mask"]} onClick={props.onClose}></div>
{props.content}
</div>
)}
</div>
);
}
export function Card(props: { children: JSX.Element[]; className?: string }) {
return (
<div className={styles.card + " " + props.className}>{props.children}</div>
);
}
2023-04-22 17:27:15 +00:00
export function ListItem(props: {
title: string;
subTitle?: string;
children?: JSX.Element | JSX.Element[];
2023-04-24 16:49:27 +00:00
icon?: JSX.Element;
className?: string;
2023-04-22 17:27:15 +00:00
}) {
return (
2023-04-24 16:49:27 +00:00
<div className={styles["list-item"] + ` ${props.className}`}>
<div className={styles["list-header"]}>
{props.icon && <div className={styles["list-icon"]}>{props.icon}</div>}
<div className={styles["list-item-title"]}>
<div>{props.title}</div>
{props.subTitle && (
<div className={styles["list-item-sub-title"]}>
{props.subTitle}
</div>
)}
</div>
2023-04-22 17:27:15 +00:00
</div>
{props.children}
</div>
);
2023-03-11 17:14:07 +00:00
}
2023-04-24 16:49:27 +00:00
export function List(props: {
children:
| Array<JSX.Element | null | undefined>
| JSX.Element
| null
| undefined;
}) {
2023-03-11 17:14:07 +00:00
return <div className={styles.list}>{props.children}</div>;
}
2023-03-15 05:50:11 +00:00
export function Loading() {
2023-03-21 14:56:27 +00:00
return (
<div
style={{
height: "100vh",
width: "100vw",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<LoadingIcon />
</div>
);
2023-03-15 17:24:03 +00:00
}
interface ModalProps {
2023-03-21 14:56:27 +00:00
title: string;
2023-04-22 17:27:15 +00:00
children?: JSX.Element | JSX.Element[];
2023-03-21 14:56:27 +00:00
actions?: JSX.Element[];
onClose?: () => void;
2023-03-15 17:24:03 +00:00
}
export function Modal(props: ModalProps) {
2023-04-21 15:28:53 +00:00
useEffect(() => {
const onKeyDown = (e: KeyboardEvent) => {
if (e.key === "Escape") {
props.onClose?.();
}
};
window.addEventListener("keydown", onKeyDown);
return () => {
window.removeEventListener("keydown", onKeyDown);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
2023-03-21 14:56:27 +00:00
return (
<div className={styles["modal-container"]}>
<div className={styles["modal-header"]}>
<div className={styles["modal-title"]}>{props.title}</div>
2023-03-15 17:24:03 +00:00
2023-03-21 14:56:27 +00:00
<div className={styles["modal-close-btn"]} onClick={props.onClose}>
<CloseIcon />
</div>
2023-03-15 17:24:03 +00:00
</div>
2023-03-21 14:56:27 +00:00
<div className={styles["modal-content"]}>{props.children}</div>
2023-03-15 17:24:03 +00:00
2023-03-21 14:56:27 +00:00
<div className={styles["modal-footer"]}>
<div className={styles["modal-actions"]}>
{props.actions?.map((action, i) => (
<div key={i} className={styles["modal-action"]}>
{action}
</div>
))}
</div>
2023-03-15 17:24:03 +00:00
</div>
</div>
2023-03-21 14:56:27 +00:00
);
2023-03-15 17:24:03 +00:00
}
export function showModal(props: ModalProps) {
2023-03-21 14:56:27 +00:00
const div = document.createElement("div");
2023-03-15 17:24:03 +00:00
div.className = "modal-mask";
2023-03-21 14:56:27 +00:00
document.body.appendChild(div);
2023-03-15 17:24:03 +00:00
2023-03-21 14:56:27 +00:00
const root = createRoot(div);
const closeModal = () => {
2023-03-15 17:24:03 +00:00
props.onClose?.();
root.unmount();
div.remove();
2023-03-21 14:56:27 +00:00
};
div.onclick = (e) => {
if (e.target === div) {
closeModal();
}
};
root.render(<Modal {...props} onClose={closeModal}></Modal>);
}
2023-04-06 16:14:27 +00:00
export type ToastProps = {
content: string;
action?: {
text: string;
onClick: () => void;
};
};
2023-03-21 14:56:27 +00:00
export function Toast(props: ToastProps) {
return (
<div className={styles["toast-container"]}>
2023-04-06 16:14:27 +00:00
<div className={styles["toast-content"]}>
<span>{props.content}</span>
{props.action && (
<button
onClick={props.action.onClick}
className={styles["toast-action"]}
>
{props.action.text}
</button>
)}
</div>
2023-03-21 14:56:27 +00:00
</div>
);
}
2023-04-06 16:14:27 +00:00
export function showToast(
content: string,
action?: ToastProps["action"],
delay = 3000,
) {
2023-03-21 14:56:27 +00:00
const div = document.createElement("div");
div.className = styles.show;
document.body.appendChild(div);
const root = createRoot(div);
const close = () => {
div.classList.add(styles.hide);
setTimeout(() => {
root.unmount();
div.remove();
}, 300);
};
setTimeout(() => {
close();
}, delay);
2023-04-06 16:14:27 +00:00
root.render(<Toast content={content} action={action} />);
2023-03-21 14:56:27 +00:00
}
export type InputProps = React.HTMLProps<HTMLTextAreaElement> & {
autoHeight?: boolean;
rows?: number;
};
export function Input(props: InputProps) {
return (
<textarea
{...props}
className={`${styles["input"]} ${props.className}`}
></textarea>
);
}
2023-04-21 17:13:23 +00:00
export function PasswordInput(props: HTMLProps<HTMLInputElement>) {
const [visible, setVisible] = useState(false);
function changeVisibility() {
setVisible(!visible);
}
return (
<div className={"password-input-container"}>
<IconButton
icon={visible ? <EyeIcon /> : <EyeOffIcon />}
onClick={changeVisibility}
className={"password-eye"}
/>
<input
{...props}
type={visible ? "text" : "password"}
className={"password-input"}
/>
</div>
);
}