Skip to content

Commit 2b91bb3

Browse files
authored
Merge pull request #27 from immccn123/snapshot-tl
feat: 用户名历史时间线
2 parents 2be8261 + a617ef8 commit 2b91bb3

File tree

13 files changed

+353
-12
lines changed

13 files changed

+353
-12
lines changed

packages/archive/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
"main": "dist/server.js",
1212
"scripts": {
1313
"build": "tsc",
14+
"start": "node dist/server.js",
1415
"test": "echo \"Error: no test specified\" && exit 1"
1516
},
1617
"dependencies": {

packages/archive/src/lib/activity.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import type { BaseLogger } from "pino";
44
import type { PrismaClient, PrismaPromise } from "@prisma/client";
55
import { getResponse } from "./parser";
66
import type { UserSummary } from "./user";
7-
import { upsertUserSnapshotHook } from "./user";
7+
import { upsertUserSnapshot } from "./user";
88

99
export interface Activity {
1010
content: string;
@@ -37,7 +37,7 @@ export async function saveActivityPage(
3737
// eslint-disable-next-line no-restricted-syntax
3838
for (const { user } of res.feeds.result) {
3939
// eslint-disable-next-line no-await-in-loop
40-
await upsertUserSnapshotHook(prisma, user);
40+
await upsertUserSnapshot(prisma, user);
4141
}
4242

4343
res.feeds.result.forEach((activity) => {

packages/archive/src/lib/judgement.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { PrismaClient, PrismaPromise } from "@prisma/client";
22
import type { BaseLogger } from "pino";
33
import { getResponse } from "./parser";
4-
import { UserSummary, upsertUserSnapshotHook } from "./user";
4+
import { UserSummary, upsertUserSnapshot } from "./user";
55

66
interface JudgementBody {
77
user: UserSummary;
@@ -43,7 +43,7 @@ export default async function saveJudgements(
4343
// eslint-disable-next-line no-restricted-syntax
4444
for (const judgement of judgements) {
4545
// eslint-disable-next-line no-await-in-loop
46-
await upsertUserSnapshotHook(prisma, judgement.user);
46+
await upsertUserSnapshot(prisma, judgement.user);
4747
if (new Date(judgement.time * 1000) <= latestJudgement.time) break;
4848
operations.push(
4949
prisma.judgement.upsert({

packages/archive/src/lib/list.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,11 @@ export default async function getPostList(
5353
await prisma.post.findMany({
5454
select: {
5555
id: true,
56-
replies: { select: { id: true }, orderBy: { id: "desc" }, take: 1 },
56+
replies: {
57+
select: { id: true },
58+
orderBy: { id: "desc" },
59+
take: 1,
60+
},
5761
},
5862
where: {
5963
id: {

packages/archive/src/lib/paste.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { BaseLogger } from "pino";
22
import type { PrismaClient } from "@prisma/client";
33
import { getResponse } from "./parser";
44
import { type UserSummary } from "./user";
5-
import { upsertUserSnapshotHook } from "./user";
5+
import { upsertUserSnapshot } from "./user";
66

77
interface Paste {
88
data: string;
@@ -49,7 +49,7 @@ export default async function savePaste(
4949
}
5050
if (json.code !== 200) throw Error(json.currentData.errorMessage);
5151
const { paste } = json.currentData;
52-
await upsertUserSnapshotHook(prisma, paste.user);
52+
await upsertUserSnapshot(prisma, paste.user);
5353
await prisma.$transaction([
5454
prisma.paste.upsert({
5555
where: { id: paste.id },

packages/archive/src/lib/post.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import type { BroadcastOperator } from "socket.io";
44
import type { PostSnapshot, PrismaClient } from "@prisma/client";
55
import { getResponse } from "./parser";
66
import type { ServerToClientEvents } from "../plugins/socket.io";
7-
import { UserSummary, upsertUserSnapshotHook } from "./user";
7+
import { UserSummary, upsertUserSnapshot } from "./user";
88

99
const PAGES_PER_SAVE = parseInt(process.env.PAGES_PER_SAVE ?? "64", 10);
1010
export const emitters: Record<number, EventEmitter> = {};
@@ -73,7 +73,7 @@ export async function savePost(
7373
// eslint-disable-next-line no-restricted-syntax
7474
for (const { author } of replies) {
7575
// eslint-disable-next-line no-await-in-loop
76-
await upsertUserSnapshotHook(prisma, author);
76+
await upsertUserSnapshot(prisma, author);
7777
}
7878
allReplies = [...allReplies, ...replies];
7979
};
@@ -129,7 +129,7 @@ export async function savePost(
129129
const { post, replies, forum } = (await fetchPage(1)).currentData;
130130
const postTime = new Date(post.time * 1000);
131131

132-
await upsertUserSnapshotHook(prisma, post.author);
132+
await upsertUserSnapshot(prisma, post.author);
133133

134134
await prisma.$transaction(
135135
async (tx) => {

packages/archive/src/lib/user.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ export interface UserSummary {
1111
isRoot?: true;
1212
}
1313

14-
export const upsertUserSnapshotHook = async (
14+
export const upsertUserSnapshot = async (
1515
prisma: PrismaClient,
1616
user: UserSummary,
1717
) => {

packages/viewer/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
"rehype-katex": "^7.0.0",
2727
"remark-luogu-flavor": "^1.0.0",
2828
"remark-math": "^6.0.0",
29+
"rsuite": "^5.68.1",
2930
"socket.io-client": "^4.7.5",
3031
"swr": "^2.2.5"
3132
},
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
"use client";
2+
3+
import Spinner from "@/components/Spinner";
4+
import UserInfo from "@/components/UserInfo";
5+
import fetcher from "@/lib/fetcher";
6+
import { UserSnapshot } from "@prisma/client";
7+
import Timeline from "rsuite/Timeline";
8+
import useSWRInfinite from "swr/infinite";
9+
import "rsuite/Timeline/styles/index.css";
10+
import { useState } from "react";
11+
import { BsChevronDown, BsChevronUp, BsThreeDots } from "react-icons/bs";
12+
13+
const PER_PAGE = 15;
14+
15+
interface PageData {
16+
snapshots: UserSnapshot[];
17+
nextCursor: string;
18+
}
19+
20+
export default function SnapshotTimeline({ uid }: { uid: number }) {
21+
const [open, setOpen] = useState(false);
22+
23+
const { data, size, setSize, isValidating } = useSWRInfinite<PageData>(
24+
(_pageIndex, prev: PageData | null) => {
25+
if (prev && !prev.nextCursor) return null;
26+
let res = `/user/${uid}/snapshots?limit=${PER_PAGE}`;
27+
if (prev) res += `&offset=${prev.nextCursor}`;
28+
return res;
29+
},
30+
fetcher,
31+
);
32+
33+
const timeline = (
34+
<>
35+
<Timeline endless isItemActive={Timeline.ACTIVE_FIRST}>
36+
{data?.map((dat) =>
37+
dat.snapshots?.map((snapshot) => (
38+
<Timeline.Item>
39+
<UserInfo
40+
user={{
41+
id: uid,
42+
userSnapshots: [snapshot],
43+
}}
44+
noHref
45+
/>
46+
<br />
47+
截至 {new Date(snapshot.until).toLocaleString()}
48+
</Timeline.Item>
49+
)),
50+
)}
51+
</Timeline>
52+
{isValidating && <Spinner className="mt-5" />}
53+
{!isValidating &&
54+
data &&
55+
data[data.length - 1].snapshots.length === PER_PAGE && (
56+
<button
57+
className="btn btn-link w-100 rounded-4 py-2x py-md-3 text-center text-decoration-none"
58+
onClick={() => {
59+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
60+
setSize(size + 1);
61+
}}
62+
type="button"
63+
>
64+
加载更多
65+
<BsThreeDots
66+
className="ms-1"
67+
style={{ position: "relative", top: "-.09em" }}
68+
/>
69+
</button>
70+
)}
71+
</>
72+
);
73+
74+
return (
75+
<>
76+
<div className="d-md-none">
77+
<button
78+
type="button"
79+
className="btn btn-link w-100 d-md-none"
80+
onMouseDown={() => setOpen(!open)}
81+
style={{ textDecoration: "none" }}
82+
>
83+
用户名历史 {open ? <BsChevronUp /> : <BsChevronDown />}
84+
</button>
85+
86+
{open && (
87+
<>
88+
{/* 对于缺失空白的一个并不优雅的解决方案 */}
89+
<div style={{ height: "10px" }} className="d-md-none" />
90+
{timeline}
91+
</>
92+
)}
93+
</div>
94+
<div className="d-md-block d-none">{timeline}</div>
95+
</>
96+
);
97+
}

packages/viewer/src/app/user/[uid]/layout.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import "@/components/markdown.css";
77
import { selectUser } from "@/lib/user";
88
import TabNavigation from "./TabNavigation";
99
import UserStatistics from "./UserStatistics";
10+
import SnapshotTimeline from "./SnapshotTimeline";
1011

1112
export async function generateMetadata({
1213
params,
@@ -65,6 +66,10 @@ export default async function Layout({
6566
</a>
6667
</div>
6768
</div>
69+
70+
<div className="rounded-4 shadow-bssb px-4 py-4 text-center mt-3">
71+
<SnapshotTimeline uid={user.id} />
72+
</div>
6873
</div>
6974
<div className="col-md-8 col-12">
7075
<TabNavigation uid={params.uid} />

0 commit comments

Comments
 (0)