159 lines
5.3 KiB
TypeScript
159 lines
5.3 KiB
TypeScript
"use client";
|
|
|
|
import { useRef, useState, useEffect, useCallback } from "react";
|
|
import Link from "next/link";
|
|
|
|
type TrendingManga = {
|
|
slug: string;
|
|
title: string;
|
|
coverUrl: string;
|
|
genre: string;
|
|
};
|
|
|
|
function RankNumber({ rank }: { rank: number }) {
|
|
// Webtoon-style large rank number
|
|
const colors =
|
|
rank === 1
|
|
? "text-yellow-400"
|
|
: rank === 2
|
|
? "text-slate-300"
|
|
: rank === 3
|
|
? "text-amber-600"
|
|
: "text-white/70";
|
|
|
|
return (
|
|
<span
|
|
className={`text-[40px] font-black leading-none ${colors} drop-shadow-[0_2px_8px_rgba(0,0,0,0.8)]`}
|
|
style={{ fontFamily: "var(--font-sans), system-ui, sans-serif" }}
|
|
>
|
|
{rank}
|
|
</span>
|
|
);
|
|
}
|
|
|
|
export function TrendingCarousel({ manga }: { manga: TrendingManga[] }) {
|
|
const scrollRef = useRef<HTMLDivElement>(null);
|
|
const [canScrollLeft, setCanScrollLeft] = useState(false);
|
|
const [canScrollRight, setCanScrollRight] = useState(true);
|
|
|
|
const checkScroll = useCallback(() => {
|
|
const el = scrollRef.current;
|
|
if (!el) return;
|
|
setCanScrollLeft(el.scrollLeft > 4);
|
|
setCanScrollRight(el.scrollLeft < el.scrollWidth - el.clientWidth - 4);
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
const el = scrollRef.current;
|
|
if (!el) return;
|
|
checkScroll();
|
|
el.addEventListener("scroll", checkScroll, { passive: true });
|
|
window.addEventListener("resize", checkScroll);
|
|
return () => {
|
|
el.removeEventListener("scroll", checkScroll);
|
|
window.removeEventListener("resize", checkScroll);
|
|
};
|
|
}, [checkScroll]);
|
|
|
|
function scroll(direction: "left" | "right") {
|
|
const el = scrollRef.current;
|
|
if (!el) return;
|
|
const amount = el.clientWidth * 0.8;
|
|
el.scrollBy({
|
|
left: direction === "left" ? -amount : amount,
|
|
behavior: "smooth",
|
|
});
|
|
}
|
|
|
|
if (manga.length === 0) return null;
|
|
|
|
return (
|
|
<section className="relative">
|
|
{/* Section header */}
|
|
<div className="flex items-center justify-between mb-4">
|
|
<div className="flex items-center gap-2.5">
|
|
<svg
|
|
viewBox="0 0 24 24"
|
|
fill="currentColor"
|
|
className="w-5 h-5 text-accent"
|
|
>
|
|
<polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2" />
|
|
</svg>
|
|
<h2 className="text-lg font-bold">Trending Now</h2>
|
|
</div>
|
|
|
|
{/* Desktop carousel arrows */}
|
|
<div className="hidden sm:flex items-center gap-1.5">
|
|
<button
|
|
onClick={() => scroll("left")}
|
|
disabled={!canScrollLeft}
|
|
className="w-8 h-8 flex items-center justify-center rounded-full bg-surface border border-border hover:bg-surface-hover disabled:opacity-25 disabled:cursor-default transition-all"
|
|
aria-label="Previous"
|
|
>
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2.5} className="w-4 h-4">
|
|
<polyline points="15 18 9 12 15 6" />
|
|
</svg>
|
|
</button>
|
|
<button
|
|
onClick={() => scroll("right")}
|
|
disabled={!canScrollRight}
|
|
className="w-8 h-8 flex items-center justify-center rounded-full bg-surface border border-border hover:bg-surface-hover disabled:opacity-25 disabled:cursor-default transition-all"
|
|
aria-label="Next"
|
|
>
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2.5} className="w-4 h-4">
|
|
<polyline points="9 18 15 12 9 6" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Carousel track */}
|
|
<div
|
|
ref={scrollRef}
|
|
className="flex gap-3 overflow-x-auto scroll-smooth snap-x snap-mandatory no-scrollbar"
|
|
style={{ WebkitOverflowScrolling: "touch" }}
|
|
>
|
|
{manga.map((m, i) => (
|
|
<Link
|
|
key={m.slug}
|
|
href={`/manga/${m.slug}`}
|
|
className="group shrink-0 snap-start"
|
|
style={{ width: "clamp(150px, 40vw, 185px)" }}
|
|
>
|
|
<div className="relative aspect-[3/4] rounded-2xl overflow-hidden bg-card">
|
|
<img
|
|
src={m.coverUrl}
|
|
alt={m.title}
|
|
className="w-full h-full object-cover transition-transform duration-300 group-hover:scale-105"
|
|
loading={i < 5 ? "eager" : "lazy"}
|
|
/>
|
|
{/* Gradient overlay */}
|
|
<div className="absolute inset-0 bg-gradient-to-t from-black/90 via-black/20 to-transparent" />
|
|
|
|
{/* Rank number - bottom left, Webtoon style */}
|
|
<div className="absolute bottom-0 left-0 right-0 p-3">
|
|
<div className="flex items-end gap-2">
|
|
<RankNumber rank={i + 1} />
|
|
<div className="flex-1 min-w-0 pb-1">
|
|
<h3 className="text-[13px] font-bold text-white leading-tight line-clamp-2 drop-shadow-md">
|
|
{m.title}
|
|
</h3>
|
|
<p className="text-[11px] text-white/50 mt-0.5">
|
|
{m.genre}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Link>
|
|
))}
|
|
</div>
|
|
|
|
{/* Mobile edge fades */}
|
|
{canScrollRight && (
|
|
<div className="absolute right-0 top-12 bottom-0 w-6 bg-gradient-to-l from-background to-transparent pointer-events-none sm:hidden" />
|
|
)}
|
|
</section>
|
|
);
|
|
}
|