mirror of
https://github.com/wahyd4/hey-search.git
synced 2026-08-09 05:06:23 +10:00
feat: unified header with hamburger menu across all pages
- New AppHeader component: logo on left, optional center slot, menu icon on right - Clicking menu icon opens dropdown: Settings, Bookmarks, Backgrounds, API Docs - Dropdown closes on outside click and Escape key - Transparent mode for home page when background image is active - All pages (home, results, gallery, bookmarks) now use AppHeader consistently - Results page: search bar in header center slot, category tabs in separate bar below - Home page: flex-col layout, content vertically centered in remaining space - Footer simplified to just the app name Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
+71
-141
@@ -1,5 +1,5 @@
|
||||
import { useState, useCallback, useEffect, useRef } from "react";
|
||||
import { Search, Settings, Globe, ImageIcon, Loader2, ExternalLink, ChevronLeft, ChevronRight, SlidersHorizontal, RefreshCw, Images, BookmarkIcon } from "lucide-react";
|
||||
import { Search, Globe, ImageIcon, Loader2, ExternalLink, ChevronLeft, ChevronRight, SlidersHorizontal, RefreshCw } from "lucide-react";
|
||||
import { SearchBar } from "@/components/SearchBar";
|
||||
import { WebResults } from "@/components/WebResults";
|
||||
import { ImageResults } from "@/components/ImageResults";
|
||||
@@ -8,6 +8,7 @@ import { SettingsModal } from "@/components/SettingsModal";
|
||||
import { ErrorToast } from "@/components/ErrorToast";
|
||||
import { BackgroundGallery } from "@/components/BackgroundGallery";
|
||||
import { Bookmarks } from "@/components/Bookmarks";
|
||||
import { AppHeader } from "@/components/AppHeader";
|
||||
import { search as apiSearch, isImageResult, getBackground, refreshBackground, getBookmarkedUrls, addBookmark, removeBookmarkByUrl, type SearchResponse, type WebResult, type ImageResult, type BackgroundInfo } from "@/lib/api";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
@@ -227,12 +228,12 @@ function App() {
|
||||
|
||||
// Gallery page
|
||||
if (showGallery) {
|
||||
return <BackgroundGallery onBack={handleGoHome} />;
|
||||
return <BackgroundGallery onBack={handleGoHome} onShowSettings={() => setShowSettings(true)} onShowBookmarks={handleShowBookmarks} />;
|
||||
}
|
||||
|
||||
// Bookmarks page
|
||||
if (showBookmarks) {
|
||||
return <Bookmarks />;
|
||||
return <Bookmarks onGoHome={handleGoHome} onShowSettings={() => setShowSettings(true)} onShowGallery={handleShowGallery} />;
|
||||
}
|
||||
|
||||
const bgUrl = bgInfo?.enabled && bgInfo?.url ? bgInfo.url : null;
|
||||
@@ -240,7 +241,7 @@ function App() {
|
||||
// Home page (no search yet)
|
||||
if (!hasSearched) {
|
||||
return (
|
||||
<div className="relative flex min-h-screen flex-col items-center justify-center px-4">
|
||||
<div className="relative flex min-h-screen flex-col">
|
||||
{/* Background image */}
|
||||
{bgUrl && (
|
||||
<div
|
||||
@@ -251,101 +252,61 @@ function App() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Top-right: settings + nav menu */}
|
||||
<div className="absolute top-4 right-4 flex items-center gap-1">
|
||||
<button
|
||||
onClick={handleShowBookmarks}
|
||||
aria-label="Bookmarks"
|
||||
title="Bookmarks"
|
||||
className={cn(
|
||||
"rounded-full p-2 transition-colors focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none",
|
||||
bgUrl ? "text-white/70 hover:text-white hover:bg-white/10" : "text-muted-foreground hover:bg-accent"
|
||||
)}
|
||||
>
|
||||
<BookmarkIcon className="h-5 w-5" aria-hidden="true" />
|
||||
</button>
|
||||
<button
|
||||
onClick={handleShowGallery}
|
||||
aria-label="Background gallery"
|
||||
title="Backgrounds"
|
||||
className={cn(
|
||||
"rounded-full p-2 transition-colors focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none",
|
||||
bgUrl ? "text-white/70 hover:text-white hover:bg-white/10" : "text-muted-foreground hover:bg-accent"
|
||||
)}
|
||||
>
|
||||
<Images className="h-5 w-5" aria-hidden="true" />
|
||||
</button>
|
||||
<a
|
||||
href="/docs"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="API Docs"
|
||||
title="API Docs"
|
||||
className={cn(
|
||||
"rounded-full p-2 transition-colors focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none",
|
||||
bgUrl ? "text-white/70 hover:text-white hover:bg-white/10" : "text-muted-foreground hover:bg-accent"
|
||||
)}
|
||||
>
|
||||
<ExternalLink className="h-5 w-5" aria-hidden="true" />
|
||||
</a>
|
||||
<div className={cn("mx-1 h-5 w-px", bgUrl ? "bg-white/20" : "bg-border")} aria-hidden="true" />
|
||||
<button
|
||||
onClick={() => setShowSettings(true)}
|
||||
aria-label="Open settings"
|
||||
title="Settings"
|
||||
className={cn(
|
||||
"rounded-full p-2 transition-colors focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none",
|
||||
bgUrl ? "text-white/80 hover:text-white hover:bg-white/10" : "text-muted-foreground hover:bg-accent"
|
||||
)}
|
||||
>
|
||||
<Settings className="h-5 w-5" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
{/* Shared header */}
|
||||
<AppHeader
|
||||
onGoHome={handleGoHome}
|
||||
onShowSettings={() => setShowSettings(true)}
|
||||
onShowBookmarks={handleShowBookmarks}
|
||||
onShowGallery={handleShowGallery}
|
||||
transparent={!!bgUrl}
|
||||
/>
|
||||
|
||||
{/* Center content */}
|
||||
<div className="mb-8 text-center">
|
||||
<h1 className="text-4xl font-bold tracking-tight sm:text-5xl">
|
||||
<span className={cn(
|
||||
"bg-clip-text text-transparent",
|
||||
bgUrl
|
||||
? "bg-gradient-to-r from-white to-white/90"
|
||||
: "bg-gradient-to-r from-blue-600 to-purple-600"
|
||||
)}>
|
||||
Hey Search
|
||||
</span>
|
||||
</h1>
|
||||
<p className={cn("mt-2", bgUrl ? "text-white/70" : "text-muted-foreground")}>
|
||||
Private metasearch engine
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-1 flex-col items-center justify-center px-4 pb-20">
|
||||
<div className="mb-8 text-center">
|
||||
<h1 className="text-4xl font-bold tracking-tight sm:text-5xl">
|
||||
<span className={cn(
|
||||
"bg-clip-text text-transparent",
|
||||
bgUrl
|
||||
? "bg-gradient-to-r from-white to-white/90"
|
||||
: "bg-gradient-to-r from-blue-600 to-purple-600"
|
||||
)}>
|
||||
Hey Search
|
||||
</span>
|
||||
</h1>
|
||||
<p className={cn("mt-2", bgUrl ? "text-white/70" : "text-muted-foreground")}>
|
||||
Private metasearch engine
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<SearchBar onSearch={(q) => doSearch(q, category)} className="w-full" />
|
||||
<SearchBar onSearch={(q) => doSearch(q, category)} className="w-full" />
|
||||
|
||||
{/* Category switch */}
|
||||
<div className="mt-4 flex items-center gap-2" role="group" aria-label="Search category">
|
||||
{([
|
||||
{ key: "web" as const, label: "Web", icon: Globe },
|
||||
{ key: "images" as const, label: "Images", icon: ImageIcon },
|
||||
]).map(({ key, label, icon: Icon }) => (
|
||||
<button
|
||||
key={key}
|
||||
onClick={() => setCategory(key)}
|
||||
aria-pressed={category === key}
|
||||
className={cn(
|
||||
"flex items-center gap-1.5 rounded-full px-4 py-1.5 text-sm font-medium transition-colors focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none",
|
||||
category === key
|
||||
? bgUrl
|
||||
? "bg-white/20 text-white border border-white/30"
|
||||
: "bg-primary text-primary-foreground"
|
||||
: bgUrl
|
||||
? "text-white/60 hover:text-white hover:bg-white/10 border border-transparent"
|
||||
: "text-muted-foreground hover:bg-accent border border-transparent"
|
||||
)}
|
||||
>
|
||||
<Icon className="h-4 w-4" aria-hidden="true" />
|
||||
{label}
|
||||
</button>
|
||||
))}
|
||||
{/* Category switch */}
|
||||
<div className="mt-4 flex items-center gap-2" role="group" aria-label="Search category">
|
||||
{([
|
||||
{ key: "web" as const, label: "Web", icon: Globe },
|
||||
{ key: "images" as const, label: "Images", icon: ImageIcon },
|
||||
]).map(({ key, label, icon: Icon }) => (
|
||||
<button
|
||||
key={key}
|
||||
onClick={() => setCategory(key)}
|
||||
aria-pressed={category === key}
|
||||
className={cn(
|
||||
"flex items-center gap-1.5 rounded-full px-4 py-1.5 text-sm font-medium transition-colors focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none",
|
||||
category === key
|
||||
? bgUrl
|
||||
? "bg-white/20 text-white border border-white/30"
|
||||
: "bg-primary text-primary-foreground"
|
||||
: bgUrl
|
||||
? "text-white/60 hover:text-white hover:bg-white/10 border border-transparent"
|
||||
: "text-muted-foreground hover:bg-accent border border-transparent"
|
||||
)}
|
||||
>
|
||||
<Icon className="h-4 w-4" aria-hidden="true" />
|
||||
{label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bottom-right: new image button */}
|
||||
@@ -402,30 +363,19 @@ function App() {
|
||||
{statusMessage}
|
||||
</div>
|
||||
|
||||
{/* Header */}
|
||||
<header className="sticky top-0 z-40 border-b bg-background/95 backdrop-blur">
|
||||
<div className="flex items-center gap-3 px-4 py-3">
|
||||
<button
|
||||
onClick={handleGoHome}
|
||||
aria-label="Go to homepage"
|
||||
className="shrink-0 text-xl font-bold focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none rounded"
|
||||
>
|
||||
<span className="bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent">
|
||||
HS
|
||||
</span>
|
||||
</button>
|
||||
<SearchBar initialQuery={query} onSearch={(q) => doSearch(q)} className="flex-1" />
|
||||
<button
|
||||
onClick={() => setShowSettings(true)}
|
||||
className="shrink-0 rounded-full p-2 text-muted-foreground hover:bg-accent focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none"
|
||||
aria-label="Open settings"
|
||||
>
|
||||
<Settings className="h-5 w-5" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
{/* Shared header with search bar in center slot */}
|
||||
<AppHeader
|
||||
onGoHome={handleGoHome}
|
||||
onShowSettings={() => setShowSettings(true)}
|
||||
onShowBookmarks={handleShowBookmarks}
|
||||
onShowGallery={handleShowGallery}
|
||||
>
|
||||
<SearchBar initialQuery={query} onSearch={(q) => doSearch(q)} />
|
||||
</AppHeader>
|
||||
|
||||
{/* Category tabs */}
|
||||
<nav aria-label="Search categories" className="flex items-center gap-1 px-4 pb-2">
|
||||
{/* Category / filter bar */}
|
||||
<div className="sticky top-[57px] z-30 border-b bg-background/95 backdrop-blur">
|
||||
<nav aria-label="Search categories" className="flex items-center gap-1 px-4 py-2">
|
||||
{([
|
||||
{ key: "web" as const, label: "Web", icon: Globe },
|
||||
{ key: "images" as const, label: "Images", icon: ImageIcon },
|
||||
@@ -469,7 +419,7 @@ function App() {
|
||||
</>
|
||||
)}
|
||||
</nav>
|
||||
</header>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<main id="main-content" className="mx-auto w-full max-w-6xl flex-1 px-4 py-6">
|
||||
@@ -554,28 +504,8 @@ function App() {
|
||||
</main>
|
||||
|
||||
{/* Footer */}
|
||||
<footer className="border-t px-4 py-3">
|
||||
<div className="mx-auto flex max-w-6xl items-center justify-between text-xs text-muted-foreground">
|
||||
<span>Hey Search</span>
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
onClick={handleShowBookmarks}
|
||||
className="flex items-center gap-1 hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none rounded transition-colors"
|
||||
>
|
||||
<BookmarkIcon className="h-3 w-3" aria-hidden="true" />
|
||||
Bookmarks
|
||||
</button>
|
||||
<a
|
||||
href="/docs"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-1 hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none rounded transition-colors"
|
||||
>
|
||||
<ExternalLink className="h-3 w-3" aria-hidden="true" />
|
||||
API Docs
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<footer className="border-t px-4 py-3 text-xs text-muted-foreground">
|
||||
<div className="mx-auto max-w-6xl text-center">Hey Search</div>
|
||||
</footer>
|
||||
|
||||
{/* Error toasts */}
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import { Menu, X, Settings, BookmarkIcon, Images, ExternalLink } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface AppHeaderProps {
|
||||
onGoHome: () => void;
|
||||
onShowSettings: () => void;
|
||||
onShowBookmarks: () => void;
|
||||
onShowGallery: () => void;
|
||||
/** Makes header transparent + white text (for home page with background image) */
|
||||
transparent?: boolean;
|
||||
/** Optional center content (e.g. search bar) */
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export function AppHeader({
|
||||
onGoHome,
|
||||
onShowSettings,
|
||||
onShowBookmarks,
|
||||
onShowGallery,
|
||||
transparent = false,
|
||||
children,
|
||||
}: AppHeaderProps) {
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
const menuRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Close menu on outside click
|
||||
useEffect(() => {
|
||||
if (!menuOpen) return;
|
||||
const handler = (e: MouseEvent) => {
|
||||
if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
|
||||
setMenuOpen(false);
|
||||
}
|
||||
};
|
||||
document.addEventListener("mousedown", handler);
|
||||
return () => document.removeEventListener("mousedown", handler);
|
||||
}, [menuOpen]);
|
||||
|
||||
// Close on Escape
|
||||
useEffect(() => {
|
||||
if (!menuOpen) return;
|
||||
const handler = (e: KeyboardEvent) => { if (e.key === "Escape") setMenuOpen(false); };
|
||||
document.addEventListener("keydown", handler);
|
||||
return () => document.removeEventListener("keydown", handler);
|
||||
}, [menuOpen]);
|
||||
|
||||
const base = transparent
|
||||
? "border-transparent bg-transparent"
|
||||
: "border-b bg-background/95 backdrop-blur";
|
||||
|
||||
const iconCls = transparent
|
||||
? "text-white/80 hover:text-white hover:bg-white/10"
|
||||
: "text-muted-foreground hover:bg-accent";
|
||||
|
||||
const logoCls = transparent
|
||||
? "text-white font-bold"
|
||||
: "bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent font-bold";
|
||||
|
||||
const dropdownCls = "absolute right-0 top-full mt-2 w-48 rounded-xl border bg-popover shadow-lg z-50 overflow-hidden";
|
||||
|
||||
const menuItem = (
|
||||
icon: React.ReactNode,
|
||||
label: string,
|
||||
action: () => void,
|
||||
isLink?: boolean,
|
||||
href?: string,
|
||||
) => {
|
||||
const cls = "flex w-full items-center gap-3 px-4 py-3 text-sm text-foreground hover:bg-accent transition-colors focus-visible:ring-inset focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none";
|
||||
if (isLink && href) {
|
||||
return (
|
||||
<a href={href} target="_blank" rel="noopener noreferrer" className={cls} onClick={() => setMenuOpen(false)}>
|
||||
{icon}
|
||||
{label}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<button className={cls} onClick={() => { action(); setMenuOpen(false); }}>
|
||||
{icon}
|
||||
{label}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<header className={cn("sticky top-0 z-40 border", base)}>
|
||||
<div className="flex items-center gap-3 px-4 py-3">
|
||||
{/* Logo */}
|
||||
<button
|
||||
onClick={onGoHome}
|
||||
aria-label="Go to homepage"
|
||||
className={cn("shrink-0 text-xl focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none rounded", logoCls)}
|
||||
>
|
||||
HS
|
||||
</button>
|
||||
|
||||
{/* Center slot */}
|
||||
{children && <div className="flex-1 min-w-0">{children}</div>}
|
||||
|
||||
{/* Spacer when no center content */}
|
||||
{!children && <div className="flex-1" />}
|
||||
|
||||
{/* Menu button */}
|
||||
<div className="relative shrink-0" ref={menuRef}>
|
||||
<button
|
||||
onClick={() => setMenuOpen((v) => !v)}
|
||||
aria-label={menuOpen ? "Close menu" : "Open menu"}
|
||||
aria-expanded={menuOpen}
|
||||
aria-haspopup="menu"
|
||||
className={cn(
|
||||
"rounded-full p-2 transition-colors focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none",
|
||||
iconCls
|
||||
)}
|
||||
>
|
||||
{menuOpen
|
||||
? <X className="h-5 w-5" aria-hidden="true" />
|
||||
: <Menu className="h-5 w-5" aria-hidden="true" />
|
||||
}
|
||||
</button>
|
||||
|
||||
{menuOpen && (
|
||||
<div className={dropdownCls} role="menu" aria-label="Site navigation">
|
||||
{menuItem(<Settings className="h-4 w-4" />, "Settings", onShowSettings)}
|
||||
{menuItem(<BookmarkIcon className="h-4 w-4" />, "Bookmarks", onShowBookmarks)}
|
||||
{menuItem(<Images className="h-4 w-4" />, "Backgrounds", onShowGallery)}
|
||||
<div className="mx-3 border-t" />
|
||||
{menuItem(<ExternalLink className="h-4 w-4" />, "API Docs", () => {}, true, "/docs")}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
@@ -1,12 +1,15 @@
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { ArrowLeft, Loader2, ExternalLink } from "lucide-react";
|
||||
import { Loader2, ExternalLink } from "lucide-react";
|
||||
import { listBackgrounds, type BackgroundListItem } from "@/lib/api";
|
||||
import { AppHeader } from "@/components/AppHeader";
|
||||
|
||||
interface BackgroundGalleryProps {
|
||||
onBack: () => void;
|
||||
onShowSettings: () => void;
|
||||
onShowBookmarks: () => void;
|
||||
}
|
||||
|
||||
export function BackgroundGallery({ onBack }: BackgroundGalleryProps) {
|
||||
export function BackgroundGallery({ onBack, onShowSettings, onShowBookmarks }: BackgroundGalleryProps) {
|
||||
const [images, setImages] = useState<BackgroundListItem[]>([]);
|
||||
const [page, setPage] = useState(1);
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -43,22 +46,19 @@ export function BackgroundGallery({ onBack }: BackgroundGalleryProps) {
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
{/* Header */}
|
||||
<header className="sticky top-0 z-10 border-b bg-background/95 backdrop-blur">
|
||||
<div className="mx-auto flex max-w-6xl items-center gap-3 px-4 py-3">
|
||||
<button
|
||||
onClick={onBack}
|
||||
aria-label="Go back"
|
||||
className="rounded-full p-2 hover:bg-accent focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none"
|
||||
>
|
||||
<ArrowLeft className="h-5 w-5" />
|
||||
</button>
|
||||
<h1 className="text-lg font-semibold">Background Gallery</h1>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{images.length} image{images.length !== 1 ? "s" : ""}
|
||||
<AppHeader
|
||||
onGoHome={onBack}
|
||||
onShowSettings={onShowSettings}
|
||||
onShowBookmarks={onShowBookmarks}
|
||||
onShowGallery={() => {}}
|
||||
>
|
||||
<h1 className="text-base font-semibold">
|
||||
Background Gallery
|
||||
<span className="ml-2 text-sm font-normal text-muted-foreground">
|
||||
({images.length})
|
||||
</span>
|
||||
</div>
|
||||
</header>
|
||||
</h1>
|
||||
</AppHeader>
|
||||
|
||||
{/* Image grid */}
|
||||
<main className="mx-auto max-w-6xl px-4 py-6">
|
||||
@@ -141,3 +141,4 @@ export function BackgroundGallery({ onBack }: BackgroundGalleryProps) {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,18 @@
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { ArrowLeft, Trash2, ExternalLink, Loader2 } from "lucide-react";
|
||||
import { Trash2, ExternalLink, Loader2 } from "lucide-react";
|
||||
import type { Bookmark } from "@/lib/api";
|
||||
import { getBookmarks, removeBookmark } from "@/lib/api";
|
||||
import { AppHeader } from "@/components/AppHeader";
|
||||
|
||||
type FilterType = "" | "web" | "image";
|
||||
|
||||
export function Bookmarks() {
|
||||
interface BookmarksProps {
|
||||
onGoHome: () => void;
|
||||
onShowSettings: () => void;
|
||||
onShowGallery: () => void;
|
||||
}
|
||||
|
||||
export function Bookmarks({ onGoHome, onShowSettings, onShowGallery }: BookmarksProps) {
|
||||
const [bookmarks, setBookmarks] = useState<Bookmark[]>([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [page, setPage] = useState(1);
|
||||
@@ -42,11 +49,6 @@ export function Bookmarks() {
|
||||
load(next, filter);
|
||||
};
|
||||
|
||||
const goHome = () => {
|
||||
window.history.pushState({}, "", "/");
|
||||
window.dispatchEvent(new PopStateEvent("popstate"));
|
||||
};
|
||||
|
||||
const tabs: { label: string; value: FilterType }[] = [
|
||||
{ label: "All", value: "" },
|
||||
{ label: "Web", value: "web" },
|
||||
@@ -56,20 +58,20 @@ export function Bookmarks() {
|
||||
const hasMore = bookmarks.length < total;
|
||||
|
||||
return (
|
||||
<div className="mx-auto min-h-screen max-w-6xl px-4 py-6">
|
||||
{/* Header */}
|
||||
<div className="mb-6 flex items-center gap-4">
|
||||
<button
|
||||
onClick={goHome}
|
||||
aria-label="Go back home"
|
||||
className="rounded-full p-2 hover:bg-accent focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none transition-colors"
|
||||
>
|
||||
<ArrowLeft className="h-5 w-5" />
|
||||
</button>
|
||||
<h1 className="text-2xl font-bold">Bookmarks</h1>
|
||||
<span className="text-sm text-muted-foreground">({total})</span>
|
||||
</div>
|
||||
<div className="min-h-screen bg-background">
|
||||
<AppHeader
|
||||
onGoHome={onGoHome}
|
||||
onShowSettings={onShowSettings}
|
||||
onShowBookmarks={() => {}}
|
||||
onShowGallery={onShowGallery}
|
||||
>
|
||||
<h1 className="text-base font-semibold">
|
||||
Bookmarks
|
||||
<span className="ml-2 text-sm font-normal text-muted-foreground">({total})</span>
|
||||
</h1>
|
||||
</AppHeader>
|
||||
|
||||
<div className="mx-auto max-w-6xl px-4 py-6">
|
||||
{/* Filter tabs */}
|
||||
<div className="mb-6 flex gap-2" role="tablist" aria-label="Bookmark type filter">
|
||||
{tabs.map((tab) => (
|
||||
@@ -209,6 +211,7 @@ export function Bookmarks() {
|
||||
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user