React Components

Build a changelog with the React components

Aktualisiert am 32 Aufrufe

A changelog usually lives in three places: a page listing every release, a page per entry, and a small "What's new" badge in your app's header. @kookee/react has a component for each. The examples use the Next.js App Router, with the SDK client and its config from lib/kookee.ts; see Using the React components with Next.js, TanStack Start or Vite for that file and for other setups.

The list page, with a type filter

tsx
// app/changelog/page.tsx
import type { ChangelogType } from '@kookee/sdk';
import { ChangelogList, ChangelogTypeFilter } from '@kookee/react/changelog';
import { kookee } from '@/lib/kookee';

const TYPES: ChangelogType[] = ['feature', 'fix', 'improvement', 'breaking', 'security', 'deprecated', 'other'];

export default async function ChangelogPage({ searchParams }: { searchParams: Promise<{ type?: string }> }) {
  const { type } = await searchParams;
  const activeType = TYPES.find((t) => t === type);

  const { data } = await kookee.changelog.list({
    limit: 20,
    filter: activeType ? { changelogType: activeType } : undefined,
  });

  return (
    <>
      <ChangelogTypeFilter activeType={activeType} href="/changelog?type={type}" allHref="/changelog" />
      <ChangelogList entries={data} href="/changelog/{slug}" empty={<p>No updates of this type yet.</p>} />
    </>
  );
}

Each item shows the publication date, the version, a badge for the release type, the title, the summary and the cover image; the version and cover only when the entry has them. The type and version come from the entry's changelogType and version fields, so there is nothing to map by hand. An entry without a type gets the "Update" badge, and so does an unknown type.

ChangelogTypeFilter is a row of links, one per release type plus "All". Pass types to show only some of them, for example types={['feature', 'fix', 'breaking']}. Because the filter is links, a filtered changelog has its own address, and /changelog?type=breaking is a page you can send to the engineers who integrate with you.

ChangelogListSkeleton renders the same layout with placeholders while entries load.

The entry page

tsx
// app/changelog/[slug]/page.tsx
import { ChangelogArticle } from '@kookee/react/changelog';
import { Reactions } from '@kookee/react/entry/client';
import { kookee, kookeeConfig } from '@/lib/kookee';

export default async function EntryPage({ params }: { params: Promise<{ slug: string }> }) {
  const entry = await kookee.changelog.getBySlug((await params).slug);

  return (
    <>
      <ChangelogArticle entry={entry} classNames={{ content: 'prose' }} />
      <Reactions entryId={entry.id} reactions={entry.reactions} config={kookeeConfig} />
    </>
  );
}

ChangelogArticle renders the date, version, type badge, title and cover, then the body. From version 1.9.3, showAuthor adds the author's avatar and name to the meta row, and headerClassNames passes classes to the header's inner elements:

tsx
<ChangelogArticle
  entry={entry}
  showAuthor
  classNames={{ content: 'prose' }}
  headerClassNames={{ title: 'text-4xl', authorAvatar: 'hidden' }}
/>

The header's author slots are author, authorAvatar and authorName, and an author without a name renders nothing. ChangelogAuthor is the same part on its own, for a header you compose yourself.

Add TrackedTableOfContents for long entries, as described in Build a blog with the React components; handle a missing slug with notFound() the same way.

Your own row layout

The default item is built from exported parts, and renderItem lets you arrange them differently. A compact one-line row:

tsx
import { ChangelogList, ChangelogTypeBadge, ChangelogTitle, ChangelogDate } from '@kookee/react/changelog';

<ChangelogList
  entries={data}
  renderItem={(entry) => (
    <div className="flex items-baseline gap-3">
      <ChangelogTypeBadge entry={entry} />
      <ChangelogTitle entry={entry} href="/changelog/{slug}" headingLevel={3} />
      <ChangelogDate entry={entry} />
    </div>
  )}
/>;

The list still wraps each row in its own <li>. The other parts are ChangelogVersion, ChangelogSummary, ChangelogCover and ChangelogAuthor. Title parts link only the title unless you pass stretched, which makes the nearest positioned ancestor clickable as a whole.

A "What's new" badge

WhatsNew shows how many changelog entries were published since the visitor last opened it, and lists them in a popover:

tsx
// in your header, a Server Component
import { WhatsNew } from '@kookee/react/changelog/client';

const { data: recent } = await kookee.changelog.list({ limit: 5 });

<WhatsNew entries={recent} href="/changelog/{slug}" allHref="/changelog" />;
  • The last-seen date is kept in the visitor's localStorage, so the count appears after hydration and never in the server's HTML.

  • On a first visit every entry you pass counts as new. Pass the latest few, not your whole history.

  • Opening the popover marks everything as seen, and every WhatsNew on the page updates at once.

  • open, defaultOpen and onOpenChange control the popover; Escape closes it.

  • storageKey (default kookee:whats-new:seen) separates the counts when one domain shows more than one project's changelog.

  • children replaces the default "What's new" label of the button, for example with an icon.

To build your own badge, useWhatsNew returns the same logic without the markup:

tsx
'use client';

import { useWhatsNew } from '@kookee/react/changelog/client';

const { unseenCount, isUnseen, markSeen } = useWhatsNew({ entries: recent });

unseenCount is null until the page has hydrated, and stays null when the browser does not allow storage, so hide the badge while it is null.

Labels and colours

Type names, the empty state and the what's-new texts come from ChangelogLabels. Pass labels to rename or translate them, for example labels={{ feature: 'New', versionPrefix: 'v' }}. Each type badge has its own colour variable, --kookee-type-feature through --kookee-type-other, and carries data-type for your own CSS. See Style and customize the React components.

War dieser Artikel hilfreich?