rebranded to BIT
This commit is contained in:
160
ARCHIVE_FEATURE_README.md
Normal file
160
ARCHIVE_FEATURE_README.md
Normal 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)
|
||||
10
CHANGELOG.md
10
CHANGELOG.md
@@ -1,11 +1,11 @@
|
||||
# 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/),
|
||||
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
|
||||
- **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
|
||||
- Prop drilling of projectStatuses through component hierarchy
|
||||
|
||||
## [0.1.5] - 2025-01-XX
|
||||
## [0.1.5] - 2025-11-22
|
||||
|
||||
### Added
|
||||
- **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
|
||||
|
||||
**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.
|
||||
|
||||
**Repository**: https://github.com/serversdwn/tesseract
|
||||
**Repository**: https://github.com/serversdwn/break-it-down
|
||||
**License**: MIT
|
||||
**Author**: serversdwn
|
||||
|
||||
25
README.md
25
README.md
@@ -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.
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
## 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
|
||||
|
||||
@@ -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
|
||||
- **Real-Time Search**: Find tasks across projects with filtering
|
||||
- **Self-Hosted**: Full data ownership and privacy
|
||||
- **Dark Cyberpunk UI**: Orange-accented dark theme optimized for focus
|
||||
|
||||
## Tech Stack
|
||||
|
||||
@@ -55,8 +54,8 @@ TESSERACT is designed for complex project management where tasks naturally decom
|
||||
|
||||
1. **Clone the repository**
|
||||
```bash
|
||||
git clone https://github.com/serversdwn/tesseract.git
|
||||
cd tesseract
|
||||
git clone https://github.com/serversdwn/break-it-down.git
|
||||
cd break-it-down
|
||||
```
|
||||
|
||||
2. **Start the application**
|
||||
@@ -166,7 +165,7 @@ The Kanban board displays tasks in a nested hierarchy while maintaining status-b
|
||||
|
||||
### 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
|
||||
- **Parent Tasks** (have subtasks): Display shows sum of ALL descendant leaf tasks
|
||||
@@ -202,7 +201,7 @@ Remaining work: 3h 30m (not 10h!)
|
||||
|
||||
### 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
|
||||
2. Upload a file matching this structure:
|
||||
@@ -307,7 +306,7 @@ tasks
|
||||
### Project Structure
|
||||
|
||||
```
|
||||
tesseract/
|
||||
break-it-down/
|
||||
├─ backend/
|
||||
│ ├─ app/
|
||||
│ │ ├─ main.py # FastAPI application
|
||||
@@ -370,10 +369,10 @@ Frontend will be available at `http://localhost:5173` (Vite default)
|
||||
**Backup:**
|
||||
```bash
|
||||
# Docker
|
||||
docker cp tesseract-backend:/app/tesseract.db ./backup.db
|
||||
docker cp bit-backend:/app/bit.db ./backup.db
|
||||
|
||||
# Local
|
||||
cp backend/tesseract.db ./backup.db
|
||||
cp backend/bit.db ./backup.db
|
||||
```
|
||||
|
||||
**Schema Changes:**
|
||||
@@ -456,8 +455,8 @@ MIT License - see LICENSE file for details
|
||||
|
||||
## Support
|
||||
|
||||
- **Issues**: https://github.com/serversdwn/tesseract/issues
|
||||
- **Discussions**: https://github.com/serversdwn/tesseract/discussions
|
||||
- **Issues**: https://github.com/serversdwn/break-it-down/issues
|
||||
- **Discussions**: https://github.com/serversdwn/break-it-down/discussions
|
||||
|
||||
## 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.
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# Database Configuration
|
||||
DATABASE_URL=sqlite:///./tesseract.db
|
||||
DATABASE_URL=sqlite:///./bit.db
|
||||
|
||||
# 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_VERSION=1.0.0
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
def get_projects(db: Session, skip: int = 0, limit: int = 100) -> List[models.Project]:
|
||||
return db.query(models.Project).offset(skip).limit(limit).all()
|
||||
def get_projects(db: Session, skip: int = 0, limit: int = 100, archived: Optional[bool] = None) -> List[models.Project]:
|
||||
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(
|
||||
|
||||
@@ -30,9 +30,14 @@ app.add_middleware(
|
||||
# ========== PROJECT ENDPOINTS ==========
|
||||
|
||||
@app.get("/api/projects", response_model=List[schemas.Project])
|
||||
def list_projects(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)):
|
||||
"""List all projects"""
|
||||
return crud.get_projects(db, skip=skip, limit=limit)
|
||||
def list_projects(
|
||||
skip: int = 0,
|
||||
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)
|
||||
@@ -314,6 +319,6 @@ def root():
|
||||
"""API health check"""
|
||||
return {
|
||||
"status": "online",
|
||||
"message": "Tesseract API - Nested Todo Tree Manager",
|
||||
"message": "Break It Down (BIT) API - Nested Todo Tree Manager",
|
||||
"docs": "/docs"
|
||||
}
|
||||
|
||||
@@ -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 datetime import datetime
|
||||
from .database import Base
|
||||
@@ -15,6 +15,7 @@ class Project(Base):
|
||||
name = Column(String(255), nullable=False)
|
||||
description = Column(Text, nullable=True)
|
||||
statuses = Column(JSON, nullable=False, default=DEFAULT_STATUSES)
|
||||
is_archived = Column(Boolean, default=False, nullable=False)
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
|
||||
|
||||
@@ -60,11 +60,13 @@ class ProjectUpdate(BaseModel):
|
||||
name: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
statuses: Optional[List[str]] = None
|
||||
is_archived: Optional[bool] = None
|
||||
|
||||
|
||||
class Project(ProjectBase):
|
||||
id: int
|
||||
statuses: List[str]
|
||||
is_archived: bool
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
|
||||
@@ -6,10 +6,10 @@ class Settings(BaseSettings):
|
||||
"""Application settings loaded from environment variables"""
|
||||
|
||||
# Database Configuration
|
||||
database_url: str = "sqlite:///./tesseract.db"
|
||||
database_url: str = "sqlite:///./bit.db"
|
||||
|
||||
# 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_version: str = "1.0.0"
|
||||
|
||||
|
||||
37
backend/migrate_add_is_archived.py
Normal file
37
backend/migrate_add_is_archived.py
Normal 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()
|
||||
@@ -10,7 +10,7 @@ DEFAULT_STATUSES = ["backlog", "in_progress", "on_hold", "done"]
|
||||
|
||||
def migrate():
|
||||
# Connect to the database
|
||||
conn = sqlite3.connect('tesseract.db')
|
||||
conn = sqlite3.connect('bit.db')
|
||||
cursor = conn.cursor()
|
||||
|
||||
try:
|
||||
|
||||
@@ -3,12 +3,12 @@ services:
|
||||
build:
|
||||
context: ./backend
|
||||
dockerfile: Dockerfile
|
||||
container_name: tesseract-backend
|
||||
container_name: bit-backend
|
||||
ports:
|
||||
- "8002:8002"
|
||||
volumes:
|
||||
- ./backend/app:/app/app
|
||||
- tesseract-db:/app
|
||||
- bit-db:/app
|
||||
env_file:
|
||||
- ./backend/.env
|
||||
environment:
|
||||
@@ -19,7 +19,7 @@ services:
|
||||
build:
|
||||
context: ./frontend
|
||||
dockerfile: Dockerfile
|
||||
container_name: tesseract-frontend
|
||||
container_name: bit-frontend
|
||||
ports:
|
||||
- "3002:80"
|
||||
env_file:
|
||||
@@ -29,4 +29,4 @@ services:
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
tesseract-db:
|
||||
bit-db:
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<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>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
4
frontend/package-lock.json
generated
4
frontend/package-lock.json
generated
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "tesseract-frontend",
|
||||
"name": "bit-frontend",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "tesseract-frontend",
|
||||
"name": "bit-frontend",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"lucide-react": "^0.303.0",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "tesseract-frontend",
|
||||
"name": "bit-frontend",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
|
||||
@@ -11,8 +11,8 @@ function App() {
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-cyber-orange">
|
||||
TESSERACT
|
||||
<span className="ml-3 text-sm text-gray-500">Task Decomposition Engine</span>
|
||||
Break It Down
|
||||
<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>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { Plus, Upload, Trash2 } from 'lucide-react'
|
||||
import { getProjects, createProject, deleteProject, importJSON } from '../utils/api'
|
||||
import { Plus, Upload, Trash2, Archive, ArchiveRestore } from 'lucide-react'
|
||||
import { getProjects, createProject, deleteProject, importJSON, archiveProject, unarchiveProject } from '../utils/api'
|
||||
|
||||
function ProjectList() {
|
||||
const [projects, setProjects] = useState([])
|
||||
@@ -12,15 +12,22 @@ function ProjectList() {
|
||||
const [newProjectDesc, setNewProjectDesc] = useState('')
|
||||
const [importJSON_Text, setImportJSONText] = useState('')
|
||||
const [error, setError] = useState('')
|
||||
const [activeTab, setActiveTab] = useState('active') // 'active', 'archived', 'all'
|
||||
const navigate = useNavigate()
|
||||
|
||||
useEffect(() => {
|
||||
loadProjects()
|
||||
}, [])
|
||||
}, [activeTab])
|
||||
|
||||
const loadProjects = async () => {
|
||||
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)
|
||||
} catch (err) {
|
||||
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) {
|
||||
return <div className="text-center text-gray-400 py-12">Loading...</div>
|
||||
}
|
||||
|
||||
return (
|
||||
<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>
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
@@ -98,6 +125,40 @@ function ProjectList() {
|
||||
</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 && (
|
||||
<div className="mb-4 p-4 bg-red-900/30 border border-red-500/50 rounded text-red-300">
|
||||
{error}
|
||||
@@ -106,8 +167,14 @@ function ProjectList() {
|
||||
|
||||
{projects.length === 0 ? (
|
||||
<div className="text-center py-16 text-gray-500">
|
||||
<p className="text-xl mb-2">No projects yet</p>
|
||||
<p className="text-sm">Create a new project or import from JSON</p>
|
||||
<p className="text-xl mb-2">
|
||||
{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 className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
@@ -115,18 +182,45 @@ function ProjectList() {
|
||||
<div
|
||||
key={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">
|
||||
<h3 className="text-xl font-semibold text-gray-100 group-hover:text-cyber-orange transition-colors">
|
||||
{project.name}
|
||||
{project.is_archived && (
|
||||
<span className="ml-2 text-xs text-gray-500">(archived)</span>
|
||||
)}
|
||||
</h3>
|
||||
<button
|
||||
onClick={(e) => handleDeleteProject(project.id, e)}
|
||||
className="text-gray-600 hover:text-red-400 transition-colors"
|
||||
>
|
||||
<Trash2 size={18} />
|
||||
</button>
|
||||
<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
|
||||
onClick={(e) => handleDeleteProject(project.id, e)}
|
||||
className="text-gray-600 hover:text-red-400 transition-colors"
|
||||
title="Delete project"
|
||||
>
|
||||
<Trash2 size={18} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{project.description && (
|
||||
<p className="text-gray-400 text-sm line-clamp-2">{project.description}</p>
|
||||
|
||||
@@ -22,7 +22,14 @@ async function fetchAPI(endpoint, options = {}) {
|
||||
}
|
||||
|
||||
// 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 createProject = (data) => fetchAPI('/projects', {
|
||||
method: 'POST',
|
||||
@@ -33,6 +40,8 @@ export const updateProject = (id, data) => fetchAPI(`/projects/${id}`, {
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
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
|
||||
export const getProjectTasks = (projectId) => fetchAPI(`/projects/${projectId}/tasks`);
|
||||
|
||||
Reference in New Issue
Block a user