From 53875974dab58bdf345a1ab26b3575bd24dab4fe Mon Sep 17 00:00:00 2001 From: harsha-vardhan-burra Date: Fri, 25 Sep 2026 17:51:31 +0530 Subject: [PATCH 1/4] feat(changelog): refine project updates experience Signed-off-by: harsha-vardhan-burra --- src/app/changelog/[slug]/page.tsx | 119 ++- src/app/changelog/page.tsx | 844 ++++++++++-------- .../changelog/CurrentReleaseHero.tsx | 148 +++ .../changelog/MilestonesSection.tsx | 108 +++ .../changelog/RecentUpdatesGrid.tsx | 116 +++ src/data/normalized-releases.json | 6 +- src/data/raw-releases.json | 12 +- src/lib/changelog.ts | 155 ++++ tests/functional/changelog-search.spec.ts | 18 + tests/smoke/changelog.spec.ts | 89 +- 10 files changed, 1233 insertions(+), 382 deletions(-) create mode 100644 src/components/changelog/CurrentReleaseHero.tsx create mode 100644 src/components/changelog/MilestonesSection.tsx create mode 100644 src/components/changelog/RecentUpdatesGrid.tsx diff --git a/src/app/changelog/[slug]/page.tsx b/src/app/changelog/[slug]/page.tsx index f0c0335..b114584 100644 --- a/src/app/changelog/[slug]/page.tsx +++ b/src/app/changelog/[slug]/page.tsx @@ -10,16 +10,26 @@ import { GitCommit, CheckCircle2, AlertTriangle, + Download, + BookOpen, + ShieldAlert, } from "lucide-react"; import { CHANGELOG_ENTRIES, getChangelogEntryBySlug, getAdjacentEntries, + getLatestProductionRelease, isDeprecatedEntry, } from "@/lib/changelog"; -import { SITE_META } from "@/lib/constants"; +import { + SITE_META, + DOCS_RELEASE_NOTES_URL, + USER_GUIDE_URL, + GITHUB_REPO_URL, +} from "@/lib/constants"; import { ReleaseVisual } from "@/components/visuals/ReleaseVisual"; import { ScrollReveal } from "@/components/motion/ScrollReveal"; +import { formatDate } from "@passcodes/passalgo"; import { cn } from "@/lib/utils"; import type { Metadata } from "next"; @@ -61,6 +71,8 @@ export default function ChangelogEntryPage({ const { prev, next } = getAdjacentEntries(params.slug); const isDeprecated = isDeprecatedEntry(entry); + const latestProd = getLatestProductionRelease(); + const isCurrentProduction = latestProd?.slug === entry.slug; return (
@@ -71,7 +83,7 @@ export default function ChangelogEntryPage({ href="/changelog" className="inline-flex items-center gap-2 text-xs font-medium text-[var(--text-muted)] transition-colors hover:text-[var(--text)]" > - +
@@ -104,17 +116,17 @@ export default function ChangelogEntryPage({ )} + {isCurrentProduction && ( + + + Current Production + + )} + - + @@ -126,7 +138,7 @@ export default function ChangelogEntryPage({ : "text-[var(--accent-light)]" )} > - + @@ -149,6 +161,15 @@ export default function ChangelogEntryPage({ {/* Action Links */}
+ {!isDeprecated && !entry.isYanked && !entry.isMilestone && ( + +
@@ -187,6 +208,15 @@ export default function ChangelogEntryPage({ {entry.deprecatedReason || "This release is no longer supported. Superseded by the newer database architecture introduced in later releases. Please use a current release instead."}

+
+ + View current production downloads +
)} @@ -194,13 +224,22 @@ export default function ChangelogEntryPage({ {entry.isYanked && (
- +

{entry.yankedReason || "This release has been explicitly yanked from active distribution. It is preserved here strictly for documentation and project history."}

+
+ + View current production downloads +
)} @@ -208,7 +247,7 @@ export default function ChangelogEntryPage({ {entry.internalDetails && (

- Build & Runtime Specifications + Build & Runtime Specifications

{entry.internalDetails.packageName && ( @@ -252,7 +291,6 @@ export default function ChangelogEntryPage({ )} - {/* 2. Article Content (Highlights + Release Sections) */}
@@ -260,7 +298,7 @@ export default function ChangelogEntryPage({ {entry.highlights && entry.highlights.length > 0 && (

- +

    @@ -304,6 +342,43 @@ export default function ChangelogEntryPage({ ))}
)} + + {/* Documentation & Resources Box */} +
+
+
+
+ + Official Docs Release Notes +
+
@@ -319,7 +394,7 @@ export default function ChangelogEntryPage({ className="subtle-card group flex flex-col gap-1 text-left" > - + @@ -337,7 +412,7 @@ export default function ChangelogEntryPage({ > Next Release - + {next.version} — {next.title} diff --git a/src/app/changelog/page.tsx b/src/app/changelog/page.tsx index cc38ac0..1d45957 100644 --- a/src/app/changelog/page.tsx +++ b/src/app/changelog/page.tsx @@ -10,70 +10,104 @@ import { Calendar, GitCommit, AlertTriangle, + Layers, + Filter, } from "lucide-react"; import { CHANGELOG_ENTRIES, CHANGELOG_CATEGORIES, + RELEASE_CHANNEL_FILTERS, isDeprecatedEntry, + getLatestProductionRelease, + getRecentUpdates, + getProjectMilestones, + matchesReleaseSearch, type CategoryFilter, + type ReleaseChannelFilter, } from "@/lib/changelog"; import { GITHUB_RELEASES_URL, DOCS_RELEASE_NOTES_URL } from "@/lib/constants"; import { ReleaseVisual } from "@/components/visuals/ReleaseVisual"; import { ScrollReveal } from "@/components/motion/ScrollReveal"; +import { CurrentReleaseHero } from "@/components/changelog/CurrentReleaseHero"; +import { RecentUpdatesGrid } from "@/components/changelog/RecentUpdatesGrid"; +import { MilestonesSection } from "@/components/changelog/MilestonesSection"; +import { formatDate } from "@passcodes/passalgo"; import { cn } from "@/lib/utils"; export default function ChangelogPage() { const [search, setSearch] = useState(""); const [activeCategory, setActiveCategory] = useState("All"); + const [activeChannel, setActiveChannel] = + useState("All"); + + const latestProduction = useMemo(() => getLatestProductionRelease(), []); + const recentUpdates = useMemo(() => getRecentUpdates(3), []); + const projectMilestones = useMemo(() => getProjectMilestones(), []); const filteredEntries = useMemo(() => { return CHANGELOG_ENTRIES.filter((entry) => { + // Category filter const matchesCategory = activeCategory === "All" || entry.category === activeCategory; - const searchLower = search.trim().toLowerCase(); - if (!searchLower) return matchesCategory; - - const matchesTitle = entry.title - .toLowerCase() - .includes(searchLower); - const matchesSummary = entry.summary - .toLowerCase() - .includes(searchLower); - const matchesVersion = entry.version - .toLowerCase() - .includes(searchLower); - const matchesTldr = entry.tldr?.toLowerCase().includes(searchLower); - const matchesHighlights = entry.highlights?.some((h) => - h.toLowerCase().includes(searchLower) - ); - const matchesSections = entry.sections?.some((s) => - s.items.some((item) => item.toLowerCase().includes(searchLower)) - ); - - return ( - matchesCategory && - (matchesTitle || - matchesSummary || - matchesVersion || - matchesTldr || - matchesHighlights || - matchesSections) - ); + if (!matchesCategory) return false; + + // Channel filter + if (activeChannel !== "All") { + const isDep = isDeprecatedEntry(entry); + const isYank = !!entry.isYanked; + const isMs = !!entry.isMilestone; + + if (activeChannel === "Milestones" && !isMs) return false; + if (activeChannel === "Deprecated" && !isDep) return false; + if (activeChannel === "Yanked" && !isYank) return false; + if ( + activeChannel === "Stable" && + (entry.releaseType !== "Stable" || isDep || isYank || isMs) + ) { + return false; + } + if ( + activeChannel === "Beta" && + (entry.releaseType !== "Beta" || isYank || isMs) + ) { + return false; + } + if ( + activeChannel === "Alpha" && + (entry.releaseType !== "Alpha" || isYank || isMs) + ) { + return false; + } + } + + // Search query across version, title, summary, highlights, technical details, release states + return matchesReleaseSearch(entry, search); }); - }, [search, activeCategory]); + }, [search, activeCategory, activeChannel]); + + const isFilterActive = + search.trim().length > 0 || + activeCategory !== "All" || + activeChannel !== "All"; + + const handleResetFilters = () => { + setSearch(""); + setActiveCategory("All"); + setActiveChannel("All"); + }; return ( -
+
{/* 1. Editorial Header */} -
+
Release Highlights

- Release Highlights & Milestones + Release Highlights & Milestones

Curated milestones, architecture transitions, and @@ -82,16 +116,16 @@ export default function ChangelogPage() { documentation and GitHub releases.

-
+
- +
+ + {/* Quick navigation anchor bar */} +
- {/* 2. Filter and Search Controls */} - -
-
- {CHANGELOG_CATEGORIES.map((cat) => { - const isSelected = activeCategory === cat; - return ( - - ); - })} + {/* 2. Featured Current Production Release */} + {latestProduction && ( +
+ + + +
+ )} + + {/* 3. Recent Updates */} + {recentUpdates.length > 0 && ( +
+ + + +
+ )} + + {/* 4. Major Architectural Milestones */} + {projectMilestones.length > 0 && ( +
+ + + +
+ )} + + {/* 5. Historical Release Archive & Timeline */} +
+ +
+ + Release Archive + +

+ Full Release History & Timeline +

+

+ Explore the complete chronological stream of production builds, preview releases, and archived tags. +

-
- - setSearch(e.target.value)} - className="w-full rounded-lg border border-[var(--border-light)] bg-[var(--card-bg)] py-1.5 pl-9 pr-8 text-sm text-[var(--text)] placeholder-[var(--text-dim)] outline-none transition-colors focus:border-[var(--accent-light)]" - aria-label="Search changelog entries" - /> - {search && ( - + )} +
+ +
+ + Showing {filteredEntries.length} of {CHANGELOG_ENTRIES.length} entries + + {isFilterActive && ( + + )} +
+
+ + {/* Middle row: Category Filters */} +
+
+ Categories: +
+
- - - )} + {CHANGELOG_CATEGORIES.map((cat) => { + const isSelected = activeCategory === cat; + return ( + + ); + })} +
+
+ + {/* Bottom row: Release Channel / State Filters */} +
+
+ Release State & Channel: +
+
+ {RELEASE_CHANNEL_FILTERS.map((channel) => { + const isSelected = activeChannel === channel; + return ( + + ); + })} +
+
-
-
+ - {/* 3. Release Timeline Stream */} - {filteredEntries.length === 0 ? ( -
-

- No updates match your filter -

-

- Try clearing your search query or selecting - "All" categories. -

- -
- ) : ( -
- {filteredEntries.map((entry, index) => { - const isLatest = - index === 0 && activeCategory === "All" && !search; + {/* 6. Release Timeline Stream */} + {filteredEntries.length === 0 ? ( +
+

+ No updates match your filter +

+

+ Try clearing your search query or selecting "All" categories. +

+ +
+ ) : ( +
+ {filteredEntries.map((entry, index) => { + const isLatest = + index === 0 && + activeCategory === "All" && + activeChannel === "All" && + !search; + + if (entry.isMilestone) { + return ( + +
+
+ + Architecture Milestone + + + {formatDate(entry.date)} + +
+ +

+ + {entry.title} + +

+ +

+ {entry.summary} +

+ + {entry.highlights && + entry.highlights.length > 0 && ( +
+ {entry.highlights.map( + (h, hIdx) => ( +
+ + + {h} + +
+ ) + )} +
+ )} + +
+ + + Read architectural overview + +
+
+
+ ); + } + + const isDeprecated = isDeprecatedEntry(entry); - if (entry.isMilestone) { return ( -
-
- - Architecture Milestone - - - May 2026 - -
- -

- - {entry.title} - -

- -

- {entry.summary} -

- - {entry.highlights && - entry.highlights.length > 0 && ( -
- {entry.highlights.map( - (h, hIdx) => ( -
- - - {h} - -
- ) +
+ {/* Left Column: Metadata & Category Node */} +
+
+
+ {isDeprecated && ( + + Deprecated + + )} + {entry.isYanked ? ( + + Yanked + + ) : ( + + {entry.releaseType} + + )} + {isLatest && ( + + Latest + + )} + {entry.isMajor && !isLatest && ( + + Major + )}
- )} -
- - - Read architectural overview - - - -
-
- - ); - } - - const isDeprecated = isDeprecatedEntry(entry); - - return ( - -
- {/* Left Column: Metadata & Category Node */} -
-
-
- {isDeprecated && ( - - Deprecated - - )} - {entry.isYanked ? ( - - Yanked - - ) : ( +
- {entry.releaseType} + {entry.version} - )} - {isLatest && ( - - Latest +
+ +
+
+ +
+ + + + {entry.category} + +
+ + {entry.internalDetails && ( +
+ {entry.internalDetails.expoSdk && ( +
Expo SDK {entry.internalDetails.expoSdk}
+ )} + {entry.internalDetails.minAndroid && ( +
Android {entry.internalDetails.minAndroid.split(" ")[0]}+
+ )} +
)} - {entry.isMajor && !isLatest && ( - - Major - + + {entry.githubUrl && ( +
+ + + GitHub Release + +
)}
+
-
- +

+ - {entry.version} - -

- -
- - -
- -
- - - - {entry.category} - - -
+ {entry.title} + + - {entry.internalDetails && ( -
- {entry.internalDetails.expoSdk && ( - Expo SDK {entry.internalDetails.expoSdk} - )} - {entry.internalDetails.minAndroid && ( - Android {entry.internalDetails.minAndroid.split(" ")[0]}+ - )} + {isDeprecated && ( +
+
+
+

+ {entry.deprecatedReason || + "This release is no longer supported. Superseded by the newer database architecture introduced in later releases. Please use a current release instead."} +

)} - {entry.githubUrl && ( -
- - - GitHub Release - - - + {entry.isYanked && entry.yankedReason && ( +
+ Release Notice: + {entry.yankedReason}
)} -
-
- - {/* Right Column: Editorial Summary & Highlights */} -
-

- - {entry.title} - -

- - {isDeprecated && ( -
-
-
-

- {entry.deprecatedReason || - "This release is no longer supported. Superseded by the newer database architecture introduced in later releases. Please use a current release instead."} -

-
- )} - {entry.isYanked && entry.yankedReason && ( -
- Release Notice: - {entry.yankedReason} -
- )} +

+ {entry.summary} +

-

- {entry.summary} -

- - {/* Highlights Bullet List */} - {entry.highlights && - entry.highlights.length > 0 && ( -
- {entry.highlights.map( - (highlight, hIdx) => ( -
- - - {highlight} - -
- ) - )} -
- )} + {/* Highlights Bullet List */} + {entry.highlights && + entry.highlights.length > 0 && ( +
+ {entry.highlights.map( + (highlight, hIdx) => ( +
+ + + {highlight} + +
+ ) + )} +
+ )} - {/* Link to detail page */} -
- - - Read full release notes - - - + {/* Link to detail page */} +
+ + + Read full release notes + +
-
-
-
- ); - })} -
- )} +
+ + ); + })} +
+ )} +
); } diff --git a/src/components/changelog/CurrentReleaseHero.tsx b/src/components/changelog/CurrentReleaseHero.tsx new file mode 100644 index 0000000..1cb6dfe --- /dev/null +++ b/src/components/changelog/CurrentReleaseHero.tsx @@ -0,0 +1,148 @@ +import Link from "next/link"; +import { + Download, + ArrowRight, + ExternalLink, + Calendar, + CheckCircle2, +} from "lucide-react"; +import type { ChangelogEntry } from "@/lib/changelog"; +import { formatDate } from "@passcodes/passalgo"; + +interface CurrentReleaseHeroProps { + release: ChangelogEntry; +} + +export function CurrentReleaseHero({ release }: CurrentReleaseHeroProps) { + const highlights = release.highlights?.slice(0, 4) || []; + + return ( +
+ {/* Ambient subtle glow */} +
+ ); +} diff --git a/src/components/changelog/MilestonesSection.tsx b/src/components/changelog/MilestonesSection.tsx new file mode 100644 index 0000000..1b2fded --- /dev/null +++ b/src/components/changelog/MilestonesSection.tsx @@ -0,0 +1,108 @@ +import Link from "next/link"; +import { ArrowRight, Layers, Milestone, GitCommit } from "lucide-react"; +import type { ChangelogEntry } from "@/lib/changelog"; +import { formatDate } from "@passcodes/passalgo"; +import { isDeprecatedEntry } from "@/lib/changelog"; + +interface MilestonesSectionProps { + milestones: ChangelogEntry[]; +} + +export function MilestonesSection({ milestones }: MilestonesSectionProps) { + if (milestones.length === 0) return null; + + return ( +
+
+ + Architecture & History + +

+ Major Architectural Milestones +

+

+ Documented architectural shifts and foundational transitions across Passcodes' open-source history. +

+
+ +
+ {milestones.map((milestone) => { + const isExplicitMilestone = milestone.isMilestone; + const isDeprecated = isDeprecatedEntry(milestone); + + return ( +
+
+
+ {isExplicitMilestone ? ( + + Architecture Shift + + ) : ( + + Milestone + + )} + + {isDeprecated && ( + + Deprecated + + )} + + + {milestone.version} + + + + · {formatDate(milestone.date)} + +
+ +

+ + {milestone.title} + +

+ +

+ {milestone.summary} +

+ + {milestone.highlights && milestone.highlights.length > 0 && ( +
    + {milestone.highlights.slice(0, 2).map((item, idx) => ( +
  • + + {item} +
  • + ))} +
+ )} +
+ +
+ + Read details +
+
+ ); + })} +
+
+ ); +} diff --git a/src/components/changelog/RecentUpdatesGrid.tsx b/src/components/changelog/RecentUpdatesGrid.tsx new file mode 100644 index 0000000..dce540f --- /dev/null +++ b/src/components/changelog/RecentUpdatesGrid.tsx @@ -0,0 +1,116 @@ +import Link from "next/link"; +import { ArrowRight, Calendar } from "lucide-react"; +import type { ChangelogEntry } from "@/lib/changelog"; +import { ReleaseVisual } from "@/components/visuals/ReleaseVisual"; +import { formatDate } from "@passcodes/passalgo"; +import { cn } from "@/lib/utils"; + +interface RecentUpdatesGridProps { + entries: ChangelogEntry[]; +} + +export function RecentUpdatesGrid({ entries }: RecentUpdatesGridProps) { + if (entries.length === 0) return null; + + return ( +
+
+ + What's New + +

+ Recent Updates +

+

+ The latest stable releases, iterative improvements, and maintenance builds. +

+
+ +
+ {entries.map((entry) => ( +
+
+ {/* Card Top: Channel, Date, Category */} +
+
+ + {entry.releaseType} + + + {entry.version} + +
+ +
+ +
+
+ + {/* Date */} +
+
+ + {/* Title */} +

+ + {entry.title} + +

+ + {/* Summary */} +

+ {entry.summary} +

+ + {/* Highlights Preview */} + {entry.highlights && entry.highlights.length > 0 && ( +
    + {entry.highlights.slice(0, 2).map((h, i) => ( +
  • + + {h} +
  • + ))} +
+ )} +
+ + {/* Card Footer Link */} +
+ + Read release notes +
+
+ ))} +
+
+ ); +} diff --git a/src/data/normalized-releases.json b/src/data/normalized-releases.json index 3a8a49f..b09e12c 100644 --- a/src/data/normalized-releases.json +++ b/src/data/normalized-releases.json @@ -50,7 +50,7 @@ "isYanked": false, "isDeprecated": false, "assetsCount": 5, - "downloadCount": 31 + "downloadCount": 43 }, { "slug": "v3-2-0-stable", @@ -266,7 +266,7 @@ "isDeprecated": true, "deprecatedReason": "Superseded by the newer database architecture introduced in later releases.", "assetsCount": 6, - "downloadCount": 37 + "downloadCount": 38 }, { "slug": "v2-1-0-beta", @@ -573,7 +573,7 @@ "isDeprecated": true, "deprecatedReason": "Superseded by the newer database architecture introduced in later releases.", "assetsCount": 6, - "downloadCount": 56 + "downloadCount": 57 }, { "slug": "v1-1-0-alpha", diff --git a/src/data/raw-releases.json b/src/data/raw-releases.json index c590da0..18d1c34 100644 --- a/src/data/raw-releases.json +++ b/src/data/raw-releases.json @@ -68,7 +68,7 @@ "state": "uploaded", "size": 35922426, "digest": "sha256:b8d20f36f9b87dcc31e5ca10ab4b0d1db7f485cf4907bfc39f53e811daa997b8", - "download_count": 1, + "download_count": 3, "created_at": "2026-08-02T14:54:35Z", "updated_at": "2026-08-02T15:02:05Z", "browser_download_url": "https://github.com/PasscodesApp/Passcodes/releases/download/v3.2.1/v3.2.1-passcodes-x86_64-app.apk" @@ -104,7 +104,7 @@ "state": "uploaded", "size": 36455085, "digest": "sha256:75efa1becac99053b7e144f9d4a83ca32941ee2f90275ad2707dadee6e44c453", - "download_count": 0, + "download_count": 2, "created_at": "2026-08-02T14:56:24Z", "updated_at": "2026-08-02T15:02:05Z", "browser_download_url": "https://github.com/PasscodesApp/Passcodes/releases/download/v3.2.1/v3.2.1-passcodes-x86-app.apk" @@ -176,7 +176,7 @@ "state": "uploaded", "size": 28447625, "digest": "sha256:110e35124036cd19002f3d838f0b559e3840c4b5fd8ec4a087e9fd1862174411", - "download_count": 1, + "download_count": 4, "created_at": "2026-08-02T15:00:28Z", "updated_at": "2026-08-02T15:02:05Z", "browser_download_url": "https://github.com/PasscodesApp/Passcodes/releases/download/v3.2.1/v3.2.1-passcodes-armeabi_v7a-app.apk" @@ -212,7 +212,7 @@ "state": "uploaded", "size": 35428199, "digest": "sha256:1db1e993f9421007bab6601e16453d90883384608345be3bff351aefc2db7f96", - "download_count": 27, + "download_count": 32, "created_at": "2026-08-02T15:00:58Z", "updated_at": "2026-08-02T15:02:05Z", "browser_download_url": "https://github.com/PasscodesApp/Passcodes/releases/download/v3.2.1/v3.2.1-passcodes-arm64_v8a-app.apk" @@ -1072,7 +1072,7 @@ "state": "uploaded", "size": 9239174, "digest": "sha256:d21a45f18f27157fa2b6a4b38f2fad3a136de36d1a4f074dc5601b503047a3e0", - "download_count": 23, + "download_count": 24, "created_at": "2026-05-19T13:32:39Z", "updated_at": "2026-05-19T13:32:48Z", "browser_download_url": "https://github.com/PasscodesApp/Passcodes/releases/download/v2.1.1/v2.1.1-passcodes-app-universal-release.apk" @@ -2716,7 +2716,7 @@ "state": "uploaded", "size": 3101486, "digest": "sha256:7ec6fb3c60b6f5b3f7844ba7fa7a386825b9d35faaae7eff833b8f41022c7c7d", - "download_count": 38, + "download_count": 39, "created_at": "2025-09-10T14:44:46Z", "updated_at": "2025-09-10T14:44:54Z", "browser_download_url": "https://github.com/PasscodesApp/Passcodes/releases/download/v1.1.1/app-universal-release.apk" diff --git a/src/lib/changelog.ts b/src/lib/changelog.ts index e8b484b..3de2018 100644 --- a/src/lib/changelog.ts +++ b/src/lib/changelog.ts @@ -106,3 +106,158 @@ export function isDeprecatedEntry(entry: ChangelogEntry): boolean { const major = parseInt(match[1], 10); return major === 1 || major === 2; } + +export type ReleaseState = + | "Stable" + | "Beta" + | "Alpha" + | "Deprecated" + | "Yanked" + | "Milestone"; + +/** + * Classify a changelog entry into its primary semantic release state. + */ +export function getReleaseState(entry: ChangelogEntry): ReleaseState { + if (entry.isMilestone) return "Milestone"; + if (entry.isYanked) return "Yanked"; + if (isDeprecatedEntry(entry)) return "Deprecated"; + return entry.releaseType; +} + +/** + * Get the current stable production release. + * Excludes milestones, yanked releases, deprecated releases, and non-stable channels. + */ +export function getLatestProductionRelease(): ChangelogEntry | undefined { + return CHANGELOG_ENTRIES.find( + (entry) => + !entry.isMilestone && + !entry.isYanked && + !isDeprecatedEntry(entry) && + entry.releaseType === "Stable" + ); +} + +/** + * Get the most recent meaningful updates (active, non-milestone releases). + */ +export function getRecentUpdates(count: number = 3): ChangelogEntry[] { + return CHANGELOG_ENTRIES.filter( + (entry) => + !entry.isMilestone && + !isDeprecatedEntry(entry) && + !entry.isYanked + ).slice(0, count); +} + +/** + * Get documented major architectural milestones. + * Includes the explicit architecture transition milestone and major version baseline releases. + */ +export function getProjectMilestones(): ChangelogEntry[] { + return CHANGELOG_ENTRIES.filter( + (entry) => + entry.isMilestone || + (entry.isMajor && + ["v3.0.0", "v2.0.0", "v1.0.0"].includes(entry.version)) + ); +} + +export const RELEASE_CHANNEL_FILTERS = [ + "All", + "Stable", + "Beta", + "Alpha", + "Milestones", + "Deprecated", + "Yanked", +] as const; + +export type ReleaseChannelFilter = (typeof RELEASE_CHANNEL_FILTERS)[number]; + +/** + * Search across version, title, summary, highlights, technical details, and release state. + */ +export function matchesReleaseSearch( + entry: ChangelogEntry, + query: string +): boolean { + const searchLower = query.trim().toLowerCase(); + if (!searchLower) return true; + + const state = getReleaseState(entry).toLowerCase(); + const releaseType = entry.releaseType.toLowerCase(); + const isDep = isDeprecatedEntry(entry) ? "deprecated" : ""; + const isYank = entry.isYanked ? "yanked" : ""; + const isMs = entry.isMilestone ? "milestone" : ""; + + // Version, title, summary, tldr + if (entry.version.toLowerCase().includes(searchLower)) return true; + if (entry.title.toLowerCase().includes(searchLower)) return true; + if (entry.summary.toLowerCase().includes(searchLower)) return true; + if (entry.tldr?.toLowerCase().includes(searchLower)) return true; + + // Highlights + if (entry.highlights?.some((h) => h.toLowerCase().includes(searchLower))) { + return true; + } + + // Sections + if ( + entry.sections?.some( + (s) => + s.title.toLowerCase().includes(searchLower) || + s.items.some((item) => + item.toLowerCase().includes(searchLower) + ) + ) + ) { + return true; + } + + // Release states & channels + if ( + state.includes(searchLower) || + releaseType.includes(searchLower) || + isDep.includes(searchLower) || + isYank.includes(searchLower) || + isMs.includes(searchLower) + ) { + return true; + } + + // Technical build & runtime details + if (entry.internalDetails) { + const details = entry.internalDetails; + if (details.packageName?.toLowerCase().includes(searchLower)) { + return true; + } + if (details.versionName?.toLowerCase().includes(searchLower)) { + return true; + } + if (details.masterDbVersion?.toLowerCase().includes(searchLower)) { + return true; + } + if (details.minAndroid?.toLowerCase().includes(searchLower)) { + return true; + } + if (details.maxAndroid?.toLowerCase().includes(searchLower)) { + return true; + } + if ( + details.expoSdk && + String(details.expoSdk).toLowerCase().includes(searchLower) + ) { + return true; + } + if ( + details.versionCode && + String(details.versionCode).includes(searchLower) + ) { + return true; + } + } + + return false; +} diff --git a/tests/functional/changelog-search.spec.ts b/tests/functional/changelog-search.spec.ts index e9ca501..c35d58a 100644 --- a/tests/functional/changelog-search.spec.ts +++ b/tests/functional/changelog-search.spec.ts @@ -44,6 +44,24 @@ test.describe("Changelog Search Functional Tests", () => { expect(restoredCount).toBeGreaterThan(5); }); + test("searching by release state filters to deprecated or yanked entries", async ({ page }) => { + const searchInput = page.getByLabel("Search changelog entries"); + + // Search for "yanked" + await searchInput.fill("yanked"); + const yankedArticles = page.locator("article").filter({ + has: page.locator(".tag").filter({ hasText: /yanked/i }), + }); + await expect(yankedArticles.first()).toBeVisible(); + + // Search for "deprecated" + await searchInput.fill("deprecated"); + const deprecatedArticles = page.locator("article").filter({ + has: page.locator(".tag.deprecated"), + }); + await expect(deprecatedArticles.first()).toBeVisible(); + }); + test("no results message appears on unmatched query and reset works", async ({ page }) => { const searchInput = page.getByLabel("Search changelog entries"); await searchInput.fill("nonexistent-release-query-xyz"); diff --git a/tests/smoke/changelog.spec.ts b/tests/smoke/changelog.spec.ts index f3085b7..7563278 100644 --- a/tests/smoke/changelog.spec.ts +++ b/tests/smoke/changelog.spec.ts @@ -7,7 +7,12 @@ test.describe("Changelog Smoke & Detail Page Tests", () => { test("changelog index loads and displays timeline stream", async ({ page }) => { await expect(page).toHaveTitle(/Passcodes/i); - await expect(page.getByRole("heading", { name: "Release Highlights & Milestones", level: 1 })).toBeVisible(); + await expect( + page.getByRole("heading", { + name: "Release Highlights & Milestones", + level: 1, + }) + ).toBeVisible(); // Articles/timeline entries render const articles = page.locator("article"); @@ -17,6 +22,59 @@ test.describe("Changelog Smoke & Detail Page Tests", () => { await expect(page.getByText("v3.2.1").first()).toBeVisible(); }); + test("current production release hero renders prominently with actions", async ({ page }) => { + // Current production badge + await expect(page.getByText(/Current Production Release/i)).toBeVisible(); + + // Top production heading + const heroHeading = page.locator("#current-release"); + await expect(heroHeading).toBeVisible(); + await expect(heroHeading.getByRole("heading", { level: 2 })).toContainText("v3.2.1"); + + // Action links + const downloadBtn = heroHeading.getByRole("link", { name: /Get v3\.2\.1 APK/i }); + await expect(downloadBtn).toBeVisible(); + await expect(downloadBtn).toHaveAttribute("href", /\/downloads\/?/); + + const detailsBtn = heroHeading.getByRole("link", { name: /Release Details/i }); + await expect(detailsBtn).toBeVisible(); + }); + + test("recent updates and architecture milestones sections render properly", async ({ page }) => { + // Recent Updates + await expect( + page.getByRole("heading", { name: "Recent Updates", level: 2 }) + ).toBeVisible(); + + // Major Architectural Milestones + await expect( + page.getByRole("heading", { + name: "Major Architectural Milestones", + level: 2, + }) + ).toBeVisible(); + await expect( + page.getByText(/End of Android-Only Codebase/i).first() + ).toBeVisible(); + }); + + test("release channel filter pills update visible timeline entries", async ({ page }) => { + const betaChannelBtn = page.getByRole("button", { name: "Beta", exact: true }); + await expect(betaChannelBtn).toBeVisible(); + await betaChannelBtn.click(); + + // Beta releases should be visible + const articles = page.locator("article"); + await expect(articles.first()).toBeVisible(); + await expect(articles.locator(".tag.beta").first()).toBeVisible(); + // Should not show active stable in beta filter + await expect(articles.locator(".tag.stable")).toHaveCount(0); + + // Reset by clicking All + const allChannelBtn = page.getByRole("button", { name: "All", exact: true }).filter({ hasText: /^All$/ }).last(); + await allChannelBtn.click(); + }); + test("historical releases display deprecated and yanked indicators appropriately", async ({ page }) => { // v2.1.1 should have Deprecated tag const v2Entry = page.locator("article").filter({ hasText: /v2\.1\.1/i }).first(); @@ -24,10 +82,15 @@ test.describe("Changelog Smoke & Detail Page Tests", () => { await expect(v2Entry.locator(".tag.deprecated")).toBeVisible(); // v1.2.0 should display Yanked tag and Deprecated tag - const v120Entry = page.locator("article").filter({ has: page.getByRole("heading", { name: /v1\.2\.0/i }) }).first(); + const v120Entry = page + .locator("article") + .filter({ has: page.getByRole("heading", { name: /v1\.2\.0/i }) }) + .first(); await expect(v120Entry).toBeVisible(); await expect(v120Entry.locator(".tag.deprecated")).toBeVisible(); - await expect(v120Entry.locator(".tag").filter({ hasText: /yanked/i })).toBeVisible(); + await expect( + v120Entry.locator(".tag").filter({ hasText: /yanked/i }) + ).toBeVisible(); }); test("release detail page loads with metadata and navigates back", async ({ page }) => { @@ -42,6 +105,14 @@ test.describe("Changelog Smoke & Detail Page Tests", () => { // Release Highlights or sections are visible await expect(page.getByText(/Release Highlights/i)).toBeVisible(); + // Documentation resources are visible + await expect(page.getByText(/Relevant Documentation/i)).toBeVisible(); + + // Download APK button is visible for stable release + await expect( + page.getByRole("link", { name: /Get v3\.2\.1 APK/i }) + ).toBeVisible(); + // Back link works const backLink = page.getByRole("link", { name: /Back to all updates/i }); await expect(backLink).toBeVisible(); @@ -57,4 +128,16 @@ test.describe("Changelog Smoke & Detail Page Tests", () => { await expect(page.locator(".tag.deprecated")).toContainText(/Deprecated Release/i); await expect(page.getByText(/Deprecated Release Notice/i)).toBeVisible(); }); + + test("yanked release detail page displays yanked notice banner without download button", async ({ page }) => { + // Navigate to yanked release detail page + await page.goto("/changelog/v1-2-0-alpha-yanked/"); + + await expect(page).toHaveTitle(/v1\.2\.0.*Passcodes Changelog/i); + await expect(page.getByText(/Yanked Release Notice/i)).toBeVisible(); + // Should not offer a direct get APK button for yanked release + await expect( + page.getByRole("link", { name: /Get v1\.2\.0 APK/i }) + ).toHaveCount(0); + }); }); From 4c29dc20dd69950ab0d3e6fe189f5d96c2a693af Mon Sep 17 00:00:00 2001 From: harsha-vardhan-burra Date: Fri, 25 Sep 2026 18:36:10 +0530 Subject: [PATCH 2/4] feat(site): improve release discovery navigation Signed-off-by: harsha-vardhan-burra --- src/app/changelog/[slug]/page.tsx | 67 ++++++++++++++++++- .../changelog/CurrentReleaseHero.tsx | 22 ++++-- src/components/downloads/DownloadCard.tsx | 40 ++++++++++- src/components/downloads/DownloadsContent.tsx | 11 ++- src/components/home/Hero.tsx | 8 ++- src/lib/changelog.ts | 19 ++++++ tests/smoke/changelog.spec.ts | 8 +++ tests/smoke/downloads.spec.ts | 10 +++ 8 files changed, 171 insertions(+), 14 deletions(-) diff --git a/src/app/changelog/[slug]/page.tsx b/src/app/changelog/[slug]/page.tsx index b114584..c6aa66c 100644 --- a/src/app/changelog/[slug]/page.tsx +++ b/src/app/changelog/[slug]/page.tsx @@ -78,7 +78,7 @@ export default function ChangelogEntryPage({
{/* 1. Header & Release Metadata */} -
+
Back to all updates + +
@@ -170,12 +178,22 @@ export default function ChangelogEntryPage({ Get {entry.version} APK )} + +
+ + {/* Next Steps Journey Card */} +
+
+
+ + Next Steps + +

+ Ready to run Passcodes? +

+

+ {isDeprecated || entry.isYanked + ? "This build is deprecated or yanked. Download the verified production release or review the setup guide." + : "Download the latest signed Android package, read our configuration walkthrough, or explore all releases."} +

+
+
+ +
+
+
diff --git a/src/components/changelog/CurrentReleaseHero.tsx b/src/components/changelog/CurrentReleaseHero.tsx index 1cb6dfe..caec599 100644 --- a/src/components/changelog/CurrentReleaseHero.tsx +++ b/src/components/changelog/CurrentReleaseHero.tsx @@ -5,9 +5,11 @@ import { ExternalLink, Calendar, CheckCircle2, + BookOpen, } from "lucide-react"; import type { ChangelogEntry } from "@/lib/changelog"; import { formatDate } from "@passcodes/passalgo"; +import { USER_GUIDE_URL } from "@/lib/constants"; interface CurrentReleaseHeroProps { release: ChangelogEntry; @@ -88,7 +90,7 @@ export function CurrentReleaseHero({ release }: CurrentReleaseHeroProps) { {/* Build Specifications Pill Group */} {release.internalDetails && (
- Specifications: + Specs: {release.internalDetails.expoSdk && ( Expo SDK {release.internalDetails.expoSdk} @@ -104,11 +106,6 @@ export function CurrentReleaseHero({ release }: CurrentReleaseHeroProps) { DB Schema {release.internalDetails.masterDbVersion} )} - {release.internalDetails.packageName && ( - - {release.internalDetails.packageName} - - )}
)} @@ -130,12 +127,23 @@ export function CurrentReleaseHero({ release }: CurrentReleaseHeroProps) {