From e5e5c0c7172d9356bcbce33efa4146b0381962bc Mon Sep 17 00:00:00 2001 From: cc Date: Wed, 15 Apr 2026 19:22:34 +0200 Subject: [PATCH] Add home page filter and display product names MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add filter input to home page header (searches by product name/version) - Move platform toggles (iOS/macOS/OS X) to header - Display product names (e.g., "macOS Big Sur") on each build card - Normalize platform names: mac→macOS, osx→OS X - Center section headers with larger font - Support left/right positions in HeaderPortal --- src/components/header-portal.tsx | 11 ++- src/components/navtop.tsx | 7 +- src/components/oslist.tsx | 125 +++++++++++++++++++++++++------ 3 files changed, 116 insertions(+), 27 deletions(-) diff --git a/src/components/header-portal.tsx b/src/components/header-portal.tsx index 192b141..9c3e358 100644 --- a/src/components/header-portal.tsx +++ b/src/components/header-portal.tsx @@ -4,8 +4,14 @@ import { createPortal } from "react-dom"; import { useEffect, useState, type ReactNode } from "react"; export const HEADER_PORTAL_ID = "header-controls-portal"; +export const HEADER_PORTAL_LEFT_ID = "header-controls-portal-left"; -export function HeaderPortal({ children }: { children: ReactNode }) { +interface HeaderPortalProps { + children: ReactNode; + position?: "left" | "right"; +} + +export function HeaderPortal({ children, position = "right" }: HeaderPortalProps) { const [mounted, setMounted] = useState(false); useEffect(() => { @@ -14,7 +20,8 @@ export function HeaderPortal({ children }: { children: ReactNode }) { if (!mounted) return null; - const target = document.getElementById(HEADER_PORTAL_ID); + const targetId = position === "left" ? HEADER_PORTAL_LEFT_ID : HEADER_PORTAL_ID; + const target = document.getElementById(targetId); if (!target) return null; return createPortal(children, target); diff --git a/src/components/navtop.tsx b/src/components/navtop.tsx index d02a97e..9da44ac 100644 --- a/src/components/navtop.tsx +++ b/src/components/navtop.tsx @@ -6,7 +6,7 @@ import { Moon, Sun, Key, Folder } from "lucide-react"; import { useEffect, useState } from "react"; import { usePathname, useSearchParams } from "next/navigation"; import { VersionSwitcher } from "./version-switcher"; -import { HEADER_PORTAL_ID } from "./header-portal"; +import { HEADER_PORTAL_ID, HEADER_PORTAL_LEFT_ID } from "./header-portal"; function ThemeToggle() { const { theme, setTheme } = useTheme(); @@ -119,12 +119,12 @@ export function NavTop() { ? "find" : "keys"; - const showHeaderControls = isOSPage && (currentPage === "keys" || currentPage === "files" || currentPage === "find"); + const showHeaderControls = isHome || (isOSPage && (currentPage === "keys" || currentPage === "files" || currentPage === "find")); return (
- {/* Left group: logo + breadcrumb */} + {/* Left group: logo + breadcrumb + left portal */}

@@ -132,6 +132,7 @@ export function NavTop() {

{isHome && } + {isHome &&
} {isOSPage && os && }
diff --git a/src/components/oslist.tsx b/src/components/oslist.tsx index 75db7ff..25beadb 100644 --- a/src/components/oslist.tsx +++ b/src/components/oslist.tsx @@ -3,10 +3,14 @@ import Link from "next/link"; import { useEffect, useState, useMemo } from "react"; import { useSearchParams } from "next/navigation"; +import { Search, X } from "lucide-react"; import { Group, OS } from "@/lib/types"; import { dataURL } from "@/lib/env"; import { Skeleton } from "./ui/skeleton"; +import { Input } from "./ui/input"; +import { Button } from "./ui/button"; +import { HeaderPortal } from "./header-portal"; function responseOK(r: Response) { if (!r.ok) { @@ -34,6 +38,16 @@ interface MajorGroup { versions: OS[]; } +const PLATFORM_NAMES: Record = { + iOS: "iOS", + mac: "macOS", + osx: "OS X", +}; + +function getPlatformName(key: string): string { + return PLATFORM_NAMES[key] || key; +} + function groupByMajor(list: OS[]): MajorGroup[] { const bucket = new Map(); @@ -61,6 +75,15 @@ export default function OSList() { const [groups, setGroups] = useState([]); const [highlights, setHighlights] = useState>(new Set()); const [selectedPlatforms, setSelectedPlatforms] = useState>(new Set()); + const [keyword, setKeyword] = useState(""); + const [debouncedKeyword, setDebouncedKeyword] = useState(""); + + useEffect(() => { + const timer = setTimeout(() => { + setDebouncedKeyword(keyword); + }, 200); + return () => clearTimeout(timer); + }, [keyword]); useEffect(() => { const set: Set = new Set(); @@ -150,8 +173,31 @@ export default function OSList() { }; const filteredGroups = useMemo(() => { - return groups.filter((g) => selectedPlatforms.has(g.name)); - }, [groups, selectedPlatforms]); + const kw = debouncedKeyword.toLowerCase(); + return groups + .filter((g) => selectedPlatforms.has(g.name)) + .map((g) => { + if (!kw) return g; + // Filter OS entries by product name or version + const filteredList = g.list.filter( + (os) => + os.name.toLowerCase().includes(kw) || + os.version.toLowerCase().includes(kw) + ); + return { ...g, list: filteredList }; + }) + .filter((g) => g.list.length > 0); + }, [groups, selectedPlatforms, debouncedKeyword]); + + const totalCount = useMemo(() => { + return groups.reduce((sum, g) => sum + g.list.length, 0); + }, [groups]); + + const filteredCount = useMemo(() => { + return filteredGroups.reduce((sum, g) => sum + g.list.length, 0); + }, [filteredGroups]); + + const isFiltering = debouncedKeyword.length > 0; return (
@@ -178,31 +224,60 @@ export default function OSList() { {!loading && groups.length > 0 && ( <> - {/* Platform filters */} -
- {groups.map((group) => ( - - ))} -
+ +
+ {groups.map((group) => ( + + ))} +
+
+ + +
+ + setKeyword(e.target.value)} + className="pl-9 pr-9" + /> + {keyword && ( + + )} +
+
+ + {isFiltering && ( +
+ {filteredCount} of {totalCount} builds +
+ )} {filteredGroups.map((group) => { const majorGroups = groupByMajor(group.list); return (
-

- {group.name} - +

+ {getPlatformName(group.name)}

{!showAll ? ( @@ -215,6 +290,9 @@ export default function OSList() { href={`/os/keys?os=${group.name}/${os.version}_${os.build}`} className="block p-3 border border-border rounded-lg hover:border-foreground/20 transition-colors hover:bg-accent/50" > +
+ {os.name} +
{os.version} @@ -230,7 +308,7 @@ export default function OSList() { {majorGroups.map((majorGroup) => (

- {group.name} {majorGroup.major} + {getPlatformName(group.name)} {majorGroup.major}

    {majorGroup.versions.map((os, index) => ( @@ -239,6 +317,9 @@ export default function OSList() { href={`/os/keys?os=${group.name}/${os.version}_${os.build}`} className="block p-3 border border-border rounded-lg hover:border-foreground/20 transition-colors hover:bg-accent/50" > +
    + {os.name} +
    {os.version}