diff options
Diffstat (limited to 'FrontEnd/src')
-rw-r--r-- | FrontEnd/src/views/common/dailog/OperationDialog.tsx | 4 | ||||
-rw-r--r-- | FrontEnd/src/views/common/input/InputPanel.css | 25 | ||||
-rw-r--r-- | FrontEnd/src/views/common/input/InputPanel.tsx | 247 | ||||
-rw-r--r-- | FrontEnd/src/views/register/index.tsx | 55 |
4 files changed, 329 insertions, 2 deletions
diff --git a/FrontEnd/src/views/common/dailog/OperationDialog.tsx b/FrontEnd/src/views/common/dailog/OperationDialog.tsx index 6bc846dd..b0ffdac9 100644 --- a/FrontEnd/src/views/common/dailog/OperationDialog.tsx +++ b/FrontEnd/src/views/common/dailog/OperationDialog.tsx @@ -1,6 +1,7 @@ import React, { useState } from "react"; import { useTranslation } from "react-i18next"; import { TwitterPicker } from "react-color"; +import classNames from "classnames"; import moment from "moment"; import { convertI18nText, I18nText, UiLogicError } from "@/common"; @@ -12,7 +13,6 @@ import LoadingButton from "../button/LoadingButton"; import Dialog from "./Dialog"; import "./OperationDialog.css"; -import classNames from "classnames"; interface DefaultErrorPromptProps { error?: string; @@ -42,7 +42,7 @@ export interface OperationDialogTextInput { initValue?: string; textFieldProps?: Omit< React.InputHTMLAttributes<HTMLInputElement>, - "type" | "value" | "onChange" | "aria-relevant" + "type" | "value" | "onChange" >; helperText?: string; } diff --git a/FrontEnd/src/views/common/input/InputPanel.css b/FrontEnd/src/views/common/input/InputPanel.css new file mode 100644 index 00000000..f9d6ac8b --- /dev/null +++ b/FrontEnd/src/views/common/input/InputPanel.css @@ -0,0 +1,25 @@ +.cru-input-panel-group { + display: block; + margin: 0.4em 0; +} + +.cru-input-panel-label { + display: block; + color: var(--cru-primary-color); +} + +.cru-input-panel-inline-label { + margin-inline-start: 0.5em; +} + +.cru-input-panel-error-text { + display: block; + font-size: 0.8em; + color: var(--cru-danger-color); +} + +.cru-input-panel-helper-text { + display: block; + font-size: 0.8em; + color: var(--cru-primary-color); +} diff --git a/FrontEnd/src/views/common/input/InputPanel.tsx b/FrontEnd/src/views/common/input/InputPanel.tsx new file mode 100644 index 00000000..1270cc53 --- /dev/null +++ b/FrontEnd/src/views/common/input/InputPanel.tsx @@ -0,0 +1,247 @@ +import React from "react"; +import classNames from "classnames"; +import { useTranslation } from "react-i18next"; +import { TwitterPicker } from "react-color"; + +import { convertI18nText, I18nText } from "@/common"; + +import "./InputPanel.css"; + +export interface TextInput { + type: "text"; + label?: I18nText; + helper?: I18nText; + password?: boolean; +} + +export interface BoolInput { + type: "bool"; + label: I18nText; + helper?: I18nText; +} + +export interface SelectInputOption { + value: string; + label: I18nText; + icon?: React.ReactElement; +} + +export interface SelectInput { + type: "select"; + label: I18nText; + options: SelectInputOption[]; +} + +export interface ColorInput { + type: "color"; + label?: I18nText; +} + +export interface DateTimeInput { + type: "datetime"; + label?: I18nText; + helper?: I18nText; +} + +export type Input = + | TextInput + | BoolInput + | SelectInput + | ColorInput + | DateTimeInput; + +interface InputTypeToValueTypeMap { + text: string; + bool: boolean; + select: string; + color: string; + datetime: string; +} + +type ValueTypes = InputTypeToValueTypeMap[keyof InputTypeToValueTypeMap]; + +type MapInputTypeToValueType<Type> = Type extends keyof InputTypeToValueTypeMap + ? InputTypeToValueTypeMap[Type] + : never; + +type MapInputToValueType<T> = T extends Input + ? MapInputTypeToValueType<T["type"]> + : T; + +type MapInputListToValueTypeList<Tuple extends readonly Input[]> = { + [Index in keyof Tuple]: MapInputToValueType<Tuple[Index]>; +} & { length: Tuple["length"] }; + +export type OperationInputError = (I18nText | null | undefined)[]; + +export interface InputPanelProps<InputList extends readonly Input[]> { + scheme: InputList; + values: MapInputListToValueTypeList<InputList>; + onChange: ( + values: MapInputListToValueTypeList<InputList>, + index: number + ) => void; + error?: OperationInputError; + disable?: boolean; +} + +const InputPanel = <InputList extends readonly Input[]>( + props: InputPanelProps<InputList> +): React.ReactElement => { + const { values, onChange, scheme, error, disable } = props; + + const { t } = useTranslation(); + + const updateValue = (index: number, newValue: ValueTypes): void => { + const oldValues = values; + const newValues = oldValues.slice(); + newValues[index] = newValue; + onChange( + newValues as unknown as MapInputListToValueTypeList<InputList>, + index + ); + }; + + return ( + <div> + {scheme.map((item, index) => { + const v = values[index]; + const e: string | null = convertI18nText(error?.[index], t); + + if (item.type === "text") { + return ( + <div + key={index} + className={classNames("cru-input-panel-group", e && "error")} + > + {item.label && ( + <label className="cru-input-panel-label"> + {convertI18nText(item.label, t)} + </label> + )} + <input + type={item.password === true ? "password" : "text"} + value={v as string} + onChange={(e) => { + const v = e.target.value; + updateValue(index, v); + }} + disabled={disable} + /> + {e && <div className="cru-input-panel-error-text">{e}</div>} + {item.helper && ( + <div className="cru-input-panel-helper-text"> + {convertI18nText(item.helper, t)} + </div> + )} + </div> + ); + } else if (item.type === "bool") { + return ( + <div + key={index} + className={classNames("cru-input-panel-group", e && "error")} + > + <input + type="checkbox" + checked={v as boolean} + onChange={(event) => { + const value = event.currentTarget.checked; + updateValue(index, value); + }} + disabled={disable} + /> + <label className="cru-input-panel-inline-label"> + {convertI18nText(item.label, t)} + </label> + {e != null && ( + <div className="cru-input-panel-error-text">{e}</div> + )} + {item.helper && ( + <div className="cru-input-panel-helper-text"> + {convertI18nText(item.helper, t)} + </div> + )} + </div> + ); + } else if (item.type === "select") { + return ( + <div + key={index} + className={classNames("cru-input-panel-group", e && "error")} + > + <label className="cru-input-panel-label"> + {convertI18nText(item.label, t)} + </label> + <select + value={v as string} + onChange={(event) => { + const value = event.target.value; + updateValue(index, value); + }} + disabled={disable} + > + {item.options.map((option, i) => { + return ( + <option value={option.value} key={i}> + {option.icon} + {convertI18nText(option.label, t)} + </option> + ); + })} + </select> + </div> + ); + } else if (item.type === "color") { + return ( + <div + key={index} + className={classNames("cru-input-panel-group", e && "error")} + > + <label className="cru-input-panel-inline-label"> + {convertI18nText(item.label, t)} + </label> + <TwitterPicker + color={v as string} + triangle="hide" + onChange={(result) => updateValue(index, result.hex)} + /> + </div> + ); + } else if (item.type === "datetime") { + return ( + <div + key={index} + className={classNames("cru-input-panel-group", e && "error")} + > + {item.label && ( + <label className="cru-input-panel-label"> + {convertI18nText(item.label, t)} + </label> + )} + <input + type="datetime-local" + value={v as string} + onChange={(e) => { + const v = e.target.value; + updateValue(index, v); + }} + disabled={disable} + /> + {e != null && ( + <div className="cru-input-panel-error-text">{e}</div> + )} + {item.helper && ( + <div className="cru-input-panel-helper-text"> + {convertI18nText(item.helper, t)} + </div> + )} + </div> + ); + } + })} + </div> + ); +}; + +export default InputPanel; diff --git a/FrontEnd/src/views/register/index.tsx b/FrontEnd/src/views/register/index.tsx new file mode 100644 index 00000000..da59ef94 --- /dev/null +++ b/FrontEnd/src/views/register/index.tsx @@ -0,0 +1,55 @@ +import React from "react"; + +const RegisterPage: React.FC = () => { + const [username, setUsername] = React.useState<string>(""); + const [password, setPassword] = React.useState<string>(""); + const [confirmPassword, setConfirmPassword] = React.useState<string>(""); + const [registerCode, setRegisterCode] = React.useState<string>(""); + + return ( + <div> + <div> + <label>Username</label> + <input + type="text" + value={username} + onChange={(e) => { + setUsername(e.target.value); + }} + /> + </div> + <div> + <label>Password</label> + <input + type="password" + value={password} + onChange={(e) => { + setPassword(e.target.value); + }} + /> + </div> + <div> + <label>Confirm Password</label> + <input + type="password" + value={confirmPassword} + onChange={(e) => { + setConfirmPassword(e.target.value); + }} + /> + </div> + <div> + <label>Register Code</label> + <input + type="text" + value={registerCode} + onChange={(e) => { + setRegisterCode(e.target.value); + }} + /> + </div> + </div> + ); +}; + +export default RegisterPage; |