Skip to content

Commit 13b077d

Browse files
OXeuSittymin
andauthored
feat: Add adjacent feeds feature (Closes #202) (#293)
* style: Add placeholder div style: Fixed FeedCard width fix: Can't delete cache chore: Adding clear adjacent feed cache refactor: Adjacent feeds API feat: Adding some cache function style: Change flex direction on mobile screens feat: Adding adjacent feeds to the frontend feat: Adding adjacent feeds API to the backend * style: 调整上一篇/下一篇样式 Signed-off-by: Xeu <thankrain@qq.com> --------- Signed-off-by: Xeu <thankrain@qq.com> Co-authored-by: Sittymin <mail@sittymin.top>
1 parent 37205bc commit 13b077d

File tree

12 files changed

+304
-55
lines changed

12 files changed

+304
-55
lines changed

client/public/locales/en/translation.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,9 @@
120120
}
121121
},
122122
"logout": "Logout",
123+
"main_content": "Main content",
123124
"next": "Next Page",
125+
"no_more": "No More",
124126
"preview": "Preview",
125127
"previous": "Previous Page",
126128
"publish": {
@@ -213,7 +215,6 @@
213215
"title": "Top"
214216
},
215217
"unlisted": "Unlisted",
216-
"untitled": "Untitled",
217218
"untop": {
218219
"title": "Untop"
219220
},

client/public/locales/ja/translation.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,9 @@
120120
}
121121
},
122122
"logout": "ログアウト",
123+
"main_content": "本文",
123124
"next": "次のページ",
125+
"no_more": "もうない",
124126
"preview": "プレビュー",
125127
"previous": "前のページ",
126128
"publish": {
@@ -213,7 +215,6 @@
213215
"title": "キャッシュをクリア"
214216
},
215217
"unlisted": "リストされていない",
216-
"untitled": "無題",
217218
"untop": {
218219
"title": "トップ解除"
219220
},

client/public/locales/zh-CN/translation.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,9 @@
120120
}
121121
},
122122
"logout": "退出登录",
123+
"main_content": "正文",
123124
"next": "下一页",
125+
"no_more": "没有更多了",
124126
"preview": "预览",
125127
"previous": "上一页",
126128
"publish": {
@@ -213,7 +215,6 @@
213215
"title": "置顶"
214216
},
215217
"unlisted": "未列出",
216-
"untitled": "未列出",
217218
"untop": {
218219
"title": "取消置顶"
219220
},

client/public/locales/zh-TW/translation.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,9 @@
120120
}
121121
},
122122
"logout": "登出",
123+
"main_content": "正文",
123124
"next": "下一頁",
125+
"no_more": "沒有更多了",
124126
"preview": "預覽",
125127
"previous": "上一頁",
126128
"publish": {
@@ -213,7 +215,6 @@
213215
"title": "置頂"
214216
},
215217
"unlisted": "未列出",
216-
"untitled": "未列出",
217218
"untop": {
218219
"title": "取消置頂"
219220
},
+81
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import {useEffect, useState} from "react";
2+
import {client} from "../main.tsx";
3+
import {timeago} from "../utils/timeago.ts";
4+
import {Link} from "wouter";
5+
import {useTranslation} from "react-i18next";
6+
7+
export type AdjacentFeed = {
8+
id: number;
9+
title: string | null;
10+
summary: string;
11+
hashtags: {
12+
id: number;
13+
name: string;
14+
}[];
15+
createdAt: Date;
16+
updatedAt: Date;
17+
};
18+
export type AdjacentFeeds = {
19+
nextFeed: AdjacentFeed | null;
20+
previousFeed: AdjacentFeed | null;
21+
};
22+
23+
export function AdjacentSection({id, setError}: { id: string, setError: (error: string) => void }) {
24+
const [adjacentFeeds, setAdjacentFeeds] = useState<AdjacentFeeds>();
25+
26+
useEffect(() => {
27+
client.feed
28+
.adjacent({id})
29+
.get()
30+
.then(({data, error}) => {
31+
if (error) {
32+
setError(error.value as string);
33+
} else if (data && typeof data !== "string") {
34+
setAdjacentFeeds(data);
35+
}
36+
});
37+
}, [id, setError]);
38+
return (
39+
<div className="rounded-2xl bg-w m-2 grid grid-cols-1 sm:grid-cols-2">
40+
<AdjacentCard data={adjacentFeeds?.previousFeed} type="previous"/>
41+
<AdjacentCard data={adjacentFeeds?.nextFeed} type="next"/>
42+
</div>
43+
)
44+
}
45+
46+
export function AdjacentCard({data, type}: { data: AdjacentFeed | null | undefined, type: "previous" | "next" }) {
47+
const direction = type === "previous" ? "text-start" : "text-end"
48+
const radius = type === "previous" ? "rounded-l-2xl" : "rounded-r-2xl"
49+
const {t} = useTranslation()
50+
if (!data) {
51+
return (<div className="w-full p-6 duration-300">
52+
<p className={`t-secondary w-full ${direction}`}>
53+
{type === "previous" ? "Previous" : "Next"}
54+
</p>
55+
<h1 className={`text-xl text-gray-700 dark:text-white text-pretty truncate ${direction}`}>
56+
{t('no_more')}
57+
</h1>
58+
</div>);
59+
}
60+
return (
61+
<Link href={`/feed/${data.id}`} target="_blank"
62+
className={`w-full p-6 duration-300 bg-button ${radius}`}>
63+
<p className={`t-secondary w-full ${direction}`}>
64+
{type === "previous" ? "Previous" : "Next"}
65+
</p>
66+
<h1 className={`text-xl font-bold text-gray-700 dark:text-white text-pretty truncate ${direction}`}>
67+
{data.title}
68+
</h1>
69+
<p className={`space-x-2 ${direction}`}>
70+
<span className="text-gray-400 text-sm" title={new Date(data.createdAt).toLocaleString()}>
71+
{data.createdAt === data.updatedAt ? timeago(data.createdAt) : t('feed_card.published$time', {time: timeago(data.createdAt)})}
72+
</span>
73+
{data.createdAt !== data.updatedAt &&
74+
<span className="text-gray-400 text-sm" title={new Date(data.updatedAt).toLocaleString()}>
75+
{t('feed_card.updated$time', {time: timeago(data.updatedAt)})}
76+
</span>
77+
}
78+
</p>
79+
</Link>
80+
)
81+
}

client/src/components/feed_card.tsx

+9-8
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
import { Link } from "wouter";
2-
import { useTranslation } from "react-i18next";
3-
import { timeago } from "../utils/timeago";
4-
import { HashTag } from "./hashtag";
5-
import { useMemo } from "react";
1+
import {Link} from "wouter";
2+
import {useTranslation} from "react-i18next";
3+
import {timeago} from "../utils/timeago";
4+
import {HashTag} from "./hashtag";
5+
import {useMemo} from "react";
6+
67
export function FeedCard({ id, title, avatar, draft, listed, top, summary, hashtags, createdAt, updatedAt }:
78
{
89
id: string, avatar?: string,
@@ -34,10 +35,10 @@ export function FeedCard({ id, title, avatar, draft, listed, top, summary, hasht
3435
}
3536
</p>
3637
<p className="space-x-2">
37-
{draft === 1 && <span className="text-gray-400 text-sm">草稿</span>}
38-
{listed === 0 && <span className="text-gray-400 text-sm">未列出</span>}
38+
{draft === 1 && <span className="text-gray-400 text-sm">{t("draft")}</span>}
39+
{listed === 0 && <span className="text-gray-400 text-sm">{t("unlisted")}</span>}
3940
{top === 1 && <span className="text-theme text-sm">
40-
置顶
41+
{t('article.top.title')}
4142
</span>}
4243
</p>
4344
<p className="text-pretty overflow-hidden dark:text-neutral-500">

client/src/page/callback.tsx

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
import { useEffect } from "react";
2-
import { setCookie } from "typescript-cookie";
3-
import { useLocation, useSearch } from "wouter";
1+
import {useEffect} from "react";
2+
import {setCookie} from "typescript-cookie";
3+
import {useLocation, useSearch} from "wouter";
44

55
export function CallbackPage() {
66
const searchParams = new URLSearchParams(useSearch());
7-
const [_, setLocation] = useLocation();
7+
const [, setLocation] = useLocation();
88
useEffect(() => {
99
const token = searchParams.get('token');
1010
if (token) {

client/src/page/feed.tsx

+22-18
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,24 @@
1-
import { useContext, useEffect, useRef, useState } from "react";
2-
import { Helmet } from "react-helmet";
3-
import { useTranslation } from "react-i18next";
1+
import {useContext, useEffect, useRef, useState} from "react";
2+
import {Helmet} from "react-helmet";
3+
import {useTranslation} from "react-i18next";
44
import ReactModal from "react-modal";
55
import Popup from "reactjs-popup";
6-
import { Link, useLocation } from "wouter";
7-
import { useAlert, useConfirm } from "../components/dialog";
8-
import { HashTag } from "../components/hashtag";
9-
import { Waiting } from "../components/loading";
10-
import { Markdown } from "../components/markdown";
11-
import { client } from "../main";
12-
import { ClientConfigContext } from "../state/config";
13-
import { ProfileContext } from "../state/profile";
14-
import { headersWithAuth } from "../utils/auth";
15-
import { siteName } from "../utils/constants";
16-
import { timeago } from "../utils/timeago";
17-
import { Button } from "../components/button";
18-
import { Tips } from "../components/tips";
19-
import { useLoginModal } from "../hooks/useLoginModal";
6+
import {Link, useLocation} from "wouter";
7+
import {useAlert, useConfirm} from "../components/dialog";
8+
import {HashTag} from "../components/hashtag";
9+
import {Waiting} from "../components/loading";
10+
import {Markdown} from "../components/markdown";
11+
import {client} from "../main";
12+
import {ClientConfigContext} from "../state/config";
13+
import {ProfileContext} from "../state/profile";
14+
import {headersWithAuth} from "../utils/auth";
15+
import {siteName} from "../utils/constants";
16+
import {timeago} from "../utils/timeago";
17+
import {Button} from "../components/button";
18+
import {Tips} from "../components/tips";
19+
import {useLoginModal} from "../hooks/useLoginModal";
2020
import mermaid from "mermaid";
21+
import {AdjacentSection} from "../components/adjacent_feed.tsx";
2122

2223
type Feed = {
2324
id: number;
@@ -39,14 +40,16 @@ type Feed = {
3940
uv: number;
4041
};
4142

43+
44+
4245
export function FeedPage({ id, TOC, clean }: { id: string, TOC: () => JSX.Element, clean: (id: string) => void }) {
4346
const { t } = useTranslation();
4447
const profile = useContext(ProfileContext);
4548
const [feed, setFeed] = useState<Feed>();
4649
const [error, setError] = useState<string>();
4750
const [headImage, setHeadImage] = useState<string>();
4851
const ref = useRef("");
49-
const [_, setLocation] = useLocation();
52+
const [, setLocation] = useLocation();
5053
const { showAlert, AlertUI } = useAlert();
5154
const { showConfirm, ConfirmUI } = useConfirm();
5255
const [top, setTop] = useState<number>(0);
@@ -296,6 +299,7 @@ export function FeedPage({ id, TOC, clean }: { id: string, TOC: () => JSX.Elemen
296299
</div>
297300
</div>
298301
</article>
302+
<AdjacentSection id={id} setError={setError}/>
299303
{feed && <Comments id={`${feed.id}`} />}
300304
<div className="h-16" />
301305
</main>

client/src/page/settings.tsx

+16-8
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,21 @@
11
import * as Switch from '@radix-ui/react-switch';
2-
import { ChangeEvent, useContext, useEffect, useRef, useState } from "react";
3-
import { useTranslation } from "react-i18next";
2+
import {ChangeEvent, useContext, useEffect, useRef, useState} from "react";
3+
import {useTranslation} from "react-i18next";
44
import ReactLoading from "react-loading";
55
import Modal from "react-modal";
6-
import { Button } from "../components/button.tsx";
7-
import { useAlert, useConfirm } from "../components/dialog.tsx";
8-
import { client, oauth_url } from "../main.tsx";
9-
import { ClientConfigContext, ConfigWrapper, defaultClientConfig, defaultClientConfigWrapper, defaultServerConfig, defaultServerConfigWrapper, ServerConfigContext } from "../state/config.tsx";
10-
import { headersWithAuth } from "../utils/auth.ts";
6+
import {Button} from "../components/button.tsx";
7+
import {useAlert, useConfirm} from "../components/dialog.tsx";
8+
import {client, oauth_url} from "../main.tsx";
9+
import {
10+
ClientConfigContext,
11+
ConfigWrapper,
12+
defaultClientConfig,
13+
defaultClientConfigWrapper,
14+
defaultServerConfig,
15+
defaultServerConfigWrapper,
16+
ServerConfigContext
17+
} from "../state/config.tsx";
18+
import {headersWithAuth} from "../utils/auth.ts";
1119
import '../utils/thumb.css';
1220

1321

@@ -81,7 +89,7 @@ export function Settings() {
8189
<div className="flex flex-col justify-center items-center">
8290
<ServerConfigContext.Provider value={serverConfig}>
8391
<ClientConfigContext.Provider value={clientConfig}>
84-
<main className="wauto rounded-2xl bg-w m-2 p-6" aria-label="正文">
92+
<main className="wauto rounded-2xl bg-w m-2 p-6" aria-label={t("main_content")}>
8593
<div className="flex flex-row items-center space-x-2">
8694
<h1 className="text-2xl font-bold t-primary">
8795
{t('settings.title')}

client/src/page/timeline.tsx

+10-9
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
import { useEffect, useRef, useState } from "react"
2-
import { Helmet } from 'react-helmet'
3-
import { Link } from "wouter"
4-
import { Waiting } from "../components/loading"
5-
import { client } from "../main"
6-
import { headersWithAuth } from "../utils/auth"
7-
import { siteName } from "../utils/constants"
8-
import { useTranslation } from "react-i18next";
1+
import {useEffect, useRef, useState} from "react"
2+
import {Helmet} from 'react-helmet'
3+
import {Link} from "wouter"
4+
import {Waiting} from "../components/loading"
5+
import {client} from "../main"
6+
import {headersWithAuth} from "../utils/auth"
7+
import {siteName} from "../utils/constants"
8+
import {useTranslation} from "react-i18next";
99

1010

1111
export function TimelinePage() {
@@ -63,7 +63,8 @@ export function TimelinePage() {
6363
</h1>
6464
<div className="w-full flex flex-col justify-center items-start my-4">
6565
{feeds[+year]?.map(({ id, title, createdAt }) => (
66-
<FeedItem key={id} id={id.toString()} title={title || t('untitled')} createdAt={new Date(createdAt)} />
66+
<FeedItem key={id} id={id.toString()} title={title || t('unlisted')}
67+
createdAt={new Date(createdAt)}/>
6768
))}
6869
</div>
6970
</div>

0 commit comments

Comments
 (0)