diff --git a/apps/www/components/Changelog/ChangelogMonthTabExplorer.tsx b/apps/www/components/Changelog/ChangelogMonthTabExplorer.tsx new file mode 100644 index 00000000000..6a56ff8525f --- /dev/null +++ b/apps/www/components/Changelog/ChangelogMonthTabExplorer.tsx @@ -0,0 +1,268 @@ +'use client' + +import type { ChangelogTimelineIndexItem } from '~/lib/changelog-github' +import { githubChangelogLabelFilterUrl, githubLabelHex } from '~/lib/changelog.utils' +import mdxComponents from '~/lib/mdx/mdxComponents' +import dayjs from 'dayjs' +import { GitCommit } from 'lucide-react' +import type { MDXRemoteSerializeResult } from 'next-mdx-remote' +import { MDXRemote } from 'next-mdx-remote' +import Link from 'next/link' +import { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader' + +import 'ui-patterns/ShimmeringLoader/index.css' + +import { cn } from 'ui' + +function groupByMonthSorted( + items: ChangelogTimelineIndexItem[] +): { key: string; label: string; entries: ChangelogTimelineIndexItem[] }[] { + const filtered = items.filter((i) => !i.title.includes('[d]')) + const map = new Map() + for (const item of filtered) { + const key = dayjs(item.sortDate).format('YYYY-MM') + if (!map.has(key)) map.set(key, []) + map.get(key)!.push(item) + } + return [...map.entries()] + .sort((a, b) => b[0].localeCompare(a[0])) + .map(([key, entries]) => ({ + key, + label: dayjs(`${key}-01`).format('MMM YYYY'), + entries, + })) +} + +type FetchedDoc = { + number: number + title: string + url: string + created_at: string + source: MDXRemoteSerializeResult | null + error?: string +} + +type Props = { + items: ChangelogTimelineIndexItem[] +} + +export function ChangelogMonthTabExplorer({ items }: Props) { + const groups = useMemo(() => groupByMonthSorted(items), [items]) + const [activeKey, setActiveKey] = useState(null) + const tabRefs = useRef>(new Map()) + const [docs, setDocs] = useState([]) + const [loadingDocs, setLoadingDocs] = useState(false) + + useEffect(() => { + if (activeKey === null && groups.length > 0) { + setActiveKey(groups[0]!.key) + } + }, [activeKey, groups]) + + const activeEntries = useMemo(() => { + if (!activeKey) return [] + return groups.find((g) => g.key === activeKey)?.entries ?? [] + }, [activeKey, groups]) + + useEffect(() => { + if (!activeKey || activeEntries.length === 0) { + setDocs([]) + setLoadingDocs(false) + return + } + + let cancelled = false + setLoadingDocs(true) + setDocs([]) + + void (async () => { + const rows: FetchedDoc[] = await Promise.all( + activeEntries.map(async (item) => { + try { + const res = await fetch(`/api/changelog-discussion/${item.number}`) + if (!res.ok) { + return { + number: item.number, + title: item.title, + url: item.url, + created_at: item.sortDate, + source: null, + error: res.status === 404 ? 'Not found' : 'Could not load', + } + } + const data = (await res.json()) as { + title: string + url: string + created_at: string + source: MDXRemoteSerializeResult + } + return { + number: item.number, + title: data.title, + url: data.url, + created_at: data.created_at, + source: data.source, + } + } catch { + return { + number: item.number, + title: item.title, + url: item.url, + created_at: item.sortDate, + source: null, + error: 'Network error', + } + } + }) + ) + if (cancelled) return + setDocs(rows) + setLoadingDocs(false) + })() + + return () => { + cancelled = true + setLoadingDocs(false) + } + }, [activeKey, activeEntries]) + + const setTabRef = useCallback((key: string, el: HTMLButtonElement | null) => { + if (el) tabRefs.current.set(key, el) + else tabRefs.current.delete(key) + }, []) + + useEffect(() => { + if (!activeKey) return + const el = tabRefs.current.get(activeKey) + el?.scrollIntoView({ behavior: 'smooth', inline: 'center', block: 'nearest' }) + }, [activeKey]) + + const indexByNumber = useMemo(() => { + const m = new Map() + for (const item of items) { + m.set(item.number, item) + } + return m + }, [items]) + + if (groups.length === 0) { + return

No changelog entries loaded.

+ } + + return ( +
+
+
+
+ {groups.map(({ key, label }) => { + const selected = key === activeKey + return ( + + ) + })} +
+
+
+ +
+ {loadingDocs && ( +
+ +
+ )} + + {!loadingDocs && activeEntries.length === 0 && ( +

Nothing in this month.

+ )} + + {!loadingDocs && activeEntries.length > 0 && ( +
+ {docs.map((doc) => { + const indexItem = indexByNumber.get(doc.number) + const labels = indexItem?.labels ?? [] + + return ( +
+
+
+
+ +
+
+ {doc.title && ( + + {doc.title} + + )} +

+ {dayjs(doc.created_at).format('MMM D, YYYY')} +

+ {labels.length > 0 && ( +
+ {labels.map((label) => ( + + + {label.name} + + + ))} +
+ )} + {doc.error && ( +

{doc.error}

+ )} +
+
+
+
+ {doc.source ? ( +
+ +
+ ) : ( + !doc.error &&

No content.

+ )} +
+
+ ) + })} +
+ )} +
+
+ ) +} diff --git a/apps/www/pages/changelog-v4.tsx b/apps/www/pages/changelog-v4.tsx new file mode 100644 index 00000000000..807cb1385a3 --- /dev/null +++ b/apps/www/pages/changelog-v4.tsx @@ -0,0 +1,61 @@ +import { ChangelogMonthTabExplorer } from '~/components/Changelog/ChangelogMonthTabExplorer' +import CTABanner from '~/components/CTABanner' +import DefaultLayout from '~/components/Layouts/Default' +import type { ChangelogTimelineIndexItem } from '~/lib/changelog-github' +import { getChangelogTimelineSortedIndex } from '~/lib/changelog-github' +import type { GetServerSideProps } from 'next' +import { NextSeo } from 'next-seo' +import Link from 'next/link' + +type PageProps = { + changelogIndex: ChangelogTimelineIndexItem[] +} + +export const getServerSideProps: GetServerSideProps = async ({ res }) => { + res.setHeader('Cache-Control', 'public, max-age=900, stale-while-revalidate=900') + try { + const changelogIndex = await getChangelogTimelineSortedIndex() + return { props: { changelogIndex } } + } catch (e) { + console.error(e) + return { props: { changelogIndex: [] } } + } +} + +export default function ChangelogV4MonthTabsPage({ changelogIndex }: PageProps) { + return ( + <> + + +
+
+

Changelog

+

+ Month tabs experiment — strip stays sticky while you scroll. Full entries load for + the selected month (same MDX as the main changelog).{' '} + + v3 timeline + + {' · '} + + v3 modal + + . +

+
+ + +
+ +
+ + ) +}