Add files via upload
This commit is contained in:
42
frontend/src/components/Projects/ProjectCard.jsx
Normal file
42
frontend/src/components/Projects/ProjectCard.jsx
Normal 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>
|
||||
)
|
||||
}
|
||||
41
frontend/src/components/Projects/ProjectList.jsx
Normal file
41
frontend/src/components/Projects/ProjectList.jsx
Normal 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>
|
||||
)
|
||||
}
|
||||
112
frontend/src/components/Projects/ProjectModal.jsx
Normal file
112
frontend/src/components/Projects/ProjectModal.jsx
Normal 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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user