diff options
Diffstat (limited to 'FrontEnd/src/views/home')
-rw-r--r-- | FrontEnd/src/views/home/TimelineListView.tsx | 101 | ||||
-rw-r--r-- | FrontEnd/src/views/home/WebsiteIntroduction.tsx | 77 | ||||
-rw-r--r-- | FrontEnd/src/views/home/index.css | 73 | ||||
-rw-r--r-- | FrontEnd/src/views/home/index.tsx | 76 |
4 files changed, 327 insertions, 0 deletions
diff --git a/FrontEnd/src/views/home/TimelineListView.tsx b/FrontEnd/src/views/home/TimelineListView.tsx new file mode 100644 index 00000000..2fb54820 --- /dev/null +++ b/FrontEnd/src/views/home/TimelineListView.tsx @@ -0,0 +1,101 @@ +import React from "react"; + +import { convertI18nText, I18nText } from "@/common"; + +import { HttpTimelineInfo } from "@/http/timeline"; +import { useTranslation } from "react-i18next"; +import { Link } from "react-router-dom"; + +interface TimelineListItemProps { + timeline: HttpTimelineInfo; +} + +const TimelineListItem: React.FC<TimelineListItemProps> = ({ timeline }) => { + const url = React.useMemo( + () => + timeline.name.startsWith("src") + ? `/users/${timeline.owner.username}` + : `/timelines/${timeline.name}`, + [timeline] + ); + + return ( + <div className="home-timeline-list-item home-timeline-list-item-timeline"> + <svg className="home-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> + <div>{timeline.title}</div> + <div> + <small className="text-secondary">{timeline.description}</small> + </div> + </div> + <Link to={url}> + <i className="icon-button bi-arrow-right ms-3" /> + </Link> + </div> + ); +}; + +const TimelineListArrow: React.FC = () => { + return ( + <div> + <div className="home-timeline-list-item"> + <svg className="home-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-timeline-list-item"> + <svg + className="home-timeline-list-item-line home-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-timeline-list"> + <div className="home-timeline-list-item"> + <svg className="home-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} />) + : null} + <TimelineListArrow /> + </div> + ); +}; + +export default TimelineListView; diff --git a/FrontEnd/src/views/home/WebsiteIntroduction.tsx b/FrontEnd/src/views/home/WebsiteIntroduction.tsx new file mode 100644 index 00000000..aea7b4b2 --- /dev/null +++ b/FrontEnd/src/views/home/WebsiteIntroduction.tsx @@ -0,0 +1,77 @@ +import React from "react"; +import { useTranslation } from "react-i18next"; +import { Link } from "react-router-dom"; + +const WebsiteIntroduction: React.FC<{ + className?: string; + style?: React.CSSProperties; +}> = ({ className, style }) => { + const { i18n } = useTranslation(); + + if (i18n.language.startsWith("zh")) { + return ( + <div className={className} style={style}> + <h2> + 欢迎来到<strong>时间线</strong>!🎉🎉🎉 + </h2> + <p> + 本网站由无数个独立的时间线构成,每一个时间线都是一个消息列表,类似于一个聊天软件(比如QQ)。 + </p> + <p> + 如果你拥有一个账号,<Link to="/login">登陆</Link> + 后你可以自由地在属于你的时间线中发送内容,支持markdown和上传图片哦!你可以创建一个新的时间线来开启一个新的话题。你也可以设置相关权限,只让一部分人能看到时间线的内容。 + </p> + <p> + 如果你没有账号,那么你可以去浏览一下公开的时间线,比如下面这些站长设置的高光时间线。 + </p> + <p> + 鉴于这个网站在我的小型服务器上部署,所以没有开放注册。如果你也想把这个服务部署到自己的服务器上,你可以在 + <Link to="/about">关于</Link>页面找到一些信息。 + </p> + <p> + <small className="text-secondary"> + 这一段介绍是我的对象抱怨多次我的网站他根本看不明白之后加的,希望你能顺利看懂这个网站的逻辑!😅 + </small> + </p> + </div> + ); + } else { + return ( + <div className={className} style={style}> + <h2> + Welcome to <strong>Timeline</strong>!🎉🎉🎉 + </h2> + <p> + This website consists of many individual timelines. Each timeline is a + list of messages just like a chat app. + </p> + <p> + If you do have an account, you can <Link to="/login">login</Link> and + post messages, which supports Markdown and images, in your timelines. + You can also create a new timeline to open a new topic. You can set + the permission of a timeline to only allow specified people to see the + content of the timeline. + </p> + <p> + If you don't have an account, you can view some public timelines + like highlight timelines below set by website manager. + </p> + <p> + Since this website is hosted on my tiny server, so account registry is + not opened. If you want to host this service on your own server, you + can find some useful information on <Link to="/about">about</Link>{" "} + page. + </p> + <p> + <small className="text-secondary"> + This introduction is added after my lover complained a lot of times + about the obscuration of my website. May you understand the logic of + it!😅 + </small> + </p> + </div> + ); + } +}; + +export default WebsiteIntroduction; diff --git a/FrontEnd/src/views/home/index.css b/FrontEnd/src/views/home/index.css new file mode 100644 index 00000000..516aba52 --- /dev/null +++ b/FrontEnd/src/views/home/index.css @@ -0,0 +1,73 @@ +.timeline-board {
+ min-height: 200px;
+ height: 100%;
+ position: relative;
+}
+
+.timeline-board-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+}
+
+.timeline-board-item {
+ font-size: 1.1em;
+ height: 48px;
+ transition: background 0.3s;
+ display: flex;
+ align-items: center;
+}
+.timeline-board-item .icon {
+ height: 1.3em;
+ color: black;
+}
+.timeline-board-item:hover {
+ background: #dee2e6;
+}
+.timeline-board-item .right {
+ display: flex;
+ align-items: center;
+ flex-shrink: 0;
+}
+.timeline-board-item .title {
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.home-timeline-list-item {
+ display: flex;
+ align-items: center;
+}
+
+.home-timeline-list-item-timeline {
+ transition: background 0.8s;
+ animation: 0.8s home-timeline-list-item-timeline-enter;
+}
+.home-timeline-list-item-timeline:hover {
+ background: #e9ecef;
+}
+
+@keyframes home-timeline-list-item-timeline-enter {
+ from {
+ transform: translate(-100%, 0);
+ opacity: 0;
+ }
+}
+.home-timeline-list-item-line {
+ width: 80px;
+ flex-shrink: 0;
+}
+
+@keyframes home-timeline-list-loading-head-animation {
+ from {
+ transform: translate(0, -30px);
+ opacity: 1;
+ }
+ to {
+ opacity: 0;
+ }
+}
+.home-timeline-list-loading-head {
+ animation: 1s infinite home-timeline-list-loading-head-animation;
+}
diff --git a/FrontEnd/src/views/home/index.tsx b/FrontEnd/src/views/home/index.tsx new file mode 100644 index 00000000..ddb72e76 --- /dev/null +++ b/FrontEnd/src/views/home/index.tsx @@ -0,0 +1,76 @@ +import React from "react"; +import { useHistory } from "react-router"; + +import { HttpTimelineInfo } from "@/http/timeline"; +import { getHttpHighlightClient } from "@/http/highlight"; + +import SearchInput from "../common/SearchInput"; +import TimelineListView from "./TimelineListView"; +import WebsiteIntroduction from "./WebsiteIntroduction"; + +import "./index.css"; + +const highlightTimelineMessageMap = { + loading: "home.loadingHighlightTimelines", + done: "home.loadedHighlightTimelines", + error: "home.errorHighlightTimelines", +} as const; + +const HomeV2: React.FC = () => { + const history = useHistory(); + + const [navText, setNavText] = React.useState<string>(""); + + const [highlightTimelineState, setHighlightTimelineState] = React.useState< + "loading" | "done" | "error" + >("loading"); + const [highlightTimelines, setHighlightTimelines] = React.useState< + HttpTimelineInfo[] | undefined + >(); + + React.useEffect(() => { + if (highlightTimelineState === "loading") { + let subscribe = true; + void getHttpHighlightClient() + .list() + .then( + (data) => { + if (subscribe) { + setHighlightTimelineState("done"); + setHighlightTimelines(data); + } + }, + () => { + if (subscribe) { + setHighlightTimelineState("error"); + setHighlightTimelines(undefined); + } + } + ); + return () => { + subscribe = false; + }; + } + }, [highlightTimelineState]); + + return ( + <> + <SearchInput + className="mx-2 my-3 float-sm-end" + value={navText} + onChange={setNavText} + onButtonClick={() => { + history.push(`search?q=${navText}`); + }} + alwaysOneline + /> + <WebsiteIntroduction className="m-2" /> + <TimelineListView + headerText={highlightTimelineMessageMap[highlightTimelineState]} + timelines={highlightTimelines} + /> + </> + ); +}; + +export default HomeV2; |