/* eslint-disable @next/next/no-img-element */ import styles from "./ui-lib.module.scss"; import LoadingIcon from "../icons/three-dots.svg"; import CloseIcon from "../icons/close.svg"; import EyeIcon from "../icons/eye.svg"; import EyeOffIcon from "../icons/eye-off.svg"; import DownIcon from "../icons/down.svg"; import ConfirmIcon from "../icons/confirm.svg"; import CancelIcon from "../icons/cancel.svg"; import MaxIcon from "../icons/max.svg"; import MinIcon from "../icons/min.svg"; import Locale from "../locales"; import { createRoot } from "react-dom/client"; import React, { HTMLProps, useEffect, useState } from "react"; import { IconButton } from "./button"; 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> ); } export function ListItem(props: { title: string; subTitle?: string; children?: JSX.Element | JSX.Element[]; icon?: JSX.Element; className?: string; onClick?: () => void; }) { return ( <div className={styles["list-item"] + ` ${props.className || ""}`} onClick={props.onClick} > <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> </div> {props.children} </div> ); } export function List(props: { children: | Array<JSX.Element | null | undefined> | JSX.Element | null | undefined; }) { return <div className={styles.list}>{props.children}</div>; } export function Loading() { return ( <div style={{ height: "100vh", width: "100vw", display: "flex", alignItems: "center", justifyContent: "center", }} > <LoadingIcon /> </div> ); } interface ModalProps { title: string; children?: any; actions?: JSX.Element[]; defaultMax?: boolean; onClose?: () => void; } export function Modal(props: ModalProps) { 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 }, []); const [isMax, setMax] = useState(!!props.defaultMax); return ( <div className={ styles["modal-container"] + ` ${isMax && styles["modal-container-max"]}` } > <div className={styles["modal-header"]}> <div className={styles["modal-title"]}>{props.title}</div> <div className={styles["modal-header-actions"]}> <div className={styles["modal-header-action"]} onClick={() => setMax(!isMax)} > {isMax ? <MinIcon /> : <MaxIcon />} </div> <div className={styles["modal-header-action"]} onClick={props.onClose} > <CloseIcon /> </div> </div> </div> <div className={styles["modal-content"]}>{props.children}</div> <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> </div> </div> ); } export function showModal(props: ModalProps) { const div = document.createElement("div"); div.className = "modal-mask"; document.body.appendChild(div); const root = createRoot(div); const closeModal = () => { props.onClose?.(); root.unmount(); div.remove(); }; div.onclick = (e) => { if (e.target === div) { closeModal(); } }; root.render(<Modal {...props} onClose={closeModal}></Modal>); } export type ToastProps = { content: string; action?: { text: string; onClick: () => void; }; onClose?: () => void; }; export function Toast(props: ToastProps) { return ( <div className={styles["toast-container"]}> <div className={styles["toast-content"]}> <span>{props.content}</span> {props.action && ( <button onClick={() => { props.action?.onClick?.(); props.onClose?.(); }} className={styles["toast-action"]} > {props.action.text} </button> )} </div> </div> ); } export function showToast( content: string, action?: ToastProps["action"], delay = 3000, ) { 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); root.render(<Toast content={content} action={action} onClose={close} />); } export type InputProps = React.HTMLProps<HTMLTextAreaElement> & { autoHeight?: boolean; rows?: number; }; export function Input(props: InputProps) { return ( <textarea {...props} className={`${styles["input"]} ${props.className}`} ></textarea> ); } 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> ); } export function Select( props: React.DetailedHTMLProps< React.SelectHTMLAttributes<HTMLSelectElement>, HTMLSelectElement >, ) { const { className, children, ...otherProps } = props; return ( <div className={`${styles["select-with-icon"]} ${className}`}> <select className={styles["select-with-icon-select"]} {...otherProps}> {children} </select> <DownIcon className={styles["select-with-icon-icon"]} /> </div> ); } export function showConfirm(content: any) { const div = document.createElement("div"); div.className = "modal-mask"; document.body.appendChild(div); const root = createRoot(div); const closeModal = () => { root.unmount(); div.remove(); }; return new Promise<boolean>((resolve) => { root.render( <Modal title={Locale.UI.Confirm} actions={[ <IconButton key="cancel" text={Locale.UI.Cancel} onClick={() => { resolve(false); closeModal(); }} icon={<CancelIcon />} tabIndex={0} bordered shadow ></IconButton>, <IconButton key="confirm" text={Locale.UI.Confirm} type="primary" onClick={() => { resolve(true); closeModal(); }} icon={<ConfirmIcon />} tabIndex={0} autoFocus bordered shadow ></IconButton>, ]} onClose={closeModal} > {content} </Modal>, ); }); } function PromptInput(props: { value: string; onChange: (value: string) => void; rows?: number; }) { const [input, setInput] = useState(props.value); const onInput = (value: string) => { props.onChange(value); setInput(value); }; return ( <textarea className={styles["modal-input"]} autoFocus value={input} onInput={(e) => onInput(e.currentTarget.value)} rows={props.rows ?? 3} ></textarea> ); } export function showPrompt(content: any, value = "", rows = 3) { const div = document.createElement("div"); div.className = "modal-mask"; document.body.appendChild(div); const root = createRoot(div); const closeModal = () => { root.unmount(); div.remove(); }; return new Promise<string>((resolve) => { let userInput = value; root.render( <Modal title={content} actions={[ <IconButton key="cancel" text={Locale.UI.Cancel} onClick={() => { closeModal(); }} icon={<CancelIcon />} bordered shadow tabIndex={0} ></IconButton>, <IconButton key="confirm" text={Locale.UI.Confirm} type="primary" onClick={() => { resolve(userInput); closeModal(); }} icon={<ConfirmIcon />} bordered shadow tabIndex={0} ></IconButton>, ]} onClose={closeModal} > <PromptInput onChange={(val) => (userInput = val)} value={value} rows={rows} ></PromptInput> </Modal>, ); }); } export function showImageModal(img: string) { showModal({ title: Locale.Export.Image.Modal, children: ( <div> <img src={img} alt="preview" style={{ maxWidth: "100%", }} ></img> </div> ), }); } export function Selector<T>(props: { items: Array<{ title: string; subTitle?: string; value: T; }>; defaultSelectedValue?: T; onSelection?: (selection: T[]) => void; onClose?: () => void; multiple?: boolean; }) { return ( <div className={styles["selector"]} onClick={() => props.onClose?.()}> <div className={styles["selector-content"]}> <List> {props.items.map((item, i) => { const selected = props.defaultSelectedValue === item.value; return ( <ListItem className={styles["selector-item"]} key={i} title={item.title} subTitle={item.subTitle} onClick={() => { props.onSelection?.([item.value]); props.onClose?.(); }} > {selected ? ( <div style={{ height: 10, width: 10, backgroundColor: "var(--primary)", borderRadius: 10, }} ></div> ) : ( <></> )} </ListItem> ); })} </List> </div> </div> ); }