Files
periscope/frontend/src/components/legal/changelog-timeline.tsx
T
micheleandCursor 8d2b85600f Rebrand Pinscope to Periscope across product and codebase.
Rename the core package to periscopex, update UI/docs/Docker/deploy defaults to periscope.michelebigi.it, and keep legacy version/storage key aliases so existing projects keep working.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-13 20:02:04 +02:00

179 lines
6.0 KiB
TypeScript

import { Cpu, ArrowLeft } from "lucide-react";
import Link from "next/link";
type Tag = "New" | "Improved" | "Fixed";
interface ChangelogEntry {
version: string;
date: string;
title?: string;
description?: string;
items: { tag: Tag; text: string }[];
}
function parseChangelog(md: string): ChangelogEntry[] {
const lines = md.split("\n");
const entries: ChangelogEntry[] = [];
let current: ChangelogEntry | null = null;
let descLines: string[] = [];
const finalize = () => {
if (!current) return;
const desc = descLines.join(" ").trim();
if (desc) current.description = desc;
entries.push(current);
current = null;
descLines = [];
};
for (const raw of lines) {
const line = raw.trimEnd();
if (line.startsWith("## ")) {
finalize();
const parts = line.slice(3).split(" — ");
current = {
version: parts[0]?.trim() ?? "",
date: parts[1]?.trim() ?? "",
title: parts[2]?.trim() || undefined,
items: [],
};
} else if (line.startsWith("- ") && current) {
const text = line.slice(2);
const m = text.match(/^\[(New|Improved|Fixed)\]\s*(.+)$/);
if (m) {
current.items.push({ tag: m[1] as Tag, text: m[2] });
} else {
current.items.push({ tag: "Improved", text });
}
} else if (current && current.items.length === 0 && line.trim() && !line.startsWith("#")) {
descLines.push(line.trim());
}
}
finalize();
return entries;
}
function formatDate(iso: string): string {
const d = new Date(iso + "T00:00:00Z");
if (Number.isNaN(d.getTime())) return iso;
return d.toLocaleDateString("en-US", {
year: "numeric",
month: "long",
day: "numeric",
timeZone: "UTC",
});
}
const TAG_STYLES: Record<Tag, string> = {
New: "border-emerald-500/40 bg-emerald-500/10 text-emerald-600 dark:text-emerald-400",
Improved: "border-blue-500/40 bg-blue-500/10 text-blue-600 dark:text-blue-400",
Fixed: "border-orange-500/40 bg-orange-500/10 text-orange-600 dark:text-orange-400",
};
function TagPill({ tag }: { tag: Tag }) {
return (
<span
className={`inline-flex h-5 w-[68px] flex-shrink-0 items-center justify-center rounded-full border px-2 text-[11px] font-medium ${TAG_STYLES[tag]}`}
>
{tag}
</span>
);
}
export function ChangelogTimeline({ content }: { content: string }) {
const entries = parseChangelog(content);
return (
<div className="min-h-screen bg-background text-foreground">
<header className="border-b border-border/50">
<div className="mx-auto max-w-3xl px-6 py-6 flex items-center justify-between">
<Link
href="/"
className="flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground transition-colors"
>
<ArrowLeft className="h-4 w-4" />
Back to home
</Link>
<Link href="/" className="flex items-center gap-2">
<Cpu className="h-4 w-4 text-blue-500" />
<span className="text-sm font-medium">Periscope</span>
</Link>
</div>
</header>
<main className="mx-auto max-w-3xl px-6 py-12">
<div className="mb-12">
<h1 className="text-3xl font-bold tracking-tight mb-2">Changelog</h1>
<p className="text-sm text-muted-foreground">What's new in Periscope.</p>
</div>
<ol className="relative">
{entries.map((entry, i) => {
const isLast = i === entries.length - 1;
return (
<li key={entry.version} className="relative pl-10 pb-12 last:pb-0">
<span
aria-hidden
className="absolute left-[5px] top-3 h-3 w-3 rounded-full border-2 border-border bg-background"
/>
{!isLast && (
<span
aria-hidden
className="absolute left-[10px] top-7 bottom-0 w-px bg-border/60"
/>
)}
<div className="flex items-center gap-2 text-sm text-muted-foreground mb-2">
<span>{formatDate(entry.date)}</span>
{entry.version && (
<>
<span aria-hidden className="text-muted-foreground/50">·</span>
<span className="font-mono text-xs">v{entry.version}</span>
</>
)}
</div>
{entry.title && (
<h2 className="text-2xl font-bold tracking-tight mb-2">
{entry.title}
</h2>
)}
{entry.description && (
<p className="text-sm text-muted-foreground leading-relaxed mb-5">
{entry.description}
</p>
)}
<ul className="space-y-2.5">
{entry.items.map((item, j) => (
<li key={j} className="flex items-start gap-3 text-sm">
<TagPill tag={item.tag} />
<span className="text-muted-foreground leading-relaxed pt-px">
{item.text}
</span>
</li>
))}
</ul>
</li>
);
})}
</ol>
</main>
<footer className="border-t border-border/50">
<div className="mx-auto max-w-3xl px-6 py-8 flex items-center justify-between text-xs text-muted-foreground">
<div className="flex items-center gap-4">
<Link href="/changelog" className="hover:text-foreground transition-colors">
Changelog
</Link>
<Link href="/privacy" className="hover:text-foreground transition-colors">
Privacy Policy
</Link>
<Link href="/terms" className="hover:text-foreground transition-colors">
Terms of Service
</Link>
</div>
<span>&copy; {new Date().getFullYear()} Faradworks</span>
</div>
</footer>
</div>
);
}