Skip to content

Commit d2af6db

Browse files
committed
feat: topic pages
Signed-off-by: Innei <[email protected]>
1 parent fef1be0 commit d2af6db

File tree

13 files changed

+287
-19
lines changed

13 files changed

+287
-19
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { dehydrate } from '@tanstack/react-query'
2+
import type { Metadata } from 'next'
3+
4+
import { QueryHydrate } from '~/components/common/QueryHydrate'
5+
import { NormalContainer } from '~/components/layout/container/Normal'
6+
import { isShallowEqualArray } from '~/lib/_'
7+
import { attachUA } from '~/lib/attach-ua'
8+
import { getQueryClient } from '~/utils/query-client.server'
9+
10+
import { getTopicQuery } from './query'
11+
12+
export const generateMetadata = async (
13+
props: NextPageParams<{
14+
slug: string
15+
}>,
16+
) => {
17+
attachUA()
18+
const queryClient = getQueryClient()
19+
20+
const query = getTopicQuery(props.params.slug)
21+
22+
const data = await queryClient.fetchQuery(query)
23+
24+
return {
25+
title: `专栏 · ${data.name}`,
26+
} satisfies Metadata
27+
}
28+
export default async function Layout(
29+
props: NextPageParams<{
30+
slug: string
31+
}>,
32+
) {
33+
attachUA()
34+
const queryClient = getQueryClient()
35+
const query = getTopicQuery(props.params.slug)
36+
const queryKey = query.queryKey
37+
await queryClient.fetchQuery(query)
38+
return (
39+
<QueryHydrate
40+
state={dehydrate(queryClient, {
41+
shouldDehydrateQuery: (query) => {
42+
// @ts-expect-error
43+
return isShallowEqualArray(query.queryKey, queryKey)
44+
},
45+
})}
46+
>
47+
<NormalContainer>{props.children}</NormalContainer>
48+
</QueryHydrate>
49+
)
50+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
'use client'
2+
3+
import { useInfiniteQuery, useQuery } from '@tanstack/react-query'
4+
import Link from 'next/link'
5+
import { useParams } from 'next/navigation'
6+
7+
import { TimelineList } from '~/components/ui/list/TimelineList'
8+
import { Loading } from '~/components/ui/loading'
9+
import { BottomToUpSoftScaleTransitionView } from '~/components/ui/transition/BottomToUpSoftScaleTransitionView'
10+
import { BottomToUpTransitionView } from '~/components/ui/transition/BottomToUpTransitionView'
11+
import { routeBuilder, Routes } from '~/lib/route-builder'
12+
import { apiClient } from '~/utils/request'
13+
14+
import { getTopicQuery } from './query'
15+
16+
export default function Page() {
17+
const { slug } = useParams()
18+
const { data } = useQuery({
19+
...getTopicQuery(slug),
20+
enabled: false,
21+
})
22+
23+
const {
24+
data: notes,
25+
isLoading,
26+
fetchNextPage,
27+
} = useInfiniteQuery({
28+
queryKey: ['topicId', data?.id],
29+
30+
enabled: !!data,
31+
queryFn: async ({ queryKey, pageParam }) => {
32+
const [, topicId] = queryKey
33+
if (!topicId) throw new Error('topicId is not ready :(')
34+
return await apiClient.note.getNoteByTopicId(topicId, pageParam)
35+
},
36+
37+
getNextPageParam: (lastPage) =>
38+
lastPage.pagination.hasNextPage
39+
? lastPage.pagination.currentPage + 1
40+
: undefined,
41+
})
42+
if (!data) throw new Error('topic data is lost :(')
43+
const { name } = data
44+
45+
if (isLoading) return <Loading useDefaultLoadingText />
46+
return (
47+
<BottomToUpSoftScaleTransitionView>
48+
<header className="prose">
49+
<h1>专栏 - {name}</h1>
50+
</header>
51+
52+
<main className="mt-10 text-zinc-950/80 dark:text-zinc-50/80">
53+
<TimelineList>
54+
{notes?.pages.map((page) =>
55+
page.data.map((child, i) => {
56+
const date = new Date(child.created)
57+
58+
return (
59+
<BottomToUpTransitionView
60+
key={child.id}
61+
delay={700 + 50 * i}
62+
as="li"
63+
className="flex min-w-0 items-center justify-between leading-loose"
64+
>
65+
<Link
66+
prefetch={false}
67+
target="_blank"
68+
href={routeBuilder(Routes.Note, {
69+
id: child.nid,
70+
})}
71+
className="min-w-0 truncate"
72+
>
73+
{child.title}
74+
</Link>
75+
<span className="meta">
76+
{(date.getMonth() + 1).toString().padStart(2, '0')}/
77+
{date.getDate().toString().padStart(2, '0')}/
78+
{date.getFullYear()}
79+
</span>
80+
</BottomToUpTransitionView>
81+
)
82+
}),
83+
)}
84+
</TimelineList>
85+
</main>
86+
</BottomToUpSoftScaleTransitionView>
87+
)
88+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { defineQuery } from '~/queries/helper'
2+
import { apiClient } from '~/utils/request'
3+
4+
export const getTopicQuery = (topicSlug: string) =>
5+
defineQuery({
6+
queryKey: ['topic', topicSlug],
7+
queryFn: async ({ queryKey }) => {
8+
const [_, slug] = queryKey
9+
return (await apiClient.topic.getTopicBySlug(slug)).$serialized
10+
},
11+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { dehydrate } from '@tanstack/react-query'
2+
import type { Metadata } from 'next'
3+
4+
import { QueryHydrate } from '~/components/common/QueryHydrate'
5+
import { NormalContainer } from '~/components/layout/container/Normal'
6+
import { isShallowEqualArray } from '~/lib/_'
7+
import { attachUA } from '~/lib/attach-ua'
8+
import { getQueryClient } from '~/utils/query-client.server'
9+
10+
import { topicsQuery } from './query'
11+
12+
export const metadata: Metadata = {
13+
title: '专栏',
14+
}
15+
16+
export default async function Layout(
17+
props: NextPageParams<{
18+
slug: string
19+
}>,
20+
) {
21+
attachUA()
22+
const queryClient = getQueryClient()
23+
24+
await queryClient.fetchQuery(topicsQuery)
25+
return (
26+
<QueryHydrate
27+
state={dehydrate(queryClient, {
28+
shouldDehydrateQuery: (query) => {
29+
// @ts-expect-error
30+
return isShallowEqualArray(query.queryKey, topicsQuery.queryKey)
31+
},
32+
})}
33+
>
34+
<NormalContainer>{props.children}</NormalContainer>
35+
</QueryHydrate>
36+
)
37+
}
+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
'use client'
2+
3+
import { useQuery } from '@tanstack/react-query'
4+
import Link from 'next/link'
5+
6+
import { TimelineList } from '~/components/ui/list/TimelineList'
7+
import { BottomToUpSoftScaleTransitionView } from '~/components/ui/transition/BottomToUpSoftScaleTransitionView'
8+
import { BottomToUpTransitionView } from '~/components/ui/transition/BottomToUpTransitionView'
9+
import { routeBuilder, Routes } from '~/lib/route-builder'
10+
11+
import { topicsQuery } from './query'
12+
13+
export default function Page() {
14+
const { data } = useQuery({
15+
...topicsQuery,
16+
enabled: false,
17+
})
18+
if (!data) throw new Error('topic data is lost :(')
19+
20+
return (
21+
<BottomToUpSoftScaleTransitionView>
22+
<header className="prose">
23+
<h1>专栏</h1>
24+
</header>
25+
26+
<main className="mt-10 text-zinc-950/80 dark:text-zinc-50/80">
27+
<TimelineList>
28+
{data.map((item, i) => {
29+
const date = new Date(item.created)
30+
31+
return (
32+
<BottomToUpTransitionView
33+
key={item.id}
34+
delay={700 + 50 * i}
35+
as="li"
36+
className="flex min-w-0 items-center justify-between leading-loose"
37+
>
38+
<Link
39+
prefetch={false}
40+
target="_blank"
41+
href={routeBuilder(Routes.NoteTopic, {
42+
slug: item.slug,
43+
})}
44+
className="min-w-0 truncate"
45+
>
46+
{item.name}
47+
</Link>
48+
<span className="meta">
49+
{(date.getMonth() + 1).toString().padStart(2, '0')}/
50+
{date.getDate().toString().padStart(2, '0')}/
51+
{date.getFullYear()}
52+
</span>
53+
</BottomToUpTransitionView>
54+
)
55+
})}
56+
</TimelineList>
57+
</main>
58+
</BottomToUpSoftScaleTransitionView>
59+
)
60+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { defineQuery } from '~/queries/helper'
2+
import { apiClient } from '~/utils/request'
3+
4+
export const topicsQuery = defineQuery({
5+
queryKey: ['topic'],
6+
queryFn: async () => {
7+
return (await apiClient.topic.getAll()).data
8+
},
9+
})

src/app/(page-detail)/layout.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,6 @@ import type { PropsWithChildren } from 'react'
22

33
import { Container } from './Container'
44

5-
export default (props: PropsWithChildren) => {
5+
export default function Page(props: PropsWithChildren<unknown>) {
66
return <Container>{props.children}</Container>
77
}

src/app/categories/[slug]/page.tsx

+5-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { useParams } from 'next/navigation'
77
import { TimelineList } from '~/components/ui/list/TimelineList'
88
import { BottomToUpSoftScaleTransitionView } from '~/components/ui/transition/BottomToUpSoftScaleTransitionView'
99
import { BottomToUpTransitionView } from '~/components/ui/transition/BottomToUpTransitionView'
10+
import { routeBuilder, Routes } from '~/lib/route-builder'
1011

1112
import { getPageBySlugQuery } from './query'
1213

@@ -46,7 +47,10 @@ export default function Page() {
4647
<Link
4748
prefetch={false}
4849
target="_blank"
49-
href={`/posts/${slug}/${child.slug}`}
50+
href={routeBuilder(Routes.Post, {
51+
slug: child.slug,
52+
category: slug,
53+
})}
5054
className="min-w-0 truncate"
5155
>
5256
{child.title}

src/app/notes/error.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ export default ({ error, reset }: { error: Error; reset: () => void }) => {
3434
)
3535
}
3636

37-
if (code === 404) {
37+
if (code === 404 || code === 422) {
3838
return (
3939
<Paper className="flex flex-col items-center">
4040
<NotFound404 />

src/app/notes/page.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { apiClient } from '~/utils/request'
1111

1212
import { Paper } from './Paper'
1313

14-
export default () => {
14+
export default function Page() {
1515
const { data } = useQuery(
1616
['note', 'latest'],
1717
async () => (await apiClient.note.getLatest()).$serialized,
+3-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
export const Content: Component = ({ children }) => {
22
return (
3-
<main className="relative z-[1] px-4 pt-[4.5rem] md:px-0">{children}</main>
3+
<main className="relative z-[1] min-h-[calc(100vh-17.5rem)] px-4 pt-[4.5rem] md:px-0">
4+
{children}
5+
</main>
46
)
57
}

src/components/widgets/note/NoteTopicInfo.tsx

+2
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,5 @@ export const NoteTopicInfo = memo(() => {
3131
</>
3232
)
3333
})
34+
35+
NoteTopicInfo.displayName = 'NoteTopicInfo'

src/lib/route-builder.ts

+19-14
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,22 @@
11
export enum Routes {
22
Home = '',
3+
34
Posts = '/posts',
45
Post = '/posts/',
6+
57
Notes = '/notes',
68
Note = '/notes/',
7-
NoteTopics = '/topics',
8-
NoteTopic = '/topics/',
9+
NoteTopics = '/notes/topics',
10+
NoteTopic = '/notes/topics/',
11+
912
Timelime = '/timeline',
13+
1014
Login = '/login',
15+
1116
Page = '/',
17+
18+
Categories = '/categories',
19+
Category = '/categories/',
1220
}
1321

1422
type Noop = never
@@ -34,11 +42,8 @@ type TimelineParams = {
3442
type: 'note' | 'post' | 'all'
3543
selectId?: string
3644
}
37-
type NoteTopicParams = {
38-
slug: string
39-
}
4045

41-
type PageParams = {
46+
type OnlySlug = {
4247
slug: string
4348
}
4449
export type RouteParams<T extends Routes> = T extends Routes.Home
@@ -54,11 +59,13 @@ export type RouteParams<T extends Routes> = T extends Routes.Home
5459
: T extends Routes.Timelime
5560
? TimelineParams
5661
: T extends Routes.NoteTopic
57-
? NoteTopicParams
62+
? OnlySlug
5863
: T extends Routes.NoteTopics
5964
? Noop
6065
: T extends Routes.Page
61-
? PageParams
66+
? OnlySlug
67+
: T extends Routes.Category
68+
? OnlySlug
6269
: never
6370

6471
export const routeBuilder = <T extends Routes>(
@@ -90,16 +97,14 @@ export const routeBuilder = <T extends Routes>(
9097
href += `?${new URLSearchParams(p as any).toString()}`
9198
break
9299
}
93-
case Routes.NoteTopic: {
94-
const p = params as NoteTopicParams
95-
href += p.slug
96-
break
97-
}
100+
case Routes.NoteTopic:
101+
case Routes.Category:
98102
case Routes.Page: {
99-
const p = params as PageParams
103+
const p = params as OnlySlug
100104
href += p.slug
101105
break
102106
}
107+
103108
case Routes.Home: {
104109
href = '/'
105110
break

0 commit comments

Comments
 (0)