diff options
Diffstat (limited to 'FrontEnd/src/app/views')
| -rw-r--r-- | FrontEnd/src/app/views/home-v2/TimelineListView.tsx | 85 | ||||
| -rw-r--r-- | FrontEnd/src/app/views/home-v2/home-v2.sass | 18 | ||||
| -rw-r--r-- | FrontEnd/src/app/views/home-v2/index.tsx | 58 | ||||
| -rw-r--r-- | FrontEnd/src/app/views/timeline-common/Timeline.tsx | 77 | ||||
| -rw-r--r-- | FrontEnd/src/app/views/timeline-common/TimelineLoading.tsx | 18 | ||||
| -rw-r--r-- | FrontEnd/src/app/views/timeline-common/TimelinePageTemplate.tsx | 116 | 
6 files changed, 266 insertions, 106 deletions
| diff --git a/FrontEnd/src/app/views/home-v2/TimelineListView.tsx b/FrontEnd/src/app/views/home-v2/TimelineListView.tsx new file mode 100644 index 00000000..1ba9f765 --- /dev/null +++ b/FrontEnd/src/app/views/home-v2/TimelineListView.tsx @@ -0,0 +1,85 @@ +import React from "react"; + +import { convertI18nText, I18nText } from "@/common"; + +import { HttpTimelineInfo } from "@/http/timeline"; +import { useTranslation } from "react-i18next"; + +interface TimelineListItemProps { +  timeline: HttpTimelineInfo; +} + +const TimelineListItem: React.FC<TimelineListItemProps> = ({ timeline }) => { +  return ( +    <div className="home-v2-timeline-list-item"> +      <svg className="home-v2-timeline-list-item-line" viewBox="0 0 120 100"> +        <path +          d="M 80,50 m 0,-12 a 12 12 180 1 1 0,24 12 12 180 1 1 0,-24 z M 60,0 h 40 v 100 h -40 z" +          fillRule="evenodd" +          fill="#007bff" +        /> +      </svg> +      <div>{timeline.title}</div> +    </div> +  ); +}; + +const TimelineListLoading: React.FC = () => { +  return ( +    <div> +      <div className="home-v2-timeline-list-item"> +        <svg className="home-v2-timeline-list-item-line" viewBox="0 0 120 60"> +          <path d="M 60,0 h 40 v 20 l -20,20 l -20,-20 z" fill="#007bff" /> +        </svg> +      </div> +      <div className="home-v2-timeline-list-item"> +        <svg +          className="home-v2-timeline-list-item-line home-v2-timeline-list-loading-head" +          viewBox="0 0 120 40" +        > +          <path +            d="M 60,10 l 20,20 l 20,-20" +            fill="none" +            stroke="#007bff" +            strokeWidth="5" +          /> +        </svg> +      </div> +    </div> +  ); +}; + +interface TimelineListViewProps { +  headerText?: I18nText; +  timelines?: HttpTimelineInfo[]; +} + +const TimelineListView: React.FC<TimelineListViewProps> = ({ +  headerText, +  timelines, +}) => { +  const { t } = useTranslation(); + +  return ( +    <div className="home-v2-timeline-list"> +      <div className="home-v2-timeline-list-item"> +        <svg className="home-v2-timeline-list-item-line" viewBox="0 0 120 120"> +          <path +            d="M 0,20 Q 80,20 80,80 l 0,40" +            stroke="#007bff" +            strokeWidth="40" +            fill="none" +          /> +        </svg> +        <h3>{convertI18nText(headerText, t)}</h3> +      </div> +      {timelines != null ? ( +        timelines.map((t) => <TimelineListItem key={t.name} timeline={t} />) +      ) : ( +        <TimelineListLoading /> +      )} +    </div> +  ); +}; + +export default TimelineListView; diff --git a/FrontEnd/src/app/views/home-v2/home-v2.sass b/FrontEnd/src/app/views/home-v2/home-v2.sass new file mode 100644 index 00000000..a3218f08 --- /dev/null +++ b/FrontEnd/src/app/views/home-v2/home-v2.sass @@ -0,0 +1,18 @@ +.home-v2-timeline-list-item
 +  display: flex
 +  align-items: center
 +
 +.home-v2-timeline-list-item-line
 +  width: 80px
 +  flex-shrink: 0
 +
 +@keyframes home-v2-timeline-list-loading-head-animation
 +  from
 +    transform: translate(0,-30px)
 +    opacity: 1
 +
 +  to
 +    opacity: 0
 +
 +.home-v2-timeline-list-loading-head
 +  animation: 1s infinite home-v2-timeline-list-loading-head-animation
 diff --git a/FrontEnd/src/app/views/home-v2/index.tsx b/FrontEnd/src/app/views/home-v2/index.tsx new file mode 100644 index 00000000..c92db78e --- /dev/null +++ b/FrontEnd/src/app/views/home-v2/index.tsx @@ -0,0 +1,58 @@ +import React from "react"; +import { useHistory } from "react-router"; +import { useTranslation } from "react-i18next"; +import { Container, Button, Row, Col } from "react-bootstrap"; + +import { useUser } from "@/services/user"; +import SearchInput from "../common/SearchInput"; + +import TimelineListView from "./TimelineListView"; +import TimelineCreateDialog from "../home/TimelineCreateDialog"; + +const HomeV2: 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); + +  return ( +    <> +      <Container fluid className="px-0"> +        <Row className="my-3 px-2 justify-content-end"> +          <Col xs="auto"> +            <SearchInput +              value={navText} +              onChange={setNavText} +              onButtonClick={() => { +                history.push(`search?q=${navText}`); +              }} +              additionalButton={ +                user != null && ( +                  <Button +                    variant="outline-success" +                    onClick={() => { +                      setDialog("create"); +                    }} +                  > +                    {t("home.createButton")} +                  </Button> +                ) +              } +            /> +          </Col> +        </Row> +        <TimelineListView headerText="home.loadingHighlightTimelines" /> +      </Container> +      {dialog === "create" && ( +        <TimelineCreateDialog open close={() => setDialog(null)} /> +      )} +    </> +  ); +}; + +export default HomeV2; diff --git a/FrontEnd/src/app/views/timeline-common/Timeline.tsx b/FrontEnd/src/app/views/timeline-common/Timeline.tsx index 60fbc45c..72d38ffd 100644 --- a/FrontEnd/src/app/views/timeline-common/Timeline.tsx +++ b/FrontEnd/src/app/views/timeline-common/Timeline.tsx @@ -9,18 +9,18 @@ import { getHttpTimelineClient, HttpTimelinePostInfo } from "@/http/timeline";  import TimelinePagedPostListView from "./TimelinePagedPostListView";  import TimelineTop from "./TimelineTop"; +import TimelineLoading from "./TimelineLoading";  export interface TimelineProps {    className?: string;    style?: React.CSSProperties; -  timelineName: string; +  timelineName?: string;    reloadKey: number;    onReload: () => void; -  onLoad?: () => void;  }  const Timeline: React.FC<TimelineProps> = (props) => { -  const { timelineName, className, style, reloadKey, onReload, onLoad } = props; +  const { timelineName, className, style, reloadKey, onReload } = props;    const [state, setState] = React.useState<      "loading" | "loaded" | "offline" | "notexist" | "forbid" | "error" @@ -33,56 +33,41 @@ const Timeline: React.FC<TimelineProps> = (props) => {    }, [timelineName]);    React.useEffect(() => { -    let subscribe = true; +    if (timelineName != null) { +      let subscribe = true; -    void getHttpTimelineClient() -      .listPost(timelineName) -      .then( -        (data) => { -          if (subscribe) { -            setState("loaded"); -            setPosts(data); +      void getHttpTimelineClient() +        .listPost(timelineName) +        .then( +          (data) => { +            if (subscribe) { +              setState("loaded"); +              setPosts(data); +            } +          }, +          (error) => { +            if (error instanceof HttpNetworkError) { +              setState("offline"); +            } else if (error instanceof HttpForbiddenError) { +              setState("forbid"); +            } else if (error instanceof HttpNotFoundError) { +              setState("notexist"); +            } else { +              console.error(error); +              setState("error"); +            }            } -        }, -        (error) => { -          if (error instanceof HttpNetworkError) { -            setState("offline"); -          } else if (error instanceof HttpForbiddenError) { -            setState("forbid"); -          } else if (error instanceof HttpNotFoundError) { -            setState("notexist"); -          } else { -            console.error(error); -            setState("error"); -          } -        } -      ); +        ); -    return () => { -      subscribe = false; -    }; -  }, [timelineName, reloadKey]); - -  React.useEffect(() => { -    if (state === "loaded") { -      onLoad?.(); +      return () => { +        subscribe = false; +      };      } -  }, [state, onLoad]); +  }, [timelineName, reloadKey]);    switch (state) {      case "loading": -      return ( -        <> -          <TimelineTop -            className="timeline-top-loading-enter" -            height={100} -            lineProps={{ -              center: "loading", -              startSegmentLength: 56, -            }} -          /> -        </> -      ); +      return <TimelineLoading />;      case "offline":        return (          <div className={className} style={style}> diff --git a/FrontEnd/src/app/views/timeline-common/TimelineLoading.tsx b/FrontEnd/src/app/views/timeline-common/TimelineLoading.tsx new file mode 100644 index 00000000..fc42f4b4 --- /dev/null +++ b/FrontEnd/src/app/views/timeline-common/TimelineLoading.tsx @@ -0,0 +1,18 @@ +import React from "react"; + +import TimelineTop from "./TimelineTop"; + +const TimelineLoading: React.FC = () => { +  return ( +    <TimelineTop +      className="timeline-top-loading-enter" +      height={100} +      lineProps={{ +        center: "loading", +        startSegmentLength: 56, +      }} +    /> +  ); +}; + +export default TimelineLoading; diff --git a/FrontEnd/src/app/views/timeline-common/TimelinePageTemplate.tsx b/FrontEnd/src/app/views/timeline-common/TimelinePageTemplate.tsx index 5cde014b..5b6dfa9c 100644 --- a/FrontEnd/src/app/views/timeline-common/TimelinePageTemplate.tsx +++ b/FrontEnd/src/app/views/timeline-common/TimelinePageTemplate.tsx @@ -1,6 +1,6 @@  import React from "react";  import { useTranslation } from "react-i18next"; -import { Container, Spinner } from "react-bootstrap"; +import { Container } from "react-bootstrap";  import { HttpNetworkError, HttpNotFoundError } from "@/http/common";  import { getHttpTimelineClient, HttpTimelineInfo } from "@/http/timeline"; @@ -33,14 +33,16 @@ const TimelinePageTemplate: React.FC<TimelinePageTemplateProps> = (props) => {    const { t } = useTranslation(); -  const [timeline, setTimeline] = React.useState< -    HttpTimelineInfo | "loading" | "offline" | "notexist" | "error" +  const [state, setState] = React.useState< +    "loading" | "done" | "offline" | "notexist" | "error"    >("loading"); +  const [timeline, setTimeline] = React.useState<HttpTimelineInfo | null>(null);    useReverseScrollPositionRemember();    React.useEffect(() => { -    setTimeline("loading"); +    setState("loading"); +    setTimeline(null);    }, [timelineName]);    React.useEffect(() => { @@ -50,19 +52,21 @@ const TimelinePageTemplate: React.FC<TimelinePageTemplateProps> = (props) => {        .then(          (data) => {            if (subscribe) { +            setState("done");              setTimeline(data);            }          },          (error) => {            if (subscribe) {              if (error instanceof HttpNetworkError) { -              setTimeline("offline"); +              setState("offline");              } else if (error instanceof HttpNotFoundError) { -              setTimeline("notexist"); +              setState("notexist");              } else {                console.error(error); -              setTimeline("error"); +              setState("error");              } +            setTimeline(null);            }          }        ); @@ -71,10 +75,6 @@ const TimelinePageTemplate: React.FC<TimelinePageTemplateProps> = (props) => {      };    }, [timelineName, reloadKey]); -  const scrollToBottom = React.useCallback(() => { -    window.scrollTo(0, document.body.scrollHeight); -  }, []); -    const [bottomSpaceHeight, setBottomSpaceHeight] = React.useState<number>(0);    const [timelineReloadKey, setTimelineReloadKey] = React.useState<number>(0); @@ -116,25 +116,9 @@ const TimelinePageTemplate: React.FC<TimelinePageTemplateProps> = (props) => {      );    }; -  let body: React.ReactElement; - -  if (timeline == "loading") { -    body = ( -      <div className="full-viewport-center-child"> -        <Spinner variant="primary" animation="grow" /> -      </div> -    ); -  } else if (timeline === "offline") { -    // TODO: i18n -    body = <p className="text-danger">Offline!</p>; -  } else if (timeline === "notexist") { -    body = <p className="text-danger">{t(props.notFoundI18nKey)}</p>; -  } else if (timeline === "error") { -    // TODO: i18n -    body = <p className="text-danger">Error!</p>; -  } else { -    body = ( -      <> +  return ( +    <> +      {timeline != null ? (          <CardComponent            className="timeline-template-card"            timeline={timeline} @@ -142,37 +126,49 @@ const TimelinePageTemplate: React.FC<TimelinePageTemplateProps> = (props) => {            toggleCollapse={toggleCardCollapse}            onReload={onReload}          /> -        <Container -          className="px-0" -          style={{ -            minHeight: `calc(100vh - ${56 + bottomSpaceHeight}px)`, -          }} -        > -          <Timeline -            timelineName={timeline.name} -            reloadKey={timelineReloadKey} -            onReload={reloadTimeline} -            onLoad={scrollToBottom} +      ) : null} +      <Container +        className="px-0" +        style={{ +          minHeight: `calc(100vh - ${56 + bottomSpaceHeight}px)`, +        }} +      > +        {(() => { +          if (state === "offline") { +            // TODO: i18n +            return <p className="text-danger">Offline!</p>; +          } else if (state === "notexist") { +            return <p className="text-danger">{t(props.notFoundI18nKey)}</p>; +          } else if (state === "error") { +            // TODO: i18n +            return <p className="text-danger">Error!</p>; +          } else { +            return ( +              <Timeline +                timelineName={timeline?.name} +                reloadKey={timelineReloadKey} +                onReload={reloadTimeline} +              /> +            ); +          } +        })()} +      </Container> +      {timeline != null && timeline.postable ? ( +        <> +          <div +            style={{ height: bottomSpaceHeight }} +            className="flex-fix-length"            /> -        </Container> -        {timeline.postable ? ( -          <> -            <div -              style={{ height: bottomSpaceHeight }} -              className="flex-fix-length" -            /> -            <TimelinePostEdit -              className="fixed-bottom" -              timeline={timeline} -              onHeightChange={onPostEditHeightChange} -              onPosted={reloadTimeline} -            /> -          </> -        ) : null} -      </> -    ); -  } -  return body; +          <TimelinePostEdit +            className="fixed-bottom" +            timeline={timeline} +            onHeightChange={onPostEditHeightChange} +            onPosted={reloadTimeline} +          /> +        </> +      ) : null} +    </> +  );  };  export default TimelinePageTemplate; | 
