From 0e183074b326cf04a23ae1f1ba8dcc56166df485 Mon Sep 17 00:00:00 2001 From: crupest Date: Thu, 20 Jul 2023 20:44:15 +0800 Subject: ... --- FrontEnd/src/App.tsx | 20 ++++---------------- 1 file changed, 4 insertions(+), 16 deletions(-) (limited to 'FrontEnd/src/App.tsx') diff --git a/FrontEnd/src/App.tsx b/FrontEnd/src/App.tsx index cfdab229..07a8780f 100644 --- a/FrontEnd/src/App.tsx +++ b/FrontEnd/src/App.tsx @@ -2,35 +2,25 @@ import * as React from "react"; import { BrowserRouter, Route, Routes } from "react-router-dom"; import AppBar from "./views/common/AppBar"; +import NotFoundPage from "./pages/404"; import LoadingPage from "./views/common/LoadingPage"; +import About from "./pages/about"; import Center from "./views/center"; -import Home from "./views/home"; import Login from "./views/login"; import Register from "./views/register"; import Settings from "./views/settings"; -import About from "./views/about"; import TimelinePage from "./views/timeline"; import Search from "./views/search"; import Admin from "./views/admin"; import AlertHost from "./views/common/alert/AlertHost"; -import { useUser } from "./services/user"; - -const NoMatch: React.FC = () => { - return
Ah-oh, 404!
; -}; - -function App(): JSX.Element { - const user = useUser(); - +export default function App() { return ( }>
- :
} /> - } /> } /> } /> } /> @@ -40,12 +30,10 @@ function App(): JSX.Element { } /> } /> } /> - } /> + } /> ); } - -export default App; -- cgit v1.2.3 From d7b050ef7f047f841ffcda96fbdc9857574e97b9 Mon Sep 17 00:00:00 2001 From: crupest Date: Fri, 21 Jul 2023 17:02:05 +0800 Subject: ... --- FrontEnd/src/App.tsx | 8 +- FrontEnd/src/index.css | 1 - FrontEnd/src/pages/about/index.css | 1 - FrontEnd/src/pages/about/index.tsx | 5 +- FrontEnd/src/pages/setting/ChangeAvatarDialog.tsx | 6 +- .../src/pages/setting/ChangeNicknameDialog.tsx | 2 +- .../src/pages/setting/ChangePasswordDialog.tsx | 2 +- FrontEnd/src/pages/setting/index.css | 54 +++- FrontEnd/src/pages/setting/index.tsx | 171 +++++----- FrontEnd/src/views/common/Card.css | 8 +- FrontEnd/src/views/common/Card.tsx | 8 +- FrontEnd/src/views/common/Page.tsx | 15 + FrontEnd/src/views/common/index.css | 8 + FrontEnd/src/views/common/theme.css | 5 +- FrontEnd/src/views/settings/ChangeAvatarDialog.tsx | 354 --------------------- .../src/views/settings/ChangeNicknameDialog.tsx | 34 -- .../src/views/settings/ChangePasswordDialog.tsx | 69 ---- FrontEnd/src/views/settings/index.css | 31 -- FrontEnd/src/views/settings/index.tsx | 338 -------------------- 19 files changed, 182 insertions(+), 938 deletions(-) create mode 100644 FrontEnd/src/views/common/Page.tsx delete mode 100644 FrontEnd/src/views/settings/ChangeAvatarDialog.tsx delete mode 100644 FrontEnd/src/views/settings/ChangeNicknameDialog.tsx delete mode 100644 FrontEnd/src/views/settings/ChangePasswordDialog.tsx delete mode 100644 FrontEnd/src/views/settings/index.css delete mode 100644 FrontEnd/src/views/settings/index.tsx (limited to 'FrontEnd/src/App.tsx') diff --git a/FrontEnd/src/App.tsx b/FrontEnd/src/App.tsx index 07a8780f..92fe0652 100644 --- a/FrontEnd/src/App.tsx +++ b/FrontEnd/src/App.tsx @@ -4,11 +4,11 @@ import { BrowserRouter, Route, Routes } from "react-router-dom"; import AppBar from "./views/common/AppBar"; import NotFoundPage from "./pages/404"; import LoadingPage from "./views/common/LoadingPage"; -import About from "./pages/about"; +import AboutPage from "./pages/about"; +import SettingPage from "./pages/setting"; import Center from "./views/center"; import Login from "./views/login"; import Register from "./views/register"; -import Settings from "./views/settings"; import TimelinePage from "./views/timeline"; import Search from "./views/search"; import Admin from "./views/admin"; @@ -24,8 +24,8 @@ export default function App() { } /> } /> } /> - } /> - } /> + } /> + } /> } /> } /> } /> diff --git a/FrontEnd/src/index.css b/FrontEnd/src/index.css index 3478db05..49791c23 100644 --- a/FrontEnd/src/index.css +++ b/FrontEnd/src/index.css @@ -1,4 +1,3 @@ -@import "npm:bootstrap/dist/css/bootstrap-grid.css"; @import "npm:bootstrap-icons/font/bootstrap-icons.css"; @import "./views/common/index.css"; diff --git a/FrontEnd/src/pages/about/index.css b/FrontEnd/src/pages/about/index.css index 487f4a0a..1ce7a7c8 100644 --- a/FrontEnd/src/pages/about/index.css +++ b/FrontEnd/src/pages/about/index.css @@ -1,5 +1,4 @@ .about-page { - padding: 1em 2em; line-height: 1.5; } diff --git a/FrontEnd/src/pages/about/index.tsx b/FrontEnd/src/pages/about/index.tsx index afd4de34..acec1735 100644 --- a/FrontEnd/src/pages/about/index.tsx +++ b/FrontEnd/src/pages/about/index.tsx @@ -1,6 +1,7 @@ import "./index.css"; import { useC } from "@/common"; +import Page from "@/views/common/Page"; interface Credit { name: string; @@ -52,7 +53,7 @@ export default function AboutPage() { const c = useC(); return ( -
+

{c("about.credits.title")}

{c("about.credits.content")}

{c("about.credits.frontend")}

@@ -81,6 +82,6 @@ export default function AboutPage() { })}
  • ...
  • -
    + ); } diff --git a/FrontEnd/src/pages/setting/ChangeAvatarDialog.tsx b/FrontEnd/src/pages/setting/ChangeAvatarDialog.tsx index 44bd2c68..b2a4e2a8 100644 --- a/FrontEnd/src/pages/setting/ChangeAvatarDialog.tsx +++ b/FrontEnd/src/pages/setting/ChangeAvatarDialog.tsx @@ -9,9 +9,9 @@ import { useUser } from "@/services/user"; import { getHttpUserClient } from "@/http/user"; -import ImageCropper, { Clip, applyClipToImage } from "../common/ImageCropper"; -import Button from "../common/button/Button"; -import Dialog from "../common/dialog/Dialog"; +import ImageCropper, { Clip, applyClipToImage } from "@/views/common/ImageCropper"; +import Button from "@/views/common/button/Button"; +import Dialog from "@/views/common/dialog/Dialog"; export interface ChangeAvatarDialogProps { open: boolean; diff --git a/FrontEnd/src/pages/setting/ChangeNicknameDialog.tsx b/FrontEnd/src/pages/setting/ChangeNicknameDialog.tsx index 7ba12de8..11c86222 100644 --- a/FrontEnd/src/pages/setting/ChangeNicknameDialog.tsx +++ b/FrontEnd/src/pages/setting/ChangeNicknameDialog.tsx @@ -2,7 +2,7 @@ import { getHttpUserClient } from "@/http/user"; import { useUser } from "@/services/user"; import * as React from "react"; -import OperationDialog from "../common/dialog/OperationDialog"; +import OperationDialog from "@/views/common/dialog/OperationDialog"; export interface ChangeNicknameDialogProps { open: boolean; diff --git a/FrontEnd/src/pages/setting/ChangePasswordDialog.tsx b/FrontEnd/src/pages/setting/ChangePasswordDialog.tsx index a34ca4a7..a523b454 100644 --- a/FrontEnd/src/pages/setting/ChangePasswordDialog.tsx +++ b/FrontEnd/src/pages/setting/ChangePasswordDialog.tsx @@ -4,7 +4,7 @@ import { useNavigate } from "react-router-dom"; import { userService } from "@/services/user"; -import OperationDialog from "../common/dialog/OperationDialog"; +import OperationDialog from "@/views/common/dialog/OperationDialog"; export interface ChangePasswordDialogProps { open: boolean; diff --git a/FrontEnd/src/pages/setting/index.css b/FrontEnd/src/pages/setting/index.css index ccf7a97a..af5ccf20 100644 --- a/FrontEnd/src/pages/setting/index.css +++ b/FrontEnd/src/pages/setting/index.css @@ -1,3 +1,5 @@ +/* TODO: Make item prettier. */ + .change-avatar-cropper-row { max-height: 400px; } @@ -8,20 +10,56 @@ max-height: 400px; } -.settings-item { +.setting-section { + padding: 1em 0; + margin: 1em 0; +} + +.setting-section-title { + padding: 0 1em; +} + +.setting-section-item-area { + margin-top: 1em; + border-top: 1px solid var(--cru-key-container-color); +} + +.setting-item-container { padding: 0.5em 1em; - transition: background 0.3s; - border-bottom: 1px solid #e9ecef; + transition: background-color 0.3s; + color: var(--cru-surface-on-color); + background-color: var(--cru-surface-color); + border-bottom: 1px solid var(--cru-key-container-color); + display: flex; align-items: center; } -.settings-item.first { - border-top: 1px solid #e9ecef; + +.setting-item-container:hover { + background-color: var(--cru-key-container-1-color); + border-bottom-color: var(--cru-key-container-1-color); +} + +.setting-item-label-sub { + color: var(--cru-secondary-text-color); } -.settings-item.clickable { + +.setting-item-container.setting-type-button { cursor: pointer; } -.settings-item:hover { - background: #dee2e6; + +.setting-item-container.setting-type-button.danger { + color: var(--cru-danger-color); +} + + +@media (max-width: 576) { + .setting-item-container.setting-type-select { + flex-direction: column; + } + + .setting-item-container.setting-type-select .setting-item-value-area { + margin-top: 1em; + } } .register-code { diff --git a/FrontEnd/src/pages/setting/index.tsx b/FrontEnd/src/pages/setting/index.tsx index 4e28585e..5d6dcbc0 100644 --- a/FrontEnd/src/pages/setting/index.tsx +++ b/FrontEnd/src/pages/setting/index.tsx @@ -11,11 +11,11 @@ import classNames from "classnames"; import { useC, Text } from "@/common"; import { useUser, userService } from "@/services/user"; import { getHttpUserClient } from "@/http/user"; -import { TimelineVisibility } from "@/http/timeline"; import ConfirmDialog from "@/views/common/dialog/ConfirmDialog"; import Card from "@/views/common/Card"; import Spinner from "@/views/common/Spinner"; +import Page from "@/views/common/Page"; import ChangePasswordDialog from "./ChangePasswordDialog"; import ChangeAvatarDialog from "./ChangeAvatarDialog"; import ChangeNicknameDialog from "./ChangeNicknameDialog"; @@ -24,9 +24,9 @@ import "./index.css"; import { pushAlert } from "@/services/alert"; interface SettingSectionProps - extends Omit, "title"> { + extends Omit, "title"> { title: Text; - children: ReactNode; + children?: ReactNode; } function SettingSection({ @@ -40,7 +40,7 @@ function SettingSection({ return (

    {c(title)}

    - {children} +
    {children}
    ); } @@ -115,6 +115,7 @@ function SelectSettingsItem({ ) : ( -
    - -
    -
    -
    - - ); - } else if (state === "crop") { - if (fileUrl == null) { - throw new UiLogicError(); - } - return ( - <> -
    -
    - -
    -
    - {t("settings.dialogChangeAvatar.prompt.crop")} -
    -
    -
    -
    -
    - - ); - } else if (state === "processcrop") { - return ( - <> -
    -
    - {t("settings.dialogChangeAvatar.prompt.processingCrop")} -
    -
    -
    -
    -
    - - ); - } else if (state === "preview") { - return ( - <> -
    - {createPreviewRow()} -
    - {t("settings.dialogChangeAvatar.prompt.preview")} -
    -
    -
    -
    -
    - - ); - } else if (state === "uploading") { - return ( - <> -
    - {createPreviewRow()} -
    - {t("settings.dialogChangeAvatar.prompt.uploading")} -
    -
    - - ); - } else if (state === "success") { - return ( - <> -
    -
    - {t("operationDialog.success")} -
    -
    -
    -
    -
    - - ); - } else { - return ( - <> -
    - {createPreviewRow()} -
    {trueMessage}
    -
    -
    -
    -
    - - ); - } - })()} - - ); -}; - -export default ChangeAvatarDialog; diff --git a/FrontEnd/src/views/settings/ChangeNicknameDialog.tsx b/FrontEnd/src/views/settings/ChangeNicknameDialog.tsx deleted file mode 100644 index 7ba12de8..00000000 --- a/FrontEnd/src/views/settings/ChangeNicknameDialog.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { getHttpUserClient } from "@/http/user"; -import { useUser } from "@/services/user"; -import * as React from "react"; - -import OperationDialog from "../common/dialog/OperationDialog"; - -export interface ChangeNicknameDialogProps { - open: boolean; - close: () => void; -} - -const ChangeNicknameDialog: React.FC = (props) => { - const user = useUser(); - - if (user == null) return null; - - return ( - { - return getHttpUserClient().patch(user.username, { - nickname: newNickname, - }); - }} - onClose={props.close} - /> - ); -}; - -export default ChangeNicknameDialog; diff --git a/FrontEnd/src/views/settings/ChangePasswordDialog.tsx b/FrontEnd/src/views/settings/ChangePasswordDialog.tsx deleted file mode 100644 index a34ca4a7..00000000 --- a/FrontEnd/src/views/settings/ChangePasswordDialog.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import { useState } from "react"; -import * as React from "react"; -import { useNavigate } from "react-router-dom"; - -import { userService } from "@/services/user"; - -import OperationDialog from "../common/dialog/OperationDialog"; - -export interface ChangePasswordDialogProps { - open: boolean; - close: () => void; -} - -const ChangePasswordDialog: React.FC = (props) => { - const navigate = useNavigate(); - - const [redirect, setRedirect] = useState(false); - - return ( - { - const result: Record = {}; - if (oldPassword === "") { - result[0] = "settings.dialogChangePassword.errorEmptyOldPassword"; - } - if (newPassword === "") { - result[1] = "settings.dialogChangePassword.errorEmptyNewPassword"; - } - if (retypedNewPassword !== newPassword) { - result[2] = "settings.dialogChangePassword.errorRetypeNotMatch"; - } - return result; - }} - onProcess={async ([oldPassword, newPassword]) => { - await userService.changePassword(oldPassword, newPassword); - setRedirect(true); - }} - onClose={() => { - props.close(); - if (redirect) { - navigate("/login"); - } - }} - /> - ); -}; - -export default ChangePasswordDialog; diff --git a/FrontEnd/src/views/settings/index.css b/FrontEnd/src/views/settings/index.css deleted file mode 100644 index ccf7a97a..00000000 --- a/FrontEnd/src/views/settings/index.css +++ /dev/null @@ -1,31 +0,0 @@ -.change-avatar-cropper-row { - max-height: 400px; -} - -.change-avatar-img { - min-width: 50%; - max-width: 100%; - max-height: 400px; -} - -.settings-item { - padding: 0.5em 1em; - transition: background 0.3s; - border-bottom: 1px solid #e9ecef; - align-items: center; -} -.settings-item.first { - border-top: 1px solid #e9ecef; -} -.settings-item.clickable { - cursor: pointer; -} -.settings-item:hover { - background: #dee2e6; -} - -.register-code { - border: 1px solid black; - border-radius: 3px; - padding: 0.2em; -} \ No newline at end of file diff --git a/FrontEnd/src/views/settings/index.tsx b/FrontEnd/src/views/settings/index.tsx deleted file mode 100644 index 6647826f..00000000 --- a/FrontEnd/src/views/settings/index.tsx +++ /dev/null @@ -1,338 +0,0 @@ -import { useState } from "react"; -import * as React from "react"; -import { useNavigate } from "react-router-dom"; -import { useTranslation } from "react-i18next"; -import classNames from "classnames"; - -import { convertI18nText, I18nText, UiLogicError } from "@/common"; -import { useUser, userService } from "@/services/user"; -import { getHttpUserClient } from "@/http/user"; -import { TimelineVisibility } from "@/http/timeline"; - -import ConfirmDialog from "../common/dialog/ConfirmDialog"; -import Card from "../common/Card"; -import Spinner from "../common/Spinner"; -import ChangePasswordDialog from "./ChangePasswordDialog"; -import ChangeAvatarDialog from "./ChangeAvatarDialog"; -import ChangeNicknameDialog from "./ChangeNicknameDialog"; - -import "./index.css"; -import { pushAlert } from "@/services/alert"; - -interface SettingSectionProps { - title: I18nText; - children: React.ReactNode; -} - -const SettingSection: React.FC = ({ title, children }) => { - const { t } = useTranslation(); - - return ( - -

    - {convertI18nText(title, t)} -

    - {children} -
    - ); -}; - -interface SettingItemContainerWithoutChildrenProps { - title: I18nText; - subtext?: I18nText; - first?: boolean; - danger?: boolean; - style?: React.CSSProperties; - className?: string; - onClick?: () => void; -} - -interface SettingItemContainerProps - extends SettingItemContainerWithoutChildrenProps { - children?: React.ReactNode; -} - -function SettingItemContainer({ - title, - subtext, - first, - danger, - children, - style, - className, - onClick, -}: SettingItemContainerProps): JSX.Element { - const { t } = useTranslation(); - - return ( -
    -
    -
    - {convertI18nText(title, t)} -
    - - {convertI18nText(subtext, t)} - -
    -
    {children}
    -
    - ); -} - -type ButtonSettingItemProps = SettingItemContainerWithoutChildrenProps; - -const ButtonSettingItem: React.FC = ({ ...props }) => { - return ; -}; - -interface SelectSettingItemProps - extends SettingItemContainerWithoutChildrenProps { - options: { - value: string; - label: I18nText; - }[]; - value?: string; - onSelect: (value: string) => void; -} - -const SelectSettingsItem: React.FC = ({ - options, - value, - onSelect, - ...props -}) => { - const { t } = useTranslation(); - - return ( - - {value == null ? ( - - ) : ( - - )} - - ); -}; - -const SettingsPage: React.FC = () => { - const { i18n } = useTranslation(); - const user = useUser(); - const navigate = useNavigate(); - - const [dialog, setDialog] = useState< - | null - | "changepassword" - | "changeavatar" - | "changenickname" - | "logout" - | "renewregistercode" - >(null); - - const [registerCode, setRegisterCode] = useState( - undefined, - ); - - const [bookmarkVisibility, setBookmarkVisibility] = - useState(); - - React.useEffect(() => { - if (user != null) { - void getHttpUserClient() - .getBookmarkVisibility(user.username) - .then(({ visibility }) => { - setBookmarkVisibility(visibility); - }); - } else { - setBookmarkVisibility(undefined); - } - }, [user]); - - React.useEffect(() => { - setRegisterCode(undefined); - }, [user]); - - React.useEffect(() => { - if (user != null && registerCode === undefined) { - void getHttpUserClient() - .getRegisterCode(user.username) - .then((code) => { - setRegisterCode(code.registerCode ?? null); - }); - } - }, [user, registerCode]); - - const language = i18n.language.slice(0, 2); - - return ( - <> -
    - {user ? ( - - setDialog("renewregistercode")} - > - {registerCode === undefined ? ( - - ) : registerCode === null ? ( - Noop - ) : ( - { - void navigator.clipboard - .writeText(registerCode) - .then(() => { - pushAlert({ - type: "success", - message: "settings.myRegisterCodeCopied", - }); - }); - event.stopPropagation(); - }} - > - {registerCode} - - )} - - setDialog("changeavatar")} - first - /> - setDialog("changenickname")} - /> - { - void getHttpUserClient() - .putBookmarkVisibility(user.username, { - visibility: value as TimelineVisibility, - }) - .then(() => { - setBookmarkVisibility(value as TimelineVisibility); - }); - }} - /> - setDialog("changepassword")} - danger - /> - { - setDialog("logout"); - }} - danger - /> - - ) : null} - - { - void i18n.changeLanguage(value); - }} - first - /> - -
    - setDialog(null)} - /> - setDialog(null)} - open={dialog === "logout"} - onConfirm={() => { - void userService.logout().then(() => { - navigate("/"); - }); - }} - /> - setDialog(null)} - open={dialog === "renewregistercode"} - onConfirm={() => { - if (user == null) throw new UiLogicError(); - void getHttpUserClient() - .renewRegisterCode(user.username) - .then(() => { - setRegisterCode(undefined); - }); - }} - /> - setDialog(null)} - /> - setDialog(null)} - /> - - ); -}; - -export default SettingsPage; -- cgit v1.2.3 From 77b03b17a59655c1eeb00e0a818c81f8ea5e326e Mon Sep 17 00:00:00 2001 From: crupest Date: Sat, 29 Jul 2023 22:41:32 +0800 Subject: ... --- FrontEnd/src/App.tsx | 2 +- FrontEnd/src/pages/login/index.css | 10 ++ FrontEnd/src/pages/login/index.tsx | 133 +++++++++++++++++++++++++ FrontEnd/src/views/common/input/InputGroup.tsx | 23 ++++- 4 files changed, 163 insertions(+), 5 deletions(-) create mode 100644 FrontEnd/src/pages/login/index.css create mode 100644 FrontEnd/src/pages/login/index.tsx (limited to 'FrontEnd/src/App.tsx') diff --git a/FrontEnd/src/App.tsx b/FrontEnd/src/App.tsx index 92fe0652..8f2bf6b0 100644 --- a/FrontEnd/src/App.tsx +++ b/FrontEnd/src/App.tsx @@ -7,7 +7,7 @@ import LoadingPage from "./views/common/LoadingPage"; import AboutPage from "./pages/about"; import SettingPage from "./pages/setting"; import Center from "./views/center"; -import Login from "./views/login"; +import Login from "./pages/login"; import Register from "./views/register"; import TimelinePage from "./views/timeline"; import Search from "./views/search"; diff --git a/FrontEnd/src/pages/login/index.css b/FrontEnd/src/pages/login/index.css new file mode 100644 index 00000000..d78b3587 --- /dev/null +++ b/FrontEnd/src/pages/login/index.css @@ -0,0 +1,10 @@ +.login-page { + display: flex; + flex-direction: column; + align-items: center; +} + +.login-page-welcome { + text-align: center; + font-size: 2em; +} \ No newline at end of file diff --git a/FrontEnd/src/pages/login/index.tsx b/FrontEnd/src/pages/login/index.tsx new file mode 100644 index 00000000..9aee455f --- /dev/null +++ b/FrontEnd/src/pages/login/index.tsx @@ -0,0 +1,133 @@ +import { useState, useEffect } from "react"; +import { Link, useNavigate } from "react-router-dom"; +import { Trans } from "react-i18next"; + +import { useUser, userService } from "@/services/user"; + +import { useC } from "@/views/common/common"; +import LoadingButton from "@/views/common/button/LoadingButton"; +import { + InputErrorDict, + InputGroup, + useInputs, +} from "@/views/common/input/InputGroup"; +import Page from "@/views/common/Page"; + +import "./index.css"; + +export default function LoginPage() { + const c = useC(); + + const user = useUser(); + + const navigate = useNavigate(); + + const [process, setProcess] = useState(false); + const [error, setError] = useState(null); + + const { hasErrorAndDirty, confirm, setAllDisabled, inputGroupProps } = + useInputs({ + init: { + scheme: { + inputs: [ + { + key: "username", + type: "text", + label: "user.username", + }, + { + key: "password", + type: "text", + label: "user.password", + password: true, + }, + { + key: "rememberMe", + type: "bool", + label: "user.rememberMe", + }, + ], + validator: ({ username, password }) => { + const result: InputErrorDict = {}; + if (username === "") { + result["username"] = "login.emptyUsername"; + } + if (password === "") { + result["password"] = "login.emptyPassword"; + } + return result; + }, + }, + dataInit: {}, + }, + }); + + useEffect(() => { + if (user != null) { + const id = setTimeout(() => navigate("/"), 3000); + return () => { + clearTimeout(id); + }; + } + }, [navigate, user]); + + if (user != null) { + return

    {c("login.alreadyLogin")}

    ; + } + + const submit = (): void => { + const confirmResult = confirm(); + if (confirmResult.type === "ok") { + const { username, password, rememberMe } = confirmResult.values; + setAllDisabled(true); + setProcess(true); + userService + .login( + { + username: username as string, + password: password as string, + }, + rememberMe as boolean, + ) + .then( + () => { + if (history.length === 0) { + navigate("/"); + } else { + navigate(-1); + } + }, + (e: Error) => { + setProcess(false); + setAllDisabled(false); + setError(e.message); + }, + ); + } + }; + + return ( + +
    +
    {c("welcome")}
    + + {error ?

    {c(error)}

    : null} +
    + { + submit(); + e.preventDefault(); + }} + disabled={hasErrorAndDirty} + > + {c("user.login")} + +
    + + 012 + +
    +
    + ); +} diff --git a/FrontEnd/src/views/common/input/InputGroup.tsx b/FrontEnd/src/views/common/input/InputGroup.tsx index 3d1e3ada..ee89b05c 100644 --- a/FrontEnd/src/views/common/input/InputGroup.tsx +++ b/FrontEnd/src/views/common/input/InputGroup.tsx @@ -23,7 +23,7 @@ * `useInputs` hook takes care of logic and generate props for `InputGroup`. */ -import { useState, Ref } from "react"; +import { useState, Ref, useId } from "react"; import classNames from "classnames"; import { useC, Text, ThemeColor } from "../common"; @@ -332,6 +332,8 @@ export function InputGroup({ }: InputGroupProps) { const c = useC(); + const id = useId(); + return (
    - {label && } + {label && ( + + )} { @@ -382,6 +391,7 @@ export function InputGroup({ return (
    { @@ -390,7 +400,9 @@ export function InputGroup({ }} disabled={disabled} /> - + {error &&
    {c(error)}
    } {helper &&
    {c(helper)}
    }
    @@ -398,8 +410,11 @@ export function InputGroup({ } else if (type === "select") { return (
    - + { - setUsername(e.target.value); - setUsernameDirty(true); - }} - value={username} - /> - {usernameDirty && username === "" && ( -
    - {t("login.emptyUsername")} -
    - )} -
    -
    - - { - setPassword(e.target.value); - setPasswordDirty(true); - }} - value={password} - onKeyDown={onEnterPressInPassword} - /> - {passwordDirty && password === "" && ( -
    - {t("login.emptyPassword")} -
    - )} -
    -
    - { - setRememberMe(e.currentTarget.checked); - }} - /> - -
    - {error ?

    {t(error)}

    : null} -
    - { - submit(); - e.preventDefault(); - }} - disabled={username === "" || password === "" ? true : undefined} - > - {t("user.login")} - -
    - - 012 - -
    - ); -}; - -export default LoginPage; -- cgit v1.2.3 From 538d6830a0022b49b99695095d85e567b0c86e71 Mon Sep 17 00:00:00 2001 From: crupest Date: Sun, 30 Jul 2023 23:47:53 +0800 Subject: ... --- FrontEnd/src/App.tsx | 14 +- FrontEnd/src/pages/home/index.css | 7 + FrontEnd/src/pages/home/index.tsx | 7 + FrontEnd/src/pages/loading/index.css | 7 + FrontEnd/src/pages/loading/index.tsx | 11 + FrontEnd/src/pages/timeline/CollapseButton.tsx | 21 ++ .../src/pages/timeline/ConnectionStatusBadge.css | 36 +++ .../src/pages/timeline/ConnectionStatusBadge.tsx | 41 ++++ FrontEnd/src/pages/timeline/MarkdownPostEdit.css | 21 ++ FrontEnd/src/pages/timeline/MarkdownPostEdit.tsx | 215 +++++++++++++++++ FrontEnd/src/pages/timeline/Timeline.css | 244 +++++++++++++++++++ FrontEnd/src/pages/timeline/Timeline.tsx | 207 ++++++++++++++++ FrontEnd/src/pages/timeline/TimelineCard.tsx | 167 +++++++++++++ FrontEnd/src/pages/timeline/TimelineDateLabel.tsx | 19 ++ .../src/pages/timeline/TimelineDeleteDialog.tsx | 61 +++++ FrontEnd/src/pages/timeline/TimelineEmptyItem.tsx | 25 ++ FrontEnd/src/pages/timeline/TimelineLine.tsx | 51 ++++ FrontEnd/src/pages/timeline/TimelineLoading.tsx | 16 ++ FrontEnd/src/pages/timeline/TimelineMember.css | 8 + FrontEnd/src/pages/timeline/TimelineMember.tsx | 202 ++++++++++++++++ .../src/pages/timeline/TimelinePostContentView.tsx | 187 +++++++++++++++ FrontEnd/src/pages/timeline/TimelinePostEdit.css | 10 + FrontEnd/src/pages/timeline/TimelinePostEdit.tsx | 267 +++++++++++++++++++++ .../src/pages/timeline/TimelinePostEditCard.tsx | 31 +++ .../src/pages/timeline/TimelinePostEditNoLogin.tsx | 18 ++ .../src/pages/timeline/TimelinePostListView.tsx | 76 ++++++ FrontEnd/src/pages/timeline/TimelinePostView.tsx | 149 ++++++++++++ .../timeline/TimelinePropertyChangeDialog.tsx | 87 +++++++ FrontEnd/src/pages/timeline/index.tsx | 23 ++ .../src/views/common/dialog/OperationDialog.tsx | 6 +- FrontEnd/src/views/common/input/InputGroup.tsx | 47 +++- 31 files changed, 2257 insertions(+), 24 deletions(-) create mode 100644 FrontEnd/src/pages/home/index.css create mode 100644 FrontEnd/src/pages/home/index.tsx create mode 100644 FrontEnd/src/pages/loading/index.css create mode 100644 FrontEnd/src/pages/loading/index.tsx create mode 100644 FrontEnd/src/pages/timeline/CollapseButton.tsx create mode 100644 FrontEnd/src/pages/timeline/ConnectionStatusBadge.css create mode 100644 FrontEnd/src/pages/timeline/ConnectionStatusBadge.tsx create mode 100644 FrontEnd/src/pages/timeline/MarkdownPostEdit.css create mode 100644 FrontEnd/src/pages/timeline/MarkdownPostEdit.tsx create mode 100644 FrontEnd/src/pages/timeline/Timeline.css create mode 100644 FrontEnd/src/pages/timeline/Timeline.tsx create mode 100644 FrontEnd/src/pages/timeline/TimelineCard.tsx create mode 100644 FrontEnd/src/pages/timeline/TimelineDateLabel.tsx create mode 100644 FrontEnd/src/pages/timeline/TimelineDeleteDialog.tsx create mode 100644 FrontEnd/src/pages/timeline/TimelineEmptyItem.tsx create mode 100644 FrontEnd/src/pages/timeline/TimelineLine.tsx create mode 100644 FrontEnd/src/pages/timeline/TimelineLoading.tsx create mode 100644 FrontEnd/src/pages/timeline/TimelineMember.css create mode 100644 FrontEnd/src/pages/timeline/TimelineMember.tsx create mode 100644 FrontEnd/src/pages/timeline/TimelinePostContentView.tsx create mode 100644 FrontEnd/src/pages/timeline/TimelinePostEdit.css create mode 100644 FrontEnd/src/pages/timeline/TimelinePostEdit.tsx create mode 100644 FrontEnd/src/pages/timeline/TimelinePostEditCard.tsx create mode 100644 FrontEnd/src/pages/timeline/TimelinePostEditNoLogin.tsx create mode 100644 FrontEnd/src/pages/timeline/TimelinePostListView.tsx create mode 100644 FrontEnd/src/pages/timeline/TimelinePostView.tsx create mode 100644 FrontEnd/src/pages/timeline/TimelinePropertyChangeDialog.tsx create mode 100644 FrontEnd/src/pages/timeline/index.tsx (limited to 'FrontEnd/src/App.tsx') diff --git a/FrontEnd/src/App.tsx b/FrontEnd/src/App.tsx index f638f5e8..ca3e4d38 100644 --- a/FrontEnd/src/App.tsx +++ b/FrontEnd/src/App.tsx @@ -1,27 +1,26 @@ -import * as React from "react"; +import { Suspense } from "react"; import { BrowserRouter, Route, Routes } from "react-router-dom"; import AppBar from "./views/common/AppBar"; import NotFoundPage from "./pages/404"; -import LoadingPage from "./views/common/LoadingPage"; +import HomePage from "./pages/home"; import AboutPage from "./pages/about"; import SettingPage from "./pages/setting"; -import Center from "./views/center"; import LoginPage from "./pages/login"; import RegisterPage from "./pages/register"; -import TimelinePage from "./views/timeline"; +import TimelinePage from "./pages/timeline"; +import LoadingPage from "./pages/loading"; import Search from "./views/search"; import Admin from "./views/admin"; import AlertHost from "./views/common/alert/AlertHost"; export default function App() { return ( - }> + }>
    - } /> } /> } /> } /> @@ -30,10 +29,11 @@ export default function App() { } /> } /> } /> + } /> } /> - + ); } diff --git a/FrontEnd/src/pages/home/index.css b/FrontEnd/src/pages/home/index.css new file mode 100644 index 00000000..bc72a182 --- /dev/null +++ b/FrontEnd/src/pages/home/index.css @@ -0,0 +1,7 @@ +.home-page { + width: 100%; + text-align: center; + padding-top: 1em; + font-size: 2em; + color: var(--cru-primary-color); +} \ No newline at end of file diff --git a/FrontEnd/src/pages/home/index.tsx b/FrontEnd/src/pages/home/index.tsx new file mode 100644 index 00000000..76a3d18c --- /dev/null +++ b/FrontEnd/src/pages/home/index.tsx @@ -0,0 +1,7 @@ +import "./index.css"; + +export default function HomePage() { + return ( +
    Be patient! I'm working on this...
    + ); +} diff --git a/FrontEnd/src/pages/loading/index.css b/FrontEnd/src/pages/loading/index.css new file mode 100644 index 00000000..08e43c22 --- /dev/null +++ b/FrontEnd/src/pages/loading/index.css @@ -0,0 +1,7 @@ +.loading-page { + width: 100%; + text-align: center; + padding-top: 1em; + font-size: 2em; + color: var(--cru-primary-color); +} \ No newline at end of file diff --git a/FrontEnd/src/pages/loading/index.tsx b/FrontEnd/src/pages/loading/index.tsx new file mode 100644 index 00000000..e4c8edab --- /dev/null +++ b/FrontEnd/src/pages/loading/index.tsx @@ -0,0 +1,11 @@ +import Spinner from "@/views/common/Spinner"; + +import "./index.css"; + +export default function LoadingPage() { + return ( +
    + +
    + ); +} diff --git a/FrontEnd/src/pages/timeline/CollapseButton.tsx b/FrontEnd/src/pages/timeline/CollapseButton.tsx new file mode 100644 index 00000000..8270e160 --- /dev/null +++ b/FrontEnd/src/pages/timeline/CollapseButton.tsx @@ -0,0 +1,21 @@ +import * as React from "react"; + +import IconButton from "@/views/common/button/IconButton"; + +const CollapseButton: React.FC<{ + collapse: boolean; + onClick: () => void; + className?: string; + style?: React.CSSProperties; +}> = ({ collapse, onClick, className, style }) => { + return ( + + ); +}; + +export default CollapseButton; diff --git a/FrontEnd/src/pages/timeline/ConnectionStatusBadge.css b/FrontEnd/src/pages/timeline/ConnectionStatusBadge.css new file mode 100644 index 00000000..7fe83b9b --- /dev/null +++ b/FrontEnd/src/pages/timeline/ConnectionStatusBadge.css @@ -0,0 +1,36 @@ +.connection-status-badge { + font-size: 0.8em; + border-radius: 5px; + padding: 0.1em 1em; + background-color: #eaf2ff; +} +.connection-status-badge::before { + width: 10px; + height: 10px; + border-radius: 50%; + display: inline-block; + content: ""; + margin-right: 0.6em; +} +.connection-status-badge.success { + color: #006100; +} +.connection-status-badge.success::before { + background-color: #006100; +} + +.connection-status-badge.warning { + color: #e4a700; +} + +.connection-status-badge.warning::before { + background-color: #e4a700; +} + +.connection-status-badge.danger { + color: #fd1616; +} + +.connection-status-badge.danger::before { + background-color: #fd1616; +} diff --git a/FrontEnd/src/pages/timeline/ConnectionStatusBadge.tsx b/FrontEnd/src/pages/timeline/ConnectionStatusBadge.tsx new file mode 100644 index 00000000..2b820454 --- /dev/null +++ b/FrontEnd/src/pages/timeline/ConnectionStatusBadge.tsx @@ -0,0 +1,41 @@ +import * as React from "react"; +import classnames from "classnames"; +import { HubConnectionState } from "@microsoft/signalr"; +import { useTranslation } from "react-i18next"; + +import "./ConnectionStatusBadge.css"; + +export interface ConnectionStatusBadgeProps { + status: HubConnectionState; + className?: string; + style?: React.CSSProperties; +} + +const classNameMap: Record = { + Connected: "success", + Connecting: "warning", + Disconnected: "danger", + Disconnecting: "warning", + Reconnecting: "warning", +}; + +const ConnectionStatusBadge: React.FC = (props) => { + const { status, className, style } = props; + + const { t } = useTranslation(); + + return ( +
    + {t(`connectionState.${status}`)} +
    + ); +}; + +export default ConnectionStatusBadge; diff --git a/FrontEnd/src/pages/timeline/MarkdownPostEdit.css b/FrontEnd/src/pages/timeline/MarkdownPostEdit.css new file mode 100644 index 00000000..e36be992 --- /dev/null +++ b/FrontEnd/src/pages/timeline/MarkdownPostEdit.css @@ -0,0 +1,21 @@ +.timeline-markdown-post-edit-page { + overflow: auto; + max-height: 300px; +} + +.timeline-markdown-post-edit-image-container { + position: relative; + text-align: center; + margin-bottom: 1em; +} + +.timeline-markdown-post-edit-image { + max-width: 100%; + max-height: 200px; +} + +.timeline-markdown-post-edit-image-delete-button { + position: absolute; + right: 10px; + top: 2px; +} diff --git a/FrontEnd/src/pages/timeline/MarkdownPostEdit.tsx b/FrontEnd/src/pages/timeline/MarkdownPostEdit.tsx new file mode 100644 index 00000000..9c497108 --- /dev/null +++ b/FrontEnd/src/pages/timeline/MarkdownPostEdit.tsx @@ -0,0 +1,215 @@ +import * as React from "react"; +import classnames from "classnames"; +import { useTranslation } from "react-i18next"; + +import { getHttpTimelineClient, HttpTimelinePostInfo } from "@/http/timeline"; + +import TimelinePostBuilder from "@/services/TimelinePostBuilder"; + +import FlatButton from "@/views/common/button/FlatButton"; +import TabPages from "@/views/common/tab/TabPages"; +import ConfirmDialog from "@/views/common/dialog/ConfirmDialog"; +import Spinner from "@/views/common/Spinner"; +import IconButton from "@/views/common/button/IconButton"; + +import "./MarkdownPostEdit.css"; + +export interface MarkdownPostEditProps { + owner: string; + timeline: string; + onPosted: (post: HttpTimelinePostInfo) => void; + onPostError: () => void; + onClose: () => void; + className?: string; + style?: React.CSSProperties; +} + +const MarkdownPostEdit: React.FC = ({ + owner: ownerUsername, + timeline: timelineName, + onPosted, + onClose, + onPostError, + className, + style, +}) => { + const { t } = useTranslation(); + + const [canLeave, setCanLeave] = React.useState(true); + + const [process, setProcess] = React.useState(false); + + const [showLeaveConfirmDialog, setShowLeaveConfirmDialog] = + React.useState(false); + + const [text, _setText] = React.useState(""); + const [images, _setImages] = React.useState<{ file: File; url: string }[]>( + [] + ); + const [previewHtml, _setPreviewHtml] = React.useState(""); + + const _builder = React.useRef(null); + + const getBuilder = (): TimelinePostBuilder => { + if (_builder.current == null) { + const builder = new TimelinePostBuilder(() => { + setCanLeave(builder.isEmpty); + _setText(builder.text); + _setImages(builder.images); + _setPreviewHtml(builder.renderHtml()); + }); + _builder.current = builder; + } + return _builder.current; + }; + + const canSend = text.length > 0; + + React.useEffect(() => { + return () => { + getBuilder().dispose(); + }; + }, []); + + React.useEffect(() => { + window.onbeforeunload = (): unknown => { + if (!canLeave) { + return t("timeline.confirmLeave"); + } + }; + + return () => { + window.onbeforeunload = null; + }; + }, [canLeave, t]); + + const send = async (): Promise => { + setProcess(true); + try { + const dataList = await getBuilder().build(); + const post = await getHttpTimelineClient().postPost( + ownerUsername, + timelineName, + { + dataList, + } + ); + onPosted(post); + onClose(); + } catch (e) { + setProcess(false); + onPostError(); + } + }; + + return ( + <> + + ) : ( +
    + { + if (canLeave) { + onClose(); + } else { + setShowLeaveConfirmDialog(true); + } + }} + /> + {canSend && ( + void send()} /> + )} +
    + ) + } + pages={[ + { + name: "text", + text: "edit", + page: ( +