|
1 | 1 | 'use client' |
2 | 2 |
|
3 | | -import type { ReactNode } from 'react' |
| 3 | +import { type ReactNode, useState } from 'react' |
4 | 4 | import { Streamdown } from 'streamdown' |
5 | 5 | import 'streamdown/styles.css' |
6 | 6 | import { Avatar, AvatarFallback, AvatarImage, Chip, cn } from '@sim/emcn' |
7 | | -import { useChangelogReleases } from '@/app/(landing)/changelog/components/changelog-timeline/use-changelog-releases' |
8 | | -import type { ChangelogEntry } from '@/app/(landing)/changelog/types' |
| 7 | +import type { ChangelogEntry, GitHubRelease } from '@/app/(landing)/changelog/types' |
| 8 | +import { mapReleases, releasesEndpoint } from '@/app/(landing)/changelog/utils' |
9 | 9 |
|
10 | 10 | /** |
11 | 11 | * The changelog timeline - the single client leaf of the changelog page. Renders |
@@ -62,9 +62,35 @@ function formatDate(value: string): string { |
62 | 62 | } |
63 | 63 |
|
64 | 64 | export function ChangelogTimeline({ initialEntries }: ChangelogTimelineProps) { |
65 | | - const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = |
66 | | - useChangelogReleases(initialEntries) |
67 | | - const entries = data?.pages.flat() ?? initialEntries |
| 65 | + const [entries, setEntries] = useState<ChangelogEntry[]>(initialEntries) |
| 66 | + const [page, setPage] = useState<number>(1) |
| 67 | + const [loading, setLoading] = useState<boolean>(false) |
| 68 | + const [done, setDone] = useState<boolean>(false) |
| 69 | + |
| 70 | + const loadMore = async () => { |
| 71 | + if (loading || done) return |
| 72 | + setLoading(true) |
| 73 | + try { |
| 74 | + const nextPage = page + 1 |
| 75 | + // boundary-raw-fetch: external GitHub Releases API (cross-origin), not a same-origin contract |
| 76 | + const res = await fetch(releasesEndpoint(nextPage), { |
| 77 | + headers: { Accept: 'application/vnd.github+json' }, |
| 78 | + }) |
| 79 | + const releases = (await res.json()) as GitHubRelease[] |
| 80 | + const mapped = mapReleases(releases ?? []) |
| 81 | + |
| 82 | + if (mapped.length === 0) { |
| 83 | + setDone(true) |
| 84 | + } else { |
| 85 | + setEntries((prev) => [...prev, ...mapped]) |
| 86 | + setPage(nextPage) |
| 87 | + } |
| 88 | + } catch { |
| 89 | + setDone(true) |
| 90 | + } finally { |
| 91 | + setLoading(false) |
| 92 | + } |
| 93 | + } |
68 | 94 |
|
69 | 95 | return ( |
70 | 96 | <div className='flex flex-col gap-7'> |
@@ -187,10 +213,10 @@ export function ChangelogTimeline({ initialEntries }: ChangelogTimelineProps) { |
187 | 213 | ) |
188 | 214 | })} |
189 | 215 |
|
190 | | - {hasNextPage ? ( |
| 216 | + {!done ? ( |
191 | 217 | <div> |
192 | | - <Chip type='button' flush onClick={() => fetchNextPage()} disabled={isFetchingNextPage}> |
193 | | - {isFetchingNextPage ? 'Loading…' : 'Show more'} |
| 218 | + <Chip type='button' flush onClick={loadMore} disabled={loading}> |
| 219 | + {loading ? 'Loading…' : 'Show more'} |
194 | 220 | </Chip> |
195 | 221 | </div> |
196 | 222 | ) : null} |
|
0 commit comments