mirror of
https://github.com/jakejarvis/sofa.git
synced 2026-08-29 00:25:38 -04:00
Improve title detail: episode stills, descriptions, provider tooltips, stronger gradient
- Episode rows now show still images (thumbnail, hidden on mobile) - Episode descriptions shown as 2-line clamp below title on desktop - Watched episodes dimmed with opacity for clear visual differentiation - Provider logos get CSS hover tooltips showing the service name - Backdrop gradient strengthened with extra overlay layer for text readability on bright/colorful backdrop images - Added stillPath to Episode interface Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -32,6 +32,7 @@ interface Episode {
|
||||
episodeNumber: number;
|
||||
name: string | null;
|
||||
overview: string | null;
|
||||
stillPath: string | null;
|
||||
airDate: string | null;
|
||||
runtimeMinutes: number | null;
|
||||
}
|
||||
@@ -462,10 +463,11 @@ export default function TitleDetailPage() {
|
||||
className="object-cover"
|
||||
priority
|
||||
/>
|
||||
{/* Three-layer gradient */}
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-background via-background/60 to-background/20" />
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-background/80 to-transparent" />
|
||||
<div className="absolute inset-0 bg-gradient-to-b from-background/40 via-transparent to-transparent" />
|
||||
{/* Multi-layer gradient for text readability on any backdrop */}
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-background via-background/70 to-background/30" />
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-background/90 via-background/40 to-transparent" />
|
||||
<div className="absolute inset-0 bg-gradient-to-b from-background/50 via-transparent to-transparent" />
|
||||
<div className="absolute inset-0 bg-background/15" />
|
||||
{/* Film grain overlay */}
|
||||
<div
|
||||
className="pointer-events-none absolute inset-0 opacity-[0.03]"
|
||||
@@ -689,10 +691,13 @@ export default function TitleDetailPage() {
|
||||
const isWatched = title.episodeWatches?.includes(
|
||||
ep.id,
|
||||
);
|
||||
const stillUrl = ep.stillPath
|
||||
? `https://image.tmdb.org/t/p/w300${ep.stillPath}`
|
||||
: null;
|
||||
return (
|
||||
<div
|
||||
key={ep.id}
|
||||
className="flex items-center gap-3 border-b border-border/30 px-4 py-3 last:border-b-0"
|
||||
className={`flex gap-3 border-b border-border/30 px-4 py-3 last:border-b-0 transition-colors ${isWatched ? "opacity-60" : ""}`}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
@@ -705,7 +710,7 @@ export default function TitleDetailPage() {
|
||||
)
|
||||
}
|
||||
disabled={watchingEp === ep.id}
|
||||
className={`flex h-6 w-6 shrink-0 items-center justify-center rounded-md border-2 transition-all ${
|
||||
className={`mt-1 flex h-6 w-6 shrink-0 items-center justify-center rounded-md border-2 transition-all ${
|
||||
isWatched
|
||||
? "border-primary bg-primary text-primary-foreground"
|
||||
: "border-muted-foreground/40 bg-muted-foreground/5 hover:border-primary/70 hover:bg-primary/10"
|
||||
@@ -725,8 +730,19 @@ export default function TitleDetailPage() {
|
||||
</motion.div>
|
||||
)}
|
||||
</button>
|
||||
{stillUrl && (
|
||||
<div className="hidden h-14 w-24 shrink-0 overflow-hidden rounded-md bg-muted sm:block">
|
||||
<Image
|
||||
src={stillUrl}
|
||||
alt={ep.name ?? ""}
|
||||
width={300}
|
||||
height={169}
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="truncate text-sm">
|
||||
<p className="text-sm">
|
||||
<span className="font-mono text-xs text-muted-foreground">
|
||||
E{String(ep.episodeNumber).padStart(2, "0")}
|
||||
</span>{" "}
|
||||
@@ -734,13 +750,17 @@ export default function TitleDetailPage() {
|
||||
{ep.name ?? "Untitled"}
|
||||
</span>
|
||||
</p>
|
||||
{ep.airDate && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{ep.airDate}
|
||||
{ep.airDate ?? ""}
|
||||
{ep.airDate && ep.runtimeMinutes ? " · " : ""}
|
||||
{ep.runtimeMinutes
|
||||
? ` · ${ep.runtimeMinutes}m`
|
||||
? `${ep.runtimeMinutes}m`
|
||||
: ""}
|
||||
</p>
|
||||
{ep.overview && (
|
||||
<p className="mt-1 hidden line-clamp-2 text-xs leading-relaxed text-muted-foreground/70 sm:block">
|
||||
{ep.overview}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -796,10 +816,8 @@ function ProviderBadge({
|
||||
const logoUrl = logoPath ? `https://image.tmdb.org/t/p/w92${logoPath}` : null;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex h-10 w-10 items-center justify-center overflow-hidden rounded-lg border border-border/30 bg-card transition-transform hover:scale-105"
|
||||
title={name}
|
||||
>
|
||||
<div className="group relative">
|
||||
<div className="flex h-10 w-10 items-center justify-center overflow-hidden rounded-lg border border-border/30 bg-card transition-transform hover:scale-105">
|
||||
{logoUrl ? (
|
||||
<Image
|
||||
src={logoUrl}
|
||||
@@ -814,5 +832,9 @@ function ProviderBadge({
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="pointer-events-none absolute -top-8 left-1/2 z-20 -translate-x-1/2 whitespace-nowrap rounded-md bg-popover px-2 py-1 text-[10px] font-medium text-popover-foreground opacity-0 shadow-md transition-opacity group-hover:opacity-100">
|
||||
{name}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user