Polish settings UI, add tooltips, replace timeAgo with date-fns

Clean up settings section design: consolidate backup cards, add
hover-reveal actions with tooltips, improve icon sizing across
components, and replace custom timeAgo helper with date-fns
formatDistanceToNow in webhook card.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-03 20:23:52 -05:00
co-authored by Claude Opus 4.6
parent b90129a981
commit 7d69262d0e
28 changed files with 128 additions and 143 deletions
+1 -1
View File
@@ -51,7 +51,7 @@ export default function RegisterPage() {
}} }}
> >
<div className="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-primary/10"> <div className="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-primary/10">
<IconLock size={24} className="text-primary" /> <IconLock className="size-6 text-primary" />
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<h1 className="font-display text-xl tracking-tight"> <h1 className="font-display text-xl tracking-tight">
@@ -48,7 +48,7 @@ export function ContinueWatchingCard({
/> />
) : ( ) : (
<div className="flex h-full items-center justify-center bg-gradient-to-br from-card via-secondary to-muted"> <div className="flex h-full items-center justify-center bg-gradient-to-br from-card via-secondary to-muted">
<IconPlayerPlay size={32} className="text-muted-foreground/30" /> <IconPlayerPlay className="size-8 text-muted-foreground/30" />
</div> </div>
)} )}
<div className="absolute inset-0 bg-gradient-to-t from-black/70 via-black/20 to-transparent" /> <div className="absolute inset-0 bg-gradient-to-t from-black/70 via-black/20 to-transparent" />
@@ -76,7 +76,7 @@ export function ContinueWatchingCard({
</p> </p>
</div> </div>
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-primary/10 text-primary transition-colors group-hover:bg-primary group-hover:text-primary-foreground"> <div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-primary/10 text-primary transition-colors group-hover:bg-primary group-hover:text-primary-foreground">
<IconPlayerPlay size={14} /> <IconPlayerPlay className="size-3.5" />
</div> </div>
</div> </div>
{progress > 0 && ( {progress > 0 && (
@@ -34,7 +34,7 @@ export async function ContinueWatchingSection({ userId }: { userId: string }) {
return ( return (
<FeedSection <FeedSection
title="Continue Watching" title="Continue Watching"
icon={<IconPlayerPlay size={20} className="text-primary" />} icon={<IconPlayerPlay className="size-5 text-primary" />}
> >
<ContinueWatchingList items={items} /> <ContinueWatchingList items={items} />
</FeedSection> </FeedSection>
@@ -21,7 +21,7 @@ export async function LibrarySection({ userId }: { userId: string }) {
return ( return (
<FeedSection <FeedSection
title="In Your Library" title="In Your Library"
icon={<IconSparkles size={20} className="text-primary" />} icon={<IconSparkles className="size-5 text-primary" />}
> >
<TitleGrid items={items} /> <TitleGrid items={items} />
</FeedSection> </FeedSection>
@@ -24,7 +24,7 @@ export async function RecommendationsSection({ userId }: { userId: string }) {
return ( return (
<FeedSection <FeedSection
title="Recommended for You" title="Recommended for You"
icon={<IconSparkles size={20} className="text-primary" />} icon={<IconSparkles className="size-5 text-primary" />}
> >
<TitleGrid items={items} /> <TitleGrid items={items} />
</FeedSection> </FeedSection>
@@ -63,7 +63,7 @@ export function StatsDisplay({ stats }: { stats: DashboardStats }) {
<div <div
className={`flex h-6 w-6 items-center justify-center rounded-md ${def.bgColor}`} className={`flex h-6 w-6 items-center justify-center rounded-md ${def.bgColor}`}
> >
<Icon size={13} className={def.color} /> <Icon className={`size-[13px] ${def.color}`} />
</div> </div>
<span className="text-[10px] font-medium uppercase tracking-wider text-muted-foreground"> <span className="text-[10px] font-medium uppercase tracking-wider text-muted-foreground">
{def.label} {def.label}
@@ -18,7 +18,7 @@ export async function StatsSection({ userId }: { userId: string }) {
{isEmpty && ( {isEmpty && (
<div className="flex flex-col items-center gap-4 rounded-xl border border-dashed border-border/50 py-16 text-center"> <div className="flex flex-col items-center gap-4 rounded-xl border border-dashed border-border/50 py-16 text-center">
<div className="animate-gentle-float rounded-full bg-primary/10 p-4"> <div className="animate-gentle-float rounded-full bg-primary/10 p-4">
<IconDeviceTv size={32} className="text-primary" /> <IconDeviceTv className="size-8 text-primary" />
</div> </div>
<div className="space-y-1"> <div className="space-y-1">
<p className="font-medium">Your library is empty</p> <p className="font-medium">Your library is empty</p>
+2 -2
View File
@@ -68,7 +68,7 @@ export function HeroBanner({
</span> </span>
{voteAverage > 0 && ( {voteAverage > 0 && (
<span className="flex items-center gap-1 text-sm text-primary"> <span className="flex items-center gap-1 text-sm text-primary">
<IconStar size={14} className="fill-primary" /> <IconStar className="size-3.5 fill-primary" />
{voteAverage.toFixed(1)} {voteAverage.toFixed(1)}
</span> </span>
)} )}
@@ -88,7 +88,7 @@ export function HeroBanner({
href={href} href={href}
className="mt-4 inline-flex h-9 items-center gap-2 rounded-lg bg-primary px-4 text-sm font-medium text-primary-foreground transition-all hover:shadow-md hover:shadow-primary/20" className="mt-4 inline-flex h-9 items-center gap-2 rounded-lg bg-primary px-4 text-sm font-medium text-primary-foreground transition-all hover:shadow-md hover:shadow-primary/20"
> >
<IconPlus size={16} /> <IconPlus className="size-4" />
Add to Library Add to Library
</Link> </Link>
</motion.div> </motion.div>
+3 -3
View File
@@ -66,19 +66,19 @@ export default async function ExplorePage() {
<TitleRow <TitleRow
heading="Trending Today" heading="Trending Today"
icon={<IconFlame size={20} className="text-primary" />} icon={<IconFlame className="size-5 text-primary" />}
items={trendingItems.slice(0, 20)} items={trendingItems.slice(0, 20)}
/> />
<TitleRow <TitleRow
heading="Popular Movies" heading="Popular Movies"
icon={<IconMovie size={20} className="text-primary" />} icon={<IconMovie className="size-5 text-primary" />}
items={popularMovieItems.slice(0, 20)} items={popularMovieItems.slice(0, 20)}
/> />
<TitleRow <TitleRow
heading="Popular TV Shows" heading="Popular TV Shows"
icon={<IconDeviceTv size={20} className="text-primary" />} icon={<IconDeviceTv className="size-5 text-primary" />}
items={popularTvItems.slice(0, 20)} items={popularTvItems.slice(0, 20)}
/> />
@@ -27,7 +27,7 @@ export function AccountSection({
return ( return (
<div> <div>
<div className="mb-3 flex items-center gap-2"> <div className="mb-3 flex items-center gap-2">
<IconUser size={16} className="text-muted-foreground" /> <IconUser className="size-4 text-muted-foreground" />
<h2 className="text-xs font-medium uppercase tracking-wider text-muted-foreground"> <h2 className="text-xs font-medium uppercase tracking-wider text-muted-foreground">
Account Account
</h2> </h2>
@@ -59,7 +59,7 @@ export function AccountSection({
router.refresh(); router.refresh();
}} }}
> >
<IconLogout size={14} /> <IconLogout />
Sign out Sign out
</Button> </Button>
</CardContent> </CardContent>
@@ -27,6 +27,11 @@ import { Button } from "@/components/ui/button";
import { CardContent, CardDescription, CardTitle } from "@/components/ui/card"; import { CardContent, CardDescription, CardTitle } from "@/components/ui/card";
import { Spinner } from "@/components/ui/spinner"; import { Spinner } from "@/components/ui/spinner";
import { Switch } from "@/components/ui/switch"; import { Switch } from "@/components/ui/switch";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import type { BackupInfo } from "@/lib/services/backup"; import type { BackupInfo } from "@/lib/services/backup";
import { import {
createBackupAction, createBackupAction,
@@ -208,38 +213,50 @@ export function BackupSection({
)} )}
</div> </div>
<div className="flex shrink-0 items-center gap-0.5 opacity-0 transition-opacity group-hover:opacity-100"> <div className="flex shrink-0 items-center gap-0.5 opacity-0 transition-opacity group-hover:opacity-100">
<Button <Tooltip>
variant="ghost" <TooltipTrigger
size="icon-sm"
nativeButton={false}
render={
<a
href={`/api/backup/${backup.filename}`}
download
title="Download"
aria-label="Download backup"
>
<IconCloudDownload />
</a>
}
/>
<AlertDialog>
<AlertDialogTrigger
render={ render={
<Button <Button
variant="ghost" variant="ghost"
size="icon-sm" size="icon-sm"
disabled={deleting === backup.filename} nativeButton={false}
title="Delete" render={
<a
href={`/api/backup/${backup.filename}`}
download
aria-label="Download backup"
>
<IconCloudDownload />
</a>
}
/> />
} }
> />
{deleting === backup.filename ? ( <TooltipContent>Download</TooltipContent>
<Spinner /> </Tooltip>
) : ( <AlertDialog>
<IconTrash /> <Tooltip>
)} <AlertDialogTrigger
</AlertDialogTrigger> render={
<TooltipTrigger
render={
<Button
variant="ghost"
size="icon-sm"
disabled={deleting === backup.filename}
/>
}
/>
}
>
{deleting === backup.filename ? (
<Spinner />
) : (
<IconTrash />
)}
</AlertDialogTrigger>
<TooltipContent>Delete</TooltipContent>
</Tooltip>
<AlertDialogContent> <AlertDialogContent>
<AlertDialogHeader> <AlertDialogHeader>
<AlertDialogTitle>Delete backup?</AlertDialogTitle> <AlertDialogTitle>Delete backup?</AlertDialogTitle>
@@ -95,7 +95,7 @@ export function IntegrationsSection({
return ( return (
<div> <div>
<div className="mb-3 flex items-center gap-2"> <div className="mb-3 flex items-center gap-2">
<IconWebhook size={16} className="text-muted-foreground" /> <IconWebhook className="size-4 text-muted-foreground" />
<h2 className="text-xs font-medium uppercase tracking-wider text-muted-foreground"> <h2 className="text-xs font-medium uppercase tracking-wider text-muted-foreground">
Integrations Integrations
</h2> </h2>
@@ -37,7 +37,7 @@ export function ServerSection({
<div className="flex items-center justify-between gap-4"> <div className="flex items-center justify-between gap-4">
<div className="flex items-start gap-3"> <div className="flex items-start gap-3">
<div className="mt-0.5 flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-primary/10"> <div className="mt-0.5 flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-primary/10">
<IconUserPlus size={16} className="text-primary" /> <IconUserPlus className="size-4 text-primary" />
</div> </div>
<div> <div>
<CardTitle>Open registration</CardTitle> <CardTitle>Open registration</CardTitle>
@@ -32,7 +32,7 @@ export function SettingsShell({
> >
<motion.div variants={sectionVariants}> <motion.div variants={sectionVariants}>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<IconSettings size={20} className="text-primary" /> <IconSettings className="size-5 text-primary" />
<h1 className="font-display text-3xl tracking-tight">Settings</h1> <h1 className="font-display text-3xl tracking-tight">Settings</h1>
</div> </div>
<p className="mt-1 text-sm text-muted-foreground"> <p className="mt-1 text-sm text-muted-foreground">
@@ -9,6 +9,7 @@ import {
IconRefresh, IconRefresh,
IconTrash, IconTrash,
} from "@tabler/icons-react"; } from "@tabler/icons-react";
import { formatDistanceToNow } from "date-fns";
import { AnimatePresence, motion } from "motion/react"; import { AnimatePresence, motion } from "motion/react";
import { useState } from "react"; import { useState } from "react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
@@ -49,17 +50,6 @@ export interface WebhookConnection {
}[]; }[];
} }
export function timeAgo(dateStr: string): string {
const diff = Date.now() - new Date(dateStr).getTime();
const mins = Math.floor(diff / 60000);
if (mins < 1) return "just now";
if (mins < 60) return `${mins}m ago`;
const hours = Math.floor(mins / 60);
if (hours < 24) return `${hours}h ago`;
const days = Math.floor(hours / 24);
return `${days}d ago`;
}
export function WebhookCard({ export function WebhookCard({
provider, provider,
connection, connection,
@@ -122,15 +112,14 @@ export function WebhookCard({
<CardDescription> <CardDescription>
{connection {connection
? connection.lastEventAt ? connection.lastEventAt
? `Last event ${timeAgo(connection.lastEventAt)}` ? `Last event ${formatDistanceToNow(new Date(connection.lastEventAt), { addSuffix: true })}`
: "Connected — no events yet" : "Connected — no events yet"
: "Not configured"} : "Not configured"}
</CardDescription> </CardDescription>
</div> </div>
</div> </div>
<IconChevronDown <IconChevronDown
size={16} className={`size-4 text-muted-foreground transition-transform duration-200 ${cardOpen ? "rotate-180" : ""}`}
className={`text-muted-foreground transition-transform duration-200 ${cardOpen ? "rotate-180" : ""}`}
/> />
</CollapsibleTrigger> </CollapsibleTrigger>
</CardContent> </CardContent>
@@ -151,10 +140,7 @@ export function WebhookCard({
{isPlex && ( {isPlex && (
<div className="flex gap-2.5 rounded-lg border border-primary/20 bg-primary/5 px-3 py-2.5"> <div className="flex gap-2.5 rounded-lg border border-primary/20 bg-primary/5 px-3 py-2.5">
<IconInfoCircle <IconInfoCircle className="mt-0.5 size-3.5 shrink-0 text-primary" />
size={14}
className="mt-0.5 shrink-0 text-primary"
/>
<p className="text-xs leading-relaxed text-foreground/80"> <p className="text-xs leading-relaxed text-foreground/80">
Plex webhooks require an active{" "} Plex webhooks require an active{" "}
<a <a
@@ -240,9 +226,9 @@ export function WebhookCard({
} }
> >
{copied ? ( {copied ? (
<IconCheck size={14} className="text-green-400" /> <IconCheck className="text-green-400" />
) : ( ) : (
<IconCopy size={14} /> <IconCopy />
)} )}
</TooltipTrigger> </TooltipTrigger>
<TooltipContent>Copy URL</TooltipContent> <TooltipContent>Copy URL</TooltipContent>
@@ -256,7 +242,7 @@ export function WebhookCard({
size="sm" size="sm"
onClick={() => onRegenerateToken(provider)} onClick={() => onRegenerateToken(provider)}
> >
<IconRefresh size={12} /> <IconRefresh />
Regenerate URL Regenerate URL
</Button> </Button>
<Button <Button
@@ -264,7 +250,7 @@ export function WebhookCard({
size="sm" size="sm"
onClick={() => onDelete(provider)} onClick={() => onDelete(provider)}
> >
<IconTrash size={12} /> <IconTrash />
Disconnect Disconnect
</Button> </Button>
</div> </div>
@@ -275,8 +261,7 @@ export function WebhookCard({
<Collapsible open={setupOpen} onOpenChange={setSetupOpen}> <Collapsible open={setupOpen} onOpenChange={setSetupOpen}>
<CollapsibleTrigger className="flex w-full items-center gap-1.5 rounded-md py-1 text-xs text-muted-foreground transition-colors hover:text-foreground"> <CollapsibleTrigger className="flex w-full items-center gap-1.5 rounded-md py-1 text-xs text-muted-foreground transition-colors hover:text-foreground">
<IconChevronDown <IconChevronDown
size={12} className={`size-3 transition-transform ${setupOpen ? "rotate-0" : "-rotate-90"}`}
className={`transition-transform ${setupOpen ? "rotate-0" : "-rotate-90"}`}
/> />
Setup instructions Setup instructions
</CollapsibleTrigger> </CollapsibleTrigger>
+1 -1
View File
@@ -112,7 +112,7 @@ export default async function SettingsPage() {
{isAdmin && ( {isAdmin && (
<div> <div>
<div className="mb-3 flex items-center gap-2"> <div className="mb-3 flex items-center gap-2">
<IconServerCog size={16} className="text-muted-foreground" /> <IconServerCog className="size-4 text-muted-foreground" />
<h2 className="text-xs font-medium uppercase tracking-wider text-muted-foreground"> <h2 className="text-xs font-medium uppercase tracking-wider text-muted-foreground">
Server Server
</h2> </h2>
+6 -6
View File
@@ -24,7 +24,7 @@ const steps = [
className="inline-flex items-center gap-1 font-medium text-primary underline decoration-primary/30 underline-offset-2 transition-colors hover:decoration-primary" className="inline-flex items-center gap-1 font-medium text-primary underline decoration-primary/30 underline-offset-2 transition-colors hover:decoration-primary"
> >
themoviedb.org themoviedb.org
<IconExternalLink size={14} /> <IconExternalLink className="size-3.5" />
</a>{" "} </a>{" "}
and sign up for a free account. and sign up for a free account.
</> </>
@@ -43,7 +43,7 @@ const steps = [
className="inline-flex items-center gap-1 font-medium text-primary underline decoration-primary/30 underline-offset-2 transition-colors hover:decoration-primary" className="inline-flex items-center gap-1 font-medium text-primary underline decoration-primary/30 underline-offset-2 transition-colors hover:decoration-primary"
> >
Settings &rarr; API Settings &rarr; API
<IconExternalLink size={14} /> <IconExternalLink className="size-3.5" />
</a>{" "} </a>{" "}
and request an API key. Choose &ldquo;Developer&rdquo; when asked. You and request an API key. Choose &ldquo;Developer&rdquo; when asked. You
need the{" "} need the{" "}
@@ -133,7 +133,7 @@ export default function SetupPage() {
{/* Header */} {/* Header */}
<motion.div variants={sectionVariants} className="space-y-3"> <motion.div variants={sectionVariants} className="space-y-3">
<div className="inline-flex items-center gap-2 rounded-full border border-primary/20 bg-primary/5 px-3 py-1 text-xs font-medium text-primary"> <div className="inline-flex items-center gap-2 rounded-full border border-primary/20 bg-primary/5 px-3 py-1 text-xs font-medium text-primary">
<IconKey size={14} /> <IconKey className="size-3.5" />
Setup required Setup required
</div> </div>
<h1 className="font-display text-3xl tracking-tight sm:text-4xl"> <h1 className="font-display text-3xl tracking-tight sm:text-4xl">
@@ -188,12 +188,12 @@ export default function SetupPage() {
> >
{copiedIdx === idx ? ( {copiedIdx === idx ? (
<> <>
<IconCheck size={12} className="text-green-400" /> <IconCheck className="size-3 text-green-400" />
Copied Copied
</> </>
) : ( ) : (
<> <>
<IconCopy size={12} /> <IconCopy className="size-3" />
Copy Copy
</> </>
)} )}
@@ -217,7 +217,7 @@ export default function SetupPage() {
{configured === true ? ( {configured === true ? (
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-green-500/10"> <div className="flex h-10 w-10 items-center justify-center rounded-full bg-green-500/10">
<IconCheck size={20} className="text-green-400" /> <IconCheck className="size-5 text-green-400" />
</div> </div>
<div> <div>
<p className="font-medium text-green-400"> <p className="font-medium text-green-400">
@@ -40,9 +40,9 @@ export function StarRating({ value, onChange }: StarRatingProps) {
}} }}
> >
{filled ? ( {filled ? (
<IconStarFilled size={18} className="text-primary" /> <IconStarFilled className="size-4.5 text-primary" />
) : ( ) : (
<IconStar size={18} className="text-muted-foreground/30" /> <IconStar className="size-4.5 text-muted-foreground/30" />
)} )}
</motion.button> </motion.button>
); );
@@ -50,7 +50,7 @@ export function StatusButton({ currentStatus, onChange }: StatusButtonProps) {
transition={{ duration: 0.15 }} transition={{ duration: 0.15 }}
className="inline-flex h-9 items-center gap-2 rounded-lg bg-primary/10 px-4 text-sm font-medium text-primary ring-1 ring-primary/20 transition-all hover:bg-primary/15 hover:ring-primary/30 active:scale-[0.97]" className="inline-flex h-9 items-center gap-2 rounded-lg bg-primary/10 px-4 text-sm font-medium text-primary ring-1 ring-primary/20 transition-all hover:bg-primary/15 hover:ring-primary/30 active:scale-[0.97]"
> >
<IconPlus size={14} strokeWidth={2.5} /> <IconPlus className="size-3.5" strokeWidth={2.5} />
Watchlist Watchlist
</motion.button> </motion.button>
) : ( ) : (
@@ -66,14 +66,8 @@ export function StatusButton({ currentStatus, onChange }: StatusButtonProps) {
className={`group inline-flex h-9 items-center gap-2 rounded-lg px-4 text-sm font-medium ring-1 transition-all active:scale-[0.97] ${config.class} ${config.bgClass} ${config.borderClass} hover:ring-destructive/30 hover:bg-destructive/10 hover:text-destructive`} className={`group inline-flex h-9 items-center gap-2 rounded-lg px-4 text-sm font-medium ring-1 transition-all active:scale-[0.97] ${config.class} ${config.bgClass} ${config.borderClass} hover:ring-destructive/30 hover:bg-destructive/10 hover:text-destructive`}
> >
<span className="grid [&>svg]:col-start-1 [&>svg]:row-start-1"> <span className="grid [&>svg]:col-start-1 [&>svg]:row-start-1">
<config.icon <config.icon className="size-3.5 transition-opacity group-hover:opacity-0" />
size={14} <IconX className="size-3.5 opacity-0 text-destructive transition-opacity group-hover:opacity-100" />
className="transition-opacity group-hover:opacity-0"
/>
<IconX
size={14}
className="opacity-0 text-destructive transition-opacity group-hover:opacity-100"
/>
</span> </span>
<span className="grid [&>span]:col-start-1 [&>span]:row-start-1"> <span className="grid [&>span]:col-start-1 [&>span]:row-start-1">
<span className="transition-opacity group-hover:opacity-0"> <span className="transition-opacity group-hover:opacity-0">
@@ -30,7 +30,7 @@ export function TitleActions() {
onClick={handleWatchMovie} onClick={handleWatchMovie}
className="inline-flex h-9 items-center gap-2 rounded-lg bg-primary px-4 text-sm font-medium text-primary-foreground transition-all active:scale-[0.97] hover:shadow-md hover:shadow-primary/20" className="inline-flex h-9 items-center gap-2 rounded-lg bg-primary px-4 text-sm font-medium text-primary-foreground transition-all active:scale-[0.97] hover:shadow-md hover:shadow-primary/20"
> >
<IconPlayerPlay size={14} /> <IconPlayerPlay className="size-3.5" />
Mark Watched Mark Watched
</button> </button>
)} )}
@@ -56,7 +56,7 @@ export function TitleSeasons() {
type="button" type="button"
className="inline-flex items-center gap-1.5 rounded-md px-2 py-1 text-[10px] font-medium uppercase tracking-wider text-muted-foreground transition-colors hover:bg-accent hover:text-foreground" className="inline-flex items-center gap-1.5 rounded-md px-2 py-1 text-[10px] font-medium uppercase tracking-wider text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
> >
<IconChecks size={14} /> <IconChecks className="size-3.5" />
Mark All Watched Mark All Watched
</button> </button>
} }
@@ -160,15 +160,9 @@ export function TitleSeasons() {
</button> </button>
)} )}
{isOpen ? ( {isOpen ? (
<IconChevronUp <IconChevronUp className="size-4 text-muted-foreground" />
size={16}
className="text-muted-foreground"
/>
) : ( ) : (
<IconChevronDown <IconChevronDown className="size-4 text-muted-foreground" />
size={16}
className="text-muted-foreground"
/>
)} )}
</div> </div>
</div> </div>
@@ -221,7 +215,7 @@ export function TitleSeasons() {
damping: 15, damping: 15,
}} }}
> >
<IconCheck size={14} /> <IconCheck className="size-3.5" />
</motion.div> </motion.div>
)} )}
</button> </button>
+2 -2
View File
@@ -97,7 +97,7 @@ export function AuthForm({
> >
<div className="space-y-2 text-center"> <div className="space-y-2 text-center">
<Link href="/" className="inline-flex justify-center text-primary"> <Link href="/" className="inline-flex justify-center text-primary">
<SofaLogo size={36} /> <SofaLogo className="size-9" />
</Link> </Link>
<h1 className="text-lg font-medium"> <h1 className="text-lg font-medium">
{isRegister ? "Create your account" : "Welcome back"} {isRegister ? "Create your account" : "Welcome back"}
@@ -124,7 +124,7 @@ export function AuthForm({
whileTap={{ scale: 0.98 }} whileTap={{ scale: 0.98 }}
className="inline-flex h-11 w-full items-center justify-center gap-2 rounded-lg border border-border/50 bg-background/50 text-sm font-medium transition-all hover:bg-accent hover:text-foreground disabled:pointer-events-none disabled:opacity-50" className="inline-flex h-11 w-full items-center justify-center gap-2 rounded-lg border border-border/50 bg-background/50 text-sm font-medium transition-all hover:bg-accent hover:text-foreground disabled:pointer-events-none disabled:opacity-50"
> >
<IconKey size={16} /> <IconKey className="size-4" />
{oidcLoading {oidcLoading
? "Redirecting..." ? "Redirecting..."
: `Sign in with ${authConfig?.oidcProviderName || "SSO"}`} : `Sign in with ${authConfig?.oidcProviderName || "SSO"}`}
+7 -10
View File
@@ -235,9 +235,9 @@ export function CommandPalette() {
</p> </p>
<div className="flex items-center gap-1.5 text-[10px] text-muted-foreground"> <div className="flex items-center gap-1.5 text-[10px] text-muted-foreground">
{r.type === "movie" ? ( {r.type === "movie" ? (
<IconMovie size={11} /> <IconMovie className="size-[11px]" />
) : ( ) : (
<IconDeviceTv size={11} /> <IconDeviceTv className="size-[11px]" />
)} )}
<span className="uppercase">{r.type}</span> <span className="uppercase">{r.type}</span>
{r.releaseDate && ( {r.releaseDate && (
@@ -273,10 +273,7 @@ export function CommandPalette() {
onSelect={() => handleRecentSearch(q)} onSelect={() => handleRecentSearch(q)}
className="group" className="group"
> >
<IconSearch <IconSearch className="size-3.5 text-muted-foreground" />
size={14}
className="text-muted-foreground"
/>
<span className="flex-1">{q}</span> <span className="flex-1">{q}</span>
<span <span
data-slot="command-shortcut" data-slot="command-shortcut"
@@ -290,7 +287,7 @@ export function CommandPalette() {
}} }}
className="rounded-sm p-0.5 text-muted-foreground opacity-0 transition-opacity hover:text-foreground group-data-[selected=true]:opacity-100" className="rounded-sm p-0.5 text-muted-foreground opacity-0 transition-opacity hover:text-foreground group-data-[selected=true]:opacity-100"
> >
<IconX size={12} /> <IconX className="size-3" />
</button> </button>
</span> </span>
</CommandItem> </CommandItem>
@@ -305,7 +302,7 @@ export function CommandPalette() {
router.push("/dashboard"); router.push("/dashboard");
}} }}
> >
<IconHome size={14} /> <IconHome className="size-3.5" />
Go to Dashboard Go to Dashboard
<CommandShortcut>G H</CommandShortcut> <CommandShortcut>G H</CommandShortcut>
</CommandItem> </CommandItem>
@@ -315,7 +312,7 @@ export function CommandPalette() {
router.push("/explore"); router.push("/explore");
}} }}
> >
<IconSearch size={14} /> <IconSearch className="size-3.5" />
Go to Explore Go to Explore
<CommandShortcut>G E</CommandShortcut> <CommandShortcut>G E</CommandShortcut>
</CommandItem> </CommandItem>
@@ -325,7 +322,7 @@ export function CommandPalette() {
setHelpOpen(true); setHelpOpen(true);
}} }}
> >
<IconKeyboard size={14} /> <IconKeyboard className="size-3.5" />
Keyboard Shortcuts Keyboard Shortcuts
<CommandShortcut>?</CommandShortcut> <CommandShortcut>?</CommandShortcut>
</CommandItem> </CommandItem>
+1 -1
View File
@@ -139,7 +139,7 @@ export function LandingPage({ posterUrls }: { posterUrls: string[] }) {
delay: 0.1, delay: 0.1,
}} }}
> >
<SofaLogo size={96} className="sm:h-28 sm:w-28 md:h-32 md:w-32" /> <SofaLogo className="size-24 sm:size-28 md:size-32" />
</motion.div> </motion.div>
<motion.p <motion.p
className="mx-auto max-w-md text-lg leading-relaxed text-muted-foreground" className="mx-auto max-w-md text-lg leading-relaxed text-muted-foreground"
+1 -2
View File
@@ -34,8 +34,7 @@ export function MobileTabBar() {
className="relative flex flex-1 flex-col items-center justify-center gap-0.5" className="relative flex flex-1 flex-col items-center justify-center gap-0.5"
> >
<Icon <Icon
size={20} className={`size-5 ${isActive ? "text-primary" : "text-muted-foreground"}`}
className={isActive ? "text-primary" : "text-muted-foreground"}
/> />
<span <span
className={`text-[10px] font-medium ${isActive ? "text-primary" : "text-muted-foreground"}`} className={`text-[10px] font-medium ${isActive ? "text-primary" : "text-muted-foreground"}`}
+26 -19
View File
@@ -7,6 +7,11 @@ import Link from "next/link";
import { usePathname, useRouter } from "next/navigation"; import { usePathname, useRouter } from "next/navigation";
import { SofaLogo } from "@/components/sofa-logo"; import { SofaLogo } from "@/components/sofa-logo";
import { Kbd } from "@/components/ui/kbd"; import { Kbd } from "@/components/ui/kbd";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { commandPaletteOpenAtom } from "@/lib/atoms/command-palette"; import { commandPaletteOpenAtom } from "@/lib/atoms/command-palette";
import { signOut, useSession } from "@/lib/auth/client"; import { signOut, useSession } from "@/lib/auth/client";
@@ -29,7 +34,7 @@ export function NavBar() {
href="/dashboard" href="/dashboard"
className="shrink-0 text-foreground transition-colors hover:text-primary" className="shrink-0 text-foreground transition-colors hover:text-primary"
> >
<SofaLogo size={28} /> <SofaLogo className="size-7" />
</Link> </Link>
<div className="hidden items-center gap-1 sm:flex"> <div className="hidden items-center gap-1 sm:flex">
{navLinks.map((link) => { {navLinks.map((link) => {
@@ -61,14 +66,14 @@ export function NavBar() {
</div> </div>
</div> </div>
<div className="flex flex-1 items-center justify-end gap-3 sm:flex-none"> <div className="flex flex-1 items-center justify-end gap-2 sm:flex-none sm:gap-1">
{/* Mobile search trigger */} {/* Mobile search trigger */}
<button <button
type="button" type="button"
onClick={() => setCommandPaletteOpen(true)} onClick={() => setCommandPaletteOpen(true)}
className="flex flex-1 items-center gap-2 rounded-lg border border-border/50 bg-card/50 px-3 py-1.5 text-sm text-muted-foreground transition-all hover:border-primary/20 hover:bg-card sm:hidden" className="flex flex-1 items-center gap-2 rounded-lg border border-border/50 bg-card/50 px-3 py-1.5 text-sm text-muted-foreground transition-all hover:border-primary/20 hover:bg-card sm:hidden"
> >
<IconSearch size={14} /> <IconSearch className="size-3.5" />
<span>Search...</span> <span>Search...</span>
</button> </button>
{/* Desktop search trigger pill */} {/* Desktop search trigger pill */}
@@ -77,32 +82,34 @@ export function NavBar() {
onClick={() => setCommandPaletteOpen(true)} onClick={() => setCommandPaletteOpen(true)}
className="hidden items-center gap-2 rounded-lg border border-border/50 bg-card/50 px-3 py-1.5 text-sm text-muted-foreground transition-all hover:border-primary/20 hover:bg-card sm:inline-flex" className="hidden items-center gap-2 rounded-lg border border-border/50 bg-card/50 px-3 py-1.5 text-sm text-muted-foreground transition-all hover:border-primary/20 hover:bg-card sm:inline-flex"
> >
<IconSearch size={14} /> <IconSearch className="size-3.5" />
<span>Search...</span> <span>Search...</span>
<Kbd className="ml-1">K</Kbd> <Kbd className="ml-1">K</Kbd>
</button> </button>
<div className="hidden items-center gap-2 rounded-lg border border-border/30 px-2.5 py-1 sm:flex"> <div className="mx-1.5 hidden h-4 w-px bg-border/50 sm:block" />
<span className="text-sm text-muted-foreground"> <Tooltip>
{session?.user?.name} <TooltipTrigger
</span> render={<Link href="/settings" />}
<Link className="hidden items-center gap-1.5 rounded-md px-2 py-1.5 text-sm leading-none text-muted-foreground transition-colors hover:text-foreground sm:inline-flex"
href="/settings"
className="inline-flex h-6 w-6 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
> >
<IconSettings size={14} /> {session?.user?.name}
</Link> <IconSettings className="size-3.5" />
<button </TooltipTrigger>
type="button" <TooltipContent>Settings</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger
onClick={async () => { onClick={async () => {
await signOut(); await signOut();
router.push("/"); router.push("/");
router.refresh(); router.refresh();
}} }}
className="inline-flex h-6 w-6 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-accent hover:text-foreground" className="hidden h-7 w-7 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-accent hover:text-foreground sm:inline-flex"
> >
<IconLogout size={14} /> <IconLogout className="size-[15px]" />
</button> </TooltipTrigger>
</div> <TooltipContent>Log out</TooltipContent>
</Tooltip>
</div> </div>
</nav> </nav>
</header> </header>
+2 -10
View File
@@ -1,17 +1,9 @@
export function SofaLogo({ export function SofaLogo({ className }: { className?: string }) {
size = 24,
className,
}: {
size?: number;
className?: string;
}) {
return ( return (
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
width={size}
height={size}
viewBox="0 0 24 24" viewBox="0 0 24 24"
className={className} className={className ?? "size-6"}
role="img" role="img"
aria-label="Sofa" aria-label="Sofa"
> >
+2 -2
View File
@@ -69,11 +69,11 @@ export function TitleCard({
<div className="px-3 pb-3 pt-2.5"> <div className="px-3 pb-3 pt-2.5">
<p className="line-clamp-1 text-sm font-medium leading-snug">{title}</p> <p className="line-clamp-1 text-sm font-medium leading-snug">{title}</p>
<div className="mt-1.5 flex items-center gap-2 text-xs text-muted-foreground"> <div className="mt-1.5 flex items-center gap-2 text-xs text-muted-foreground">
<TypeIcon size={14} className="shrink-0 text-primary/60" /> <TypeIcon className="size-3.5 shrink-0 text-primary/60" />
{year && <span>{year}</span>} {year && <span>{year}</span>}
{voteAverage != null && voteAverage > 0 && ( {voteAverage != null && voteAverage > 0 && (
<span className="ml-auto flex items-center gap-0.5 text-primary/80"> <span className="ml-auto flex items-center gap-0.5 text-primary/80">
<IconStarFilled size={11} /> <IconStarFilled className="size-[11px]" />
{voteAverage.toFixed(1)} {voteAverage.toFixed(1)}
</span> </span>
)} )}