Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,11 @@ Aidventure is an MVP web application that helps adventure racers generate and ma
- ✅ Complete type definitions for checklists / items (`src/types/checklist.ts`)
- ✅ LocalStorage persistence layer with versioning + cross-tab sync
- ✅ Zustand state management with full CRUD operations
- ✅ Checklist UI: category grouping, add/edit/delete items, inline editing, bulk complete/reset, progress metrics (see `CHECKLIST_UI.md`)
- ✅ Accessibility basics: keyboard flows, ARIA labels on interactive elements
- ✅ Checklist UI: category-based organization, add/edit/delete items, inline editing, bulk complete/reset, progress metrics
- ✅ Category labels displayed and editable from both checklist view and item edit mode
- ✅ In-page confirmations for all delete actions (checklist and items)
- ✅ Hover underlines on clickable checklist and item names
- ✅ Accessibility basics: keyboard flows, ARIA labels on interactive elements, no popups for confirmations
- ✅ Comprehensive tests for storage + state layers (Vitest + RTL)
- ✅ Docker setup for consistent development

Expand Down
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@ Aidventure is a React-based web application designed to help adventure racers cr
- Complete type system for checklists and items
- LocalStorage persistence layer with cross-tab synchronization
- Zustand state management with full CRUD operations
- Checklist UI (category grouping, add/edit/delete items, progress, bulk actions) – see `frontend/CHECKLIST_UI.md`
- Checklist UI with category-based organization, inline editing, and delete confirmations
- Category labels displayed and editable from both checklist view and item scope
- In-page confirmations for all delete actions (no popups)
- Visual feedback with hover underlines on clickable elements
- Comprehensive test coverage (storage + state management; UI tests upcoming)
- Docker development environment with hot reload
- Complete linting and formatting setup
Expand Down
6 changes: 0 additions & 6 deletions frontend/src/components/ChecklistDemo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,37 +32,32 @@ export function ChecklistDemo() {
await addItem(checklist.id, {
name: 'Topographic maps',
category: 'Navigation',
priority: 'high',
completed: false,
});

await addItem(checklist.id, {
name: 'Compass',
category: 'Navigation',
priority: 'high',
completed: false,
});

await addItem(checklist.id, {
name: 'Energy gels',
category: 'Nutrition',
quantity: 10,
priority: 'normal',
completed: false,
});

await addItem(checklist.id, {
name: 'Water bottles',
category: 'Hydration',
quantity: 2,
priority: 'high',
completed: false,
});

await addItem(checklist.id, {
name: 'First aid kit',
category: 'Emergency',
priority: 'high',
completed: false,
});

Expand Down Expand Up @@ -157,7 +152,6 @@ export function ChecklistDemo() {
</p>
<p className="text-sm text-gray-600">
{item.category}
{item.priority && ` • ${item.priority}`}
</p>
</div>
</div>
Expand Down
45 changes: 15 additions & 30 deletions frontend/src/components/checklist/AddItemForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,16 @@ import type { Item } from '../../types/checklist';

interface AddItemFormProps {
checklistId: string;
category: string;
category?: string; // Optional, not used but kept for backward compatibility
onAdd: (checklistId: string, item: Omit<Item, 'id'>) => Promise<void>;
onCancel?: () => void;
}

export function AddItemForm({ checklistId, category, onAdd, onCancel }: AddItemFormProps) {
export function AddItemForm({ checklistId, onAdd, onCancel }: AddItemFormProps) {
Comment on lines 4 to +11
Copy link

Copilot AI Dec 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The category parameter is still defined but no longer used in this component. The comment on line 6 acknowledges this is for "backward compatibility," but since this is an internal component (not a public API), there's no need to maintain backward compatibility. The parameter should be removed entirely from the interface and the function signature to keep the code clean and avoid confusion.

Copilot uses AI. Check for mistakes.
const [name, setName] = useState('');
const [notes, setNotes] = useState('');
const [quantity, setQuantity] = useState('1');
const [priority, setPriority] = useState<'high' | 'normal' | 'optional'>('normal');
const [selectedCategory, setSelectedCategory] = useState(category);
const [selectedCategory, setSelectedCategory] = useState('');
const [isSubmitting, setIsSubmitting] = useState(false);
const inputRef = useRef<HTMLInputElement>(null);

Expand All @@ -32,18 +31,17 @@ export function AddItemForm({ checklistId, category, onAdd, onCancel }: AddItemF
try {
await onAdd(checklistId, {
name: name.trim(),
category: selectedCategory,
category: selectedCategory.trim() || undefined,
notes: notes.trim() || undefined,
quantity: quantity ? parseInt(quantity, 10) : undefined,
priority,
completed: false,
});

// Reset form
setName('');
setNotes('');
setQuantity('1');
setPriority('normal');
setSelectedCategory('');
inputRef.current?.focus();
} finally {
setIsSubmitting(false);
Expand Down Expand Up @@ -92,29 +90,16 @@ export function AddItemForm({ checklistId, category, onAdd, onCancel }: AddItemF
aria-label="Notes"
/>

<div className="flex gap-2">
<input
type="number"
value={quantity}
onChange={(e) => setQuantity(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="Quantity"
className="w-24 px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-accent"
aria-label="Quantity"
min="1"
/>

<select
value={priority}
onChange={(e) => setPriority(e.target.value as 'high' | 'normal' | 'optional')}
className="flex-1 px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-accent"
aria-label="Priority"
>
<option value="high">High Priority</option>
<option value="normal">Normal</option>
<option value="optional">Optional</option>
</select>
</div>
<input
type="number"
value={quantity}
onChange={(e) => setQuantity(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="Quantity"
className="w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-accent"
aria-label="Quantity"
min="1"
/>

<div className="flex gap-2">
<button
Expand Down
89 changes: 55 additions & 34 deletions frontend/src/components/checklist/ChecklistOverview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ export function ChecklistOverview() {
setIsCreatingChecklist(false);
};

// TODO: [Accessibility] Using confirm() is not ideal for accessibility. Replace with a proper modal dialog component that supports keyboard navigation and screen readers.
const handleDeleteChecklist = async (id: string, name: string) => {
const handleDeleteChecklist = async (e: React.MouseEvent, id: string, name: string) => {
e.stopPropagation(); // Prevent opening the checklist
if (
window.confirm(`Are you sure you want to delete "${name}"? This action cannot be undone.`)
) {
Expand All @@ -49,6 +49,11 @@ export function ChecklistOverview() {
}
};

const handleEditChecklist = (e: React.MouseEvent, id: string) => {
e.stopPropagation(); // Prevent opening the checklist
setCurrentChecklist(id);
};

const handleSelectChecklist = (id: string) => {
setCurrentChecklist(id);
};
Expand Down Expand Up @@ -182,56 +187,72 @@ export function ChecklistOverview() {
return (
<div
key={checklist.id}
className="bg-white rounded-lg border border-gray-200 p-4 hover:shadow-md transition-shadow relative"
className="bg-white rounded-lg border border-gray-200 p-4 hover:shadow-md transition-shadow relative group"
>
{/* Delete Button */}
<button
onClick={() => handleDeleteChecklist(checklist.id, checklist.name)}
disabled={isDeleting}
className="absolute top-2 right-2 p-1 text-gray-400 hover:text-red-600 focus:outline-none focus:ring-2 focus:ring-red-500 rounded"
aria-label={`Delete ${checklist.name}`}
>
{isDeleting ? (
<div className="w-4 h-4 border-2 border-gray-300 border-t-red-600 rounded-full animate-spin" />
) : (
{/* Edit and Delete Buttons */}
<div className="absolute top-2 right-2 flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
<button
onClick={(e) => handleEditChecklist(e, checklist.id)}
className="p-1 text-gray-400 hover:text-accent focus:outline-none focus:ring-2 focus:ring-accent rounded"
aria-label={`Edit ${checklist.name}`}
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"
/>
</svg>
)}
</button>
</button>
<button
onClick={(e) => handleDeleteChecklist(e, checklist.id, checklist.name)}
disabled={isDeleting}
className="p-1 text-gray-400 hover:text-red-600 focus:outline-none focus:ring-2 focus:ring-red-500 rounded"
aria-label={`Delete ${checklist.name}`}
>
{isDeleting ? (
<div className="w-4 h-4 border-2 border-gray-300 border-t-red-600 rounded-full animate-spin" />
) : (
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
/>
</svg>
)}
</button>
</div>

{/* Checklist Content */}
<div className="cursor-pointer" onClick={() => handleSelectChecklist(checklist.id)}>
<h3 className="text-lg font-semibold text-gray-900 mb-2 pr-8">
<h3 className="text-lg font-semibold text-gray-900 mb-2 pr-16 hover:underline">
{checklist.name}
</h3>

<div className="space-y-2 mb-4">
<div className="flex items-center justify-between text-sm text-gray-600">
<span>
{completedItems} of {totalItems} items completed
</span>
<span className="font-medium">{completionPercentage}%</span>
</div>

<div className="w-full bg-gray-200 rounded-full h-2">
<div
className="bg-success h-2 rounded-full transition-all"
style={{ width: `${completionPercentage}%` }}
/>
</div>
<div className="space-y-2 mb-4">
<div className="flex items-center justify-between text-sm text-gray-600">
<span>
{completedItems} of {totalItems} items completed
</span>
<span className="font-medium">{completionPercentage}%</span>
</div>

<div className="flex items-center justify-between text-xs text-gray-500">
<span>Created: {formatDate(checklist.createdAt)}</span>
<span>Updated: {formatDate(checklist.updatedAt)}</span>
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className="bg-success h-2 rounded-full transition-all"
style={{ width: `${completionPercentage}%` }}
/>
</div>
</div>

<div className="flex items-center justify-between text-xs text-gray-500">
<span>Created: {formatDate(checklist.createdAt)}</span>
<span>Updated: {formatDate(checklist.updatedAt)}</span>
</div>
</div>
</div>
);
})}
Expand Down
64 changes: 57 additions & 7 deletions frontend/src/components/checklist/ChecklistPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,13 @@ export function ChecklistPage() {
deleteItem,
addItem,
setCurrentChecklist,
deleteChecklist,
} = useChecklistStore();

const [isAddingItem, setIsAddingItem] = useState(false);
const [isCreatingChecklist, setIsCreatingChecklist] = useState(false);
const [newChecklistName, setNewChecklistName] = useState('');
const [isDeletingChecklist, setIsDeletingChecklist] = useState(false);

useEffect(() => {
loadChecklists();
Expand Down Expand Up @@ -48,6 +50,22 @@ export function ChecklistPage() {
setIsAddingItem(false);
};

const handleDeleteChecklistClick = () => {
setIsDeletingChecklist(true);
};

const handleConfirmDeleteChecklist = async () => {
if (currentChecklistId) {
await deleteChecklist(currentChecklistId);
setCurrentChecklist(null);
setIsDeletingChecklist(false);
}
};

const handleCancelDeleteChecklist = () => {
setIsDeletingChecklist(false);
};

if (isLoading && checklists.length === 0) {
return (
<div className="flex items-center justify-center h-64">
Expand Down Expand Up @@ -93,12 +111,21 @@ export function ChecklistPage() {
{currentChecklist?.name || 'Packing Checklist'}
</h1>
</div>
<button
onClick={() => setIsCreatingChecklist(true)}
className="px-4 py-2 bg-accent text-white rounded-lg hover:opacity-90 focus:outline-none focus:ring-2 focus:ring-accent"
>
New Checklist
</button>
{currentChecklist ? (
<button
onClick={handleDeleteChecklistClick}
className="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-600"
>
Delete Checklist
</button>
) : (
<button
onClick={() => setIsCreatingChecklist(true)}
className="px-4 py-2 bg-accent text-white rounded-lg hover:opacity-90 focus:outline-none focus:ring-2 focus:ring-accent"
>
New Checklist
</button>
)}
</div>

{currentChecklist && (
Expand All @@ -118,6 +145,29 @@ export function ChecklistPage() {
)}
</div>

{/* Delete Checklist Confirmation */}
{isDeletingChecklist && currentChecklist && (
<div className="mb-6 p-4 bg-red-50 border border-red-500 rounded-lg">
<p className="text-sm text-gray-900 mb-3">
Are you sure you want to delete "{currentChecklist.name}"? This action cannot be undone.
</p>
<div className="flex gap-2">
<button
onClick={handleConfirmDeleteChecklist}
className="px-4 py-2 bg-red-600 text-white rounded-md hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-600"
>
Delete Checklist
</button>
<button
onClick={handleCancelDeleteChecklist}
className="px-4 py-2 bg-gray-100 text-gray-700 rounded-md hover:bg-gray-200 focus:outline-none focus:ring-2 focus:ring-gray-400"
>
Cancel
</button>
</div>
</div>
)}

{/* Create Checklist Form */}
{isCreatingChecklist && (
<div className="mb-6 bg-white p-4 rounded-lg border border-gray-200 shadow-sm">
Expand Down Expand Up @@ -218,7 +268,7 @@ export function ChecklistPage() {
{isAddingItem ? (
<AddItemForm
checklistId={currentChecklist.id}
category="Miscellaneous"
category=""
Copy link

Copilot AI Dec 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the category parameter is no longer used in AddItemForm, passing it here (even as an empty string) is unnecessary and creates dead code. Remove the category="" prop entirely to match the updated component interface.

Suggested change
category=""

Copilot uses AI. Check for mistakes.
onAdd={handleAddItem}
onCancel={() => setIsAddingItem(false)}
/>
Expand Down
Loading