132 lines
5.5 KiB
TypeScript
132 lines
5.5 KiB
TypeScript
import { useState, useEffect } from 'react';
|
|
import { Link } from 'react-router-dom';
|
|
import { FolderKanban, Wrench, TrendingUp, Sparkles, ArrowRight, CheckCircle2 } from 'lucide-react';
|
|
import type { Project, Tool } from '../types';
|
|
import { getProjects, getTools } from '../api';
|
|
import ProjectCard from '../components/projects/ProjectCard';
|
|
import ToolCard from '../components/tools/ToolCard';
|
|
import ProgressBar from '../components/ui/ProgressBar';
|
|
import { useSettings } from '../hooks/useSettings';
|
|
|
|
function StatCard({ label, value, icon: Icon, sub }: { label: string; value: string | number; icon: any; sub?: string }) {
|
|
return (
|
|
<div className="bg-card border border-border rounded-xl p-5">
|
|
<div className="flex items-start justify-between mb-3">
|
|
<p className="text-sm text-muted font-medium">{label}</p>
|
|
<div className="w-8 h-8 rounded-lg bg-[var(--accent-dim)] border border-[var(--accent)]/20 flex items-center justify-center">
|
|
<Icon size={15} style={{ color: 'var(--accent)' }} />
|
|
</div>
|
|
</div>
|
|
<p className="text-3xl font-bold text-white tabular-nums">{value}</p>
|
|
{sub && <p className="text-xs text-muted mt-1">{sub}</p>}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default function Dashboard() {
|
|
const { settings } = useSettings();
|
|
const [projects, setProjects] = useState<Project[]>([]);
|
|
const [tools, setTools] = useState<Tool[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
useEffect(() => {
|
|
Promise.all([getProjects(), getTools()])
|
|
.then(([p, t]) => { setProjects(p); setTools(t); })
|
|
.finally(() => setLoading(false));
|
|
}, []);
|
|
|
|
const active = projects.filter((p) => p.status === 'active');
|
|
const complete = projects.filter((p) => p.status === 'complete');
|
|
const newTools = tools.filter((t) => t.is_new);
|
|
const avgCompletion = projects.length
|
|
? Math.round(projects.reduce((s, p) => s + p.completion, 0) / projects.length)
|
|
: 0;
|
|
const recent = [...projects].slice(0, 6);
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="flex items-center justify-center h-64">
|
|
<div className="w-6 h-6 border-2 border-[var(--accent)] border-t-transparent rounded-full animate-spin" />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="p-8 max-w-7xl mx-auto">
|
|
{/* Header */}
|
|
<div className="mb-8">
|
|
<h1 className="text-2xl font-bold text-white mb-1">{settings.app_title}</h1>
|
|
<p className="text-muted text-sm">High-level overview of tools and projects.</p>
|
|
</div>
|
|
|
|
{/* Stats */}
|
|
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
|
|
<StatCard label="Total Projects" value={projects.length} icon={FolderKanban} sub={`${active.length} active`} />
|
|
<StatCard label="Completed" value={complete.length} icon={CheckCircle2} sub="finished projects" />
|
|
<StatCard label="Avg Completion" value={`${avgCompletion}%`} icon={TrendingUp} sub="across all projects" />
|
|
<StatCard label="Available Tools" value={tools.length} icon={Wrench} sub={`${newTools.length} new`} />
|
|
</div>
|
|
|
|
{/* Overall progress */}
|
|
{projects.length > 0 && (
|
|
<div className="bg-card border border-border rounded-xl p-5 mb-8">
|
|
<div className="flex items-center justify-between mb-3">
|
|
<p className="text-sm font-medium text-white">Overall Portfolio Progress</p>
|
|
<span className="text-sm font-mono text-white">{avgCompletion}%</span>
|
|
</div>
|
|
<ProgressBar value={avgCompletion} size="md" />
|
|
</div>
|
|
)}
|
|
|
|
{/* New Tools Spotlight */}
|
|
{newTools.length > 0 && (
|
|
<section className="mb-8">
|
|
<div className="flex items-center justify-between mb-4">
|
|
<div className="flex items-center gap-2">
|
|
<Sparkles size={16} style={{ color: 'var(--accent)' }} />
|
|
<h2 className="text-base font-semibold text-white">New Tools Available</h2>
|
|
</div>
|
|
<Link to="/tools" className="text-xs text-muted hover:text-white flex items-center gap-1 transition-colors">
|
|
View all <ArrowRight size={12} />
|
|
</Link>
|
|
</div>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
{newTools.slice(0, 3).map((tool) => (
|
|
<ToolCard key={tool.id} tool={tool} />
|
|
))}
|
|
</div>
|
|
</section>
|
|
)}
|
|
|
|
{/* Recent Projects */}
|
|
<section>
|
|
<div className="flex items-center justify-between mb-4">
|
|
<div className="flex items-center gap-2">
|
|
<FolderKanban size={16} style={{ color: 'var(--accent)' }} />
|
|
<h2 className="text-base font-semibold text-white">Recent Projects</h2>
|
|
</div>
|
|
<Link to="/projects" className="text-xs text-muted hover:text-white flex items-center gap-1 transition-colors">
|
|
View all <ArrowRight size={12} />
|
|
</Link>
|
|
</div>
|
|
|
|
{recent.length === 0 ? (
|
|
<div className="bg-card border border-border rounded-xl p-12 text-center">
|
|
<FolderKanban size={32} className="mx-auto mb-3 text-muted/40" />
|
|
<p className="text-muted text-sm">No projects yet.</p>
|
|
<Link to="/projects" className="text-xs mt-2 inline-block" style={{ color: 'var(--accent)' }}>
|
|
Create your first project →
|
|
</Link>
|
|
</div>
|
|
) : (
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
{recent.map((project) => (
|
|
<ProjectCard key={project.id} project={project} />
|
|
))}
|
|
</div>
|
|
)}
|
|
</section>
|
|
</div>
|
|
);
|
|
}
|