145 lines
5.0 KiB
TypeScript
145 lines
5.0 KiB
TypeScript
'use client';
|
|
|
|
import { useState } from 'react';
|
|
import { SortableContext, rectSortingStrategy } from '@dnd-kit/sortable';
|
|
import { useDroppable } from '@dnd-kit/core';
|
|
import SortableCounter from './SortableCounter';
|
|
import { Counter } from './CounterCard';
|
|
import { Group } from './CounterModal';
|
|
|
|
interface Props {
|
|
group: Group | null; // null = ungrouped section
|
|
counters: Counter[];
|
|
onIncrement: (id: number) => void;
|
|
onDecrement: (id: number) => void;
|
|
onEdit: (counter: Counter) => void;
|
|
onHistory: (id: number) => void;
|
|
onPrefetch?: (id: number) => void;
|
|
editMode: boolean;
|
|
dragHandleProps?: Record<string, unknown>;
|
|
onRenameGroup?: (group: Group) => void;
|
|
onDeleteGroup?: (id: number) => void;
|
|
}
|
|
|
|
export default function GroupSection({
|
|
group,
|
|
counters,
|
|
onIncrement,
|
|
onDecrement,
|
|
onEdit,
|
|
onHistory,
|
|
onPrefetch,
|
|
editMode,
|
|
dragHandleProps,
|
|
onRenameGroup,
|
|
onDeleteGroup,
|
|
}: Props) {
|
|
const droppableId = group ? `group-${group.id}` : 'ungrouped';
|
|
const { setNodeRef, isOver } = useDroppable({ id: droppableId, data: { groupId: group?.id ?? null } });
|
|
const [editingName, setEditingName] = useState(false);
|
|
const [newName, setNewName] = useState(group?.name ?? '');
|
|
const [confirmDelete, setConfirmDelete] = useState(false);
|
|
|
|
function commitRename() {
|
|
if (group && newName.trim() && newName.trim() !== group.name) {
|
|
onRenameGroup?.({ ...group, name: newName.trim() });
|
|
}
|
|
setEditingName(false);
|
|
}
|
|
|
|
const isEmpty = counters.length === 0;
|
|
|
|
return (
|
|
<div className="mb-8">
|
|
{/* Section header */}
|
|
<div className="flex items-center gap-3 mb-3">
|
|
{group && editMode && dragHandleProps && (
|
|
<div
|
|
{...dragHandleProps}
|
|
className="cursor-grab active:cursor-grabbing text-ctp-overlay0 hover:text-ctp-subtext1 transition-colors shrink-0"
|
|
title="Drag to reorder group"
|
|
>
|
|
<svg xmlns="http://www.w3.org/2000/svg" className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
<line x1="8" y1="6" x2="21" y2="6" /><line x1="8" y1="12" x2="21" y2="12" /><line x1="8" y1="18" x2="21" y2="18" />
|
|
<line x1="3" y1="6" x2="3.01" y2="6" /><line x1="3" y1="12" x2="3.01" y2="12" /><line x1="3" y1="18" x2="3.01" y2="18" />
|
|
</svg>
|
|
</div>
|
|
)}
|
|
{group ? (
|
|
editingName ? (
|
|
<input
|
|
autoFocus
|
|
value={newName}
|
|
onChange={e => setNewName(e.target.value)}
|
|
onBlur={commitRename}
|
|
onKeyDown={e => { if (e.key === 'Enter') commitRename(); if (e.key === 'Escape') setEditingName(false); }}
|
|
className="text-lg font-bold bg-transparent border-b-2 border-ctp-mauve outline-none text-ctp-text"
|
|
/>
|
|
) : (
|
|
editMode ? (
|
|
<h2
|
|
className="text-lg font-bold text-ctp-text cursor-pointer hover:text-ctp-mauve transition-colors"
|
|
title="Click to rename"
|
|
onClick={() => { setNewName(group.name); setEditingName(true); }}
|
|
>
|
|
{group.name}
|
|
</h2>
|
|
) : (
|
|
<h2 className="text-lg font-bold text-ctp-text">{group.name}</h2>
|
|
)
|
|
)
|
|
) : (
|
|
<h2 className="text-lg font-bold text-ctp-overlay1">Ungrouped</h2>
|
|
)}
|
|
|
|
{group && editMode && (
|
|
<button
|
|
onClick={() => {
|
|
if (confirmDelete) { onDeleteGroup?.(group.id); }
|
|
else {
|
|
setConfirmDelete(true);
|
|
setTimeout(() => setConfirmDelete(false), 3000);
|
|
}
|
|
}}
|
|
className={`text-xs px-2 py-0.5 rounded-full transition-colors ${
|
|
confirmDelete
|
|
? 'bg-ctp-red text-ctp-base'
|
|
: 'bg-ctp-surface0 text-ctp-overlay1 hover:bg-ctp-red/10 hover:text-ctp-red'
|
|
}`}
|
|
>
|
|
{confirmDelete ? 'Sure?' : 'Delete group'}
|
|
</button>
|
|
)}
|
|
</div>
|
|
|
|
{/* Drop zone */}
|
|
<div
|
|
ref={setNodeRef}
|
|
className={`grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-3 min-h-[80px] rounded-2xl transition-colors ${
|
|
isOver ? 'bg-ctp-mauve/10 ring-2 ring-ctp-mauve' : ''
|
|
} ${isEmpty ? 'border-2 border-dashed border-ctp-surface1 p-4' : ''}`}
|
|
>
|
|
<SortableContext items={counters.map(c => c.id)} strategy={rectSortingStrategy}>
|
|
{counters.map(counter => (
|
|
<SortableCounter
|
|
key={counter.id}
|
|
counter={counter}
|
|
onIncrement={onIncrement}
|
|
onDecrement={onDecrement}
|
|
onEdit={onEdit}
|
|
onHistory={onHistory}
|
|
onPrefetch={onPrefetch}
|
|
editMode={editMode}
|
|
/>
|
|
))}
|
|
</SortableContext>
|
|
{isEmpty && (
|
|
<p className="col-span-full text-center text-sm text-ctp-overlay0 self-center">
|
|
Drop counters here
|
|
</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|