Web app ready
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
'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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user