diff options
Diffstat (limited to 'Timeline/ClientApp/src/app/views/home')
7 files changed, 460 insertions, 0 deletions
diff --git a/Timeline/ClientApp/src/app/views/home/BoardWithUser.tsx b/Timeline/ClientApp/src/app/views/home/BoardWithUser.tsx new file mode 100644 index 00000000..dcd39cbe --- /dev/null +++ b/Timeline/ClientApp/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/Timeline/ClientApp/src/app/views/home/BoardWithoutUser.tsx b/Timeline/ClientApp/src/app/views/home/BoardWithoutUser.tsx new file mode 100644 index 00000000..ebfddb50 --- /dev/null +++ b/Timeline/ClientApp/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/Timeline/ClientApp/src/app/views/home/OfflineBoard.tsx b/Timeline/ClientApp/src/app/views/home/OfflineBoard.tsx new file mode 100644 index 00000000..fc05bd74 --- /dev/null +++ b/Timeline/ClientApp/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/Timeline/ClientApp/src/app/views/home/TimelineBoard.tsx b/Timeline/ClientApp/src/app/views/home/TimelineBoard.tsx new file mode 100644 index 00000000..a3d176e1 --- /dev/null +++ b/Timeline/ClientApp/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/Timeline/ClientApp/src/app/views/home/TimelineCreateDialog.tsx b/Timeline/ClientApp/src/app/views/home/TimelineCreateDialog.tsx new file mode 100644 index 00000000..d9467719 --- /dev/null +++ b/Timeline/ClientApp/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/Timeline/ClientApp/src/app/views/home/home.sass b/Timeline/ClientApp/src/app/views/home/home.sass new file mode 100644 index 00000000..f5d6ffc3 --- /dev/null +++ b/Timeline/ClientApp/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/Timeline/ClientApp/src/app/views/home/index.tsx b/Timeline/ClientApp/src/app/views/home/index.tsx new file mode 100644 index 00000000..760adcea --- /dev/null +++ b/Timeline/ClientApp/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;  | 
