Written by: Marlon Colca
Posted on 14 Sep 2025 - 20 days ago
nextjs typescript clones
Surface in‑progress items based on local resume data
Goal: Surface in‑progress items based on local resume data.
src/components/ContinueWatching.tsx
(excerpt) 🧩"use client";
import { useEffect, useMemo, useState } from "react";
import Row from "@/components/Row";
import { getAllMovies, type Movie } from "@/lib/catalog";
type Entry = { id: string; t: number; d?: number; u?: number };
function readEntries(): Entry[] {
const prefix = "vp:pos:movie:";
const out: Entry[] = [];
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (!key?.startsWith(prefix)) continue;
const id = key.slice(prefix.length);
const raw = localStorage.getItem(key);
if (!raw) continue;
try {
const { t, d, u } = JSON.parse(raw) as Entry;
if (typeof t === "number") out.push({ id, t, d, u });
} catch {}
}
return out;
}
export default function ContinueWatching() {
const [entries, setEntries] = useState<Entry[]>([]);
const movies = getAllMovies();
useEffect(() => {
setEntries(readEntries());
const onStorage = () => setEntries(readEntries());
window.addEventListener("storage", onStorage);
return () => window.removeEventListener("storage", onStorage);
}, []);
const items: Movie[] = useMemo(() => {
const byId = new Map(movies.map((m) => [m.id, m] as const));
return entries
.filter((e) => byId.has(e.id))
.filter((e) => {
const d = e.d ?? 0;
if (!d) return true;
const pct = e.t / d;
return pct > 0.02 && pct < 0.98;
})
.sort((a, b) => (b.u ?? 0) - (a.u ?? 0))
.map((e) => byId.get(e.id)!)
.slice(0, 12);
}, [entries, movies]);
if (!items.length) return null;
return <Row title="Continue Watching" items={items} />;
}
<ContinueWatching />
near the top of src/app/page.tsx
, below the hero.localStorage
keys with the vp:pos:movie:
prefix to avoid hard‑coded ids and keep it generic.u
(last update timestamp) so the most recently viewed shows first.storage
event to refresh the row when another tab or the player updates progress.Handle videos, posters and subtitles responsibly in an open‑source project
15 Sep 2025 - 19 days ago