aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--FrontEnd/package-lock.json14
-rw-r--r--FrontEnd/package.json2
-rw-r--r--FrontEnd/src/app/App.tsx7
-rw-r--r--FrontEnd/src/app/index.sass1
-rw-r--r--FrontEnd/src/app/locales/en/translation.json1
-rw-r--r--FrontEnd/src/app/locales/zh/translation.json1
-rw-r--r--FrontEnd/src/app/views/home-v2/TimelineListView.tsx85
-rw-r--r--FrontEnd/src/app/views/home-v2/home-v2.sass18
-rw-r--r--FrontEnd/src/app/views/home-v2/index.tsx58
-rw-r--r--FrontEnd/src/app/views/timeline-common/Timeline.tsx77
-rw-r--r--FrontEnd/src/app/views/timeline-common/TimelineLoading.tsx18
-rw-r--r--FrontEnd/src/app/views/timeline-common/TimelinePageTemplate.tsx116
12 files changed, 284 insertions, 114 deletions
diff --git a/FrontEnd/package-lock.json b/FrontEnd/package-lock.json
index debd4ac9..48d41b54 100644
--- a/FrontEnd/package-lock.json
+++ b/FrontEnd/package-lock.json
@@ -91,7 +91,7 @@
"ts-loader": "^8.1.0",
"typescript": "^4.2.4",
"url-loader": "^4.1.1",
- "webpack": "^5.32.0",
+ "webpack": "^5.33.2",
"webpack-chain": "^6.5.1",
"webpack-cli": "^4.6.0",
"webpack-dev-server": "^3.11.2",
@@ -14886,9 +14886,9 @@
"dev": true
},
"node_modules/webpack": {
- "version": "5.32.0",
- "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.32.0.tgz",
- "integrity": "sha512-jB9PrNMFnPRiZGnm/j3qfNqJmP3ViRzkuQMIf8za0dgOYvSLi/cgA+UEEGvik9EQHX1KYyGng5PgBTTzGrH9xg==",
+ "version": "5.33.2",
+ "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.33.2.tgz",
+ "integrity": "sha512-X4b7F1sYBmJx8mlh2B7mV5szEkE0jYNJ2y3akgAP0ERi0vLCG1VvdsIxt8lFd4st6SUy0lf7W0CCQS566MBpJg==",
"dev": true,
"dependencies": {
"@types/eslint-scope": "^3.7.0",
@@ -27734,9 +27734,9 @@
"dev": true
},
"webpack": {
- "version": "5.32.0",
- "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.32.0.tgz",
- "integrity": "sha512-jB9PrNMFnPRiZGnm/j3qfNqJmP3ViRzkuQMIf8za0dgOYvSLi/cgA+UEEGvik9EQHX1KYyGng5PgBTTzGrH9xg==",
+ "version": "5.33.2",
+ "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.33.2.tgz",
+ "integrity": "sha512-X4b7F1sYBmJx8mlh2B7mV5szEkE0jYNJ2y3akgAP0ERi0vLCG1VvdsIxt8lFd4st6SUy0lf7W0CCQS566MBpJg==",
"dev": true,
"requires": {
"@types/eslint-scope": "^3.7.0",
diff --git a/FrontEnd/package.json b/FrontEnd/package.json
index e65b09a2..8296c5d9 100644
--- a/FrontEnd/package.json
+++ b/FrontEnd/package.json
@@ -106,7 +106,7 @@
"ts-loader": "^8.1.0",
"typescript": "^4.2.4",
"url-loader": "^4.1.1",
- "webpack": "^5.32.0",
+ "webpack": "^5.33.2",
"webpack-chain": "^6.5.1",
"webpack-cli": "^4.6.0",
"webpack-dev-server": "^3.11.2",
diff --git a/FrontEnd/src/app/App.tsx b/FrontEnd/src/app/App.tsx
index fb57bd1e..5c4b7eb2 100644
--- a/FrontEnd/src/app/App.tsx
+++ b/FrontEnd/src/app/App.tsx
@@ -4,6 +4,7 @@ import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
import AppBar from "./views/common/AppBar";
import LoadingPage from "./views/common/LoadingPage";
import Home from "./views/home";
+import HomeV2 from "./views/home-v2";
import Login from "./views/login";
import Settings from "./views/settings";
import About from "./views/about";
@@ -41,6 +42,12 @@ const App: React.FC = () => {
<Route exact path="/">
<Home />
</Route>
+ <Route exact path="/home">
+ <Home />
+ </Route>
+ <Route exact path="/home-new">
+ <HomeV2 />
+ </Route>
<Route exact path="/login">
<Login />
</Route>
diff --git a/FrontEnd/src/app/index.sass b/FrontEnd/src/app/index.sass
index 2079cad8..6527a65f 100644
--- a/FrontEnd/src/app/index.sass
+++ b/FrontEnd/src/app/index.sass
@@ -4,6 +4,7 @@
@import './views/common/common'
@import './views/common/alert/alert'
@import './views/home/home'
+@import './views/home-v2/home-v2'
@import './views/about/about'
@import './views/login/login'
@import './views/settings/settings'
diff --git a/FrontEnd/src/app/locales/en/translation.json b/FrontEnd/src/app/locales/en/translation.json
index 65ddbe0c..6707105a 100644
--- a/FrontEnd/src/app/locales/en/translation.json
+++ b/FrontEnd/src/app/locales/en/translation.json
@@ -27,6 +27,7 @@
"chooseImage": "Choose a image",
"loadImageError": "Failed to load image.",
"home": {
+ "loadingHighlightTimelines": "Loading highlight timelines...",
"highlightTimeline": "Highlight Timelines",
"relatedTimeline": "Timelines Related To You",
"publicTimeline": "Public Timelines",
diff --git a/FrontEnd/src/app/locales/zh/translation.json b/FrontEnd/src/app/locales/zh/translation.json
index f6971241..dbff0a28 100644
--- a/FrontEnd/src/app/locales/zh/translation.json
+++ b/FrontEnd/src/app/locales/zh/translation.json
@@ -27,6 +27,7 @@
"chooseImage": "选择一个图片",
"loadImageError": "加载图片失败",
"home": {
+ "loadingHighlightTimelines": "正在加载高光时间线...",
"highlightTimeline": "高光时间线",
"relatedTimeline": "关于你的时间线",
"publicTimeline": "公开时间线",
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;