Add files via upload

This commit is contained in:
jasonMPM
2026-03-06 00:03:06 -06:00
committed by GitHub
parent 118c186881
commit 59619c4ed1
6 changed files with 211 additions and 83 deletions

View File

@@ -1,9 +1,12 @@
import os import os
from flask import Flask, send_from_directory from flask import Flask, send_from_directory
from sqlalchemy import text from sqlalchemy import text
from .extensions import db, migrate, cors from .extensions import db, migrate, cors
from config import config from config import config
def create_app(config_name=None): def create_app(config_name=None):
if config_name is None: if config_name is None:
config_name = os.environ.get('FLASK_ENV', 'production') config_name = os.environ.get('FLASK_ENV', 'production')
@@ -17,6 +20,7 @@ def create_app(config_name=None):
from .routes.projects import projects_bp from .routes.projects import projects_bp
from .routes.deliverables import deliverables_bp from .routes.deliverables import deliverables_bp
app.register_blueprint(projects_bp, url_prefix='/api') app.register_blueprint(projects_bp, url_prefix='/api')
app.register_blueprint(deliverables_bp, url_prefix='/api') app.register_blueprint(deliverables_bp, url_prefix='/api')
@@ -43,7 +47,9 @@ def _run_migrations():
""" """
migrations = [ migrations = [
'ALTER TABLE projects ADD COLUMN drive_url VARCHAR(500)', 'ALTER TABLE projects ADD COLUMN drive_url VARCHAR(500)',
'ALTER TABLE projects ADD COLUMN archived_at DATETIME',
] ]
with db.engine.connect() as conn: with db.engine.connect() as conn:
for stmt in migrations: for stmt in migrations:
try: try:

View File

@@ -1,6 +1,7 @@
from .extensions import db from .extensions import db
from datetime import datetime, date from datetime import datetime, date
class Project(db.Model): class Project(db.Model):
__tablename__ = 'projects' __tablename__ = 'projects'
@@ -9,11 +10,14 @@ class Project(db.Model):
color = db.Column(db.String(7), nullable=False, default='#C9A84C') color = db.Column(db.String(7), nullable=False, default='#C9A84C')
description = db.Column(db.Text) description = db.Column(db.Text)
drive_url = db.Column(db.String(500)) drive_url = db.Column(db.String(500))
archived_at = db.Column(db.DateTime, nullable=True)
created_at = db.Column(db.DateTime, default=datetime.utcnow) created_at = db.Column(db.DateTime, default=datetime.utcnow)
deliverables = db.relationship( deliverables = db.relationship(
'Deliverable', backref='project', 'Deliverable',
cascade='all, delete-orphan', lazy=True backref='project',
cascade='all, delete-orphan',
lazy=True,
) )
def to_dict(self, include_deliverables=True): def to_dict(self, include_deliverables=True):
@@ -23,6 +27,7 @@ class Project(db.Model):
'color': self.color, 'color': self.color,
'description': self.description, 'description': self.description,
'drive_url': self.drive_url, 'drive_url': self.drive_url,
'archived_at': self.archived_at.isoformat() if self.archived_at else None,
'created_at': self.created_at.isoformat() if self.created_at else None, 'created_at': self.created_at.isoformat() if self.created_at else None,
} }
if include_deliverables: if include_deliverables:
@@ -36,7 +41,11 @@ class Deliverable(db.Model):
__tablename__ = 'deliverables' __tablename__ = 'deliverables'
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
project_id = db.Column(db.Integer, db.ForeignKey('projects.id', ondelete='CASCADE'), nullable=False) project_id = db.Column(
db.Integer,
db.ForeignKey('projects.id', ondelete='CASCADE'),
nullable=False,
)
title = db.Column(db.String(300), nullable=False) title = db.Column(db.String(300), nullable=False)
due_date = db.Column(db.Date, nullable=False) due_date = db.Column(db.Date, nullable=False)
status = db.Column(db.String(20), nullable=False, default='upcoming') status = db.Column(db.String(20), nullable=False, default='upcoming')
@@ -44,9 +53,8 @@ class Deliverable(db.Model):
def effective_status(self): def effective_status(self):
""" """
Returns 'overdue' if the due date has passed and the deliverable Returns 'overdue' if the due date has passed and the deliverable has not been
has not been marked completed. Completed deliverables are never marked completed. Completed deliverables are never auto-downgraded.
auto-downgraded regardless of date.
""" """
if self.status != 'completed' and self.due_date < date.today(): if self.status != 'completed' and self.due_date < date.today():
return 'overdue' return 'overdue'

View File

@@ -1,20 +1,23 @@
from flask import Blueprint, jsonify, request from flask import Blueprint, jsonify, request
from ..models import Project, Deliverable from ..models import Project, Deliverable
from ..extensions import db from ..extensions import db
from datetime import date from datetime import date, datetime
projects_bp = Blueprint('projects', __name__) projects_bp = Blueprint('projects', __name__)
@projects_bp.route('/projects', methods=['GET']) @projects_bp.route('/projects', methods=['GET'])
def get_projects(): def get_projects():
projects = Project.query.order_by(Project.created_at.desc()).all() projects = Project.query.order_by(Project.created_at.desc()).all()
return jsonify([p.to_dict() for p in projects]) return jsonify([p.to_dict() for p in projects])
@projects_bp.route('/projects/<int:id>', methods=['GET']) @projects_bp.route('/projects/<int:id>', methods=['GET'])
def get_project(id): def get_project(id):
project = Project.query.get_or_404(id) project = Project.query.get_or_404(id)
return jsonify(project.to_dict()) return jsonify(project.to_dict())
@projects_bp.route('/projects', methods=['POST']) @projects_bp.route('/projects', methods=['POST'])
def create_project(): def create_project():
data = request.get_json() data = request.get_json()
@@ -26,6 +29,7 @@ def create_project():
) )
db.session.add(project) db.session.add(project)
db.session.flush() db.session.flush()
for d in data.get('deliverables', []): for d in data.get('deliverables', []):
if d.get('title') and d.get('due_date'): if d.get('title') and d.get('due_date'):
db.session.add(Deliverable( db.session.add(Deliverable(
@@ -34,9 +38,11 @@ def create_project():
due_date=date.fromisoformat(d['due_date']), due_date=date.fromisoformat(d['due_date']),
status=d.get('status', 'upcoming'), status=d.get('status', 'upcoming'),
)) ))
db.session.commit() db.session.commit()
return jsonify(project.to_dict()), 201 return jsonify(project.to_dict()), 201
@projects_bp.route('/projects/<int:id>', methods=['PATCH']) @projects_bp.route('/projects/<int:id>', methods=['PATCH'])
def update_project(id): def update_project(id):
project = Project.query.get_or_404(id) project = Project.query.get_or_404(id)
@@ -47,6 +53,23 @@ def update_project(id):
db.session.commit() db.session.commit()
return jsonify(project.to_dict()) return jsonify(project.to_dict())
@projects_bp.route('/projects/<int:id>/archive', methods=['PATCH'])
def archive_project(id):
project = Project.query.get_or_404(id)
project.archived_at = datetime.utcnow()
db.session.commit()
return jsonify(project.to_dict())
@projects_bp.route('/projects/<int:id>/unarchive', methods=['PATCH'])
def unarchive_project(id):
project = Project.query.get_or_404(id)
project.archived_at = None
db.session.commit()
return jsonify(project.to_dict())
@projects_bp.route('/projects/<int:id>', methods=['DELETE']) @projects_bp.route('/projects/<int:id>', methods=['DELETE'])
def delete_project(id): def delete_project(id):
project = Project.query.get_or_404(id) project = Project.query.get_or_404(id)

View File

@@ -1,7 +1,11 @@
import axios from 'axios' import axios from 'axios'
const B = '/api' const B = '/api'
export const fetchProjects = () => axios.get(`${B}/projects`).then(r => r.data) export const fetchProjects = () => axios.get(`${B}/projects`).then(r => r.data)
export const fetchProject = (id) => axios.get(`${B}/projects/${id}`).then(r => r.data) export const fetchProject = (id) => axios.get(`${B}/projects/${id}`).then(r => r.data)
export const createProject = (data) => axios.post(`${B}/projects`, data).then(r => r.data) export const createProject = (data) => axios.post(`${B}/projects`, data).then(r => r.data)
export const updateProject = (id, d) => axios.patch(`${B}/projects/${id}`, d).then(r => r.data) export const updateProject = (id, d) => axios.patch(`${B}/projects/${id}`, d).then(r => r.data)
export const deleteProject = (id) => axios.delete(`${B}/projects/${id}`).then(r => r.data) export const deleteProject = (id) => axios.delete(`${B}/projects/${id}`).then(r => r.data)
export const archiveProject = (id) => axios.patch(`${B}/projects/${id}/archive`).then(r => r.data)
export const unarchiveProject = (id) => axios.patch(`${B}/projects/${id}/unarchive`).then(r => r.data)

View File

@@ -4,25 +4,21 @@ import { formatDate } from '../../utils/dateHelpers'
import useFocusStore from '../../store/useFocusStore' import useFocusStore from '../../store/useFocusStore'
import useProjectStore from '../../store/useProjectStore' import useProjectStore from '../../store/useProjectStore'
import { deleteDeliverable } from '../../api/deliverables' import { deleteDeliverable } from '../../api/deliverables'
import { archiveProject, unarchiveProject } from '../../api/projects'
import DeliverableModal from '../Deliverables/DeliverableModal' import DeliverableModal from '../Deliverables/DeliverableModal'
import ContextMenu from '../UI/ContextMenu' import ContextMenu from '../UI/ContextMenu'
function DriveIcon() { function DriveIcon() {
return ( return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 87.3 78" className="w-3.5 h-3.5"> <svg width="11" height="11" viewBox="0 0 24 24" fill="currentColor">
<path d="m6.6 66.85 3.85 6.65c.8 1.4 1.95 2.5 3.3 3.3l13.75-23.8h-27.5c0 1.55.4 3.1 1.2 4.5z" fill="#0066da"/> <path d="M4.43 20.933l2.893-5.01H22l-2.893 5.01H4.43zm1.55-5.01L2.55 10.08 9.163 2h4.903l-6.613 8.08 3.043 5.843H5.98zm8.753 0l-3.043-5.843L17.987 2H22l-4.297 6.89 3.043 5.843h-6.013z"/>
<path d="m43.65 25-13.75-23.8c-1.35.8-2.5 1.9-3.3 3.3l-25.4 44a9.06 9.06 0 0 0 -1.2 4.5h27.5z" fill="#00ac47"/>
<path d="m73.55 76.8c1.35-.8 2.5-1.9 3.3-3.3l1.6-2.75 7.65-13.25c.8-1.4 1.2-2.95 1.2-4.5h-27.502l5.852 11.5z" fill="#ea4335"/>
<path d="m43.65 25 13.75-23.8c-1.35-.8-2.9-1.2-4.5-1.2h-18.5c-1.6 0-3.15.45-4.5 1.2z" fill="#00832d"/>
<path d="m59.8 53h-32.3l-13.75 23.8c1.35.8 2.9 1.2 4.5 1.2h50.8c1.6 0 3.15-.45 4.5-1.2z" fill="#2684fc"/>
<path d="m73.4 26.5-12.7-22c-.8-1.4-1.95-2.5-3.3-3.3l-13.75 23.8 16.15 27h27.45c0-1.55-.4-3.1-1.2-4.5z" fill="#ffba00"/>
</svg> </svg>
) )
} }
export default function ProjectCard({ project, onEdit, onDelete }) { export default function ProjectCard({ project, onEdit, onDelete, onArchiveToggle }) {
const openFocus = useFocusStore(s => s.openFocus) const openFocus = useFocusStore(s => s.openFocus)
const { removeDeliverable } = useProjectStore() const { removeDeliverable, updateProject } = useProjectStore()
const [delModal, setDelModal] = useState({ open: false, deliverable: null }) const [delModal, setDelModal] = useState({ open: false, deliverable: null })
const [ctxMenu, setCtxMenu] = useState(null) const [ctxMenu, setCtxMenu] = useState(null)
@@ -52,11 +48,25 @@ export default function ProjectCard({ project, onEdit, onDelete }) {
const handleHeaderCtx = (e) => { const handleHeaderCtx = (e) => {
e.preventDefault() e.preventDefault()
const isArchived = !!project.archived_at
setCtxMenu({ setCtxMenu({
x: e.clientX, y: e.clientY, x: e.clientX, y: e.clientY,
items: [ items: [
{ icon: '✎', label: 'Edit Project', action: () => onEdit(project) }, { icon: '✎', label: 'Edit Project', action: () => onEdit(project) },
...(project.drive_url ? [{ icon: '⬡', label: 'Open Drive', action: () => window.open(project.drive_url, '_blank') }] : []), ...(project.drive_url
? [{ icon: '⬡', label: 'Open Drive', action: () => window.open(project.drive_url, '_blank') }]
: []),
{
icon: isArchived ? '↺' : '⏸',
label: isArchived ? 'Unarchive Project' : 'Archive Project',
action: async () => {
const updated = isArchived
? await unarchiveProject(project.id)
: await archiveProject(project.id)
updateProject(updated)
onArchiveToggle?.()
},
},
{ separator: true }, { separator: true },
{ icon: '✕', label: 'Delete Project', danger: true, action: () => onDelete(project) }, { icon: '✕', label: 'Delete Project', danger: true, action: () => onDelete(project) },
], ],
@@ -64,61 +74,68 @@ export default function ProjectCard({ project, onEdit, onDelete }) {
} }
return ( return (
<div className="bg-surface-elevated border border-surface-border rounded-lg overflow-hidden transition-all hover:border-gold/20"> <div className={`rounded-xl border bg-surface-raised overflow-hidden ${
<div className="h-1 w-full" style={{ backgroundColor: project.color }} /> project.archived_at ? 'opacity-60 border-surface-border' : 'border-surface-border'
<div className="p-3"> }`}>
{/* Header — double-click to edit, right-click for menu */} {/* Header */}
<div <div
className="flex items-start justify-between mb-1.5 cursor-default" className="flex items-center gap-2 px-3 py-2.5 cursor-pointer hover:bg-surface-elevated/40 transition-colors"
onDoubleClick={() => onEdit(project)} onDoubleClick={() => onEdit(project)}
onContextMenu={handleHeaderCtx} onContextMenu={handleHeaderCtx}
title="Double-click to edit project" title="Double-click to edit project"
> >
<div className="flex items-center gap-2 min-w-0"> <div className="w-3 h-3 rounded-full flex-shrink-0" style={{ backgroundColor: project.color }} />
<div className="w-2.5 h-2.5 rounded-full flex-shrink-0" style={{ backgroundColor: project.color }} /> <span className="font-semibold text-sm text-text-primary truncate flex-1">{project.name}</span>
<span className="text-sm font-semibold text-text-primary truncate">{project.name}</span>
</div> {project.archived_at && (
<div className="flex items-center gap-0.5 flex-shrink-0 ml-1"> <span className="text-[9px] font-semibold tracking-widest uppercase text-amber-400/70 bg-amber-400/10 border border-amber-400/20 rounded px-1.5 py-0.5 flex-shrink-0">
Archived
</span>
)}
{project.drive_url && ( {project.drive_url && (
<a href={project.drive_url} target="_blank" rel="noopener noreferrer" <a
title="Open Google Drive folder" onClick={e => e.stopPropagation()} href={project.drive_url}
className="flex items-center gap-1 text-[10px] text-text-muted hover:text-text-primary bg-surface hover:bg-surface-border/40 border border-surface-border hover:border-gold/30 rounded px-1.5 py-1 transition-all mr-1"> target="_blank"
<DriveIcon /><span>Drive</span> rel="noreferrer"
onClick={e => e.stopPropagation()}
className="flex items-center gap-1 text-[10px] text-text-muted hover:text-text-primary bg-surface hover:bg-surface-border/40 border border-surface-border hover:border-gold/30 rounded px-1.5 py-1 transition-all mr-1"
>
<DriveIcon /> Drive
</a> </a>
)} )}
<button onClick={() => onEdit(project)} className="text-text-muted hover:text-gold p-1 transition-colors text-sm" title="Edit project"></button> <button onClick={() => onEdit(project)} className="text-text-muted hover:text-gold p-1 transition-colors text-sm" title="Edit project"></button>
<button onClick={() => onDelete(project)} className="text-text-muted hover:text-red-400 p-1 transition-colors text-sm" title="Delete project"></button> <button onClick={() => onDelete(project)} className="text-text-muted hover:text-red-400 p-1 transition-colors text-sm" title="Delete project"></button>
</div> </div>
</div>
{project.description && ( {project.description && (
<p className="text-xs text-text-muted mb-2 line-clamp-1">{project.description}</p> <p className="text-text-muted text-xs px-3 pb-1.5 -mt-1 truncate">{project.description}</p>
)} )}
{/* Deliverable rows */} {/* Deliverable rows */}
<div className="space-y-1"> <div className="px-2 pb-2 space-y-1">
{(project.deliverables || []).map(d => ( {(project.deliverables || []).map(d => (
<button <button
key={d.id} key={d.id}
onClick={() => openFocus(project.id, d.id)} onClick={() => openFocus(project.id, d.id)}
onDoubleClick={(e) => { e.stopPropagation(); openDelEdit(d) }} onDoubleClick={e => { e.stopPropagation(); openDelEdit(d) }}
onContextMenu={(e) => handleRowCtx(e, d)} onContextMenu={e => handleRowCtx(e, d)}
title="Click: Focus View · Double-click: Edit · Right-click: Menu" title="Click: Focus View · Double-click: Edit · Right-click: Menu"
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" 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> <span className="truncate text-text-primary group-hover:text-white transition-colors">{d.title}</span>
<div className="flex items-center gap-1.5 flex-shrink-0"> <div className="flex items-center gap-1.5 flex-shrink-0 ml-2">
<span className="text-text-muted/60 font-mono text-[9px]">{formatDate(d.due_date)}</span> <span className="text-text-muted/60 text-[10px]">{formatDate(d.due_date)}</span>
<Badge status={d.status} /> <Badge status={d.status} />
</div> </div>
</button> </button>
))} ))}
{(!project.deliverables || project.deliverables.length === 0) && (
<p className="text-[11px] text-text-muted/40 italic text-center py-1">No deliverables</p>
)}
</div>
{(!project.deliverables || project.deliverables.length === 0) && (
<p className="text-text-muted/40 text-xs px-1 py-1">No deliverables</p>
)}
</div> </div>
{/* Local deliverable edit modal */} {/* Local deliverable edit modal */}

View File

@@ -5,28 +5,58 @@ import Button from '../UI/Button'
import AgendaPanel from '../Calendar/AgendaPanel' import AgendaPanel from '../Calendar/AgendaPanel'
import useProjectStore from '../../store/useProjectStore' import useProjectStore from '../../store/useProjectStore'
import useUIStore from '../../store/useUIStore' import useUIStore from '../../store/useUIStore'
import { deleteProject } from '../../api/projects' import { deleteProject, fetchProjects } from '../../api/projects'
const VIEW_OPTIONS = [
{ key: 'active', label: 'Active' },
{ key: 'archived', label: 'Archived' },
{ key: 'all', label: 'All' },
]
export default function ProjectList({ onRegisterNewProject }) { export default function ProjectList({ onRegisterNewProject }) {
const { projects, removeProject } = useProjectStore() const { projects, removeProject, setProjects } = useProjectStore()
const { sidebarTab, setSidebarTab } = useUIStore() const { sidebarTab, setSidebarTab } = useUIStore()
const [showModal, setShowModal] = useState(false) const [showModal, setShowModal] = useState(false)
const [editing, setEditing] = useState(null) const [editing, setEditing] = useState(null)
const [projectView, setProjectView] = useState('active')
const [search, setSearch] = useState('')
useEffect(() => { onRegisterNewProject?.(() => setShowModal(true)) }, [onRegisterNewProject]) useEffect(() => {
onRegisterNewProject?.(() => setShowModal(true))
}, [onRegisterNewProject])
const refreshProjects = async () => {
const data = await fetchProjects()
setProjects(data)
}
const handleEdit = (p) => { setEditing(p); setShowModal(true) } const handleEdit = (p) => { setEditing(p); setShowModal(true) }
const handleDelete = async (p) => { const handleDelete = async (p) => {
if (window.confirm(`Delete "${p.name}" and all its deliverables?`)) { if (window.confirm(`Delete "${p.name}" and all its deliverables?`)) {
await deleteProject(p.id); removeProject(p.id) await deleteProject(p.id)
removeProject(p.id)
} }
} }
const handleClose = () => { setShowModal(false); setEditing(null) } const handleClose = () => { setShowModal(false); setEditing(null) }
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)
})
return ( return (
<div className="flex flex-col h-full"> <div className="flex flex-col h-full">
{/* Header — taller to give logo more presence */} {/* Header */}
<div className="flex items-center gap-3 px-4 py-4 border-b border-surface-border flex-shrink-0 pl-10"> <div className="flex items-center gap-3 px-4 py-4 border-b border-surface-border flex-shrink-0 pl-10">
<img <img
src="/logo.png" src="/logo.png"
@@ -48,7 +78,11 @@ export default function ProjectList({ onRegisterNewProject }) {
<div className="flex border-b border-surface-border flex-shrink-0"> <div className="flex border-b border-surface-border flex-shrink-0">
{[['projects','Projects'],['agenda','Upcoming']].map(([key, label]) => ( {[['projects','Projects'],['agenda','Upcoming']].map(([key, label]) => (
<button key={key} onClick={() => setSidebarTab(key)} <button key={key} onClick={() => setSidebarTab(key)}
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'}`}> 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'
}`}>
{label} {label}
</button> </button>
))} ))}
@@ -58,7 +92,41 @@ export default function ProjectList({ onRegisterNewProject }) {
<div className="flex-1 overflow-y-auto"> <div className="flex-1 overflow-y-auto">
{sidebarTab === 'projects' ? ( {sidebarTab === 'projects' ? (
<div className="p-3 space-y-2"> <div className="p-3 space-y-2">
{projects.length === 0 ? (
{/* 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 ? (
<div className="flex flex-col items-center justify-center py-10 px-4 text-center"> <div className="flex flex-col items-center justify-center py-10 px-4 text-center">
<div className="opacity-20 mb-3"> <div className="opacity-20 mb-3">
<svg width="56" height="56" viewBox="0 0 56 56" fill="none"> <svg width="56" height="56" viewBox="0 0 56 56" fill="none">
@@ -70,14 +138,16 @@ export default function ProjectList({ onRegisterNewProject }) {
<line x1="17" y1="39" x2="30" y2="39" 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> </svg>
</div> </div>
<p className="text-text-muted text-sm font-medium">No projects yet</p> <p className="text-text-muted text-sm font-medium">
{q ? 'No matching projects' : projectView === 'archived' ? 'No archived projects' : 'No projects yet'}
</p>
<p className="text-text-muted/50 text-xs mt-1"> <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 Press <kbd className="bg-surface-border px-1.5 py-0.5 rounded text-[10px] font-mono">N</kbd> or click + Project
</p> </p>
</div> </div>
) : ( ) : (
projects.map(p => ( visibleProjects.map(p => (
<ProjectCard key={p.id} project={p} onEdit={handleEdit} onDelete={handleDelete} /> <ProjectCard key={p.id} project={p} onEdit={handleEdit} onDelete={handleDelete} onArchiveToggle={refreshProjects} />
)) ))
)} )}
</div> </div>