import Image from "next/image"; import PixelImage from "./PixelImage"; import Link from "next/link"; import { formatCompactNumber } from "@/utils/format"; import { useBaseRoms } from "@/contexts/BaseRomContext"; import { baseRoms } from "@/data/baseRoms"; import { useEffect, useRef, useState } from "react"; import useEmblaCarousel from "embla-carousel-react"; import { usePathname } from "next/navigation"; import { FaRegImages } from "react-icons/fa6"; import { ImDownload } from "react-icons/im"; type CardHack = { slug: string; title: string; author: string; covers: string[]; tags: string[]; downloads: number; baseRomId?: string; version: string; summary?: string; description?: string; }; export default function HackCard({ hack, clickable = true, className = "" }: { hack: CardHack; clickable?: boolean; className?: string }) { const { isLinked, hasPermission, hasCached } = useBaseRoms(); const match = baseRoms.find((r) => r.id === hack.baseRomId); const baseId = match?.id ?? undefined; const baseName = match?.name ?? undefined; const linked = baseId ? isLinked(baseId) : false; const ready = baseId ? hasPermission(baseId) || hasCached(baseId) : false; const images = (hack.covers && hack.covers.length > 0 ? hack.covers : []).filter(Boolean); const isCarousel = images.length > 1; const pathname = usePathname(); const showTitlePlaceholder = (pathname || "").startsWith("/submit") && images.length === 0; const [emblaRef, emblaApi] = useEmblaCarousel({ loop: true }); const [selectedIndex, setSelectedIndex] = useState(0); const dragStartRef = useRef<{ x: number; y: number } | null>(null); const didDragRef = useRef(false); useEffect(() => { if (!emblaApi) return; const onSelect = () => setSelectedIndex(emblaApi.selectedScrollSnap()); emblaApi.on("select", onSelect); onSelect(); return () => { emblaApi.off("select", onSelect); }; }, [emblaApi]); const cardClass = `rounded-[12px] overflow-hidden ${ clickable ? "transition-transform duration-300 hover:-translate-y-0.5 hover:shadow-xl anim-float" : "" } ring-1 ${ready ? "ring-emerald-400/50 bg-emerald-500/10" : "card ring-[var(--border)]"}`; const gradientBgClass = `bg-gradient-to-b ${ready ? 'from-emerald-300/5 to-emerald-400/30 dark:from-emerald-950/10 dark:to-emerald-600/40' : 'from-black/30 to-black/10 dark:from-black/80 dark:to-black/40'}`; const shadowClass = `shadow-xl ${ready ? "shadow-emerald-700/40 dark:shadow-emerald-200/40" : "shadow-slate-500/40 dark:shadow-slate-300/40"}`; const content = (
{showTitlePlaceholder ? (
) : isCarousel ? (
{ dragStartRef.current = { x: e.clientX, y: e.clientY }; didDragRef.current = false; try { (e.currentTarget as any).setPointerCapture?.(e.pointerId); } catch {} }} onPointerMove={(e) => { const start = dragStartRef.current; if (!start) return; const dx = e.clientX - start.x; const dy = e.clientY - start.y; if (!didDragRef.current && dx * dx + dy * dy > 25) { didDragRef.current = true; // movement > 5px } }} onPointerUp={(e) => { dragStartRef.current = null; try { (e.currentTarget as any).releasePointerCapture?.(e.pointerId); } catch {} }} onPointerCancel={() => { dragStartRef.current = null; }} >
{images.map((src, idx) => (
))}
) : images[0] ? (
) : null}
{hack.tags.slice(0, 2).map((t) => ( {t} ))} {ready ? "Ready" : linked ? "Permission needed" : "Base ROM needed"}
{isCarousel && (
{images.map((_, i) => (
)}

{hack.title}

{hack.version}

By {hack.author}

{formatCompactNumber(hack.downloads)}

{(() => { const text = (hack as any).summary ?? (hack as any).description ?? ""; return text.length > 120 ? text.slice(0, 120).trimEnd() + "…" : text; })()}

Base: {baseName ?? "Unknown"}
); if (clickable) { return ( { e.preventDefault(); }} onClick={(e) => { if (didDragRef.current) { e.preventDefault(); e.stopPropagation(); didDragRef.current = false; } }} > {content} ); } return
{content}
; }