diff options
Diffstat (limited to 'FrontEnd/src/migrating')
-rw-r--r-- | FrontEnd/src/migrating/admin/Admin.tsx | 27 | ||||
-rw-r--r-- | FrontEnd/src/migrating/admin/AdminNav.tsx | 29 | ||||
-rw-r--r-- | FrontEnd/src/migrating/admin/MoreAdmin.tsx | 7 | ||||
-rw-r--r-- | FrontEnd/src/migrating/admin/UserAdmin.tsx | 301 | ||||
-rw-r--r-- | FrontEnd/src/migrating/admin/index.css | 33 | ||||
-rw-r--r-- | FrontEnd/src/migrating/admin/index.tsx | 7 | ||||
-rw-r--r-- | FrontEnd/src/migrating/center/CenterBoards.tsx | 131 | ||||
-rw-r--r-- | FrontEnd/src/migrating/center/TimelineBoard.tsx | 390 | ||||
-rw-r--r-- | FrontEnd/src/migrating/center/TimelineCreateDialog.tsx | 57 | ||||
-rw-r--r-- | FrontEnd/src/migrating/center/index.css | 43 | ||||
-rw-r--r-- | FrontEnd/src/migrating/center/index.tsx | 60 |
11 files changed, 1085 insertions, 0 deletions
diff --git a/FrontEnd/src/migrating/admin/Admin.tsx b/FrontEnd/src/migrating/admin/Admin.tsx new file mode 100644 index 00000000..986c36b4 --- /dev/null +++ b/FrontEnd/src/migrating/admin/Admin.tsx @@ -0,0 +1,27 @@ +import { Route, Routes } from "react-router-dom"; +import { useTranslation } from "react-i18next"; + +import AdminNav from "./AdminNav"; +import UserAdmin from "./UserAdmin"; +import MoreAdmin from "./MoreAdmin"; + +import "./index.css"; + +const Admin: React.FC = () => { + useTranslation("admin"); + + return ( + <> + <div className="container"> + <AdminNav className="mt-2" /> + <Routes> + <Route index element={<UserAdmin />} /> + <Route path="user" element={<UserAdmin />} /> + <Route path="more" element={<MoreAdmin />} /> + </Routes> + </div> + </> + ); +}; + +export default Admin; diff --git a/FrontEnd/src/migrating/admin/AdminNav.tsx b/FrontEnd/src/migrating/admin/AdminNav.tsx new file mode 100644 index 00000000..b7385e5c --- /dev/null +++ b/FrontEnd/src/migrating/admin/AdminNav.tsx @@ -0,0 +1,29 @@ +import { useLocation } from "react-router-dom"; + +import Tabs from "../common/tab/Tabs"; + +export function AdminNav({ className }: { className?: string }) { + const location = useLocation(); + const name = location.pathname.split("/")[2] ?? "user"; + + return ( + <Tabs + className={className} + activeTabName={name} + tabs={[ + { + name: "user", + text: "admin:nav.users", + link: "/admin/user", + }, + { + name: "more", + text: "admin:nav.more", + link: "/admin/more", + }, + ]} + /> + ); +} + +export default AdminNav; diff --git a/FrontEnd/src/migrating/admin/MoreAdmin.tsx b/FrontEnd/src/migrating/admin/MoreAdmin.tsx new file mode 100644 index 00000000..d49d211f --- /dev/null +++ b/FrontEnd/src/migrating/admin/MoreAdmin.tsx @@ -0,0 +1,7 @@ +import * as React from "react"; + +const MoreAdmin: React.FC = () => { + return <>More...</>; +}; + +export default MoreAdmin; diff --git a/FrontEnd/src/migrating/admin/UserAdmin.tsx b/FrontEnd/src/migrating/admin/UserAdmin.tsx new file mode 100644 index 00000000..08560c87 --- /dev/null +++ b/FrontEnd/src/migrating/admin/UserAdmin.tsx @@ -0,0 +1,301 @@ +// eslint-disable +// @ts-nocheck + +import { useState, useEffect } from "react"; +import * as React from "react"; +import { Trans, useTranslation } from "react-i18next"; +import classnames from "classnames"; + +import { getHttpUserClient, HttpUser, kUserPermissionList } from "@/http/user"; + +import OperationDialog from "../common/dialog/OperationDialog"; +import Button from "../common/button/Button"; +import Spinner from "../common/Spinner"; +import FlatButton from "../common/button/FlatButton"; +import IconButton from "../common/button/IconButton"; + +const CreateUserDialog: React.FC<{ + open: boolean; + close: () => void; + onSuccess: (user: HttpUser) => void; +}> = ({ open, close, onSuccess }) => { + return ( + <OperationDialog + title="admin:user.dialog.create.title" + inputPrompt="admin:user.dialog.create.prompt" + inputs={[ + { key: "username", type: "text", label: "admin:user.username" }, + { key: "password", type: "text", label: "admin:user.password" }, + ]} + onProcess={({ username, password }) => + getHttpUserClient().post({ + username: username as string, + password: password as string, + }) + } + onClose={close} + open={open} + onSuccessAndClose={onSuccess} + /> + ); +}; + +const UsernameLabel: React.FC<{ children: React.ReactNode }> = (props) => { + return <span style={{ color: "blue" }}>{props.children}</span>; +}; + +const UserDeleteDialog: React.FC<{ + open: boolean; + close: () => void; + user: HttpUser; + onSuccess: () => void; +}> = ({ open, close, user, onSuccess }) => { + return ( + <OperationDialog + open={open} + onClose={close} + title="admin:user.dialog.delete.title" + themeColor="danger" + inputPrompt={() => ( + <Trans i18nKey="user.dialog.delete.prompt" ns="admin"> + 0<UsernameLabel>{user.username}</UsernameLabel>2 + </Trans> + )} + onProcess={() => getHttpUserClient().delete(user.username)} + onSuccessAndClose={onSuccess} + /> + ); +}; + +const UserModifyDialog: React.FC<{ + open: boolean; + close: () => void; + user: HttpUser; + onSuccess: () => void; +}> = ({ open, close, user, onSuccess }) => { + return ( + <OperationDialog + open={open} + onClose={close} + title="admin:user.dialog.modify.title" + inputPromptNode={ + <Trans i18nKey="admin:user.dialog.modify.prompt"> + 0<UsernameLabel>{user.username}</UsernameLabel>2 + </Trans> + } + inputs={ + [ + { + type: "text", + label: "admin:user.username", + initValue: user.username, + }, + { type: "text", label: "admin:user.password" }, + { + type: "text", + label: "admin:user.nickname", + initValue: user.nickname, + }, + ] as const + } + onProcess={([username, password, nickname]) => + getHttpUserClient().patch(user.username, { + username: username !== user.username ? username : undefined, + password: password !== "" ? password : undefined, + nickname: nickname !== user.nickname ? nickname : undefined, + }) + } + onSuccessAndClose={onSuccess} + /> + ); +}; + +const UserPermissionModifyDialog: React.FC<{ + open: boolean; + close: () => void; + user: HttpUser; + onSuccess: () => void; +}> = ({ open, close, user, onSuccess }) => { + const oldPermissionBoolList: boolean[] = kUserPermissionList.map( + (permission) => user.permissions.includes(permission), + ); + + return ( + <OperationDialog + open={open} + onClose={close} + title="admin:user.dialog.modifyPermissions.title" + themeColor="danger" + inputPrompt={() => ( + <Trans i18nKey="admin:user.dialog.modifyPermissions.prompt"> + 0<UsernameLabel>{user.username}</UsernameLabel>2 + </Trans> + )} + inputScheme={kUserPermissionList.map<OperationDialogBoolInput>( + (permission, index) => ({ + type: "bool", + label: { type: "custom", value: permission }, + initValue: oldPermissionBoolList[index], + }), + )} + onProcess={async (newPermissionBoolList): Promise<boolean[]> => { + for (let index = 0; index < kUserPermissionList.length; index++) { + const oldValue = oldPermissionBoolList[index]; + const newValue = newPermissionBoolList[index]; + const permission = kUserPermissionList[index]; + if (oldValue === newValue) continue; + if (newValue) { + await getHttpUserClient().putUserPermission( + user.username, + permission, + ); + } else { + await getHttpUserClient().deleteUserPermission( + user.username, + permission, + ); + } + } + return newPermissionBoolList; + }} + onSuccessAndClose={onSuccess} + /> + ); +}; + +interface UserItemProps { + user: HttpUser; + onChange: () => void; +} + +const UserItem: React.FC<UserItemProps> = ({ user, onChange }) => { + const { t } = useTranslation(); + + const [dialog, setDialog] = useState< + "delete" | "modify" | "permission" | null + >(null); + + const [editMaskVisible, setEditMaskVisible] = React.useState<boolean>(false); + + return ( + <> + <div className="admin-user-item"> + <IconButton + icon="pencil-square" + className="cru-float-right" + onClick={() => setEditMaskVisible(true)} + /> + <h5 className="cru-color-primary">{user.username}</h5> + <small className="d-block cru-color-secondary"> + {t("admin:user.nickname")} + {user.nickname} + </small> + <small className="d-block cru-color-secondary"> + {t("admin:user.uniqueId")} + {user.uniqueId} + </small> + <small className="d-block cru-color-secondary"> + {t("admin:user.permissions")} + {user.permissions.map((permission) => { + return ( + <span key={permission} className="cru-color-danger"> + {permission} + </span> + ); + })} + </small> + <div + className={classnames("edit-mask", !editMaskVisible && "d-none")} + onClick={() => setEditMaskVisible(false)} + > + <FlatButton + text="admin:user.modify" + onClick={() => setDialog("modify")} + /> + <FlatButton + text="admin:user.modifyPermissions" + onClick={() => setDialog("permission")} + /> + <FlatButton + text="admin:user.delete" + color="danger" + onClick={() => setDialog("delete")} + /> + </div> + </div> + <UserDeleteDialog + open={dialog === "delete"} + close={() => setDialog(null)} + user={user} + onSuccess={onChange} + /> + <UserModifyDialog + open={dialog === "modify"} + close={() => setDialog(null)} + user={user} + onSuccess={onChange} + /> + <UserPermissionModifyDialog + open={dialog === "permission"} + close={() => setDialog(null)} + user={user} + onSuccess={onChange} + /> + </> + ); +}; + +const UserAdmin: React.FC = () => { + const [users, setUsers] = useState<HttpUser[] | null>(null); + const [dialog, setDialog] = useState<"create" | null>(null); + const [usersVersion, setUsersVersion] = useState<number>(0); + const updateUsers = (): void => { + setUsersVersion(usersVersion + 1); + }; + + useEffect(() => { + let subscribe = true; + void getHttpUserClient() + .list() + .then((us) => { + if (subscribe) { + setUsers(us.items); + } + }); + return () => { + subscribe = false; + }; + }, [usersVersion]); + + if (users) { + const userComponents = users.map((user) => { + return ( + <UserItem key={user.username} user={user} onChange={updateUsers} /> + ); + }); + + return ( + <> + <div className="row justify-content-end my-2"> + <div className="col col-auto"> + <Button + text="admin:create" + color="success" + onClick={() => setDialog("create")} + /> + </div> + </div> + {userComponents} + <CreateUserDialog + open={dialog === "create"} + close={() => setDialog(null)} + onSuccess={updateUsers} + /> + </> + ); + } else { + return <Spinner />; + } +}; + +export default UserAdmin; diff --git a/FrontEnd/src/migrating/admin/index.css b/FrontEnd/src/migrating/admin/index.css new file mode 100644 index 00000000..17e24586 --- /dev/null +++ b/FrontEnd/src/migrating/admin/index.css @@ -0,0 +1,33 @@ +.admin-user-item {
+ position: relative;
+ border: var(--cru-primary-color) solid;
+ border-width: 1px 1px 0;
+ padding: 1em;
+}
+
+.admin-user-item:last-of-type {
+ border-bottom-width: 1px;
+}
+
+.admin-user-item .edit-mask {
+ position: absolute;
+ top: 0;
+ left: 0;
+ bottom: 0;
+ right: 0;
+ background: rgba(255, 255, 255, 0.9);
+ position: absolute;
+ display: flex;
+ justify-content: space-around;
+ align-items: center;
+}
+
+@media (max-width: 576px) {
+ .admin-user-item .edit-mask {
+ flex-direction: column;
+ }
+}
+
+.admin-user-item .edit-mask button {
+ margin: 0.5em 2em;
+}
diff --git a/FrontEnd/src/migrating/admin/index.tsx b/FrontEnd/src/migrating/admin/index.tsx new file mode 100644 index 00000000..0467711d --- /dev/null +++ b/FrontEnd/src/migrating/admin/index.tsx @@ -0,0 +1,7 @@ +import { lazy } from "react"; + +const Admin = lazy( + () => import(/* webpackChunkName: "admin" */ "./Admin") +); + +export default Admin; diff --git a/FrontEnd/src/migrating/center/CenterBoards.tsx b/FrontEnd/src/migrating/center/CenterBoards.tsx new file mode 100644 index 00000000..a8be2c29 --- /dev/null +++ b/FrontEnd/src/migrating/center/CenterBoards.tsx @@ -0,0 +1,131 @@ +import * as React from "react"; +import { useTranslation } from "react-i18next"; + +import { highlightTimelineUsername } from "@/common"; + +import { pushAlert } from "@/services/alert"; +import { useUserLoggedIn } from "@/services/user"; + +import { getHttpTimelineClient } from "@/http/timeline"; +import { getHttpBookmarkClient } from "@/http/bookmark"; + +import TimelineBoard from "./TimelineBoard"; + +const CenterBoards: React.FC = () => { + const { t } = useTranslation(); + + const user = useUserLoggedIn(); + + return ( + <> + <div className="row justify-content-center"> + <div className="col col-12 col-md-6"> + <div className="row"> + <div className="col col-12 my-2"> + <TimelineBoard + title={t("home.bookmarkTimeline")} + load={() => + getHttpBookmarkClient() + .list(user.username) + .then((p) => p.items) + } + editHandler={{ + onDelete: (owner, timeline) => { + return getHttpBookmarkClient() + .delete(user.username, owner, timeline) + .catch((e) => { + pushAlert({ + message: "home.message.deleteBookmarkFail", + type: "danger", + }); + throw e; + }); + }, + onMove: (owner, timeline, index, offset) => { + return getHttpBookmarkClient() + .move( + user.username, + owner, + timeline, + index + offset + 1 // +1 because backend contract: index starts at 1 + ) + .catch((e) => { + pushAlert({ + message: "home.message.moveBookmarkFail", + type: "danger", + }); + throw e; + }) + .then(); + }, + }} + /> + </div> + <div className="col col-12 my-2"> + <TimelineBoard + title={t("home.highlightTimeline")} + load={() => + getHttpBookmarkClient() + .list(highlightTimelineUsername) + .then((p) => p.items) + } + editHandler={ + user.username === highlightTimelineUsername + ? { + onDelete: (owner, timeline) => { + return getHttpBookmarkClient() + .delete(highlightTimelineUsername, owner, timeline) + .catch((e) => { + pushAlert({ + message: "home.message.deleteHighlightFail", + type: "danger", + }); + throw e; + }); + }, + onMove: (owner, timeline, index, offset) => { + return getHttpBookmarkClient() + .move( + highlightTimelineUsername, + owner, + timeline, + index + offset + 1 // +1 because backend contract: index starts at 1 + ) + .catch((e) => { + pushAlert({ + message: "home.message.moveBookmarkFail", + type: "danger", + }); + throw e; + }) + .then(); + }, + } + : undefined + } + /> + </div> + </div> + </div> + <div className="col-12 col-md-6 my-2"> + <TimelineBoard + title={t("home.relatedTimeline")} + load={() => + getHttpTimelineClient() + .listTimeline({ relate: user.username }) + .then((l) => + l.map((t, index) => ({ + timelineOwner: t.owner.username, + timelineName: t.nameV2, + position: index + 1, + })) + ) + } + /> + </div> + </div> + </> + ); +}; + +export default CenterBoards; diff --git a/FrontEnd/src/migrating/center/TimelineBoard.tsx b/FrontEnd/src/migrating/center/TimelineBoard.tsx new file mode 100644 index 00000000..b3ccdf8c --- /dev/null +++ b/FrontEnd/src/migrating/center/TimelineBoard.tsx @@ -0,0 +1,390 @@ +import * as React from "react"; +import classnames from "classnames"; +import { Link } from "react-router-dom"; + +import { TimelineBookmark } from "@/http/bookmark"; + +import TimelineLogo from "../common/TimelineLogo"; +import LoadFailReload from "../common/LoadFailReload"; +import FlatButton from "../common/button/FlatButton"; +import Card from "../common/Card"; +import Spinner from "../common/Spinner"; +import IconButton from "../common/button/IconButton"; + +interface TimelineBoardItemProps { + timeline: TimelineBookmark; + // In height. + offset?: number; + // In px. + arbitraryOffset?: number; + // If not null, will disable navigation on click. + actions?: { + onDelete: () => void; + onMove: { + start: (e: React.PointerEvent) => void; + moving: (e: React.PointerEvent) => void; + end: (e: React.PointerEvent) => void; + }; + }; +} + +const TimelineBoardItem: React.FC<TimelineBoardItemProps> = ({ + timeline, + arbitraryOffset, + offset, + actions, +}) => { + const content = ( + <> + <TimelineLogo className="icon" /> + <span className="title"> + {timeline.timelineOwner}/{timeline.timelineName} + </span> + <span className="flex-grow-1"></span> + {actions != null ? ( + <div className="right"> + <IconButton + icon="trash" + color="danger" + className="px-2" + onClick={actions.onDelete} + /> + <IconButton + icon="grip-vertical" + className="px-2 touch-action-none" + onPointerDown={(e) => { + e.currentTarget.setPointerCapture(e.pointerId); + actions.onMove.start(e); + }} + onPointerUp={(e) => { + actions.onMove.end(e); + try { + e.currentTarget.releasePointerCapture(e.pointerId); + } catch (_) { + void null; + } + }} + onPointerMove={actions.onMove.moving} + /> + </div> + ) : null} + </> + ); + + const offsetStyle: React.CSSProperties = { + transform: + arbitraryOffset != null + ? `translate(0,${arbitraryOffset}px)` + : offset != null + ? `translate(0,${offset * 100}%)` + : undefined, + transition: offset != null ? "transform 0.5s" : undefined, + zIndex: arbitraryOffset != null ? 1 : undefined, + }; + + return actions == null ? ( + <Link + to={`${timeline.timelineOwner}/${timeline.timelineName}`} + className="timeline-board-item" + > + {content} + </Link> + ) : ( + <div style={offsetStyle} className="timeline-board-item"> + {content} + </div> + ); +}; + +interface TimelineBoardItemContainerProps { + timelines: TimelineBookmark[]; + editHandler?: { + // offset may exceed index range plusing index. + onMove: ( + owner: string, + timeline: string, + index: number, + offset: number + ) => void; + onDelete: (owner: string, timeline: string) => void; + }; +} + +const TimelineBoardItemContainer: React.FC<TimelineBoardItemContainerProps> = ({ + timelines, + editHandler, +}) => { + const [moveState, setMoveState] = React.useState<null | { + index: number; + offset: number; + startPointY: number; + }>(null); + + return ( + <> + {timelines.map((timeline, index) => { + const height = 48; + + let offset: number | undefined = undefined; + let arbitraryOffset: number | undefined = undefined; + if (moveState != null) { + if (index === moveState.index) { + arbitraryOffset = moveState.offset; + } else { + if (moveState.offset >= 0) { + const offsetCount = Math.round(moveState.offset / height); + if ( + index > moveState.index && + index <= moveState.index + offsetCount + ) { + offset = -1; + } else { + offset = 0; + } + } else { + const offsetCount = Math.round(-moveState.offset / height); + if ( + index < moveState.index && + index >= moveState.index - offsetCount + ) { + offset = 1; + } else { + offset = 0; + } + } + } + } + + return ( + <TimelineBoardItem + key={timeline.timelineOwner + "/" + timeline.timelineName} + timeline={timeline} + offset={offset} + arbitraryOffset={arbitraryOffset} + actions={ + editHandler != null + ? { + onDelete: () => { + editHandler.onDelete( + timeline.timelineOwner, + timeline.timelineName + ); + }, + onMove: { + start: (e) => { + if (moveState != null) return; + setMoveState({ + index, + offset: 0, + startPointY: e.clientY, + }); + }, + moving: (e) => { + if (moveState == null) return; + setMoveState({ + index, + offset: e.clientY - moveState.startPointY, + startPointY: moveState.startPointY, + }); + }, + end: () => { + if (moveState != null) { + const offsetCount = Math.round( + moveState.offset / height + ); + editHandler.onMove( + timeline.timelineOwner, + timeline.timelineName, + moveState.index, + offsetCount + ); + } + setMoveState(null); + }, + }, + } + : undefined + } + /> + ); + })} + </> + ); +}; + +interface TimelineBoardUIProps { + title?: string | null; + state: "offline" | "loading" | "loaded"; + timelines: TimelineBookmark[]; + onReload: () => void; + className?: string; + editHandler?: { + onMove: ( + owner: string, + timeline: string, + index: number, + offset: number + ) => void; + onDelete: (owner: string, timeline: string) => void; + }; +} + +const TimelineBoardUI: React.FC<TimelineBoardUIProps> = (props) => { + const { title, state, timelines, className, editHandler } = props; + + const editable = editHandler != null; + + const [editing, setEditing] = React.useState<boolean>(false); + + return ( + <Card className={classnames("timeline-board", className)}> + <div className="timeline-board-header"> + {title != null && <h3>{title}</h3>} + {editable && + (editing ? ( + <FlatButton + text="done" + onClick={() => { + setEditing(false); + }} + /> + ) : ( + <FlatButton + text="edit" + onClick={() => { + setEditing(true); + }} + /> + ))} + </div> + {(() => { + if (state === "loading") { + return ( + <div className="d-flex flex-grow-1 justify-content-center align-items-center"> + <Spinner /> + </div> + ); + } else if (state === "offline") { + return ( + <div className="d-flex flex-grow-1 justify-content-center align-items-center"> + <LoadFailReload onReload={props.onReload} /> + </div> + ); + } else { + return ( + <TimelineBoardItemContainer + timelines={timelines} + editHandler={ + editHandler && editing + ? { + onDelete: editHandler.onDelete, + onMove: (owner, timeline, index, offset) => { + if (index + offset >= timelines.length) { + offset = timelines.length - index - 1; + } else if (index + offset < 0) { + offset = -index; + } + editHandler.onMove(owner, timeline, index, offset); + }, + } + : undefined + } + /> + ); + } + })()} + </Card> + ); +}; + +export interface TimelineBoardProps { + title?: string | null; + className?: string; + load: () => Promise<TimelineBookmark[]>; + editHandler?: { + onMove: ( + owner: string, + timeline: string, + index: number, + offset: number + ) => Promise<void>; + onDelete: (owner: string, timeline: string) => Promise<void>; + }; +} + +const TimelineBoard: React.FC<TimelineBoardProps> = ({ + className, + title, + load, + editHandler, +}) => { + const [state, setState] = React.useState<"offline" | "loading" | "loaded">( + "loading" + ); + const [timelines, setTimelines] = React.useState<TimelineBookmark[]>([]); + + React.useEffect(() => { + let subscribe = true; + if (state === "loading") { + void load().then( + (timelines) => { + if (subscribe) { + setState("loaded"); + setTimelines(timelines); + } + }, + () => { + setState("offline"); + } + ); + } + return () => { + subscribe = false; + }; + }, [load, state]); + + return ( + <TimelineBoardUI + title={title} + className={className} + state={state} + timelines={timelines} + onReload={() => { + setState("loaded"); + }} + editHandler={ + typeof timelines === "object" && editHandler != null + ? { + onMove: (owner, timeline, index, offset) => { + const newTimelines = timelines.slice(); + const [t] = newTimelines.splice(index, 1); + newTimelines.splice(index + offset, 0, t); + setTimelines(newTimelines); + editHandler + .onMove(owner, timeline, index, offset) + .then(null, () => { + setTimelines(timelines); + }); + }, + onDelete: (owner, timeline) => { + const newTimelines = timelines.slice(); + newTimelines.splice( + timelines.findIndex( + (t) => + t.timelineOwner === owner && t.timelineName === timeline + ), + 1 + ); + setTimelines(newTimelines); + editHandler.onDelete(owner, timeline).then(null, () => { + setTimelines(timelines); + }); + }, + } + : undefined + } + /> + ); +}; + +export default TimelineBoard; diff --git a/FrontEnd/src/migrating/center/TimelineCreateDialog.tsx b/FrontEnd/src/migrating/center/TimelineCreateDialog.tsx new file mode 100644 index 00000000..63742936 --- /dev/null +++ b/FrontEnd/src/migrating/center/TimelineCreateDialog.tsx @@ -0,0 +1,57 @@ +import * as React from "react"; +import { useNavigate } from "react-router-dom"; + +import { validateTimelineName } from "@/services/timeline"; +import { getHttpTimelineClient, HttpTimelineInfo } from "@/http/timeline"; + +import OperationDialog from "../common/dialog/OperationDialog"; +import { useUserLoggedIn } from "@/services/user"; + +interface TimelineCreateDialogProps { + open: boolean; + close: () => void; +} + +const TimelineCreateDialog: React.FC<TimelineCreateDialogProps> = (props) => { + const navigate = useNavigate(); + + const user = useUserLoggedIn(); + + return ( + <OperationDialog + open={props.open} + onClose={props.close} + themeColor="success" + title="home.createDialog.title" + inputScheme={ + [ + { + type: "text", + label: "home.createDialog.name", + helperText: "home.createDialog.nameFormat", + }, + ] as const + } + inputValidator={([name]) => { + if (name.length === 0) { + return { 0: "home.createDialog.noEmpty" }; + } else if (name.length > 26) { + return { 0: "home.createDialog.tooLong" }; + } else if (!validateTimelineName(name)) { + return { 0: "home.createDialog.badFormat" }; + } else { + return null; + } + }} + onProcess={([name]): Promise<HttpTimelineInfo> => + getHttpTimelineClient().postTimeline({ name }) + } + onSuccessAndClose={(timeline: HttpTimelineInfo) => { + navigate(`${user.username}/${timeline.nameV2}`); + }} + failurePrompt={(e) => `${e as string}`} + /> + ); +}; + +export default TimelineCreateDialog; diff --git a/FrontEnd/src/migrating/center/index.css b/FrontEnd/src/migrating/center/index.css new file mode 100644 index 00000000..a779ff90 --- /dev/null +++ b/FrontEnd/src/migrating/center/index.css @@ -0,0 +1,43 @@ +.timeline-board {
+ min-height: 200px;
+ height: 100%;
+ position: relative;
+ padding: 1em 0;
+ display: flex;
+ flex-direction: column;
+}
+
+.timeline-board-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 0 1em;
+}
+
+.timeline-board-item {
+ font-size: 1.1em;
+ height: 48px;
+ transition: background 0.3s;
+ display: flex;
+ align-items: center;
+ padding: 0 1em;
+}
+
+.timeline-board-item .icon {
+ height: 1.3em;
+ color: black;
+}
+
+.timeline-board-item:hover {
+ background: #dee2e6;
+}
+.timeline-board-item .right {
+ display: flex;
+ align-items: center;
+ flex-shrink: 0;
+}
+.timeline-board-item .title {
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
diff --git a/FrontEnd/src/migrating/center/index.tsx b/FrontEnd/src/migrating/center/index.tsx new file mode 100644 index 00000000..77af2c20 --- /dev/null +++ b/FrontEnd/src/migrating/center/index.tsx @@ -0,0 +1,60 @@ +import * as React from "react"; +import { useNavigate } from "react-router-dom"; + +import { useUserLoggedIn } from "@/services/user"; + +import SearchInput from "../common/SearchInput"; +import Button from "../common/button/Button"; +import CenterBoards from "./CenterBoards"; +import TimelineCreateDialog from "./TimelineCreateDialog"; + +import "./index.css"; + +const HomePage: React.FC = () => { + const navigate = useNavigate(); + + const user = useUserLoggedIn(); + + const [navText, setNavText] = React.useState<string>(""); + + const [dialog, setDialog] = React.useState<"create" | null>(null); + + return ( + <> + <div className="container"> + <div className="row my-3 justify-content-center"> + <div className="col col-12 col-md-8"> + <SearchInput + className="justify-content-center" + value={navText} + onChange={setNavText} + onButtonClick={() => { + navigate(`search?q=${navText}`); + }} + additionalButton={ + user != null && ( + <Button + text="home.createButton" + color="success" + onClick={() => { + setDialog("create"); + }} + /> + ) + } + /> + </div> + </div> + <CenterBoards /> + </div> + <TimelineCreateDialog + open={dialog === "create"} + close={() => { + setDialog(null); + }} + /> + </> + ); +}; + +export default HomePage; |