'use client'; /** * The two drag-sortable tiles of the page builder (#608): a hero photo, and * a subpage in the page list. Album cards are in ./AlbumCard.tsx. */ import { useSortable } from '@dnd-kit/sortable'; import { useDroppable } from '@dnd-kit/core'; import { CSS } from '@dnd-kit/utilities'; import type { ReactNode } from 'react'; import { IconFileText, IconFolder, IconGripVertical, IconLock, IconPencil, IconX } from '../Icons'; import type { Subpage } from './types'; // ── Sortable Hero Tile ───────────────────────────────────────── export function SortableHeroTile({ id, index, onRemove, }: { id: string; index: number; onRemove: () => void; }) { const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: `hero-${index}`, }); const style = { transform: CSS.Transform.toString(transform), transition, opacity: isDragging ? 0.5 : 1, zIndex: isDragging ? 10 : undefined, }; return (
{index + 1}
); } // ── Sortable Subpage Tile ────────────────────────────────────── export function SortableSubpageTile({ sp, spIndex, isActive, onClick, getFirstThumb, }: { sp: Subpage; spIndex: number; isActive: boolean; onClick: () => void; getFirstThumb: (sp: Subpage) => string | null; }) { const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: `subpage-${spIndex}`, }); const style = { transform: CSS.Transform.toString(transform), transition, opacity: isDragging ? 0.5 : 1, zIndex: isDragging ? 10 : undefined, }; const totalAlbums = sp.albums.length + (sp.sections?.reduce((sum, sec) => sum + sec.albums.length, 0) || 0); const firstThumb = getFirstThumb(sp); const slug = sp.name .toLowerCase() .replace(/[^a-z0-9]+/g, '-') .replace(/^-|-$/g, ''); return (
{sp.enabled === false && ( Disabled )} {sp.hidden === true && sp.enabled !== false && ( Unlisted )} {sp.password && ( Password )}
{firstThumb ? ( ) : (
)}
Edit Page
{sp.title || sp.name} /{slug}
{totalAlbums} album{totalAlbums !== 1 ? 's' : ''}
); } // ── Component ────────────────────────────────────────────────── // ── Sortable Subpage Row (stage 4 structure list) ────────────── /** * One subpage in the page builder's structure list (UX stage 4): drag handle, * cover, name, state and album count, in one line. Same sortable id as the * tile it replaces, so the builder's drag handler is unchanged. */ export function SortableSubpageRow({ sp, spIndex, isActive, onClick, getFirstThumb, }: { sp: Subpage; spIndex: number; isActive: boolean; onClick: () => void; getFirstThumb: (sp: Subpage) => string | null; }) { const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: `subpage-${spIndex}`, }); const totalAlbums = sp.albums.length + (sp.sections?.reduce((sum, sec) => sum + sec.albums.length, 0) || 0); const thumb = getFirstThumb(sp); return (
); } // ── Sortable Page Row (#722) ─────────────────────────────────── /** * A content page in the structure list: document icon and a PAGE tag, so it * reads as something other than a subpage. The same row sits in the menu * list and in "Not in menu"; `id` says which, for the drag handler. */ export function SortablePageRow({ id, title, isActive, draft, hasPassword, missing, onClick, }: { id: string; title: string; isActive: boolean; draft?: boolean; hasPassword?: boolean; /** Referenced from gallery.yaml, but there is no file for it. */ missing?: boolean; onClick: () => void; }) { const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id, }); return (
); } /** * The "Not in menu" group as a drop target, so a page dragged out of the * menu lands there even when the group is empty (#722). */ export function OffMenuDropZone({ id, children }: { id: string; children: ReactNode }) { const { setNodeRef, isOver } = useDroppable({ id }); return (
{children}
); }