From 05ccb4d8f1bbe3fb64e117136b4a89bcfb0b0b33 Mon Sep 17 00:00:00 2001 From: crupest Date: Tue, 27 Oct 2020 19:21:35 +0800 Subject: Split front and back end. --- FrontEnd/src/app/views/home/BoardWithUser.tsx | 101 +++++++++++++++++++++ FrontEnd/src/app/views/home/BoardWithoutUser.tsx | 60 ++++++++++++ FrontEnd/src/app/views/home/OfflineBoard.tsx | 61 +++++++++++++ FrontEnd/src/app/views/home/TimelineBoard.tsx | 73 +++++++++++++++ .../src/app/views/home/TimelineCreateDialog.tsx | 53 +++++++++++ FrontEnd/src/app/views/home/home.sass | 13 +++ FrontEnd/src/app/views/home/index.tsx | 99 ++++++++++++++++++++ 7 files changed, 460 insertions(+) create mode 100644 FrontEnd/src/app/views/home/BoardWithUser.tsx create mode 100644 FrontEnd/src/app/views/home/BoardWithoutUser.tsx create mode 100644 FrontEnd/src/app/views/home/OfflineBoard.tsx create mode 100644 FrontEnd/src/app/views/home/TimelineBoard.tsx create mode 100644 FrontEnd/src/app/views/home/TimelineCreateDialog.tsx create mode 100644 FrontEnd/src/app/views/home/home.sass create mode 100644 FrontEnd/src/app/views/home/index.tsx (limited to 'FrontEnd/src/app/views/home') diff --git a/FrontEnd/src/app/views/home/BoardWithUser.tsx b/FrontEnd/src/app/views/home/BoardWithUser.tsx new file mode 100644 index 00000000..dcd39cbe --- /dev/null +++ b/FrontEnd/src/app/views/home/BoardWithUser.tsx @@ -0,0 +1,101 @@ +import React from "react"; +import { Row, Col } from "react-bootstrap"; +import { useTranslation } from "react-i18next"; + +import { UserWithToken } from "@/services/user"; +import { TimelineInfo } from "@/services/timeline"; +import { getHttpTimelineClient } from "@/http/timeline"; + +import TimelineBoard from "./TimelineBoard"; +import OfflineBoard from "./OfflineBoard"; + +const BoardWithUser: React.FC<{ user: UserWithToken }> = ({ user }) => { + const { t } = useTranslation(); + + const [ownTimelines, setOwnTimelines] = React.useState< + TimelineInfo[] | "offline" | "loading" + >("loading"); + const [joinTimelines, setJoinTimelines] = React.useState< + TimelineInfo[] | "offline" | "loading" + >("loading"); + + React.useEffect(() => { + let subscribe = true; + if (ownTimelines === "loading") { + void getHttpTimelineClient() + .listTimeline({ relate: user.username, relateType: "own" }) + .then( + (timelines) => { + if (subscribe) { + setOwnTimelines(timelines); + } + }, + () => { + setOwnTimelines("offline"); + } + ); + } + return () => { + subscribe = false; + }; + }, [user, ownTimelines]); + + React.useEffect(() => { + let subscribe = true; + if (joinTimelines === "loading") { + void getHttpTimelineClient() + .listTimeline({ relate: user.username, relateType: "join" }) + .then( + (timelines) => { + if (subscribe) { + setJoinTimelines(timelines); + } + }, + () => { + setJoinTimelines("offline"); + } + ); + } + return () => { + subscribe = false; + }; + }, [user, joinTimelines]); + + return ( + + {ownTimelines === "offline" && joinTimelines === "offline" ? ( + + { + setOwnTimelines("loading"); + setJoinTimelines("loading"); + }} + /> + + ) : ( + <> + + { + setOwnTimelines("loading"); + }} + /> + + + { + setJoinTimelines("loading"); + }} + /> + + + )} + + ); +}; + +export default BoardWithUser; diff --git a/FrontEnd/src/app/views/home/BoardWithoutUser.tsx b/FrontEnd/src/app/views/home/BoardWithoutUser.tsx new file mode 100644 index 00000000..ebfddb50 --- /dev/null +++ b/FrontEnd/src/app/views/home/BoardWithoutUser.tsx @@ -0,0 +1,60 @@ +import React from "react"; +import { Row, Col } from "react-bootstrap"; + +import { TimelineInfo } from "@/services/timeline"; +import { getHttpTimelineClient } from "@/http/timeline"; + +import TimelineBoard from "./TimelineBoard"; +import OfflineBoard from "./OfflineBoard"; + +const BoardWithoutUser: React.FC = () => { + const [publicTimelines, setPublicTimelines] = React.useState< + TimelineInfo[] | "offline" | "loading" + >("loading"); + + React.useEffect(() => { + let subscribe = true; + if (publicTimelines === "loading") { + void getHttpTimelineClient() + .listTimeline({ visibility: "Public" }) + .then( + (timelines) => { + if (subscribe) { + setPublicTimelines(timelines); + } + }, + () => { + setPublicTimelines("offline"); + } + ); + } + return () => { + subscribe = false; + }; + }, [publicTimelines]); + + return ( + + {publicTimelines === "offline" ? ( + + { + setPublicTimelines("loading"); + }} + /> + + ) : ( + + { + setPublicTimelines("loading"); + }} + /> + + )} + + ); +}; + +export default BoardWithoutUser; diff --git a/FrontEnd/src/app/views/home/OfflineBoard.tsx b/FrontEnd/src/app/views/home/OfflineBoard.tsx new file mode 100644 index 00000000..fc05bd74 --- /dev/null +++ b/FrontEnd/src/app/views/home/OfflineBoard.tsx @@ -0,0 +1,61 @@ +import React from "react"; +import { Link } from "react-router-dom"; +import { Trans } from "react-i18next"; + +import { getAllCachedTimelineNames } from "@/services/timeline"; +import UserTimelineLogo from "../common/UserTimelineLogo"; +import TimelineLogo from "../common/TimelineLogo"; + +export interface OfflineBoardProps { + onReload: () => void; +} + +const OfflineBoard: React.FC = ({ onReload }) => { + const [timelines, setTimelines] = React.useState([]); + + React.useEffect(() => { + let subscribe = true; + void getAllCachedTimelineNames().then((t) => { + if (subscribe) setTimelines(t); + }); + return () => { + subscribe = false; + }; + }); + + return ( + <> + + 0 + { + onReload(); + e.preventDefault(); + }} + > + 1 + + 2 + + {timelines.map((timeline) => { + const isPersonal = timeline.startsWith("@"); + const url = isPersonal + ? `/users/${timeline.slice(1)}` + : `/timelines/${timeline}`; + return ( +
+ {isPersonal ? ( + + ) : ( + + )} + {timeline} +
+ ); + })} + + ); +}; + +export default OfflineBoard; diff --git a/FrontEnd/src/app/views/home/TimelineBoard.tsx b/FrontEnd/src/app/views/home/TimelineBoard.tsx new file mode 100644 index 00000000..a3d176e1 --- /dev/null +++ b/FrontEnd/src/app/views/home/TimelineBoard.tsx @@ -0,0 +1,73 @@ +import React from "react"; +import clsx from "clsx"; +import { Link } from "react-router-dom"; +import { Trans } from "react-i18next"; +import { Spinner } from "react-bootstrap"; + +import { TimelineInfo } from "@/services/timeline"; +import TimelineLogo from "../common/TimelineLogo"; +import UserTimelineLogo from "../common/UserTimelineLogo"; + +export interface TimelineBoardProps { + title?: string; + timelines: TimelineInfo[] | "offline" | "loading"; + onReload: () => void; + className?: string; +} + +const TimelineBoard: React.FC = (props) => { + const { title, timelines, className } = props; + + return ( +
+ {title != null &&

{title}

} + {(() => { + if (timelines === "loading") { + return ( +
+ +
+ ); + } else if (timelines === "offline") { + return ( + + ); + } else { + return timelines.map((timeline) => { + const { name } = timeline; + const isPersonal = name.startsWith("@"); + const url = isPersonal + ? `/users/${timeline.owner.username}` + : `/timelines/${name}`; + return ( +
+ {isPersonal ? ( + + ) : ( + + )} + {name} +
+ ); + }); + } + })()} +
+ ); +}; + +export default TimelineBoard; diff --git a/FrontEnd/src/app/views/home/TimelineCreateDialog.tsx b/FrontEnd/src/app/views/home/TimelineCreateDialog.tsx new file mode 100644 index 00000000..d9467719 --- /dev/null +++ b/FrontEnd/src/app/views/home/TimelineCreateDialog.tsx @@ -0,0 +1,53 @@ +import React from "react"; +import { useHistory } from "react-router"; + +import { validateTimelineName, timelineService } from "@/services/timeline"; +import OperationDialog from "../common/OperationDialog"; + +interface TimelineCreateDialogProps { + open: boolean; + close: () => void; +} + +const TimelineCreateDialog: React.FC = (props) => { + const history = useHistory(); + + let nameSaved: string; + + return ( + { + if (name.length === 0) { + return "home.createDialog.noEmpty"; + } else if (name.length > 26) { + return "home.createDialog.tooLong"; + } else if (!validateTimelineName(name)) { + return "home.createDialog.badFormat"; + } else { + return null; + } + }, + }, + ]} + onProcess={([name]) => { + nameSaved = name as string; + return timelineService.createTimeline(nameSaved).toPromise(); + }} + onSuccessAndClose={() => { + history.push(`timelines/${nameSaved}`); + }} + failurePrompt={(e) => `${e as string}`} + /> + ); +}; + +export default TimelineCreateDialog; diff --git a/FrontEnd/src/app/views/home/home.sass b/FrontEnd/src/app/views/home/home.sass new file mode 100644 index 00000000..28a2e5f3 --- /dev/null +++ b/FrontEnd/src/app/views/home/home.sass @@ -0,0 +1,13 @@ +.timeline-board-item + font-size: 1.1em + @extend .my-2 + .icon + height: 1.3em + @extend .mr-2 + +.timeline-board + @extend .cru-card + @extend .d-flex + @extend .flex-column + @extend .p-3 + min-height: 200px diff --git a/FrontEnd/src/app/views/home/index.tsx b/FrontEnd/src/app/views/home/index.tsx new file mode 100644 index 00000000..760adcea --- /dev/null +++ b/FrontEnd/src/app/views/home/index.tsx @@ -0,0 +1,99 @@ +import React from "react"; +import { useHistory } from "react-router"; +import { useTranslation } from "react-i18next"; +import { Row, Container, Button, Col } from "react-bootstrap"; + +import { useUser } from "@/services/user"; +import SearchInput from "../common/SearchInput"; + +import BoardWithoutUser from "./BoardWithoutUser"; +import BoardWithUser from "./BoardWithUser"; +import TimelineCreateDialog from "./TimelineCreateDialog"; + +const HomePage: React.FC = () => { + const history = useHistory(); + + const { t } = useTranslation(); + + const user = useUser(); + + const [navText, setNavText] = React.useState(""); + + const [dialog, setDialog] = React.useState<"create" | null>(null); + + const goto = React.useCallback((): void => { + if (navText === "") { + history.push("users/crupest"); + } else if (navText.startsWith("@")) { + history.push(`users/${navText.slice(1)}`); + } else { + history.push(`timelines/${navText}`); + } + }, [navText, history]); + + return ( + <> + + + + { + setDialog("create"); + }} + > + {t("home.createButton")} + + ) + } + /> + + + {(() => { + if (user == null) { + return ; + } else { + return ; + } + })()} + + + {dialog === "create" && ( + { + setDialog(null); + }} + /> + )} + + ); +}; + +export default HomePage; -- cgit v1.2.3