Add /dashboard route, mark-all-watched on complete, keyboard fixes

- Move dashboard page to app/(pages)/dashboard, redirect / to /dashboard
  for authenticated users, update all nav/breadcrumb/auth hrefs
- Mark all episodes watched when a TV show status is set to "completed"
  (markAllEpisodesWatched skips already-watched episodes)
- Refactor KeyboardProvider to store shortcuts in a ref instead of
  state, eliminating unnecessary re-renders; use react-hotkeys-hook for
  Cmd+K so it works reliably in form inputs
- Fix useRegisterShortcut: re-register on every render (stable ref read)
  with a separate cleanup effect, removing brittle key memoization
- Search page: silently navigate to title detail on import instead of
  showing "Added to library" toast
This commit is contained in:
2026-03-01 11:06:26 -05:00
parent b6aaad243f
commit dcb48eaf37
13 changed files with 125 additions and 59 deletions
+5 -10
View File
@@ -35,11 +35,7 @@ export default function SearchPage() {
if (l) setSearched(true);
}, []);
async function handleImport(
tmdbId: number,
type: "movie" | "tv",
title: string,
) {
async function handleOpen(tmdbId: number, type: "movie" | "tv") {
setImporting(tmdbId);
try {
const res = await fetch("/api/titles/import", {
@@ -49,11 +45,10 @@ export default function SearchPage() {
});
const data = await res.json();
if (data.id) {
toast.success(`Added "${title}" to library`);
router.push(`/titles/${data.id}`);
}
} catch {
toast.error("Failed to import title");
toast.error("Failed to load title");
} finally {
setImporting(null);
}
@@ -64,7 +59,7 @@ export default function SearchPage() {
<div className="space-y-2">
<h1 className="font-display text-3xl tracking-tight">Search</h1>
<p className="text-sm text-muted-foreground">
Find movies and TV shows to add to your library
Find movies and TV shows to track
</p>
</div>
@@ -91,14 +86,14 @@ export default function SearchPage() {
posterPath={r.posterPath}
releaseDate={r.releaseDate}
voteAverage={r.voteAverage}
onImport={() => handleImport(r.tmdbId, r.type, r.title)}
onImport={() => handleOpen(r.tmdbId, r.type)}
/>
{importing === r.tmdbId && (
<div className="absolute inset-0 flex items-center justify-center rounded-xl bg-background/80 backdrop-blur-sm">
<div className="flex items-center gap-2">
<div className="h-4 w-4 animate-spin rounded-full border-2 border-primary border-t-transparent" />
<span className="text-sm font-medium text-primary">
Importing
Loading
</span>
</div>
</div>
+3 -1
View File
@@ -380,7 +380,9 @@ export default function TitleDetailPage() {
<Breadcrumb className="relative z-20">
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink render={<Link href="/" />}>Home</BreadcrumbLink>
<BreadcrumbLink render={<Link href="/dashboard" />}>
Home
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
+14
View File
@@ -2,8 +2,22 @@
import { motion } from "motion/react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useEffect } from "react";
import { useSession } from "@/lib/auth/client";
export default function Home() {
const { data: session, isPending } = useSession();
const router = useRouter();
useEffect(() => {
if (!isPending && session?.user) {
router.replace("/dashboard");
}
}, [session, isPending, router]);
if (isPending) return null;
return (
<div className="relative flex min-h-screen flex-col items-center justify-center overflow-hidden">
{/* Background grain texture */}