mirror of
https://github.com/jakejarvis/sofa.git
synced 2026-08-29 03:55:38 -04:00
Fix updateTag during render, hide scrollbars, polish type badge and hover styles
- Add `revalidate` option to `refreshCredits` and `refreshRecommendations`;
pass `{ revalidate: false }` from `ensureEnriched` to avoid calling
`updateTag` outside Server Actions/Route Handlers
- Add `hideScrollbar` prop to all horizontal ScrollArea instances
(continue-watching, title rows, filterable row, cast carousel)
- Replace text-only type badge in HeroBanner with icon + label using
IconMovie / IconDeviceTv
- Fix StatusButton destructive hover overrides with `!important` so
they correctly supersede the status-specific background/text/ring
This commit is contained in:
@@ -12,6 +12,7 @@ export function ContinueWatchingList({
|
||||
return (
|
||||
<ScrollArea
|
||||
scrollFade
|
||||
hideScrollbar
|
||||
className="-mx-4 sm:-mx-0 [&_[data-slot=scroll-area-content]]:px-px"
|
||||
>
|
||||
<div className="flex gap-4 px-4 py-2 sm:px-0">
|
||||
|
||||
@@ -101,7 +101,7 @@ export function FilterableTitleRow({
|
||||
</div>
|
||||
|
||||
{/* Genre chips */}
|
||||
<ScrollArea scrollFade>
|
||||
<ScrollArea scrollFade hideScrollbar>
|
||||
<div className="flex gap-2">
|
||||
{genres.map((genre) => (
|
||||
<Button
|
||||
@@ -148,6 +148,7 @@ export function FilterableTitleRow({
|
||||
<ScrollArea
|
||||
key={selectedGenre ?? "default"}
|
||||
scrollFade
|
||||
hideScrollbar
|
||||
className="-mx-6 sm:-mx-2"
|
||||
>
|
||||
<div className="flex gap-4 px-6 py-2 sm:px-2">
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
"use client";
|
||||
|
||||
import { IconPlus, IconStar } from "@tabler/icons-react";
|
||||
import {
|
||||
IconDeviceTv,
|
||||
IconMovie,
|
||||
IconPlus,
|
||||
IconStar,
|
||||
} from "@tabler/icons-react";
|
||||
import Image from "next/image";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useTransition } from "react";
|
||||
@@ -65,8 +70,18 @@ export function HeroBanner({
|
||||
style={{ "--stagger-index": 3 } as React.CSSProperties}
|
||||
>
|
||||
<div className="mb-3 flex items-center gap-2">
|
||||
<span className="rounded bg-primary/20 px-2 py-0.5 font-semibold text-[10px] text-primary uppercase tracking-wider">
|
||||
{type}
|
||||
<span className="inline-flex cursor-default items-center justify-center gap-1 rounded bg-primary/10 px-1.5 py-1 font-medium text-primary text-xs">
|
||||
{type === "movie" ? (
|
||||
<>
|
||||
<IconMovie aria-hidden className="size-3.5" />
|
||||
Movie
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<IconDeviceTv aria-hidden className="size-3.5" />
|
||||
TV
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
{voteAverage > 0 && (
|
||||
<span className="flex items-center gap-1 text-primary text-sm">
|
||||
|
||||
@@ -35,7 +35,7 @@ export function TitleRow({
|
||||
{heading}
|
||||
</h2>
|
||||
</div>
|
||||
<ScrollArea scrollFade className="-mx-6 sm:-mx-2">
|
||||
<ScrollArea scrollFade hideScrollbar className="-mx-6 sm:-mx-2">
|
||||
<div className="flex gap-4 px-6 py-2 sm:px-2">
|
||||
{items.map((item, i) => (
|
||||
<div
|
||||
|
||||
@@ -18,7 +18,7 @@ export function CastCarousel({ actors, titleType }: CastCarouselProps) {
|
||||
</div>
|
||||
|
||||
{actors.length > 0 && (
|
||||
<ScrollArea scrollFade className="-mx-4 sm:-mx-0">
|
||||
<ScrollArea scrollFade hideScrollbar className="-mx-4 sm:-mx-0">
|
||||
<div className="flex gap-4 px-4 py-2 sm:px-0">
|
||||
{actors.map((member, i) => (
|
||||
<div key={member.id} className="w-[100px] shrink-0 sm:w-[120px]">
|
||||
|
||||
@@ -63,7 +63,7 @@ export function StatusButton({ currentStatus, onChange }: StatusButtonProps) {
|
||||
exit={{ opacity: 0, y: -4 }}
|
||||
transition={{ duration: 0.15 }}
|
||||
title="Remove from library"
|
||||
className={`group inline-flex h-9 items-center gap-2 rounded-lg px-4 font-medium text-sm ring-1 transition-all active:scale-[0.97] ${config.class} ${config.bgClass} ${config.borderClass} hover:bg-destructive/10 hover:text-destructive hover:ring-destructive/30`}
|
||||
className={`group inline-flex h-9 items-center gap-2 rounded-lg px-4 font-medium text-sm ring-1 transition-all active:scale-[0.97] ${config.class} ${config.bgClass} ${config.borderClass} hover:!bg-destructive/10 hover:!text-destructive hover:!ring-destructive/30`}
|
||||
>
|
||||
<span className="grid [&>svg]:col-start-1 [&>svg]:row-start-1">
|
||||
<config.icon
|
||||
|
||||
@@ -81,7 +81,10 @@ function batchUpsertPersons(people: PersonData[]): Map<number, string> {
|
||||
return idMap;
|
||||
}
|
||||
|
||||
export async function refreshCredits(titleId: string) {
|
||||
export async function refreshCredits(
|
||||
titleId: string,
|
||||
{ revalidate = true }: { revalidate?: boolean } = {},
|
||||
) {
|
||||
const title = db.select().from(titles).where(eq(titles.id, titleId)).get();
|
||||
if (!title) return;
|
||||
|
||||
@@ -273,9 +276,11 @@ export async function refreshCredits(titleId: string) {
|
||||
}
|
||||
}
|
||||
|
||||
if (revalidate) {
|
||||
for (const personId of personIds.values()) {
|
||||
updateTag(`person-${personId}`);
|
||||
}
|
||||
}
|
||||
|
||||
log.debug(`Credits refreshed for "${title.title}"`);
|
||||
|
||||
|
||||
@@ -440,7 +440,10 @@ export async function refreshTvChildren(
|
||||
}
|
||||
}
|
||||
|
||||
export async function refreshRecommendations(titleId: string) {
|
||||
export async function refreshRecommendations(
|
||||
titleId: string,
|
||||
{ revalidate = true }: { revalidate?: boolean } = {},
|
||||
) {
|
||||
const title = db.select().from(titles).where(eq(titles.id, titleId)).get();
|
||||
if (!title) return;
|
||||
|
||||
@@ -575,7 +578,9 @@ export async function refreshRecommendations(titleId: string) {
|
||||
}
|
||||
});
|
||||
|
||||
if (revalidate) {
|
||||
updateTag(`recs-${titleId}`);
|
||||
}
|
||||
}
|
||||
|
||||
/** Fetch seasons from the DB, building the Season[] structure. */
|
||||
@@ -684,9 +689,12 @@ async function ensureEnriched(
|
||||
): Promise<boolean> {
|
||||
const tasks: Promise<void>[] = [];
|
||||
|
||||
// Called during render — skip updateTag calls (not allowed outside Server Actions/Route Handlers)
|
||||
const noRevalidate = { revalidate: false } as const;
|
||||
|
||||
if (!existing.hasCast) {
|
||||
tasks.push(
|
||||
refreshCredits(titleId).catch((err) =>
|
||||
refreshCredits(titleId, noRevalidate).catch((err) =>
|
||||
log.debug("Credits enrichment failed:", err),
|
||||
),
|
||||
);
|
||||
@@ -710,7 +718,7 @@ async function ensureEnriched(
|
||||
.get() != null;
|
||||
if (!hasRecommendations) {
|
||||
tasks.push(
|
||||
refreshRecommendations(titleId).catch((err) =>
|
||||
refreshRecommendations(titleId, noRevalidate).catch((err) =>
|
||||
log.debug("Recommendations enrichment failed:", err),
|
||||
),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user