Add files via upload

This commit is contained in:
jasonMPM
2026-03-05 12:13:22 -06:00
committed by GitHub
parent bfa3887e61
commit 20e71ee7f9
40 changed files with 1352 additions and 368 deletions

View File

@@ -0,0 +1,42 @@
import Badge from '../UI/Badge'
import { formatDate } from '../../utils/dateHelpers'
import useFocusStore from '../../store/useFocusStore'
export default function ProjectCard({ project, onEdit, onDelete }) {
const openFocus = useFocusStore(s => s.openFocus)
return (
<div className="bg-surface-elevated border border-surface-border rounded-lg overflow-hidden transition-all hover:border-gold/20">
<div className="h-1 w-full" style={{ backgroundColor: project.color }} />
<div className="p-3">
<div className="flex items-start justify-between mb-2">
<div className="flex items-center gap-2 min-w-0">
<div className="w-2.5 h-2.5 rounded-full flex-shrink-0" style={{ backgroundColor: project.color }} />
<span className="text-sm font-semibold text-text-primary truncate">{project.name}</span>
</div>
<div className="flex gap-0.5 flex-shrink-0 ml-1">
<button onClick={() => onEdit(project)} className="text-text-muted hover:text-gold p-1 transition-colors text-sm"></button>
<button onClick={() => onDelete(project)} className="text-text-muted hover:text-red-400 p-1 transition-colors text-sm"></button>
</div>
</div>
{project.description && (
<p className="text-xs text-text-muted mb-2 line-clamp-1">{project.description}</p>
)}
<div className="space-y-1">
{(project.deliverables || []).map(d => (
<button key={d.id} onClick={() => openFocus(project.id, d.id)}
className="w-full flex items-center justify-between text-xs bg-surface rounded px-2 py-1.5 border border-transparent hover:border-gold/20 hover:bg-surface-border/20 transition-all text-left group">
<span className="text-text-muted group-hover:text-text-primary truncate flex-1 pr-2">{d.title}</span>
<div className="flex items-center gap-1.5 flex-shrink-0">
<span className="text-text-muted/60 font-mono text-[9px]">{formatDate(d.due_date)}</span>
<Badge status={d.status} />
</div>
</button>
))}
{(!project.deliverables || project.deliverables.length === 0) && (
<p className="text-[11px] text-text-muted/40 italic text-center py-1">No deliverables</p>
)}
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,41 @@
import { useState } from 'react'
import ProjectCard from './ProjectCard'
import ProjectModal from './ProjectModal'
import Button from '../UI/Button'
import useProjectStore from '../../store/useProjectStore'
import { deleteProject } from '../../api/projects'
export default function ProjectList() {
const { projects, removeProject } = useProjectStore()
const [showModal, setShowModal] = useState(false)
const [editing, setEditing] = useState(null)
const handleEdit = (p) => { setEditing(p); setShowModal(true) }
const handleDelete = async (p) => {
if (window.confirm(`Delete "${p.name}" and all its deliverables?`)) {
await deleteProject(p.id); removeProject(p.id)
}
}
const handleClose = () => { setShowModal(false); setEditing(null) }
return (
<div className="flex flex-col h-full">
<div className="flex items-center justify-between px-4 py-3 border-b border-surface-border flex-shrink-0">
<h1 className="text-gold font-bold text-lg tracking-widest uppercase">FabDash</h1>
<Button size="sm" onClick={() => setShowModal(true)}>+ Project</Button>
</div>
<div className="flex-1 overflow-y-auto p-3 space-y-2">
{projects.length === 0 && (
<div className="text-center py-10">
<p className="text-text-muted text-sm">No projects yet.</p>
<p className="text-text-muted/50 text-xs mt-1">Click "+ Project" to begin.</p>
</div>
)}
{projects.map(p => (
<ProjectCard key={p.id} project={p} onEdit={handleEdit} onDelete={handleDelete} />
))}
</div>
<ProjectModal isOpen={showModal} onClose={handleClose} project={editing} />
</div>
)
}

View File

@@ -0,0 +1,112 @@
import { useState, useEffect } from 'react'
import Modal from '../UI/Modal'
import Button from '../UI/Button'
import { createProject, updateProject } from '../../api/projects'
import useProjectStore from '../../store/useProjectStore'
import { STATUS_OPTIONS } from '../../utils/statusHelpers'
const PALETTE = ['#4A90D9','#2ECC9A','#9B59B6','#E74C3C','#E67E22','#27AE60','#E91E8C','#00BCD4','#5C6BC0','#F39C12','#C9A84C','#E8608A']
const emptyRow = () => ({ title: '', due_date: '', status: 'upcoming' })
export default function ProjectModal({ isOpen, onClose, project }) {
const { addProject, updateProject: storeUpdate } = useProjectStore()
const [name, setName] = useState('')
const [desc, setDesc] = useState('')
const [color, setColor] = useState('#4A90D9')
const [rows, setRows] = useState([emptyRow()])
const [saving, setSaving] = useState(false)
const isEditing = !!project
useEffect(() => {
if (project) {
setName(project.name || '')
setDesc(project.description || '')
setColor(project.color || '#4A90D9')
setRows(project.deliverables?.length
? project.deliverables.map(d => ({ id: d.id, title: d.title, due_date: d.due_date?.substring(0,10)||'', status: d.status }))
: [emptyRow()])
} else {
setName(''); setDesc(''); setColor('#4A90D9'); setRows([emptyRow()])
}
}, [project, isOpen])
const updRow = (i, f, v) => setRows(r => r.map((row, idx) => idx === i ? { ...row, [f]: v } : row))
const handleSubmit = async () => {
if (!name.trim()) return
setSaving(true)
try {
if (isEditing) {
const updated = await updateProject(project.id, { name, description: desc, color })
storeUpdate({ ...updated, deliverables: project.deliverables })
} else {
const valid = rows.filter(r => r.title.trim() && r.due_date)
const created = await createProject({ name, description: desc, color, deliverables: valid })
addProject(created)
}
onClose()
} finally { setSaving(false) }
}
return (
<Modal isOpen={isOpen} onClose={onClose} title={isEditing ? 'Edit Project' : 'New Project'} size="lg">
<div className="space-y-4">
<div>
<label className="block text-xs text-text-muted mb-1 font-medium">Project Name *</label>
<input className="w-full bg-surface border border-surface-border rounded-lg px-3 py-2 text-sm text-text-primary focus:outline-none focus:border-gold transition-colors"
value={name} onChange={e => setName(e.target.value)} placeholder="e.g. CODA" />
</div>
<div>
<label className="block text-xs text-text-muted mb-1 font-medium">Description</label>
<textarea className="w-full bg-surface border border-surface-border rounded-lg px-3 py-2 text-sm text-text-primary focus:outline-none focus:border-gold transition-colors resize-none"
rows={2} value={desc} onChange={e => setDesc(e.target.value)} placeholder="Optional..." />
</div>
<div>
<label className="block text-xs text-text-muted mb-2 font-medium">Color</label>
<div className="flex flex-wrap gap-2 items-center">
{PALETTE.map(c => (
<button key={c} type="button" onClick={() => setColor(c)}
className={`w-7 h-7 rounded-full transition-all ${color===c ? 'ring-2 ring-offset-2 ring-offset-surface-raised ring-white scale-110' : 'hover:scale-105'}`}
style={{ backgroundColor: c }} />
))}
<input type="color" value={color} onChange={e => setColor(e.target.value)}
className="w-7 h-7 rounded cursor-pointer border-0 bg-transparent" title="Custom color" />
</div>
</div>
{!isEditing && (
<div>
<div className="flex items-center justify-between mb-2">
<label className="block text-xs text-text-muted font-medium">Deliverables</label>
<button type="button" onClick={() => setRows(r => [...r, emptyRow()])}
className="text-xs text-gold hover:text-gold-light transition-colors">+ Add Row</button>
</div>
<div className="space-y-2">
{rows.map((r, i) => (
<div key={i} className="flex gap-2 items-center">
<input className="flex-1 bg-surface border border-surface-border rounded-lg px-3 py-1.5 text-xs text-text-primary focus:outline-none focus:border-gold"
placeholder={`Deliverable ${i+1}`} value={r.title} onChange={e => updRow(i,'title',e.target.value)} />
<input type="date" className="bg-surface border border-surface-border rounded-lg px-2 py-1.5 text-xs text-text-primary focus:outline-none focus:border-gold"
value={r.due_date} onChange={e => updRow(i,'due_date',e.target.value)} />
<select className="bg-surface border border-surface-border rounded-lg px-2 py-1.5 text-xs text-text-primary focus:outline-none focus:border-gold"
value={r.status} onChange={e => updRow(i,'status',e.target.value)}>
{STATUS_OPTIONS.map(s => <option key={s.value} value={s.value}>{s.label}</option>)}
</select>
{rows.length > 1 && (
<button type="button" onClick={() => setRows(d => d.filter((_,idx)=>idx!==i))}
className="text-red-400 hover:text-red-300 px-1"></button>
)}
</div>
))}
</div>
</div>
)}
<div className="flex justify-end gap-2 pt-2 border-t border-surface-border">
<Button variant="secondary" onClick={onClose}>Cancel</Button>
<Button onClick={handleSubmit} disabled={saving || !name.trim()}>
{saving ? 'Saving...' : isEditing ? 'Save Changes' : 'Create Project'}
</Button>
</div>
</div>
</Modal>
)
}