From f0e47c7581ca22cdd57096c2dc5475850952f863 Mon Sep 17 00:00:00 2001 From: kurbycchua <50901466+kurbycchua@users.noreply.github.com> Date: Fri, 26 Sep 2025 14:26:24 +0800 Subject: [PATCH 001/257] Test What's New --- pages/guides/whats-new.mdx | 303 +++++++++++++++++++++++++++++++++++++ 1 file changed, 303 insertions(+) create mode 100644 pages/guides/whats-new.mdx diff --git a/pages/guides/whats-new.mdx b/pages/guides/whats-new.mdx new file mode 100644 index 0000000000..b9dca90384 --- /dev/null +++ b/pages/guides/whats-new.mdx @@ -0,0 +1,303 @@ +--- +title: "What's New" +description: "Auto-updating highlights from Mixpanel Changelogs" +--- + +import React, {useEffect, useMemo, useState} from 'react' + +/** + * What's New — auto-updating from changelogs in mixpanel/docs + * Save as: pages/whats-new.mdx + * + * How it works: + * - Fetches directory listing from the GitHub API for /pages/changelogs + * - Sorts by filename date prefix (YYYY-MM-DD) desc + * - Fetches the top N raw MDX files and parses frontmatter (title, video, thumbnail, etc.) + * - Renders either Cards (grid) or Timeline (by month) with a toggle + * + * Notes: + * - Public GitHub API (rate-limited ~60 req/hr per IP). We keep requests minimal. + * - Frontmatter is optional; we fallback to slug-derived titles and first paragraph if needed. + * - To bump counts/scope, tweak FETCH_COUNT / DEFAULT_LIMIT below. + */ + +const REPO_LIST_URL = + 'https://api.github.com/repos/mixpanel/docs/contents/pages/changelogs?ref=main' + +const DEFAULT_LIMIT = 10 // default number of items shown in Cards view +const FETCH_COUNT = 30 // number of files to fetch & parse (supports Timeline/grouping) + +function PageHeader({count30d}) { + return ( +
+

What’s New

+

+ Fresh from our Changelog. + We shipped {count30d} updates in the last 30 days. +

+
+ ) +} + +function ViewToggle({mode, setMode}) { + return ( +
+ + +
+ ) +} + +// -------- Data fetching & parsing -------- + +async function fetchChangelogIndex() { + const res = await fetch(REPO_LIST_URL, { + headers: {'Accept': 'application/vnd.github.v3+json'} + }) + if (!res.ok) throw new Error(`GitHub API error: ${res.status}`) + const files = await res.json() + return (Array.isArray(files) ? files : []).filter(f => f.name.endsWith('.mdx')) +} + +function parseDateFromName(name) { + const m = name.match(/^(\d{4}-\d{2}-\d{2})-/) + return m ? m[1] : null +} + +async function fetchFrontmatter(file) { + // we only need frontmatter and an excerpt; fetch full then strip + const rawRes = await fetch(file.download_url) + const raw = await rawRes.text() + const fm = parseFrontmatter(raw) + const summary = (fm.description ?? extractFirstParagraph(raw))?.slice(0, 260) ?? '' + const dateStr = fm.date ?? parseDateFromName(file.name) ?? '' + const url = `/changelogs/${file.name.replace(/\.mdx$/, '')}` + return { + title: fm.title || humanizeSlug(file.name), + date: dateStr, + description: summary, + thumbnail: fm.thumbnail || '', + video: fm.video || '', + category: fm.category || fm.type || '', + url + } +} + +function parseFrontmatter(md) { + const m = md.match(/^---\n([\s\S]*?)\n---/) + if (!m) return {} + const out = {} + let key = null + for (const lineRaw of m[1].split('\n')) { + const line = lineRaw.trim() + if (!line) continue + const kv = line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/) + if (kv) { + key = kv[1] + let val = kv[2] + if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) { + val = val.slice(1, -1) + } + out[key] = val + } else if (key && line.startsWith('- ')) { + // naive list support + if (!Array.isArray(out[key])) out[key] = [] + out[key].push(line.slice(2).trim()) + } + } + return out +} + +function extractFirstParagraph(md) { + // strip frontmatter + md = md.replace(/^---[\s\S]*?---/, '').trim() + // skip imports/exports and blank lines, collect a few lines + const lines = md.split('\n').filter(Boolean).filter(l => !l.startsWith('import ') && !l.startsWith('export ')) + return lines.slice(0, 4).join(' ') +} + +function humanizeSlug(name) { + const noExt = name.replace(/\.mdx$/, '') + const noDate = noExt.replace(/^\d{4}-\d{2}-\d{2}-/, '') + return noDate.replace(/-/g, ' ').replace(/\b\w/g, m => m.toUpperCase()) +} + +// -------- UI helpers -------- + +function isNew(dateStr) { + const d = new Date(dateStr) + if (isNaN(d)) return false + const days = (Date.now() - d.getTime()) / (1000*60*60*24) + return days <= 14 +} + +function fmtDate(dateStr) { + const d = new Date(dateStr) + if (isNaN(d)) return dateStr || '' + return d.toLocaleDateString(undefined, {year:'numeric', month:'short', day:'numeric'}) +} + +function monthKey(dateStr) { + const d = new Date(dateStr) + if (isNaN(d)) return 'Unknown' + return d.toLocaleDateString(undefined, {year:'numeric', month:'long'}) +} + +function countLast30(items) { + const cutoff = Date.now() - 30*24*60*60*1000 + return items.filter(i => !isNaN(new Date(i.date)) && new Date(i.date).getTime() >= cutoff).length +} + +// -------- Cards -------- + +function Card({item}) { + return ( + +
+ {item.thumbnail ? ( + /* eslint-disable @next/next/no-img-element */ + + ) : item.video ? ( +
🎬
+ ) : ( +
+ )} +
+
+ {fmtDate(item.date)} + {item.category && • {item.category}} + {isNew(item.date) && NEW} +
+

{item.title}

+ {item.description &&

{item.description}

} +
+
+
+ ) +} + +function CardsView({items, limit=DEFAULT_LIMIT}) { + const shown = items.slice(0, limit) + return ( +
+ {shown.map((i) => )} +
+ ) +} + +// -------- Timeline -------- + +function Timeline({items}) { + const groups = useMemo(() => { + const map = new Map() + for (const i of items) { + const k = monthKey(i.date) + if (!map.has(k)) map.set(k, []) + map.get(k).push(i) + } + // sort groups by most recent month + return Array.from(map.entries()).sort((a,b) => { + const da = new Date(a[0]) + const db = new Date(b[0]) + return db - da + }) + }, [items]) + + return ( +
+ {groups.map(([month, arr]) => ( +
+

{month} ({arr.length})

+ +
+ ))} +
+ ) +} + +// -------- Page -------- + +export default function WhatsNewPage() { + const [mode, setMode] = useState('cards') + const [items, setItems] = useState([]) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + + useEffect(() => { + let cancelled = false + ;(async () => { + try { + const index = await fetchChangelogIndex() + // Sort by filename desc (YYYY-MM-DD-*.mdx lexicographically works) + index.sort((a,b) => b.name.localeCompare(a.name)) + const toFetch = index.slice(0, FETCH_COUNT) + const results = [] + for (const f of toFetch) { + try { + results.push(await fetchFrontmatter(f)) + } catch (e) { + // ignore a single post failure + console.warn('changelog parse fail', f.name, e) + } + } + if (!cancelled) setItems(results.filter(Boolean)) + } catch (e) { + if (!cancelled) setError(e?.message || String(e)) + } finally { + if (!cancelled) setLoading(false) + } + })() + return () => { cancelled = true } + }, []) + + const shipped30d = useMemo(() => countLast30(items), [items]) + + return ( +
+ + + {loading &&
Loading latest updates…
} + {error &&
Error: {error}
} + {!loading && !error && ( + mode === 'cards' + ? + : + )} + +
+ Don’t see what you’re looking for? See the full Changelog. +
+ + {/* No-JS fallback */} + +
+ ) +} From 97ecb2959fa6c4277f540c8624c3991bfa12d247 Mon Sep 17 00:00:00 2001 From: kurbycchua <50901466+kurbycchua@users.noreply.github.com> Date: Fri, 26 Sep 2025 14:45:56 +0800 Subject: [PATCH 002/257] Update whats-new.mdx --- pages/guides/whats-new.mdx | 191 +++++-------------------------------- 1 file changed, 24 insertions(+), 167 deletions(-) diff --git a/pages/guides/whats-new.mdx b/pages/guides/whats-new.mdx index b9dca90384..b5a70137c7 100644 --- a/pages/guides/whats-new.mdx +++ b/pages/guides/whats-new.mdx @@ -3,31 +3,17 @@ title: "What's New" description: "Auto-updating highlights from Mixpanel Changelogs" --- -import React, {useEffect, useMemo, useState} from 'react' +import { useEffect, useMemo, useState } from 'react' -/** - * What's New — auto-updating from changelogs in mixpanel/docs - * Save as: pages/whats-new.mdx - * - * How it works: - * - Fetches directory listing from the GitHub API for /pages/changelogs - * - Sorts by filename date prefix (YYYY-MM-DD) desc - * - Fetches the top N raw MDX files and parses frontmatter (title, video, thumbnail, etc.) - * - Renders either Cards (grid) or Timeline (by month) with a toggle - * - * Notes: - * - Public GitHub API (rate-limited ~60 req/hr per IP). We keep requests minimal. - * - Frontmatter is optional; we fallback to slug-derived titles and first paragraph if needed. - * - To bump counts/scope, tweak FETCH_COUNT / DEFAULT_LIMIT below. - */ +/* Save as: pages/guides/whats-new.mdx */ const REPO_LIST_URL = 'https://api.github.com/repos/mixpanel/docs/contents/pages/changelogs?ref=main' -const DEFAULT_LIMIT = 10 // default number of items shown in Cards view -const FETCH_COUNT = 30 // number of files to fetch & parse (supports Timeline/grouping) +const DEFAULT_LIMIT = 10 +const FETCH_COUNT = 30 -function PageHeader({count30d}) { +function PageHeader({ count30d }) { return (

What’s New

@@ -39,28 +25,28 @@ function PageHeader({count30d}) { ) } -function ViewToggle({mode, setMode}) { +function ViewToggle({ mode, setMode }) { return (
) } -// -------- Data fetching & parsing -------- +/* -------- Data fetching & parsing -------- */ async function fetchChangelogIndex() { const res = await fetch(REPO_LIST_URL, { - headers: {'Accept': 'application/vnd.github.v3+json'} + headers: { Accept: 'application/vnd.github.v3+json' } }) if (!res.ok) throw new Error(`GitHub API error: ${res.status}`) const files = await res.json() @@ -73,7 +59,6 @@ function parseDateFromName(name) { } async function fetchFrontmatter(file) { - // we only need frontmatter and an excerpt; fetch full then strip const rawRes = await fetch(file.download_url) const raw = await rawRes.text() const fm = parseFrontmatter(raw) @@ -108,7 +93,6 @@ function parseFrontmatter(md) { } out[key] = val } else if (key && line.startsWith('- ')) { - // naive list support if (!Array.isArray(out[key])) out[key] = [] out[key].push(line.slice(2).trim()) } @@ -117,10 +101,11 @@ function parseFrontmatter(md) { } function extractFirstParagraph(md) { - // strip frontmatter md = md.replace(/^---[\s\S]*?---/, '').trim() - // skip imports/exports and blank lines, collect a few lines - const lines = md.split('\n').filter(Boolean).filter(l => !l.startsWith('import ') && !l.startsWith('export ')) + const lines = md + .split('\n') + .filter(Boolean) + .filter(l => !l.startsWith('import ') && !l.startsWith('export ')) return lines.slice(0, 4).join(' ') } @@ -130,40 +115,39 @@ function humanizeSlug(name) { return noDate.replace(/-/g, ' ').replace(/\b\w/g, m => m.toUpperCase()) } -// -------- UI helpers -------- +/* -------- UI helpers -------- */ function isNew(dateStr) { const d = new Date(dateStr) if (isNaN(d)) return false - const days = (Date.now() - d.getTime()) / (1000*60*60*24) + const days = (Date.now() - d.getTime()) / (1000 * 60 * 60 * 24) return days <= 14 } function fmtDate(dateStr) { const d = new Date(dateStr) if (isNaN(d)) return dateStr || '' - return d.toLocaleDateString(undefined, {year:'numeric', month:'short', day:'numeric'}) + return d.toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' }) } function monthKey(dateStr) { const d = new Date(dateStr) if (isNaN(d)) return 'Unknown' - return d.toLocaleDateString(undefined, {year:'numeric', month:'long'}) + return d.toLocaleDateString(undefined, { year: 'numeric', month: 'long' }) } function countLast30(items) { - const cutoff = Date.now() - 30*24*60*60*1000 + const cutoff = Date.now() - 30 * 24 * 60 * 60 * 1000 return items.filter(i => !isNaN(new Date(i.date)) && new Date(i.date).getTime() >= cutoff).length } -// -------- Cards -------- +/* -------- Cards -------- */ -function Card({item}) { +function Card({ item }) { return (
{item.thumbnail ? ( - /* eslint-disable @next/next/no-img-element */ ) : item.video ? (
🎬
@@ -173,131 +157,4 @@ function Card({item}) {
{fmtDate(item.date)} - {item.category && • {item.category}} - {isNew(item.date) && NEW} -
-

{item.title}

- {item.description &&

{item.description}

} -
-
-
- ) -} - -function CardsView({items, limit=DEFAULT_LIMIT}) { - const shown = items.slice(0, limit) - return ( -
- {shown.map((i) => )} -
- ) -} - -// -------- Timeline -------- - -function Timeline({items}) { - const groups = useMemo(() => { - const map = new Map() - for (const i of items) { - const k = monthKey(i.date) - if (!map.has(k)) map.set(k, []) - map.get(k).push(i) - } - // sort groups by most recent month - return Array.from(map.entries()).sort((a,b) => { - const da = new Date(a[0]) - const db = new Date(b[0]) - return db - da - }) - }, [items]) - - return ( -
- {groups.map(([month, arr]) => ( -
-

{month} ({arr.length})

- -
- ))} -
- ) -} - -// -------- Page -------- - -export default function WhatsNewPage() { - const [mode, setMode] = useState('cards') - const [items, setItems] = useState([]) - const [loading, setLoading] = useState(true) - const [error, setError] = useState(null) - - useEffect(() => { - let cancelled = false - ;(async () => { - try { - const index = await fetchChangelogIndex() - // Sort by filename desc (YYYY-MM-DD-*.mdx lexicographically works) - index.sort((a,b) => b.name.localeCompare(a.name)) - const toFetch = index.slice(0, FETCH_COUNT) - const results = [] - for (const f of toFetch) { - try { - results.push(await fetchFrontmatter(f)) - } catch (e) { - // ignore a single post failure - console.warn('changelog parse fail', f.name, e) - } - } - if (!cancelled) setItems(results.filter(Boolean)) - } catch (e) { - if (!cancelled) setError(e?.message || String(e)) - } finally { - if (!cancelled) setLoading(false) - } - })() - return () => { cancelled = true } - }, []) - - const shipped30d = useMemo(() => countLast30(items), [items]) - - return ( -
- - - {loading &&
Loading latest updates…
} - {error &&
Error: {error}
} - {!loading && !error && ( - mode === 'cards' - ? - : - )} - -
- Don’t see what you’re looking for? See the full Changelog. -
- - {/* No-JS fallback */} - -
- ) -} + {item.category && Date: Fri, 26 Sep 2025 14:50:36 +0800 Subject: [PATCH 003/257] Update whats-new.mdx --- pages/guides/whats-new.mdx | 206 ++++++++++++------------------------- 1 file changed, 63 insertions(+), 143 deletions(-) diff --git a/pages/guides/whats-new.mdx b/pages/guides/whats-new.mdx index b5a70137c7..fd5ff667ff 100644 --- a/pages/guides/whats-new.mdx +++ b/pages/guides/whats-new.mdx @@ -5,156 +5,76 @@ description: "Auto-updating highlights from Mixpanel Changelogs" import { useEffect, useMemo, useState } from 'react' -/* Save as: pages/guides/whats-new.mdx */ - -const REPO_LIST_URL = - 'https://api.github.com/repos/mixpanel/docs/contents/pages/changelogs?ref=main' - -const DEFAULT_LIMIT = 10 -const FETCH_COUNT = 30 - -function PageHeader({ count30d }) { - return ( -
-

What’s New

-

- Fresh from our Changelog. - We shipped {count30d} updates in the last 30 days. -

-
- ) -} - -function ViewToggle({ mode, setMode }) { - return ( -
- - -
- ) -} - -/* -------- Data fetching & parsing -------- */ - -async function fetchChangelogIndex() { - const res = await fetch(REPO_LIST_URL, { - headers: { Accept: 'application/vnd.github.v3+json' } - }) - if (!res.ok) throw new Error(`GitHub API error: ${res.status}`) - const files = await res.json() - return (Array.isArray(files) ? files : []).filter(f => f.name.endsWith('.mdx')) -} - -function parseDateFromName(name) { - const m = name.match(/^(\d{4}-\d{2}-\d{2})-/) - return m ? m[1] : null -} - -async function fetchFrontmatter(file) { - const rawRes = await fetch(file.download_url) - const raw = await rawRes.text() - const fm = parseFrontmatter(raw) - const summary = (fm.description ?? extractFirstParagraph(raw))?.slice(0, 260) ?? '' - const dateStr = fm.date ?? parseDateFromName(file.name) ?? '' - const url = `/changelogs/${file.name.replace(/\.mdx$/, '')}` - return { - title: fm.title || humanizeSlug(file.name), - date: dateStr, - description: summary, - thumbnail: fm.thumbnail || '', - video: fm.video || '', - category: fm.category || fm.type || '', - url +export default function WhatsNewPage() { + // ---- Config ---- + const REPO_LIST_URL = + 'https://api.github.com/repos/mixpanel/docs/contents/pages/changelogs?ref=main' + const DEFAULT_LIMIT = 10 + const FETCH_COUNT = 30 + + // ---- Utilities (scoped inside the component for MDX compatibility) ---- + const parseDateFromName = (name) => { + const m = name.match(/^(\d{4}-\d{2}-\d{2})-/) + return m ? m[1] : null } -} -function parseFrontmatter(md) { - const m = md.match(/^---\n([\s\S]*?)\n---/) - if (!m) return {} - const out = {} - let key = null - for (const lineRaw of m[1].split('\n')) { - const line = lineRaw.trim() - if (!line) continue - const kv = line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/) - if (kv) { - key = kv[1] - let val = kv[2] - if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) { - val = val.slice(1, -1) + const parseFrontmatter = (md) => { + const m = md.match(/^---\n([\s\S]*?)\n---/) + if (!m) return {} + const out = {} + let key = null + for (const lineRaw of m[1].split('\n')) { + const line = lineRaw.trim() + if (!line) continue + const kv = line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/) + if (kv) { + key = kv[1] + let val = kv[2] + if ( + (val.startsWith('"') && val.endsWith('"')) || + (val.startsWith("'") && val.endsWith("'")) + ) { + val = val.slice(1, -1) + } + out[key] = val + } else if (key && line.startsWith('- ')) { + if (!Array.isArray(out[key])) out[key] = [] + out[key].push(line.slice(2).trim()) } - out[key] = val - } else if (key && line.startsWith('- ')) { - if (!Array.isArray(out[key])) out[key] = [] - out[key].push(line.slice(2).trim()) } + return out } - return out -} -function extractFirstParagraph(md) { - md = md.replace(/^---[\s\S]*?---/, '').trim() - const lines = md - .split('\n') - .filter(Boolean) - .filter(l => !l.startsWith('import ') && !l.startsWith('export ')) - return lines.slice(0, 4).join(' ') -} - -function humanizeSlug(name) { - const noExt = name.replace(/\.mdx$/, '') - const noDate = noExt.replace(/^\d{4}-\d{2}-\d{2}-/, '') - return noDate.replace(/-/g, ' ').replace(/\b\w/g, m => m.toUpperCase()) -} - -/* -------- UI helpers -------- */ - -function isNew(dateStr) { - const d = new Date(dateStr) - if (isNaN(d)) return false - const days = (Date.now() - d.getTime()) / (1000 * 60 * 60 * 24) - return days <= 14 -} - -function fmtDate(dateStr) { - const d = new Date(dateStr) - if (isNaN(d)) return dateStr || '' - return d.toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' }) -} + const extractFirstParagraph = (md) => { + const stripped = md.replace(/^---[\s\S]*?---/, '').trim() + const lines = stripped + .split('\n') + .filter(Boolean) + .filter((l) => !l.startsWith('import ') && !l.startsWith('export ')) + return lines.slice(0, 4).join(' ') + } -function monthKey(dateStr) { - const d = new Date(dateStr) - if (isNaN(d)) return 'Unknown' - return d.toLocaleDateString(undefined, { year: 'numeric', month: 'long' }) -} + const humanizeSlug = (name) => { + const noExt = name.replace(/\.mdx$/, '') + const noDate = noExt.replace(/^\d{4}-\d{2}-\d{2}-/, '') + return noDate.replace(/-/g, ' ').replace(/\b\w/g, (m) => m.toUpperCase()) + } -function countLast30(items) { - const cutoff = Date.now() - 30 * 24 * 60 * 60 * 1000 - return items.filter(i => !isNaN(new Date(i.date)) && new Date(i.date).getTime() >= cutoff).length -} + const isNew = (dateStr) => { + const d = new Date(dateStr) + if (isNaN(d)) return false + const days = (Date.now() - d.getTime()) / (1000 * 60 * 60 * 24) + return days <= 14 + } -/* -------- Cards -------- */ + const fmtDate = (dateStr) => { + const d = new Date(dateStr) + if (isNaN(d)) return dateStr || '' + return d.toLocaleDateString(undefined, { + year: 'numeric', + month: 'short', + day: 'numeric' + }) + } -function Card({ item }) { - return ( - -
- {item.thumbnail ? ( - - ) : item.video ? ( -
🎬
- ) : ( -
- )} -
-
- {fmtDate(item.date)} - {item.category && Date: Fri, 26 Sep 2025 15:04:03 +0800 Subject: [PATCH 004/257] Update whats-new.mdx --- pages/guides/whats-new.mdx | 234 ++++++++++++++++++++++++++++++++++++- 1 file changed, 229 insertions(+), 5 deletions(-) diff --git a/pages/guides/whats-new.mdx b/pages/guides/whats-new.mdx index fd5ff667ff..ba985a5c24 100644 --- a/pages/guides/whats-new.mdx +++ b/pages/guides/whats-new.mdx @@ -6,16 +6,16 @@ description: "Auto-updating highlights from Mixpanel Changelogs" import { useEffect, useMemo, useState } from 'react' export default function WhatsNewPage() { - // ---- Config ---- + // Config const REPO_LIST_URL = 'https://api.github.com/repos/mixpanel/docs/contents/pages/changelogs?ref=main' const DEFAULT_LIMIT = 10 const FETCH_COUNT = 30 - // ---- Utilities (scoped inside the component for MDX compatibility) ---- + // Utilities (scoped inside the component for MDX compatibility) const parseDateFromName = (name) => { const m = name.match(/^(\d{4}-\d{2}-\d{2})-/) - return m ? m[1] : null + return m ? m[1] : '' } const parseFrontmatter = (md) => { @@ -57,7 +57,7 @@ export default function WhatsNewPage() { const humanizeSlug = (name) => { const noExt = name.replace(/\.mdx$/, '') const noDate = noExt.replace(/^\d{4}-\d{2}-\d{2}-/, '') - return noDate.replace(/-/g, ' ').replace(/\b\w/g, (m) => m.toUpperCase()) + return noDate.replace(/-/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase()) } const isNew = (dateStr) => { @@ -77,4 +77,228 @@ export default function WhatsNewPage() { }) } - const month + const monthKey = (dateStr) => { + const d = new Date(dateStr) + if (isNaN(d)) return 'Unknown' + return d.toLocaleDateString(undefined, { year: 'numeric', month: 'long' }) + } + + const countLast30 = (items) => { + const cutoff = Date.now() - 30 * 24 * 60 * 60 * 1000 + return items.filter( + (i) => + !isNaN(new Date(i.date)) && + new Date(i.date).getTime() >= cutoff + ).length + } + + // Data fetching + const fetchChangelogIndex = async () => { + const res = await fetch(REPO_LIST_URL, { + headers: { Accept: 'application/vnd.github.v3+json' } + }) + if (!res.ok) throw new Error(`GitHub API error: ${res.status}`) + const files = await res.json() + return (Array.isArray(files) ? files : []).filter((f) => f.name.endsWith('.mdx')) + } + + const fetchFrontmatter = async (file) => { + const rawRes = await fetch(file.download_url) + const raw = await rawRes.text() + const fm = parseFrontmatter(raw) + const summary = (fm.description ?? extractFirstParagraph(raw))?.slice(0, 260) ?? '' + const dateStr = fm.date ?? parseDateFromName(file.name) ?? '' + const url = `/changelogs/${file.name.replace(/\.mdx$/, '')}` + return { + title: fm.title || humanizeSlug(file.name), + date: dateStr, + description: summary, + thumbnail: fm.thumbnail || '', + video: fm.video || '', + category: fm.category || fm.type || '', + url + } + } + + // Local components (defined inside for MDX) + const ViewToggle = ({ mode, setMode }) => ( +
+ + +
+ ) + + const Card = ({ item }) => ( +
+
+ {item.thumbnail ? ( + + ) : item.video ? ( +
🎬
+ ) : ( +
+ )} +
+
+ {fmtDate(item.date)} + {item.category && • {item.category}} + {isNew(item.date) && ( + + NEW + + )} +
+

{item.title}

+ {item.description && ( +

{item.description}

+ )} +
+
+
+ ) + + const CardsView = ({ items, limit = DEFAULT_LIMIT }) => { + const shown = items.slice(0, limit) + return ( +
+ {shown.map((i) => ( + + ))} +
+ ) + } + + const Timeline = ({ items }) => { + const groups = useMemo(() => { + const map = new Map() + for (const i of items) { + const k = monthKey(i.date) + if (!map.has(k)) map.set(k, []) + map.get(k).push(i) + } + return Array.from(map.entries()).sort((a, b) => { + const da = new Date(a[0]) + const db = new Date(b[0]) + return db - da + }) + }, [items]) + + return ( +
+ {groups.map(([month, arr]) => ( +
+

+ {month} ({arr.length}) +

+ +
+ ))} +
+ ) + } + + // State and effects + const [mode, setMode] = useState('cards') + const [items, setItems] = useState([]) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + + useEffect(() => { + let cancelled = false + ;(async () => { + try { + const index = await fetchChangelogIndex() + index.sort((a, b) => b.name.localeCompare(a.name)) + const toFetch = index.slice(0, FETCH_COUNT) + const results = [] + for (const f of toFetch) { + try { + results.push(await fetchFrontmatter(f)) + } catch (e) { + // ignore single post failure + } + } + if (!cancelled) setItems(results.filter(Boolean)) + } catch (e) { + if (!cancelled) setError(e?.message || String(e)) + } finally { + if (!cancelled) setLoading(false) + } + })() + return () => { + cancelled = true + } + }, []) + + const shipped30d = useMemo(() => countLast30(items), [items]) + + // Render + return ( +
+
+

What's New

+

+ Fresh from our Changelog. We shipped{' '} + {shipped30d} updates in the last 30 days. +

+
+ + + + {loading &&
Loading latest updates...
} + {error &&
Error: {error}
} + {!loading && !error && (mode === 'cards' ? : )} + +
+ Do not see what you are looking for? See the full{' '} + Changelog. +
+ + +
+ ) +} From 386cefe2f437cb41daedfdbe37d4dca17eaf36c9 Mon Sep 17 00:00:00 2001 From: kurbycchua <50901466+kurbycchua@users.noreply.github.com> Date: Fri, 26 Sep 2025 15:13:15 +0800 Subject: [PATCH 005/257] Update whats-new.mdx --- pages/guides/whats-new.mdx | 246 +++++++++++-------------------------- 1 file changed, 69 insertions(+), 177 deletions(-) diff --git a/pages/guides/whats-new.mdx b/pages/guides/whats-new.mdx index ba985a5c24..2a8749ea12 100644 --- a/pages/guides/whats-new.mdx +++ b/pages/guides/whats-new.mdx @@ -3,62 +3,58 @@ title: "What's New" description: "Auto-updating highlights from Mixpanel Changelogs" --- -import { useEffect, useMemo, useState } from 'react' +import { useMemo, useState } from 'react' +import { getPagesUnderRoute } from 'nextra/context' + +/** + * Static, zero-fetch version — uses Nextra to read local /pages/changelogs. + * Auto-updates whenever new changelog MDX is added to the repo. + */ + +export const changelogPages = getPagesUnderRoute('/changelogs') export default function WhatsNewPage() { - // Config - const REPO_LIST_URL = - 'https://api.github.com/repos/mixpanel/docs/contents/pages/changelogs?ref=main' const DEFAULT_LIMIT = 10 - const FETCH_COUNT = 30 - // Utilities (scoped inside the component for MDX compatibility) - const parseDateFromName = (name) => { - const m = name.match(/^(\d{4}-\d{2}-\d{2})-/) - return m ? m[1] : '' - } - - const parseFrontmatter = (md) => { - const m = md.match(/^---\n([\s\S]*?)\n---/) - if (!m) return {} - const out = {} - let key = null - for (const lineRaw of m[1].split('\n')) { - const line = lineRaw.trim() - if (!line) continue - const kv = line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/) - if (kv) { - key = kv[1] - let val = kv[2] - if ( - (val.startsWith('"') && val.endsWith('"')) || - (val.startsWith("'") && val.endsWith("'")) - ) { - val = val.slice(1, -1) - } - out[key] = val - } else if (key && line.startsWith('- ')) { - if (!Array.isArray(out[key])) out[key] = [] - out[key].push(line.slice(2).trim()) - } + // Normalize Nextra page objects into lightweight items + const items = useMemo(() => { + const parseDateFromString = (s = '') => { + const m = s.match(/(\d{4}-\d{2}-\d{2})/) + return m ? m[1] : '' } - return out - } + const humanize = (s = '') => + s.replace(/^\d{4}-\d{2}-\d{2}-/, '').replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase()) + + return (changelogPages || []) + .map((p) => { + const fm = p.frontMatter || p.meta || {} + const route = p.route || '' + const name = p.name || route.split('/').pop() || '' + const date = fm.date || parseDateFromString(name) || parseDateFromString(route) + return { + url: route, + title: fm.title || p.title || humanize(name), + date, + description: fm.description || '', + thumbnail: fm.thumbnail || '', + video: fm.video || '', + category: fm.category || fm.type || '' + } + }) + // Sort newest first by date (fallback to route/name if date missing) + .sort((a, b) => { + const da = new Date(a.date || parseDateFromString(a.url || '')) + const db = new Date(b.date || parseDateFromString(b.url || '')) + return db - da + }) + }, []) - const extractFirstParagraph = (md) => { - const stripped = md.replace(/^---[\s\S]*?---/, '').trim() - const lines = stripped - .split('\n') - .filter(Boolean) - .filter((l) => !l.startsWith('import ') && !l.startsWith('export ')) - return lines.slice(0, 4).join(' ') - } + const shipped30d = useMemo(() => { + const cutoff = Date.now() - 30 * 24 * 60 * 60 * 1000 + return items.filter(i => !isNaN(new Date(i.date)) && new Date(i.date).getTime() >= cutoff).length + }, [items]) - const humanizeSlug = (name) => { - const noExt = name.replace(/\.mdx$/, '') - const noDate = noExt.replace(/^\d{4}-\d{2}-\d{2}-/, '') - return noDate.replace(/-/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase()) - } + const [mode, setMode] = useState('cards') const isNew = (dateStr) => { const d = new Date(dateStr) @@ -70,11 +66,7 @@ export default function WhatsNewPage() { const fmtDate = (dateStr) => { const d = new Date(dateStr) if (isNaN(d)) return dateStr || '' - return d.toLocaleDateString(undefined, { - year: 'numeric', - month: 'short', - day: 'numeric' - }) + return d.toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' }) } const monthKey = (dateStr) => { @@ -83,44 +75,6 @@ export default function WhatsNewPage() { return d.toLocaleDateString(undefined, { year: 'numeric', month: 'long' }) } - const countLast30 = (items) => { - const cutoff = Date.now() - 30 * 24 * 60 * 60 * 1000 - return items.filter( - (i) => - !isNaN(new Date(i.date)) && - new Date(i.date).getTime() >= cutoff - ).length - } - - // Data fetching - const fetchChangelogIndex = async () => { - const res = await fetch(REPO_LIST_URL, { - headers: { Accept: 'application/vnd.github.v3+json' } - }) - if (!res.ok) throw new Error(`GitHub API error: ${res.status}`) - const files = await res.json() - return (Array.isArray(files) ? files : []).filter((f) => f.name.endsWith('.mdx')) - } - - const fetchFrontmatter = async (file) => { - const rawRes = await fetch(file.download_url) - const raw = await rawRes.text() - const fm = parseFrontmatter(raw) - const summary = (fm.description ?? extractFirstParagraph(raw))?.slice(0, 260) ?? '' - const dateStr = fm.date ?? parseDateFromName(file.name) ?? '' - const url = `/changelogs/${file.name.replace(/\.mdx$/, '')}` - return { - title: fm.title || humanizeSlug(file.name), - date: dateStr, - description: summary, - thumbnail: fm.thumbnail || '', - video: fm.video || '', - category: fm.category || fm.type || '', - url - } - } - - // Local components (defined inside for MDX) const ViewToggle = ({ mode, setMode }) => (

{item.title}

- {item.description && ( -

{item.description}

- )} + {item.description &&

{item.description}

}
@@ -173,9 +123,7 @@ export default function WhatsNewPage() { const shown = items.slice(0, limit) return (
- {shown.map((i) => ( - - ))} + {shown.map(i => )}
) } @@ -189,9 +137,7 @@ export default function WhatsNewPage() { map.get(k).push(i) } return Array.from(map.entries()).sort((a, b) => { - const da = new Date(a[0]) - const db = new Date(b[0]) - return db - da + const da = new Date(a[0]); const db = new Date(b[0]); return db - da }) }, [items]) @@ -203,35 +149,25 @@ export default function WhatsNewPage() { {month} ({arr.length})
+ + + ))} ))} @@ -239,66 +175,22 @@ export default function WhatsNewPage() { ) } - // State and effects - const [mode, setMode] = useState('cards') - const [items, setItems] = useState([]) - const [loading, setLoading] = useState(true) - const [error, setError] = useState(null) - - useEffect(() => { - let cancelled = false - ;(async () => { - try { - const index = await fetchChangelogIndex() - index.sort((a, b) => b.name.localeCompare(a.name)) - const toFetch = index.slice(0, FETCH_COUNT) - const results = [] - for (const f of toFetch) { - try { - results.push(await fetchFrontmatter(f)) - } catch (e) { - // ignore single post failure - } - } - if (!cancelled) setItems(results.filter(Boolean)) - } catch (e) { - if (!cancelled) setError(e?.message || String(e)) - } finally { - if (!cancelled) setLoading(false) - } - })() - return () => { - cancelled = true - } - }, []) - - const shipped30d = useMemo(() => countLast30(items), [items]) - - // Render return (

What's New

- Fresh from our Changelog. We shipped{' '} - {shipped30d} updates in the last 30 days. + Fresh from our Changelog. We shipped {shipped30d} updates in the last 30 days.

- {loading &&
Loading latest updates...
} - {error &&
Error: {error}
} - {!loading && !error && (mode === 'cards' ? : )} + {mode === 'cards' ? : }
- Do not see what you are looking for? See the full{' '} - Changelog. + Do not see what you are looking for? See the full Changelog.
- -
) } From 31f2394569ff713bb204c345ed209908ede50c18 Mon Sep 17 00:00:00 2001 From: kurbycchua <50901466+kurbycchua@users.noreply.github.com> Date: Fri, 26 Sep 2025 15:24:27 +0800 Subject: [PATCH 006/257] Update whats-new.mdx --- pages/guides/whats-new.mdx | 63 +++++++++++++++++++++++++------------- 1 file changed, 42 insertions(+), 21 deletions(-) diff --git a/pages/guides/whats-new.mdx b/pages/guides/whats-new.mdx index 2a8749ea12..6fb0bffe29 100644 --- a/pages/guides/whats-new.mdx +++ b/pages/guides/whats-new.mdx @@ -3,22 +3,22 @@ title: "What's New" description: "Auto-updating highlights from Mixpanel Changelogs" --- -import { useMemo, useState } from 'react' +import { useMemo, useRef, useState } from 'react' import { getPagesUnderRoute } from 'nextra/context' /** - * Static, zero-fetch version — uses Nextra to read local /pages/changelogs. - * Auto-updates whenever new changelog MDX is added to the repo. + * Static, zero-fetch version that reads local /pages/changelogs. + * Cards render as horizontal scrolling tiles with scroll-snap and controls. */ export const changelogPages = getPagesUnderRoute('/changelogs') export default function WhatsNewPage() { - const DEFAULT_LIMIT = 10 + const DEFAULT_LIMIT = 12 - // Normalize Nextra page objects into lightweight items + // Build items from local pages const items = useMemo(() => { - const parseDateFromString = (s = '') => { + const parseDate = (s = '') => { const m = s.match(/(\d{4}-\d{2}-\d{2})/) return m ? m[1] : '' } @@ -30,7 +30,7 @@ export default function WhatsNewPage() { const fm = p.frontMatter || p.meta || {} const route = p.route || '' const name = p.name || route.split('/').pop() || '' - const date = fm.date || parseDateFromString(name) || parseDateFromString(route) + const date = fm.date || parseDate(name) || parseDate(route) return { url: route, title: fm.title || p.title || humanize(name), @@ -41,10 +41,9 @@ export default function WhatsNewPage() { category: fm.category || fm.type || '' } }) - // Sort newest first by date (fallback to route/name if date missing) .sort((a, b) => { - const da = new Date(a.date || parseDateFromString(a.url || '')) - const db = new Date(b.date || parseDateFromString(b.url || '')) + const da = new Date(a.date || '') + const db = new Date(b.date || '') return db - da }) }, []) @@ -54,7 +53,7 @@ export default function WhatsNewPage() { return items.filter(i => !isNaN(new Date(i.date)) && new Date(i.date).getTime() >= cutoff).length }, [items]) - const [mode, setMode] = useState('cards') + const [mode, setMode] = useState('tiles') const isNew = (dateStr) => { const d = new Date(dateStr) @@ -78,11 +77,11 @@ export default function WhatsNewPage() { const ViewToggle = ({ mode, setMode }) => (
+ +
) } @@ -186,7 +207,7 @@ export default function WhatsNewPage() { - {mode === 'cards' ? : } + {mode === 'tiles' ? : }
Do not see what you are looking for? See the full Changelog. From cf2508674a6214557267644756e993ecad964c3b Mon Sep 17 00:00:00 2001 From: kurbycchua <50901466+kurbycchua@users.noreply.github.com> Date: Fri, 26 Sep 2025 15:30:14 +0800 Subject: [PATCH 007/257] Create what-new-too.mdx --- pages/guides/what-new-too.mdx | 107 ++++++++++++++++++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 pages/guides/what-new-too.mdx diff --git a/pages/guides/what-new-too.mdx b/pages/guides/what-new-too.mdx new file mode 100644 index 0000000000..8393d888ea --- /dev/null +++ b/pages/guides/what-new-too.mdx @@ -0,0 +1,107 @@ +--- +title: "What's New — Timeline" +description: "Auto-updating timeline of Mixpanel Changelogs with type filter" +--- + +import { useMemo, useState } from 'react' +import { getPagesUnderRoute } from 'nextra/context' + +/** + * Timeline-only view with a type filter. + * - Reads local /pages/changelogs MDX (no network requests). + * - Type filter uses: frontMatter.category, frontMatter.type, or frontMatter.tags. + * - Auto-updates as new changelog MDX is added. + */ + +export const changelogPages = getPagesUnderRoute('/changelogs') + +export default function WhatsNewTimeline() { + // Build normalized items from local pages + const items = useMemo(() => { + const parseDate = (s = '') => { + const m = s.match(/(\d{4}-\d{2}-\d{2})/) + return m ? m[1] : '' + } + const humanize = (s = '') => + s.replace(/^\d{4}-\d{2}-\d{2}-/, '').replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase()) + + const toTypes = (fm = {}) => { + const out = [] + const cat = fm.category || fm.type + if (cat) out.push(String(cat)) + const tags = fm.tags + if (Array.isArray(tags)) out.push(...tags.map(String)) + else if (typeof tags === 'string') out.push(...tags.split(',').map(t => t.trim()).filter(Boolean)) + // de-dupe and keep short labels + return Array.from(new Set(out.filter(Boolean))) + } + + return (changelogPages || []) + .map((p) => { + const fm = p.frontMatter || p.meta || {} + const route = p.route || '' + if (!/\/changelogs\/.+/.test(route)) return null // skip /changelogs root + const name = p.name || route.split('/').pop() || '' + const date = fm.date || parseDate(name) || parseDate(route) + return { + url: route, + title: fm.title || p.title || humanize(name), + date, + description: fm.description || '', + thumbnail: fm.thumbnail || '', + video: fm.video || '', + // types derived from category/type/tags + types: toTypes(fm) + } + }) + .filter(Boolean) + .sort((a, b) => new Date(b.date || '') - new Date(a.date || '')) + }, []) + + // Derive list of type options (for the filter UI) + const typeOptions = useMemo(() => { + const set = new Set() + for (const i of items) for (const t of (i.types || [])) set.add(t) + return ['All', ...Array.from(set).sort((a, b) => a.localeCompare(b))] + }, [items]) + + const [selectedType, setSelectedType] = useState('All') + + const filtered = useMemo(() => { + if (selectedType === 'All') return items + return items.filter(i => (i.types || []).includes(selectedType)) + }, [items, selectedType]) + + const shipped30d = useMemo(() => { + const cutoff = Date.now() - 30 * 24 * 60 * 60 * 1000 + return filtered.filter(i => !isNaN(new Date(i.date)) && new Date(i.date).getTime() >= cutoff).length + }, [filtered]) + + const isNew = (dateStr) => { + const d = new Date(dateStr) + if (isNaN(d)) return false + const days = (Date.now() - d.getTime()) / (1000 * 60 * 60 * 24) + return days <= 14 + } + + const fmtDate = (dateStr) => { + const d = new Date(dateStr) + if (isNaN(d)) return dateStr || '' + return d.toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' }) + } + + const monthKey = (dateStr) => { + const d = new Date(dateStr) + if (isNaN(d)) return 'Unknown' + return d.toLocaleDateString(undefined, { year: 'numeric', month: 'long' }) + } + + // Group by month (post-filter) + const groups = useMemo(() => { + const map = new Map() + for (const i of filtered) { + const k = monthKey(i.date) + if (!map.has(k)) map.set(k, []) + map.get(k).push(i) + } + return Array.from(map.entries()).sort((a, b) => n From 57706e284dd316bef380d03e8fe5c184590be134 Mon Sep 17 00:00:00 2001 From: kurbycchua <50901466+kurbycchua@users.noreply.github.com> Date: Fri, 26 Sep 2025 15:36:30 +0800 Subject: [PATCH 008/257] Update whats-new.mdx --- pages/guides/whats-new.mdx | 195 +++++++++++++++---------------------- 1 file changed, 79 insertions(+), 116 deletions(-) diff --git a/pages/guides/whats-new.mdx b/pages/guides/whats-new.mdx index 6fb0bffe29..86535a96a8 100644 --- a/pages/guides/whats-new.mdx +++ b/pages/guides/whats-new.mdx @@ -3,18 +3,15 @@ title: "What's New" description: "Auto-updating highlights from Mixpanel Changelogs" --- -import { useMemo, useRef, useState } from 'react' +import { useEffect, useMemo, useRef, useState } from 'react' import { getPagesUnderRoute } from 'nextra/context' -/** - * Static, zero-fetch version that reads local /pages/changelogs. - * Cards render as horizontal scrolling tiles with scroll-snap and controls. - */ +/* Option 1 — Horizontal scrolling tiles */ export const changelogPages = getPagesUnderRoute('/changelogs') export default function WhatsNewPage() { - const DEFAULT_LIMIT = 12 + const DEFAULT_LIMIT = 16 // Build items from local pages const items = useMemo(() => { @@ -29,6 +26,7 @@ export default function WhatsNewPage() { .map((p) => { const fm = p.frontMatter || p.meta || {} const route = p.route || '' + if (!/\/changelogs\/.+/.test(route)) return null const name = p.name || route.split('/').pop() || '' const date = fm.date || parseDate(name) || parseDate(route) return { @@ -41,11 +39,8 @@ export default function WhatsNewPage() { category: fm.category || fm.type || '' } }) - .sort((a, b) => { - const da = new Date(a.date || '') - const db = new Date(b.date || '') - return db - da - }) + .filter(Boolean) + .sort((a, b) => new Date(b.date || '') - new Date(a.date || '')) }, []) const shipped30d = useMemo(() => { @@ -53,163 +48,131 @@ export default function WhatsNewPage() { return items.filter(i => !isNaN(new Date(i.date)) && new Date(i.date).getTime() >= cutoff).length }, [items]) - const [mode, setMode] = useState('tiles') - const isNew = (dateStr) => { - const d = new Date(dateStr) - if (isNaN(d)) return false + const d = new Date(dateStr); if (isNaN(d)) return false const days = (Date.now() - d.getTime()) / (1000 * 60 * 60 * 24) return days <= 14 } - const fmtDate = (dateStr) => { - const d = new Date(dateStr) - if (isNaN(d)) return dateStr || '' + const d = new Date(dateStr); if (isNaN(d)) return dateStr || '' return d.toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' }) } - const monthKey = (dateStr) => { - const d = new Date(dateStr) - if (isNaN(d)) return 'Unknown' - return d.toLocaleDateString(undefined, { year: 'numeric', month: 'long' }) - } - - const ViewToggle = ({ mode, setMode }) => ( -
- - -
- ) - const Card = ({ item }) => ( - +
{item.thumbnail ? ( ) : item.video ? ( -
Video
+
Video
) : ( -
+
)}
{fmtDate(item.date)} {item.category && • {item.category}} - {isNew(item.date) && ( - NEW - )} + {isNew(item.date) && NEW}

{item.title}

- {item.description &&

{item.description}

} + {item.description &&

{item.description}

}
) - // Horizontal scrolling tiles with snap and controls + // ------- HORIZONTAL SCROLLER ------- const Tiles = ({ items, limit = DEFAULT_LIMIT }) => { const shown = items.slice(0, limit) const ref = useRef(null) - const page = () => Math.max((ref.current?.clientWidth || 0) - 64, 240) + const [canScroll, setCanScroll] = useState(false) + const [progress, setProgress] = useState(0) + + useEffect(() => { + const el = ref.current + if (!el) return + const update = () => { + const max = el.scrollWidth - el.clientWidth + setCanScroll(max > 0) + setProgress(max <= 0 ? 0 : el.scrollLeft / max) + } + update() + el.addEventListener('scroll', update, { passive: true }) + window.addEventListener('resize', update) + return () => { + el.removeEventListener('scroll', update) + window.removeEventListener('resize', update) + } + }, [shown.length]) + + const page = () => Math.max(240, Math.floor((ref.current?.clientWidth || 0) * 0.9)) const scrollLeft = () => ref.current?.scrollBy({ left: -page(), behavior: 'smooth' }) const scrollRight = () => ref.current?.scrollBy({ left: page(), behavior: 'smooth' }) return ( -
+
+ {/* Scroll container */}
- {shown.map((i) => ( -
+ {shown.map(i => ( +
))}
-
- - -
-
- ) - } - const Timeline = ({ items }) => { - const groups = useMemo(() => { - const map = new Map() - for (const i of items) { - const k = monthKey(i.date) - if (!map.has(k)) map.set(k, []) - map.get(k).push(i) - } - return Array.from(map.entries()).sort((a, b) => { - const da = new Date(a[0]); const db = new Date(b[0]); return db - da - }) - }, [items]) + {/* Progress bar */} + {canScroll && ( +
+
+
+ )} - return ( -
- {groups.map(([month, arr]) => ( -
-

- {month} ({arr.length}) -

- -
- ))} + {/* Arrow controls */} + {canScroll && ( + <> + + + + )}
) } + // ------------------------------------ return ( -
-
-

What's New

-

- Fresh from our Changelog. We shipped {shipped30d} updates in the last 30 days. +

+
+

New releases

+

+ Learn more about our newly released capabilities. We shipped {shipped30d} updates in the last 30 days.

- - - {mode === 'tiles' ? : } + -
+
Do not see what you are looking for? See the full Changelog.
From 5ce7aa235b47204da587d395cac144ea5b4f6906 Mon Sep 17 00:00:00 2001 From: kurbycchua <50901466+kurbycchua@users.noreply.github.com> Date: Fri, 26 Sep 2025 15:38:17 +0800 Subject: [PATCH 009/257] Delete pages/guides/what-new-too.mdx --- pages/guides/what-new-too.mdx | 107 ---------------------------------- 1 file changed, 107 deletions(-) delete mode 100644 pages/guides/what-new-too.mdx diff --git a/pages/guides/what-new-too.mdx b/pages/guides/what-new-too.mdx deleted file mode 100644 index 8393d888ea..0000000000 --- a/pages/guides/what-new-too.mdx +++ /dev/null @@ -1,107 +0,0 @@ ---- -title: "What's New — Timeline" -description: "Auto-updating timeline of Mixpanel Changelogs with type filter" ---- - -import { useMemo, useState } from 'react' -import { getPagesUnderRoute } from 'nextra/context' - -/** - * Timeline-only view with a type filter. - * - Reads local /pages/changelogs MDX (no network requests). - * - Type filter uses: frontMatter.category, frontMatter.type, or frontMatter.tags. - * - Auto-updates as new changelog MDX is added. - */ - -export const changelogPages = getPagesUnderRoute('/changelogs') - -export default function WhatsNewTimeline() { - // Build normalized items from local pages - const items = useMemo(() => { - const parseDate = (s = '') => { - const m = s.match(/(\d{4}-\d{2}-\d{2})/) - return m ? m[1] : '' - } - const humanize = (s = '') => - s.replace(/^\d{4}-\d{2}-\d{2}-/, '').replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase()) - - const toTypes = (fm = {}) => { - const out = [] - const cat = fm.category || fm.type - if (cat) out.push(String(cat)) - const tags = fm.tags - if (Array.isArray(tags)) out.push(...tags.map(String)) - else if (typeof tags === 'string') out.push(...tags.split(',').map(t => t.trim()).filter(Boolean)) - // de-dupe and keep short labels - return Array.from(new Set(out.filter(Boolean))) - } - - return (changelogPages || []) - .map((p) => { - const fm = p.frontMatter || p.meta || {} - const route = p.route || '' - if (!/\/changelogs\/.+/.test(route)) return null // skip /changelogs root - const name = p.name || route.split('/').pop() || '' - const date = fm.date || parseDate(name) || parseDate(route) - return { - url: route, - title: fm.title || p.title || humanize(name), - date, - description: fm.description || '', - thumbnail: fm.thumbnail || '', - video: fm.video || '', - // types derived from category/type/tags - types: toTypes(fm) - } - }) - .filter(Boolean) - .sort((a, b) => new Date(b.date || '') - new Date(a.date || '')) - }, []) - - // Derive list of type options (for the filter UI) - const typeOptions = useMemo(() => { - const set = new Set() - for (const i of items) for (const t of (i.types || [])) set.add(t) - return ['All', ...Array.from(set).sort((a, b) => a.localeCompare(b))] - }, [items]) - - const [selectedType, setSelectedType] = useState('All') - - const filtered = useMemo(() => { - if (selectedType === 'All') return items - return items.filter(i => (i.types || []).includes(selectedType)) - }, [items, selectedType]) - - const shipped30d = useMemo(() => { - const cutoff = Date.now() - 30 * 24 * 60 * 60 * 1000 - return filtered.filter(i => !isNaN(new Date(i.date)) && new Date(i.date).getTime() >= cutoff).length - }, [filtered]) - - const isNew = (dateStr) => { - const d = new Date(dateStr) - if (isNaN(d)) return false - const days = (Date.now() - d.getTime()) / (1000 * 60 * 60 * 24) - return days <= 14 - } - - const fmtDate = (dateStr) => { - const d = new Date(dateStr) - if (isNaN(d)) return dateStr || '' - return d.toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' }) - } - - const monthKey = (dateStr) => { - const d = new Date(dateStr) - if (isNaN(d)) return 'Unknown' - return d.toLocaleDateString(undefined, { year: 'numeric', month: 'long' }) - } - - // Group by month (post-filter) - const groups = useMemo(() => { - const map = new Map() - for (const i of filtered) { - const k = monthKey(i.date) - if (!map.has(k)) map.set(k, []) - map.get(k).push(i) - } - return Array.from(map.entries()).sort((a, b) => n From d3ff1d851d4806b946c51667a7cb8b59e8ca5e05 Mon Sep 17 00:00:00 2001 From: kurbycchua <50901466+kurbycchua@users.noreply.github.com> Date: Fri, 26 Sep 2025 15:47:57 +0800 Subject: [PATCH 010/257] Update whats-new.mdx --- pages/guides/whats-new.mdx | 131 +++++++++++++++++++++---------------- 1 file changed, 75 insertions(+), 56 deletions(-) diff --git a/pages/guides/whats-new.mdx b/pages/guides/whats-new.mdx index 86535a96a8..cb63aef21c 100644 --- a/pages/guides/whats-new.mdx +++ b/pages/guides/whats-new.mdx @@ -6,13 +6,9 @@ description: "Auto-updating highlights from Mixpanel Changelogs" import { useEffect, useMemo, useRef, useState } from 'react' import { getPagesUnderRoute } from 'nextra/context' -/* Option 1 — Horizontal scrolling tiles */ - export const changelogPages = getPagesUnderRoute('/changelogs') -export default function WhatsNewPage() { - const DEFAULT_LIMIT = 16 - +export default function WhatsNewPromo() { // Build items from local pages const items = useMemo(() => { const parseDate = (s = '') => { @@ -36,7 +32,7 @@ export default function WhatsNewPage() { description: fm.description || '', thumbnail: fm.thumbnail || '', video: fm.video || '', - category: fm.category || fm.type || '' + category: (fm.category || fm.type || '').toString() } }) .filter(Boolean) @@ -58,32 +54,44 @@ export default function WhatsNewPage() { return d.toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' }) } - const Card = ({ item }) => ( - -
- {item.thumbnail ? ( - - ) : item.video ? ( -
Video
- ) : ( -
- )} -
-
- {fmtDate(item.date)} - {item.category && • {item.category}} - {isNew(item.date) && NEW} -
-

{item.title}

- {item.description &&

{item.description}

} + // -------- Tile card (large, promo style) -------- + const Tile = ({ item }) => ( +
+
+
+ {fmtDate(item.date)} + {item.category && • {item.category}} + {isNew(item.date) && NEW} +
+

{item.title}

+ {item.description &&

{item.description}

} + +
+ {item.thumbnail ? ( + + ) : ( +
+ )} + {/* soft glow */} +
+
+ +
+ + Read update + +
) - // ------- HORIZONTAL SCROLLER ------- - const Tiles = ({ items, limit = DEFAULT_LIMIT }) => { - const shown = items.slice(0, limit) + // -------- Horizontal scroller with fade, arrows, progress -------- + const Carousel = ({ items, tileWidth = 420, gap = 24 }) => { const ref = useRef(null) const [canScroll, setCanScroll] = useState(false) const [progress, setProgress] = useState(0) @@ -103,54 +111,61 @@ export default function WhatsNewPage() { el.removeEventListener('scroll', update) window.removeEventListener('resize', update) } - }, [shown.length]) + }, [items.length]) - const page = () => Math.max(240, Math.floor((ref.current?.clientWidth || 0) * 0.9)) - const scrollLeft = () => ref.current?.scrollBy({ left: -page(), behavior: 'smooth' }) - const scrollRight = () => ref.current?.scrollBy({ left: page(), behavior: 'smooth' }) + const page = () => Math.max(tileWidth + gap, Math.floor((ref.current?.clientWidth || 0) * 0.9)) + const left = () => ref.current?.scrollBy({ left: -page(), behavior: 'smooth' }) + const right = () => ref.current?.scrollBy({ left: page(), behavior: 'smooth' }) return (
- {/* Scroll container */} + {/* the glowing backdrop behind tiles */} +
+ + {/* scroll container */}
- {shown.map(i => ( -
- + {items.map((i) => ( +
+
))}
- {/* Progress bar */} + {/* progress bar */} {canScroll && ( -
+
)} - {/* Arrow controls */} + {/* arrows */} {canScroll && ( <> @@ -159,22 +174,26 @@ export default function WhatsNewPage() {
) } - // ------------------------------------ + // -------- Layout: left intro + right carousel -------- return ( -
-
-

New releases

-

- Learn more about our newly released capabilities. We shipped {shipped30d} updates in the last 30 days. -

-
+
+
+
+

New releases

+

+ Learn more about our newly released capabilities. We shipped {shipped30d} updates in the last 30 days. +

+
- +
+ +
+
Do not see what you are looking for? See the full Changelog.
-
+ ) } From b7c842df500688bad82eac433b1f197d4ee0c836 Mon Sep 17 00:00:00 2001 From: kurbycchua <50901466+kurbycchua@users.noreply.github.com> Date: Fri, 26 Sep 2025 16:00:35 +0800 Subject: [PATCH 011/257] Update whats-new.mdx --- pages/guides/whats-new.mdx | 35 +++++++++++++++++++---------------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/pages/guides/whats-new.mdx b/pages/guides/whats-new.mdx index cb63aef21c..7419004203 100644 --- a/pages/guides/whats-new.mdx +++ b/pages/guides/whats-new.mdx @@ -54,7 +54,7 @@ export default function WhatsNewPromo() { return d.toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' }) } - // -------- Tile card (large, promo style) -------- + // Large promo tile const Tile = ({ item }) => (
@@ -74,23 +74,19 @@ export default function WhatsNewPromo() { background: 'radial-gradient(120% 120% at 0% 100%, rgba(168,85,247,0.25), transparent 60%), radial-gradient(120% 120% at 100% 0%, rgba(59,130,246,0.25), transparent 60%)' }} /> )} - {/* soft glow */} -
+
- Read update - + Read update ->
) - // -------- Horizontal scroller with fade, arrows, progress -------- + // Carousel using whitespace-nowrap + inline-block (very robust) const Carousel = ({ items, tileWidth = 420, gap = 24 }) => { const ref = useRef(null) const [canScroll, setCanScroll] = useState(false) @@ -118,8 +114,8 @@ export default function WhatsNewPromo() { const right = () => ref.current?.scrollBy({ left: page(), behavior: 'smooth' }) return ( -
- {/* the glowing backdrop behind tiles */} +
+ {/* glow behind tiles */}
- {items.map((i) => ( -
+ {items.map((i, idx) => ( +
))} @@ -175,14 +175,17 @@ export default function WhatsNewPromo() { ) } - // -------- Layout: left intro + right carousel -------- + // Layout: left intro + right carousel return ( -
+

New releases

- Learn more about our newly released capabilities. We shipped {shipped30d} updates in the last 30 days. + Learn more about our newly released capabilities. We shipped {useMemo(() => { + const cutoff = Date.now() - 30 * 24 * 60 * 60 * 1000 + return items.filter(i => !isNaN(new Date(i.date)) && new Date(i.date).getTime() >= cutoff).length + }, [items])} updates in the last 30 days.

From ea0ac0124343f2d32d31443c89e7fc254944ffe0 Mon Sep 17 00:00:00 2001 From: kurbycchua <50901466+kurbycchua@users.noreply.github.com> Date: Fri, 26 Sep 2025 16:09:31 +0800 Subject: [PATCH 012/257] Create WhatsNewPromo.tsx --- pages/guides/components/WhatsNewPromo.tsx | 221 ++++++++++++++++++++++ 1 file changed, 221 insertions(+) create mode 100644 pages/guides/components/WhatsNewPromo.tsx diff --git a/pages/guides/components/WhatsNewPromo.tsx b/pages/guides/components/WhatsNewPromo.tsx new file mode 100644 index 0000000000..416a5da05e --- /dev/null +++ b/pages/guides/components/WhatsNewPromo.tsx @@ -0,0 +1,221 @@ +'use client'; + +import React, { useEffect, useMemo, useRef, useState } from 'react'; +import { getPagesUnderRoute } from 'nextra/context'; + +type Item = { + url: string; + title: string; + date: string; + description: string; + thumbnail: string; + video: string; + category: string; +}; + +// Read local /pages/changelogs at build/runtime (no network requests) +const changelogPages = getPagesUnderRoute('/changelogs'); + +function buildItems(): Item[] { + const parseDate = (s = '') => { + const m = s.match(/(\d{4}-\d{2}-\d{2})/); + return m ? m[1] : ''; + }; + const humanize = (s = '') => + s.replace(/^\d{4}-\d{2}-\d{2}-/, '').replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase()); + + return (changelogPages || []) + .map((p: any) => { + const fm = p.frontMatter || p.meta || {}; + const route = p.route || ''; + if (!/\/changelogs\/.+/.test(route)) return null; // skip index + const name = p.name || route.split('/').pop() || ''; + const date = fm.date || parseDate(name) || parseDate(route); + return { + url: route, + title: fm.title || p.title || humanize(name), + date, + description: fm.description || '', + thumbnail: fm.thumbnail || '', + video: fm.video || '', + category: String(fm.category || fm.type || ''), + } as Item; + }) + .filter(Boolean) + .sort((a: Item, b: Item) => new Date(b.date || '').getTime() - new Date(a.date || '').getTime()); +} + +function fmtDate(dateStr: string) { + const d = new Date(dateStr); + if (isNaN(d as any)) return dateStr || ''; + return d.toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' }); +} + +function isNew(dateStr: string) { + const d = new Date(dateStr); + if (isNaN(d as any)) return false; + const days = (Date.now() - d.getTime()) / (1000 * 60 * 60 * 24); + return days <= 14; +} + +const Tile = ({ item }: { item: Item }) => ( + +
+
+ {fmtDate(item.date)} + {item.category && • {item.category}} + {isNew(item.date) && ( + + NEW + + )} +
+

{item.title}

+ {item.description &&

{item.description}

} + +
+ {item.thumbnail ? ( + + ) : ( +
+ )} +
+
+ +
+ + Read update -> + +
+
+
+); + +function Carousel({ items, tileWidth = 420, gap = 24 }: { items: Item[]; tileWidth?: number; gap?: number }) { + const ref = useRef(null); + const [canScroll, setCanScroll] = useState(false); + const [progress, setProgress] = useState(0); + + useEffect(() => { + const el = ref.current; + if (!el) return; + const update = () => { + const max = el.scrollWidth - el.clientWidth; + setCanScroll(max > 0); + setProgress(max <= 0 ? 0 : el.scrollLeft / max); + }; + update(); + el.addEventListener('scroll', update, { passive: true } as any); + window.addEventListener('resize', update); + return () => { + el.removeEventListener('scroll', update as any); + window.removeEventListener('resize', update); + }; + }, [items.length]); + + const page = () => Math.max(tileWidth + gap, Math.floor((ref.current?.clientWidth || 0) * 0.9)); + const left = () => ref.current?.scrollBy({ left: -page(), behavior: 'smooth' }); + const right = () => ref.current?.scrollBy({ left: page(), behavior: 'smooth' }); + + return ( +
+
+
+ {items.map((i, idx) => ( +
+ +
+ ))} +
+ + {canScroll && ( +
+
+
+ )} + + {canScroll && ( + <> + + + + )} +
+ ); +} + +export default function WhatsNewPromo() { + const items = useMemo(buildItems, []); + const shipped30d = useMemo(() => { + const cutoff = Date.now() - 30 * 24 * 60 * 60 * 1000; + return items.filter(i => !isNaN(new Date(i.date) as any) && new Date(i.date).getTime() >= cutoff).length; + }, [items]); + + return ( +
+
+
+

New releases

+

+ Learn more about our newly released capabilities. We shipped {shipped30d} updates in the last 30 days. +

+
+
+ +
+
+ +
+ Do not see what you are looking for? See the full Changelog. +
+
+ ); +} From adf456871bfd21fe0c4bf8c308d4b2b70f2d159d Mon Sep 17 00:00:00 2001 From: kurbycchua <50901466+kurbycchua@users.noreply.github.com> Date: Fri, 26 Sep 2025 16:10:33 +0800 Subject: [PATCH 013/257] Update whats-new.mdx --- pages/guides/whats-new.mdx | 198 +------------------------------------ 1 file changed, 2 insertions(+), 196 deletions(-) diff --git a/pages/guides/whats-new.mdx b/pages/guides/whats-new.mdx index 7419004203..3266b88da8 100644 --- a/pages/guides/whats-new.mdx +++ b/pages/guides/whats-new.mdx @@ -3,200 +3,6 @@ title: "What's New" description: "Auto-updating highlights from Mixpanel Changelogs" --- -import { useEffect, useMemo, useRef, useState } from 'react' -import { getPagesUnderRoute } from 'nextra/context' +import WhatsNewPromo from '../../components/WhatsNewPromo' -export const changelogPages = getPagesUnderRoute('/changelogs') - -export default function WhatsNewPromo() { - // Build items from local pages - const items = useMemo(() => { - const parseDate = (s = '') => { - const m = s.match(/(\d{4}-\d{2}-\d{2})/) - return m ? m[1] : '' - } - const humanize = (s = '') => - s.replace(/^\d{4}-\d{2}-\d{2}-/, '').replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase()) - - return (changelogPages || []) - .map((p) => { - const fm = p.frontMatter || p.meta || {} - const route = p.route || '' - if (!/\/changelogs\/.+/.test(route)) return null - const name = p.name || route.split('/').pop() || '' - const date = fm.date || parseDate(name) || parseDate(route) - return { - url: route, - title: fm.title || p.title || humanize(name), - date, - description: fm.description || '', - thumbnail: fm.thumbnail || '', - video: fm.video || '', - category: (fm.category || fm.type || '').toString() - } - }) - .filter(Boolean) - .sort((a, b) => new Date(b.date || '') - new Date(a.date || '')) - }, []) - - const shipped30d = useMemo(() => { - const cutoff = Date.now() - 30 * 24 * 60 * 60 * 1000 - return items.filter(i => !isNaN(new Date(i.date)) && new Date(i.date).getTime() >= cutoff).length - }, [items]) - - const isNew = (dateStr) => { - const d = new Date(dateStr); if (isNaN(d)) return false - const days = (Date.now() - d.getTime()) / (1000 * 60 * 60 * 24) - return days <= 14 - } - const fmtDate = (dateStr) => { - const d = new Date(dateStr); if (isNaN(d)) return dateStr || '' - return d.toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' }) - } - - // Large promo tile - const Tile = ({ item }) => ( - -
-
- {fmtDate(item.date)} - {item.category && • {item.category}} - {isNew(item.date) && NEW} -
-

{item.title}

- {item.description &&

{item.description}

} - -
- {item.thumbnail ? ( - - ) : ( -
- )} -
-
- -
- - Read update -> - -
-
-
- ) - - // Carousel using whitespace-nowrap + inline-block (very robust) - const Carousel = ({ items, tileWidth = 420, gap = 24 }) => { - const ref = useRef(null) - const [canScroll, setCanScroll] = useState(false) - const [progress, setProgress] = useState(0) - - useEffect(() => { - const el = ref.current - if (!el) return - const update = () => { - const max = el.scrollWidth - el.clientWidth - setCanScroll(max > 0) - setProgress(max <= 0 ? 0 : el.scrollLeft / max) - } - update() - el.addEventListener('scroll', update, { passive: true }) - window.addEventListener('resize', update) - return () => { - el.removeEventListener('scroll', update) - window.removeEventListener('resize', update) - } - }, [items.length]) - - const page = () => Math.max(tileWidth + gap, Math.floor((ref.current?.clientWidth || 0) * 0.9)) - const left = () => ref.current?.scrollBy({ left: -page(), behavior: 'smooth' }) - const right = () => ref.current?.scrollBy({ left: page(), behavior: 'smooth' }) - - return ( -
- {/* glow behind tiles */} -
- - {/* scroll container */} -
- {items.map((i, idx) => ( -
- -
- ))} -
- - {/* progress bar */} - {canScroll && ( -
-
-
- )} - - {/* arrows */} - {canScroll && ( - <> - - - - )} -
- ) - } - - // Layout: left intro + right carousel - return ( -
-
-
-

New releases

-

- Learn more about our newly released capabilities. We shipped {useMemo(() => { - const cutoff = Date.now() - 30 * 24 * 60 * 60 * 1000 - return items.filter(i => !isNaN(new Date(i.date)) && new Date(i.date).getTime() >= cutoff).length - }, [items])} updates in the last 30 days. -

-
- -
- -
-
- -
- Do not see what you are looking for? See the full Changelog. -
-
- ) -} + From d3c93f3e3b8933f66a0669e4dbdbd62711fb8d17 Mon Sep 17 00:00:00 2001 From: kurbycchua <50901466+kurbycchua@users.noreply.github.com> Date: Fri, 26 Sep 2025 16:10:54 +0800 Subject: [PATCH 014/257] Delete pages/guides/components directory --- pages/guides/components/WhatsNewPromo.tsx | 221 ---------------------- 1 file changed, 221 deletions(-) delete mode 100644 pages/guides/components/WhatsNewPromo.tsx diff --git a/pages/guides/components/WhatsNewPromo.tsx b/pages/guides/components/WhatsNewPromo.tsx deleted file mode 100644 index 416a5da05e..0000000000 --- a/pages/guides/components/WhatsNewPromo.tsx +++ /dev/null @@ -1,221 +0,0 @@ -'use client'; - -import React, { useEffect, useMemo, useRef, useState } from 'react'; -import { getPagesUnderRoute } from 'nextra/context'; - -type Item = { - url: string; - title: string; - date: string; - description: string; - thumbnail: string; - video: string; - category: string; -}; - -// Read local /pages/changelogs at build/runtime (no network requests) -const changelogPages = getPagesUnderRoute('/changelogs'); - -function buildItems(): Item[] { - const parseDate = (s = '') => { - const m = s.match(/(\d{4}-\d{2}-\d{2})/); - return m ? m[1] : ''; - }; - const humanize = (s = '') => - s.replace(/^\d{4}-\d{2}-\d{2}-/, '').replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase()); - - return (changelogPages || []) - .map((p: any) => { - const fm = p.frontMatter || p.meta || {}; - const route = p.route || ''; - if (!/\/changelogs\/.+/.test(route)) return null; // skip index - const name = p.name || route.split('/').pop() || ''; - const date = fm.date || parseDate(name) || parseDate(route); - return { - url: route, - title: fm.title || p.title || humanize(name), - date, - description: fm.description || '', - thumbnail: fm.thumbnail || '', - video: fm.video || '', - category: String(fm.category || fm.type || ''), - } as Item; - }) - .filter(Boolean) - .sort((a: Item, b: Item) => new Date(b.date || '').getTime() - new Date(a.date || '').getTime()); -} - -function fmtDate(dateStr: string) { - const d = new Date(dateStr); - if (isNaN(d as any)) return dateStr || ''; - return d.toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' }); -} - -function isNew(dateStr: string) { - const d = new Date(dateStr); - if (isNaN(d as any)) return false; - const days = (Date.now() - d.getTime()) / (1000 * 60 * 60 * 24); - return days <= 14; -} - -const Tile = ({ item }: { item: Item }) => ( - -
-
- {fmtDate(item.date)} - {item.category && • {item.category}} - {isNew(item.date) && ( - - NEW - - )} -
-

{item.title}

- {item.description &&

{item.description}

} - -
- {item.thumbnail ? ( - - ) : ( -
- )} -
-
- -
- - Read update -> - -
-
-
-); - -function Carousel({ items, tileWidth = 420, gap = 24 }: { items: Item[]; tileWidth?: number; gap?: number }) { - const ref = useRef(null); - const [canScroll, setCanScroll] = useState(false); - const [progress, setProgress] = useState(0); - - useEffect(() => { - const el = ref.current; - if (!el) return; - const update = () => { - const max = el.scrollWidth - el.clientWidth; - setCanScroll(max > 0); - setProgress(max <= 0 ? 0 : el.scrollLeft / max); - }; - update(); - el.addEventListener('scroll', update, { passive: true } as any); - window.addEventListener('resize', update); - return () => { - el.removeEventListener('scroll', update as any); - window.removeEventListener('resize', update); - }; - }, [items.length]); - - const page = () => Math.max(tileWidth + gap, Math.floor((ref.current?.clientWidth || 0) * 0.9)); - const left = () => ref.current?.scrollBy({ left: -page(), behavior: 'smooth' }); - const right = () => ref.current?.scrollBy({ left: page(), behavior: 'smooth' }); - - return ( -
-
-
- {items.map((i, idx) => ( -
- -
- ))} -
- - {canScroll && ( -
-
-
- )} - - {canScroll && ( - <> - - - - )} -
- ); -} - -export default function WhatsNewPromo() { - const items = useMemo(buildItems, []); - const shipped30d = useMemo(() => { - const cutoff = Date.now() - 30 * 24 * 60 * 60 * 1000; - return items.filter(i => !isNaN(new Date(i.date) as any) && new Date(i.date).getTime() >= cutoff).length; - }, [items]); - - return ( -
-
-
-

New releases

-

- Learn more about our newly released capabilities. We shipped {shipped30d} updates in the last 30 days. -

-
-
- -
-
- -
- Do not see what you are looking for? See the full Changelog. -
-
- ); -} From d476445b288c1de429329185cde33f13afde35ec Mon Sep 17 00:00:00 2001 From: kurbycchua <50901466+kurbycchua@users.noreply.github.com> Date: Fri, 26 Sep 2025 16:11:59 +0800 Subject: [PATCH 015/257] Create WhatsNewPromo.tsx --- components/WhatsNewPromo.tsx | 221 +++++++++++++++++++++++++++++++++++ 1 file changed, 221 insertions(+) create mode 100644 components/WhatsNewPromo.tsx diff --git a/components/WhatsNewPromo.tsx b/components/WhatsNewPromo.tsx new file mode 100644 index 0000000000..416a5da05e --- /dev/null +++ b/components/WhatsNewPromo.tsx @@ -0,0 +1,221 @@ +'use client'; + +import React, { useEffect, useMemo, useRef, useState } from 'react'; +import { getPagesUnderRoute } from 'nextra/context'; + +type Item = { + url: string; + title: string; + date: string; + description: string; + thumbnail: string; + video: string; + category: string; +}; + +// Read local /pages/changelogs at build/runtime (no network requests) +const changelogPages = getPagesUnderRoute('/changelogs'); + +function buildItems(): Item[] { + const parseDate = (s = '') => { + const m = s.match(/(\d{4}-\d{2}-\d{2})/); + return m ? m[1] : ''; + }; + const humanize = (s = '') => + s.replace(/^\d{4}-\d{2}-\d{2}-/, '').replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase()); + + return (changelogPages || []) + .map((p: any) => { + const fm = p.frontMatter || p.meta || {}; + const route = p.route || ''; + if (!/\/changelogs\/.+/.test(route)) return null; // skip index + const name = p.name || route.split('/').pop() || ''; + const date = fm.date || parseDate(name) || parseDate(route); + return { + url: route, + title: fm.title || p.title || humanize(name), + date, + description: fm.description || '', + thumbnail: fm.thumbnail || '', + video: fm.video || '', + category: String(fm.category || fm.type || ''), + } as Item; + }) + .filter(Boolean) + .sort((a: Item, b: Item) => new Date(b.date || '').getTime() - new Date(a.date || '').getTime()); +} + +function fmtDate(dateStr: string) { + const d = new Date(dateStr); + if (isNaN(d as any)) return dateStr || ''; + return d.toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' }); +} + +function isNew(dateStr: string) { + const d = new Date(dateStr); + if (isNaN(d as any)) return false; + const days = (Date.now() - d.getTime()) / (1000 * 60 * 60 * 24); + return days <= 14; +} + +const Tile = ({ item }: { item: Item }) => ( + +
+
+ {fmtDate(item.date)} + {item.category && • {item.category}} + {isNew(item.date) && ( + + NEW + + )} +
+

{item.title}

+ {item.description &&

{item.description}

} + +
+ {item.thumbnail ? ( + + ) : ( +
+ )} +
+
+ +
+ + Read update -> + +
+
+
+); + +function Carousel({ items, tileWidth = 420, gap = 24 }: { items: Item[]; tileWidth?: number; gap?: number }) { + const ref = useRef(null); + const [canScroll, setCanScroll] = useState(false); + const [progress, setProgress] = useState(0); + + useEffect(() => { + const el = ref.current; + if (!el) return; + const update = () => { + const max = el.scrollWidth - el.clientWidth; + setCanScroll(max > 0); + setProgress(max <= 0 ? 0 : el.scrollLeft / max); + }; + update(); + el.addEventListener('scroll', update, { passive: true } as any); + window.addEventListener('resize', update); + return () => { + el.removeEventListener('scroll', update as any); + window.removeEventListener('resize', update); + }; + }, [items.length]); + + const page = () => Math.max(tileWidth + gap, Math.floor((ref.current?.clientWidth || 0) * 0.9)); + const left = () => ref.current?.scrollBy({ left: -page(), behavior: 'smooth' }); + const right = () => ref.current?.scrollBy({ left: page(), behavior: 'smooth' }); + + return ( +
+
+
+ {items.map((i, idx) => ( +
+ +
+ ))} +
+ + {canScroll && ( +
+
+
+ )} + + {canScroll && ( + <> + + + + )} +
+ ); +} + +export default function WhatsNewPromo() { + const items = useMemo(buildItems, []); + const shipped30d = useMemo(() => { + const cutoff = Date.now() - 30 * 24 * 60 * 60 * 1000; + return items.filter(i => !isNaN(new Date(i.date) as any) && new Date(i.date).getTime() >= cutoff).length; + }, [items]); + + return ( +
+
+
+

New releases

+

+ Learn more about our newly released capabilities. We shipped {shipped30d} updates in the last 30 days. +

+
+
+ +
+
+ +
+ Do not see what you are looking for? See the full Changelog. +
+
+ ); +} From 5a33214ef0ff0a19c95db5df22ca865c57e37853 Mon Sep 17 00:00:00 2001 From: kurbycchua <50901466+kurbycchua@users.noreply.github.com> Date: Fri, 26 Sep 2025 16:18:20 +0800 Subject: [PATCH 016/257] Update WhatsNewPromo.tsx --- components/WhatsNewPromo.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/WhatsNewPromo.tsx b/components/WhatsNewPromo.tsx index 416a5da05e..25dd3250ca 100644 --- a/components/WhatsNewPromo.tsx +++ b/components/WhatsNewPromo.tsx @@ -96,9 +96,9 @@ const Tile = ({ item }: { item: Item }) => (
- Read update -> + Read update -
+
); From bfcbc46715ae2a8dbb2a5a450d6d16f5f69d9171 Mon Sep 17 00:00:00 2001 From: kurbycchua <50901466+kurbycchua@users.noreply.github.com> Date: Fri, 26 Sep 2025 16:28:53 +0800 Subject: [PATCH 017/257] Update WhatsNewPromo.tsx --- components/WhatsNewPromo.tsx | 39 ++++++++++++++++-------------------- 1 file changed, 17 insertions(+), 22 deletions(-) diff --git a/components/WhatsNewPromo.tsx b/components/WhatsNewPromo.tsx index 25dd3250ca..bc77cf7fe0 100644 --- a/components/WhatsNewPromo.tsx +++ b/components/WhatsNewPromo.tsx @@ -13,7 +13,6 @@ type Item = { category: string; }; -// Read local /pages/changelogs at build/runtime (no network requests) const changelogPages = getPagesUnderRoute('/changelogs'); function buildItems(): Item[] { @@ -28,7 +27,7 @@ function buildItems(): Item[] { .map((p: any) => { const fm = p.frontMatter || p.meta || {}; const route = p.route || ''; - if (!/\/changelogs\/.+/.test(route)) return null; // skip index + if (!/\/changelogs\/.+/.test(route)) return null; const name = p.name || route.split('/').pop() || ''; const date = fm.date || parseDate(name) || parseDate(route); return { @@ -50,7 +49,6 @@ function fmtDate(dateStr: string) { if (isNaN(d as any)) return dateStr || ''; return d.toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' }); } - function isNew(dateStr: string) { const d = new Date(dateStr); if (isNaN(d as any)) return false; @@ -88,17 +86,14 @@ const Tile = ({ item }: { item: Item }) => ( }} /> )} -
+
Read update -
+
); @@ -117,7 +112,7 @@ function Carousel({ items, tileWidth = 420, gap = 24 }: { items: Item[]; tileWid setProgress(max <= 0 ? 0 : el.scrollLeft / max); }; update(); - el.addEventListener('scroll', update, { passive: true } as any); + el.addEventListener('scroll', update as any, { passive: true } as any); window.addEventListener('resize', update); return () => { el.removeEventListener('scroll', update as any); @@ -130,7 +125,8 @@ function Carousel({ items, tileWidth = 420, gap = 24 }: { items: Item[]; tileWid const right = () => ref.current?.scrollBy({ left: page(), behavior: 'smooth' }); return ( -
+
+ {/* soft glow backdrop */}
+ + {/* SCROLLER: grid with horizontal auto-flow */}
- {items.map((i, idx) => ( -
+ {items.map((i) => ( +
))}
+ {/* progress */} {canScroll && (
)} + {/* arrows */} {canScroll && ( <> +
+ {years.map(yv => ( + + ))} +
+ +
- {/* Type filter */} -
- + {/* Filters */} +
+ + {selectedType !== 'All' && ( + + )}
- {/* Timeline by month */} - + + {/* progress */} + {monthCols.length > 0 && ( +
+
+
+ )} + + {/* arrows */} + {monthCols.length > 0 && ( + <> + + + + )}
+ {/* Footer link */}
- Do not see what you are looking for? See the full{' '} + Looking for something older? See the full{' '} Changelog.
From 03835e987a31d0d5877309529292be947016349f Mon Sep 17 00:00:00 2001 From: kurbycchua <50901466+kurbycchua@users.noreply.github.com> Date: Fri, 26 Sep 2025 17:11:03 +0800 Subject: [PATCH 023/257] Update WhatsNewPromo.tsx --- components/WhatsNewPromo.tsx | 151 +++-------------------------------- 1 file changed, 13 insertions(+), 138 deletions(-) diff --git a/components/WhatsNewPromo.tsx b/components/WhatsNewPromo.tsx index 14617a22d3..651626a664 100644 --- a/components/WhatsNewPromo.tsx +++ b/components/WhatsNewPromo.tsx @@ -1,109 +1,9 @@ -'use client'; - -import React, { useEffect, useMemo, useRef, useState } from 'react'; -import { getPagesUnderRoute } from 'nextra/context'; - -type Item = { - url: string; - title: string; - date: string; - description: string; - thumbnail: string; - video: string; - category: string; -}; - -const changelogPages = getPagesUnderRoute('/changelogs'); - -function buildItems(): Item[] { - const parseDate = (s = '') => { - const m = s.match(/(\d{4}-\d{2}-\d{2})/); - return m ? m[1] : ''; - }; - const humanize = (s = '') => - s.replace(/^\d{4}-\d{2}-\d{2}-/, '').replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase()); - - return (changelogPages || []) - .map((p: any) => { - const fm = p.frontMatter || p.meta || {}; - const route = p.route || ''; - if (!/\/changelogs\/.+/.test(route)) return null; - const name = p.name || route.split('/').pop() || ''; - const date = fm.date || parseDate(name) || parseDate(route); - return { - url: route, - title: fm.title || p.title || humanize(name), - date, - description: fm.description || '', - thumbnail: fm.thumbnail || '', - video: fm.video || '', - category: String(fm.category || fm.type || ''), - } as Item; - }) - .filter(Boolean) - .sort((a: Item, b: Item) => new Date(b.date || '').getTime() - new Date(a.date || '').getTime()); -} - -function fmtDate(dateStr: string) { - const d = new Date(dateStr); - if (isNaN(d as any)) return dateStr || ''; - return d.toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' }); -} -function isNew(dateStr: string) { - const d = new Date(dateStr); - if (isNaN(d as any)) return false; - const days = (Date.now() - d.getTime()) / (1000 * 60 * 60 * 24); - return days <= 14; -} - -const Tile = ({ item }: { item: Item }) => ( - -
-
- {fmtDate(item.date)} - {item.category && • {item.category}} - {isNew(item.date) && ( - - NEW - - )} -
-

{item.title}

- {item.description &&

{item.description}

} - -
- {item.thumbnail ? ( - - ) : ( -
- )} -
-
- -
- - Read update - -
-
-
-); - function Carousel({ items }: { items: Item[] }) { const ref = useRef(null); const [canScroll, setCanScroll] = useState(false); const [progress, setProgress] = useState(0); - // fixed tile/gap values (inline styles to defeat theme overrides) + // fixed sizes const TILE_W = 420; const GAP = 24; @@ -129,10 +29,12 @@ function Carousel({ items }: { items: Item[] }) { const right = () => ref.current?.scrollBy({ left: page(), behavior: 'smooth' }); return ( -
- {/* soft glow backdrop */} + // give the container a stacking context +
+ {/* BACKDROP GLOW — move BEHIND the scroller */}
); From ed671ba22b5d5077b14711197196e95f93353c10 Mon Sep 17 00:00:00 2001 From: kurbycchua <50901466+kurbycchua@users.noreply.github.com> Date: Sat, 27 Sep 2025 10:27:13 +0800 Subject: [PATCH 026/257] Create WhatsNewVertical --- components/WhatsNewVertical | 254 ++++++++++++++++++++++++++++++++++++ 1 file changed, 254 insertions(+) create mode 100644 components/WhatsNewVertical diff --git a/components/WhatsNewVertical b/components/WhatsNewVertical new file mode 100644 index 0000000000..dd7f30fe67 --- /dev/null +++ b/components/WhatsNewVertical @@ -0,0 +1,254 @@ +'use client'; + +import React, { useEffect, useMemo, useState } from 'react'; +import { getPagesUnderRoute } from 'nextra/context'; + +type Item = { + url: string; + title: string; + date: string; + description: string; + thumbnail: string; + types: string[]; +}; + +const changelogPages = getPagesUnderRoute('/changelogs'); + +// ---------- helpers ---------- +const parseDate = (s = '') => { + const m = s.match(/(\d{4}-\d{2}-\d{2})/); + return m ? m[1] : ''; +}; +const humanize = (s = '') => + s.replace(/^\d{4}-\d{2}-\d{2}-/, '').replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase()); + +const fmtDay = (dateStr: string) => { + const d = new Date(dateStr); + if (isNaN(d as any)) return dateStr || ''; + return d.toLocaleDateString(undefined, { month: 'short', day: 'numeric' }); +}; +const fullMonth = (dateStr: string) => { + const d = new Date(dateStr); + if (isNaN(d as any)) return 'Unknown'; + return d.toLocaleDateString(undefined, { year: 'numeric', month: 'long' }); +}; +const ymKey = (dateStr: string) => { + const d = new Date(dateStr); + if (isNaN(d as any)) return '0000-00'; + const yyyy = d.getFullYear(); + const mm = String(d.getMonth() + 1).padStart(2, '0'); + return `${yyyy}-${mm}`; +}; + +// Mixpanel-flavored category normalization (lightweight) +const normalizeCategories = (raw: string[], title: string): string[] => { + const txt = (title || '').toLowerCase(); + const set = new Set(); + const add = (k: string) => set.add(k); + + const addFrom = (s: string) => { + const v = s.toLowerCase(); + if (/session\s*replay/.test(v)) add('Session Replay'); + else if (/lexicon/.test(v)) add('Lexicon'); + else if (/(metric|dashboard|report|insight|funnel|cohort)/.test(v)) add('Core Analytics'); + else if (/(sdk|ios|android|javascript|react native|flutter)/.test(v)) add('SDKs'); + else if (/api|export|pipeline|warehouse/.test(v)) add('APIs & Pipelines'); + else if (/(segment|rudder|snowflake|bigquery|dbt|integration)/.test(v)) add('Integrations'); + else if (/(privacy|security|gdpr|governance)/.test(v)) add('Governance & Privacy'); + else if (/(billing|pricing|admin)/.test(v)) add('Admin & Billing'); + else if (/(experiment|beta|flag)/.test(v)) add('Experiments'); + }; + + raw?.forEach(addFrom); + addFrom(txt); + if (set.size === 0) add('Feature'); + return Array.from(set); +}; + +const palette: Record = { + 'Session Replay': '#22c55e', + 'Lexicon': '#06b6d4', + 'Core Analytics': '#8b5cf6', + 'SDKs': '#f59e0b', + 'APIs & Pipelines': '#0ea5e9', + 'Integrations': '#10b981', + 'Governance & Privacy': '#ef4444', + 'Admin & Billing': '#f97316', + 'Experiments': '#eab308', + 'Feature': '#d946ef' +}; +const colorFor = (cat?: string) => palette[cat || 'Feature'] || palette['Feature']; +const chipStyle = (cat?: string) => { + const base = colorFor(cat); + return { + backgroundColor: `${base}1A`, + border: `1px solid ${base}33`, + color: base + } as React.CSSProperties; +}; + +// ---------- build items ---------- +function buildItems(): Item[] { + return (changelogPages || []) + .map((p: any) => { + const fm = p.frontMatter || p.meta || {}; + const route = p.route || ''; + if (!/\/changelogs\/.+/.test(route)) return null; + const name = p.name || route.split('/').pop() || ''; + const date = fm.date || parseDate(name) || parseDate(route); + + const raw: string[] = (() => { + const out: string[] = []; + const cat = fm.category || fm.type; + if (cat) out.push(String(cat)); + const tags = fm.tags; + if (Array.isArray(tags)) out.push(...tags.map(String)); + else if (typeof tags === 'string') out.push(...tags.split(',').map((t: string) => t.trim()).filter(Boolean)); + return out; + })(); + + return { + url: route, + title: fm.title || p.title || humanize(name), + date, + description: fm.description || '', + thumbnail: fm.thumbnail || '', + types: normalizeCategories(raw, fm.title || p.title || name) + } as Item; + }) + .filter(Boolean) + .sort((a: Item, b: Item) => new Date(b.date).getTime() - new Date(a.date).getTime()); +} + +// ---------- UI ---------- +function Card({ item }: { item: Item }) { + return ( + +
+ {/* compact media */} +
+ {item.thumbnail ? ( + + ) : ( +
+ )} +
+ + {/* text content */} +
+
+ {fmtDay(item.date)} + {(item.types || []).slice(0, 2).map((t) => ( + + {t} + + ))} +
+

{item.title}

+ {item.description && ( +

{item.description}

+ )} +
+
+
+ ); +} + +export default function WhatsNewVertical() { + const items = useMemo(buildItems, []); + + // Group by month (desc) + const groups = useMemo(() => { + const map = new Map(); + for (const it of items) { + const k = ymKey(it.date); + if (!map.has(k)) map.set(k, []); + map.get(k)!.push(it); + } + const arr = Array.from(map.entries()).sort((a, b) => (a[0] < b[0] ? 1 : -1)); + return arr.map(([key, list]) => { + const label = fullMonth(list[0]?.date || key + '-01'); + const sorted = list.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()); + return { key, label, list: sorted }; + }); + }, [items]); + + // Default: only newest 2 months expanded + const defaultOpen = useMemo(() => new Set(groups.slice(0, 2).map(g => g.key)), [groups]); + const [open, setOpen] = useState>(defaultOpen); + + useEffect(() => { setOpen(new Set(groups.slice(0, 2).map(g => g.key))); }, [groups]); + + const toggle = (k: string) => { + setOpen(prev => { + const s = new Set(prev); + if (s.has(k)) s.delete(k); else s.add(k); + return s; + }); + }; + + const expandAll = () => setOpen(new Set(groups.map(g => g.key))); + const collapseAll = () => setOpen(new Set(groups.slice(0, 2).map(g => g.key))); + + // small stat + const shipped30d = useMemo(() => { + const cutoff = Date.now() - 30 * 24 * 60 * 60 * 1000; + return items.filter(i => !isNaN(new Date(i.date) as any) && new Date(i.date).getTime() >= cutoff).length; + }, [items]); + + return ( +
+
+

What's New

+

+ Vertical compact view. Latest 2 months are expanded; older months are collapsed. + We shipped {shipped30d} updates in the last 30 days. +

+
+ + +
+
+ +
+ {groups.map(g => { + const isOpen = open.has(g.key); + return ( +
+ {/* Month header */} + + + {/* Items */} + {isOpen && ( +
    + {g.list.map(it => ( +
  • + +
  • + ))} +
+ )} +
+ ); + })} +
+ +
+ Looking for something else? See the full Changelog. +
+
+ ); +} From f408eabfa3f89c0cd0145861decbe3bc8246545d Mon Sep 17 00:00:00 2001 From: kurbycchua <50901466+kurbycchua@users.noreply.github.com> Date: Sat, 27 Sep 2025 10:27:54 +0800 Subject: [PATCH 027/257] Create whats-new-compact --- pages/guides/whats-new-compact | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 pages/guides/whats-new-compact diff --git a/pages/guides/whats-new-compact b/pages/guides/whats-new-compact new file mode 100644 index 0000000000..79e20051be --- /dev/null +++ b/pages/guides/whats-new-compact @@ -0,0 +1,10 @@ +--- +title: "What's New — Compact" +description: "Vertical compact board (months collapsed by default)" +--- + +import WhatsNewVertical from '../../components/WhatsNewVertical' + +
+ +
From 9c60d3753fe517b0f9d6382bf59f67af62684a2f Mon Sep 17 00:00:00 2001 From: kurbycchua <50901466+kurbycchua@users.noreply.github.com> Date: Sat, 27 Sep 2025 10:41:11 +0800 Subject: [PATCH 028/257] Rename whats-new-compact to whats-new-compact.mdx --- pages/guides/{whats-new-compact => whats-new-compact.mdx} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename pages/guides/{whats-new-compact => whats-new-compact.mdx} (100%) diff --git a/pages/guides/whats-new-compact b/pages/guides/whats-new-compact.mdx similarity index 100% rename from pages/guides/whats-new-compact rename to pages/guides/whats-new-compact.mdx From b089a7aae25c449997b1caabd94d5e968caca80b Mon Sep 17 00:00:00 2001 From: kurbycchua <50901466+kurbycchua@users.noreply.github.com> Date: Sat, 27 Sep 2025 10:45:52 +0800 Subject: [PATCH 029/257] Rename WhatsNewVertical to WhatsNewVertical.tsx --- components/{WhatsNewVertical => WhatsNewVertical.tsx} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename components/{WhatsNewVertical => WhatsNewVertical.tsx} (100%) diff --git a/components/WhatsNewVertical b/components/WhatsNewVertical.tsx similarity index 100% rename from components/WhatsNewVertical rename to components/WhatsNewVertical.tsx From 6f3f7d1f2eca01cad1f5b15bc6e1fe9d5813ed9f Mon Sep 17 00:00:00 2001 From: kurbycchua <50901466+kurbycchua@users.noreply.github.com> Date: Sat, 27 Sep 2025 11:06:10 +0800 Subject: [PATCH 030/257] Update WhatsNewVertical.tsx --- components/WhatsNewVertical.tsx | 292 +++++++++++--------------------- 1 file changed, 102 insertions(+), 190 deletions(-) diff --git a/components/WhatsNewVertical.tsx b/components/WhatsNewVertical.tsx index dd7f30fe67..b9da143bc6 100644 --- a/components/WhatsNewVertical.tsx +++ b/components/WhatsNewVertical.tsx @@ -1,15 +1,13 @@ 'use client'; -import React, { useEffect, useMemo, useState } from 'react'; +import React, { useMemo, useState } from 'react'; import { getPagesUnderRoute } from 'nextra/context'; type Item = { url: string; title: string; date: string; - description: string; thumbnail: string; - types: string[]; }; const changelogPages = getPagesUnderRoute('/changelogs'); @@ -25,230 +23,144 @@ const humanize = (s = '') => const fmtDay = (dateStr: string) => { const d = new Date(dateStr); if (isNaN(d as any)) return dateStr || ''; - return d.toLocaleDateString(undefined, { month: 'short', day: 'numeric' }); -}; -const fullMonth = (dateStr: string) => { - const d = new Date(dateStr); - if (isNaN(d as any)) return 'Unknown'; - return d.toLocaleDateString(undefined, { year: 'numeric', month: 'long' }); -}; -const ymKey = (dateStr: string) => { - const d = new Date(dateStr); - if (isNaN(d as any)) return '0000-00'; - const yyyy = d.getFullYear(); - const mm = String(d.getMonth() + 1).padStart(2, '0'); - return `${yyyy}-${mm}`; -}; - -// Mixpanel-flavored category normalization (lightweight) -const normalizeCategories = (raw: string[], title: string): string[] => { - const txt = (title || '').toLowerCase(); - const set = new Set(); - const add = (k: string) => set.add(k); - - const addFrom = (s: string) => { - const v = s.toLowerCase(); - if (/session\s*replay/.test(v)) add('Session Replay'); - else if (/lexicon/.test(v)) add('Lexicon'); - else if (/(metric|dashboard|report|insight|funnel|cohort)/.test(v)) add('Core Analytics'); - else if (/(sdk|ios|android|javascript|react native|flutter)/.test(v)) add('SDKs'); - else if (/api|export|pipeline|warehouse/.test(v)) add('APIs & Pipelines'); - else if (/(segment|rudder|snowflake|bigquery|dbt|integration)/.test(v)) add('Integrations'); - else if (/(privacy|security|gdpr|governance)/.test(v)) add('Governance & Privacy'); - else if (/(billing|pricing|admin)/.test(v)) add('Admin & Billing'); - else if (/(experiment|beta|flag)/.test(v)) add('Experiments'); - }; - - raw?.forEach(addFrom); - addFrom(txt); - if (set.size === 0) add('Feature'); - return Array.from(set); -}; - -const palette: Record = { - 'Session Replay': '#22c55e', - 'Lexicon': '#06b6d4', - 'Core Analytics': '#8b5cf6', - 'SDKs': '#f59e0b', - 'APIs & Pipelines': '#0ea5e9', - 'Integrations': '#10b981', - 'Governance & Privacy': '#ef4444', - 'Admin & Billing': '#f97316', - 'Experiments': '#eab308', - 'Feature': '#d946ef' -}; -const colorFor = (cat?: string) => palette[cat || 'Feature'] || palette['Feature']; -const chipStyle = (cat?: string) => { - const base = colorFor(cat); - return { - backgroundColor: `${base}1A`, - border: `1px solid ${base}33`, - color: base - } as React.CSSProperties; + return d.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' }); }; -// ---------- build items ---------- +// ---------- build items (latest first) ---------- function buildItems(): Item[] { return (changelogPages || []) .map((p: any) => { const fm = p.frontMatter || p.meta || {}; const route = p.route || ''; - if (!/\/changelogs\/.+/.test(route)) return null; + if (!/\/changelogs\/.+/.test(route)) return null; // skip /changelogs index const name = p.name || route.split('/').pop() || ''; const date = fm.date || parseDate(name) || parseDate(route); - - const raw: string[] = (() => { - const out: string[] = []; - const cat = fm.category || fm.type; - if (cat) out.push(String(cat)); - const tags = fm.tags; - if (Array.isArray(tags)) out.push(...tags.map(String)); - else if (typeof tags === 'string') out.push(...tags.split(',').map((t: string) => t.trim()).filter(Boolean)); - return out; - })(); - return { url: route, title: fm.title || p.title || humanize(name), date, - description: fm.description || '', - thumbnail: fm.thumbnail || '', - types: normalizeCategories(raw, fm.title || p.title || name) + thumbnail: fm.thumbnail || '' } as Item; }) .filter(Boolean) - .sort((a: Item, b: Item) => new Date(b.date).getTime() - new Date(a.date).getTime()); + .sort( + (a: Item, b: Item) => + new Date(b.date || '').getTime() - new Date(a.date || '').getTime() + ); } // ---------- UI ---------- -function Card({ item }: { item: Item }) { +function Row({ item }: { item: Item }) { return ( - -
- {/* compact media */} -
- {item.thumbnail ? ( - - ) : ( -
- )} -
+
  • + +
  • ); } export default function WhatsNewVertical() { const items = useMemo(buildItems, []); - // Group by month (desc) - const groups = useMemo(() => { - const map = new Map(); - for (const it of items) { - const k = ymKey(it.date); - if (!map.has(k)) map.set(k, []); - map.get(k)!.push(it); - } - const arr = Array.from(map.entries()).sort((a, b) => (a[0] < b[0] ? 1 : -1)); - return arr.map(([key, list]) => { - const label = fullMonth(list[0]?.date || key + '-01'); - const sorted = list.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()); - return { key, label, list: sorted }; - }); - }, [items]); - - // Default: only newest 2 months expanded - const defaultOpen = useMemo(() => new Set(groups.slice(0, 2).map(g => g.key)), [groups]); - const [open, setOpen] = useState>(defaultOpen); - - useEffect(() => { setOpen(new Set(groups.slice(0, 2).map(g => g.key))); }, [groups]); - - const toggle = (k: string) => { - setOpen(prev => { - const s = new Set(prev); - if (s.has(k)) s.delete(k); else s.add(k); - return s; - }); - }; + // paging (latest X) + const [pageSize, setPageSize] = useState(10); // default X = 10 + const [offset, setOffset] = useState(0); - const expandAll = () => setOpen(new Set(groups.map(g => g.key))); - const collapseAll = () => setOpen(new Set(groups.slice(0, 2).map(g => g.key))); + const total = items.length; + const start = offset; + const end = Math.min(offset + pageSize, total); + const page = items.slice(start, end); + + const canPrev = start > 0; + const canNext = end < total; + + const changeSize = (n: number) => { + setPageSize(n); + setOffset(0); // reset to latest whenever size changes + }; - // small stat - const shipped30d = useMemo(() => { - const cutoff = Date.now() - 30 * 24 * 60 * 60 * 1000; - return items.filter(i => !isNaN(new Date(i.date) as any) && new Date(i.date).getTime() >= cutoff).length; - }, [items]); + const prev = () => setOffset(Math.max(0, offset - pageSize)); + const next = () => setOffset(Math.min(total, offset + pageSize)); return ( -
    -
    +
    + {/* Header + controls */} +

    What's New

    -

    - Vertical compact view. Latest 2 months are expanded; older months are collapsed. - We shipped {shipped30d} updates in the last 30 days. -

    -
    - - + +
    + + + +
    + + +
    -
    - -
    - {groups.map(g => { - const isOpen = open.has(g.key); - return ( -
    - {/* Month header */} - - - {/* Items */} - {isOpen && ( -
      - {g.list.map(it => ( -
    • - -
    • - ))} -
    - )} -
    - ); - })}
    -
    - Looking for something else? See the full Changelog. -
    + {/* status */} +
    + Showing {total === 0 ? 0 : start + 1}–{end} of {total} +
    + + {/* list */} +
      + {page.map(item => )} +
    + + {/* footer link */} +
    + Looking for more? See the full Changelog. +
    ); } From 53caa6237408e1e57c0d36d457e4c9fc908b2de0 Mon Sep 17 00:00:00 2001 From: kurbycchua <50901466+kurbycchua@users.noreply.github.com> Date: Sat, 27 Sep 2025 11:15:19 +0800 Subject: [PATCH 031/257] Update WhatsNewVertical.tsx --- components/WhatsNewVertical.tsx | 79 +++++++++++++++++---------------- 1 file changed, 40 insertions(+), 39 deletions(-) diff --git a/components/WhatsNewVertical.tsx b/components/WhatsNewVertical.tsx index b9da143bc6..2ed604b76c 100644 --- a/components/WhatsNewVertical.tsx +++ b/components/WhatsNewVertical.tsx @@ -26,7 +26,7 @@ const fmtDay = (dateStr: string) => { return d.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' }); }; -// ---------- build items (latest first) ---------- +// ---------- build items (newest first) ---------- function buildItems(): Item[] { return (changelogPages || []) .map((p: any) => { @@ -43,10 +43,8 @@ function buildItems(): Item[] { } as Item; }) .filter(Boolean) - .sort( - (a: Item, b: Item) => - new Date(b.date || '').getTime() - new Date(a.date || '').getTime() - ); + .sort((a: Item, b: Item) => new Date(b.date).getTime() - new Date(a.date).getTime()) + .reverse(); } // ---------- UI ---------- @@ -54,9 +52,9 @@ function Row({ item }: { item: Item }) { return (
  • -
    - {/* compact media (smaller but not tiny) */} -
    +
    + {/* compact media (between preview and old size) */} +
    {item.thumbnail ? ( ) : ( @@ -85,8 +83,8 @@ function Row({ item }: { item: Item }) { export default function WhatsNewVertical() { const items = useMemo(buildItems, []); - // paging (latest X) - const [pageSize, setPageSize] = useState(10); // default X = 10 + // paging (Latest X) + const [pageSize, setPageSize] = useState(5); // default to 5 to match preview feel const [offset, setOffset] = useState(0); const total = items.length; @@ -99,7 +97,7 @@ export default function WhatsNewVertical() { const changeSize = (n: number) => { setPageSize(n); - setOffset(0); // reset to latest whenever size changes + setOffset(0); // reset to latest batch when size changes }; const prev = () => setOffset(Math.max(0, offset - pageSize)); @@ -107,54 +105,57 @@ export default function WhatsNewVertical() { return (
    - {/* Header + controls */} -
    -

    What's New

    + {/* Header row */} +
    +

    What's New

    + {/* Controls aligned top-right */}
    - + Show -
    - - -
    + +
    {/* status */} -
    +
    Showing {total === 0 ? 0 : start + 1}–{end} of {total}
    {/* list */}
      - {page.map(item => )} + {page.map((item) => ( + + ))}
    {/* footer link */} From cbf88c4e7650beebb2d2502342da0aa6772a0781 Mon Sep 17 00:00:00 2001 From: kurbycchua <50901466+kurbycchua@users.noreply.github.com> Date: Sat, 27 Sep 2025 13:20:04 +0800 Subject: [PATCH 032/257] Update WhatsNewVertical.tsx --- components/WhatsNewVertical.tsx | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/components/WhatsNewVertical.tsx b/components/WhatsNewVertical.tsx index 2ed604b76c..90119ce37f 100644 --- a/components/WhatsNewVertical.tsx +++ b/components/WhatsNewVertical.tsx @@ -35,11 +35,13 @@ function buildItems(): Item[] { if (!/\/changelogs\/.+/.test(route)) return null; // skip /changelogs index const name = p.name || route.split('/').pop() || ''; const date = fm.date || parseDate(name) || parseDate(route); + const thumb = + fm.thumbnail || fm.image || fm.cover || fm.screenshot || fm.hero || ''; return { url: route, title: fm.title || p.title || humanize(name), date, - thumbnail: fm.thumbnail || '' + thumbnail: thumb } as Item; }) .filter(Boolean) @@ -50,10 +52,10 @@ function buildItems(): Item[] { // ---------- UI ---------- function Row({ item }: { item: Item }) { return ( -
  • +
  • - {/* compact media (between preview and old size) */} + {/* compact media (between previous big and preview small) */}
    {item.thumbnail ? ( @@ -84,7 +86,7 @@ export default function WhatsNewVertical() { const items = useMemo(buildItems, []); // paging (Latest X) - const [pageSize, setPageSize] = useState(5); // default to 5 to match preview feel + const [pageSize, setPageSize] = useState(5); const [offset, setOffset] = useState(0); const total = items.length; @@ -97,7 +99,7 @@ export default function WhatsNewVertical() { const changeSize = (n: number) => { setPageSize(n); - setOffset(0); // reset to latest batch when size changes + setOffset(0); // reset to latest }; const prev = () => setOffset(Math.max(0, offset - pageSize)); @@ -105,12 +107,11 @@ export default function WhatsNewVertical() { return (
    - {/* Header row */} -
    + {/* Header grid keeps controls pinned right even in narrow columns */} +

    What's New

    - {/* Controls aligned top-right */} -
    +
    Show changeSize(Number(e.target.value))} - aria-label="Select how many latest updates to show" - > - {[5, 10, 15, 20].map(n => ( - - ))} - - - - -
    -
    - - {/* status */} -
    - Showing {total === 0 ? 0 : start + 1}–{end} of {total} -
    - - {/* list */} -
      - {page.map((item) => ( - - ))} -
    - - {/* footer link */} -
    -
    - ); -} From 367e59decc3acc4823991e8c07b5fbf5133c6e04 Mon Sep 17 00:00:00 2001 From: kurbycchua <50901466+kurbycchua@users.noreply.github.com> Date: Sat, 27 Sep 2025 13:28:48 +0800 Subject: [PATCH 034/257] Create WhatsNewVertical.tsx --- components/WhatsNewVertical.tsx | 168 ++++++++++++++++++++++++++++++++ 1 file changed, 168 insertions(+) create mode 100644 components/WhatsNewVertical.tsx diff --git a/components/WhatsNewVertical.tsx b/components/WhatsNewVertical.tsx new file mode 100644 index 0000000000..90119ce37f --- /dev/null +++ b/components/WhatsNewVertical.tsx @@ -0,0 +1,168 @@ +'use client'; + +import React, { useMemo, useState } from 'react'; +import { getPagesUnderRoute } from 'nextra/context'; + +type Item = { + url: string; + title: string; + date: string; + thumbnail: string; +}; + +const changelogPages = getPagesUnderRoute('/changelogs'); + +// ---------- helpers ---------- +const parseDate = (s = '') => { + const m = s.match(/(\d{4}-\d{2}-\d{2})/); + return m ? m[1] : ''; +}; +const humanize = (s = '') => + s.replace(/^\d{4}-\d{2}-\d{2}-/, '').replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase()); + +const fmtDay = (dateStr: string) => { + const d = new Date(dateStr); + if (isNaN(d as any)) return dateStr || ''; + return d.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' }); +}; + +// ---------- build items (newest first) ---------- +function buildItems(): Item[] { + return (changelogPages || []) + .map((p: any) => { + const fm = p.frontMatter || p.meta || {}; + const route = p.route || ''; + if (!/\/changelogs\/.+/.test(route)) return null; // skip /changelogs index + const name = p.name || route.split('/').pop() || ''; + const date = fm.date || parseDate(name) || parseDate(route); + const thumb = + fm.thumbnail || fm.image || fm.cover || fm.screenshot || fm.hero || ''; + return { + url: route, + title: fm.title || p.title || humanize(name), + date, + thumbnail: thumb + } as Item; + }) + .filter(Boolean) + .sort((a: Item, b: Item) => new Date(b.date).getTime() - new Date(a.date).getTime()) + .reverse(); +} + +// ---------- UI ---------- +function Row({ item }: { item: Item }) { + return ( +
  • + +
  • + ); +} + +export default function WhatsNewVertical() { + const items = useMemo(buildItems, []); + + // paging (Latest X) + const [pageSize, setPageSize] = useState(5); + const [offset, setOffset] = useState(0); + + const total = items.length; + const start = offset; + const end = Math.min(offset + pageSize, total); + const page = items.slice(start, end); + + const canPrev = start > 0; + const canNext = end < total; + + const changeSize = (n: number) => { + setPageSize(n); + setOffset(0); // reset to latest + }; + + const prev = () => setOffset(Math.max(0, offset - pageSize)); + const next = () => setOffset(Math.min(total, offset + pageSize)); + + return ( +
    + {/* Header grid keeps controls pinned right even in narrow columns */} +
    +

    What's New

    + +
    + Show + + + + +
    +
    + + {/* status */} +
    + Showing {total === 0 ? 0 : start + 1}–{end} of {total} +
    + + {/* list */} +
      + {page.map((item) => ( + + ))} +
    + + {/* footer link */} +
    + Looking for more? See the full Changelog. +
    +
    + ); +} From d7373e82bf526d474c29224d11aaf6fbe27aa55d Mon Sep 17 00:00:00 2001 From: kurbycchua <50901466+kurbycchua@users.noreply.github.com> Date: Sat, 27 Sep 2025 13:37:47 +0800 Subject: [PATCH 035/257] Update WhatsNewVertical.tsx --- components/WhatsNewVertical.tsx | 71 +++++++++++++++++++++++---------- 1 file changed, 51 insertions(+), 20 deletions(-) diff --git a/components/WhatsNewVertical.tsx b/components/WhatsNewVertical.tsx index 90119ce37f..3574a31139 100644 --- a/components/WhatsNewVertical.tsx +++ b/components/WhatsNewVertical.tsx @@ -51,18 +51,45 @@ function buildItems(): Item[] { // ---------- UI ---------- function Row({ item }: { item: Item }) { + // explicit inline sizing so we’re not relying on arbitrary Tailwind sizes + const thumbStyle: React.CSSProperties = { + width: 120, + height: 72, + borderRadius: 8, + overflow: 'hidden', + background: 'rgba(0,0,0,0.1)', + flex: '0 0 auto' + }; + + const titleStyle: React.CSSProperties = { + marginTop: 2, + fontSize: 15, + fontWeight: 600, + lineHeight: 1.25, + display: '-webkit-box', + WebkitLineClamp: 2, + WebkitBoxOrient: 'vertical', + overflow: 'hidden' + }; + return ( -
  • - -
    - {/* compact media (between previous big and preview small) */} -
    +
  • + +
    +
    {item.thumbnail ? ( - + // eslint-disable-next-line @next/next/no-img-element + ) : (
    -
    +
    {fmtDay(item.date)}
    -

    - {item.title} -

    +

    {item.title}

    @@ -105,13 +130,21 @@ export default function WhatsNewVertical() { const prev = () => setOffset(Math.max(0, offset - pageSize)); const next = () => setOffset(Math.min(total, offset + pageSize)); + // header grid keeps controls pinned right even on narrow center columns + const headerGrid: React.CSSProperties = { + display: 'grid', + gridTemplateColumns: '1fr auto', + alignItems: 'center', + gap: 12, + marginBottom: 16 + }; + return ( -
    - {/* Header grid keeps controls pinned right even in narrow columns */} -
    +
    +

    What's New

    -
    +
    Show changeSize(Number(e.target.value))} + aria-label="Select how many latest updates to show" + > + {[5, 10, 15, 20].map(n => ( + + ))} + + + + +
    + ); +} + +function ControlsBottom({ + canPrev, + canNext, + prev, + next +}: { + canPrev: boolean; + canNext: boolean; + prev: () => void; + next: () => void; +}) { + return ( +
    + + +
    + ); } -// ---------- UI ---------- +// ---------- Card ---------- function Row({ item }: { item: Item }) { - // Stacked layout: big 16:9 preview, then date + title + const headerRow: React.CSSProperties = { + display: 'grid', + gridTemplateColumns: '1fr auto', + alignItems: 'center', + gap: 12, + marginBottom: 8 + }; + const imgWrap: React.CSSProperties = { width: '100%', borderRadius: 12, @@ -79,20 +228,23 @@ function Row({ item }: { item: Item }) { background: 'radial-gradient(120% 120% at 0% 100%, rgba(168,85,247,0.18), transparent 60%), radial-gradient(120% 120% at 100% 0%, rgba(59,130,246,0.18), transparent 60%)' }; - const titleStyle: React.CSSProperties = { - marginTop: 4, - fontSize: 16, - fontWeight: 600, - lineHeight: 1.3, - display: '-webkit-box', - WebkitLineClamp: 2, - WebkitBoxOrient: 'vertical', - overflow: 'hidden' - }; return (
  • + {/* Title (2 lines) left, date right */} +
    +

    + {item.title} +

    +
    {fmtDay(item.date)}
    +
    + + {/* Image */}
    {item.thumbnail ? ( // eslint-disable-next-line @next/next/no-img-element @@ -107,15 +259,26 @@ function Row({ item }: { item: Item }) { )}
    -
    {fmtDay(item.date)}
    -

    {item.title}

    + {/* Summary (3 lines) if present */} + {item.description ? ( +

    + {item.description} +

    + ) : null} + + {/* Inline read link */} +
    + Read update → +
  • ); } +// ---------- Main ---------- export default function WhatsNewVertical() { const items = useMemo(buildItems, []); + const shipped30 = countLast30d(items); // paging (Latest X) const [pageSize, setPageSize] = useState(5); @@ -131,75 +294,72 @@ export default function WhatsNewVertical() { const changeSize = (n: number) => { setPageSize(n); - setOffset(0); // reset to newest batch when size changes + setOffset(0); // back to newest window on size change }; - const prev = () => setOffset(Math.max(0, offset - pageSize)); const next = () => setOffset(Math.min(total, offset + pageSize)); - // Header grid: title left, controls pinned right + // Header grid: hero left, controls right const headerGrid: React.CSSProperties = { display: 'grid', gridTemplateColumns: '1fr auto', alignItems: 'center', - gap: 12, + gap: 16, marginBottom: 16 }; return (
    + {/* Hero (Variant B) */}
    -

    What's New

    - -
    - Show - - - - +
    +

    What's New

    +

    + Stay up to date with Mixpanel product releases and improvements. We shipped{' '} + {shipped30} updates in the last 30 days. +

    +
    + + {/* Top controls */} +
    -
    + {/* Status */} +
    Showing {total === 0 ? 0 : start + 1}–{end} of {total}
    + {/* List */}
      {page.map((item) => ( ))}
    -
    - Looking for more? See the full Changelog. + {/* Bottom controls (Prev/Next only) */} + + + {/* CTA */} +
    ); From 254d44786f06a204ed30dc0d5a3f2e2f35ffff4b Mon Sep 17 00:00:00 2001 From: kurbycchua <50901466+kurbycchua@users.noreply.github.com> Date: Sat, 27 Sep 2025 14:37:08 +0800 Subject: [PATCH 039/257] Update WhatsNewVertical.tsx --- components/WhatsNewVertical.tsx | 172 +++++++++++--------------------- 1 file changed, 57 insertions(+), 115 deletions(-) diff --git a/components/WhatsNewVertical.tsx b/components/WhatsNewVertical.tsx index b5a9009eee..1e0f7e680b 100644 --- a/components/WhatsNewVertical.tsx +++ b/components/WhatsNewVertical.tsx @@ -13,7 +13,7 @@ type Item = { const changelogPages = getPagesUnderRoute('/changelogs'); -// ---------- helpers ---------- +/* ---------- helpers ---------- */ const parseDate = (s = '') => { const m = s.match(/(\d{4}-\d{2}-\d{2})/); return m ? m[1] : ''; @@ -34,27 +34,23 @@ const firstNonEmpty = (...vals: any[]) => return typeof v === 'string'; }); -// Count items in last 30 days -const countLast30d = (items: Item[]) => { - const cutoff = Date.now() - 30 * 24 * 60 * 60 * 1000; - return items.filter((i) => { - const t = new Date(i.date).getTime(); - return !isNaN(t) && t >= cutoff; - }).length; -}; +const clampStyle = (lines: number): React.CSSProperties => ({ + display: '-webkit-box', + WebkitLineClamp: lines, + WebkitBoxOrient: 'vertical', + overflow: 'hidden' +}); -// ---------- build items (NEWEST first) ---------- +/* ---------- build items (NEWEST → OLDEST) ---------- */ function buildItems(): Item[] { return (changelogPages || []) .map((p: any) => { const fm = p.frontMatter || p.meta || {}; const route = p.route || ''; - if (!/\/changelogs\/.+/.test(route)) return null; // skip index + if (!/\/changelogs\/.+/.test(route)) return null; // skip /changelogs index const name = p.name || route.split('/').pop() || ''; const date = fm.date || parseDate(name) || parseDate(route); - - // smart thumbnail fallback across common keys const thumb = firstNonEmpty( fm.thumbnail, fm.image, @@ -74,61 +70,17 @@ function buildItems(): Item[] { } as Item; }) .filter(Boolean) - .sort((a: Item, b: Item) => new Date(b.date).getTime() - new Date(a.date).getTime()) - .reverse(); + .sort((a: Item, b: Item) => new Date(b.date).getTime() - new Date(a.date).getTime()); // NEWEST FIRST } -// ---------- minor utilities ---------- -const clampStyle = (lines: number): React.CSSProperties => ({ - display: '-webkit-box', - WebkitLineClamp: lines, - WebkitBoxOrient: 'vertical', - overflow: 'hidden' -}); - -function SubscribeButton() { - // Tries to open the existing changelog modal (if present on the page). - const onClick = () => { - try { - // Example hooks you might have on /changelogs: - // 1) A button with data attribute to open modal - const trigger = - (document.querySelector('[data-changelog-subscribe]') as HTMLElement) || - (document.querySelector('button[aria-label="Subscribe"]') as HTMLElement); - if (trigger) { - trigger.click(); - return; - } - // 2) A global function (if exposed) - // @ts-ignore - if (typeof window.openChangelogSubscribe === 'function') { - // @ts-ignore - window.openChangelogSubscribe(); - return; - } - } catch {} - // Fallback - window.location.href = '/changelogs#subscribe'; - }; - - return ( - - ); -} - -// ---------- Controls ---------- +/* ---------- controls ---------- */ function ControlsTop({ pageSize, canPrev, canNext, changeSize, prev, - next + next, }: { pageSize: number; canPrev: boolean; @@ -138,7 +90,7 @@ function ControlsTop({ next: () => void; }) { return ( -
    +
    Show void; }) { return ( -
    +
    - -
    - ); -} - -function ControlsBottom({ - canPrev, - canNext, - prev, - next, -}: { - canPrev: boolean; - canNext: boolean; - prev: () => void; - next: () => void; -}) { - return ( -
    - - -
    - ); -} - -/* ---------- card ---------- */ -function Row({ item }: { item: Item }) { - const headerRow: React.CSSProperties = { - display: 'grid', - gridTemplateColumns: '1fr auto', - alignItems: 'center', - gap: 12, - marginBottom: 8, - }; - - const imgWrap: React.CSSProperties = { - width: '100%', - borderRadius: 12, - overflow: 'hidden', - aspectRatio: '16 / 9', - background: - 'radial-gradient(120% 120% at 0% 100%, rgba(168,85,247,0.18), transparent 60%), radial-gradient(120% 120% at 100% 0%, rgba(59,130,246,0.18), transparent 60%)', - }; - - return ( -
  • - -
    -

    - {item.title} -

    -
    {fmtDay(item.date)}
    -
    - -
  • - ); -} - -/* ---------- main ---------- */ -export default function WhatsNewVertical() { - const items = useMemo(buildItems, []); - - // paging (Latest X) - const [pageSize, setPageSize] = useState(5); - const [offset, setOffset] = useState(0); - - const total = items.length; - const start = offset; - const end = Math.min(offset + pageSize, total); - const page = items.slice(start, end); - - const canPrev = start > 0; - const canNext = end < total; - - const changeSize = (n: number) => { - setPageSize(n); - setOffset(0); - }; - const prev = () => setOffset(Math.max(0, offset - pageSize)); - const next = () => setOffset(Math.min(total, offset + pageSize)); - - return ( -
    - {/* HERO */} -
    -

    What's New

    - -

    - Track Mixpanel product releases and improvements in one place. See what’s - new, what got faster, and what opens up entirely new ways to answer questions about your - product. These changes are built from customer feedback and real workflows—less setup, - fewer manual steps, clearer answers. -

    -

    - From performance boosts to streamlined analysis and collaboration, each release is here to - shorten the path from “what happened?” to “what should we do?”. Browse the highlights below - and put the most impactful updates to work on your team today. -

    - - -
    - - {/* TOP CONTROLS — single line */} -
    -
    - Showing {total === 0 ? 0 : start + 1}–{end} of {total} -
    -
    - -
    -
    - - {/* LIST */} -
      - {page.map((item) => ( - - ))} -
    - - {/* BOTTOM BAR — single line */} - -
    - ); -} From 0af32c24898c16d0828c20af0f69132817d15159 Mon Sep 17 00:00:00 2001 From: kurbycchua <50901466+kurbycchua@users.noreply.github.com> Date: Sat, 27 Sep 2025 21:32:08 +0800 Subject: [PATCH 044/257] Create WhatsNewVertical.tsx --- components/WhatsNewVertical.tsx | 339 ++++++++++++++++++++++++++++++++ 1 file changed, 339 insertions(+) create mode 100644 components/WhatsNewVertical.tsx diff --git a/components/WhatsNewVertical.tsx b/components/WhatsNewVertical.tsx new file mode 100644 index 0000000000..748210b555 --- /dev/null +++ b/components/WhatsNewVertical.tsx @@ -0,0 +1,339 @@ +'use client'; + +import React, { useMemo, useState } from 'react'; +import { getPagesUnderRoute } from 'nextra/context'; + +type Item = { + url: string; + title: string; + date: string; + thumbnail: string; +}; + +const changelogPages = getPagesUnderRoute('/changelogs'); + +/* ---------- helpers ---------- */ +const parseDate = (s = '') => { + const m = s.match(/(\d{4}-\d{2}-\d{2})/); + return m ? m[1] : ''; +}; +const humanize = (s = '') => + s.replace(/^\d{4}-\d{2}-\d{2}-/, '').replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase()); + +const fmtDay = (dateStr: string) => { + const d = new Date(dateStr); + if (isNaN(d as any)) return dateStr || ''; + return d.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' }); +}; + +const firstNonEmpty = (...vals: any[]) => + vals.find((v) => { + if (!v) return false; + if (Array.isArray(v)) return v.length > 0 && typeof v[0] === 'string'; + return typeof v === 'string'; + }); + +const clampStyle = (lines: number): React.CSSProperties => ({ + display: '-webkit-box', + WebkitLineClamp: lines, + WebkitBoxOrient: 'vertical', + overflow: 'hidden' +}); + +/* ---------- build items (NEWEST → OLDEST) ---------- */ +function buildItems(): Item[] { + return (changelogPages || []) + .map((p: any) => { + const fm = p.frontMatter || p.meta || {}; + const route = p.route || ''; + if (!/\/changelogs\/.+/.test(route)) return null; + + const name = p.name || route.split('/').pop() || ''; + const date = fm.date || parseDate(name) || parseDate(route); + const thumb = firstNonEmpty( + fm.thumbnail, + fm.image, + fm.cover, + fm.ogImage, + fm.hero, + fm.screenshot, + Array.isArray(fm.images) ? fm.images[0] : undefined + ) as string | undefined; + + return { + url: route, + title: fm.title || p.title || humanize(name), + date, + thumbnail: thumb || '' + } as Item; + }) + .filter(Boolean) + .sort((a: Item, b: Item) => new Date(b.date).getTime() - new Date(a.date).getTime()); +} + +/* ---------- shared inline styles ---------- */ +const s = { + page: { maxWidth: 880, margin: '0 auto' }, + h1: { + fontSize: '44px', + lineHeight: 1.1, + fontWeight: 600 as const, + letterSpacing: '-0.02em' as const, + margin: 0, + }, + heroP: { + marginTop: 12, + fontSize: 15, + lineHeight: 1.6, + color: 'rgba(255,255,255,0.8)', + }, + heroLink: { + marginTop: 12, + fontSize: 14, + textDecoration: 'underline', + textUnderlineOffset: '4px', + color: 'rgba(255,255,255,0.85)', + display: 'inline-block', + }, + rowBar: { + marginTop: 24, + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + gap: 12, + }, + showing: { + fontSize: 12, + color: 'rgba(255,255,255,0.55)', + }, + controlsWrap: { whiteSpace: 'nowrap' as const, minWidth: 0 }, + btn: { + fontSize: 12, + padding: '6px 8px', + borderRadius: 6, + border: '1px solid rgba(255,255,255,0.18)', + background: 'transparent', + color: 'inherit', + cursor: 'pointer', + }, + select: { + fontSize: 12, + padding: '6px 8px', + borderRadius: 6, + border: '1px solid rgba(255,255,255,0.18)', + background: 'transparent', + color: 'inherit', + }, + cardLi: { padding: '12px 0' }, + cardA: { display: 'block', borderRadius: 12, padding: 12, textDecoration: 'none' }, + cardHeader: { + display: 'grid', + gridTemplateColumns: '1fr auto', + alignItems: 'center', + gap: 12, + marginBottom: 8, + }, + cardTitle: { + fontSize: 20, + fontWeight: 600 as const, + lineHeight: 1.2, + textDecorationThickness: '1px', + textUnderlineOffset: '4px', + }, + cardDate: { fontSize: 12, color: 'rgba(255,255,255,0.55)' }, + imgWrap: { + width: '100%', + borderRadius: 12, + overflow: 'hidden', + aspectRatio: '16 / 9', + background: + 'radial-gradient(120% 120% at 0% 100%, rgba(168,85,247,0.18), transparent 60%), radial-gradient(120% 120% at 100% 0%, rgba(59,130,246,0.18), transparent 60%)', + }, + readLink: { + marginTop: 6, + fontSize: 13, + textDecoration: 'underline', + textUnderlineOffset: '4px', + display: 'inline-block', + }, + footerLink: { + fontSize: 14, + color: 'rgb(167 139 250)', // violet-ish + textDecoration: 'underline', + textUnderlineOffset: '4px', + }, +}; + +/* ---------- control components (inline-styled) ---------- */ +function ControlsTop({ + pageSize, + canPrev, + canNext, + changeSize, + prev, + next, +}: { + pageSize: number; + canPrev: boolean; + canNext: boolean; + changeSize: (n: number) => void; + prev: () => void; + next: () => void; +}) { + return ( +
    + Show + + + +
    + ); +} + +function ControlsBottom({ + canPrev, + canNext, + prev, + next, +}: { + canPrev: boolean; + canNext: boolean; + prev: () => void; + next: () => void; +}) { + return ( +
    + + +
    + ); +} + +/* ---------- card ---------- */ +function Row({ item }: { item: Item }) { + return ( +
  • + +
    +

    + {item.title} +

    +
    {fmtDay(item.date)}
    +
    + +
  • + ); +} + +/* ---------- main ---------- */ +export default function WhatsNewVertical() { + const items = useMemo(buildItems, []); + + // paging (Latest X) + const [pageSize, setPageSize] = useState(5); + const [offset, setOffset] = useState(0); + + const total = items.length; + const start = offset; + const end = Math.min(offset + pageSize, total); + const page = items.slice(start, end); + + const canPrev = start > 0; + const canNext = end < total; + + const changeSize = (n: number) => { + setPageSize(n); + setOffset(0); + }; + const prev = () => setOffset(Math.max(0, offset - pageSize)); + const next = () => setOffset(Math.min(total, offset + pageSize)); + + return ( +
    + {/* HERO */} +
    +

    What's New

    + +

    + Track Mixpanel product releases and improvements in one place. See what’s + new, what got faster, and what opens up entirely new ways to answer questions about your + product. These changes are built from customer feedback and real workflows—less setup, + fewer manual steps, clearer answers. +

    +

    + From performance boosts to streamlined analysis and collaboration, each release is here to + shorten the path from “what happened?” to “what should we do?”. Browse the highlights below + and put the most impactful updates to work on your team today. +

    + + + Browse Changelog + +
    + + {/* TOP BAR — single line */} +
    +
    + Showing {total === 0 ? 0 : start + 1}–{end} of {total} +
    + +
    + + {/* LIST */} +
      + {page.map((item) => ( + + ))} +
    + + {/* BOTTOM BAR — single line */} + +
    + ); +} From 2319fc100f829fb4114f956b536ee55adfa0a0d8 Mon Sep 17 00:00:00 2001 From: kurbycchua <50901466+kurbycchua@users.noreply.github.com> Date: Sat, 27 Sep 2025 21:41:07 +0800 Subject: [PATCH 045/257] Update and rename whats-new.mdx to whats-new-horizontal.mdx --- pages/guides/{whats-new.mdx => whats-new-horizontal.mdx} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename pages/guides/{whats-new.mdx => whats-new-horizontal.mdx} (71%) diff --git a/pages/guides/whats-new.mdx b/pages/guides/whats-new-horizontal.mdx similarity index 71% rename from pages/guides/whats-new.mdx rename to pages/guides/whats-new-horizontal.mdx index 7c02b48fa1..534a8208cb 100644 --- a/pages/guides/whats-new.mdx +++ b/pages/guides/whats-new-horizontal.mdx @@ -3,7 +3,7 @@ title: "What's New" description: "Auto-updating highlights from Mixpanel Changelogs" --- -import WhatsNewPromo from '../../components/WhatsNewPromo' +import WhatsNewPromo from '../../components/WhatsNewHorizontal'
    From 0939fd513ae57f16adf73995fc0d33c52940548b Mon Sep 17 00:00:00 2001 From: kurbycchua <50901466+kurbycchua@users.noreply.github.com> Date: Sat, 27 Sep 2025 21:41:26 +0800 Subject: [PATCH 046/257] Rename WhatsNewPromo.tsx to WhatsNewHorizontal.tsx --- components/{WhatsNewPromo.tsx => WhatsNewHorizontal.tsx} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename components/{WhatsNewPromo.tsx => WhatsNewHorizontal.tsx} (100%) diff --git a/components/WhatsNewPromo.tsx b/components/WhatsNewHorizontal.tsx similarity index 100% rename from components/WhatsNewPromo.tsx rename to components/WhatsNewHorizontal.tsx From 640a52f005c290c804b51dd4bb3b6f9f3e43154b Mon Sep 17 00:00:00 2001 From: kurbycchua <50901466+kurbycchua@users.noreply.github.com> Date: Sat, 27 Sep 2025 21:41:56 +0800 Subject: [PATCH 047/257] Rename whats-new-compact.mdx to whats-new.mdx --- pages/guides/{whats-new-compact.mdx => whats-new.mdx} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename pages/guides/{whats-new-compact.mdx => whats-new.mdx} (100%) diff --git a/pages/guides/whats-new-compact.mdx b/pages/guides/whats-new.mdx similarity index 100% rename from pages/guides/whats-new-compact.mdx rename to pages/guides/whats-new.mdx From 37b3cdd4af690ad38aaef95ad662e4964cd8d526 Mon Sep 17 00:00:00 2001 From: kurbycchua <50901466+kurbycchua@users.noreply.github.com> Date: Sat, 27 Sep 2025 21:43:33 +0800 Subject: [PATCH 048/257] Update WhatsNewVertical.tsx --- components/WhatsNewVertical.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/components/WhatsNewVertical.tsx b/components/WhatsNewVertical.tsx index 748210b555..5a814e11ca 100644 --- a/components/WhatsNewVertical.tsx +++ b/components/WhatsNewVertical.tsx @@ -75,12 +75,13 @@ function buildItems(): Item[] { const s = { page: { maxWidth: 880, margin: '0 auto' }, h1: { + marginTop: 16, // ← extra space under the breadcrumb + marginBottom: 0, fontSize: '44px', lineHeight: 1.1, fontWeight: 600 as const, letterSpacing: '-0.02em' as const, - margin: 0, - }, + }, heroP: { marginTop: 12, fontSize: 15, From 25339bb504809db2ed5d1da75f66bfac51e7adb1 Mon Sep 17 00:00:00 2001 From: kurbycchua <50901466+kurbycchua@users.noreply.github.com> Date: Sat, 27 Sep 2025 21:54:20 +0800 Subject: [PATCH 049/257] Update whats-new.mdx --- pages/guides/whats-new.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/guides/whats-new.mdx b/pages/guides/whats-new.mdx index 8504d733b8..9c6dce03cf 100644 --- a/pages/guides/whats-new.mdx +++ b/pages/guides/whats-new.mdx @@ -1,5 +1,5 @@ --- -title: "What's New — Compact" +title: "What's New" description: "Vertical compact board showing the latest updates" --- From 52905d93ef4dff2ae330d898bf3b63c260b1831d Mon Sep 17 00:00:00 2001 From: kurbycchua <50901466+kurbycchua@users.noreply.github.com> Date: Sat, 27 Sep 2025 21:54:35 +0800 Subject: [PATCH 050/257] Update whats-new-horizontal.mdx --- pages/guides/whats-new-horizontal.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/guides/whats-new-horizontal.mdx b/pages/guides/whats-new-horizontal.mdx index 534a8208cb..2017fd82af 100644 --- a/pages/guides/whats-new-horizontal.mdx +++ b/pages/guides/whats-new-horizontal.mdx @@ -1,5 +1,5 @@ --- -title: "What's New" +title: "What's New - Horizontal" description: "Auto-updating highlights from Mixpanel Changelogs" --- From f8684e4768028c399318b8ca60064748f38934f7 Mon Sep 17 00:00:00 2001 From: kurbycchua <50901466+kurbycchua@users.noreply.github.com> Date: Sat, 27 Sep 2025 22:02:16 +0800 Subject: [PATCH 051/257] Create WhatsNewVerticalV2.tsx --- components/WhatsNewVerticalV2.tsx | 398 ++++++++++++++++++++++++++++++ 1 file changed, 398 insertions(+) create mode 100644 components/WhatsNewVerticalV2.tsx diff --git a/components/WhatsNewVerticalV2.tsx b/components/WhatsNewVerticalV2.tsx new file mode 100644 index 0000000000..56c5e579be --- /dev/null +++ b/components/WhatsNewVerticalV2.tsx @@ -0,0 +1,398 @@ +'use client'; + +import React, { useMemo, useState } from 'react'; +import { getPagesUnderRoute } from 'nextra/context'; + +type Item = { + url: string; + title: string; + date: string; + thumbnail: string; +}; + +const changelogPages = getPagesUnderRoute('/changelogs'); + +/* ---------- helpers ---------- */ +const parseDate = (s = '') => { + const m = s.match(/(\d{4}-\d{2}-\d{2})/); + return m ? m[1] : ''; +}; +const humanize = (s = '') => + s.replace(/^\d{4}-\d{2}-\d{2}-/, '').replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase()); + +const fmtDay = (dateStr: string) => { + const d = new Date(dateStr); + if (isNaN(d as any)) return dateStr || ''; + return d.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' }); +}; + +const firstNonEmpty = (...vals: any[]) => + vals.find((v) => { + if (!v) return false; + if (Array.isArray(v)) return v.length > 0 && typeof v[0] === 'string'; + return typeof v === 'string'; + }); + +const clampStyle = (lines: number): React.CSSProperties => ({ + display: '-webkit-box', + WebkitLineClamp: lines, + WebkitBoxOrient: 'vertical', + overflow: 'hidden' +}); + +/* ---------- build items (NEWEST → OLDEST) ---------- */ +function buildItems(): Item[] { + return (changelogPages || []) + .map((p: any) => { + const fm = p.frontMatter || p.meta || {}; + const route = p.route || ''; + if (!/\/changelogs\/.+/.test(route)) return null; + + const name = p.name || route.split('/').pop() || ''; + const date = fm.date || parseDate(name) || parseDate(route); + const thumb = firstNonEmpty( + fm.thumbnail, + fm.image, + fm.cover, + fm.ogImage, + fm.hero, + fm.screenshot, + Array.isArray(fm.images) ? fm.images[0] : undefined + ) as string | undefined; + + return { + url: route, + title: fm.title || p.title || humanize(name), + date, + thumbnail: thumb || '' + } as Item; + }) + .filter(Boolean) + .sort((a: Item, b: Item) => new Date(b.date).getTime() - new Date(a.date).getTime()); +} + +/* ---------- shared inline styles ---------- */ +const TL_X = 12; // timeline line X (relative to UL left) +const TL_PAD = TL_X + 16; // left padding so content clears the gutter + +const s = { + page: { maxWidth: 880, margin: '0 auto' }, + h1: { + marginTop: 16, + marginBottom: 0, + fontSize: '44px', + lineHeight: 1.1, + fontWeight: 600 as const, + letterSpacing: '-0.02em' as const, + }, + heroP: { + marginTop: 12, + fontSize: 15, + lineHeight: 1.6, + color: 'rgba(255,255,255,0.8)', + }, + heroLink: { + marginTop: 12, + fontSize: 14, + textDecoration: 'underline', + textUnderlineOffset: '4px', + color: 'rgba(255,255,255,0.85)', + display: 'inline-block', + }, + rowBar: { + marginTop: 24, + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + gap: 12, + }, + showing: { + fontSize: 12, + color: 'rgba(255,255,255,0.55)', + }, + controlsWrap: { whiteSpace: 'nowrap' as const, minWidth: 0 }, + btn: { + fontSize: 12, + padding: '6px 8px', + borderRadius: 6, + border: '1px solid rgba(255,255,255,0.18)', + background: 'transparent', + color: 'inherit', + cursor: 'pointer', + }, + select: { + fontSize: 12, + padding: '6px 8px', + borderRadius: 6, + border: '1px solid rgba(255,255,255,0.18)', + background: 'transparent', + color: 'inherit', + }, + /* list + timeline */ + list: { + marginTop: 12, + listStyle: 'none', + padding: 0, + position: 'relative' as const, + paddingLeft: TL_PAD, + }, + timelineLine: { + position: 'absolute' as const, + left: TL_X, + top: 0, + bottom: 0, + width: 2, + background: 'rgba(255,255,255,0.07)', + }, + /* card */ + cardLi: { padding: '12px 0', position: 'relative' as const }, + cardA: { display: 'block', borderRadius: 12, padding: 12, textDecoration: 'none' }, + cardHeader: { + display: 'grid', + gridTemplateColumns: '1fr auto', + alignItems: 'center', + gap: 12, + marginBottom: 8, + }, + cardTitle: { + fontSize: 20, + fontWeight: 600 as const, + lineHeight: 1.2, + textDecorationThickness: '1px', + textUnderlineOffset: '4px', + }, + cardDate: { fontSize: 12, color: 'rgba(255,255,255,0.55)' }, + imgWrap: { + width: '100%', + borderRadius: 12, + overflow: 'hidden', + aspectRatio: '16 / 9', + background: + 'radial-gradient(120% 120% at 0% 100%, rgba(168,85,247,0.18), transparent 60%), radial-gradient(120% 120% at 100% 0%, rgba(59,130,246,0.18), transparent 60%)', + }, + readLink: { + marginTop: 6, + fontSize: 13, + textDecoration: 'underline', + textUnderlineOffset: '4px', + display: 'inline-block', + }, + footerLink: { + fontSize: 14, + color: 'rgb(167 139 250)', + textDecoration: 'underline', + textUnderlineOffset: '4px', + }, + /* timeline dot */ + dot: { + position: 'absolute' as const, + left: -(TL_PAD - TL_X), // aligns dot on the vertical line + top: 12, // aligns to top of the card + width: 8, + height: 8, + borderRadius: 999, + background: 'rgb(167 139 250)', // violet dot + boxShadow: '0 0 0 2px rgba(20,20,30,0.9)', // ring to separate from bg + }, + /* NEW badge */ + newBadge: { + marginLeft: 8, + fontSize: 11, + fontWeight: 700 as const, + letterSpacing: '0.02em', + color: 'rgb(26, 26, 31)', + background: + 'linear-gradient(90deg, rgba(167,139,250,0.95), rgba(99,102,241,0.95))', + borderRadius: 999, + padding: '2px 6px', + lineHeight: 1.1, + verticalAlign: 'middle', + }, +}; + +/* ---------- control components ---------- */ +function ControlsTop({ + pageSize, + canPrev, + canNext, + changeSize, + prev, + next, +}: { + pageSize: number; + canPrev: boolean; + canNext: boolean; + changeSize: (n: number) => void; + prev: () => void; + next: () => void; +}) { + return ( +
    + Show + + + +
    + ); +} + +function ControlsBottom({ + canPrev, + canNext, + prev, + next, +}: { + canPrev: boolean; + canNext: boolean; + prev: () => void; + next: () => void; +}) { + return ( +
    + + +
    + ); +} + +/* ---------- card ---------- */ +function Row({ item }: { item: Item }) { + // NEW badge if within last 14 days + const isNew = (() => { + const d = new Date(item.date); + if (isNaN(d as any)) return false; + const days = (Date.now() - d.getTime()) / (1000 * 60 * 60 * 24); + return days <= 14; + })(); + + return ( +
  • + {/* timeline dot */} +
  • + ); +} + +/* ---------- main ---------- */ +export default function WhatsNewVertical() { + const items = useMemo(buildItems, []); + + // paging (Latest X) + const [pageSize, setPageSize] = useState(5); + const [offset, setOffset] = useState(0); + + const total = items.length; + const start = offset; + const end = Math.min(offset + pageSize, total); + const page = items.slice(start, end); + + const canPrev = start > 0; + const canNext = end < total; + + const changeSize = (n: number) => { + setPageSize(n); + setOffset(0); + }; + const prev = () => setOffset(Math.max(0, offset - pageSize)); + const next = () => setOffset(Math.min(total, offset + pageSize)); + + return ( +
    + {/* HERO */} +
    +

    What's New

    + +

    + Track Mixpanel product releases and improvements in one place. See what’s + new, what got faster, and what opens up entirely new ways to answer questions about your + product. These changes are built from customer feedback and real workflows—less setup, + fewer manual steps, clearer answers. +

    +

    + From performance boosts to streamlined analysis and collaboration, each release is here to + shorten the path from “what happened?” to “what should we do?”. Browse the highlights below + and put the most impactful updates to work on your team today. +

    + + + Browse Changelog + +
    + + {/* TOP BAR — single line */} +
    +
    + Showing {total === 0 ? 0 : start + 1}–{end} of {total} +
    + +
    + + {/* LIST + TIMELINE */} +
      +
    + + {/* BOTTOM BAR — single line */} + +
    + ); +} From df76c8f3c5ba01c16b96d0c0b4719e661b9f0e13 Mon Sep 17 00:00:00 2001 From: kurbycchua <50901466+kurbycchua@users.noreply.github.com> Date: Sat, 27 Sep 2025 22:02:26 +0800 Subject: [PATCH 052/257] Update whats-new.mdx --- pages/guides/whats-new.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pages/guides/whats-new.mdx b/pages/guides/whats-new.mdx index 9c6dce03cf..c667768631 100644 --- a/pages/guides/whats-new.mdx +++ b/pages/guides/whats-new.mdx @@ -3,9 +3,9 @@ title: "What's New" description: "Vertical compact board showing the latest updates" --- -import WhatsNewVertical from '../../components/WhatsNewVertical' +import WhatsNewVertical from '../../components/WhatsNewVerticalV2' {/* Keep this page minimal: no stray text above/below the component */}
    - +
    From 25e69ee3845de3077b29944c43b043cab7641359 Mon Sep 17 00:00:00 2001 From: kurbycchua <50901466+kurbycchua@users.noreply.github.com> Date: Sat, 27 Sep 2025 22:06:47 +0800 Subject: [PATCH 053/257] Update whats-new.mdx --- pages/guides/whats-new.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/guides/whats-new.mdx b/pages/guides/whats-new.mdx index c667768631..bab1694c3b 100644 --- a/pages/guides/whats-new.mdx +++ b/pages/guides/whats-new.mdx @@ -7,5 +7,5 @@ import WhatsNewVertical from '../../components/WhatsNewVerticalV2' {/* Keep this page minimal: no stray text above/below the component */}
    - +
    From 521c3fd6b4215650d4a2b358901d0b1bc18bd7b4 Mon Sep 17 00:00:00 2001 From: kurbycchua <50901466+kurbycchua@users.noreply.github.com> Date: Mon, 6 Oct 2025 13:46:46 +0800 Subject: [PATCH 054/257] Update WhatsNewVertical.tsx --- components/WhatsNewVertical.tsx | 144 ++++++++++++++++++++++++++++++++ 1 file changed, 144 insertions(+) diff --git a/components/WhatsNewVertical.tsx b/components/WhatsNewVertical.tsx index 5a814e11ca..9a2e4b5721 100644 --- a/components/WhatsNewVertical.tsx +++ b/components/WhatsNewVertical.tsx @@ -72,6 +72,7 @@ function buildItems(): Item[] { } /* ---------- shared inline styles ---------- */ +/* const s = { page: { maxWidth: 880, margin: '0 auto' }, h1: { @@ -164,6 +165,149 @@ const s = { textUnderlineOffset: '4px', }, }; +*/ +/* ---------- shared inline styles (theme-safe) ---------- */ +const TL_X = 12; // timeline line X (relative to UL left) +const TL_PAD = TL_X + 16; // left padding so content clears the gutter + +const s = { + page: { maxWidth: 880, margin: '0 auto' }, + h1: { + marginTop: 16, + marginBottom: 0, + fontSize: '44px', + lineHeight: 1.1, + fontWeight: 600 as const, + letterSpacing: '-0.02em' as const, + }, + heroP: { + marginTop: 12, + fontSize: 15, + lineHeight: 1.6, + /* inherit color so it works in light & dark */ + }, + heroLink: { + marginTop: 12, + fontSize: 14, + textDecoration: 'underline', + textUnderlineOffset: '4px', + /* inherit color; let site/theme decide link color */ + display: 'inline-block', + }, + rowBar: { + marginTop: 24, + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + gap: 12, + }, + showing: { + fontSize: 12, + opacity: 0.7, // theme-safe + }, + controlsWrap: { whiteSpace: 'nowrap' as const, minWidth: 0 }, + btn: { + fontSize: 12, + padding: '6px 8px', + borderRadius: 6, + border: '1px solid currentColor', // theme-safe border + background: 'transparent', + color: 'inherit', + cursor: 'pointer', + }, + select: { + fontSize: 12, + padding: '6px 8px', + borderRadius: 6, + border: '1px solid currentColor', // theme-safe border + background: 'transparent', + color: 'inherit', + }, + /* list + timeline */ + list: { + marginTop: 12, + listStyle: 'none', + padding: 0, + position: 'relative' as const, + paddingLeft: TL_PAD, + }, + timelineLine: { + position: 'absolute' as const, + left: TL_X, + top: 0, + bottom: 0, + width: 2, + background: 'currentColor', // theme-safe + opacity: 0.12, // subtle in both themes + }, + /* card */ + cardLi: { padding: '12px 0', position: 'relative' as const }, + cardA: { display: 'block', borderRadius: 12, padding: 12, textDecoration: 'none' }, + cardHeader: { + display: 'grid', + gridTemplateColumns: '1fr auto', + alignItems: 'center', + gap: 12, + marginBottom: 8, + }, + cardTitle: { + fontSize: 20, + fontWeight: 600 as const, + lineHeight: 1.2, + textDecorationThickness: '1px', + textUnderlineOffset: '4px', + }, + cardDate: { + fontSize: 12, + opacity: 0.6, // theme-safe instead of white + }, + imgWrap: { + width: '100%', + borderRadius: 12, + overflow: 'hidden', + aspectRatio: '16 / 9', + background: + 'radial-gradient(120% 120% at 0% 100%, rgba(168,85,247,0.18), transparent 60%), radial-gradient(120% 120% at 100% 0%, rgba(59,130,246,0.18), transparent 60%)', + }, + readLink: { + marginTop: 6, + fontSize: 13, + textDecoration: 'underline', + textUnderlineOffset: '4px', + display: 'inline-block', + }, + footerLink: { + fontSize: 14, + color: 'rgb(167 139 250)', // accent that reads on both themes + textDecoration: 'underline', + textUnderlineOffset: '4px', + }, + /* timeline dot */ + dot: { + position: 'absolute' as const, + left: -(TL_PAD - TL_X), // aligns dot on the vertical line + top: 12, // aligns to top of the card + width: 8, + height: 8, + borderRadius: 999, + background: 'rgb(167 139 250)', // violet dot (visible in both) + boxShadow: '0 0 0 2px rgba(20,20,30,0.9)', // subtle ring for contrast + }, + /* NEW badge */ + newBadge: { + marginLeft: 8, + fontSize: 11, + fontWeight: 700 as const, + letterSpacing: '0.02em', + color: 'rgb(26, 26, 31)', + background: + 'linear-gradient(90deg, rgba(167,139,250,0.95), rgba(99,102,241,0.95))', + borderRadius: 999, + padding: '2px 6px', + lineHeight: 1.1, + verticalAlign: 'middle', + }, +}; /* ---------- control components (inline-styled) ---------- */ function ControlsTop({ From 3ad91ee76f6310c9de5032ebb3cf37072090b17d Mon Sep 17 00:00:00 2001 From: kurbycchua <50901466+kurbycchua@users.noreply.github.com> Date: Mon, 6 Oct 2025 16:42:47 +0800 Subject: [PATCH 055/257] Update WhatsNewVertical.tsx --- components/WhatsNewVertical.tsx | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/components/WhatsNewVertical.tsx b/components/WhatsNewVertical.tsx index 9a2e4b5721..b2dfdc756d 100644 --- a/components/WhatsNewVertical.tsx +++ b/components/WhatsNewVertical.tsx @@ -171,7 +171,7 @@ const TL_X = 12; // timeline line X (relative to UL left) const TL_PAD = TL_X + 16; // left padding so content clears the gutter const s = { - page: { maxWidth: 880, margin: '0 auto' }, + page: { maxWidth: 880, margin: '0 auto', color: 'inherit' }, // ensure we inherit theme text color h1: { marginTop: 16, marginBottom: 0, @@ -184,14 +184,15 @@ const s = { marginTop: 12, fontSize: 15, lineHeight: 1.6, - /* inherit color so it works in light & dark */ + color: 'currentColor', // explicitly use theme text color (fixes light mode washout) }, heroLink: { marginTop: 12, fontSize: 14, textDecoration: 'underline', textUnderlineOffset: '4px', - /* inherit color; let site/theme decide link color */ + color: 'currentColor', // readable in both themes + textDecorationColor: 'currentColor', display: 'inline-block', }, rowBar: { @@ -238,7 +239,7 @@ const s = { bottom: 0, width: 2, background: 'currentColor', // theme-safe - opacity: 0.12, // subtle in both themes + opacity: 0.18, // a touch stronger so it shows on light bg }, /* card */ cardLi: { padding: '12px 0', position: 'relative' as const }, @@ -278,9 +279,10 @@ const s = { }, footerLink: { fontSize: 14, - color: 'rgb(167 139 250)', // accent that reads on both themes + color: 'currentColor', // keeps it obvious in both themes textDecoration: 'underline', textUnderlineOffset: '4px', + textDecorationColor: 'currentColor', }, /* timeline dot */ dot: { @@ -291,7 +293,8 @@ const s = { height: 8, borderRadius: 999, background: 'rgb(167 139 250)', // violet dot (visible in both) - boxShadow: '0 0 0 2px rgba(20,20,30,0.9)', // subtle ring for contrast + // slightly lighter ring so it doesn't look too heavy in light mode + boxShadow: '0 0 0 2px rgba(0,0,0,0.25)', }, /* NEW badge */ newBadge: { From b6869591a51b0224944e74b1c4cb1872846d2ff0 Mon Sep 17 00:00:00 2001 From: kurbycchua <50901466+kurbycchua@users.noreply.github.com> Date: Mon, 6 Oct 2025 17:29:00 +0800 Subject: [PATCH 056/257] Update WhatsNewVertical.tsx --- components/WhatsNewVertical.tsx | 321 ++++++++++++++++---------------- 1 file changed, 163 insertions(+), 158 deletions(-) diff --git a/components/WhatsNewVertical.tsx b/components/WhatsNewVertical.tsx index b2dfdc756d..47ad130aff 100644 --- a/components/WhatsNewVertical.tsx +++ b/components/WhatsNewVertical.tsx @@ -6,24 +6,37 @@ import { getPagesUnderRoute } from 'nextra/context'; type Item = { url: string; title: string; - date: string; - thumbnail: string; + date: string; // ISO-ish e.g. 2025-09-22 + thumbnail: string; // URL or empty string }; +/* ------------------------------------------------------- + Source: all pages under /changelogs +------------------------------------------------------- */ const changelogPages = getPagesUnderRoute('/changelogs'); -/* ---------- helpers ---------- */ -const parseDate = (s = '') => { +/* ------------------------------------------------------- + Utilities +------------------------------------------------------- */ +const parseDateFromString = (s = '') => { const m = s.match(/(\d{4}-\d{2}-\d{2})/); return m ? m[1] : ''; }; -const humanize = (s = '') => - s.replace(/^\d{4}-\d{2}-\d{2}-/, '').replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase()); -const fmtDay = (dateStr: string) => { +const humanizeSlug = (s = '') => + s + .replace(/^\d{4}-\d{2}-\d{2}-/, '') + .replace(/-/g, ' ') + .replace(/\b\w/g, (c) => c.toUpperCase()); + +const formatDay = (dateStr: string) => { const d = new Date(dateStr); - if (isNaN(d as any)) return dateStr || ''; - return d.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' }); + if (isNaN(d as unknown as number)) return dateStr || ''; + return d.toLocaleDateString(undefined, { + month: 'short', + day: 'numeric', + year: 'numeric', + }); }; const firstNonEmpty = (...vals: any[]) => @@ -37,10 +50,12 @@ const clampStyle = (lines: number): React.CSSProperties => ({ display: '-webkit-box', WebkitLineClamp: lines, WebkitBoxOrient: 'vertical', - overflow: 'hidden' + overflow: 'hidden', }); -/* ---------- build items (NEWEST → OLDEST) ---------- */ +/* ------------------------------------------------------- + Build items (NEWEST → OLDEST) +------------------------------------------------------- */ function buildItems(): Item[] { return (changelogPages || []) .map((p: any) => { @@ -49,7 +64,7 @@ function buildItems(): Item[] { if (!/\/changelogs\/.+/.test(route)) return null; const name = p.name || route.split('/').pop() || ''; - const date = fm.date || parseDate(name) || parseDate(route); + const date = fm.date || parseDateFromString(name) || parseDateFromString(route); const thumb = firstNonEmpty( fm.thumbnail, fm.image, @@ -62,116 +77,49 @@ function buildItems(): Item[] { return { url: route, - title: fm.title || p.title || humanize(name), + title: fm.title || p.title || humanizeSlug(name), date, - thumbnail: thumb || '' + thumbnail: thumb || '', } as Item; }) .filter(Boolean) - .sort((a: Item, b: Item) => new Date(b.date).getTime() - new Date(a.date).getTime()); + .sort( + (a: Item, b: Item) => + new Date(b.date).getTime() - new Date(a.date).getTime() + ); } -/* ---------- shared inline styles ---------- */ -/* +/* ------------------------------------------------------- + Theme variables (work in light & dark) +------------------------------------------------------- */ +function ThemeVars() { + return ( + + ); +} + +/* ------------------------------------------------------- + Inline styles (theme-safe via CSS vars) +------------------------------------------------------- */ +const TL_X = 12; // timeline line X from UL left +const TL_PAD = TL_X + 16; // left padding to clear gutter + const s = { page: { maxWidth: 880, margin: '0 auto' }, - h1: { - marginTop: 16, // ← extra space under the breadcrumb - marginBottom: 0, - fontSize: '44px', - lineHeight: 1.1, - fontWeight: 600 as const, - letterSpacing: '-0.02em' as const, - }, - heroP: { - marginTop: 12, - fontSize: 15, - lineHeight: 1.6, - color: 'rgba(255,255,255,0.8)', - }, - heroLink: { - marginTop: 12, - fontSize: 14, - textDecoration: 'underline', - textUnderlineOffset: '4px', - color: 'rgba(255,255,255,0.85)', - display: 'inline-block', - }, - rowBar: { - marginTop: 24, - display: 'flex', - alignItems: 'center', - justifyContent: 'space-between', - gap: 12, - }, - showing: { - fontSize: 12, - color: 'rgba(255,255,255,0.55)', - }, - controlsWrap: { whiteSpace: 'nowrap' as const, minWidth: 0 }, - btn: { - fontSize: 12, - padding: '6px 8px', - borderRadius: 6, - border: '1px solid rgba(255,255,255,0.18)', - background: 'transparent', - color: 'inherit', - cursor: 'pointer', - }, - select: { - fontSize: 12, - padding: '6px 8px', - borderRadius: 6, - border: '1px solid rgba(255,255,255,0.18)', - background: 'transparent', - color: 'inherit', - }, - cardLi: { padding: '12px 0' }, - cardA: { display: 'block', borderRadius: 12, padding: 12, textDecoration: 'none' }, - cardHeader: { - display: 'grid', - gridTemplateColumns: '1fr auto', - alignItems: 'center', - gap: 12, - marginBottom: 8, - }, - cardTitle: { - fontSize: 20, - fontWeight: 600 as const, - lineHeight: 1.2, - textDecorationThickness: '1px', - textUnderlineOffset: '4px', - }, - cardDate: { fontSize: 12, color: 'rgba(255,255,255,0.55)' }, - imgWrap: { - width: '100%', - borderRadius: 12, - overflow: 'hidden', - aspectRatio: '16 / 9', - background: - 'radial-gradient(120% 120% at 0% 100%, rgba(168,85,247,0.18), transparent 60%), radial-gradient(120% 120% at 100% 0%, rgba(59,130,246,0.18), transparent 60%)', - }, - readLink: { - marginTop: 6, - fontSize: 13, - textDecoration: 'underline', - textUnderlineOffset: '4px', - display: 'inline-block', - }, - footerLink: { - fontSize: 14, - color: 'rgb(167 139 250)', // violet-ish - textDecoration: 'underline', - textUnderlineOffset: '4px', - }, -}; -*/ -/* ---------- shared inline styles (theme-safe) ---------- */ -const TL_X = 12; // timeline line X (relative to UL left) -const TL_PAD = TL_X + 16; // left padding so content clears the gutter -const s = { - page: { maxWidth: 880, margin: '0 auto', color: 'inherit' }, // ensure we inherit theme text color h1: { marginTop: 16, marginBottom: 0, @@ -179,22 +127,25 @@ const s = { lineHeight: 1.1, fontWeight: 600 as const, letterSpacing: '-0.02em' as const, + color: 'var(--wn-text)', }, + heroP: { marginTop: 12, fontSize: 15, lineHeight: 1.6, - color: 'currentColor', // explicitly use theme text color (fixes light mode washout) + color: 'var(--wn-text)', }, + heroLink: { marginTop: 12, fontSize: 14, textDecoration: 'underline', textUnderlineOffset: '4px', - color: 'currentColor', // readable in both themes - textDecorationColor: 'currentColor', + color: 'var(--wn-text)', display: 'inline-block', }, + rowBar: { marginTop: 24, display: 'flex', @@ -202,29 +153,34 @@ const s = { justifyContent: 'space-between', gap: 12, }, + showing: { fontSize: 12, - opacity: 0.7, // theme-safe + color: 'var(--wn-muted)', }, + controlsWrap: { whiteSpace: 'nowrap' as const, minWidth: 0 }, + btn: { fontSize: 12, padding: '6px 8px', borderRadius: 6, - border: '1px solid currentColor', // theme-safe border + border: '1px solid var(--wn-muted)', background: 'transparent', - color: 'inherit', + color: 'var(--wn-text)', cursor: 'pointer', }, + select: { fontSize: 12, padding: '6px 8px', borderRadius: 6, - border: '1px solid currentColor', // theme-safe border + border: '1px solid var(--wn-muted)', background: 'transparent', - color: 'inherit', + color: 'var(--wn-text)', }, - /* list + timeline */ + + /* List + timeline */ list: { marginTop: 12, listStyle: 'none', @@ -232,18 +188,21 @@ const s = { position: 'relative' as const, paddingLeft: TL_PAD, }, + timelineLine: { position: 'absolute' as const, left: TL_X, top: 0, bottom: 0, width: 2, - background: 'currentColor', // theme-safe - opacity: 0.18, // a touch stronger so it shows on light bg + background: 'var(--wn-line)', }, - /* card */ + + /* Card */ cardLi: { padding: '12px 0', position: 'relative' as const }, + cardA: { display: 'block', borderRadius: 12, padding: 12, textDecoration: 'none' }, + cardHeader: { display: 'grid', gridTemplateColumns: '1fr auto', @@ -251,17 +210,18 @@ const s = { gap: 12, marginBottom: 8, }, + cardTitle: { fontSize: 20, fontWeight: 600 as const, lineHeight: 1.2, + color: 'var(--wn-text)', textDecorationThickness: '1px', textUnderlineOffset: '4px', }, - cardDate: { - fontSize: 12, - opacity: 0.6, // theme-safe instead of white - }, + + cardDate: { fontSize: 12, color: 'var(--wn-muted)' }, + imgWrap: { width: '100%', borderRadius: 12, @@ -270,6 +230,7 @@ const s = { background: 'radial-gradient(120% 120% at 0% 100%, rgba(168,85,247,0.18), transparent 60%), radial-gradient(120% 120% at 100% 0%, rgba(59,130,246,0.18), transparent 60%)', }, + readLink: { marginTop: 6, fontSize: 13, @@ -277,25 +238,26 @@ const s = { textUnderlineOffset: '4px', display: 'inline-block', }, + footerLink: { fontSize: 14, - color: 'currentColor', // keeps it obvious in both themes + color: 'var(--wn-text)', textDecoration: 'underline', textUnderlineOffset: '4px', - textDecorationColor: 'currentColor', }, - /* timeline dot */ + + /* Timeline dot */ dot: { position: 'absolute' as const, - left: -(TL_PAD - TL_X), // aligns dot on the vertical line - top: 12, // aligns to top of the card + left: -(TL_PAD - TL_X), + top: 12, width: 8, height: 8, borderRadius: 999, - background: 'rgb(167 139 250)', // violet dot (visible in both) - // slightly lighter ring so it doesn't look too heavy in light mode - boxShadow: '0 0 0 2px rgba(0,0,0,0.25)', + background: 'rgb(167 139 250)', // violet dot + boxShadow: '0 0 0 2px rgba(0,0,0,0.25)', // subtle ring for light mode too }, + /* NEW badge */ newBadge: { marginLeft: 8, @@ -312,7 +274,9 @@ const s = { }, }; -/* ---------- control components (inline-styled) ---------- */ +/* ------------------------------------------------------- + Controls +------------------------------------------------------- */ function ControlsTop({ pageSize, canPrev, @@ -330,18 +294,35 @@ function ControlsTop({ }) { return (
    - Show - changeSize(Number(e.target.value))} + > {[5, 10, 15, 20].map((n) => ( ))} - -
    @@ -361,26 +342,45 @@ function ControlsBottom({ }) { return (
    - -
    ); } -/* ---------- card ---------- */ +/* ------------------------------------------------------- + Card +------------------------------------------------------- */ function Row({ item }: { item: Item }) { + // “NEW” if within last 14 days + const isNew = (() => { + const d = new Date(item.date); + if (isNaN(d as unknown as number)) return false; + const days = (Date.now() - d.getTime()) / (1000 * 60 * 60 * 24); + return days <= 14; + })(); + return ( -
  • - +
  • + {/* timeline dot */} +

    {item.title} + {isNew && NEW}

    -
    {fmtDay(item.date)}
    +
    {formatDay(item.date)}
    @@ -405,7 +405,9 @@ function Row({ item }: { item: Item }) { ); } -/* ---------- main ---------- */ +/* ------------------------------------------------------- + Main +------------------------------------------------------- */ export default function WhatsNewVertical() { const items = useMemo(buildItems, []); @@ -430,20 +432,22 @@ export default function WhatsNewVertical() { return (
    + + {/* HERO */}

    What's New

    - Track Mixpanel product releases and improvements in one place. See what’s - new, what got faster, and what opens up entirely new ways to answer questions about your - product. These changes are built from customer feedback and real workflows—less setup, - fewer manual steps, clearer answers. + Track Mixpanel product releases and improvements in one place. See + what’s new, what got faster, and what opens up entirely new ways to answer questions + about your product. These changes are built from customer feedback and real + workflows—less setup, fewer manual steps, clearer answers.

    - From performance boosts to streamlined analysis and collaboration, each release is here to - shorten the path from “what happened?” to “what should we do?”. Browse the highlights below - and put the most impactful updates to work on your team today. + From performance boosts to streamlined analysis and collaboration, each release is here + to shorten the path from “what happened?” to “what should we do?”. Browse the highlights + below and put the most impactful updates to work on your team today.

    @@ -466,8 +470,9 @@ export default function WhatsNewVertical() { />
    - {/* LIST */} -
      + {/* LIST + TIMELINE */} +
        + +
    + + {/* List with timeline gutter */} +
  • + ) } -/* ---------- shared inline styles ---------- */ -const TL_X = 12; // timeline line X (relative to UL left) -const TL_PAD = TL_X + 16; // left padding so content clears the gutter +// ----------------------------- +// Styles (inline → preferred fix) +// ----------------------------- +const s: Record = { + page: { + position: 'relative', + margin: '0 auto', + maxWidth: 980, + padding: '8px 0 64px', + }, + + headerWrap: { + marginBottom: 12, + }, -const s = { - page: { maxWidth: 880, margin: '0 auto' }, h1: { - marginTop: 16, - marginBottom: 0, - fontSize: '44px', + fontSize: 44, lineHeight: 1.1, - fontWeight: 600 as const, - letterSpacing: '-0.02em' as const, + fontWeight: 700, + margin: '0 0 12px 0', + color: 'var(--wn-text)', + }, + + hero: { + marginTop: 6, + marginBottom: 8, }, + heroP: { + // THE KEY: set tokenized color inline so Light mode is readable marginTop: 12, fontSize: 15, lineHeight: 1.6, - color: 'rgba(255,255,255,0.8)', + color: 'var(--wn-text)', }, + heroLink: { marginTop: 12, + color: 'var(--wn-text)', fontSize: 14, textDecoration: 'underline', textUnderlineOffset: '4px', - color: 'rgba(255,255,255,0.85)', display: 'inline-block', }, - rowBar: { + + controlsRow: { marginTop: 24, display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12, }, + showing: { fontSize: 12, - color: 'rgba(255,255,255,0.55)', + color: 'var(--wn-muted)', + }, + + controlsRight: { + display: 'flex', + alignItems: 'center', + gap: 8, }, - controlsWrap: { whiteSpace: 'nowrap' as const, minWidth: 0 }, - btn: { + + showLabel: { fontSize: 12, - padding: '6px 8px', - borderRadius: 6, - border: '1px solid rgba(255,255,255,0.18)', - background: 'transparent', - color: 'inherit', - cursor: 'pointer', + color: 'var(--wn-muted)', + marginRight: 4, }, + select: { - fontSize: 12, - padding: '6px 8px', - borderRadius: 6, - border: '1px solid rgba(255,255,255,0.18)', + appearance: 'none', + WebkitAppearance: 'none', + MozAppearance: 'none', + border: `1px solid var(--wn-card-border)`, background: 'transparent', - color: 'inherit', - }, - /* list + timeline */ - list: { - marginTop: 12, - listStyle: 'none', - padding: 0, + color: 'var(--wn-text)', + borderRadius: 8, + padding: '6px 28px 6px 10px', + fontSize: 14, position: 'relative' as const, - paddingLeft: TL_PAD, - }, - timelineLine: { - position: 'absolute' as const, - left: TL_X, - top: 0, - bottom: 0, - width: 2, - background: 'rgba(255,255,255,0.07)', + backgroundImage: + `linear-gradient(45deg, transparent 50%, var(--wn-text) 50%), + linear-gradient(135deg, var(--wn-text) 50%, transparent 50%)`, + backgroundPosition: 'right 10px center, right 5px center', + backgroundSize: '6px 6px, 6px 6px', + backgroundRepeat: 'no-repeat', }, - /* card */ - cardLi: { padding: '12px 0', position: 'relative' as const }, - cardA: { display: 'block', borderRadius: 12, padding: 12, textDecoration: 'none' }, - cardHeader: { - display: 'grid', - gridTemplateColumns: '1fr auto', - alignItems: 'center', - gap: 12, - marginBottom: 8, + + navBtn: { + fontSize: 14, + border: `1px solid var(--wn-card-border)`, + background: 'transparent', + color: 'var(--wn-text)', + padding: '6px 10px', + borderRadius: 8, + cursor: 'pointer', + transition: 'background 160ms ease', }, - cardTitle: { - fontSize: 20, - fontWeight: 600 as const, - lineHeight: 1.2, - textDecorationThickness: '1px', - textUnderlineOffset: '4px', + + listWrap: { + position: 'relative', + marginTop: 20, }, - cardDate: { fontSize: 12, color: 'rgba(255,255,255,0.55)' }, - imgWrap: { - width: '100%', - borderRadius: 12, - overflow: 'hidden', - aspectRatio: '16 / 9', + + timeline: { + position: 'absolute', + left: -24, + top: 12, + bottom: 12, + width: 2, background: - 'radial-gradient(120% 120% at 0% 100%, rgba(168,85,247,0.18), transparent 60%), radial-gradient(120% 120% at 100% 0%, rgba(59,130,246,0.18), transparent 60%)', - }, - readLink: { - marginTop: 6, - fontSize: 13, - textDecoration: 'underline', - textUnderlineOffset: '4px', - display: 'inline-block', + 'linear-gradient(to bottom, var(--wn-card-border), var(--wn-card-border))', + borderRadius: 2, + pointerEvents: 'none', }, - footerLink: { - fontSize: 14, - color: 'rgb(167 139 250)', - textDecoration: 'underline', - textUnderlineOffset: '4px', + + card: { + position: 'relative', + margin: '28px 0 40px', + paddingLeft: 0, }, - /* timeline dot */ + dot: { - position: 'absolute' as const, - left: -(TL_PAD - TL_X), // aligns dot on the vertical line - top: 12, // aligns to top of the card - width: 8, - height: 8, - borderRadius: 999, - background: 'rgb(167 139 250)', // violet dot - boxShadow: '0 0 0 2px rgba(20,20,30,0.9)', // ring to separate from bg + position: 'absolute', + left: -28, + top: 8, + width: 10, + height: 10, + background: 'var(--wn-dot)', + borderRadius: '999px', + boxShadow: '0 0 0 3px var(--wn-ring)', + }, + + titleRow: { + display: 'flex', + alignItems: 'baseline', + justifyContent: 'space-between', + gap: 12, + marginBottom: 10, }, - /* NEW badge */ + + h2: { + fontSize: 22, + lineHeight: 1.25, + fontWeight: 700, + color: 'var(--wn-text)', + margin: 0, + display: 'flex', + alignItems: 'center', + gap: 10, + }, + newBadge: { - marginLeft: 8, fontSize: 11, - fontWeight: 700 as const, - letterSpacing: '0.02em', - color: 'rgb(26, 26, 31)', - background: - 'linear-gradient(90deg, rgba(167,139,250,0.95), rgba(99,102,241,0.95))', + lineHeight: 1, + padding: '4px 6px', borderRadius: 999, - padding: '2px 6px', - lineHeight: 1.1, - verticalAlign: 'middle', + border: `1px solid var(--wn-card-border)`, + background: 'transparent', + color: 'var(--wn-text)', }, -}; - -/* ---------- control components ---------- */ -function ControlsTop({ - pageSize, - canPrev, - canNext, - changeSize, - prev, - next, -}: { - pageSize: number; - canPrev: boolean; - canNext: boolean; - changeSize: (n: number) => void; - prev: () => void; - next: () => void; -}) { - return ( -
    - Show - - - -
    - ); -} -function ControlsBottom({ - canPrev, - canNext, - prev, - next, -}: { - canPrev: boolean; - canNext: boolean; - prev: () => void; - next: () => void; -}) { - return ( -
    - - -
    - ); -} - -/* ---------- card ---------- */ -function Row({ item }: { item: Item }) { - // NEW badge if within last 14 days - const isNew = (() => { - const d = new Date(item.date); - if (isNaN(d as any)) return false; - const days = (Date.now() - d.getTime()) / (1000 * 60 * 60 * 24); - return days <= 14; - })(); - - return ( -
  • - {/* timeline dot */} -
  • - ); -} - -/* ---------- main ---------- */ -export default function WhatsNewVertical() { - const items = useMemo(buildItems, []); + cardDate: { + fontSize: 12, + color: 'var(--wn-muted)', + whiteSpace: 'nowrap', + }, - // paging (Latest X) - const [pageSize, setPageSize] = useState(5); - const [offset, setOffset] = useState(0); + mediaLink: { + display: 'block', + borderRadius: 16, + overflow: 'hidden', + }, - const total = items.length; - const start = offset; - const end = Math.min(offset + pageSize, total); - const page = items.slice(start, end); + media: { + borderRadius: 16, + border: `1px solid var(--wn-card-border)`, + background: + 'radial-gradient(180px 120px at 30% 20%, rgba(106,92,255,0.35), transparent), ' + + 'radial-gradient(220px 160px at 75% 70%, rgba(168,160,255,0.28), transparent), ' + + 'var(--wn-card)', + padding: 18, + }, - const canPrev = start > 0; - const canNext = end < total; + img: { + display: 'block', + width: '100%', + height: 'auto', + borderRadius: 10, + }, - const changeSize = (n: number) => { - setPageSize(n); - setOffset(0); - }; - const prev = () => setOffset(Math.max(0, offset - pageSize)); - const next = () => setOffset(Math.min(total, offset + pageSize)); + placeholder: { + width: '100%', + height: 260, + borderRadius: 12, + background: + 'repeating-linear-gradient( 45deg, rgba(0,0,0,0.06), rgba(0,0,0,0.06) 10px, transparent 10px, transparent 20px )', + }, - return ( -
    - {/* HERO */} -
    -

    What's New

    + readRow: { + marginTop: 10, + }, -

    - Track Mixpanel product releases and improvements in one place. See what’s - new, what got faster, and what opens up entirely new ways to answer questions about your - product. These changes are built from customer feedback and real workflows—less setup, - fewer manual steps, clearer answers. -

    -

    - From performance boosts to streamlined analysis and collaboration, each release is here to - shorten the path from “what happened?” to “what should we do?”. Browse the highlights below - and put the most impactful updates to work on your team today. -

    - - - Browse Changelog - -
    + readLink: { + fontSize: 14, + textDecoration: 'underline', + textUnderlineOffset: '3px', + color: 'var(--wn-text)', + }, - {/* TOP BAR — single line */} -
    -
    - Showing {total === 0 ? 0 : start + 1}–{end} of {total} -
    - -
    + footerRow: { + marginTop: 28, + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + gap: 12, + }, - {/* LIST + TIMELINE */} -
      -
    - - {/* BOTTOM BAR — single line */} - -
    - ); + footerLink: { + color: 'var(--wn-text)', + textDecoration: 'underline', + textUnderlineOffset: '4px', + fontSize: 14, + }, } From f61ea21141acd9fae3d31d8e4922794d98f51c05 Mon Sep 17 00:00:00 2001 From: kurbycchua <50901466+kurbycchua@users.noreply.github.com> Date: Mon, 6 Oct 2025 18:42:34 +0800 Subject: [PATCH 061/257] Update WhatsNewVerticalV2.tsx --- components/WhatsNewVerticalV2.tsx | 761 +++++++++++++----------------- 1 file changed, 340 insertions(+), 421 deletions(-) diff --git a/components/WhatsNewVerticalV2.tsx b/components/WhatsNewVerticalV2.tsx index 6a65c2b6db..56c5e579be 100644 --- a/components/WhatsNewVerticalV2.tsx +++ b/components/WhatsNewVerticalV2.tsx @@ -1,479 +1,398 @@ -import * as React from 'react' -import { getPagesUnderRoute } from 'nextra/context' +'use client'; -// ----------------------------- -// Theme variables for light/dark -// ----------------------------- -function ThemeVars() { - return ( - - ) -} - -// ----------------------------- -// Types -// ----------------------------- type Item = { - route: string - name: string - frontMatter?: { - title?: string - date?: string - image?: string - } -} - -function parseDate(s?: string): Date | null { - if (!s) return null - const d = new Date(s) - return isNaN(+d) ? null : d -} - -function formatDate(d: Date): string { - const dt = new Intl.DateTimeFormat('en-GB', { - day: '2-digit', - month: 'short', - year: 'numeric', - }) - // 27 Sept 2025 - const parts = dt.formatToParts(d) - const day = parts.find(p => p.type === 'day')?.value ?? '' - const mon = parts.find(p => p.type === 'month')?.value ?? '' - const yr = parts.find(p => p.type === 'year')?.value ?? '' - return `${day} ${mon} ${yr}` -} - -function isNew(d: Date | null): boolean { - if (!d) return false - const ms = Date.now() - d.getTime() - return ms <= 14 * 24 * 60 * 60 * 1000 -} - -// ----------------------------- -// Main component -// ----------------------------- -export default function WhatsNewVertical() { - // Pull all changelog posts - const raw = getPagesUnderRoute('/changelogs') as unknown as Item[] - - // Normalize & sort (newest first) - const items = React.useMemo(() => { - const mapped = (raw || []).map((p) => { - const date = parseDate(p.frontMatter?.date) - const title = p.frontMatter?.title ?? p.name ?? 'Untitled' - const image = p.frontMatter?.image - return { ...p, title, date, image } - }) - mapped.sort((a, b) => { - const da = a.date ? a.date.getTime() : 0 - const db = b.date ? b.date.getTime() : 0 - return db - da + url: string; + title: string; + date: string; + thumbnail: string; +}; + +const changelogPages = getPagesUnderRoute('/changelogs'); + +/* ---------- helpers ---------- */ +const parseDate = (s = '') => { + const m = s.match(/(\d{4}-\d{2}-\d{2})/); + return m ? m[1] : ''; +}; +const humanize = (s = '') => + s.replace(/^\d{4}-\d{2}-\d{2}-/, '').replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase()); + +const fmtDay = (dateStr: string) => { + const d = new Date(dateStr); + if (isNaN(d as any)) return dateStr || ''; + return d.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' }); +}; + +const firstNonEmpty = (...vals: any[]) => + vals.find((v) => { + if (!v) return false; + if (Array.isArray(v)) return v.length > 0 && typeof v[0] === 'string'; + return typeof v === 'string'; + }); + +const clampStyle = (lines: number): React.CSSProperties => ({ + display: '-webkit-box', + WebkitLineClamp: lines, + WebkitBoxOrient: 'vertical', + overflow: 'hidden' +}); + +/* ---------- build items (NEWEST → OLDEST) ---------- */ +function buildItems(): Item[] { + return (changelogPages || []) + .map((p: any) => { + const fm = p.frontMatter || p.meta || {}; + const route = p.route || ''; + if (!/\/changelogs\/.+/.test(route)) return null; + + const name = p.name || route.split('/').pop() || ''; + const date = fm.date || parseDate(name) || parseDate(route); + const thumb = firstNonEmpty( + fm.thumbnail, + fm.image, + fm.cover, + fm.ogImage, + fm.hero, + fm.screenshot, + Array.isArray(fm.images) ? fm.images[0] : undefined + ) as string | undefined; + + return { + url: route, + title: fm.title || p.title || humanize(name), + date, + thumbnail: thumb || '' + } as Item; }) - return mapped - }, [raw]) - - // Pagination - const SIZE_OPTIONS = [5, 10, 15] - const [size, setSize] = React.useState(SIZE_OPTIONS[0]) - const [page, setPage] = React.useState(0) - - const total = items.length - const start = page * size - const end = Math.min(start + size, total) - - React.useEffect(() => { - if (start >= total) setPage(0) - }, [size, total]) // reset if the list shrinks - - const pageItems = items.slice(start, end) - - const next = () => setPage((p) => (end >= total ? p : p + 1)) - const prev = () => setPage((p) => (p <= 0 ? 0 : p - 1)) - - return ( -
    - - - {/* Header */} -
    -

    What's New

    - -
    - {/* PREFERRED FIX: use theme tokens inline (no hard-coded white) */} -

    - Track Mixpanel product releases and improvements in one place.{' '} - See what’s new, what got faster, and what opens up entirely new ways to answer questions about your product. - These changes are built from customer feedback and real workflows—less setup, fewer manual steps, clearer answers. -

    -

    - From performance boosts to streamlined analysis and collaboration, each release is here to shorten the path - from “what happened?” to “what should we do?”. Browse the highlights below and put the most impactful updates - to work on your team today. -

    - Browse Changelog -
    - - {/* Controls row */} -
    -
    - Showing {total === 0 ? 0 : start + 1}–{end} of {total} -
    - -
    - Show - - - - -
    -
    -
    - - {/* List with timeline gutter */} -
    -
    - {pageItems.map((it) => { - const d = it.date - const badge = isNew(d) - return ( - - ) - })} -
    - - {/* Footer controls */} -
    - Browse the full Changelog → -
    - - -
    -
    -
    - ) + .filter(Boolean) + .sort((a: Item, b: Item) => new Date(b.date).getTime() - new Date(a.date).getTime()); } -// ----------------------------- -// Styles (inline → preferred fix) -// ----------------------------- -const s: Record = { - page: { - position: 'relative', - margin: '0 auto', - maxWidth: 980, - padding: '8px 0 64px', - }, - - headerWrap: { - marginBottom: 12, - }, +/* ---------- shared inline styles ---------- */ +const TL_X = 12; // timeline line X (relative to UL left) +const TL_PAD = TL_X + 16; // left padding so content clears the gutter +const s = { + page: { maxWidth: 880, margin: '0 auto' }, h1: { - fontSize: 44, + marginTop: 16, + marginBottom: 0, + fontSize: '44px', lineHeight: 1.1, - fontWeight: 700, - margin: '0 0 12px 0', - color: 'var(--wn-text)', - }, - - hero: { - marginTop: 6, - marginBottom: 8, + fontWeight: 600 as const, + letterSpacing: '-0.02em' as const, }, - heroP: { - // THE KEY: set tokenized color inline so Light mode is readable marginTop: 12, fontSize: 15, lineHeight: 1.6, - color: 'var(--wn-text)', + color: 'rgba(255,255,255,0.8)', }, - heroLink: { marginTop: 12, - color: 'var(--wn-text)', fontSize: 14, textDecoration: 'underline', textUnderlineOffset: '4px', + color: 'rgba(255,255,255,0.85)', display: 'inline-block', }, - - controlsRow: { + rowBar: { marginTop: 24, display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12, }, - showing: { fontSize: 12, - color: 'var(--wn-muted)', - }, - - controlsRight: { - display: 'flex', - alignItems: 'center', - gap: 8, + color: 'rgba(255,255,255,0.55)', }, - - showLabel: { + controlsWrap: { whiteSpace: 'nowrap' as const, minWidth: 0 }, + btn: { fontSize: 12, - color: 'var(--wn-muted)', - marginRight: 4, + padding: '6px 8px', + borderRadius: 6, + border: '1px solid rgba(255,255,255,0.18)', + background: 'transparent', + color: 'inherit', + cursor: 'pointer', }, - select: { - appearance: 'none', - WebkitAppearance: 'none', - MozAppearance: 'none', - border: `1px solid var(--wn-card-border)`, + fontSize: 12, + padding: '6px 8px', + borderRadius: 6, + border: '1px solid rgba(255,255,255,0.18)', background: 'transparent', - color: 'var(--wn-text)', - borderRadius: 8, - padding: '6px 28px 6px 10px', - fontSize: 14, + color: 'inherit', + }, + /* list + timeline */ + list: { + marginTop: 12, + listStyle: 'none', + padding: 0, position: 'relative' as const, - backgroundImage: - `linear-gradient(45deg, transparent 50%, var(--wn-text) 50%), - linear-gradient(135deg, var(--wn-text) 50%, transparent 50%)`, - backgroundPosition: 'right 10px center, right 5px center', - backgroundSize: '6px 6px, 6px 6px', - backgroundRepeat: 'no-repeat', + paddingLeft: TL_PAD, }, - - navBtn: { - fontSize: 14, - border: `1px solid var(--wn-card-border)`, - background: 'transparent', - color: 'var(--wn-text)', - padding: '6px 10px', - borderRadius: 8, - cursor: 'pointer', - transition: 'background 160ms ease', + timelineLine: { + position: 'absolute' as const, + left: TL_X, + top: 0, + bottom: 0, + width: 2, + background: 'rgba(255,255,255,0.07)', }, - - listWrap: { - position: 'relative', - marginTop: 20, + /* card */ + cardLi: { padding: '12px 0', position: 'relative' as const }, + cardA: { display: 'block', borderRadius: 12, padding: 12, textDecoration: 'none' }, + cardHeader: { + display: 'grid', + gridTemplateColumns: '1fr auto', + alignItems: 'center', + gap: 12, + marginBottom: 8, }, - - timeline: { - position: 'absolute', - left: -24, - top: 12, - bottom: 12, - width: 2, - background: - 'linear-gradient(to bottom, var(--wn-card-border), var(--wn-card-border))', - borderRadius: 2, - pointerEvents: 'none', + cardTitle: { + fontSize: 20, + fontWeight: 600 as const, + lineHeight: 1.2, + textDecorationThickness: '1px', + textUnderlineOffset: '4px', }, - - card: { - position: 'relative', - margin: '28px 0 40px', - paddingLeft: 0, + cardDate: { fontSize: 12, color: 'rgba(255,255,255,0.55)' }, + imgWrap: { + width: '100%', + borderRadius: 12, + overflow: 'hidden', + aspectRatio: '16 / 9', + background: + 'radial-gradient(120% 120% at 0% 100%, rgba(168,85,247,0.18), transparent 60%), radial-gradient(120% 120% at 100% 0%, rgba(59,130,246,0.18), transparent 60%)', }, - - dot: { - position: 'absolute', - left: -28, - top: 8, - width: 10, - height: 10, - background: 'var(--wn-dot)', - borderRadius: '999px', - boxShadow: '0 0 0 3px var(--wn-ring)', + readLink: { + marginTop: 6, + fontSize: 13, + textDecoration: 'underline', + textUnderlineOffset: '4px', + display: 'inline-block', }, - - titleRow: { - display: 'flex', - alignItems: 'baseline', - justifyContent: 'space-between', - gap: 12, - marginBottom: 10, + footerLink: { + fontSize: 14, + color: 'rgb(167 139 250)', + textDecoration: 'underline', + textUnderlineOffset: '4px', }, - - h2: { - fontSize: 22, - lineHeight: 1.25, - fontWeight: 700, - color: 'var(--wn-text)', - margin: 0, - display: 'flex', - alignItems: 'center', - gap: 10, + /* timeline dot */ + dot: { + position: 'absolute' as const, + left: -(TL_PAD - TL_X), // aligns dot on the vertical line + top: 12, // aligns to top of the card + width: 8, + height: 8, + borderRadius: 999, + background: 'rgb(167 139 250)', // violet dot + boxShadow: '0 0 0 2px rgba(20,20,30,0.9)', // ring to separate from bg }, - + /* NEW badge */ newBadge: { + marginLeft: 8, fontSize: 11, - lineHeight: 1, - padding: '4px 6px', + fontWeight: 700 as const, + letterSpacing: '0.02em', + color: 'rgb(26, 26, 31)', + background: + 'linear-gradient(90deg, rgba(167,139,250,0.95), rgba(99,102,241,0.95))', borderRadius: 999, - border: `1px solid var(--wn-card-border)`, - background: 'transparent', - color: 'var(--wn-text)', + padding: '2px 6px', + lineHeight: 1.1, + verticalAlign: 'middle', }, +}; + +/* ---------- control components ---------- */ +function ControlsTop({ + pageSize, + canPrev, + canNext, + changeSize, + prev, + next, +}: { + pageSize: number; + canPrev: boolean; + canNext: boolean; + changeSize: (n: number) => void; + prev: () => void; + next: () => void; +}) { + return ( +
    + Show + + + +
    + ); +} - cardDate: { - fontSize: 12, - color: 'var(--wn-muted)', - whiteSpace: 'nowrap', - }, +function ControlsBottom({ + canPrev, + canNext, + prev, + next, +}: { + canPrev: boolean; + canNext: boolean; + prev: () => void; + next: () => void; +}) { + return ( +
    + + +
    + ); +} - mediaLink: { - display: 'block', - borderRadius: 16, - overflow: 'hidden', - }, +/* ---------- card ---------- */ +function Row({ item }: { item: Item }) { + // NEW badge if within last 14 days + const isNew = (() => { + const d = new Date(item.date); + if (isNaN(d as any)) return false; + const days = (Date.now() - d.getTime()) / (1000 * 60 * 60 * 24); + return days <= 14; + })(); - media: { - borderRadius: 16, - border: `1px solid var(--wn-card-border)`, - background: - 'radial-gradient(180px 120px at 30% 20%, rgba(106,92,255,0.35), transparent), ' + - 'radial-gradient(220px 160px at 75% 70%, rgba(168,160,255,0.28), transparent), ' + - 'var(--wn-card)', - padding: 18, - }, + return ( +
  • + {/* timeline dot */} +
  • + ); +} - readRow: { - marginTop: 10, - }, +/* ---------- main ---------- */ +export default function WhatsNewVertical() { + const items = useMemo(buildItems, []); - readLink: { - fontSize: 14, - textDecoration: 'underline', - textUnderlineOffset: '3px', - color: 'var(--wn-text)', - }, + // paging (Latest X) + const [pageSize, setPageSize] = useState(5); + const [offset, setOffset] = useState(0); - footerRow: { - marginTop: 28, - display: 'flex', - alignItems: 'center', - justifyContent: 'space-between', - gap: 12, - }, + const total = items.length; + const start = offset; + const end = Math.min(offset + pageSize, total); + const page = items.slice(start, end); - footerLink: { - color: 'var(--wn-text)', - textDecoration: 'underline', - textUnderlineOffset: '4px', - fontSize: 14, - }, + const canPrev = start > 0; + const canNext = end < total; + + const changeSize = (n: number) => { + setPageSize(n); + setOffset(0); + }; + const prev = () => setOffset(Math.max(0, offset - pageSize)); + const next = () => setOffset(Math.min(total, offset + pageSize)); + + return ( +
    + {/* HERO */} +
    +

    What's New

    + +

    + Track Mixpanel product releases and improvements in one place. See what’s + new, what got faster, and what opens up entirely new ways to answer questions about your + product. These changes are built from customer feedback and real workflows—less setup, + fewer manual steps, clearer answers. +

    +

    + From performance boosts to streamlined analysis and collaboration, each release is here to + shorten the path from “what happened?” to “what should we do?”. Browse the highlights below + and put the most impactful updates to work on your team today. +

    + + + Browse Changelog + +
    + + {/* TOP BAR — single line */} +
    +
    + Showing {total === 0 ? 0 : start + 1}–{end} of {total} +
    + +
    + + {/* LIST + TIMELINE */} +
      +
    + + {/* BOTTOM BAR — single line */} + +
    + ); } From 6fcaa9b970c94e2345563a0fb7aefa0fd8b76541 Mon Sep 17 00:00:00 2001 From: kurbycchua <50901466+kurbycchua@users.noreply.github.com> Date: Mon, 6 Oct 2025 18:47:56 +0800 Subject: [PATCH 062/257] Update WhatsNewVerticalV2.tsx --- components/WhatsNewVerticalV2.tsx | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/components/WhatsNewVerticalV2.tsx b/components/WhatsNewVerticalV2.tsx index 56c5e579be..f11f2807bc 100644 --- a/components/WhatsNewVerticalV2.tsx +++ b/components/WhatsNewVerticalV2.tsx @@ -71,7 +71,7 @@ function buildItems(): Item[] { .sort((a: Item, b: Item) => new Date(b.date).getTime() - new Date(a.date).getTime()); } -/* ---------- shared inline styles ---------- */ +/* ---------- shared inline styles (theme-safe) ---------- */ const TL_X = 12; // timeline line X (relative to UL left) const TL_PAD = TL_X + 16; // left padding so content clears the gutter @@ -89,14 +89,14 @@ const s = { marginTop: 12, fontSize: 15, lineHeight: 1.6, - color: 'rgba(255,255,255,0.8)', + /* inherit color so it works in light & dark */ }, heroLink: { marginTop: 12, fontSize: 14, textDecoration: 'underline', textUnderlineOffset: '4px', - color: 'rgba(255,255,255,0.85)', + /* inherit color; let site/theme decide link color */ display: 'inline-block', }, rowBar: { @@ -108,14 +108,14 @@ const s = { }, showing: { fontSize: 12, - color: 'rgba(255,255,255,0.55)', + opacity: 0.7, // theme-safe }, controlsWrap: { whiteSpace: 'nowrap' as const, minWidth: 0 }, btn: { fontSize: 12, padding: '6px 8px', borderRadius: 6, - border: '1px solid rgba(255,255,255,0.18)', + border: '1px solid currentColor', // theme-safe border background: 'transparent', color: 'inherit', cursor: 'pointer', @@ -124,7 +124,7 @@ const s = { fontSize: 12, padding: '6px 8px', borderRadius: 6, - border: '1px solid rgba(255,255,255,0.18)', + border: '1px solid currentColor', // theme-safe border background: 'transparent', color: 'inherit', }, @@ -142,7 +142,8 @@ const s = { top: 0, bottom: 0, width: 2, - background: 'rgba(255,255,255,0.07)', + background: 'currentColor', // theme-safe + opacity: 0.12, // subtle in both themes }, /* card */ cardLi: { padding: '12px 0', position: 'relative' as const }, @@ -161,7 +162,10 @@ const s = { textDecorationThickness: '1px', textUnderlineOffset: '4px', }, - cardDate: { fontSize: 12, color: 'rgba(255,255,255,0.55)' }, + cardDate: { + fontSize: 12, + opacity: 0.6, // theme-safe instead of white + }, imgWrap: { width: '100%', borderRadius: 12, @@ -179,7 +183,7 @@ const s = { }, footerLink: { fontSize: 14, - color: 'rgb(167 139 250)', + color: 'rgb(167 139 250)', // accent that reads on both themes textDecoration: 'underline', textUnderlineOffset: '4px', }, @@ -191,8 +195,8 @@ const s = { width: 8, height: 8, borderRadius: 999, - background: 'rgb(167 139 250)', // violet dot - boxShadow: '0 0 0 2px rgba(20,20,30,0.9)', // ring to separate from bg + background: 'rgb(167 139 250)', // violet dot (visible in both) + boxShadow: '0 0 0 2px rgba(20,20,30,0.9)', // subtle ring for contrast }, /* NEW badge */ newBadge: { From 051c6875e28a6b00f7e097541978d64f92ce18ad Mon Sep 17 00:00:00 2001 From: kurbycchua <50901466+kurbycchua@users.noreply.github.com> Date: Mon, 6 Oct 2025 19:02:32 +0800 Subject: [PATCH 063/257] Update WhatsNewVerticalV2.tsx --- components/WhatsNewVerticalV2.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/components/WhatsNewVerticalV2.tsx b/components/WhatsNewVerticalV2.tsx index f11f2807bc..8ec4c12c31 100644 --- a/components/WhatsNewVerticalV2.tsx +++ b/components/WhatsNewVerticalV2.tsx @@ -232,7 +232,7 @@ function ControlsTop({ }) { return (
    - Show + Show changeSize(Number(e.target.value))}> {[5, 10, 15, 20].map((n) => (
    -
    - {item.thumbnail ? ( +
    + {showImage && ( // eslint-disable-next-line @next/next/no-img-element - ) : ( -
    + )} + + {!showImage && item.videoSrc && ( +
    @@ -391,7 +519,7 @@ export default function WhatsNewVertical() { {/* BOTTOM BAR — single line */}
    From 099756ee67ffe5bf0ba475ddd26fe722ef695ff1 Mon Sep 17 00:00:00 2001 From: kurbycchua <50901466+kurbycchua@users.noreply.github.com> Date: Mon, 6 Oct 2025 21:33:12 +0800 Subject: [PATCH 065/257] Update WhatsNewVerticalV2.tsx --- components/WhatsNewVerticalV2.tsx | 183 +++++++++--------------------- 1 file changed, 51 insertions(+), 132 deletions(-) diff --git a/components/WhatsNewVerticalV2.tsx b/components/WhatsNewVerticalV2.tsx index aab4b5daaa..8a69d8f65d 100644 --- a/components/WhatsNewVerticalV2.tsx +++ b/components/WhatsNewVerticalV2.tsx @@ -1,6 +1,6 @@ 'use client'; -import React, { useMemo, useState, useRef } from 'react'; +import React, { useMemo, useState } from 'react'; import { getPagesUnderRoute } from 'nextra/context'; type Item = { @@ -8,10 +8,7 @@ type Item = { title: string; date: string; thumbnail: string; - // NEW: optional media - videoSrc?: string; // self-hosted .mp4/.webm/etc - videoPoster?: string; // optional poster for the