feat(ui): add shared PageBack component for consistent back navigation

Replace the eight inconsistent back affordances across user-facing pages
with a single absolute-positioned chevron pill, so the control lives in
the same screen position regardless of title length or hero content.

DetailBreadcrumb keeps its textual hierarchy path but no longer owns the
back chevron; PageBack does. DetailHero gains a topNav slot consumed by
Movie/Series/Season/Episode/Request detail pages. Non-hero pages drop
their bespoke back buttons and add PageBack inside a relative wrapper.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Quick
2026-05-26 20:18:05 -04:00
co-authored by Claude Opus 4.7
parent 6c0983c34e
commit f7cae960c4
18 changed files with 201 additions and 182 deletions
+73
View File
@@ -0,0 +1,73 @@
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { MemoryRouter } from "react-router";
import { describe, expect, it, vi } from "vitest";
const mocks = vi.hoisted(() => ({
navigate: vi.fn(),
}));
vi.mock("react-router", async () => {
const actual = await vi.importActual<typeof import("react-router")>("react-router");
return {
...actual,
useNavigate: () => mocks.navigate,
};
});
import PageBack from "./PageBack";
describe("PageBack", () => {
it("renders a button with the default 'Go back' aria-label", () => {
render(
<MemoryRouter>
<PageBack />
</MemoryRouter>,
);
expect(screen.getByRole("button", { name: "Go back" })).toBeInTheDocument();
});
it("uses a custom label when provided", () => {
render(
<MemoryRouter>
<PageBack label="Return to library" />
</MemoryRouter>,
);
expect(screen.getByRole("button", { name: "Return to library" })).toBeInTheDocument();
});
it("calls navigate(-1) on click", async () => {
mocks.navigate.mockClear();
render(
<MemoryRouter>
<PageBack />
</MemoryRouter>,
);
await userEvent.click(screen.getByRole("button", { name: "Go back" }));
expect(mocks.navigate).toHaveBeenCalledTimes(1);
expect(mocks.navigate).toHaveBeenCalledWith(-1);
});
it("applies the documented positioning and glass-subtle styling", () => {
render(
<MemoryRouter>
<PageBack />
</MemoryRouter>,
);
const button = screen.getByRole("button", { name: "Go back" });
expect(button).toHaveClass(
"glass-subtle",
"absolute",
"top-4",
"left-4",
"z-20",
"rounded-full",
"p-1.5",
);
});
});
+20
View File
@@ -0,0 +1,20 @@
import { ChevronLeft } from "lucide-react";
import { useNavigate } from "react-router";
interface PageBackProps {
label?: string;
}
export default function PageBack({ label = "Go back" }: PageBackProps) {
const navigate = useNavigate();
return (
<button
type="button"
aria-label={label}
onClick={() => navigate(-1)}
className="glass-subtle text-muted-foreground hover:text-foreground absolute top-4 left-4 z-20 flex items-center justify-center rounded-full p-1.5 transition-colors sm:top-6 sm:left-6"
>
<ChevronLeft className="size-5" />
</button>
);
}
+19 -39
View File
@@ -1,8 +1,7 @@
import { Link, useNavigate, useParams } from "react-router";
import { ArrowLeft } from "lucide-react";
import { useNavigate, useParams } from "react-router";
import type { Collection, UserCollectionType } from "@/api/types";
import { Button } from "@/components/ui/button";
import PageBack from "@/components/PageBack";
import { Card, CardHeader, CardDescription, CardTitle } from "@/components/ui/card";
import { useCollections } from "@/hooks/queries/collections";
@@ -32,13 +31,8 @@ export default function CollectionEditor() {
if (id && !collection && !isLoading) {
return (
<div className="page-shell space-y-4 py-4 sm:py-6">
<Button asChild variant="ghost" className="w-fit px-0">
<Link to="/collections">
<ArrowLeft className="mr-2 h-4 w-4" />
Back to Collections
</Link>
</Button>
<div className="page-shell relative space-y-4 py-4 sm:py-6">
<PageBack />
<Card className="surface-panel rounded-[1.7rem] border-0 shadow-none">
<CardHeader>
<CardTitle>Collection not found</CardTitle>
@@ -51,21 +45,14 @@ export default function CollectionEditor() {
if (collection && isImportedCollection(collection)) {
return (
<div className="page-shell space-y-6 py-4 sm:py-6">
<div className="space-y-3">
<Button asChild variant="ghost" className="w-fit px-0">
<Link to="/collections">
<ArrowLeft className="mr-2 h-4 w-4" />
Back to Collections
</Link>
</Button>
<div>
<h1 className="page-title text-[clamp(2rem,4vw,3rem)]">{collection.name}</h1>
<p className="page-subtitle mt-1 text-sm sm:text-base">
Edit what's local — name, libraries, sharing. Source-managed details (URL, schedule,
item ordering) are locked.
</p>
</div>
<div className="page-shell relative space-y-6 py-4 sm:py-6">
<PageBack />
<div>
<h1 className="page-title text-[clamp(2rem,4vw,3rem)]">{collection.name}</h1>
<p className="page-subtitle mt-1 text-sm sm:text-base">
Edit what's local — name, libraries, sharing. Source-managed details (URL, schedule,
item ordering) are locked.
</p>
</div>
<ImportedCollectionEditor
key={collection.id}
@@ -80,20 +67,13 @@ export default function CollectionEditor() {
// the wizard so users can see the live card preview while tuning filters.
if (collection && collection.collection_type === "manual") {
return (
<div className="page-shell space-y-6 py-4 sm:py-6">
<div className="space-y-3">
<Button asChild variant="ghost" className="w-fit px-0">
<Link to="/collections">
<ArrowLeft className="mr-2 h-4 w-4" />
Back to Collections
</Link>
</Button>
<div>
<h1 className="page-title text-[clamp(2rem,4vw,3rem)]">Edit {collection.name}</h1>
<p className="page-subtitle mt-1 text-sm sm:text-base">
Manual collections are curated by adding titles directly.
</p>
</div>
<div className="page-shell relative space-y-6 py-4 sm:py-6">
<PageBack />
<div>
<h1 className="page-title text-[clamp(2rem,4vw,3rem)]">Edit {collection.name}</h1>
<p className="page-subtitle mt-1 text-sm sm:text-base">
Manual collections are curated by adding titles directly.
</p>
</div>
<UserCollectionForm collection={collection} onClose={() => navigate("/collections")} />
<section className="space-y-3">
+3
View File
@@ -22,6 +22,7 @@ interface DetailHeroProps {
scoreRow?: ReactNode;
crewLine?: ReactNode;
variant?: "full" | "compact";
topNav?: ReactNode;
}
export default function DetailHero({
@@ -45,6 +46,7 @@ export default function DetailHero({
scoreRow,
crewLine,
variant = "full",
topNav,
}: DetailHeroProps) {
const [backdropLoaded, setBackdropLoaded] = useState(false);
const [posterLoaded, setPosterLoaded] = useState(false);
@@ -65,6 +67,7 @@ export default function DetailHero({
return (
<section className="item-detail-hero border-border/10 relative isolate overflow-hidden border-b">
{topNav}
{(backdropUrl || backdropPlaceholder) && (
<div
className="absolute inset-0 h-full w-full"
@@ -225,7 +225,7 @@ describe("EpisodeContent", () => {
mocks.useRating.mockReturnValue({ data: { rating: 3, rated_at: "2026-03-22T00:00:00Z" } });
});
it("links the season breadcrumb and back button to the resolved season page", () => {
it("links the season breadcrumb segment to the resolved season page", () => {
const markup = renderToStaticMarkup(
<MemoryRouter initialEntries={["/item/episode-1"]}>
<EpisodeContent item={makeEpisodeItem()} />
@@ -233,7 +233,7 @@ describe("EpisodeContent", () => {
);
expect(countOccurrences(markup, 'href="/item/series-1"')).toBe(1);
expect(countOccurrences(markup, 'href="/item/season-1"')).toBe(2);
expect(countOccurrences(markup, 'href="/item/season-1"')).toBe(1);
expect(markup).toContain(">Season 1<");
});
@@ -258,7 +258,7 @@ describe("EpisodeContent", () => {
</MemoryRouter>,
);
expect(countOccurrences(markup, 'href="/item/season-99"')).toBe(2);
expect(countOccurrences(markup, 'href="/item/season-99"')).toBe(1);
expect(markup).toContain(">Season 99<");
});
@@ -16,6 +16,7 @@ import CrewList from "@/components/CrewList";
import DownloadVersionPicker from "@/components/DownloadVersionPicker";
import EditMetadataDialog from "@/components/EditMetadataDialog";
import MediaLocations from "@/components/MediaLocations";
import PageBack from "@/components/PageBack";
import EpisodeCarousel from "./components/EpisodeCarousel";
import DetailHero from "./DetailHero";
import MetadataBadges from "./components/MetadataBadges";
@@ -221,6 +222,7 @@ export default function EpisodeContent({ item }: { item: ItemDetail & { type: "e
<div>
<DetailHero
title={title}
topNav={<PageBack />}
context={
<div className="space-y-3">
<DetailBreadcrumb segments={breadcrumbSegments} />
@@ -16,6 +16,7 @@ import DownloadVersionPicker from "@/components/DownloadVersionPicker";
import EditMetadataDialog from "@/components/EditMetadataDialog";
import MediaLocations from "@/components/MediaLocations";
import MatchItemDialog from "@/components/MatchItemDialog";
import PageBack from "@/components/PageBack";
import RecommendationGrid from "@/components/RecommendationGrid";
import DetailHero from "./DetailHero";
import MetadataBadges from "./components/MetadataBadges";
@@ -179,6 +180,7 @@ export default function MovieContent({ item }: { item: ItemDetail & { type: "mov
<div>
<DetailHero
title={title}
topNav={<PageBack />}
context="Movie"
studioLabel={firstStudio}
backdropUrl={item.backdrop_url}
@@ -8,6 +8,7 @@ import { useAuth } from "@/hooks/useAuth";
import CastCarousel from "@/components/CastCarousel";
import CrewList from "@/components/CrewList";
import EditMetadataDialog from "@/components/EditMetadataDialog";
import PageBack from "@/components/PageBack";
import DetailHero from "./DetailHero";
import MetadataBadges from "./components/MetadataBadges";
import ActionBar from "./components/ActionBar";
@@ -82,6 +83,7 @@ export default function SeasonContent({ item }: { item: ItemDetail & { type: "se
<DetailHero
variant="compact"
title={displayTitle}
topNav={<PageBack />}
context={breadcrumb}
backdropUrl={item.backdrop_url}
backdropThumbhash={item.backdrop_thumbhash}
@@ -14,6 +14,7 @@ import CastCarousel from "@/components/CastCarousel";
import CrewList from "@/components/CrewList";
import EditMetadataDialog from "@/components/EditMetadataDialog";
import MatchItemDialog from "@/components/MatchItemDialog";
import PageBack from "@/components/PageBack";
import RecommendationGrid from "@/components/RecommendationGrid";
import DetailHero from "./DetailHero";
import SeasonCarousel from "./SeasonCarousel";
@@ -116,6 +117,7 @@ export default function SeriesContent({ item }: { item: ItemDetail & { type: "se
<div>
<DetailHero
title={title}
topNav={<PageBack />}
context="Series"
studioLabel={firstNetwork}
backdropUrl={item.backdrop_url}
@@ -1,5 +1,5 @@
import { Link } from "react-router";
import { ChevronLeft, ChevronRight } from "lucide-react";
import { ChevronRight } from "lucide-react";
interface BreadcrumbSegment {
label: string;
@@ -13,44 +13,32 @@ interface DetailBreadcrumbProps {
export default function DetailBreadcrumb({ segments }: DetailBreadcrumbProps) {
if (segments.length === 0) return null;
const backSegment = [...segments].reverse().find((segment) => segment.href);
return (
<nav aria-label="Breadcrumb">
<div className="flex items-center gap-2.5">
{backSegment?.href && (
<Link
to={backSegment.href}
className="glass-subtle text-muted-foreground hover:text-foreground flex items-center justify-center rounded-full p-1.5 transition-colors"
>
<ChevronLeft className="size-5" />
</Link>
)}
<div className="flex items-center gap-1.5">
{segments.map((segment, i) => {
const isLast = i === segments.length - 1;
return (
<span key={i} className="flex items-center gap-1.5">
{i > 0 && <ChevronRight className="text-muted-foreground/40 size-4" />}
{segment.href && !isLast ? (
<Link
to={segment.href}
className="text-muted-foreground hover:text-foreground text-[13px] transition-colors"
>
{segment.label}
</Link>
) : (
<span
className="text-muted-foreground/60 text-[13px]"
{...(isLast ? { "aria-current": "page" as const } : {})}
>
{segment.label}
</span>
)}
</span>
);
})}
</div>
<div className="flex items-center gap-1.5">
{segments.map((segment, i) => {
const isLast = i === segments.length - 1;
return (
<span key={i} className="flex items-center gap-1.5">
{i > 0 && <ChevronRight className="text-muted-foreground/40 size-4" />}
{segment.href && !isLast ? (
<Link
to={segment.href}
className="text-muted-foreground hover:text-foreground text-[13px] transition-colors"
>
{segment.label}
</Link>
) : (
<span
className="text-muted-foreground/60 text-[13px]"
{...(isLast ? { "aria-current": "page" as const } : {})}
>
{segment.label}
</span>
)}
</span>
);
})}
</div>
</nav>
);
+3 -1
View File
@@ -8,6 +8,7 @@ import { createEmptyQueryDefinition } from "@/api/types";
import type { CatalogSearchState } from "@/pages/catalogSearchParams";
import EditPersonDialog from "@/components/EditPersonDialog";
import ItemGrid from "@/components/ItemGrid";
import PageBack from "@/components/PageBack";
import { Button } from "@/components/ui/button";
import { useCatalogWindow } from "@/hooks/queries/catalog";
import { personKeys } from "@/hooks/queries/keys";
@@ -76,7 +77,8 @@ export default function PersonDetail() {
return (
<div>
{/* Person Header */}
<section className="page-shell pt-8 pb-6 sm:pt-10 sm:pb-8">
<section className="page-shell relative pt-8 pb-6 sm:pt-10 sm:pb-8">
<PageBack />
<div className="flex flex-col gap-6 lg:flex-row lg:gap-8">
{/* Photo */}
<div className="shrink-0 self-start">
+3 -1
View File
@@ -1,4 +1,5 @@
import { useEffect, useState } from "react";
import PageBack from "@/components/PageBack";
import ProfileSectionRow from "@/components/ProfileSectionRow";
import RecipeGalleryModal from "@/components/RecipeGallery/RecipeGalleryModal";
import RecipeConfigDrawer from "@/components/RecipeGallery/RecipeConfigDrawer";
@@ -207,7 +208,8 @@ export default function ProfileCustomizeHome() {
}
return (
<div className="mx-auto max-w-3xl p-6">
<div className="relative mx-auto max-w-3xl p-6">
<PageBack />
<div className="flex items-center justify-between border-b border-white/10 pb-3">
<h1 className="text-base font-semibold">Customize home</h1>
<div className="flex gap-2">
+12 -19
View File
@@ -1,6 +1,7 @@
import { Link, useParams } from "react-router";
import { ArrowLeft, RefreshCw, Sparkles } from "lucide-react";
import { useParams } from "react-router";
import { RefreshCw, Sparkles } from "lucide-react";
import PageBack from "@/components/PageBack";
import SectionItemCard from "@/components/SectionItemCard";
import { Skeleton } from "@/components/ui/skeleton";
import { useRecommendationSection } from "@/hooks/queries/recommendations";
@@ -75,23 +76,15 @@ export default function RecommendationsSection() {
useDocumentTitle(title);
return (
<div className="space-y-6 px-4 pt-6 pb-12 sm:px-6 lg:px-10 xl:px-12">
<div className="space-y-3">
<Link
to="/recommendations"
className="text-muted-foreground hover:text-foreground inline-flex items-center gap-1.5 text-xs font-medium tracking-[0.16em] uppercase transition-colors"
>
<ArrowLeft className="h-3.5 w-3.5" />
Recommendations
</Link>
<div className="flex flex-col gap-1.5">
<h1 className="text-foreground text-2xl font-bold tracking-tight sm:text-3xl">{title}</h1>
{data && data.items.length > 0 && (
<p className="text-muted-foreground text-sm">
{data.items.length} {data.items.length === 1 ? "title" : "titles"}
</p>
)}
</div>
<div className="relative space-y-6 px-4 pt-6 pb-12 sm:px-6 lg:px-10 xl:px-12">
<PageBack />
<div className="flex flex-col gap-1.5">
<h1 className="text-foreground text-2xl font-bold tracking-tight sm:text-3xl">{title}</h1>
{data && data.items.length > 0 && (
<p className="text-muted-foreground text-sm">
{data.items.length} {data.items.length === 1 ? "title" : "titles"}
</p>
)}
</div>
{isLoading ? (
+6 -16
View File
@@ -1,5 +1,5 @@
import { Link, useParams, useSearchParams } from "react-router";
import { ArrowLeft } from "lucide-react";
import { useParams, useSearchParams } from "react-router";
import PageBack from "@/components/PageBack";
import RequestPosterCard from "@/components/RequestPosterCard";
import { Button } from "@/components/ui/button";
import {
@@ -83,29 +83,19 @@ export default function RequestBrowse({ kind }: RequestBrowseProps) {
if (browse.isError && (browse.error as { status?: number }).status === 404) {
return (
<div className="space-y-4 py-10 text-center">
<div className="relative space-y-4 py-10 text-center">
<PageBack />
<p className="text-foreground text-lg font-semibold">
{kind === "studio" ? "Studio" : kind === "network" ? "Network" : "Genre"} not found.
</p>
<Link
to="/requests"
className="text-muted-foreground hover:text-foreground text-sm underline"
>
Back to Requests
</Link>
</div>
);
}
return (
<div className="space-y-6 py-6 sm:py-8">
<div className="relative space-y-6 py-6 sm:py-8">
<PageBack />
<div className="space-y-4 px-4 sm:px-6 lg:px-10 xl:px-12">
<Link
to="/requests"
className="text-muted-foreground hover:text-foreground inline-flex items-center gap-1 text-sm"
>
<ArrowLeft className="h-4 w-4" /> Back to Requests
</Link>
<div className="flex flex-wrap items-center justify-between gap-4">
<div className="flex min-w-0 items-center gap-4">
<BrowseHeaderTile browse={browse.data} kind={kind} fallback={title} />
+6 -23
View File
@@ -1,7 +1,8 @@
import { useNavigate, useParams } from "react-router";
import { ArrowLeft, Check, Clock, Loader2, Plus, Star } from "lucide-react";
import { useParams } from "react-router";
import { Check, Clock, Loader2, Plus, Star } from "lucide-react";
import CastCarousel from "@/components/CastCarousel";
import MediaCarousel from "@/components/MediaCarousel";
import PageBack from "@/components/PageBack";
import RequestPosterCard from "@/components/RequestPosterCard";
import DetailHero from "@/pages/ItemDetail/DetailHero";
import { Button } from "@/components/ui/button";
@@ -23,7 +24,6 @@ import {
} from "@/lib/mediaRequests";
export default function RequestDetail() {
const navigate = useNavigate();
const params = useParams<{ mediaType: string; tmdbId: string }>();
const mediaType = (params.mediaType === "series" ? "series" : "movie") as "movie" | "series";
const tmdbID = Number(params.tmdbId) || 0;
@@ -39,17 +39,12 @@ export default function RequestDetail() {
if (detail.isError || !detail.data) {
return (
<div className="page-shell space-y-3 py-12 text-center">
<div className="page-shell relative space-y-3 py-12 text-center">
<PageBack />
<p className="text-foreground text-base font-semibold">Couldn't load this title.</p>
<p className="text-muted-foreground text-sm">
The TMDB record may be temporarily unavailable.
</p>
<div className="pt-4">
<Button variant="outline" size="sm" onClick={() => navigate(-1)}>
<ArrowLeft className="h-4 w-4" />
Back
</Button>
</div>
</div>
);
}
@@ -63,6 +58,7 @@ export default function RequestDetail() {
<div>
<DetailHero
title={item.title}
topNav={<PageBack />}
context={<RequestContext mediaType={mediaType} />}
studioLabel={studioLabel}
backdropUrl={backdropUrl}
@@ -79,7 +75,6 @@ export default function RequestDetail() {
createRequest.isPending && createRequest.variables?.tmdb_id === item.tmdb_id
}
onRequest={() => createRequest.mutate(requestInputFromMediaResult(item))}
onBack={() => navigate(-1)}
/>
}
/>
@@ -198,12 +193,10 @@ function RequestActions({
item,
isSubmitting,
onRequest,
onBack,
}: {
item: RequestMediaDetail;
isSubmitting: boolean;
onRequest: () => void;
onBack: () => void;
}) {
const requestable = item.request.requestable;
const statusLabel = item.request.status ? formatRequestStatus(item.request.status) : null;
@@ -213,16 +206,6 @@ function RequestActions({
return (
<div className="flex flex-wrap items-center gap-2">
<Button
variant="ghost"
size="sm"
onClick={onBack}
className="text-muted-foreground hover:text-foreground"
>
<ArrowLeft className="h-4 w-4" />
Back
</Button>
{requestable ? (
<Button
onClick={onRequest}
+2 -3
View File
@@ -20,15 +20,14 @@ describe("SettingsLayout", () => {
});
});
it("includes a back link to the main app shell", () => {
it("includes a PageBack control at the top of the page", () => {
const markup = renderToStaticMarkup(
<MemoryRouter initialEntries={["/settings/playback"]}>
<SettingsLayout />
</MemoryRouter>,
);
expect(markup).toContain('href="/"');
expect(markup).toContain(">Back<");
expect(markup).toContain('aria-label="Go back"');
});
it("does not include a plugins section in personal settings", () => {
+3 -9
View File
@@ -1,6 +1,5 @@
import { Link, Outlet, useLocation } from "react-router";
import {
ArrowLeft,
Play,
Library,
Clock,
@@ -16,6 +15,7 @@ import {
} from "lucide-react";
// Sparkles is used by the Personalization nav entry below.
import type { LucideIcon } from "lucide-react";
import PageBack from "@/components/PageBack";
import { useDocumentTitle } from "@/hooks/useDocumentTitle";
import { useAuth } from "@/hooks/useAuth";
import { resolveSettingsDocumentTitle } from "@/lib/documentTitle";
@@ -155,7 +155,8 @@ export default function SettingsLayout() {
return (
<div className="min-h-[100dvh]">
<main className="page-shell-wide flex min-h-[100dvh] flex-col py-4 sm:py-6">
<main className="page-shell-wide relative flex min-h-[100dvh] flex-col py-4 sm:py-6">
<PageBack />
<div className="page-header gap-5">
<div className="min-w-0 space-y-3">
<h1 className="page-title text-[clamp(2rem,4vw,3rem)]">Settings</h1>
@@ -163,13 +164,6 @@ export default function SettingsLayout() {
Manage your playback preferences, libraries, and display options.
</p>
</div>
<Link
to="/"
className="surface-panel-subtle text-muted-foreground hover:text-foreground inline-flex shrink-0 items-center gap-2 rounded-[1.1rem] px-3 py-2 text-sm font-medium transition-colors"
>
<ArrowLeft className="h-4 w-4" />
<span className="hidden sm:inline">Back</span>
</Link>
</div>
{/* Mobile: horizontal scrolling tab bar */}
+15 -31
View File
@@ -1,5 +1,4 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { Link } from "react-router";
import { ArrowLeft, ArrowRight, Check } from "lucide-react";
import type {
@@ -15,6 +14,7 @@ import { normalizeQueryDefinition } from "@/api/types";
import CatalogFiltersPanel from "@/components/catalog/CatalogFiltersPanel";
import { ImageUploadField } from "@/components/ImageUploadField";
import ItemGrid from "@/components/ItemGrid";
import PageBack from "@/components/PageBack";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
@@ -118,12 +118,12 @@ export default function SmartCollectionWizard(wizard: SmartCollectionWizardProps
}, []);
const headerTitle = isEdit ? draft.title || "Edit Collection" : "New Collection";
const backHref = wizard.mode === "user" ? "/collections" : adminBackHref(wizard);
const adminLibraries = wizard.mode === "admin" ? wizard.libraries : [];
return (
<div className="page-shell-wide space-y-6 py-4 sm:py-6">
<WizardHeader backHref={backHref} title={headerTitle} step={step} isEdit={isEdit} />
<div className="page-shell-wide relative space-y-6 py-4 sm:py-6">
<PageBack />
<WizardHeader title={headerTitle} step={step} isEdit={isEdit} />
{step === 1 ? (
<Step1FiltersAndPreview
@@ -171,44 +171,28 @@ function smartUserDraft(collection: Collection | null): CollectionBuilderValue {
: createCollectionBuilderValue({ ...value, collection_type: "smart" });
}
function adminBackHref(props: AdminModeProps): string {
return props.initialLibraryId
? `/admin/collections?libraryId=${props.initialLibraryId}`
: "/admin/collections";
}
function WizardHeader({
backHref,
title,
step,
isEdit,
}: {
backHref: string;
title: string;
step: WizardStep;
isEdit: boolean;
}) {
return (
<div className="space-y-3">
<Button asChild variant="ghost" className="w-fit px-0">
<Link to={backHref}>
<ArrowLeft className="mr-2 h-4 w-4" />
Back to Collections
</Link>
</Button>
<div className="flex flex-wrap items-end justify-between gap-4">
<div>
<h1 className="page-title text-[clamp(1.75rem,3vw,2.5rem)]">{title}</h1>
<p className="page-subtitle mt-1 text-sm">
{step === 1
? "Tune the filters until the cards below show the collection you want."
: isEdit
? "Update naming, artwork, and sharing for this collection."
: "Give your new collection a name, artwork, and sharing rules."}
</p>
</div>
<StepIndicator step={step} />
<div className="flex flex-wrap items-end justify-between gap-4">
<div>
<h1 className="page-title text-[clamp(1.75rem,3vw,2.5rem)]">{title}</h1>
<p className="page-subtitle mt-1 text-sm">
{step === 1
? "Tune the filters until the cards below show the collection you want."
: isEdit
? "Update naming, artwork, and sharing for this collection."
: "Give your new collection a name, artwork, and sharing rules."}
</p>
</div>
<StepIndicator step={step} />
</div>
);
}