feat: Mission Control dashboard for project management
This commit is contained in:
@@ -2,17 +2,9 @@
|
||||
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
|
||||
type SpeechRecognitionInstance = {
|
||||
lang: string;
|
||||
interimResults: boolean;
|
||||
continuous: boolean;
|
||||
onresult: ((event: { results: Array<{ 0?: { transcript?: string } }> }) => void) | null;
|
||||
onerror: ((event: unknown) => void) | null;
|
||||
onend: (() => void) | null;
|
||||
start: () => void;
|
||||
stop: () => void;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
type SpeechRecognitionInstance = any;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
type SpeechRecognitionConstructor = new () => SpeechRecognitionInstance;
|
||||
|
||||
type ChatMessage = {
|
||||
|
||||
@@ -0,0 +1,272 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import { useMissionControl } from "@/lib/mission-control/store";
|
||||
import { Task, TaskStatus } from "@/lib/mission-control/types";
|
||||
|
||||
interface ProjectSummary {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
status: "active" | "paused" | "completed";
|
||||
color: string;
|
||||
}
|
||||
|
||||
const projects: ProjectSummary[] = [
|
||||
{
|
||||
id: "sitemente",
|
||||
name: "SiteMente",
|
||||
description: "AI website platform for local businesses (B2B)",
|
||||
status: "active",
|
||||
color: "#ff7bc0",
|
||||
},
|
||||
{
|
||||
id: "holacompi",
|
||||
name: "HolaCompi",
|
||||
description: "AI ally for immigrants/consumers (B2C)",
|
||||
status: "paused",
|
||||
color: "#6366f1",
|
||||
},
|
||||
];
|
||||
|
||||
const statusConfig: Record<TaskStatus, { label: string; color: string; bg: string }> = {
|
||||
todo: { label: "To Do", color: "text-white/70", bg: "bg-white/10" },
|
||||
in_progress: { label: "In Progress", color: "text-yellow-400", bg: "bg-yellow-500/20" },
|
||||
done: { label: "Done", color: "text-green-400", bg: "bg-green-500/20" },
|
||||
blocked: { label: "Blocked", color: "text-red-400", bg: "bg-red-500/20" },
|
||||
paused: { label: "Paused", color: "text-gray-400", bg: "bg-gray-500/20" },
|
||||
};
|
||||
|
||||
const fadeUp = {
|
||||
hidden: { opacity: 0, y: 20 },
|
||||
visible: { opacity: 1, y: 0 },
|
||||
};
|
||||
|
||||
export default function MissionControlDashboard() {
|
||||
const { tasks, toggleTask, updateTaskStatus, getProjectProgress, getTasksByProject } =
|
||||
useMissionControl();
|
||||
const [selectedProject, setSelectedProject] = useState<"sitemente" | "holacompi">("sitemente");
|
||||
const [filter, setFilter] = useState<TaskStatus | "all">("all");
|
||||
|
||||
const projectTasks = getTasksByProject(selectedProject);
|
||||
const filteredTasks = filter === "all" ? projectTasks : projectTasks.filter((t) => t.status === filter);
|
||||
const progress = getProjectProgress(selectedProject);
|
||||
|
||||
const selectedProjectData = projects.find((p) => p.id === selectedProject)!;
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#1a1625] text-white">
|
||||
{/* Header */}
|
||||
<header className="border-b border-white/10 bg-[#2d2640] px-6 py-4">
|
||||
<div className="mx-auto flex max-w-7xl items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-xl bg-brand-pink text-xl">
|
||||
👁️
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-xl font-bold">Mission Control</h1>
|
||||
<p className="text-xs text-white/60">SiteMente + HolaCompi</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="text-right">
|
||||
<p className="text-sm text-white/60">Total Progress</p>
|
||||
<p className="text-2xl font-bold">{progress}%</p>
|
||||
</div>
|
||||
<div className="h-12 w-12 rounded-full border-4 border-brand-pink bg-white/10">
|
||||
<svg className="h-full w-full -rotate-90" viewBox="0 0 36 36">
|
||||
<circle
|
||||
cx="18"
|
||||
cy="18"
|
||||
r="16"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="3"
|
||||
className="text-white/20"
|
||||
/>
|
||||
<circle
|
||||
cx="18"
|
||||
cy="18"
|
||||
r="16"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="3"
|
||||
strokeDasharray={`${progress}, 100`}
|
||||
className="text-brand-pink"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className="mx-auto max-w-7xl px-6 py-8">
|
||||
{/* Project Tabs */}
|
||||
<div className="mb-8 flex gap-4">
|
||||
{projects.map((project) => {
|
||||
const p = getProjectProgress(project.id);
|
||||
return (
|
||||
<button
|
||||
key={project.id}
|
||||
onClick={() => setSelectedProject(project.id as "sitemente" | "holacompi")}
|
||||
className={`relative flex-1 rounded-xl border p-4 text-left transition ${
|
||||
selectedProject === project.id
|
||||
? "border-white/30 bg-white/10"
|
||||
: "border-white/10 bg-white/5 hover:border-white/20"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div
|
||||
className="h-3 w-3 rounded-full"
|
||||
style={{ backgroundColor: project.color }}
|
||||
/>
|
||||
<span className="font-semibold">{project.name}</span>
|
||||
<span
|
||||
className={`rounded-full px-2 py-0.5 text-xs ${
|
||||
project.status === "active"
|
||||
? "bg-green-500/20 text-green-400"
|
||||
: "bg-yellow-500/20 text-yellow-400"
|
||||
}`}
|
||||
>
|
||||
{project.status}
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-sm font-bold">{p}%</span>
|
||||
</div>
|
||||
<div className="mt-3 h-1.5 w-full rounded-full bg-white/10">
|
||||
<div
|
||||
className="h-full rounded-full transition-all duration-500"
|
||||
style={{ width: `${p}%`, backgroundColor: project.color }}
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Stats Row */}
|
||||
<div className="mb-8 grid grid-cols-4 gap-4">
|
||||
{(["todo", "in_progress", "done", "blocked"] as TaskStatus[]).map((status) => {
|
||||
const count = projectTasks.filter((t) => t.status === status).length;
|
||||
const config = statusConfig[status];
|
||||
return (
|
||||
<button
|
||||
key={status}
|
||||
onClick={() => setFilter(filter === status ? "all" : status)}
|
||||
className={`rounded-xl border p-4 text-center transition ${
|
||||
filter === status
|
||||
? "border-white/40 bg-white/10"
|
||||
: "border-white/10 bg-white/5 hover:border-white/20"
|
||||
}`}
|
||||
>
|
||||
<p className={`text-2xl font-bold ${config.color}`}>{count}</p>
|
||||
<p className="text-xs text-white/60">{config.label}</p>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Task List */}
|
||||
<div className="rounded-xl border border-white/10 bg-white/5">
|
||||
<div className="border-b border-white/10 px-6 py-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-lg font-semibold">
|
||||
{selectedProjectData.name} Tasks
|
||||
</h2>
|
||||
<p className="text-sm text-white/60">
|
||||
{filteredTasks.filter((t) => t.status === "done").length} /{" "}
|
||||
{filteredTasks.length} completed
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="divide-y divide-white/5">
|
||||
{filteredTasks.map((task, index) => {
|
||||
const config = statusConfig[task.status];
|
||||
return (
|
||||
<motion.div
|
||||
key={task.id}
|
||||
variants={fadeUp}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
transition={{ duration: 0.3, delay: index * 0.03 }}
|
||||
className={`flex items-center gap-4 px-6 py-4 transition hover:bg-white/5 ${
|
||||
task.status === "done" ? "opacity-50" : ""
|
||||
}`}
|
||||
>
|
||||
<button
|
||||
onClick={() => toggleTask(task.id)}
|
||||
className={`flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full border-2 transition ${
|
||||
task.status === "done"
|
||||
? "border-green-500 bg-green-500 text-white"
|
||||
: "border-white/30 hover:border-white/50"
|
||||
}`}
|
||||
>
|
||||
{task.status === "done" && (
|
||||
<svg className="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={3} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<p
|
||||
className={`font-medium ${
|
||||
task.status === "done" ? "line-through text-white/50" : ""
|
||||
}`}
|
||||
>
|
||||
{task.title}
|
||||
</p>
|
||||
{task.priority === "critical" && (
|
||||
<span className="rounded-full bg-red-500/20 px-2 py-0.5 text-xs text-red-400">
|
||||
CRITICAL
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm text-white/50 truncate">{task.description}</p>
|
||||
</div>
|
||||
|
||||
<select
|
||||
value={task.status}
|
||||
onChange={(e) => updateTaskStatus(task.id, e.target.value as TaskStatus)}
|
||||
className={`rounded-lg border border-white/20 bg-white/10 px-3 py-1.5 text-xs ${config.color} focus:border-white/40 focus:outline-none`}
|
||||
>
|
||||
<option value="todo">To Do</option>
|
||||
<option value="in_progress">In Progress</option>
|
||||
<option value="done">Done</option>
|
||||
<option value="blocked">Blocked</option>
|
||||
</select>
|
||||
</motion.div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{filteredTasks.length === 0 && (
|
||||
<div className="px-6 py-12 text-center text-white/50">
|
||||
No tasks match the current filter.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Quick Actions */}
|
||||
<div className="mt-8 flex gap-4">
|
||||
<button
|
||||
onClick={() => {
|
||||
const confirmed = window.confirm("Reset all tasks? This cannot be undone.");
|
||||
if (confirmed && typeof window !== "undefined") {
|
||||
localStorage.removeItem("sitemente:mission-control");
|
||||
window.location.reload();
|
||||
}
|
||||
}}
|
||||
className="rounded-lg border border-white/20 bg-white/5 px-4 py-2 text-sm text-white/70 hover:bg-white/10"
|
||||
>
|
||||
Reset All Tasks
|
||||
</button>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user