Main branch Resync on w/ gitea. v0.1.6 #1

Merged
serversdown merged 20 commits from claude/nested-todo-app-mvp-014vUjLpD8wNjkMhLMXS6Kd5 into main 2026-01-04 04:30:52 -05:00
18 changed files with 368 additions and 55 deletions
Showing only changes of commit 3fc90063b4 - Show all commits

160
ARCHIVE_FEATURE_README.md Normal file
View File

@@ -0,0 +1,160 @@
# Project Archive Feature
## Overview
The Break It Down (BIT) application now supports archiving projects! This helps you organize your workspace by hiding completed or inactive projects from the main view while keeping them accessible.
## Features Added
### 1. **Archive Status**
- Projects can be marked as archived or active
- Archived projects are kept in the database but hidden from the default view
- All tasks and data are preserved when archiving
### 2. **Tab Navigation**
The main Projects page now has three tabs:
- **Active** (default) - Shows only non-archived projects
- **Archived** - Shows only archived projects
- **All** - Shows all projects regardless of archive status
### 3. **Quick Actions**
Each project card now has two action buttons:
- **Archive/Unarchive** (📦/↩️) - Toggle archive status
- **Delete** (🗑️) - Permanently delete the project
### 4. **Visual Indicators**
- Archived projects appear with reduced opacity and gray border
- "(archived)" label appears next to archived project names
- Context-aware empty state messages
## Database Changes
A new column has been added to the `projects` table:
- `is_archived` (BOOLEAN) - Defaults to `False` for new projects
## Installation & Migration
### For New Installations
No action needed! The database will be created with the correct schema automatically.
### For Existing Databases
If you have an existing Break It Down database, run the migration script:
```bash
cd backend
python3 migrate_add_is_archived.py
```
This will add the `is_archived` column to your existing projects table and set all existing projects to `is_archived=False`.
### Restart the Backend
After migration, restart the backend server to load the updated models:
```bash
# Stop the current backend process
pkill -f "uvicorn.*backend.main:app"
# Start the backend again
cd /path/to/break-it-down
uvicorn backend.main:app --host 0.0.0.0 --port 8001
```
Or if using Docker:
```bash
docker-compose restart backend
```
## API Changes
### Updated Endpoint: GET /api/projects
New optional query parameter:
- `archived` (boolean, optional) - Filter projects by archive status
- `archived=false` - Returns only active projects
- `archived=true` - Returns only archived projects
- No parameter - Returns all projects
Examples:
```bash
# Get active projects
GET /api/projects?archived=false
# Get archived projects
GET /api/projects?archived=true
# Get all projects
GET /api/projects
```
### Updated Endpoint: PUT /api/projects/{id}
New field in request body:
```json
{
"name": "Project Name", // optional
"description": "Description", // optional
"statuses": [...], // optional
"is_archived": true // optional - new!
}
```
### New API Helper Functions
The frontend API client now includes:
- `archiveProject(id)` - Archive a project
- `unarchiveProject(id)` - Unarchive a project
## File Changes Summary
### Backend
- `backend/app/models.py` - Added `is_archived` Boolean column to Project model
- `backend/app/schemas.py` - Updated ProjectUpdate and Project schemas
- `backend/app/main.py` - Added `archived` query parameter to list_projects endpoint
- `backend/app/crud.py` - Updated get_projects to support archive filtering
- `backend/migrate_add_is_archived.py` - Migration script (new file)
### Frontend
- `frontend/src/pages/ProjectList.jsx` - Added tabs, archive buttons, and filtering logic
- `frontend/src/utils/api.js` - Added archive/unarchive functions and updated getProjects
## Usage Examples
### Archive a Project
1. Go to the Projects page
2. Click the archive icon (📦) on any project card
3. The project disappears from the Active view
4. Switch to the "Archived" tab to see it
### Unarchive a Project
1. Switch to the "Archived" tab
2. Click the unarchive icon (↩️) on the project card
3. The project returns to the Active view
### View All Projects
Click the "All" tab to see both active and archived projects together.
## Status vs Archive
**Important distinction:**
- **Task Status** (backlog, in_progress, on_hold, done) - Applied to individual tasks within a project
- **Project Archive** - Applied to entire projects to organize your workspace
The existing task status "on_hold" is still useful for pausing work on specific tasks, while archiving is for entire projects you want to hide from your main view.
## Backward Compatibility
All changes are backward compatible:
- Existing projects will default to `is_archived=false` (active)
- Old API calls without the `archived` parameter still work (returns all projects)
- The frontend gracefully handles projects with or without the archive field
## Future Enhancements
Potential additions:
- Archive date tracking
- Bulk archive operations
- Archive projects from the ProjectSettings modal
- Auto-archive projects after X days of inactivity
- Project status badges (active, archived, on-hold, completed)

View File

@@ -1,11 +1,11 @@
# Changelog # Changelog
All notable changes to TESSERACT will be documented in this file. All notable changes to Break It Down (BIT) will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.1.6] - 2025-01-25 ## [0.1.6] - 2025-11-25
### Added ### Added
- **Dynamic Status Management System** - **Dynamic Status Management System**
@@ -69,7 +69,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Helper functions for status formatting and color coding - Helper functions for status formatting and color coding
- Prop drilling of projectStatuses through component hierarchy - Prop drilling of projectStatuses through component hierarchy
## [0.1.5] - 2025-01-XX ## [0.1.5] - 2025-11-22
### Added ### Added
- **Nested Kanban View** - Major feature implementation - **Nested Kanban View** - Major feature implementation
@@ -238,9 +238,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Project Information ## Project Information
**TESSERACT** - Task Decomposition Engine **Break It Down (BIT)** - Task Decomposition Engine
A self-hosted web application for managing deeply nested todo trees with advanced time tracking and project planning capabilities. A self-hosted web application for managing deeply nested todo trees with advanced time tracking and project planning capabilities.
**Repository**: https://github.com/serversdwn/tesseract **Repository**: https://github.com/serversdwn/break-it-down
**License**: MIT **License**: MIT
**Author**: serversdwn **Author**: serversdwn

View File

@@ -1,4 +1,4 @@
# TESSERACT # Break It Down - BIT
**Task Decomposition Engine** - A self-hosted web application for managing deeply nested todo trees with advanced time tracking and project planning capabilities. **Task Decomposition Engine** - A self-hosted web application for managing deeply nested todo trees with advanced time tracking and project planning capabilities.
@@ -7,7 +7,7 @@
## Overview ## Overview
TESSERACT is designed for complex project management where tasks naturally decompose into hierarchical structures. Whether you're breaking down software projects, research tasks, or multi-phase initiatives, TESSERACT helps you visualize, track, and manage work at any level of granularity. Break It Down is designed for complex project management where tasks naturally decompose into hierarchical structures. Whether you're breaking down software projects, research tasks, or multi-phase initiatives, BIT helps you visualize, track, and manage work at any level of granularity.
### Key Features ### Key Features
@@ -33,7 +33,6 @@ TESSERACT is designed for complex project management where tasks naturally decom
- **LLM Integration**: Import JSON task trees generated by AI assistants - **LLM Integration**: Import JSON task trees generated by AI assistants
- **Real-Time Search**: Find tasks across projects with filtering - **Real-Time Search**: Find tasks across projects with filtering
- **Self-Hosted**: Full data ownership and privacy - **Self-Hosted**: Full data ownership and privacy
- **Dark Cyberpunk UI**: Orange-accented dark theme optimized for focus
## Tech Stack ## Tech Stack
@@ -55,8 +54,8 @@ TESSERACT is designed for complex project management where tasks naturally decom
1. **Clone the repository** 1. **Clone the repository**
```bash ```bash
git clone https://github.com/serversdwn/tesseract.git git clone https://github.com/serversdwn/break-it-down.git
cd tesseract cd break-it-down
``` ```
2. **Start the application** 2. **Start the application**
@@ -166,7 +165,7 @@ The Kanban board displays tasks in a nested hierarchy while maintaining status-b
### Understanding Time Estimates ### Understanding Time Estimates
TESSERACT uses **leaf-based time calculation** for accurate project planning: Break It Down uses **leaf-based time calculation** for accurate project planning:
- **Leaf Tasks** (no subtasks): Display shows their own time estimate - **Leaf Tasks** (no subtasks): Display shows their own time estimate
- **Parent Tasks** (have subtasks): Display shows sum of ALL descendant leaf tasks - **Parent Tasks** (have subtasks): Display shows sum of ALL descendant leaf tasks
@@ -202,7 +201,7 @@ Remaining work: 3h 30m (not 10h!)
### JSON Import ### JSON Import
TESSERACT can import task hierarchies from JSON files, making it perfect for LLM-generated project breakdowns. Break It Down can import task hierarchies from JSON files, making it perfect for LLM-generated project breakdowns.
1. Click "Import JSON" on a project 1. Click "Import JSON" on a project
2. Upload a file matching this structure: 2. Upload a file matching this structure:
@@ -307,7 +306,7 @@ tasks
### Project Structure ### Project Structure
``` ```
tesseract/ break-it-down/
├─ backend/ ├─ backend/
│ ├─ app/ │ ├─ app/
│ │ ├─ main.py # FastAPI application │ │ ├─ main.py # FastAPI application
@@ -370,10 +369,10 @@ Frontend will be available at `http://localhost:5173` (Vite default)
**Backup:** **Backup:**
```bash ```bash
# Docker # Docker
docker cp tesseract-backend:/app/tesseract.db ./backup.db docker cp bit-backend:/app/bit.db ./backup.db
# Local # Local
cp backend/tesseract.db ./backup.db cp backend/bit.db ./backup.db
``` ```
**Schema Changes:** **Schema Changes:**
@@ -456,8 +455,8 @@ MIT License - see LICENSE file for details
## Support ## Support
- **Issues**: https://github.com/serversdwn/tesseract/issues - **Issues**: https://github.com/serversdwn/break-it-down/issues
- **Discussions**: https://github.com/serversdwn/tesseract/discussions - **Discussions**: https://github.com/serversdwn/break-it-down/discussions
## Acknowledgments ## Acknowledgments
@@ -467,4 +466,4 @@ MIT License - see LICENSE file for details
--- ---
**TESSERACT** - Decompose complexity, achieve clarity. **Break It Down (BIT)** - Decompose complexity, achieve clarity.

View File

@@ -1,8 +1,8 @@
# Database Configuration # Database Configuration
DATABASE_URL=sqlite:///./tesseract.db DATABASE_URL=sqlite:///./bit.db
# API Configuration # API Configuration
API_TITLE=Tesseract - Nested Todo Tree API API_TITLE=Break It Down (BIT) - Nested Todo Tree API
API_DESCRIPTION=API for managing deeply nested todo trees API_DESCRIPTION=API for managing deeply nested todo trees
API_VERSION=1.0.0 API_VERSION=1.0.0

View File

@@ -21,8 +21,14 @@ def get_project(db: Session, project_id: int) -> Optional[models.Project]:
return db.query(models.Project).filter(models.Project.id == project_id).first() return db.query(models.Project).filter(models.Project.id == project_id).first()
def get_projects(db: Session, skip: int = 0, limit: int = 100) -> List[models.Project]: def get_projects(db: Session, skip: int = 0, limit: int = 100, archived: Optional[bool] = None) -> List[models.Project]:
return db.query(models.Project).offset(skip).limit(limit).all() query = db.query(models.Project)
# Filter by archive status if specified
if archived is not None:
query = query.filter(models.Project.is_archived == archived)
return query.offset(skip).limit(limit).all()
def update_project( def update_project(

View File

@@ -30,9 +30,14 @@ app.add_middleware(
# ========== PROJECT ENDPOINTS ========== # ========== PROJECT ENDPOINTS ==========
@app.get("/api/projects", response_model=List[schemas.Project]) @app.get("/api/projects", response_model=List[schemas.Project])
def list_projects(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)): def list_projects(
"""List all projects""" skip: int = 0,
return crud.get_projects(db, skip=skip, limit=limit) limit: int = 100,
archived: Optional[bool] = None,
db: Session = Depends(get_db)
):
"""List all projects with optional archive filter"""
return crud.get_projects(db, skip=skip, limit=limit, archived=archived)
@app.post("/api/projects", response_model=schemas.Project, status_code=201) @app.post("/api/projects", response_model=schemas.Project, status_code=201)
@@ -314,6 +319,6 @@ def root():
"""API health check""" """API health check"""
return { return {
"status": "online", "status": "online",
"message": "Tesseract API - Nested Todo Tree Manager", "message": "Break It Down (BIT) API - Nested Todo Tree Manager",
"docs": "/docs" "docs": "/docs"
} }

View File

@@ -1,4 +1,4 @@
from sqlalchemy import Column, Integer, String, Text, DateTime, ForeignKey, JSON from sqlalchemy import Column, Integer, String, Text, DateTime, ForeignKey, JSON, Boolean
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from datetime import datetime from datetime import datetime
from .database import Base from .database import Base
@@ -15,6 +15,7 @@ class Project(Base):
name = Column(String(255), nullable=False) name = Column(String(255), nullable=False)
description = Column(Text, nullable=True) description = Column(Text, nullable=True)
statuses = Column(JSON, nullable=False, default=DEFAULT_STATUSES) statuses = Column(JSON, nullable=False, default=DEFAULT_STATUSES)
is_archived = Column(Boolean, default=False, nullable=False)
created_at = Column(DateTime, default=datetime.utcnow) created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)

View File

@@ -60,11 +60,13 @@ class ProjectUpdate(BaseModel):
name: Optional[str] = None name: Optional[str] = None
description: Optional[str] = None description: Optional[str] = None
statuses: Optional[List[str]] = None statuses: Optional[List[str]] = None
is_archived: Optional[bool] = None
class Project(ProjectBase): class Project(ProjectBase):
id: int id: int
statuses: List[str] statuses: List[str]
is_archived: bool
created_at: datetime created_at: datetime
updated_at: datetime updated_at: datetime

View File

@@ -6,10 +6,10 @@ class Settings(BaseSettings):
"""Application settings loaded from environment variables""" """Application settings loaded from environment variables"""
# Database Configuration # Database Configuration
database_url: str = "sqlite:///./tesseract.db" database_url: str = "sqlite:///./bit.db"
# API Configuration # API Configuration
api_title: str = "Tesseract - Nested Todo Tree API" api_title: str = "Break It Down (BIT) - Nested Todo Tree API"
api_description: str = "API for managing deeply nested todo trees" api_description: str = "API for managing deeply nested todo trees"
api_version: str = "1.0.0" api_version: str = "1.0.0"

View File

@@ -0,0 +1,37 @@
"""
Migration script to add is_archived column to existing projects.
Run this once if you have an existing database.
"""
import sqlite3
import os
# Get the database path
db_path = os.path.join(os.path.dirname(__file__), 'bit.db')
if not os.path.exists(db_path):
print(f"Database not found at {db_path}")
print("No migration needed - new database will be created with the correct schema.")
exit(0)
# Connect to the database
conn = sqlite3.connect(db_path)
cursor = conn.cursor()
try:
# Check if the column already exists
cursor.execute("PRAGMA table_info(projects)")
columns = [column[1] for column in cursor.fetchall()]
if 'is_archived' in columns:
print("Column 'is_archived' already exists. Migration not needed.")
else:
# Add the is_archived column
cursor.execute("ALTER TABLE projects ADD COLUMN is_archived BOOLEAN NOT NULL DEFAULT 0")
conn.commit()
print("Successfully added 'is_archived' column to projects table.")
print("All existing projects have been set to is_archived=False (0).")
except Exception as e:
print(f"Error during migration: {e}")
conn.rollback()
finally:
conn.close()

View File

@@ -10,7 +10,7 @@ DEFAULT_STATUSES = ["backlog", "in_progress", "on_hold", "done"]
def migrate(): def migrate():
# Connect to the database # Connect to the database
conn = sqlite3.connect('tesseract.db') conn = sqlite3.connect('bit.db')
cursor = conn.cursor() cursor = conn.cursor()
try: try:

View File

@@ -3,12 +3,12 @@ services:
build: build:
context: ./backend context: ./backend
dockerfile: Dockerfile dockerfile: Dockerfile
container_name: tesseract-backend container_name: bit-backend
ports: ports:
- "8002:8002" - "8002:8002"
volumes: volumes:
- ./backend/app:/app/app - ./backend/app:/app/app
- tesseract-db:/app - bit-db:/app
env_file: env_file:
- ./backend/.env - ./backend/.env
environment: environment:
@@ -19,7 +19,7 @@ services:
build: build:
context: ./frontend context: ./frontend
dockerfile: Dockerfile dockerfile: Dockerfile
container_name: tesseract-frontend container_name: bit-frontend
ports: ports:
- "3002:80" - "3002:80"
env_file: env_file:
@@ -29,4 +29,4 @@ services:
restart: unless-stopped restart: unless-stopped
volumes: volumes:
tesseract-db: bit-db:

View File

@@ -4,7 +4,7 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" /> <link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Tesseract - Task Decomposition Engine</title> <title>Break It Down - Task Decomposition Engine</title>
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>

View File

@@ -1,11 +1,11 @@
{ {
"name": "tesseract-frontend", "name": "bit-frontend",
"version": "1.0.0", "version": "1.0.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "tesseract-frontend", "name": "bit-frontend",
"version": "1.0.0", "version": "1.0.0",
"dependencies": { "dependencies": {
"lucide-react": "^0.303.0", "lucide-react": "^0.303.0",

View File

@@ -1,5 +1,5 @@
{ {
"name": "tesseract-frontend", "name": "bit-frontend",
"private": true, "private": true,
"version": "1.0.0", "version": "1.0.0",
"type": "module", "type": "module",

View File

@@ -11,8 +11,8 @@ function App() {
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<div> <div>
<h1 className="text-2xl font-bold text-cyber-orange"> <h1 className="text-2xl font-bold text-cyber-orange">
TESSERACT Break It Down
<span className="ml-3 text-sm text-gray-500">Task Decomposition Engine</span> <span className="ml-3 text-sm text-gray-500">BIT - Task Decomposition Engine</span>
<span className="ml-2 text-xs text-gray-600">v{import.meta.env.VITE_APP_VERSION || '0.1.6'}</span> <span className="ml-2 text-xs text-gray-600">v{import.meta.env.VITE_APP_VERSION || '0.1.6'}</span>
</h1> </h1>
</div> </div>

View File

@@ -1,7 +1,7 @@
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
import { useNavigate } from 'react-router-dom' import { useNavigate } from 'react-router-dom'
import { Plus, Upload, Trash2 } from 'lucide-react' import { Plus, Upload, Trash2, Archive, ArchiveRestore } from 'lucide-react'
import { getProjects, createProject, deleteProject, importJSON } from '../utils/api' import { getProjects, createProject, deleteProject, importJSON, archiveProject, unarchiveProject } from '../utils/api'
function ProjectList() { function ProjectList() {
const [projects, setProjects] = useState([]) const [projects, setProjects] = useState([])
@@ -12,15 +12,22 @@ function ProjectList() {
const [newProjectDesc, setNewProjectDesc] = useState('') const [newProjectDesc, setNewProjectDesc] = useState('')
const [importJSON_Text, setImportJSONText] = useState('') const [importJSON_Text, setImportJSONText] = useState('')
const [error, setError] = useState('') const [error, setError] = useState('')
const [activeTab, setActiveTab] = useState('active') // 'active', 'archived', 'all'
const navigate = useNavigate() const navigate = useNavigate()
useEffect(() => { useEffect(() => {
loadProjects() loadProjects()
}, []) }, [activeTab])
const loadProjects = async () => { const loadProjects = async () => {
try { try {
const data = await getProjects() setLoading(true)
let archivedFilter = null
if (activeTab === 'active') archivedFilter = false
if (activeTab === 'archived') archivedFilter = true
// 'all' tab uses null to get all projects
const data = await getProjects(archivedFilter)
setProjects(data) setProjects(data)
} catch (err) { } catch (err) {
setError(err.message) setError(err.message)
@@ -72,13 +79,33 @@ function ProjectList() {
} }
} }
const handleArchiveProject = async (projectId, e) => {
e.stopPropagation()
try {
await archiveProject(projectId)
setProjects(projects.filter(p => p.id !== projectId))
} catch (err) {
setError(err.message)
}
}
const handleUnarchiveProject = async (projectId, e) => {
e.stopPropagation()
try {
await unarchiveProject(projectId)
setProjects(projects.filter(p => p.id !== projectId))
} catch (err) {
setError(err.message)
}
}
if (loading) { if (loading) {
return <div className="text-center text-gray-400 py-12">Loading...</div> return <div className="text-center text-gray-400 py-12">Loading...</div>
} }
return ( return (
<div> <div>
<div className="flex justify-between items-center mb-8"> <div className="flex justify-between items-center mb-6">
<h2 className="text-3xl font-bold text-gray-100">Projects</h2> <h2 className="text-3xl font-bold text-gray-100">Projects</h2>
<div className="flex gap-3"> <div className="flex gap-3">
<button <button
@@ -98,6 +125,40 @@ function ProjectList() {
</div> </div>
</div> </div>
{/* Tabs */}
<div className="flex gap-1 mb-6 border-b border-cyber-orange/20">
<button
onClick={() => setActiveTab('active')}
className={`px-4 py-2 font-medium transition-colors ${
activeTab === 'active'
? 'text-cyber-orange border-b-2 border-cyber-orange'
: 'text-gray-400 hover:text-gray-200'
}`}
>
Active
</button>
<button
onClick={() => setActiveTab('archived')}
className={`px-4 py-2 font-medium transition-colors ${
activeTab === 'archived'
? 'text-cyber-orange border-b-2 border-cyber-orange'
: 'text-gray-400 hover:text-gray-200'
}`}
>
Archived
</button>
<button
onClick={() => setActiveTab('all')}
className={`px-4 py-2 font-medium transition-colors ${
activeTab === 'all'
? 'text-cyber-orange border-b-2 border-cyber-orange'
: 'text-gray-400 hover:text-gray-200'
}`}
>
All
</button>
</div>
{error && ( {error && (
<div className="mb-4 p-4 bg-red-900/30 border border-red-500/50 rounded text-red-300"> <div className="mb-4 p-4 bg-red-900/30 border border-red-500/50 rounded text-red-300">
{error} {error}
@@ -106,8 +167,14 @@ function ProjectList() {
{projects.length === 0 ? ( {projects.length === 0 ? (
<div className="text-center py-16 text-gray-500"> <div className="text-center py-16 text-gray-500">
<p className="text-xl mb-2">No projects yet</p> <p className="text-xl mb-2">
<p className="text-sm">Create a new project or import from JSON</p> {activeTab === 'archived' ? 'No archived projects' : 'No projects yet'}
</p>
<p className="text-sm">
{activeTab === 'archived'
? 'Archive projects to keep them out of your active workspace'
: 'Create a new project or import from JSON'}
</p>
</div> </div>
) : ( ) : (
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3"> <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
@@ -115,19 +182,46 @@ function ProjectList() {
<div <div
key={project.id} key={project.id}
onClick={() => navigate(`/project/${project.id}`)} onClick={() => navigate(`/project/${project.id}`)}
className="p-6 bg-cyber-darkest border border-cyber-orange/30 rounded-lg hover:border-cyber-orange hover:shadow-cyber transition-all cursor-pointer group" className={`p-6 bg-cyber-darkest border rounded-lg hover:border-cyber-orange hover:shadow-cyber transition-all cursor-pointer group ${
project.is_archived
? 'border-gray-700 opacity-75'
: 'border-cyber-orange/30'
}`}
> >
<div className="flex justify-between items-start mb-2"> <div className="flex justify-between items-start mb-2">
<h3 className="text-xl font-semibold text-gray-100 group-hover:text-cyber-orange transition-colors"> <h3 className="text-xl font-semibold text-gray-100 group-hover:text-cyber-orange transition-colors">
{project.name} {project.name}
{project.is_archived && (
<span className="ml-2 text-xs text-gray-500">(archived)</span>
)}
</h3> </h3>
<div className="flex gap-2">
{project.is_archived ? (
<button
onClick={(e) => handleUnarchiveProject(project.id, e)}
className="text-gray-600 hover:text-cyber-orange transition-colors"
title="Unarchive project"
>
<ArchiveRestore size={18} />
</button>
) : (
<button
onClick={(e) => handleArchiveProject(project.id, e)}
className="text-gray-600 hover:text-yellow-400 transition-colors"
title="Archive project"
>
<Archive size={18} />
</button>
)}
<button <button
onClick={(e) => handleDeleteProject(project.id, e)} onClick={(e) => handleDeleteProject(project.id, e)}
className="text-gray-600 hover:text-red-400 transition-colors" className="text-gray-600 hover:text-red-400 transition-colors"
title="Delete project"
> >
<Trash2 size={18} /> <Trash2 size={18} />
</button> </button>
</div> </div>
</div>
{project.description && ( {project.description && (
<p className="text-gray-400 text-sm line-clamp-2">{project.description}</p> <p className="text-gray-400 text-sm line-clamp-2">{project.description}</p>
)} )}

View File

@@ -22,7 +22,14 @@ async function fetchAPI(endpoint, options = {}) {
} }
// Projects // Projects
export const getProjects = () => fetchAPI('/projects'); export const getProjects = (archived = null) => {
const params = new URLSearchParams();
if (archived !== null) {
params.append('archived', archived);
}
const queryString = params.toString();
return fetchAPI(`/projects${queryString ? `?${queryString}` : ''}`);
};
export const getProject = (id) => fetchAPI(`/projects/${id}`); export const getProject = (id) => fetchAPI(`/projects/${id}`);
export const createProject = (data) => fetchAPI('/projects', { export const createProject = (data) => fetchAPI('/projects', {
method: 'POST', method: 'POST',
@@ -33,6 +40,8 @@ export const updateProject = (id, data) => fetchAPI(`/projects/${id}`, {
body: JSON.stringify(data), body: JSON.stringify(data),
}); });
export const deleteProject = (id) => fetchAPI(`/projects/${id}`, { method: 'DELETE' }); export const deleteProject = (id) => fetchAPI(`/projects/${id}`, { method: 'DELETE' });
export const archiveProject = (id) => updateProject(id, { is_archived: true });
export const unarchiveProject = (id) => updateProject(id, { is_archived: false });
// Tasks // Tasks
export const getProjectTasks = (projectId) => fetchAPI(`/projects/${projectId}/tasks`); export const getProjectTasks = (projectId) => fetchAPI(`/projects/${projectId}/tasks`);