From 03f9e3bc2e027156c757450fa41f97753e96475a Mon Sep 17 00:00:00 2001 From: Daniel Date: Thu, 29 Oct 2026 20:30:45 +0700 Subject: [PATCH] feat: apply diff review design to homepage and public pages Rebuild the homepage as a pull-request style review: a diff hero, live stats as CI-style checks, a files-changed sidebar, review comments on example releases, FAQ threads and a merge box with the digest form. Session redirect, JSON-LD and FAQ schema are unchanged. Add shared public building blocks (SiteHeader, SiteFooter, PageIntro, FileCard, DiffLine, ReviewComment, MergeBox, Checks, StatGrid, CenteredShell) and move stacks, release, upgrade planner, skills, status, legal, sign-in, 404, error and unsubscribe pages onto them. Auth, vote, digest and unsubscribe logic is untouched; those files change markup and copy only. Remove the unused StackPulseHero, document the new language in DESIGN_SYSTEM.md and mock the new imports in the release page test. Co-Authored-By: Claude Opus 5.5 --- docs/DESIGN_SYSTEM.md | 59 +- src/app/(auth)/sign-in/page.tsx | 57 +- src/app/digest/unsubscribe/page.tsx | 151 ++-- src/app/global-error.tsx | 13 +- src/app/globals.css | 24 +- src/app/not-found.tsx | 81 +-- src/app/page.tsx | 680 ++++++++++-------- src/app/privacy/page.tsx | 2 +- src/app/skills/[slug]/page.tsx | 19 +- .../skills/[slug]/updates/[updateId]/page.tsx | 38 +- src/app/skills/loading.tsx | 11 +- src/app/skills/page.tsx | 21 +- src/app/stacks/[slug]/loading.tsx | 131 ++-- src/app/stacks/[slug]/page.tsx | 405 +++++------ .../[slug]/releases/[releaseId]/loading.tsx | 49 +- .../[slug]/releases/[releaseId]/page.tsx | 159 ++-- src/app/stacks/[slug]/upgrade/loading.tsx | 98 +-- src/app/stacks/[slug]/upgrade/page.tsx | 324 ++++----- src/app/stacks/page.tsx | 251 +++---- src/app/status/page.tsx | 184 +++-- src/app/terms/page.tsx | 2 +- src/components/landing/digest-signup-form.tsx | 37 +- src/components/landing/legal-shell.tsx | 79 +- src/components/review/review-ui.tsx | 397 ++++++++++ src/components/site/site-footer.tsx | 66 ++ src/components/site/site-header.tsx | 77 ++ src/components/skills/skill-ui.tsx | 33 +- src/components/stack-pulse-hero.tsx | 252 ------- src/components/ui/error-view.tsx | 104 +-- tests/release-votes.test.mjs | 3 + 30 files changed, 1907 insertions(+), 1900 deletions(-) create mode 100644 src/components/review/review-ui.tsx create mode 100644 src/components/site/site-footer.tsx create mode 100644 src/components/site/site-header.tsx delete mode 100644 src/components/stack-pulse-hero.tsx diff --git a/docs/DESIGN_SYSTEM.md b/docs/DESIGN_SYSTEM.md index 4da8790..fe8319e 100644 --- a/docs/DESIGN_SYSTEM.md +++ b/docs/DESIGN_SYSTEM.md @@ -1,7 +1,7 @@ # Design system **Status:** current -**Last verified:** 2026-10-16 (theme architecture + tokens) +**Last verified:** 2026-10-29 (diff review language for public pages; theme architecture + tokens) StackPulse **UI design system** and application architecture. Read this before adding pages, components, or visual changes so new work matches the terminal-inspired product shell. @@ -89,9 +89,24 @@ flowchart TB - Dark void or off-white background with dotted grid + subtle noise (`globals.css` `body::before/after`) - Monospace voice for headings, labels, CLI copy, errors -- “Window chrome” panels: traffic-light dots, title bars, faux shell paths (`~/dashboard/feed`) - Syntax-inspired accents on semantic meaning (lime = primary action, rose = danger/breaking) -- Lowercase headings with a **lime full stop** after the title: `your feed.` + +**Public pages use the "diff review" language** (homepage, stacks, skills, status, legal, +sign-in, 404/error, digest unsubscribe): every page reads like a pull request. + +- Page intro: state badge + breadcrumb path (`stackpulse / stacks / react`), then a sentence-case + mono title, lede and actions (`PageIntro`). +- Panels are changed files: a file-path header that sticks under the site header, then content + (`FileCard`). Diff gutters (`DiffLine`: `+` emerald, `-` rose, `@@` hunk cyan) are used where the + content really is a list of changes or a file. +- Risky release notes are review comments from `stackpulse` with a semantic badge (`ReviewComment`). +- Real counts render as CI-style checks or stat tiles (`Checks`, `StatGrid`); never invent numbers. + Example data is labelled `example`. +- Calls to action are a merge box (`MergeBox`). + +The signed-in app (dashboard, onboarding, settings) still uses the original window-chrome panels +(`.frame` + `.frame-titlebar` + `win-dots`) and lowercase headings with a **lime full stop** +(`your feed.`). New public surfaces use the diff review language. **Do not introduce:** @@ -195,12 +210,12 @@ Fonts loaded in `src/app/layout.tsx` (`Geist`, `Geist_Mono`). | Pattern | Usage | |---------|--------| -| **Sticky header** | `sticky top-0 z-30 border-b border-line bg-void/80 backdrop-blur`, height `h-14`, `max-w-7xl px-6` | -| **Content width** | Landing/stacks: `max-w-7xl`; dashboard feed: `max-w-4xl`; legal: `max-w-3xl`; auth/errors: `max-w-md`–`max-w-lg` | -| **Page intro block** | Kicker → `h1` → `p` with `animate-fade-up` | -| **Frame panel** | `.frame` + optional `.frame-titlebar` + `win-dots` — cards, auth, errors, empty states | -| **Primary button** | `rounded-md bg-lime px-4 py-2.5 font-mono font-semibold text-void hover:bg-lime/85` with optional `$` prefix | -| **Secondary button** | `border border-ruling bg-shade … hover:border-edge hover:bg-lift` | +| **Site header** | Public pages: `SiteHeader` (`components/site/site-header.tsx`), sticky `h-14`, `bg-void/85 backdrop-blur-md`; app pages keep their own sticky header | +| **Content width** | Public: `max-w-6xl` (release `max-w-5xl`, upgrade `max-w-4xl`, legal `max-w-3xl`); dashboard feed: `max-w-4xl`; auth/errors: `max-w-md` (`CenteredShell`) | +| **Page intro block** | Public: `PageIntro`. App: kicker → `h1` → `p` with `animate-fade-up` | +| **File card** | Public: `FileCard` — `rounded-xl border-line bg-shade`, `bg-lift` file-path header sticky at `top-14` (`overflow-clip`, not `overflow-hidden`) | +| **Frame panel** | App only: `.frame` + optional `.frame-titlebar` + `win-dots` | +| **Buttons** | `button.primary` / `button.secondary` / `button.ghost` from `components/review/review-ui.tsx`: `min-h-11 rounded-lg font-mono text-[13px]`, lime primary with `text-void` | | **Destructive** | `bg-rose text-void` or rose text on rose/5 hover bg | Reuse `` in headers; link home with `hover:opacity-80 transition-opacity`. @@ -215,7 +230,7 @@ Reuse `` in headers; link home with `hover:opacity-80 transition-opacity | Tool | When | |------|------| | `animate-fade-up`, `animate-fade-in`, `stagger-*` | Page enter, lists | -| `motion/react` | Landing hero only (`stack-pulse-hero.tsx`) — avoid sprinkling motion elsewhere | +| `animate-diff-strike`, `animate-diff-land` | Homepage hero only: the removed line strikes through, then the added line lands. Pair with `motion-reduce:animate-none` | | `PulseLoader` | Loading states (not spinners) | | `.skeleton` | Placeholders — pulse, not shimmer gradient | @@ -231,26 +246,30 @@ Respect `prefers-reduced-motion` (already in `globals.css`). | `PulseLoader` | `components/ui/pulse-loader.tsx` | Inline/sm/lg loading | | `Button` | `components/ui/button.tsx` | shadcn/base-ui when variant system helps | | `LegalShell` | `components/landing/legal-shell.tsx` | Privacy/terms layout | -| `DigestSignupForm` | `components/landing/digest-signup-form.tsx` | Email capture block | +| `DigestSignupForm` | `components/landing/digest-signup-form.tsx` | Email capture block (`framed={false}` inside another card) | +| `SiteHeader`, `SiteFooter` | `components/site/` | Public header (sticky, theme switcher, optional `account` slot) and footer | +| `PageIntro`, `FileCard`, `DiffLines`/`DiffLine`, `ReviewComment`, `MergeBox`, `Checks`, `StatGrid`, `CardNote`, `Badge`, `CenteredShell`, `button` | `components/review/review-ui.tsx` | Diff review building blocks for public pages; server-safe | New surfaces should compose these before inventing parallel patterns. ### Copy voice -- **Lowercase** for UI chrome: `sign in`, `your feed`, `configure stack` -- **CLI metaphor:** `$ command`, `// comments`, `cd ..`, `exit code 1`, `→ hints` -- **Errors:** plain language, monospace, rose for fault — no corporate alert boxes +- **Lowercase** for small UI chrome: nav links, file paths, badges (`stacks`, `sign in`, `breaking`) +- **Sentence case** for public headings, ledes and buttons: `Review what breaks.`, `Sign in with GitHub` +- **Review metaphor** on public pages: file paths, `+`/`-` lines, `@@` hunks, "stackpulse reviewed"; + the app keeps the CLI metaphor (`$ command`, `// comments`, `exit code 1`) +- **Errors:** plain language, monospace, rose for fault — say what happened and the next step - **Avoid title case** in headings and buttons unless proper nouns (GitHub, StackPulse) ### Page templates When adding a page, pick the closest template: -1. **Marketing / landing** — `page.tsx`: hero section, `StackPulseHero`, frame blocks, footer with mono links +1. **Marketing / landing** — `page.tsx`: diff hero, checks, files-changed sidebar + `FileCard`s, `MergeBox`, `SiteFooter` 2. **App shell** — sticky header + kicker/title + main (`dashboard`, `onboarding`) -3. **Public content** — stacks layout: header, stats, release cards with signal badges -4. **Auth / empty / error** — centered `frame` with titlebar + terminal copy -5. **Legal** — `LegalShell` + `legal-prose` class +3. **Public content** — `SiteHeader` + `PageIntro` (stats `FileCard` in `aside`) + `FileCard`s + `SiteFooter` +4. **Auth / empty / error** — `CenteredShell` + `FileCard` (sign-in keeps the same look inline) +5. **Legal** — `LegalShell` (`PageIntro` + `FileCard` + `legal-prose`) --- @@ -286,8 +305,8 @@ release_fetch_runs (audit) Before shipping UI or architecture changes: - [ ] Uses design tokens (`text-dust`, `border-line`, `bg-lime`) — no ad-hoc hex -- [ ] Headings follow mono + lowercase + lime `.` pattern where appropriate -- [ ] Panels use `frame` / sticky header patterns from sibling pages +- [ ] Public pages use `SiteHeader`/`PageIntro`/`FileCard`/`SiteFooter`; app pages keep mono + lowercase + lime `.` headings +- [ ] Panels use `FileCard` (public) or `frame` (app) patterns from sibling pages - [ ] Badges match semantic tone table above - [ ] Icons from `hugeicons-react` in product surfaces - [ ] Loading uses `PulseLoader` or `.skeleton` diff --git a/src/app/(auth)/sign-in/page.tsx b/src/app/(auth)/sign-in/page.tsx index 0edf150..069ca88 100644 --- a/src/app/(auth)/sign-in/page.tsx +++ b/src/app/(auth)/sign-in/page.tsx @@ -29,49 +29,43 @@ export default function SignInPage() { return (
-
- +
+
- - cd .. +
-
-
- - - - +
+
+ auth/sign-in + + github only - ~/auth/sign-in.sh - secure
-
- $ auth --login --provider github -
-

- welcome back +

+ Welcome back.

-

- we use github sign-in only — same account you use to ship code. +

+ We use GitHub sign-in only, the same account you use to ship code.

{error && ( -

- → - error: {error} +

+ Could not sign in with GitHub. Try again in a moment.

)} -
-

{'// no email, no password, no spam.'}

-

{'// we read your public profile and email only.'}

-
+
    +
  • No password and no spam.
  • +
  • We read your public profile and email only.
  • +

-
- new here? - same button works for new accounts. +
+ New here? + The same button creates your account.
diff --git a/src/app/digest/unsubscribe/page.tsx b/src/app/digest/unsubscribe/page.tsx index 3844f21..7f09338 100644 --- a/src/app/digest/unsubscribe/page.tsx +++ b/src/app/digest/unsubscribe/page.tsx @@ -1,8 +1,7 @@ import type { Metadata } from 'next' import Link from 'next/link' -import { Logo } from '@/components/logo' -import { ThemeSwitcher } from '@/components/theme-switcher' +import { CenteredShell, FileCard } from '@/components/review/review-ui' import { unsubscribeFromDigest } from '@/lib/actions' export const metadata: Metadata = { @@ -29,97 +28,67 @@ export default async function UnsubscribePage({ const hasValidToken = UUID.test(token) return ( -
-
-
- - - - -
- -
-
- - - - - - ~/digest/unsubscribe.sh -
- -
- {isDone ? ( - <> -
- $ digest --unsubscribe -
-

- unsubscribed. -

-

- Your email was removed. No more weekly digests will be sent to it. -

- - ) : isError ? ( - <> -
- $ digest --unsubscribe -
-

- something broke. -

-

- Could not unsubscribe right now. Try the link from your email again in a minute. -

- - ) : hasValidToken ? ( - <> -
- $ digest --unsubscribe --confirm -
-

- stop the weekly digest? -

-

- Confirm to remove your email from the StackPulse weekly digest list. -

-
- - -
- - ) : ( - <> -
- $ digest --unsubscribe -
-

- invalid link. -

-

- This unsubscribe link is missing its token. Use the link from the bottom of a - digest email. -

- - )} + + +
+ {isDone ? ( + <> +

+ Unsubscribed. +

+

+ Your email was removed. No more weekly digests will be sent to it. +

+ + ) : isError ? ( + <> +

+ Could not unsubscribe. +

+

+ Something went wrong on our side. Try the link from your email again in a minute. +

+ + ) : hasValidToken ? ( + <> +

+ Stop the weekly digest? +

+

+ Confirm to remove your email from the StackPulse weekly digest list. +

+
+ + +
+ + ) : ( + <> +

+ Invalid link. +

+

+ This unsubscribe link is missing its token. Use the link from the bottom of a + digest email. +

+ + )} -
- - cd ~ → back to stackpulse - -
+
+ + Back to StackPulse +
-
-
+ + ) } diff --git a/src/app/global-error.tsx b/src/app/global-error.tsx index 1518182..2aab35d 100644 --- a/src/app/global-error.tsx +++ b/src/app/global-error.tsx @@ -45,25 +45,24 @@ export default function GlobalError({ maxWidth: '460px', background: 'var(--shade)', border: '1px solid var(--line)', - borderRadius: '6px', + borderRadius: '12px', overflow: 'hidden', }} >
- - - - ~/fatal + app/root.tsx + fatal
diff --git a/src/app/globals.css b/src/app/globals.css index 9b5079b..20b9ad3 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -84,6 +84,26 @@ } } + /* Homepage hero: the removed line strikes through, then the added line lands */ + --animate-diff-strike: diff-strike 0.6s cubic-bezier(0.65, 0, 0.35, 1) 0.5s both; + --animate-diff-land: diff-land 0.55s cubic-bezier(0.16, 1, 0.3, 1) 1.05s both; + + @keyframes diff-strike { + from { + transform: scaleX(0); + } + to { + transform: scaleX(1); + } + } + + @keyframes diff-land { + from { + opacity: 0; + transform: translateY(-0.25em); + } + } + /* Logo: beats rise off the feed line, flagged beat pings once */ @keyframes logo-rise { from { @@ -519,8 +539,8 @@ a:not([class]):hover { letter-spacing: -0.01em; } .legal-prose h2::before { - content: '§ '; - color: var(--lime); + content: '## '; + color: var(--fade); } .legal-prose p { margin-bottom: 1rem; diff --git a/src/app/not-found.tsx b/src/app/not-found.tsx index 4c6709a..e830f69 100644 --- a/src/app/not-found.tsx +++ b/src/app/not-found.tsx @@ -1,7 +1,7 @@ import type { Metadata } from 'next' import Link from 'next/link' -import { Logo } from '@/components/logo' -import { ThemeSwitcher } from '@/components/theme-switcher' + +import { CenteredShell, DiffLine, DiffLines, FileCard, button } from '@/components/review/review-ui' export const metadata: Metadata = { title: '404 — route not found', @@ -10,62 +10,29 @@ export const metadata: Metadata = { export default function NotFound() { return ( -
-
-
- - - - - http 404 -
- -
-
- - - - - - ~/router - ● 404 -
- -
-
- $ - cd - ./the-page-you-wanted -
-
- bash: cd: no such file or directory -
- -
- 404 -
-

- this route doesn't exist — it may have moved, or never shipped. -

- -
- - $ - cd ~/ - - - open dashboard - -
+ http 404}> + 404}> + + @@ route lookup @@ + + /the-page-you-wanted + + +
+

404

+

+ This route doesn't exist. It may have moved, or it never shipped. +

+
+ + Go home + + + Open the feed +
-
-
+ + ) } diff --git a/src/app/page.tsx b/src/app/page.tsx index 3b39c37..8393989 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,81 +1,124 @@ import Link from 'next/link' -import { redirect } from 'next/navigation' -import { getAuth } from '@/lib/auth' import { headers } from 'next/headers' -import { - ReactIcon, - TailwindcssIcon, - Typescript03Icon, - Database02Icon, - SourceCodeIcon, - ZapIcon, - ShadcnIcon, - ServerStack01Icon, - RocketIcon, - Package01Icon, - Atom01Icon, - Flag03Icon, -} from 'hugeicons-react' -import { Logo } from '@/components/logo' -import { ThemeSwitcher } from '@/components/theme-switcher' +import { redirect } from 'next/navigation' +import { GithubIcon } from 'hugeicons-react' + import { DigestSignupForm } from '@/components/landing/digest-signup-form' -import { StackPulseHero } from '@/components/stack-pulse-hero' +import { + Badge, + Checks, + DiffLine, + DiffLines, + FileCard, + MergeBox, + ReviewComment, + button, +} from '@/components/review/review-ui' +import { SiteFooter } from '@/components/site/site-footer' +import { SiteHeader } from '@/components/site/site-header' +import { getAuth } from '@/lib/auth' import { getLandingStats } from '@/lib/landing-stats' const steps = [ { - n: '01', - cmd: 'stack add ', - title: 'pick your stack', + title: 'Pick your stack', body: 'Paste your package.json and follow your whole stack in one click, choose from 90+ registry stacks, or add any GitHub repo URL.', }, { - n: '02', - cmd: 'pulse --watch', - title: 'we watch the releases', + title: 'StackPulse watches the releases', body: 'Every new GitHub release for your stack is fetched twice a day, parsed, and run through an AI summariser tuned for changelogs and migration notes.', }, { - n: '03', - cmd: 'feed --read', - title: 'review upgrade risk', + title: 'Review the upgrade risk', body: 'Read one feed with breaking changes, deprecations, new APIs, and source links called out before you update production dependencies.', }, ] const trackedStacks = [ - { name: 'react', slug: 'react', icon: ReactIcon }, - { name: 'next', slug: 'nextjs', icon: SourceCodeIcon }, - { name: 'tailwind', slug: 'tailwindcss', icon: TailwindcssIcon }, - { name: 'shadcn', slug: 'shadcn-ui', icon: ShadcnIcon }, - { name: 'typescript', slug: 'typescript', icon: Typescript03Icon }, - { name: 'drizzle', slug: 'drizzle-orm', icon: Database02Icon }, - { name: 'remix', slug: 'remix', icon: ServerStack01Icon }, - { name: 'astro', slug: 'astro', icon: RocketIcon }, - { name: 'vite', slug: 'vite', icon: ZapIcon }, - { name: 'bun', slug: 'bun', icon: Package01Icon }, - { name: 'svelte', slug: 'svelte', icon: Flag03Icon }, - { name: 'solid', slug: 'solidjs', icon: Atom01Icon }, + { name: 'react', slug: 'react' }, + { name: 'next', slug: 'nextjs' }, + { name: 'tailwind', slug: 'tailwindcss' }, + { name: 'shadcn', slug: 'shadcn-ui' }, + { name: 'typescript', slug: 'typescript' }, + { name: 'drizzle', slug: 'drizzle-orm' }, + { name: 'remix', slug: 'remix' }, + { name: 'astro', slug: 'astro' }, + { name: 'vite', slug: 'vite' }, + { name: 'bun', slug: 'bun' }, + { name: 'svelte', slug: 'svelte' }, + { name: 'solid', slug: 'solidjs' }, ] const releaseSignals = [ { + tone: 'breaking', label: 'breaking changes', title: 'Find the release notes that can break production.', body: 'StackPulse highlights risky changes from upstream changelogs so you can scan the impact before bumping a framework, package, or tool.', }, { + tone: 'deprecation', label: 'deprecations', title: 'Catch deprecated APIs before they disappear.', body: 'Follow the libraries in your stack and surface removals, renamed options, and migration warnings in the same place as normal releases.', }, { + tone: 'migration', label: 'upgrade notes', title: 'Turn long changelogs into practical next steps.', body: 'Each digest keeps the important context close: what changed, why it matters, and where to read the original release before changing code.', }, ] +const delivery = [ + { + title: 'Weekly digest', + body: 'One email on Mondays with what shipped in the stacks you follow.', + }, + { + title: 'Slack or Discord', + body: 'Send new releases above an importance threshold to one webhook.', + }, + { + title: 'Ask about a release', + body: 'Signed in, ask follow-up questions about a specific upgrade.', + }, + { + title: 'Public stack pages', + body: 'Source-linked release history for every stack in the registry.', + }, +] + +// Illustrative rows for the homepage review. Labelled "example" where they render. +const exampleReleases: { stack: string; version: string; signal: string; comment?: string }[] = [ + { + stack: 'next', + version: 'v16.2.6', + signal: 'breaking', + comment: + 'Migration impact highlighted. Read the summary and the source release before you bump.', + }, + { + stack: 'drizzle-orm', + version: 'v0.45.2', + signal: 'deprecation', + comment: 'Deprecated options called out. Plan the replacement before the next major.', + }, + { stack: 'tailwindcss', version: 'v4.0.0', signal: 'migration' }, + { stack: 'react', version: 'v19.2.0', signal: 'feature' }, + { stack: 'better-auth', version: 'v1.6.10', signal: 'feature' }, +] + +const files = [ + { id: 'feed', path: 'releases/since-last-read.diff', stat: '+5' }, + { id: 'how-it-works', path: 'how-it-works.sh', stat: '+6' }, + { id: 'signals', path: 'review/signals.md', stat: '3 comments' }, + { id: 'registry', path: 'registry.json', stat: '+12' }, + { id: 'mcp', path: '.mcp.json', stat: '+1' }, + { id: 'pricing', path: 'pricing.diff', stat: '+3' }, + { id: 'faq', path: 'FAQ.md', stat: '4 threads' }, +] + const faqItems = [ { question: 'What is StackPulse?', @@ -174,306 +217,313 @@ export default async function LandingPage() { dangerouslySetInnerHTML={{ __html: JSON.stringify(faqJsonLd) }} /> -
- - - - -
+
- +
+

+ open + + stackpulse wants to change how you + upgrade + +

- {/* Live stats */} - {stats && stats.releases > 0 && ( -
-
- $ stackpulse --stats - - {stats.releases.toLocaleString()}{' '} - releases distilled - - - {stats.breaking.toLocaleString()}{' '} - with breaking changes - - - {stats.stacks.toLocaleString()}{' '} - stacks in registry +
+
+ your-week.md + + +1 -1
-
- )} - - {/* Release signals */} -
-
-

- § release_intelligence -

-
- source-backed -
-
- -
-
- -

- StackPulse is built for developers who need to know when a framework release, - dependency update, or package changelog contains work that can affect production code. + - + + Read every changelog. + +

+

+ + Review what breaks. +

-
- {releaseSignals.map((signal) => ( -
-

- {signal.label} -

-

- {signal.title} -

-

{signal.body}

-
- ))} -
-
- - {/* How it works */} -
-
-

- § how_it_works -

-
- 3 steps -
-
- -
-

- How StackPulse tracks GitHub releases and turns them into AI-summarised digests. -

-
- {steps.map((s) => ( -
-
- {s.n} - step -
-
- $ - {s.cmd.split(' ')[0]}{' '} - {s.cmd.split(' ').slice(1).join(' ')} -
-

- {s.title} -

-

{s.body}

-
-
- ))} -
-
- - {/* Tracked stacks */} -
-
-

- § tracked_stacks -

-
- + custom repos -
-
- -
-

- Frameworks and libraries StackPulse tracks out of the box, plus support for any custom - GitHub repository. -

-
-
- - - - - - ~/registry.json - 90+ stacks in registry -
-
- {trackedStacks.map(({ name, slug, icon: Icon }) => ( - -
- {/* CTA bar */} -
-
-
- ready? -
-

- stop missing upgrade work. -

-

- Free, open-source, no paywall. Sign in with GitHub and start tracking framework - releases, breaking changes, and deprecation notes in under a minute. +

+ + +
+ example} + title="Your feed, as a review" > - $ - ./browse-public-feed - - - + @@ 5 releases since your last read @@ + {exampleReleases.map((release, i) => ( +
+ + {release.stack} + @ + {release.version} + {!release.comment && ( + + {release.signal} + + )} + + {release.comment && ( + + {release.comment} + + )} +
+ ))} + +
+ + +6} + title="How it works" > - $ - ./track-my-stack - -
- -
-
-
+ + {steps.map((step, i) => ( +
+ + + # {i + 1}. {step.title} + + + + {step.body} + +
+ ))} +
+ - {/* FAQ */} -
-
-

- § faq -

-
- searchable answers -
+ +
+ {releaseSignals.map((signal) => ( + + {signal.title}{' '} + {signal.body} + + ))} +
+
    + {delivery.map((item) => ( +
  • +

    {item.title}

    +

    {item.body}

    +
  • + ))} +
+
-
- {faqItems.map((item) => ( -
-

{item.question}

-

- {item.answer} -

-
- ))} -
-
+ + + + [ + + {trackedStacks.map((stack, i) => ( + + + "{stack.name}" + + , + + ))} + + + {'// …and more. Missing one? '} + + Add any public GitHub repo + + + + + ] + + + - +
+ +
) } diff --git a/src/app/privacy/page.tsx b/src/app/privacy/page.tsx index d7792ac..a582498 100644 --- a/src/app/privacy/page.tsx +++ b/src/app/privacy/page.tsx @@ -8,7 +8,7 @@ export const metadata: Metadata = { export default function PrivacyPage() { return ( - +

StackPulse is a non-profit, open-source service that helps developers track GitHub release notes for the libraries they use. We try to collect as little as possible. This page diff --git a/src/app/skills/[slug]/page.tsx b/src/app/skills/[slug]/page.tsx index 7e4f1c2..d604b55 100644 --- a/src/app/skills/[slug]/page.tsx +++ b/src/app/skills/[slug]/page.tsx @@ -1,6 +1,6 @@ import type { Metadata } from 'next' -import Link from 'next/link' import { notFound } from 'next/navigation' +import { PageIntro } from '@/components/review/review-ui' import { SkillDate, SkillPanel, SkillShell, UpdateRows } from '@/components/skills/skill-ui' import { SKILL_CATALOG, skillSourceUrl } from '@/lib/skill-catalog' import { getPublicSkill, skillCheckLabel } from '@/lib/public-skills' @@ -28,14 +28,15 @@ export default async function SkillPage({ params }: Props) { const { skill, updates } = data return ( - - ← all skills - -

- {skill.name} - . -

-

{skill.description}

+

{skill.repository} diff --git a/src/app/skills/[slug]/updates/[updateId]/page.tsx b/src/app/skills/[slug]/updates/[updateId]/page.tsx index ce6b61d..b1f2c01 100644 --- a/src/app/skills/[slug]/updates/[updateId]/page.tsx +++ b/src/app/skills/[slug]/updates/[updateId]/page.tsx @@ -1,6 +1,6 @@ import type { Metadata } from 'next' -import Link from 'next/link' import { notFound } from 'next/navigation' +import { PageIntro } from '@/components/review/review-ui' import { SkillDate, SkillPanel, SkillShell } from '@/components/skills/skill-ui' import { getPublicSkillUpdate, skillAnalysisLabel } from '@/lib/public-skills' @@ -37,20 +37,28 @@ export default async function SkillUpdatePage({ params }: Props) { } return ( - - ← {skill.name} history - -

- {skill.name} - . -

-

- {update.previousRevision.slice(0, 7)} → {update.newRevision.slice(0, 7)} -

-

- {skillAnalysisLabel(update.analysisStatus)} · detected{' '} - -

+ + {skill.name}{' '} + + {update.previousRevision.slice(0, 7)}…{update.newRevision.slice(0, 7)} + + + } + lede={ +

+ Detected +

+ } + /> {report ? ( <> diff --git a/src/app/skills/loading.tsx b/src/app/skills/loading.tsx index b47f8b2..b247f6e 100644 --- a/src/app/skills/loading.tsx +++ b/src/app/skills/loading.tsx @@ -1,14 +1,13 @@ import { SkillPanel, SkillShell } from '@/components/skills/skill-ui' +import { PulseLoader } from '@/components/ui/pulse-loader' export default function Loading() { return ( -

- reading skill revisions… -

+ -
-
-
+
+
+
) diff --git a/src/app/skills/page.tsx b/src/app/skills/page.tsx index cb5dfe0..bbe5113 100644 --- a/src/app/skills/page.tsx +++ b/src/app/skills/page.tsx @@ -1,5 +1,6 @@ import type { Metadata } from 'next' import Link from 'next/link' +import { PageIntro } from '@/components/review/review-ui' import { SkillDate, SkillPanel, SkillShell, UpdateRows } from '@/components/skills/skill-ui' import { getPublicSkills, skillCheckLabel } from '@/lib/public-skills' @@ -19,19 +20,13 @@ export default async function SkillsPage() { const { skills, updates } = await getPublicSkills() return ( -

- § agent skill intelligence · phase 1 -

-

- instructions change. -
- understand the impact. -

-

- Source changes, potential behavior shifts, and advisory update assessments for ten curated - skills. Compare immutable revisions before changing your pinned copy. -

-
+ +
{[ ['curated', skills.length], ['baselined', skills.filter((s) => s.baselineRevision).length], diff --git a/src/app/stacks/[slug]/loading.tsx b/src/app/stacks/[slug]/loading.tsx index 6cef2f0..de7980a 100644 --- a/src/app/stacks/[slug]/loading.tsx +++ b/src/app/stacks/[slug]/loading.tsx @@ -1,10 +1,6 @@ -import Link from 'next/link' -import { ArrowLeft01Icon } from 'hugeicons-react' - -import { Logo } from '@/components/logo' -import { ThemeSwitcher } from '@/components/theme-switcher' -import { Skeleton } from '@/components/ui/skeleton' +import { SiteHeader } from '@/components/site/site-header' import { PulseLoader } from '@/components/ui/pulse-loader' +import { Skeleton } from '@/components/ui/skeleton' // `params` and `searchParams` are URL-specific, so Partial Prefetching shares this App // Shell across every stack link instead of rendering every slug ahead of time. Chrome and @@ -12,103 +8,66 @@ import { PulseLoader } from '@/components/ui/pulse-loader' export default function StackPageLoading() { return (
-
- - - - -
- -
-
- # - stacks - / - -
+ -
+
+
-
- - +
+ stackpulse + / + stacks + / + +
+
+ +
-
- - - - +
+ + +
- +
-
- {['releases', 'breaking', 'security', 'deprecated', 'migrations'].map((label) => ( -
-
- {label} +
+
+ +
+
+ {['releases', 'breaking', 'security', 'deprecated', 'migrations'].map((label) => ( +
+
{label}
+
- -
- ))} -
-
- -
- {Array.from({ length: 3 }).map((_, i) => ( -
- - -
- - - -
+ ))}
- ))} -
- -
-
-

- # latest_releases -

-
- source-backed -
- -
- {['all', 'breaking', 'deprecation', 'migration', 'security'].map((signal) => ( - - ))}
+
-
+
+

+ Latest releases +

+
{Array.from({ length: 3 }).map((_, i) => ( -
-
+
+
- - + +
@@ -118,10 +77,6 @@ export default function StackPageLoading() {
-
- - -
))} diff --git a/src/app/stacks/[slug]/page.tsx b/src/app/stacks/[slug]/page.tsx index 529a1aa..7150e22 100644 --- a/src/app/stacks/[slug]/page.tsx +++ b/src/app/stacks/[slug]/page.tsx @@ -1,12 +1,22 @@ import type { Metadata } from 'next' import Link from 'next/link' import { notFound } from 'next/navigation' -import { ArrowLeft01Icon, ArrowUp01Icon, Link01Icon, RssIcon } from 'hugeicons-react' +import { ArrowUp01Icon, GitCompareIcon, Link01Icon, RssIcon } from 'hugeicons-react' import { DigestSignupForm } from '@/components/landing/digest-signup-form' -import { Logo } from '@/components/logo' import { ReleaseNotes } from '@/components/releases/release-notes' -import { ThemeSwitcher } from '@/components/theme-switcher' +import { + Badge, + CardNote, + FileCard, + PageIntro, + ReviewComment, + StatGrid, + button, + cx, +} from '@/components/review/review-ui' +import { SiteFooter } from '@/components/site/site-footer' +import { SiteHeader } from '@/components/site/site-header' import { getPublicStackPage, getPublicStackSlugs, @@ -20,20 +30,7 @@ type Props = { const appUrl = process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000' -const signalTone: Record = { - breaking: 'border-rose/30 bg-rose/10 text-rose', - deprecation: 'border-amber/30 bg-amber/10 text-amber', - migration: 'border-cyan/30 bg-cyan/10 text-cyan', - feature: 'border-emerald/30 bg-emerald/10 text-emerald', - security: 'border-rose/30 bg-rose/10 text-rose', -} - -const importanceTone: Record = { - critical: 'border-rose/30 bg-rose/10 text-rose', - high: 'border-amber/30 bg-amber/10 text-amber', - medium: 'border-cyan/30 bg-cyan/10 text-cyan', - low: 'border-fade/20 bg-dust/10 text-fade', -} +const visibleSignals = new Set(['breaking', 'deprecation', 'migration', 'feature', 'security']) const signalFilters = ['all', 'breaking', 'deprecation', 'migration', 'security'] as const type SignalFilter = (typeof signalFilters)[number] @@ -120,162 +117,154 @@ export default async function StackPage({ params, searchParams }: Props) { dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }} /> -
- - - - -
- -
-
- # - stacks/{data.tech.slug} - {data.tech.category && ( + + +
+ - / - {data.tech.category} + {withFullStop( + data.tech.description || `Follow ${data.tech.name} releases from GitHub`, + )}{' '} + StackPulse turns upstream changelogs into scannable summaries with risky changes, + deprecations, migration notes, and source links. - )} -
- -
-
-

- {data.tech.name} release notes, breaking changes, and upgrade notes - . -

-

- {data.tech.description || `Follow ${data.tech.name} releases from GitHub.`} StackPulse - turns upstream changelogs into scannable summaries with risky changes, deprecations, - migration notes, and source links. -

-
+ } + actions={ + <> + + Track this stack + + +
- -
- - - -
- -
-
+
+

- # latest_releases + Latest releases + + {filteredReleases.length} +

-
- source-backed -
- -
- {signalFilters.map((signal) => ( - - {signal} - - ))} +
{data.releases.length === 0 ? ( -
-

- no releases have been indexed for this stack yet. -

-

- follow it from the dashboard to start collecting release notes. -

-
+ + + Follow this stack from the dashboard to start collecting its release notes. New + releases are fetched at 00:00 and 12:00 UTC. + + ) : filteredReleases.length === 0 ? ( -
-

- no releases match the {signalFilter} filter. -

-

- try viewing all indexed releases for this stack. -

-
+ + + + View all indexed releases + + + ) : ( -
+
{filteredReleases.map((release) => ( ))} @@ -283,113 +272,75 @@ export default async function StackPage({ params, searchParams }: Props) { )}
+ +
) } +function withFullStop(text: string) { + return /[.!?]$/.test(text.trim()) ? text.trim() : `${text.trim()}.` +} + function parseSignalFilter(value: string | string[] | undefined): SignalFilter { const raw = Array.isArray(value) ? value[0] : value return signalFilters.includes(raw as SignalFilter) ? (raw as SignalFilter) : 'all' } -function SeoBlock({ label, title, body }: { label: string; title: string; body: string }) { - return ( -
-

{label}

-

{title}

-

{body}

-
- ) -} - function ReleaseArticle({ release, techSlug }: { release: PublicStackRelease; techSlug: string }) { const importance = release.importanceLevel || 'medium' - const visibleSignals = release.releaseSignals.filter((signal) => signal in signalTone) + const signals = release.releaseSignals.filter((signal) => visibleSignals.has(signal)) return ( -
-
+
+
{release.version} - - {importance} - - {visibleSignals.map((signal) => ( - + {importance} + {signals.map((signal) => ( + {signal} - + ))} - {release.isPrerelease && ( - - prerelease - - )} + {release.isPrerelease && prerelease} {release.publishedAt && ( - {formatDate(release.publishedAt)} + )}
-

+

{release.title || release.version}

- - view details - - - {release.rawReleaseUrl && ( - + - - view source on github - -> - - )} +
) } -function Stat({ - label, - value, - tone, -}: { - label: string - value: number - tone: 'ink' | 'rose' | 'amber' | 'cyan' -}) { - const toneClass = - tone === 'rose' - ? 'text-rose' - : tone === 'amber' - ? 'text-amber' - : tone === 'cyan' - ? 'text-cyan' - : 'text-ink' - - return ( -
-
{label}
-
{value}
-
- ) -} - function formatDate(date: Date) { return new Intl.DateTimeFormat('en-US', { month: 'short', diff --git a/src/app/stacks/[slug]/releases/[releaseId]/loading.tsx b/src/app/stacks/[slug]/releases/[releaseId]/loading.tsx index 8cb0dc2..f07b773 100644 --- a/src/app/stacks/[slug]/releases/[releaseId]/loading.tsx +++ b/src/app/stacks/[slug]/releases/[releaseId]/loading.tsx @@ -1,30 +1,35 @@ -import { Logo } from '@/components/logo' +import { SiteHeader } from '@/components/site/site-header' import { PulseLoader } from '@/components/ui/pulse-loader' import { Skeleton } from '@/components/ui/skeleton' export default function LoadingRelease() { return ( -
- -
- -
- -
-
- - - +
+ +
+ + +
+
+
+ +
+
+ + + +
+
+
+ + +
-
- - -
-
-
+
+
) } diff --git a/src/app/stacks/[slug]/releases/[releaseId]/page.tsx b/src/app/stacks/[slug]/releases/[releaseId]/page.tsx index 5dbef78..f5ecf83 100644 --- a/src/app/stacks/[slug]/releases/[releaseId]/page.tsx +++ b/src/app/stacks/[slug]/releases/[releaseId]/page.tsx @@ -1,13 +1,13 @@ import type { Metadata } from 'next' -import Link from 'next/link' import { notFound } from 'next/navigation' -import { ArrowLeft01Icon, Link01Icon } from 'hugeicons-react' +import { Link01Icon } from 'hugeicons-react' -import { Logo } from '@/components/logo' import { ReleaseAuth } from '@/components/releases/release-auth' import { ReleaseNotes } from '@/components/releases/release-notes' import { ReleaseVote } from '@/components/releases/release-vote' -import { ThemeSwitcher } from '@/components/theme-switcher' +import { Badge, FileCard, PageIntro } from '@/components/review/review-ui' +import { SiteFooter } from '@/components/site/site-footer' +import { SiteHeader } from '@/components/site/site-header' import { getPublicRelease } from '@/lib/public-releases' type Props = { params: Promise<{ slug: string; releaseId: string }> } @@ -53,87 +53,90 @@ export default async function ReleasePage({ params }: Props) { return (
-
- - - - -
-
- -
+
) } diff --git a/src/app/stacks/[slug]/upgrade/loading.tsx b/src/app/stacks/[slug]/upgrade/loading.tsx index e52f070..fbd6485 100644 --- a/src/app/stacks/[slug]/upgrade/loading.tsx +++ b/src/app/stacks/[slug]/upgrade/loading.tsx @@ -1,94 +1,44 @@ -import Link from 'next/link' -import { ArrowLeft01Icon } from 'hugeicons-react' - -import { Logo } from '@/components/logo' -import { ThemeSwitcher } from '@/components/theme-switcher' +import { SiteHeader } from '@/components/site/site-header' import { PulseLoader } from '@/components/ui/pulse-loader' import { Skeleton } from '@/components/ui/skeleton' export default function UpgradePlannerLoading() { return (
-
- - - - -
+ -
-
- # +
+
+ stackpulse + / stacks - / + / - / + / upgrade
- - -
- + +
+ +
-
-
- - - - - - ~/upgrade-planner.sh - no sign-in needed +
+
+ Compare versions
-
- - - - +
+ + +
-
- {['releases in range', 'breaking', 'security', 'deprecations'].map((label) => ( -
- -
- {label} -
-
- ))} -
- -
- {['breaking changes', 'security notes', 'deprecations', 'migration checklist'].map( - (section) => ( -
-
- - § {section.replace(/ /g, '_')} - -
-
-
- - -
-
- ), - )} +
+
diff --git a/src/app/stacks/[slug]/upgrade/page.tsx b/src/app/stacks/[slug]/upgrade/page.tsx index 77eb009..086ad69 100644 --- a/src/app/stacks/[slug]/upgrade/page.tsx +++ b/src/app/stacks/[slug]/upgrade/page.tsx @@ -1,10 +1,19 @@ import type { Metadata } from 'next' import Link from 'next/link' import { notFound } from 'next/navigation' -import { ArrowLeft01Icon } from 'hugeicons-react' +import { GitCompareIcon } from 'hugeicons-react' -import { Logo } from '@/components/logo' -import { ThemeSwitcher } from '@/components/theme-switcher' +import { + CardNote, + FileCard, + MergeBox, + PageIntro, + StatGrid, + button, + cx, +} from '@/components/review/review-ui' +import { SiteFooter } from '@/components/site/site-footer' +import { SiteHeader } from '@/components/site/site-header' import { getUpgradePlan, type UpgradePlanItem } from '@/lib/upgrade-plan' type Props = { @@ -48,159 +57,166 @@ export default async function UpgradePlannerPage({ params, searchParams }: Props if (!plan) notFound() const hasRange = plan.fromIsValid && plan.fromVersion !== null + const latest = plan.latest?.version ?? 'latest' return (
-
- - - - -
+ -
-
- # - stacks/{plan.tech.slug}/upgrade -
+
+ + Enter the version you run today. StackPulse lines up every stored breaking change, + deprecation, security note, and migration step between it and{' '} + {plan.latest ? ( + {plan.latest.version} + ) : ( + 'the latest release' + )} + . + + } + /> -

- upgrade {plan.tech.name.toLowerCase()} - . -

-

- Enter the version you run today. StackPulse lines up every stored breaking change, - deprecation, security note, and migration step between it and{' '} - {plan.latest ? ( - {plan.latest.version} - ) : ( - 'the latest release' - )} - . -

- - {/* Version form */}
-
- - - - +
+ + - ~/upgrade-planner.sh - no sign-in needed + no sign-in needed
-
- $ - upgrade - --from - - --to - {plan.latest?.version ?? 'latest'} -
{plan.fromVersion && !plan.fromIsValid && ( -

- → - could not parse "{plan.fromVersion}" as a version — try something like 14.2.0 +

+ Could not read "{plan.fromVersion}" as a version. Try something like 14.2.0.

)} {hasRange && ( <> - {/* Range summary */} -
- - - - -
+ + Comparing {plan.fromVersion}… + {latest} + + } + meta={`${plan.releasesInRange.length} releases`} + className="mt-10" + > + + {plan.releasesInRange.length === 0 && ( + + {plan.latest + ? `You may already be on the latest version (${plan.latest.version}).` + : 'Nothing to compare yet.'} + + )} + - {plan.releasesInRange.length === 0 ? ( -
- → - no stored releases newer than {plan.fromVersion} - {plan.latest ? ` — you may already be on the latest (${plan.latest.version})` : ''}. -
- ) : ( -
+ {plan.releasesInRange.length > 0 && ( +
- {/* Releases in range */} -
- -
    + +
      {plan.releasesInRange.map((release) => (
    • {release.version} - {release.isPrerelease && [prerelease]} + {release.isPrerelease && [prerelease]} {release.title ?? ''} - + {release.publishedAt ? release.publishedAt.toISOString().slice(0, 10) : ''} @@ -208,89 +224,45 @@ export default async function UpgradePlannerPage({ params, searchParams }: Props
    • ))}
    -
+
)} -

- {'// '}based on {plan.storedCount} stored release +

+ {'// '}Based on {plan.storedCount} stored release {plan.storedCount === 1 ? '' : 's'} for {plan.tech.name} - {plan.oldestStored ? ` (oldest: ${plan.oldestStored.version})` : ''}. releases - published before StackPulse started tracking this stack may be missing — always check - the{' '} + {plan.oldestStored ? ` (oldest: ${plan.oldestStored.version})` : ''}. Releases + published before StackPulse started tracking this stack may be missing, so always + check the{' '} source changelog {' '} before shipping.

- {/* CTA */} -
-

- want project-specific advice? -

-

- ask the ai about this upgrade - . -

-

- Sign in, follow {plan.tech.name}, and ask upgrade questions on any release — with - your project context taken into account. -

- + - $ - ./sign-in --github - +
+ + Sign in with GitHub + +
+
)}
-
- ) -} - -const statTone: Record = { - ink: 'text-ink', - rose: 'text-rose', - amber: 'text-amber', - cyan: 'text-cyan', -} -function PlanStat({ - label, - value, - tone, -}: { - label: string - value: number - tone: 'ink' | 'rose' | 'amber' | 'cyan' -}) { - return ( -
-
0 ? statTone[tone] : 'text-fade'}`}> - {value} -
-
{label}
-
- ) -} - -function SectionHeading({ title, count }: { title: string; count: number }) { - return ( -
-

- § {title.replace(/ /g, '_')} -

-
- {count} +
) } @@ -302,12 +274,14 @@ const itemTone: Record = { } function ItemSection({ + path, title, tone, items, emptyNote, checklist = false, }: { + path: string title: string tone: 'rose' | 'amber' | 'cyan' items: UpgradePlanItem[] @@ -315,28 +289,30 @@ function ItemSection({ checklist?: boolean }) { return ( -
- + {items.length === 0 ? ( -

→ {emptyNote}

+ ) : ( -
    +
      {items.map((entry, index) => (
    • - + - + {entry.item} - {entry.version} + {entry.version}
    • ))}
    )} -
+ ) } diff --git a/src/app/stacks/page.tsx b/src/app/stacks/page.tsx index 3120c95..b8472da 100644 --- a/src/app/stacks/page.tsx +++ b/src/app/stacks/page.tsx @@ -1,10 +1,11 @@ import type { Metadata } from 'next' import Link from 'next/link' -import { ArrowRight01Icon, Link01Icon } from 'hugeicons-react' +import { Link01Icon } from 'hugeicons-react' import { DigestSignupForm } from '@/components/landing/digest-signup-form' -import { Logo } from '@/components/logo' -import { ThemeSwitcher } from '@/components/theme-switcher' +import { FileCard, PageIntro, StatGrid } from '@/components/review/review-ui' +import { SiteFooter } from '@/components/site/site-footer' +import { SiteHeader } from '@/components/site/site-header' import { getPublicStackIndex, type PublicStackIndexItem } from '@/lib/public-stacks' const appUrl = process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000' @@ -61,174 +62,124 @@ export default async function StacksPage() { dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }} /> -
- - - - -
- -
-
- § - stacks - · - {stacks.length} tracked -
- -
-
-

- Framework release notes, breaking changes, and upgrade notes - . -

-

- Browse AI-distilled GitHub release feeds for common developer stacks. Each stack page - collects source-linked release notes, risky changes, deprecations, and migration - signals from the projects developers ship with. -

-
- -
- - - - -
- -
+ + +
+ + + + + +
+ } + />
-
-

- § stack_directory -

-
- github-backed -
- -
+

+ Stack directory + + {stacks.length} + +

+ +
    {stacks.map((stack) => ( - +
  • + +
  • ))} -
+
+ +
) } function StackCard({ stack }: { stack: PublicStackIndexItem }) { return ( -
-
-
-
- {stack.category || 'stack'} -
-

- {stack.name} -

-
- - - +
+
+ stacks/{stack.slug} + {stack.category || 'stack'}
-

- {stack.description || `Track ${stack.name} releases, breaking changes, and upgrade notes.`} -

+
+

+ + {stack.name} + +

-
- - - -
+

+ {stack.description || + `Track ${stack.name} releases, breaking changes, and upgrade notes.`} +

-
- - - repo - ↗ - - - {stack.latestPublishedAt - ? `latest ${formatDate(stack.latestPublishedAt)}` - : 'no releases yet'} - +
+
+
releases
+
{stack.releases}
+
+
+
security
+
0 ? 'tabular-nums text-rose' : 'tabular-nums text-dust'} + > + {stack.security} +
+
+
+
migrations
+
0 ? 'tabular-nums text-cyan' : 'tabular-nums text-dust'} + > + {stack.migrations} +
+
+
+ +
+ + + + {stack.latestPublishedAt + ? `latest ${formatDate(stack.latestPublishedAt)}` + : 'no releases yet'} + +
) } -function Stat({ - label, - value, - tone, -}: { - label: string - value: number - tone: 'ink' | 'lime' | 'rose' | 'cyan' -}) { - const toneClass = - tone === 'lime' - ? 'text-lime' - : tone === 'rose' - ? 'text-rose' - : tone === 'cyan' - ? 'text-cyan' - : 'text-ink' - - return ( -
-
{label}
-
{value}
-
- ) -} - -function MiniStat({ - label, - value, - tone, -}: { - label: string - value: number - tone: 'ink' | 'rose' | 'cyan' -}) { - const toneClass = tone === 'rose' ? 'text-rose' : tone === 'cyan' ? 'text-cyan' : 'text-ink' - - return ( -
-
{label}
-
{value}
-
- ) -} - function formatDate(date: Date) { return new Intl.DateTimeFormat('en-US', { month: 'short', diff --git a/src/app/status/page.tsx b/src/app/status/page.tsx index 28fdc22..0858c24 100644 --- a/src/app/status/page.tsx +++ b/src/app/status/page.tsx @@ -1,10 +1,9 @@ import type { Metadata } from 'next' import { cacheLife } from 'next/cache' -import Link from 'next/link' -import { ArrowLeft01Icon } from 'hugeicons-react' -import { Logo } from '@/components/logo' -import { ThemeSwitcher } from '@/components/theme-switcher' +import { Badge, CardNote, FileCard, PageIntro } from '@/components/review/review-ui' +import { SiteFooter } from '@/components/site/site-footer' +import { SiteHeader } from '@/components/site/site-header' import { getRecentFetchRuns, type FetchRunRow } from '@/lib/release-ingestion' export const metadata: Metadata = { @@ -15,15 +14,18 @@ export const metadata: Metadata = { } const statusTone: Record = { - completed: 'border-emerald/30 bg-emerald/10 text-emerald', - completed_with_errors: 'border-amber/30 bg-amber/10 text-amber', - failed: 'border-rose/30 bg-rose/10 text-rose', - running: 'border-cyan/30 bg-cyan/10 text-cyan', + completed: 'feature', + completed_with_errors: 'deprecation', + failed: 'breaking', + running: 'migration', } function formatDuration(run: FetchRunRow) { if (!run.finishedAt) return '—' - const seconds = Math.max(0, Math.round((run.finishedAt.getTime() - run.startedAt.getTime()) / 1000)) + const seconds = Math.max( + 0, + Math.round((run.finishedAt.getTime() - run.startedAt.getTime()) / 1000), + ) return seconds < 60 ? `${seconds}s` : `${Math.floor(seconds / 60)}m ${seconds % 60}s` } @@ -49,128 +51,120 @@ export default async function StatusPage() { return (
-
- - - - -
- -
-
- # - status -
+ -

- ingestion status. -

-

- Every release fetch run, in the open: stacks scanned, releases inserted, and errors. The - cron runs twice daily; custom repos also fetch the moment they are added. -

+
+ -
-
-
+
+
+
last fetch run
+
{lastRun ? formatRelative(lastRun.startedAt) : 'no runs yet'} -
-
- last fetch run -
+
-
-
- {finished.length > 0 ? `${cleanRuns}/${finished.length}` : '—'} -
-
+
+
clean runs (last {finished.length || '0'}) -
-
-
-
{insertedTotal}
-
- releases inserted (shown runs) -
+ +
+ {finished.length > 0 ? `${cleanRuns}/${finished.length}` : '—'} +
-
- -
-
- - - - - - ~/var/log/fetch-runs.log - last {runs.length} runs +
+
releases inserted (shown runs)
+
+ {insertedTotal} +
+ + {runs.length === 0 ? ( -
- → no fetch runs recorded yet. -
+ + The first run appears here after the next scheduled fetch at 00:00 or 12:00 UTC. + ) : (
- +
+ - - - - - - - - + + + + + + + + {runs.map((run) => ( - - + - + + - - - + ))}
Recent release fetch runs
startedtriggerstatusscannedinsertederrorsduration
+ started + + trigger + + status + + scanned + + inserted + + errors + + duration +
+
{run.startedAt.toISOString().slice(0, 16).replace('T', ' ')} utc {run.trigger} - + + {run.status.replace(/_/g, ' ')} - + + + {run.technologiesScanned} + + {run.releasesInserted} {run.technologiesScanned}{run.releasesInserted} 0 ? 'text-rose' : 'text-fade'}`} + className={`px-4 py-2.5 text-right tabular-nums ${run.errors > 0 ? 'text-rose' : 'text-dust'}`} > {run.errors} {formatDuration(run)}{formatDuration(run)}
)} -
+ -

- {'// '}this page revalidates every 5 minutes. errors here mean a stack's GitHub fetch +

+ {'// '}This page revalidates every 5 minutes. Errors here mean a stack's GitHub fetch or AI summarisation failed for that run. Later runs check the latest five releases; older gaps may need recovery.

+ +
) } diff --git a/src/app/terms/page.tsx b/src/app/terms/page.tsx index 8e43ee3..f3c83fc 100644 --- a/src/app/terms/page.tsx +++ b/src/app/terms/page.tsx @@ -8,7 +8,7 @@ export const metadata: Metadata = { export default function TermsPage() { return ( - +

StackPulse is a non-profit, open-source side project. It's offered to the developer community for free, with no warranty. By using it you agree to a few common-sense things diff --git a/src/components/landing/digest-signup-form.tsx b/src/components/landing/digest-signup-form.tsx index b5e887c..c52f380 100644 --- a/src/components/landing/digest-signup-form.tsx +++ b/src/components/landing/digest-signup-form.tsx @@ -1,16 +1,22 @@ 'use client' -import { useState, useTransition } from 'react' +import { useId, useState, useTransition } from 'react' +import { PulseLoader } from '@/components/ui/pulse-loader' import { subscribeToDigest } from '@/lib/actions' export function DigestSignupForm({ stackSlug, source = 'public', + framed = true, }: { stackSlug?: string source?: string + /** Draw its own card; turn off when the form already sits inside one. */ + framed?: boolean }) { + const emailId = useId() + const messageId = useId() const [email, setEmail] = useState('') const [website, setWebsite] = useState('') const [message, setMessage] = useState(null) @@ -19,7 +25,7 @@ export function DigestSignupForm({ return (

{ event.preventDefault() setMessage(null) @@ -35,8 +41,8 @@ export function DigestSignupForm({ }) }} > -