import { BookmarkPlus, Clock3, History, Star } from 'lucide-solid' import { For, Show, createMemo, createResource, createSignal } from 'solid-js' import { MediaCard } from '@/components/media/media-card' import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' import { Card, CardContent } from '@/components/ui/card' import { DataRow } from '@/components/ui/data-row' import { EmptyState } from '@/components/ui/empty-state' import { MetricCard } from '@/components/ui/metric-card' import { SectionHeading } from '@/components/ui/section-heading' import { Skeleton } from '@/components/ui/skeleton' import { Tabs } from '@/components/ui/tabs' import { dashboardService } from '@/services/dashboard-service' import { watchLaterService } from '@/services/watch-later-service' import { useAuth } from '@/stores/auth-store' import type { MediaItem, MediaType } from '@/types/domain' import { formatDate, formatRating } from '@/utils/format' import { mediaBadgeVariant, mediaTypeLabel } from '@/utils/media' type HistoryScope = 'all' | MediaType interface CompletionEntry { item: MediaItem watchedAt: string personalScore: number summary: string } const scopeOptions = [ { value: 'all', label: 'All' }, { value: 'movie', label: 'Movies' }, { value: 'show', label: 'Shows' }, { value: 'game', label: 'Games' }, ] as const const completionOffsets = [1, 3, 5, 8, 12, 16, 21, 27] const scoreAdjustments = [0.4, 0.1, 0.3, 0.2, -0.1, 0.2, 0.1, 0.4] const clampScore = (value: number): number => Math.max(6.5, Math.min(9.9, Number(value.toFixed(1)))) const completionSummary = (item: MediaItem): string => item.type === 'movie' ? 'Closed cleanly and worth keeping in your replay lane.' : item.type === 'show' ? 'Finished a strong run of episodes and filed it for an easy revisit.' : 'Campaign wrapped with enough momentum to stay in the backlog conversation.' const relativeTimeLabel = (isoDate: string): string => { const diffMs = Math.max(0, Date.now() - new Date(isoDate).getTime()) const diffHours = Math.floor(diffMs / (1000 * 60 * 60)) const diffDays = Math.floor(diffHours / 24) if (diffHours < 24) { return diffHours <= 1 ? '1 hour ago' : `${diffHours} hours ago` } if (diffDays === 1) { return 'Yesterday' } if (diffDays < 14) { return `${diffDays} days ago` } return formatDate(isoDate) } const buildCompletionEntries = (items: MediaItem[]): CompletionEntry[] => items .map((item, index) => { const watchedAt = new Date() watchedAt.setDate(watchedAt.getDate() - (completionOffsets[index] ?? (index + 1) * 4)) watchedAt.setHours(item.type === 'game' ? 22 : 21 - (index % 2), 10 + ((index * 11) % 35), 0, 0) return { item, watchedAt: watchedAt.toISOString(), personalScore: clampScore(item.rating + (scoreAdjustments[index] ?? 0)), summary: completionSummary(item), } }) .sort((left, right) => new Date(right.watchedAt).getTime() - new Date(left.watchedAt).getTime()) export const WatchedPage = () => { const auth = useAuth() const [scope, setScope] = createSignal('all') const [busyMediaId, setBusyMediaId] = createSignal(null) const [actionError, setActionError] = createSignal(null) const [reloadToken, setReloadToken] = createSignal(0) const [dashboardData, { refetch: refetchDashboard }] = createResource(reloadToken, () => dashboardService.getDashboard(), ) const [queueData, { refetch: refetchQueue }] = createResource(reloadToken, async () => { const accessToken = auth.accessToken() if (!accessToken) { return [] } return watchLaterService.getWatchLater(accessToken) }) const completionEntries = createMemo(() => buildCompletionEntries(dashboardData()?.recentlyWatched ?? [])) const queueIds = createMemo(() => new Set((queueData() ?? []).map((item) => item.id))) const filteredEntries = createMemo(() => scope() === 'all' ? completionEntries() : completionEntries().filter((entry) => entry.item.type === scope()), ) const averageScore = createMemo(() => { const entries = filteredEntries() if (entries.length === 0) { return null } return formatRating(entries.reduce((total, entry) => total + entry.personalScore, 0) / entries.length) }) const toggleReplayQueue = async (mediaId: number): Promise => { const accessToken = auth.accessToken() if (!accessToken) { setActionError('Your session expired. Please sign in again.') return } setActionError(null) setBusyMediaId(mediaId) try { if (queueIds().has(mediaId)) { await watchLaterService.removeWatchLater(accessToken, mediaId) } else { await watchLaterService.addWatchLater(accessToken, mediaId) } setReloadToken((value) => value + 1) } catch (error) { const message = error instanceof Error ? error.message : 'Unable to update replay queue' setActionError(message) } finally { setBusyMediaId(null) } } return (
{filteredEntries().length} entries} />
setScope(value as HistoryScope)} options={scopeOptions.map((option) => ({ value: option.value, label: option.label }))} />
queueIds().has(entry.item.id)).length}`} detail="replay candidates saved" tone="secondary" />
{actionError()}
{filteredEntries().length} logged} />
index)}>{() => }
0} fallback={ } >
{(entry) => ( {mediaTypeLabel(entry.item.type)} } trailing={ } /> )}
{filteredEntries().slice(0, 3).length} surfaced} /> 0} fallback={ } >
{(entry) => (
)}
{dashboardData.error?.message ?? queueData.error?.message ?? 'Unable to load watched history.'}
) }