"use client"; import { useEffect, useRef, useState, useTransition } from "react"; import { useRouter } from "next/navigation"; import Link from "next/link"; import { Pencil, User, Image as ImageIcon, ArrowLeft } from "lucide-react"; import { ActressPortraitEditor } from "./ActressPortraitEditor"; import { ActressMetaEditor } from "./ActressMetaEditor"; import { CategoryIcon } from "./CategoryIcon"; import { buildAltNameChips } from "@/lib/jav/nameUtils"; import type { ActressCategory, ActressAllPortraits, PortraitSlotKey } from "@/lib/db/queries"; import { toggleActressCategory } from "@/app/actions/actressCategories"; import { reorderActressPortraitSlots } from "@/app/actions/actressPortrait"; import { portraitUrl } from "@/lib/assetUrls"; interface Props { actress: { id: number; name: string; slug: string; altNames: string | null; notes: string | null; portraits: ActressAllPortraits; categories: ActressCategory[]; bornOn: string | null; heightCm: number | null; weightKg: number | null; cupSize: string | null; }; coverCount: number; allCategories: ActressCategory[]; } function computeAge(bornOn: string | null): number | null { if (!bornOn) return null; const d = new Date(bornOn); if (Number.isNaN(d.getTime())) return null; const now = new Date(); let age = now.getFullYear() - d.getFullYear(); const m = now.getMonth() - d.getMonth(); if (m < 0 || (m === 0 && now.getDate() < d.getDate())) age--; return age >= 0 ? age : null; } const PHI = 1.618; const FRAME_H = 308; const PORTRAIT_W = Math.floor(FRAME_H / PHI); const HORIZ_W = Math.floor(FRAME_H * PHI); const SLOT_LABEL: Record = { "1": "P1", "2": "P2", "3": "P3", "4": "P4", "h": "L" }; export function ActressHero({ actress, coverCount, allCategories }: Props) { const [editingMeta, setEditingMeta] = useState(false); const [editingSlot, setEditingSlot] = useState(null); const router = useRouter(); const [, start] = useTransition(); const altChips = buildAltNameChips(actress.name, actress.altNames); const ringColor = actress.categories[0]?.color ?? null; const activeIds = new Set(actress.categories.map((c) => c.id)); const orderedCategories = (() => { const list = [...allCategories]; const vipIdx = list.findIndex((c) => c.slug === "vip"); const favIdx = list.findIndex((c) => c.slug === "favorite"); if (vipIdx !== -1 && favIdx !== -1 && vipIdx > favIdx) { const [vip] = list.splice(vipIdx, 1); list.splice(favIdx, 0, vip); } return list; })(); function toggleCat(id: number) { start(async () => { await toggleActressCategory(actress.id, id); router.refresh(); }); } const dragSlotRef = useRef(null); const [dragSlot, setDragSlotState] = useState(null); function setDragSlot(s: PortraitSlotKey | null) { dragSlotRef.current = s; setDragSlotState(s); } const [overSlot, setOverSlot] = useState(null); const [optimistic, setOptimistic] = useState(null); const portraits = optimistic ?? actress.portraits; useEffect(() => { setOptimistic(null); }, [actress.portraits]); function handleDrop(target: PortraitSlotKey) { const src = dragSlotRef.current; setDragSlot(null); setOverSlot(null); if (!src || src === target) return; if (src === "h" || target === "h") return; const order: PortraitSlotKey[] = ["1", "2", "3", "4"]; const keyMap: Record<"1" | "2" | "3" | "4", "p1" | "p2" | "p3" | "p4"> = { "1": "p1", "2": "p2", "3": "p3", "4": "p4" }; const arr = order.map((k) => portraits[keyMap[k as "1" | "2" | "3" | "4"]]); const srcIdx = order.indexOf(src); const destIdx = order.indexOf(target); const [moved] = arr.splice(srcIdx, 1); arr.splice(destIdx, 0, moved); setOptimistic({ ...portraits, p1: arr[0], p2: arr[1], p3: arr[2], p4: arr[3] }); start(async () => { await reorderActressPortraitSlots(actress.id, src, target); router.refresh(); }); } return ( <>
All Actresses

{actress.name}

{altChips.length > 0 && (
{altChips.map((c) => ( {c.value} ))}
)}
setEditingSlot("1")} /> setEditingSlot("2")} /> setEditingSlot("3")} /> setEditingSlot("4")} /> setEditingSlot("h")} />
{allCategories.length > 0 && (
Categories
{orderedCategories.map((c) => { const active = activeIds.has(c.id); const color = c.color ?? "#888"; return ( ); })}
)} {actress.notes && (

{actress.notes}

)}
{editingMeta && ( setEditingMeta(false)} /> )} {editingSlot && ( setEditingSlot(null)} /> )} ); function PortraitFrame({ slot, data, width, height, onEdit, }: { slot: PortraitSlotKey; data: ActressAllPortraits[keyof ActressAllPortraits]; width: number; height: number; onEdit: () => void; }) { const reorderable = slot !== "h"; const isDragging = dragSlot === slot; const isDropTarget = reorderable && overSlot === slot && dragSlot !== null && dragSlot !== "h" && dragSlot !== slot; return (
{ if (!reorderable) return; dragSlotRef.current = slot; e.dataTransfer.effectAllowed = "move"; e.dataTransfer.setData("text/plain", slot); requestAnimationFrame(() => setDragSlotState(slot)); }} onDragEnd={() => { dragSlotRef.current = null; setDragSlotState(null); setOverSlot(null); }} onDragOver={(e) => { if (!reorderable) return; const ds = dragSlotRef.current; if (ds && ds !== "h" && ds !== slot) { e.preventDefault(); e.dataTransfer.dropEffect = "move"; } }} onDragEnter={() => { if (reorderable) setOverSlot(slot); }} onDragLeave={(e) => { if (e.currentTarget === e.target) setOverSlot((s) => (s === slot ? null : s)); }} onDrop={(e) => { e.preventDefault(); handleDrop(slot); }} > {data.path ? ( /* eslint-disable-next-line @next/next/no-img-element */ ) : (
)}
{SLOT_LABEL[slot]}
); } } function BioPanel({ actress, width, height, }: { actress: { bornOn: string | null; heightCm: number | null; weightKg: number | null; cupSize: string | null }; width: number; height: number; }) { const age = computeAge(actress.bornOn); return (
); } function Section({ title, children }: { title: string; children: React.ReactNode }) { return (
{title}
{children}
); } function BioRow({ label, value }: { label: string; value: string | null }) { return (
{label} {value ?? "—"}
); } function Stat({ label, value }: { label: string; value: number }) { return (
{value}
{label}
); }