aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--FrontEnd/src/views/common/dailog/OperationDialog.tsx4
-rw-r--r--FrontEnd/src/views/common/input/InputPanel.css25
-rw-r--r--FrontEnd/src/views/common/input/InputPanel.tsx247
-rw-r--r--FrontEnd/src/views/register/index.tsx55
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;