add checklists

This commit is contained in:
david 2026-02-27 19:12:01 +03:00
parent 799e089deb
commit d295a00e2f
8 changed files with 556 additions and 856 deletions

View file

@ -1,514 +0,0 @@
# Kanban API Documentation
This document describes all the API endpoints for the Kanban application. All endpoints require authentication unless otherwise noted.
## Base URL
```
http://localhost:5000/api
```
## Authentication
All endpoints (except register and login) require a JWT token in the Authorization header:
```
Authorization: Bearer <your_jwt_token>
```
## Boards
### Get All Boards
```
GET /api/boards
```
Get all boards for the current authenticated user.
**Response:** Array of board objects
### Get Single Board
```
GET /api/boards/<board_id>
```
Get a board with all its details including lists, cards, and labels.
**Response:** Board object with nested lists, cards, and labels
### Create Board
```
POST /api/boards
```
Create a new board.
**Request Body:**
```json
{
"name": "My Project Board",
"description": "Project management board",
"url": "https://example.com/board/123",
"short_link": "abc123",
"short_url": "https://example.com/b/abc123",
"prefs": {},
"label_names": {},
"limits": {}
}
```
**Response:** Created board object
### Update Board
```
PUT /api/boards/<board_id>
```
Update a board.
**Request Body:** Partial board object (only include fields to update)
**Response:** Updated board object
### Delete Board
```
DELETE /api/boards/<board_id>
```
Delete a board and all its associated data.
**Response:** Success message
## Lists
### Create List
```
POST /api/boards/<board_id>/lists
```
Create a new list in a board.
**Request Body:**
```json
{
"name": "To Do",
"pos": 0
}
```
**Response:** Created list object
### Update List
```
PUT /api/lists/<list_id>
```
Update a list.
**Request Body:** Partial list object
**Response:** Updated list object
### Delete List
```
DELETE /api/lists/<list_id>
```
Delete a list and all its cards.
**Response:** Success message
## Cards
### Create Card
```
POST /api/lists/<list_id>/cards
```
Create a new card in a list.
**Request Body:**
```json
{
"name": "Task Name",
"description": "Task description",
"pos": 0,
"due": "2026-12-31T23:59:59Z",
"due_complete": false,
"badges": {},
"cover": {},
"desc_data": {}
}
```
**Response:** Created card object
### Get Single Card
```
GET /api/cards/<card_id>
```
Get a card with full details including labels, checklists, and comments.
**Response:** Card object with nested labels, checklists, items, and comments
### Update Card
```
PUT /api/cards/<card_id>
```
Update a card. Can also move card to different list by providing `list_id`.
**Request Body:** Partial card object
**Response:** Updated card object
### Delete Card
```
DELETE /api/cards/<card_id>
```
Delete a card and all its associated data.
**Response:** Success message
## Labels
### Get Board Labels
```
GET /api/boards/<board_id>/labels
```
Get all labels for a board.
**Response:** Array of label objects
### Create Label
```
POST /api/boards/<board_id>/labels
```
Create a new label in a board.
**Request Body:**
```json
{
"name": "Urgent",
"color": "red"
}
```
**Response:** Created label object
### Add Label to Card
```
POST /api/cards/<card_id>/labels
```
Add a label to a card.
**Request Body:**
```json
{
"label_id": 1
}
```
**Response:** Created card-label association object
### Remove Label from Card
```
DELETE /api/cards/<card_id>/labels/<label_id>
```
Remove a label from a card.
**Response:** Success message
## Checklists
### Create Checklist
```
POST /api/cards/<card_id>/checklists
```
Create a new checklist in a card.
**Request Body:**
```json
{
"name": "Tasks",
"pos": 0
}
```
**Response:** Created checklist object
### Create Check Item
```
POST /api/checklists/<checklist_id>/items
```
Create a new check item in a checklist.
**Request Body:**
```json
{
"name": "Complete task",
"pos": 0,
"state": "incomplete",
"due": "2026-12-31T23:59:59Z"
}
```
**Response:** Created check item object
### Update Check Item
```
PUT /api/check-items/<item_id>
```
Update a check item (typically used to toggle state).
**Request Body:** Partial check item object
**Response:** Updated check item object
### Delete Checklist
```
DELETE /api/checklists/<checklist_id>
```
Delete a checklist and all its items.
**Response:** Success message
### Delete Check Item
```
DELETE /api/check-items/<item_id>
```
Delete a check item.
**Response:** Success message
## Comments
### Get Card Comments
```
GET /api/cards/<card_id>/comments
```
Get all comments for a card, ordered by creation date (newest first).
**Response:** Array of comment objects with user information
### Create Comment
```
POST /api/cards/<card_id>/comments
```
Create a new comment on a card.
**Request Body:**
```json
{
"text": "This is a comment"
}
```
**Response:** Created comment object
### Update Comment
```
PUT /api/comments/<comment_id>
```
Update a comment (only by the comment author).
**Request Body:** Partial comment object
**Response:** Updated comment object
### Delete Comment
```
DELETE /api/comments/<comment_id>
```
Delete a comment (only by the comment author).
**Response:** Success message
## Data Models
### Board
```json
{
"id": 1,
"name": "My Board",
"description": "Board description",
"closed": false,
"url": "https://example.com",
"short_link": "abc123",
"short_url": "https://example.com/b/abc123",
"user_id": 1,
"date_last_activity": "2026-01-01T00:00:00Z",
"date_last_view": "2026-01-01T00:00:00Z",
"created_at": "2026-01-01T00:00:00Z",
"updated_at": "2026-01-01T00:00:00Z",
"prefs": {},
"label_names": {},
"limits": {}
}
```
### List
```json
{
"id": 1,
"name": "To Do",
"closed": false,
"pos": 0,
"board_id": 1,
"created_at": "2026-01-01T00:00:00Z",
"updated_at": "2026-01-01T00:00:00Z"
}
```
### Card
```json
{
"id": 1,
"name": "Task Name",
"description": "Task description",
"closed": false,
"due": "2026-12-31T23:59:59Z",
"due_complete": false,
"pos": 0,
"id_short": 1,
"board_id": 1,
"list_id": 1,
"date_last_activity": "2026-01-01T00:00:00Z",
"created_at": "2026-01-01T00:00:00Z",
"updated_at": "2026-01-01T00:00:00Z",
"badges": {},
"cover": {},
"desc_data": {},
"labels": [],
"checklists": [],
"comments": []
}
```
### Label
```json
{
"id": 1,
"name": "Urgent",
"color": "red",
"uses": 0,
"board_id": 1,
"created_at": "2026-01-01T00:00:00Z",
"updated_at": "2026-01-01T00:00:00Z"
}
```
### Checklist
```json
{
"id": 1,
"name": "Tasks",
"pos": 0,
"board_id": 1,
"card_id": 1,
"created_at": "2026-01-01T00:00:00Z",
"updated_at": "2026-01-01T00:00:00Z",
"items": []
}
```
### Check Item
```json
{
"id": 1,
"name": "Complete task",
"pos": 0,
"state": "incomplete",
"due": "2026-12-31T23:59:59Z",
"checklist_id": 1,
"user_id": 1,
"created_at": "2026-01-01T00:00:00Z",
"updated_at": "2026-01-01T00:00:00Z"
}
```
### Comment
```json
{
"id": 1,
"text": "Comment text",
"card_id": 1,
"user_id": 1,
"created_at": "2026-01-01T00:00:00Z",
"updated_at": "2026-01-01T00:00:00Z",
"user": {}
}
```
## Common Response Codes
- `200 OK` - Request successful
- `201 Created` - Resource created successfully
- `400 Bad Request` - Invalid request data
- `401 Unauthorized` - Authentication required or invalid
- `403 Forbidden` - Access denied (not your resource)
- `404 Not Found` - Resource not found
- `500 Internal Server Error` - Server error
## Usage Examples
### Creating a complete board with lists and cards
```bash
# 1. Login to get token
curl -X POST http://localhost:5000/api/auth/login \
-H "Content-Type: application/json" \
-d '{"email": "user@example.com", "password": "password"}'
# 2. Create a board
curl -X POST http://localhost:5000/api/boards \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <token>" \
-d '{"name": "My Project"}'
# 3. Create a list
curl -X POST http://localhost:5000/api/boards/1/lists \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <token>" \
-d '{"name": "To Do", "pos": 0}'
# 4. Create a card
curl -X POST http://localhost:5000/api/lists/1/cards \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <token>" \
-d '{"name": "Task 1", "pos": 0}'
```
### Adding labels to a card
```bash
# 1. Create a label
curl -X POST http://localhost:5000/api/boards/1/labels \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <token>" \
-d '{"name": "Urgent", "color": "red"}'
# 2. Add label to card
curl -X POST http://localhost:5000/api/cards/1/labels \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <token>" \
-d '{"label_id": 1}'
```
### Creating a checklist with items
```bash
# 1. Create checklist
curl -X POST http://localhost:5000/api/cards/1/checklists \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <token>" \
-d '{"name": "Subtasks", "pos": 0}'
# 2. Add check items
curl -X POST http://localhost:5000/api/checklists/1/items \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <token>" \
-d '{"name": "Task 1", "pos": 0}'
# 3. Mark item as complete
curl -X PUT http://localhost:5000/api/check-items/1 \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <token>" \
-d '{"state": "complete"}'
```
### Adding comments
```bash
curl -X POST http://localhost:5000/api/cards/1/comments \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <token>" \
-d '{"text": "This is a comment"}'

View file

@ -1,300 +0,0 @@
# Kanban Backend Implementation Summary
## Overview
This document summarizes the Kanban backend implementation for the project. The backend provides a complete API for managing Kanban boards, lists, cards, labels, checklists, and comments.
## Database Schema
### Tables Created
1. **boards** - Main board entities
- id, name, description, closed, url, short_link, short_url
- user_id (foreign key to users)
- date_last_activity, date_last_view, created_at, updated_at
- JSONB fields: prefs, label_names, limits
- Relationships: lists, cards, labels
2. **lists** - Board columns/lists
- id, name, closed, pos
- board_id (foreign key to boards)
- created_at, updated_at
- Relationships: cards
3. **cards** - Task cards within lists
- id, name, description, closed, due, due_complete, pos, id_short
- board_id (foreign key to boards), list_id (foreign key to lists)
- date_last_activity, created_at, updated_at
- JSONB fields: badges, cover, desc_data
- Relationships: checklists, labels, comments
4. **labels** - Color-coded tags
- id, name, color, uses
- board_id (foreign key to boards)
- created_at, updated_at
- Relationships: card_labels
5. **card_labels** - Many-to-many relationship between cards and labels
- id, card_id, label_id
- Unique constraint on (card_id, label_id)
6. **checklists** - Checklists within cards
- id, name, pos
- board_id (foreign key to boards), card_id (foreign key to cards)
- created_at, updated_at
- Relationships: check_items
7. **check_items** - Items within checklists
- id, name, pos, state, due
- checklist_id (foreign key to checklists), user_id (foreign key to users)
- created_at, updated_at
8. **comments** - Comments on cards
- id, text
- card_id (foreign key to cards), user_id (foreign key to users)
- created_at, updated_at
## API Endpoints
### Boards (`/api/boards`)
- `GET /api/boards` - Get all boards for current user
- `GET /api/boards/<id>` - Get board with full details (lists, cards, labels)
- `POST /api/boards` - Create new board
- `PUT /api/boards/<id>` - Update board
- `DELETE /api/boards/<id>` - Delete board
### Lists (`/api/lists`, `/api/boards/<id>/lists`)
- `POST /api/boards/<board_id>/lists` - Create list in board
- `PUT /api/lists/<id>` - Update list
- `DELETE /api/lists/<id>` - Delete list
### Cards (`/api/cards`, `/api/lists/<id>/cards`)
- `POST /api/lists/<list_id>/cards` - Create card in list
- `GET /api/cards/<id>` - Get card with full details
- `PUT /api/cards/<id>` - Update card (includes moving between lists)
- `DELETE /api/cards/<id>` - Delete card
### Labels (`/api/boards/<id>/labels`, `/api/cards/<id>/labels`)
- `GET /api/boards/<board_id>/labels` - Get all labels for board
- `POST /api/boards/<board_id>/labels` - Create label
- `POST /api/cards/<card_id>/labels` - Add label to card
- `DELETE /api/cards/<card_id>/labels/<label_id>` - Remove label from card
### Checklists (`/api/checklists`, `/api/cards/<id>/checklists`)
- `POST /api/cards/<card_id>/checklists` - Create checklist
- `DELETE /api/checklists/<id>` - Delete checklist
### Check Items (`/api/check-items`, `/api/checklists/<id>/items`)
- `POST /api/checklists/<checklist_id>/items` - Create check item
- `PUT /api/check-items/<id>` - Update check item
- `DELETE /api/check-items/<id>` - Delete check item
### Comments (`/api/comments`, `/api/cards/<id>/comments`)
- `GET /api/cards/<card_id>/comments` - Get all comments for card
- `POST /api/cards/<card_id>/comments` - Create comment
- `PUT /api/comments/<id>` - Update comment
- `DELETE /api/comments/<id>` - Delete comment
## Key Features
### Authentication
- All endpoints (except register/login) require JWT authentication
- User can only access their own boards and related resources
- Comments can only be edited/deleted by their author
### Data Integrity
- Cascading deletes ensure cleanup of related data
- Foreign key constraints maintain referential integrity
- Unique constraints prevent duplicate card-label associations
### Position Tracking
- All orderable entities (lists, cards, checklists, check items) have `pos` field
- Enables flexible sorting and drag-and-drop functionality
### Activity Tracking
- Boards track `date_last_activity` and `date_last_view`
- Cards track `date_last_activity`
- Useful for showing recent activity
### JSONB Fields
- Flexible storage for complex data (prefs, limits, badges, cover, desc_data)
- Allows extensibility without schema changes
- Supports Trello-like feature parity
## File Structure
```
backend/app/
├── models/
│ ├── board.py # Board model
│ ├── list_model.py # List model (named to avoid Python conflict)
│ ├── card.py # Card model
│ ├── label.py # Label model
│ ├── card_label.py # Card-Label junction table
│ ├── checklist.py # Checklist model
│ ├── check_item.py # CheckItem model
│ ├── comment.py # Comment model
│ └── user.py # Updated with boards relationship
├── routes/
│ └── kanban.py # All Kanban API routes
└── __init__.py # Updated to import models and register blueprint
docs/
├── kanban_api.md # Complete API documentation
└── kanban_implementation_summary.md # This file
```
## Migration
Migration file: `backend/migrations/versions/1c0b9dfbd933_add_kanban_models_board_list_card_label_.py`
To apply migrations:
```bash
cd backend && . venv/bin/activate && flask db upgrade
```
## Usage Flow
### Typical User Workflow
1. **Register/Login**
- User registers account or logs in
- Receives JWT token for authentication
2. **Create Board**
- User creates a new board
- Board is associated with their user ID
3. **Add Lists**
- User adds lists (columns) to the board
- Examples: "To Do", "In Progress", "Done"
4. **Add Cards**
- User creates cards within lists
- Cards can have descriptions, due dates, etc.
5. **Enhance Cards**
- Add labels for categorization
- Add checklists for subtasks
- Add comments for collaboration
6. **Manage Work**
- Move cards between lists (drag-and-drop)
- Update card details
- Mark checklist items as complete
- Delete completed items
## Design Decisions
### Single User App
- No organization or membership models (as specified)
- Each board belongs to exactly one user
- Simplifies permissions model
### Trello-Inspired Schema
- Uses similar field names and structure as Trello
- Makes it familiar to users
- Supports importing from Trello JSON exports
### Position-Based Ordering
- Uses float `pos` field for ordering
- Allows inserting items between others
- Supports infinite granularity for drag-and-drop
### Cascading Deletes
- Deleting a board deletes all its lists, cards, labels, etc.
- Deleting a list deletes all its cards
- Ensures no orphaned data
### JSONB for Flexible Data
- Stores complex nested data without normalized tables
- Simplifies schema for optional features
- Maintains flexibility for future enhancements
## Next Steps
### Backend
- [ ] Add validation schemas (Pydantic)
- [ ] Add comprehensive tests
- [ ] Add rate limiting
- [ ] Add file upload support for attachments
- [ ] Add activity logging/history
- [ ] Add search functionality
- [ ] Add filtering and sorting options
### Frontend
- [ ] Create board list view
- [ ] Create board detail view with drag-and-drop
- [ ] Implement card creation/editing
- [ ] Implement label management
- [ ] Implement checklist functionality
- [ ] Implement comments
- [ ] Add real-time updates (WebSocket)
## Testing
Test the API using the examples in `docs/kanban_api.md` or use tools like:
- Postman
- Insomnia
- curl (command line)
Example:
```bash
# Start the backend server
make dev-services # Start postgres & redis
make dev-backend # Start Flask server
# Test endpoints
curl http://localhost:5000/api/health
```
## Security Considerations
- JWT authentication required for all operations
- User isolation: users can only access their own resources
- SQL injection prevention through SQLAlchemy ORM
- Input validation on all endpoints
- CORS configured for frontend integration
## Performance Notes
- Indexed fields: user_id, board_id, list_id, card_id, etc.
- Lazy loading relationships to avoid N+1 queries
- Efficient queries using SQLAlchemy's query builder
- JSONB fields use PostgreSQL's optimized JSON storage
## Troubleshooting
### Migration Issues
```bash
# Reset migrations (WARNING: deletes data)
cd backend && . venv/bin/activate && flask db downgrade base
rm -rf migrations/versions/*.py
flask db migrate -m "Initial migration"
flask db upgrade
```
### Database Connection Issues
```bash
# Restart services
make dev-stop-services
make dev-services
```
### Import Errors
```bash
# Ensure venv is activated
cd backend && . venv/bin/activate
pip install -r requirements/dev.txt
```
## Summary
The Kanban backend is now fully implemented with:
- ✅ Complete database schema
- ✅ RESTful API endpoints
- ✅ Authentication and authorization
- ✅ Data integrity and validation
- ✅ Comprehensive documentation
- ✅ Database migration
The backend is ready for frontend integration and testing.

View file

@ -0,0 +1,216 @@
import CheckSquareIcon from './icons/CheckSquareIcon';
import Trash2Icon from './icons/Trash2Icon';
import Edit2Icon from './icons/Edit2Icon';
import PlusIcon from './icons/PlusIcon';
import { useModal } from '../context/modals/useModal';
import { CreateChecklistModal } from './CreateChecklistModal';
import { DeleteChecklistModal } from './DeleteChecklistModal';
import { EditCheckItemModal } from './EditCheckItemModal';
import { CheckItem } from '../types/kanban';
interface CardChecklistsProps {
checklists: any[];
cardId: number;
addChecklist: (name: string, pos: number) => Promise<boolean>;
removeChecklist: (id: number) => Promise<boolean>;
addCheckItem: (
checklistId: number,
name: string,
pos: number,
state?: 'incomplete' | 'complete'
) => Promise<boolean>;
toggleCheckItem: (item: CheckItem, currentState: 'incomplete' | 'complete') => Promise<boolean>;
editCheckItem: (
itemId: number,
name: string,
pos: number,
state: 'incomplete' | 'complete'
) => Promise<boolean>;
removeCheckItem: (itemId: number) => Promise<boolean>;
}
export function CardChecklists({
checklists,
addChecklist,
removeChecklist,
addCheckItem,
toggleCheckItem,
editCheckItem,
removeCheckItem,
}: CardChecklistsProps) {
const { openModal } = useModal();
const handleAddChecklist = () => {
openModal((props) => (
<CreateChecklistModal
onSave={async (name) => {
const checklistCount = checklists?.length || 0;
return await addChecklist(name, checklistCount);
}}
onClose={props.onClose}
/>
));
};
const handleDeleteChecklist = (checklist: any) => {
openModal((props) => (
<DeleteChecklistModal
checklistName={checklist.name}
onDelete={async () => {
const success = await removeChecklist(checklist.id);
if (success) {
props.onClose();
}
}}
onClose={props.onClose}
/>
));
};
const handleAddCheckItem = (checklist: any) => {
const itemName = prompt('Enter item name:');
if (itemName && itemName.trim()) {
const itemCount = checklist.items?.length || 0;
addCheckItem(checklist.id, itemName.trim(), itemCount);
}
};
const handleToggleCheckItem = async (item: any) => {
await toggleCheckItem(item, item.state);
};
const handleEditCheckItem = (item: any) => {
openModal((props) => (
<EditCheckItemModal
itemName={item.name}
onSave={async (name) => {
return await editCheckItem(item.id, name, item.pos, item.state);
}}
onClose={props.onClose}
/>
));
};
const handleDeleteCheckItem = (item: any) => {
if (confirm('Are you sure you want to delete this item?')) {
removeCheckItem(item.id);
}
};
return (
<div className="bg-gray-800 rounded-lg p-6">
<div className="flex justify-between items-center mb-4">
<h2 className="text-xl font-bold text-white flex items-center gap-2">
<span className="w-5 h-5">
<CheckSquareIcon />
</span>
Checklists
</h2>
<button
onClick={handleAddChecklist}
className="bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium py-1.5 px-3 rounded-lg transition-colors flex items-center gap-1"
>
<span className="w-4 h-4">
<PlusIcon />
</span>
Add Checklist
</button>
</div>
{checklists && checklists.length > 0 ? (
<div className="space-y-4">
{checklists.map((checklist: any) => {
const completedCount =
checklist.items?.filter((item: any) => item.state === 'complete').length || 0;
const totalCount = checklist.items?.length || 0;
const progress = totalCount > 0 ? (completedCount / totalCount) * 100 : 0;
return (
<div key={checklist.id} className="border border-gray-700 rounded-lg p-4">
<div className="flex justify-between items-start mb-3">
<div className="flex-1">
<h3 className="font-semibold text-white mb-1">{checklist.name}</h3>
<div className="flex items-center gap-2">
<div className="flex-1 h-2 bg-gray-700 rounded-full overflow-hidden">
<div
className="h-full bg-green-500 transition-all duration-300"
style={{ width: `${progress}%` }}
/>
</div>
<span className="text-xs text-gray-400 whitespace-nowrap">
{completedCount}/{totalCount}
</span>
</div>
</div>
<button
onClick={() => handleDeleteChecklist(checklist)}
className="text-gray-400 hover:text-red-400 transition-colors ml-2"
title="Delete checklist"
>
<span className="w-4 h-4">
<Trash2Icon />
</span>
</button>
</div>
<div className="space-y-2">
{checklist.items && checklist.items.length > 0 ? (
checklist.items.map((item: any) => (
<div
key={item.id}
className="flex items-center gap-3 p-2 bg-gray-700 rounded group hover:bg-gray-600 transition-colors"
>
<input
type="checkbox"
checked={item.state === 'complete'}
onChange={() => handleToggleCheckItem(item)}
className="w-5 h-5 rounded cursor-pointer"
/>
<span
onClick={() => handleToggleCheckItem(item)}
className={`flex-1 text-sm cursor-pointer ${item.state === 'complete' ? 'text-gray-400 line-through' : 'text-white'}`}
>
{item.name}
</span>
<div className="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
<button
onClick={() => handleEditCheckItem(item)}
className="text-gray-400 hover:text-white transition-colors p-1"
title="Edit item"
>
<span className="w-3.5 h-3.5">
<Edit2Icon />
</span>
</button>
<button
onClick={() => handleDeleteCheckItem(item)}
className="text-gray-400 hover:text-red-400 transition-colors p-1"
title="Delete item"
>
<span className="w-3.5 h-3.5">
<Trash2Icon />
</span>
</button>
</div>
</div>
))
) : (
<p className="text-gray-400 text-sm">No items yet</p>
)}
<button
onClick={() => handleAddCheckItem(checklist)}
className="text-blue-400 hover:text-blue-300 text-sm font-medium mt-2 flex items-center gap-1"
>
<span className="w-4 h-4">
<PlusIcon />
</span>
Add an item
</button>
</div>
</div>
);
})}
</div>
) : (
<p className="text-gray-400 text-sm">No checklists yet. Add one to get started!</p>
)}
</div>
);
}

View file

@ -0,0 +1,55 @@
import { useState } from 'react';
interface CreateChecklistModalProps {
onSave: (name: string) => Promise<boolean>;
onClose: () => void;
}
export function CreateChecklistModal({ onSave, onClose }: CreateChecklistModalProps) {
const [name, setName] = useState('');
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!name.trim()) return;
const success = await onSave(name);
if (success) {
onClose();
}
};
return (
<div className="bg-gray-800 rounded-lg p-6 max-w-md w-full">
<h3 className="text-xl font-bold text-white mb-4">Add Checklist</h3>
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="block text-gray-300 text-sm font-medium mb-2">Title</label>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
className="w-full bg-gray-700 text-white rounded-lg p-3 border border-gray-600 focus:border-blue-500 focus:outline-none"
placeholder="Checklist title..."
autoFocus
/>
</div>
<div className="flex gap-2 pt-2">
<button
type="button"
onClick={onClose}
className="flex-1 bg-gray-700 hover:bg-gray-600 text-white font-medium py-2 px-4 rounded-lg transition-colors"
>
Cancel
</button>
<button
type="submit"
disabled={!name.trim()}
className="flex-1 bg-blue-600 hover:bg-blue-700 disabled:bg-gray-600 disabled:cursor-not-allowed text-white font-medium py-2 px-4 rounded-lg transition-colors"
>
Add
</button>
</div>
</form>
</div>
);
}

View file

@ -0,0 +1,45 @@
import Trash2Icon from './icons/Trash2Icon';
interface DeleteChecklistModalProps {
checklistName: string;
onDelete: () => void;
onClose: () => void;
}
export function DeleteChecklistModal({
checklistName,
onDelete,
onClose,
}: DeleteChecklistModalProps) {
return (
<div className="bg-gray-800 rounded-lg p-6 max-w-md w-full">
<div className="flex items-center gap-3 mb-4">
<span className="w-10 h-10 bg-red-600 rounded-full flex items-center justify-center">
<span className="w-5 h-5">
<Trash2Icon />
</span>
</span>
<h3 className="text-xl font-bold text-white">Delete Checklist</h3>
</div>
<p className="text-gray-300 mb-6">
Are you sure you want to delete
<span className="text-white font-semibold">&quot;{checklistName}&quot;</span>? This action
cannot be undone.
</p>
<div className="flex justify-end gap-3">
<button
onClick={onClose}
className="px-4 py-2 bg-gray-700 hover:bg-gray-600 text-white rounded-lg transition-colors"
>
Cancel
</button>
<button
onClick={onDelete}
className="px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded-lg transition-colors"
>
Delete
</button>
</div>
</div>
);
}

View file

@ -0,0 +1,60 @@
import { useState, useEffect } from 'react';
interface EditCheckItemModalProps {
itemName: string;
onSave: (name: string) => Promise<boolean>;
onClose: () => void;
}
export function EditCheckItemModal({ itemName, onSave, onClose }: EditCheckItemModalProps) {
const [name, setName] = useState(itemName);
useEffect(() => {
setName(itemName);
}, [itemName]);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!name.trim()) return;
const success = await onSave(name);
if (success) {
onClose();
}
};
return (
<div className="bg-gray-800 rounded-lg p-6 max-w-md w-full">
<h3 className="text-xl font-bold text-white mb-4">Edit Item</h3>
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="block text-gray-300 text-sm font-medium mb-2">Item Name</label>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
className="w-full bg-gray-700 text-white rounded-lg p-3 border border-gray-600 focus:border-blue-500 focus:outline-none"
placeholder="Item name..."
autoFocus
/>
</div>
<div className="flex gap-2 pt-2">
<button
type="button"
onClick={onClose}
className="flex-1 bg-gray-700 hover:bg-gray-600 text-white font-medium py-2 px-4 rounded-lg transition-colors"
>
Cancel
</button>
<button
type="submit"
disabled={!name.trim()}
className="flex-1 bg-blue-600 hover:bg-blue-700 disabled:bg-gray-600 disabled:cursor-not-allowed text-white font-medium py-2 px-4 rounded-lg transition-colors"
>
Save
</button>
</div>
</form>
</div>
);
}

View file

@ -0,0 +1,167 @@
import { useApi } from './useApi';
import { useLoader } from '../context/loaders/useLoader';
import { useToast } from '../context/toasts/useToast';
import { CheckItem } from '../types/kanban';
export function useChecklistMutations(cardId: number, onUpdate: () => void) {
const { createChecklist, deleteChecklist, createCheckItem, updateCheckItem, deleteCheckItem } =
useApi();
const { withLoader } = useLoader();
const { addNotification } = useToast();
const addChecklist = async (name: string, pos: number) => {
try {
await withLoader(() => createChecklist(cardId, { name, pos }), 'Creating checklist...');
onUpdate();
addNotification({
type: 'success',
title: 'Checklist Created',
message: 'Checklist has been created successfully.',
duration: 3000,
});
return true;
} catch (err) {
const errorMessage = err instanceof Error ? err.message : 'Failed to create checklist';
addNotification({
type: 'error',
title: 'Error',
message: errorMessage,
duration: 5000,
});
return false;
}
};
const removeChecklist = async (checklistId: number) => {
try {
await withLoader(() => deleteChecklist(checklistId), 'Deleting checklist...');
onUpdate();
addNotification({
type: 'success',
title: 'Checklist Deleted',
message: 'Checklist has been deleted successfully.',
duration: 3000,
});
return true;
} catch (err) {
const errorMessage = err instanceof Error ? err.message : 'Failed to delete checklist';
addNotification({
type: 'error',
title: 'Error',
message: errorMessage,
duration: 5000,
});
return false;
}
};
const addCheckItem = async (
checklistId: number,
name: string,
pos: number,
state: 'incomplete' | 'complete' = 'incomplete'
) => {
try {
await withLoader(() => createCheckItem(checklistId, { name, pos, state }), 'Adding item...');
onUpdate();
addNotification({
type: 'success',
title: 'Item Added',
message: 'Check item has been added successfully.',
duration: 3000,
});
return true;
} catch (err) {
const errorMessage = err instanceof Error ? err.message : 'Failed to add item';
addNotification({
type: 'error',
title: 'Error',
message: errorMessage,
duration: 5000,
});
return false;
}
};
const toggleCheckItem = async (item: CheckItem, currentState: 'incomplete' | 'complete') => {
console.log('item', item);
try {
const newState = currentState === 'incomplete' ? 'complete' : 'incomplete';
await withLoader(
() => updateCheckItem(item.id, { name: item.name, pos: 0, state: newState }),
'Updating item...'
);
onUpdate();
return true;
} catch (err) {
const errorMessage = err instanceof Error ? err.message : 'Failed to update item';
addNotification({
type: 'error',
title: 'Error',
message: errorMessage,
duration: 5000,
});
return false;
}
};
const editCheckItem = async (
itemId: number,
name: string,
pos: number,
state: 'incomplete' | 'complete'
) => {
try {
await withLoader(() => updateCheckItem(itemId, { name, pos, state }), 'Updating item...');
onUpdate();
addNotification({
type: 'success',
title: 'Item Updated',
message: 'Check item has been updated successfully.',
duration: 3000,
});
return true;
} catch (err) {
const errorMessage = err instanceof Error ? err.message : 'Failed to update item';
addNotification({
type: 'error',
title: 'Error',
message: errorMessage,
duration: 5000,
});
return false;
}
};
const removeCheckItem = async (itemId: number) => {
try {
await withLoader(() => deleteCheckItem(itemId), 'Deleting item...');
onUpdate();
addNotification({
type: 'success',
title: 'Item Deleted',
message: 'Check item has been deleted successfully.',
duration: 3000,
});
return true;
} catch (err) {
const errorMessage = err instanceof Error ? err.message : 'Failed to delete item';
addNotification({
type: 'error',
title: 'Error',
message: errorMessage,
duration: 5000,
});
return false;
}
};
return {
addChecklist,
removeChecklist,
addCheckItem,
toggleCheckItem,
editCheckItem,
removeCheckItem,
};
}

View file

@ -1,12 +1,13 @@
import { useParams, Link, useNavigate } from 'react-router-dom'; import { useParams, Link, useNavigate } from 'react-router-dom';
import { useCard } from '../hooks/useCard'; import { useCard } from '../hooks/useCard';
import { useCardDetailMutations } from '../hooks/useCardDetailMutations'; import { useCardDetailMutations } from '../hooks/useCardDetailMutations';
import { useChecklistMutations } from '../hooks/useChecklistMutations';
import { useModal } from '../context/modals/useModal'; import { useModal } from '../context/modals/useModal';
import { CardSidebar } from '../components/CardSidebar'; import { CardSidebar } from '../components/CardSidebar';
import { CardComments } from '../components/CardComments'; import { CardComments } from '../components/CardComments';
import { CardChecklists } from '../components/CardChecklists';
import { EditCardModal } from '../components/EditCardModal'; import { EditCardModal } from '../components/EditCardModal';
import { DeleteCardModal } from '../components/DeleteCardModal'; import { DeleteCardModal } from '../components/DeleteCardModal';
import CheckSquareIcon from '../components/icons/CheckSquareIcon';
import TagIcon from '../components/icons/TagIcon'; import TagIcon from '../components/icons/TagIcon';
import Trash2Icon from '../components/icons/Trash2Icon'; import Trash2Icon from '../components/icons/Trash2Icon';
import ArrowLeftIcon from '../components/icons/ArrowLeftIcon'; import ArrowLeftIcon from '../components/icons/ArrowLeftIcon';
@ -25,6 +26,7 @@ export function CardDetail() {
} = useCardDetailMutations(parseInt(cardId || '0'), card, fetchCard); } = useCardDetailMutations(parseInt(cardId || '0'), card, fetchCard);
const { openModal } = useModal(); const { openModal } = useModal();
const checklistMutations = useChecklistMutations(parseInt(cardId || '0'), fetchCard);
const handleEditCard = () => { const handleEditCard = () => {
if (!card) return; if (!card) return;
@ -153,47 +155,16 @@ export function CardDetail() {
)} )}
{/* Checklists Section */} {/* Checklists Section */}
{card.checklists && card.checklists.length > 0 && ( <CardChecklists
<div className="bg-gray-800 rounded-lg p-6"> checklists={card.checklists || []}
<h2 className="text-xl font-bold text-white flex items-center gap-2 mb-4"> cardId={parseInt(cardId || '0')}
<span className="w-5 h-5"> addChecklist={checklistMutations.addChecklist}
<CheckSquareIcon /> removeChecklist={checklistMutations.removeChecklist}
</span> addCheckItem={checklistMutations.addCheckItem}
Checklists toggleCheckItem={checklistMutations.toggleCheckItem}
</h2> editCheckItem={checklistMutations.editCheckItem}
<div className="space-y-4"> removeCheckItem={checklistMutations.removeCheckItem}
{card.checklists.map((checklist: any) => (
<div key={checklist.id} className="border border-gray-700 rounded-lg p-4">
<h3 className="font-semibold text-white mb-3">{checklist.name}</h3>
<div className="space-y-2">
{checklist.items && checklist.items.length > 0 ? (
checklist.items.map((item: any) => (
<div
key={item.id}
className="flex items-center gap-3 p-2 bg-gray-700 rounded"
>
<input
type="checkbox"
checked={item.state === 'complete'}
readOnly
className="w-5 h-5 rounded"
/> />
<span
className={`text-sm ${item.state === 'complete' ? 'text-gray-400 line-through' : 'text-white'}`}
>
{item.name}
</span>
</div>
))
) : (
<p className="text-gray-400 text-sm">No items yet</p>
)}
</div>
</div>
))}
</div>
</div>
)}
<CardComments <CardComments
card={card} card={card}