diff options
author | crupest <crupest@outlook.com> | 2020-10-27 19:21:35 +0800 |
---|---|---|
committer | crupest <crupest@outlook.com> | 2020-10-27 19:21:35 +0800 |
commit | ac769e656b122ff569c3f1534701b71e00fed586 (patch) | |
tree | 72966645ff1e25139d3995262e1c4349f2c14733 /FrontEnd/src/app/views/home | |
parent | 14e5848c23c643cea9b5d709770747d98c3d75e2 (diff) | |
download | timeline-ac769e656b122ff569c3f1534701b71e00fed586.tar.gz timeline-ac769e656b122ff569c3f1534701b71e00fed586.tar.bz2 timeline-ac769e656b122ff569c3f1534701b71e00fed586.zip |
Split front and back end.
Diffstat (limited to 'FrontEnd/src/app/views/home')
-rw-r--r-- | FrontEnd/src/app/views/home/BoardWithUser.tsx | 101 | ||||
-rw-r--r-- | FrontEnd/src/app/views/home/BoardWithoutUser.tsx | 60 | ||||
-rw-r--r-- | FrontEnd/src/app/views/home/OfflineBoard.tsx | 61 | ||||
-rw-r--r-- | FrontEnd/src/app/views/home/TimelineBoard.tsx | 73 | ||||
-rw-r--r-- | FrontEnd/src/app/views/home/TimelineCreateDialog.tsx | 53 | ||||
-rw-r--r-- | FrontEnd/src/app/views/home/home.sass | 13 | ||||
-rw-r--r-- | FrontEnd/src/app/views/home/index.tsx | 99 |
7 files changed, 460 insertions, 0 deletions
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 ( + <Row className="my-2 justify-content-center"> + {ownTimelines === "offline" && joinTimelines === "offline" ? ( + <Col className="py-2" sm="8" lg="6"> + <OfflineBoard + onReload={() => { + setOwnTimelines("loading"); + setJoinTimelines("loading"); + }} + /> + </Col> + ) : ( + <> + <Col sm="6" lg="5" className="py-2"> + <TimelineBoard + title={t("home.ownTimeline")} + timelines={ownTimelines} + onReload={() => { + setOwnTimelines("loading"); + }} + /> + </Col> + <Col sm="6" lg="5" className="py-2"> + <TimelineBoard + title={t("home.joinTimeline")} + timelines={joinTimelines} + onReload={() => { + setJoinTimelines("loading"); + }} + /> + </Col> + </> + )} + </Row> + ); +}; + +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 ( + <Row className="my-2 justify-content-center"> + {publicTimelines === "offline" ? ( + <Col sm="8" lg="6"> + <OfflineBoard + onReload={() => { + setPublicTimelines("loading"); + }} + /> + </Col> + ) : ( + <Col sm="8" lg="6"> + <TimelineBoard + timelines={publicTimelines} + onReload={() => { + setPublicTimelines("loading"); + }} + /> + </Col> + )} + </Row> + ); +}; + +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<OfflineBoardProps> = ({ onReload }) => { + const [timelines, setTimelines] = React.useState<string[]>([]); + + React.useEffect(() => { + let subscribe = true; + void getAllCachedTimelineNames().then((t) => { + if (subscribe) setTimelines(t); + }); + return () => { + subscribe = false; + }; + }); + + return ( + <> + <Trans i18nKey="home.offlinePrompt"> + 0 + <a + href="#" + onClick={(e) => { + onReload(); + e.preventDefault(); + }} + > + 1 + </a> + 2 + </Trans> + {timelines.map((timeline) => { + const isPersonal = timeline.startsWith("@"); + const url = isPersonal + ? `/users/${timeline.slice(1)}` + : `/timelines/${timeline}`; + return ( + <div key={timeline} className="timeline-board-item"> + {isPersonal ? ( + <UserTimelineLogo className="icon" /> + ) : ( + <TimelineLogo className="icon" /> + )} + <Link to={url}>{timeline}</Link> + </div> + ); + })} + </> + ); +}; + +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<TimelineBoardProps> = (props) => { + const { title, timelines, className } = props; + + return ( + <div className={clsx("timeline-board", className)}> + {title != null && <h3 className="text-center">{title}</h3>} + {(() => { + if (timelines === "loading") { + return ( + <div className="d-flex flex-grow-1 justify-content-center align-items-center"> + <Spinner variant="primary" animation="border" /> + </div> + ); + } else if (timelines === "offline") { + return ( + <div className="d-flex flex-grow-1 justify-content-center align-items-center"> + <Trans i18nKey="loadFailReload" parent="div"> + 0 + <a + href="#" + onClick={(e) => { + props.onReload(); + e.preventDefault(); + }} + > + 1 + </a> + 2 + </Trans> + </div> + ); + } else { + return timelines.map((timeline) => { + const { name } = timeline; + const isPersonal = name.startsWith("@"); + const url = isPersonal + ? `/users/${timeline.owner.username}` + : `/timelines/${name}`; + return ( + <div key={name} className="timeline-board-item"> + {isPersonal ? ( + <UserTimelineLogo className="icon" /> + ) : ( + <TimelineLogo className="icon" /> + )} + <Link to={url}>{name}</Link> + </div> + ); + }); + } + })()} + </div> + ); +}; + +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<TimelineCreateDialogProps> = (props) => { + const history = useHistory(); + + let nameSaved: string; + + return ( + <OperationDialog + open={props.open} + close={props.close} + titleColor="success" + title="home.createDialog.title" + inputScheme={[ + { + type: "text", + label: "home.createDialog.name", + helperText: "home.createDialog.nameFormat", + validator: (name) => { + 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<string>(""); + + 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 ( + <> + <Container fluid> + <Row className="justify-content-center"> + <Col xs={12} sm={10} md={8} lg={6}> + <SearchInput + className="justify-content-center" + value={navText} + onChange={setNavText} + onButtonClick={goto} + buttonText={t("home.go")} + placeholder="@crupest" + additionalButton={ + user != null && ( + <Button + variant="outline-success" + onClick={() => { + setDialog("create"); + }} + > + {t("home.createButton")} + </Button> + ) + } + /> + </Col> + </Row> + {(() => { + if (user == null) { + return <BoardWithoutUser />; + } else { + return <BoardWithUser user={user} />; + } + })()} + </Container> + <footer className="text-right"> + <a + className="mx-3 text-muted" + href="http://beian.miit.gov.cn/" + target="_blank" + rel="noopener noreferrer" + > + <small>鄂ICP备18030913号-1</small> + </a> + <a + className="mx-3 text-muted" + href="http://www.beian.gov.cn/" + target="_blank" + rel="noopener noreferrer" + > + <small className="white-space-no-wrap">公安备案 42112102000124</small> + </a> + </footer> + {dialog === "create" && ( + <TimelineCreateDialog + open + close={() => { + setDialog(null); + }} + /> + )} + </> + ); +}; + +export default HomePage; |