2026-03-05 15:39:21 -06:00
|
|
|
import { useState, useEffect } from 'react'
|
2026-03-05 12:13:22 -06:00
|
|
|
import ProjectCard from './ProjectCard'
|
|
|
|
|
import ProjectModal from './ProjectModal'
|
|
|
|
|
import Button from '../UI/Button'
|
2026-03-05 15:39:21 -06:00
|
|
|
import AgendaPanel from '../Calendar/AgendaPanel'
|
2026-03-05 12:13:22 -06:00
|
|
|
import useProjectStore from '../../store/useProjectStore'
|
2026-03-05 15:39:21 -06:00
|
|
|
import useUIStore from '../../store/useUIStore'
|
2026-03-06 00:03:06 -06:00
|
|
|
import { deleteProject, fetchProjects } from '../../api/projects'
|
|
|
|
|
|
|
|
|
|
const VIEW_OPTIONS = [
|
|
|
|
|
{ key: 'active', label: 'Active' },
|
|
|
|
|
{ key: 'archived', label: 'Archived' },
|
|
|
|
|
{ key: 'all', label: 'All' },
|
|
|
|
|
]
|
2026-03-05 12:13:22 -06:00
|
|
|
|
2026-03-05 15:39:21 -06:00
|
|
|
export default function ProjectList({ onRegisterNewProject }) {
|
2026-03-06 00:03:06 -06:00
|
|
|
const { projects, removeProject, setProjects } = useProjectStore()
|
2026-03-05 16:08:22 -06:00
|
|
|
const { sidebarTab, setSidebarTab } = useUIStore()
|
2026-03-06 00:03:06 -06:00
|
|
|
const [showModal, setShowModal] = useState(false)
|
|
|
|
|
const [editing, setEditing] = useState(null)
|
|
|
|
|
const [projectView, setProjectView] = useState('active')
|
|
|
|
|
const [search, setSearch] = useState('')
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
onRegisterNewProject?.(() => setShowModal(true))
|
|
|
|
|
}, [onRegisterNewProject])
|
2026-03-05 15:39:21 -06:00
|
|
|
|
2026-03-06 00:03:06 -06:00
|
|
|
const refreshProjects = async () => {
|
|
|
|
|
const data = await fetchProjects()
|
|
|
|
|
setProjects(data)
|
|
|
|
|
}
|
2026-03-05 12:13:22 -06:00
|
|
|
|
|
|
|
|
const handleEdit = (p) => { setEditing(p); setShowModal(true) }
|
|
|
|
|
const handleDelete = async (p) => {
|
|
|
|
|
if (window.confirm(`Delete "${p.name}" and all its deliverables?`)) {
|
2026-03-06 00:03:06 -06:00
|
|
|
await deleteProject(p.id)
|
|
|
|
|
removeProject(p.id)
|
2026-03-05 12:13:22 -06:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
const handleClose = () => { setShowModal(false); setEditing(null) }
|
|
|
|
|
|
2026-03-06 00:03:06 -06:00
|
|
|
const q = search.trim().toLowerCase()
|
|
|
|
|
const visibleProjects = (projects || [])
|
|
|
|
|
.filter(p => {
|
|
|
|
|
const isArchived = !!p.archived_at
|
|
|
|
|
if (projectView === 'active') return !isArchived
|
|
|
|
|
if (projectView === 'archived') return isArchived
|
|
|
|
|
return true
|
|
|
|
|
})
|
|
|
|
|
.filter(p => {
|
|
|
|
|
if (!q) return true
|
|
|
|
|
const hay = `${p.name || ''} ${p.description || ''}`.toLowerCase()
|
|
|
|
|
return hay.includes(q)
|
|
|
|
|
})
|
|
|
|
|
|
2026-03-05 12:13:22 -06:00
|
|
|
return (
|
|
|
|
|
<div className="flex flex-col h-full">
|
2026-03-05 16:08:22 -06:00
|
|
|
|
2026-03-06 00:03:06 -06:00
|
|
|
{/* Header */}
|
2026-03-05 16:21:00 -06:00
|
|
|
<div className="flex items-center gap-3 px-4 py-4 border-b border-surface-border flex-shrink-0 pl-10">
|
2026-03-05 16:08:22 -06:00
|
|
|
<img
|
|
|
|
|
src="/logo.png"
|
|
|
|
|
alt="FabDash logo"
|
2026-03-05 16:21:00 -06:00
|
|
|
className="w-12 h-12 object-contain rounded flex-shrink-0"
|
2026-03-05 16:08:22 -06:00
|
|
|
onError={e => { e.target.style.display = 'none' }}
|
|
|
|
|
/>
|
2026-03-05 16:21:00 -06:00
|
|
|
<div className="flex flex-col justify-center min-w-0">
|
|
|
|
|
<span className="text-base font-black tracking-widest uppercase leading-none">
|
|
|
|
|
<span className="text-gold">FAB</span><span className="text-white">DASH</span>
|
|
|
|
|
</span>
|
2026-03-05 16:08:22 -06:00
|
|
|
</div>
|
|
|
|
|
<div className="ml-auto flex-shrink-0">
|
|
|
|
|
<Button size="sm" onClick={() => setShowModal(true)}>+ Project</Button>
|
|
|
|
|
</div>
|
2026-03-05 12:13:22 -06:00
|
|
|
</div>
|
2026-03-05 15:39:21 -06:00
|
|
|
|
|
|
|
|
{/* Tab toggle */}
|
|
|
|
|
<div className="flex border-b border-surface-border flex-shrink-0">
|
|
|
|
|
{[['projects','Projects'],['agenda','Upcoming']].map(([key, label]) => (
|
|
|
|
|
<button key={key} onClick={() => setSidebarTab(key)}
|
2026-03-06 00:03:06 -06:00
|
|
|
className={`flex-1 py-2 text-xs font-semibold transition-colors ${
|
|
|
|
|
sidebarTab === key
|
|
|
|
|
? 'text-gold border-b-2 border-gold'
|
|
|
|
|
: 'text-text-muted hover:text-text-primary'
|
|
|
|
|
}`}>
|
2026-03-05 15:39:21 -06:00
|
|
|
{label}
|
|
|
|
|
</button>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Content */}
|
|
|
|
|
<div className="flex-1 overflow-y-auto">
|
|
|
|
|
{sidebarTab === 'projects' ? (
|
|
|
|
|
<div className="p-3 space-y-2">
|
2026-03-06 00:03:06 -06:00
|
|
|
|
|
|
|
|
{/* View toggle + Search + Refresh */}
|
|
|
|
|
<div className="flex items-center gap-2 pb-1">
|
|
|
|
|
<div className="flex bg-surface-elevated border border-surface-border rounded-lg overflow-hidden flex-shrink-0">
|
|
|
|
|
{VIEW_OPTIONS.map(v => (
|
|
|
|
|
<button
|
|
|
|
|
key={v.key}
|
|
|
|
|
onClick={() => setProjectView(v.key)}
|
|
|
|
|
className={`px-2.5 py-1.5 text-[10px] font-semibold transition-colors ${
|
|
|
|
|
projectView === v.key
|
|
|
|
|
? 'bg-gold text-surface'
|
|
|
|
|
: 'text-text-muted hover:text-text-primary'
|
|
|
|
|
}`}
|
|
|
|
|
title={`Show ${v.label.toLowerCase()} projects`}
|
|
|
|
|
>
|
|
|
|
|
{v.label}
|
|
|
|
|
</button>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
<input
|
|
|
|
|
value={search}
|
|
|
|
|
onChange={e => setSearch(e.target.value)}
|
|
|
|
|
placeholder="Search projects…"
|
|
|
|
|
className="flex-1 min-w-0 bg-surface-elevated border border-surface-border rounded-lg px-2.5 py-1.5 text-xs text-text-primary placeholder:text-text-muted/50 outline-none focus:border-gold/40 transition-colors"
|
|
|
|
|
/>
|
|
|
|
|
<button
|
|
|
|
|
onClick={refreshProjects}
|
|
|
|
|
className="flex-shrink-0 text-[10px] px-2 py-1.5 rounded-lg border border-surface-border bg-surface-elevated text-text-muted hover:text-text-primary hover:border-gold/30 transition-colors"
|
|
|
|
|
title="Refresh projects"
|
|
|
|
|
>
|
|
|
|
|
↺
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{visibleProjects.length === 0 ? (
|
2026-03-05 15:39:21 -06:00
|
|
|
<div className="flex flex-col items-center justify-center py-10 px-4 text-center">
|
|
|
|
|
<div className="opacity-20 mb-3">
|
|
|
|
|
<svg width="56" height="56" viewBox="0 0 56 56" fill="none">
|
|
|
|
|
<rect x="7" y="12" width="42" height="37" rx="3" stroke="#C9A84C" strokeWidth="1.5"/>
|
|
|
|
|
<line x1="7" y1="21" x2="49" y2="21" stroke="#C9A84C" strokeWidth="1.5"/>
|
|
|
|
|
<circle cx="17" cy="7" r="3.5" stroke="#C9A84C" strokeWidth="1.5"/>
|
|
|
|
|
<circle cx="39" cy="7" r="3.5" stroke="#C9A84C" strokeWidth="1.5"/>
|
|
|
|
|
<line x1="17" y1="31" x2="39" y2="31" stroke="#C9A84C" strokeWidth="1.5" strokeDasharray="4 2.5"/>
|
|
|
|
|
<line x1="17" y1="39" x2="30" y2="39" stroke="#C9A84C" strokeWidth="1.5" strokeDasharray="4 2.5"/>
|
|
|
|
|
</svg>
|
|
|
|
|
</div>
|
2026-03-06 00:03:06 -06:00
|
|
|
<p className="text-text-muted text-sm font-medium">
|
|
|
|
|
{q ? 'No matching projects' : projectView === 'archived' ? 'No archived projects' : 'No projects yet'}
|
|
|
|
|
</p>
|
2026-03-05 15:39:21 -06:00
|
|
|
<p className="text-text-muted/50 text-xs mt-1">
|
|
|
|
|
Press <kbd className="bg-surface-border px-1.5 py-0.5 rounded text-[10px] font-mono">N</kbd> or click + Project
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
2026-03-06 00:03:06 -06:00
|
|
|
visibleProjects.map(p => (
|
|
|
|
|
<ProjectCard key={p.id} project={p} onEdit={handleEdit} onDelete={handleDelete} onArchiveToggle={refreshProjects} />
|
2026-03-05 15:39:21 -06:00
|
|
|
))
|
|
|
|
|
)}
|
2026-03-05 12:13:22 -06:00
|
|
|
</div>
|
2026-03-05 15:39:21 -06:00
|
|
|
) : (
|
|
|
|
|
<AgendaPanel />
|
2026-03-05 12:13:22 -06:00
|
|
|
)}
|
2026-03-05 15:39:21 -06:00
|
|
|
</div>
|
|
|
|
|
|
2026-03-05 16:08:22 -06:00
|
|
|
{/* Keyboard shortcut legend */}
|
2026-03-05 15:39:21 -06:00
|
|
|
<div className="flex-shrink-0 border-t border-surface-border px-4 py-2 flex flex-wrap gap-x-3 gap-y-1">
|
2026-03-05 16:08:22 -06:00
|
|
|
{[['N','New'],['B','Sidebar'],['← →','Navigate'],['T','Today']].map(([key, desc]) => (
|
2026-03-05 15:39:21 -06:00
|
|
|
<span key={key} className="flex items-center gap-1 text-[10px] text-text-muted/50">
|
|
|
|
|
<kbd className="bg-surface-border px-1 py-0.5 rounded text-[9px] font-mono">{key}</kbd>
|
|
|
|
|
{desc}
|
|
|
|
|
</span>
|
2026-03-05 12:13:22 -06:00
|
|
|
))}
|
|
|
|
|
</div>
|
2026-03-05 15:39:21 -06:00
|
|
|
|
2026-03-05 12:13:22 -06:00
|
|
|
<ProjectModal isOpen={showModal} onClose={handleClose} project={editing} />
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|