From 436f3d7fada2dce6c041915e5919ed786e1b0f6e Mon Sep 17 00:00:00 2001
From: Jake Jarvis
Date: Thu, 5 Mar 2026 19:15:52 -0500
Subject: [PATCH] Fix Select trigger display values and add TMDB attribution
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Base UI Select.Value renders raw values by default — add children render
functions to map values to human-readable labels (e.g. "this_month" →
"This Month", "0" → "unlimited"). Switch inline select underlines from
border-bottom to text-decoration for proper baseline alignment. Add TMDB
attribution with logo and disclaimer to settings footer.
Co-Authored-By: Claude Opus 4.6
---
app/(auth)/setup/page.tsx | 21 +-
.../dashboard/_components/stats-display.tsx | 57 +++---
.../filterable-title-row.tsx | 12 +-
.../explore/{ => _components}/hero-banner.tsx | 0
.../explore/{ => _components}/title-row.tsx | 0
app/(pages)/explore/page.tsx | 6 +-
.../[id]/_components/filmography-grid.tsx | 49 +++--
.../people/[id]/_components/person-hero.tsx | 5 +-
.../settings/_components/account-section.tsx | 5 +-
.../_components/backup-restore-section.tsx | 2 +-
.../_components/backup-schedule-section.tsx | 192 +++++++++---------
.../settings/_components/backup-section.tsx | 2 +-
.../settings/_components/webhook-card.tsx | 86 ++++----
app/(pages)/settings/page.tsx | 15 ++
.../titles/[id]/_components/title-actions.tsx | 12 +-
.../titles/[id]/_components/title-hero.tsx | 12 +-
.../titles/[id]/_components/title-seasons.tsx | 30 +--
components/auth-form.tsx | 103 +++++-----
components/command-palette.tsx | 2 +-
components/nav-bar.tsx | 6 +-
components/ui/native-select.tsx | 55 -----
21 files changed, 351 insertions(+), 321 deletions(-)
rename app/(pages)/explore/{ => _components}/filterable-title-row.tsx (94%)
rename app/(pages)/explore/{ => _components}/hero-banner.tsx (100%)
rename app/(pages)/explore/{ => _components}/title-row.tsx (100%)
delete mode 100644 components/ui/native-select.tsx
diff --git a/app/(auth)/setup/page.tsx b/app/(auth)/setup/page.tsx
index 48a710c..5f79e5c 100644
--- a/app/(auth)/setup/page.tsx
+++ b/app/(auth)/setup/page.tsx
@@ -9,6 +9,8 @@ import {
import { motion } from "motion/react";
import { useRouter } from "next/navigation";
import { useCallback, useEffect, useState } from "react";
+import { Button } from "@/components/ui/button";
+import { Spinner } from "@/components/ui/spinner";
const steps = [
{
@@ -181,10 +183,11 @@ export default function SetupPage() {
{snippet.label}
-
+
{snippet.code}
@@ -244,21 +247,21 @@ export default function SetupPage() {
Click the button to verify your configuration
-
+
)}
diff --git a/app/(pages)/dashboard/_components/stats-display.tsx b/app/(pages)/dashboard/_components/stats-display.tsx
index 517a00d..c8e9347 100644
--- a/app/(pages)/dashboard/_components/stats-display.tsx
+++ b/app/(pages)/dashboard/_components/stats-display.tsx
@@ -2,19 +2,18 @@
import {
IconCheck,
- IconChevronDown,
IconLibrary,
IconMovie,
IconPlayerPlay,
} from "@tabler/icons-react";
import { useAtom, useAtomValue } from "jotai";
import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuRadioGroup,
- DropdownMenuRadioItem,
- DropdownMenuTrigger,
-} from "@/components/ui/dropdown-menu";
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select";
import {
episodePeriodAtom,
episodeStatsAtom,
@@ -83,6 +82,9 @@ function StatCard({
);
}
+const inlineTriggerClass =
+ "h-auto w-auto gap-0.5 rounded-none border-0 bg-transparent p-0 [font-size:inherit] [line-height:inherit] shadow-none underline decoration-dotted decoration-muted-foreground/50 underline-offset-4 hover:bg-transparent hover:text-foreground hover:decoration-foreground/50 focus-visible:ring-0 focus-visible:decoration-solid focus-visible:decoration-foreground dark:bg-transparent dark:hover:bg-transparent";
+
function PeriodSelector({
noun,
period,
@@ -95,24 +97,29 @@ function PeriodSelector({
return (
{noun}{" "}
-
-
- {periodLabels[period]}
-
-
-
- onPeriodChange(v as TimePeriod)}
- >
- {periods.map((p) => (
-
- {periodLabels[p]}
-
- ))}
-
-
-
+
);
}
diff --git a/app/(pages)/explore/filterable-title-row.tsx b/app/(pages)/explore/_components/filterable-title-row.tsx
similarity index 94%
rename from app/(pages)/explore/filterable-title-row.tsx
rename to app/(pages)/explore/_components/filterable-title-row.tsx
index bd51d9a..31e6b47 100644
--- a/app/(pages)/explore/filterable-title-row.tsx
+++ b/app/(pages)/explore/_components/filterable-title-row.tsx
@@ -5,6 +5,7 @@ import { createStore, Provider, useAtom, useAtomValue } from "jotai";
import { useState } from "react";
import { TitleCardSkeleton } from "@/components/skeletons";
import { ExploreTitleCard } from "@/components/title-card";
+import { Button } from "@/components/ui/button";
import {
Carousel,
CarouselContent,
@@ -114,18 +115,19 @@ function FilterableTitleRowInner({
{/* Genre chips */}
{genres.map((genre) => (
-
+
))}
diff --git a/app/(pages)/explore/hero-banner.tsx b/app/(pages)/explore/_components/hero-banner.tsx
similarity index 100%
rename from app/(pages)/explore/hero-banner.tsx
rename to app/(pages)/explore/_components/hero-banner.tsx
diff --git a/app/(pages)/explore/title-row.tsx b/app/(pages)/explore/_components/title-row.tsx
similarity index 100%
rename from app/(pages)/explore/title-row.tsx
rename to app/(pages)/explore/_components/title-row.tsx
diff --git a/app/(pages)/explore/page.tsx b/app/(pages)/explore/page.tsx
index 4a067c7..4ffbf4a 100644
--- a/app/(pages)/explore/page.tsx
+++ b/app/(pages)/explore/page.tsx
@@ -6,9 +6,9 @@ import {
} from "@/lib/services/tracking";
import { getGenres, getPopular, getTrending } from "@/lib/tmdb/client";
import { tmdbImageUrl } from "@/lib/tmdb/image";
-import { FilterableTitleRow } from "./filterable-title-row";
-import { HeroBanner } from "./hero-banner";
-import { TitleRow } from "./title-row";
+import { FilterableTitleRow } from "./_components/filterable-title-row";
+import { HeroBanner } from "./_components/hero-banner";
+import { TitleRow } from "./_components/title-row";
function mapResults(
results: {
diff --git a/app/(pages)/people/[id]/_components/filmography-grid.tsx b/app/(pages)/people/[id]/_components/filmography-grid.tsx
index 3efef65..f6f6a7c 100644
--- a/app/(pages)/people/[id]/_components/filmography-grid.tsx
+++ b/app/(pages)/people/[id]/_components/filmography-grid.tsx
@@ -4,6 +4,14 @@ import { IconMovie } from "@tabler/icons-react";
import Image from "next/image";
import Link from "next/link";
import { useMemo, useState } from "react";
+import { Button } from "@/components/ui/button";
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select";
import type { PersonCredit } from "@/lib/types/title";
type Filter = "all" | "movie" | "tv";
@@ -62,31 +70,44 @@ export function FilmographyGrid({ credits }: FilmographyGridProps) {
-
+
+
+ {(value: string | null) =>
+ value === "newest"
+ ? "Newest"
+ : value === "rating"
+ ? "Rating"
+ : null
+ }
+
+
+
+ Newest
+ Rating
+
+
{filters.map((f) => (
-
+
))}
diff --git a/app/(pages)/people/[id]/_components/person-hero.tsx b/app/(pages)/people/[id]/_components/person-hero.tsx
index 83e03a0..2d19112 100644
--- a/app/(pages)/people/[id]/_components/person-hero.tsx
+++ b/app/(pages)/people/[id]/_components/person-hero.tsx
@@ -3,6 +3,7 @@
import { IconCalendar, IconMapPin } from "@tabler/icons-react";
import Image from "next/image";
import { useState } from "react";
+import { Badge } from "@/components/ui/badge";
import type { ResolvedPerson } from "@/lib/types/title";
interface PersonHeroProps {
@@ -53,9 +54,9 @@ export function PersonHero({ person }: PersonHeroProps) {
{person.knownForDepartment && (
-
+
{person.knownForDepartment}
-
+
)}
diff --git a/app/(pages)/settings/_components/account-section.tsx b/app/(pages)/settings/_components/account-section.tsx
index b5bc435..f99b461 100644
--- a/app/(pages)/settings/_components/account-section.tsx
+++ b/app/(pages)/settings/_components/account-section.tsx
@@ -2,6 +2,7 @@
import { IconLogout, IconUser } from "@tabler/icons-react";
import { useRouter } from "next/navigation";
+import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Card,
@@ -41,9 +42,9 @@ export function AccountSection({
{user.name}
{user.role === "admin" && (
-
+
Admin
-
+
)}
{user.email}
diff --git a/app/(pages)/settings/_components/backup-restore-section.tsx b/app/(pages)/settings/_components/backup-restore-section.tsx
index ca827bf..5d4e9d4 100644
--- a/app/(pages)/settings/_components/backup-restore-section.tsx
+++ b/app/(pages)/settings/_components/backup-restore-section.tsx
@@ -119,7 +119,7 @@ export function BackupRestoreSection() {
disabled={restoring}
>
{restoring ? : }
- {restoring ? "Restoring\u2026" : "Upload"}
+ {restoring ? "Restoring…" : "Upload"}
diff --git a/app/(pages)/settings/_components/backup-schedule-section.tsx b/app/(pages)/settings/_components/backup-schedule-section.tsx
index 072865f..43d339c 100644
--- a/app/(pages)/settings/_components/backup-schedule-section.tsx
+++ b/app/(pages)/settings/_components/backup-schedule-section.tsx
@@ -1,19 +1,20 @@
"use client";
-import { IconCalendarWeek, IconChevronDown } from "@tabler/icons-react";
+import { IconCalendarWeek } from "@tabler/icons-react";
import { format, formatDistanceToNow } from "date-fns";
import { useAtomValue } from "jotai";
import { useHydrateAtoms } from "jotai/utils";
import { AnimatePresence, motion } from "motion/react";
import { Button } from "@/components/ui/button";
+import { ButtonGroup } from "@/components/ui/button-group";
import { CardContent, CardDescription, CardTitle } from "@/components/ui/card";
import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuRadioGroup,
- DropdownMenuRadioItem,
- DropdownMenuTrigger,
-} from "@/components/ui/dropdown-menu";
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import {
backupScheduleAtom,
@@ -161,29 +162,33 @@ function BackupScheduleInner() {
{formatNextBackup(frequency, time, dow)}.
{" "}
Keeping{" "}
-
-
- {maxRetention === 0
- ? "unlimited"
- : `last ${maxRetention}`}
-
-
-
- changeMaxRetention(Number(v))}
- >
- {[3, 5, 7, 14, 30, 0].map((n) => (
-
- {n === 0 ? "unlimited" : n}
-
- ))}
-
-
- {" "}
+ {" "}
backups.
) : (
@@ -217,7 +222,7 @@ function BackupScheduleInner() {
Frequency
-
+
{FREQUENCY_OPTIONS.map((opt) => (
+
{/* Day of week — shown for 7d only */}
@@ -250,32 +255,33 @@ function BackupScheduleInner() {
Day:{" "}
-
-
- {DAYS_OF_WEEK[dow]}
-
-
-
-
- changeSchedule(frequency, time, Number(v))
+
-
-
+
+
+
+ {DAYS_OF_WEEK.map((day, i) => (
+
+ {day}
+
+ ))}
+
+
)}
@@ -293,41 +299,43 @@ function BackupScheduleInner() {
{frequency === "12h" ? "Starting at" : "Time:"}{" "}
-
-
- {format(
- new Date(
- 2000,
- 0,
- 1,
- ...(time.split(":").map(Number) as [
- number,
- number,
- ]),
- ),
- "h:mm a",
- )}
-
-
-
- changeSchedule(frequency, v)}
- >
- {HOURS.map((h) => {
- const val = `${String(h).padStart(2, "0")}:00`;
- return (
-
- {format(new Date(2000, 0, 1, h, 0), "h:mm a")}
-
- );
- })}
-
-
-
+
)}
diff --git a/app/(pages)/settings/_components/backup-section.tsx b/app/(pages)/settings/_components/backup-section.tsx
index 1830736..3807116 100644
--- a/app/(pages)/settings/_components/backup-section.tsx
+++ b/app/(pages)/settings/_components/backup-section.tsx
@@ -119,7 +119,7 @@ export function BackupSection({
) : (
)}
- {creating ? "Creating\u2026" : "New backup"}
+ {creating ? "Creating…" : "New backup"}
diff --git a/app/(pages)/settings/_components/webhook-card.tsx b/app/(pages)/settings/_components/webhook-card.tsx
index 73a98e4..0ed310d 100644
--- a/app/(pages)/settings/_components/webhook-card.tsx
+++ b/app/(pages)/settings/_components/webhook-card.tsx
@@ -13,6 +13,7 @@ import {
import { formatDistanceToNow } from "date-fns";
import { AnimatePresence, motion } from "motion/react";
import { useState } from "react";
+import { Alert, AlertDescription } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import {
Card,
@@ -25,7 +26,13 @@ import {
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
-import { Input } from "@/components/ui/input";
+import {
+ InputGroup,
+ InputGroupAddon,
+ InputGroupButton,
+ InputGroupInput,
+} from "@/components/ui/input-group";
+import { Label } from "@/components/ui/label";
import {
Tooltip,
TooltipContent,
@@ -117,12 +124,9 @@ export function WebhookCard({
{isPlex && (
-
-
-
+
+
+
Requires an active{" "}
{" "}
subscription.
-
-
+
+
)}
{isEmby && (
-
-
-
+
+
+
Requires{" "}
Emby Server 4.7.9+
@@ -166,8 +167,8 @@ export function WebhookCard({
/>
{" "}
license.
-
-
+
+
)}
{!connection ? (
@@ -177,7 +178,7 @@ export function WebhookCard({
size="lg"
className="w-full"
>
- {connecting ? "Connecting\u2026" : `Connect ${label}`}
+ {connecting ? "Connecting…" : `Connect ${label}`}
) : (
@@ -189,38 +190,39 @@ export function WebhookCard({
className="space-y-3 overflow-hidden"
>
-
-
-
+
+
-
-
- }
- >
- {copied ? (
-
- ) : (
-
- )}
-
- Copy URL
-
-
+
+
+
+ }
+ >
+ {copied ? (
+
+ ) : (
+
+ )}
+
+ Copy URL
+
+
+
diff --git a/app/(pages)/settings/page.tsx b/app/(pages)/settings/page.tsx
index 53a6e12..88bc25c 100644
--- a/app/(pages)/settings/page.tsx
+++ b/app/(pages)/settings/page.tsx
@@ -5,6 +5,7 @@ import {
} from "@tabler/icons-react";
import { desc, eq } from "drizzle-orm";
import { redirect } from "next/navigation";
+import { TmdbLogo } from "@/components/tmdb-logo";
import { Card } from "@/components/ui/card";
import { getSession } from "@/lib/auth/session";
import { db } from "@/lib/db/client";
@@ -145,6 +146,20 @@ export default async function SettingsPage() {
)}
+
+
+
+
+
+ This product uses the TMDB API but is not endorsed or certified by
+ TMDB.
+
+
}
>
diff --git a/app/(pages)/titles/[id]/_components/title-actions.tsx b/app/(pages)/titles/[id]/_components/title-actions.tsx
index 67fcdbb..06a5f69 100644
--- a/app/(pages)/titles/[id]/_components/title-actions.tsx
+++ b/app/(pages)/titles/[id]/_components/title-actions.tsx
@@ -2,6 +2,8 @@
import { IconCheck } from "@tabler/icons-react";
import { useAtomValue } from "jotai";
+import { Button } from "@/components/ui/button";
+import { Separator } from "@/components/ui/separator";
import {
titleTypeAtom,
userRatingAtom,
@@ -25,16 +27,16 @@ export function TitleActions() {
onChange={handleStatusChange}
/>
{titleType === "movie" && (
-
+
)}
-
+
);
diff --git a/app/(pages)/titles/[id]/_components/title-hero.tsx b/app/(pages)/titles/[id]/_components/title-hero.tsx
index cc24fbf..7e3a34a 100644
--- a/app/(pages)/titles/[id]/_components/title-hero.tsx
+++ b/app/(pages)/titles/[id]/_components/title-hero.tsx
@@ -1,6 +1,7 @@
import Image from "next/image";
import type { ReactNode } from "react";
import { TmdbLogo } from "@/components/tmdb-logo";
+import { Badge } from "@/components/ui/badge";
import type { ColorPalette, ResolvedTitle } from "@/lib/types/title";
import { TrailerDialog } from "./trailer-dialog";
@@ -100,9 +101,9 @@ export function TitleHero({
{title.title}
-
+
{title.type}
-
+
{year &&
{year}}
{title.voteAverage != null && title.voteAverage > 0 && (
@@ -115,9 +116,12 @@ export function TitleHero({
)}
{title.status && (
-
+
{title.status}
-
+
)}
-
Seasons
+ Episodes
{userStatus && userStatus !== "completed" && (
Mark All Watched
-
+
}
/>
@@ -151,28 +153,30 @@ export function TitleSeasons({
>
)}
{totalCount > 0 && watchedCount < totalCount && (
-
+
)}
{totalCount > 0 && watchedCount === totalCount && (
-
+
)}
{totalCount > 0 && (
diff --git a/components/auth-form.tsx b/components/auth-form.tsx
index be94a69..413fcef 100644
--- a/components/auth-form.tsx
+++ b/components/auth-form.tsx
@@ -6,6 +6,10 @@ import Link from "next/link";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { SofaLogo } from "@/components/sofa-logo";
+import { Alert, AlertDescription } from "@/components/ui/alert";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
import { authClient, signIn, signUp } from "@/lib/auth/client";
export interface AuthConfig {
@@ -15,6 +19,9 @@ export interface AuthConfig {
registrationOpen?: boolean;
}
+const authInputClass =
+ "h-11 rounded-lg border-border/50 bg-background/50 px-4 py-0 placeholder:text-muted-foreground/50 focus-visible:border-primary/40 focus-visible:ring-ring md:text-sm";
+
const fieldVariants = {
hidden: { opacity: 0, y: 10 },
visible: {
@@ -117,19 +124,20 @@ export function AuthForm({
visible: { transition: { staggerChildren: 0.08 } },
}}
>
-
-
- {oidcLoading
- ? "Redirecting\u2026"
- : `Sign in with ${authConfig?.oidcProviderName || "SSO"}`}
-
+
+
+
)}
@@ -154,33 +162,33 @@ export function AuthForm({
>
{isRegister && (
-
-
+ setName(e.target.value)}
- className="flex h-11 w-full rounded-lg border border-border/50 bg-background/50 px-4 text-sm transition-colors placeholder:text-muted-foreground/50 focus-visible:border-primary/40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
- placeholder="Your name\u2026"
+ className={authInputClass}
+ placeholder="Your name…"
/>
)}
-
-
+ setEmail(e.target.value)}
- className="flex h-11 w-full rounded-lg border border-border/50 bg-background/50 px-4 text-sm transition-colors placeholder:text-muted-foreground/50 focus-visible:border-primary/40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
+ className={authInputClass}
placeholder="wwhite@graymatter.biz"
/>
-
-
+ setPassword(e.target.value)}
- className="flex h-11 w-full rounded-lg border border-border/50 bg-background/50 px-4 text-sm transition-colors placeholder:text-muted-foreground/50 focus-visible:border-primary/40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
- placeholder="Min 8 characters\u2026"
+ className={authInputClass}
+ placeholder="Min 8 characters…"
/>
-
- {loading
- ? "Loading\u2026"
- : isRegister
- ? "Create account"
- : "Sign in"}
-
+
+
+
)}
{error && (
- {error}
+
+
+ {error}
+
+
)}
diff --git a/components/command-palette.tsx b/components/command-palette.tsx
index 2c7ce34..ca1de96 100644
--- a/components/command-palette.tsx
+++ b/components/command-palette.tsx
@@ -152,7 +152,7 @@ export function CommandPalette() {
>
diff --git a/components/nav-bar.tsx b/components/nav-bar.tsx
index 15deebc..117aec0 100644
--- a/components/nav-bar.tsx
+++ b/components/nav-bar.tsx
@@ -7,6 +7,7 @@ import Link from "next/link";
import { usePathname, useRouter } from "next/navigation";
import { SofaLogo } from "@/components/sofa-logo";
import { Kbd } from "@/components/ui/kbd";
+import { Separator } from "@/components/ui/separator";
import {
Tooltip,
TooltipContent,
@@ -86,7 +87,10 @@ export function NavBar() {
Search…
⌘K
-
+
}
diff --git a/components/ui/native-select.tsx b/components/ui/native-select.tsx
deleted file mode 100644
index 70e2477..0000000
--- a/components/ui/native-select.tsx
+++ /dev/null
@@ -1,55 +0,0 @@
-import { IconSelector } from "@tabler/icons-react";
-import type * as React from "react";
-import { cn } from "@/lib/utils";
-
-type NativeSelectProps = Omit, "size"> & {
- size?: "sm" | "default";
-};
-
-function NativeSelect({
- className,
- size = "default",
- ...props
-}: NativeSelectProps) {
- return (
-
-
-
-
- );
-}
-
-function NativeSelectOption({ ...props }: React.ComponentProps<"option">) {
- return ;
-}
-
-function NativeSelectOptGroup({
- className,
- ...props
-}: React.ComponentProps<"optgroup">) {
- return (
-
- );
-}
-
-export { NativeSelect, NativeSelectOptGroup, NativeSelectOption };