mirror of
https://github.com/jakejarvis/sofa.git
synced 2026-08-29 03:55:38 -04:00
Polish mobile layouts, extract ExpandableText, fix image sizing
- Stack title-hero poster/metadata vertically on mobile (flex-col md:flex-row), switch backdrop tall breakpoint from sm to md - Extract bio expand/collapse logic into reusable ExpandableText component; use it in PersonHero and TitleHero - Fix ContinueWatchingCard image to use fill + sizes instead of fixed width/height to avoid layout shift - Add fade-out gradient on genre chip scrollbar edge on mobile - Show scaled-down ambient glow on mobile instead of hiding it entirely - Humanize knownForDepartment labels (Acting→Actor, Directing→Director, etc.) - Change cast member name from truncate to line-clamp-2 for wrapping - Hide tooltip arrow via [&>:last-child]:hidden instead of color-matching it - Apply safe-area-inset padding to HeroBanner content on notched devices
This commit is contained in:
@@ -35,16 +35,16 @@ export function ContinueWatchingCard({
|
||||
return (
|
||||
<Link
|
||||
href={`/titles/${item.title.id}`}
|
||||
className="group relative w-[calc(100vw-3rem)] shrink-0 overflow-hidden rounded-xl bg-card/50 ring-1 ring-white/[0.06] transition-shadow hover:shadow-lg hover:shadow-black/25 sm:w-72"
|
||||
className="group relative inline-block w-64 shrink-0 overflow-hidden rounded-xl bg-card/50 ring-1 ring-white/[0.06] transition-shadow hover:shadow-black/25 hover:shadow-lg sm:w-72"
|
||||
>
|
||||
<div className="relative aspect-video overflow-hidden rounded-t-xl bg-muted">
|
||||
{stillUrl ? (
|
||||
<Image
|
||||
src={stillUrl}
|
||||
alt={item.nextEpisode?.name ?? item.title.title}
|
||||
width={500}
|
||||
height={281}
|
||||
className="h-full w-full object-cover motion-safe:transition-transform motion-safe:duration-300 motion-safe:group-hover:scale-105"
|
||||
fill
|
||||
sizes="(min-width: 640px) 288px, 256px"
|
||||
className="object-cover motion-safe:transition-transform motion-safe:duration-300 motion-safe:group-hover:scale-105"
|
||||
/>
|
||||
) : (
|
||||
<div className="flex h-full items-center justify-center bg-gradient-to-br from-card via-secondary to-muted">
|
||||
@@ -56,13 +56,13 @@ export function ContinueWatchingCard({
|
||||
)}
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/70 via-black/20 to-transparent" />
|
||||
{item.nextEpisode && (
|
||||
<div className="absolute bottom-2.5 left-3 right-3">
|
||||
<p className="flex items-center gap-1.5 text-[10px] font-medium uppercase tracking-wider text-primary">
|
||||
<span className="inline-block h-1.5 w-1.5 motion-safe:animate-pulse rounded-full bg-primary" />
|
||||
<div className="absolute right-3 bottom-2.5 left-3">
|
||||
<p className="flex items-center gap-1.5 font-medium text-[10px] text-primary uppercase tracking-wider">
|
||||
<span className="inline-block h-1.5 w-1.5 rounded-full bg-primary motion-safe:animate-pulse" />
|
||||
Up next
|
||||
</p>
|
||||
<p className="mt-0.5 truncate text-sm font-medium text-white">
|
||||
<span className="font-mono text-xs text-white/60 [word-spacing:-0.25em] mr-0.5">
|
||||
<p className="mt-0.5 truncate font-medium text-sm text-white">
|
||||
<span className="mr-0.5 font-mono text-white/60 text-xs [word-spacing:-0.25em]">
|
||||
S{item.nextEpisode.seasonNumber} E
|
||||
{item.nextEpisode.episodeNumber}
|
||||
</span>{" "}
|
||||
@@ -73,8 +73,8 @@ export function ContinueWatchingCard({
|
||||
</div>
|
||||
<div className="flex items-center gap-3 p-3">
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="truncate text-sm font-medium">{item.title.title}</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
<p className="truncate font-medium text-sm">{item.title.title}</p>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{item.watchedEpisodes}/{item.totalEpisodes} episodes
|
||||
</p>
|
||||
</div>
|
||||
@@ -83,7 +83,7 @@ export function ContinueWatchingCard({
|
||||
</div>
|
||||
</div>
|
||||
{progress > 0 && (
|
||||
<div className="absolute bottom-0 left-0 right-0 h-0.5 bg-muted">
|
||||
<div className="absolute right-0 bottom-0 left-0 h-0.5 bg-muted">
|
||||
<div
|
||||
className="h-full bg-primary transition-[width]"
|
||||
style={{ width: `${progress}%` }}
|
||||
|
||||
@@ -18,7 +18,7 @@ export function ContinueWatchingList({
|
||||
return (
|
||||
<Carousel
|
||||
opts={{ align: "start", dragFree: true, containScroll: "trimSnaps" }}
|
||||
className="-mx-4 sm:-mx-0"
|
||||
className="-mx-4 sm:-mx-0 [&>[data-slot=carousel-content]]:px-px"
|
||||
>
|
||||
<CarouselContent className="px-4 sm:px-0">
|
||||
{items.map((item, i) => (
|
||||
|
||||
@@ -15,7 +15,7 @@ export function FeedSection({
|
||||
<section className="space-y-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<span aria-hidden={true}>{icon}</span>
|
||||
<h2 className="font-display text-xl tracking-tight text-balance">
|
||||
<h2 className="text-balance font-display text-xl tracking-tight">
|
||||
{title}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
@@ -53,7 +53,7 @@ function StatCard({
|
||||
}: StatCardProps) {
|
||||
return (
|
||||
<div
|
||||
className="animate-stagger-item relative overflow-hidden rounded-xl border border-border/30 bg-card/50 p-4"
|
||||
className="relative animate-stagger-item overflow-hidden rounded-xl border border-border/30 bg-card/50 p-4"
|
||||
style={{ "--stagger-index": index } as React.CSSProperties}
|
||||
>
|
||||
{sparklineData && <Sparkline data={sparklineData} color={color} />}
|
||||
@@ -63,7 +63,7 @@ function StatCard({
|
||||
>
|
||||
<Icon aria-hidden={true} className={`size-[13px] ${color}`} />
|
||||
</div>
|
||||
<span className="text-[10px] font-medium uppercase tracking-wider text-muted-foreground">
|
||||
<span className="font-medium text-[10px] text-muted-foreground uppercase tracking-wider">
|
||||
{label}
|
||||
</span>
|
||||
</div>
|
||||
@@ -97,7 +97,7 @@ function PeriodSelector({
|
||||
onValueChange={(v) => v && onPeriodChange(v as TimePeriod)}
|
||||
>
|
||||
<SelectTrigger
|
||||
className={`${inlineTriggerClass} uppercase text-foreground/80`}
|
||||
className={`${inlineTriggerClass} text-foreground/80 uppercase`}
|
||||
>
|
||||
<SelectValue>
|
||||
{(value: TimePeriod | null) => (value ? periodLabels[value] : null)}
|
||||
@@ -132,11 +132,11 @@ export function StatsDisplay({ stats }: { stats: DashboardStats }) {
|
||||
} | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
getStatsAction("movies", moviePeriod).then(setMovieStats);
|
||||
void getStatsAction("movies", moviePeriod).then(setMovieStats);
|
||||
}, [moviePeriod]);
|
||||
|
||||
useEffect(() => {
|
||||
getStatsAction("episodes", episodePeriod).then(setEpisodeStats);
|
||||
void getStatsAction("episodes", episodePeriod).then(setEpisodeStats);
|
||||
}, [episodePeriod]);
|
||||
|
||||
const movieCount = movieStats?.count ?? stats.moviesThisMonth;
|
||||
|
||||
@@ -16,19 +16,19 @@ export async function StatsSection({ userId }: { userId: string }) {
|
||||
<>
|
||||
<StatsDisplay stats={stats} />
|
||||
{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-border/50 border-dashed py-16 text-center">
|
||||
<div className="rounded-full bg-primary/10 p-4">
|
||||
<IconDeviceTv aria-hidden={true} className="size-8 text-primary" />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<p className="font-medium">Your library is empty</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Search for movies and TV shows to start tracking
|
||||
</p>
|
||||
</div>
|
||||
<Link
|
||||
href="/explore"
|
||||
className="inline-flex h-9 items-center rounded-lg bg-primary px-4 text-sm font-medium text-primary-foreground transition-shadow hover:shadow-md hover:shadow-primary/20"
|
||||
className="inline-flex h-9 items-center rounded-lg bg-primary px-4 font-medium text-primary-foreground text-sm transition-shadow hover:shadow-md hover:shadow-primary/20"
|
||||
>
|
||||
Start exploring
|
||||
</Link>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
export function WelcomeHeader({ name }: { name?: string | null }) {
|
||||
return (
|
||||
<div>
|
||||
<h1 className="font-display text-3xl tracking-tight text-balance">
|
||||
<h1 className="text-balance font-display text-3xl tracking-tight">
|
||||
Welcome back{name ? `, ${name}` : ""}
|
||||
</h1>
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
<p className="mt-1 text-muted-foreground text-sm">
|
||||
Here's what's happening with your library
|
||||
</p>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user