From 874cbfb6a824c5693865170567e92306726e8bdd Mon Sep 17 00:00:00 2001 From: jason Date: Wed, 22 Apr 2026 21:25:42 -0500 Subject: [PATCH] initial design --- ai-tools-dashboard/.dockerignore | 6 + ai-tools-dashboard/Dockerfile | 53 +++ ai-tools-dashboard/INSTALL.md | 185 ++++++++++ ai-tools-dashboard/client/index.html | 15 + ai-tools-dashboard/client/package.json | 29 ++ ai-tools-dashboard/client/postcss.config.js | 6 + ai-tools-dashboard/client/src/api/index.ts | 94 ++++++ .../client/src/components/layout/Layout.tsx | 13 + .../client/src/components/layout/Sidebar.tsx | 129 +++++++ .../src/components/projects/ProjectCard.tsx | 110 ++++++ .../src/components/projects/ProjectForm.tsx | 143 ++++++++ .../client/src/components/tools/ToolCard.tsx | 60 ++++ .../client/src/components/ui/Badge.tsx | 20 ++ .../client/src/components/ui/Button.tsx | 34 ++ .../client/src/components/ui/Modal.tsx | 36 ++ .../client/src/components/ui/ProgressBar.tsx | 25 ++ .../client/src/hooks/useAuth.ts | 72 ++++ .../client/src/hooks/useSettings.ts | 36 ++ ai-tools-dashboard/client/src/index.css | 44 +++ ai-tools-dashboard/client/src/main.tsx | 83 +++++ .../client/src/pages/AdminUsers.tsx | 315 ++++++++++++++++++ .../client/src/pages/Dashboard.tsx | 131 ++++++++ ai-tools-dashboard/client/src/pages/Login.tsx | 241 ++++++++++++++ .../client/src/pages/ProjectDetail.tsx | 296 ++++++++++++++++ .../client/src/pages/Projects.tsx | 125 +++++++ .../client/src/pages/Settings.tsx | 179 ++++++++++ ai-tools-dashboard/client/src/pages/Tools.tsx | 198 +++++++++++ ai-tools-dashboard/client/src/types/index.ts | 53 +++ ai-tools-dashboard/client/tailwind.config.js | 28 ++ ai-tools-dashboard/client/tsconfig.json | 19 ++ ai-tools-dashboard/client/vite.config.ts | 15 + ai-tools-dashboard/docker-compose.yml | 22 ++ ai-tools-dashboard/package.json | 14 + ai-tools-dashboard/server/package.json | 32 ++ ai-tools-dashboard/server/src/db/schema.ts | 99 ++++++ ai-tools-dashboard/server/src/index.ts | 39 +++ ai-tools-dashboard/server/src/lib/pinHash.ts | 9 + .../server/src/middleware/auth.ts | 46 +++ ai-tools-dashboard/server/src/routes/auth.ts | 54 +++ .../server/src/routes/projects.ts | 103 ++++++ .../server/src/routes/settings.ts | 55 +++ ai-tools-dashboard/server/src/routes/tools.ts | 79 +++++ .../server/src/routes/uploads.ts | 68 ++++ ai-tools-dashboard/server/src/routes/users.ts | 102 ++++++ ai-tools-dashboard/server/tsconfig.json | 15 + 45 files changed, 3530 insertions(+) create mode 100644 ai-tools-dashboard/.dockerignore create mode 100644 ai-tools-dashboard/Dockerfile create mode 100644 ai-tools-dashboard/INSTALL.md create mode 100644 ai-tools-dashboard/client/index.html create mode 100644 ai-tools-dashboard/client/package.json create mode 100644 ai-tools-dashboard/client/postcss.config.js create mode 100644 ai-tools-dashboard/client/src/api/index.ts create mode 100644 ai-tools-dashboard/client/src/components/layout/Layout.tsx create mode 100644 ai-tools-dashboard/client/src/components/layout/Sidebar.tsx create mode 100644 ai-tools-dashboard/client/src/components/projects/ProjectCard.tsx create mode 100644 ai-tools-dashboard/client/src/components/projects/ProjectForm.tsx create mode 100644 ai-tools-dashboard/client/src/components/tools/ToolCard.tsx create mode 100644 ai-tools-dashboard/client/src/components/ui/Badge.tsx create mode 100644 ai-tools-dashboard/client/src/components/ui/Button.tsx create mode 100644 ai-tools-dashboard/client/src/components/ui/Modal.tsx create mode 100644 ai-tools-dashboard/client/src/components/ui/ProgressBar.tsx create mode 100644 ai-tools-dashboard/client/src/hooks/useAuth.ts create mode 100644 ai-tools-dashboard/client/src/hooks/useSettings.ts create mode 100644 ai-tools-dashboard/client/src/index.css create mode 100644 ai-tools-dashboard/client/src/main.tsx create mode 100644 ai-tools-dashboard/client/src/pages/AdminUsers.tsx create mode 100644 ai-tools-dashboard/client/src/pages/Dashboard.tsx create mode 100644 ai-tools-dashboard/client/src/pages/Login.tsx create mode 100644 ai-tools-dashboard/client/src/pages/ProjectDetail.tsx create mode 100644 ai-tools-dashboard/client/src/pages/Projects.tsx create mode 100644 ai-tools-dashboard/client/src/pages/Settings.tsx create mode 100644 ai-tools-dashboard/client/src/pages/Tools.tsx create mode 100644 ai-tools-dashboard/client/src/types/index.ts create mode 100644 ai-tools-dashboard/client/tailwind.config.js create mode 100644 ai-tools-dashboard/client/tsconfig.json create mode 100644 ai-tools-dashboard/client/vite.config.ts create mode 100644 ai-tools-dashboard/docker-compose.yml create mode 100644 ai-tools-dashboard/package.json create mode 100644 ai-tools-dashboard/server/package.json create mode 100644 ai-tools-dashboard/server/src/db/schema.ts create mode 100644 ai-tools-dashboard/server/src/index.ts create mode 100644 ai-tools-dashboard/server/src/lib/pinHash.ts create mode 100644 ai-tools-dashboard/server/src/middleware/auth.ts create mode 100644 ai-tools-dashboard/server/src/routes/auth.ts create mode 100644 ai-tools-dashboard/server/src/routes/projects.ts create mode 100644 ai-tools-dashboard/server/src/routes/settings.ts create mode 100644 ai-tools-dashboard/server/src/routes/tools.ts create mode 100644 ai-tools-dashboard/server/src/routes/uploads.ts create mode 100644 ai-tools-dashboard/server/src/routes/users.ts create mode 100644 ai-tools-dashboard/server/tsconfig.json diff --git a/ai-tools-dashboard/.dockerignore b/ai-tools-dashboard/.dockerignore new file mode 100644 index 0000000..f80b7cc --- /dev/null +++ b/ai-tools-dashboard/.dockerignore @@ -0,0 +1,6 @@ +node_modules +*/node_modules +*/dist +data +.git +*.log diff --git a/ai-tools-dashboard/Dockerfile b/ai-tools-dashboard/Dockerfile new file mode 100644 index 0000000..bb37eaf --- /dev/null +++ b/ai-tools-dashboard/Dockerfile @@ -0,0 +1,53 @@ +# ── Stage 1: Build client ─────────────────────────────────────────────────── +FROM node:20-alpine AS client-builder +WORKDIR /build + +COPY package.json ./ +COPY client/package.json ./client/ +COPY server/package.json ./server/ + +RUN npm install --workspace=client + +COPY client/ ./client/ +RUN npm run build --workspace=client + +# ── Stage 2: Build server ─────────────────────────────────────────────────── +FROM node:20-alpine AS server-builder +WORKDIR /build + +COPY package.json ./ +COPY server/package.json ./server/ + +RUN npm install --workspace=server + +COPY server/ ./server/ +RUN npm run build --workspace=server + +# ── Stage 3: Runtime ──────────────────────────────────────────────────────── +FROM node:20-alpine AS runtime +WORKDIR /app + +ENV NODE_ENV=production +ENV PORT=3000 +ENV DATA_DIR=/data +ENV MAX_UPLOAD_MB=50 +ENV ADMIN_USERNAME=admin +ENV ADMIN_PASSWORD=codedump2024 +ENV JWT_SECRET=changeme-use-a-long-random-string + +# Install production deps only +COPY package.json ./ +COPY server/package.json ./server/ +RUN npm install --workspace=server --omit=dev + +# Copy built artifacts +COPY --from=server-builder /build/server/dist ./server/dist +COPY --from=client-builder /build/client/dist ./client/dist + +VOLUME ["/data"] +EXPOSE 3000 + +HEALTHCHECK --interval=30s --timeout=5s --start-period=10s \ + CMD wget -qO- http://localhost:3000/api/settings || exit 1 + +CMD ["node", "server/dist/index.js"] diff --git a/ai-tools-dashboard/INSTALL.md b/ai-tools-dashboard/INSTALL.md new file mode 100644 index 0000000..8a3ded7 --- /dev/null +++ b/ai-tools-dashboard/INSTALL.md @@ -0,0 +1,185 @@ +# Unraid Install Guide — CODEDUMP + +## Prerequisites + +- Unraid 6.10 or later +- Docker enabled (default on all modern Unraid installs) +- Community Applications plugin installed (recommended, not required) + +--- + +## Option A — Community Applications (Easiest) + +> Use this if you have the CA plugin installed. + +1. Open the **Apps** tab in the Unraid web UI. +2. Search for **CODEDUMP**. +3. Click **Install** and fill in the variables described in the [Variables](#variables) section below. +4. Click **Apply**. + +--- + +## Option B — Manual Docker Container (No CA Plugin) + +### Step 1 — Open the Docker tab + +In the Unraid web UI, click **Docker** in the top navigation bar. + +### Step 2 — Add a new container + +Click **Add Container** at the bottom of the page. + +### Step 3 — Fill in Basic Settings + +| Field | Value | +|---|---| +| **Name** | `codedump` | +| **Repository** | `your-registry/codedump:latest` *(or build locally — see below)* | +| **Network Type** | `bridge` | +| **Console shell command** | `sh` | + +> **Building locally:** If you cloned this repo to Unraid, open a terminal and run: +> ```bash +> cd /path/to/ai-tools-dashboard +> docker build -t codedump:local . +> ``` +> Then set **Repository** to `codedump:local`. + +### Step 4 — Port Mapping + +Click **Add another Path, Port, Variable, Label or Device** → **Port**. + +| Field | Value | +|---|---| +| **Name** | `WebUI` | +| **Container Port** | `3000` | +| **Host Port** | `3000` *(or any open port on your Unraid server)* | +| **Connection Type** | `TCP` | + +### Step 5 — Volume (Persistent Data) + +Click **Add another Path, Port, Variable, Label or Device** → **Path**. + +| Field | Value | +|---|---| +| **Name** | `Data` | +| **Container Path** | `/data` | +| **Host Path** | `/mnt/user/appdata/codedump` | +| **Access Mode** | `Read/Write` | + +> This single volume stores the **SQLite database**, all **uploaded files**, and **user accounts**. +> Unraid creates the directory automatically on first run. + +### Step 6 — Environment Variables {#variables} + +For each variable, click **Add another Path, Port, Variable, Label or Device** → **Variable**. + +| Variable | Default | Required | Description | +|---|---|---|---| +| `PORT` | `3000` | No | Port the app listens on inside the container. Match to your Container Port above. | +| `DATA_DIR` | `/data` | No | Path inside the container for persistent data. Do not change unless you remapped the volume. | +| `MAX_UPLOAD_MB` | `50` | No | Maximum upload size in MB for documents and logos. | +| `ADMIN_USERNAME` | `admin` | **Yes** | Username for the bootstrap admin account. Set this before first launch. | +| `ADMIN_PASSWORD` | `codedump2024` | **Yes** | Password for the bootstrap admin account. **Change this to something strong.** | +| `JWT_SECRET` | *(insecure default)* | **Yes** | Secret used to sign login tokens. Set to a long random string (32+ chars). | + +> **Security note:** `ADMIN_PASSWORD` and `JWT_SECRET` must be changed from their defaults before exposing CODEDUMP to your network. The bootstrap admin is only created once — changing `ADMIN_PASSWORD` after first launch has no effect on the existing account (use the Admin → Users page instead). + +### Step 7 — WebUI Link (Optional but Recommended) + +| Field | Value | +|---|---| +| **WebUI** | `http://[IP]:[PORT:3000]` | + +This adds a clickable **WebUI** button on the Docker tab. + +### Step 8 — Apply + +Click **Apply**. Unraid will pull/build the image and start the container. + +--- + +## First Launch & Login + +1. Open `http://YOUR-UNRAID-IP:3000` in your browser. +2. You will be redirected to the **login page**. +3. Click **Admin Login** at the bottom of the login screen. +4. Enter the `ADMIN_USERNAME` and `ADMIN_PASSWORD` you set in the environment variables. +5. Once in, navigate to **Admin → Users** to create user accounts for your team. + +### User Account Types + +| Type | Login Method | Access | +|---|---|---| +| **Admin** | Username + Password | Full access including Settings and User Management | +| **User** | Select name → 4-digit PIN | Projects, Tools, Documents — all read/write | + +### Creating Users (Admin only) + +1. Click **Users** in the sidebar (Admin section). +2. Click **New User**. +3. Enter a username, select role **User**, and assign a **4-digit PIN**. +4. The user's name will appear on the login screen — they select it and enter their PIN. + +--- + +## Updating + +### Registry image: +1. Go to **Docker** tab → click the container icon → **Update** (or Force Update). +2. The `/data` volume is preserved — database, uploads, and user accounts are safe. + +### Locally built image: +```bash +cd /path/to/ai-tools-dashboard +git pull +docker build -t codedump:local . +``` +Then restart the container from the Unraid Docker tab. + +--- + +## Backup + +Everything lives in one directory: + +``` +/mnt/user/appdata/codedump/ +├── dashboard.db ← SQLite database (projects, tools, settings, users) +└── uploads/ ← Uploaded files (logos, markdown docs) +``` + +Back this up with Unraid's **Appdata Backup** plugin or any solution that covers `/mnt/user/appdata`. + +--- + +## Generating a Strong JWT_SECRET + +Run this in an Unraid terminal or any Linux shell: + +```bash +openssl rand -hex 32 +``` + +Paste the output as the value of `JWT_SECRET`. + +--- + +## Port Conflicts + +Change **Host Port** to an unused port (e.g. `3100`) and update the WebUI link to match. + +--- + +## Troubleshooting + +| Symptom | Fix | +|---|---| +| Stuck on login / "Session expired" | Clear browser localStorage and reload. | +| "No user accounts yet" on login | Log in as admin first, then create users via Admin → Users. | +| Admin can't log in after reinstall | If `/data` was wiped, bootstrap re-runs on next start using the current env vars. | +| Container exits immediately | Check Docker logs — usually a permissions issue on `/data`. | +| Can't upload files | Verify `MAX_UPLOAD_MB` and that the host path is writable. | +| Blank page / 404 | Wait 15 seconds after start, then refresh. | + +View container logs: **Docker tab** → click the container icon → **Logs**. diff --git a/ai-tools-dashboard/client/index.html b/ai-tools-dashboard/client/index.html new file mode 100644 index 0000000..63ceef9 --- /dev/null +++ b/ai-tools-dashboard/client/index.html @@ -0,0 +1,15 @@ + + + + + + CODEDUMP + + + + + +
+ + + diff --git a/ai-tools-dashboard/client/package.json b/ai-tools-dashboard/client/package.json new file mode 100644 index 0000000..e398276 --- /dev/null +++ b/ai-tools-dashboard/client/package.json @@ -0,0 +1,29 @@ +{ + "name": "ai-tools-dashboard-client", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview" + }, + "dependencies": { + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-router-dom": "^6.22.3", + "react-markdown": "^9.0.1", + "remark-gfm": "^4.0.0", + "lucide-react": "^0.364.0" + }, + "devDependencies": { + "@types/react": "^18.3.1", + "@types/react-dom": "^18.3.0", + "@vitejs/plugin-react": "^4.2.1", + "autoprefixer": "^10.4.19", + "postcss": "^8.4.38", + "tailwindcss": "^3.4.3", + "typescript": "^5.4.2", + "vite": "^5.2.6" + } +} diff --git a/ai-tools-dashboard/client/postcss.config.js b/ai-tools-dashboard/client/postcss.config.js new file mode 100644 index 0000000..2aa7205 --- /dev/null +++ b/ai-tools-dashboard/client/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/ai-tools-dashboard/client/src/api/index.ts b/ai-tools-dashboard/client/src/api/index.ts new file mode 100644 index 0000000..96f6dca --- /dev/null +++ b/ai-tools-dashboard/client/src/api/index.ts @@ -0,0 +1,94 @@ +import type { Project, Tool, Settings } from '../types'; + +const BASE = '/api'; +const TOKEN_KEY = 'codedump_token'; + +function getToken(): string | null { + return localStorage.getItem(TOKEN_KEY); +} + +async function req(path: string, options: RequestInit = {}): Promise { + const token = getToken(); + const headers: Record = { + ...(options.headers as Record || {}), + }; + if (token) headers['Authorization'] = `Bearer ${token}`; + if (options.body && typeof options.body === 'string') headers['Content-Type'] = 'application/json'; + + const res = await fetch(`${BASE}${path}`, { ...options, headers }); + + if (res.status === 401) { + // Token expired — clear storage and reload to login + localStorage.removeItem(TOKEN_KEY); + localStorage.removeItem('codedump_user'); + window.location.href = '/login'; + throw new Error('Session expired'); + } + + if (!res.ok) { + const err = await res.json().catch(() => ({ error: res.statusText })); + throw new Error(err.error || res.statusText); + } + if (res.status === 204) return undefined as T; + return res.json(); +} + +// Auth +export const pinLogin = (pin: string) => + req<{ token: string; user: any }>('/auth/pin', { method: 'POST', body: JSON.stringify({ pin }) }); +export const adminLogin = (username: string, password: string) => + req<{ token: string; user: any }>('/auth/login', { method: 'POST', body: JSON.stringify({ username, password }) }); + +// Users (admin only) +export const getUsers = () => req('/users'); +export const createUser = (data: any) => + req('/users', { method: 'POST', body: JSON.stringify(data) }); +export const updateUser = (id: string, data: any) => + req(`/users/${id}`, { method: 'PUT', body: JSON.stringify(data) }); +export const deleteUser = (id: string) => req(`/users/${id}`, { method: 'DELETE' }); + +// Projects +export const getProjects = () => req('/projects'); +export const getProject = (id: string) => req(`/projects/${id}`); +export const createProject = (data: Partial) => + req('/projects', { method: 'POST', body: JSON.stringify(data) }); +export const updateProject = (id: string, data: Partial) => + req(`/projects/${id}`, { method: 'PUT', body: JSON.stringify(data) }); +export const deleteProject = (id: string) => req(`/projects/${id}`, { method: 'DELETE' }); + +// Tools +export const getTools = () => req('/tools'); +export const createTool = (data: Partial) => + req('/tools', { method: 'POST', body: JSON.stringify(data) }); +export const updateTool = (id: string, data: Partial) => + req(`/tools/${id}`, { method: 'PUT', body: JSON.stringify(data) }); +export const deleteTool = (id: string) => req(`/tools/${id}`, { method: 'DELETE' }); + +// Uploads +export const uploadDocument = (projectId: string, file: File) => { + const form = new FormData(); + form.append('file', file); + const token = getToken(); + return fetch(`${BASE}/uploads/projects/${projectId}`, { + method: 'POST', + headers: token ? { Authorization: `Bearer ${token}` } : {}, + body: form, + }).then((r) => r.json()); +}; +export const deleteDocument = (id: string) => req(`/uploads/documents/${id}`, { method: 'DELETE' }); +export const getFileUrl = (filename: string) => `${BASE}/uploads/${filename}`; + +// Settings +export const getSettings = () => req('/settings'); +export const updateSettings = (data: Partial) => + req('/settings', { method: 'PUT', body: JSON.stringify(data) }); +export const uploadLogo = (file: File) => { + const form = new FormData(); + form.append('logo', file); + const token = getToken(); + return fetch(`${BASE}/settings/logo`, { + method: 'POST', + headers: token ? { Authorization: `Bearer ${token}` } : {}, + body: form, + }).then((r) => r.json()); +}; diff --git a/ai-tools-dashboard/client/src/components/layout/Layout.tsx b/ai-tools-dashboard/client/src/components/layout/Layout.tsx new file mode 100644 index 0000000..196037e --- /dev/null +++ b/ai-tools-dashboard/client/src/components/layout/Layout.tsx @@ -0,0 +1,13 @@ +import { Outlet } from 'react-router-dom'; +import Sidebar from './Sidebar'; + +export default function Layout() { + return ( +
+ +
+ +
+
+ ); +} diff --git a/ai-tools-dashboard/client/src/components/layout/Sidebar.tsx b/ai-tools-dashboard/client/src/components/layout/Sidebar.tsx new file mode 100644 index 0000000..de004a5 --- /dev/null +++ b/ai-tools-dashboard/client/src/components/layout/Sidebar.tsx @@ -0,0 +1,129 @@ +import { NavLink, useNavigate } from 'react-router-dom'; +import { LayoutDashboard, FolderKanban, Wrench, Settings, Users, LogOut, Shield } from 'lucide-react'; +import { useSettings } from '../../hooks/useSettings'; +import { useAuth } from '../../hooks/useAuth'; + +const userLinks = [ + { to: '/', icon: LayoutDashboard, label: 'Dashboard' }, + { to: '/projects', icon: FolderKanban, label: 'Projects' }, + { to: '/tools', icon: Wrench, label: 'Tools' }, +]; + +const adminLinks = [ + { to: '/settings', icon: Settings, label: 'Settings' }, + { to: '/admin/users', icon: Users, label: 'Users' }, +]; + +export default function Sidebar() { + const { settings } = useSettings(); + const { user, logout } = useAuth(); + const navigate = useNavigate(); + + const handleLogout = () => { + logout(); + navigate('/login', { replace: true }); + }; + + const isAdmin = user?.role === 'admin'; + + return ( + + ); +} diff --git a/ai-tools-dashboard/client/src/components/projects/ProjectCard.tsx b/ai-tools-dashboard/client/src/components/projects/ProjectCard.tsx new file mode 100644 index 0000000..f072800 --- /dev/null +++ b/ai-tools-dashboard/client/src/components/projects/ProjectCard.tsx @@ -0,0 +1,110 @@ +import { useNavigate } from 'react-router-dom'; +import { ExternalLink, FolderOpen, FileText, Percent } from 'lucide-react'; +import type { Project } from '../../types'; +import ProgressBar from '../ui/ProgressBar'; +import Badge from '../ui/Badge'; + +interface Props { + project: Project; +} + +export default function ProjectCard({ project }: Props) { + const navigate = useNavigate(); + + return ( +
navigate(`/projects/${project.id}`)} + className="group relative bg-card border border-border rounded-xl p-5 cursor-pointer hover:border-[var(--accent)]/40 hover:shadow-lg hover:shadow-black/20 transition-all duration-200 animate-fade-in" + > + {/* Accent top bar */} +
+ + {/* Header */} +
+
+
+ {project.is_new && New} + {project.status} +
+

{project.name}

+

{project.category}

+
+
+ +
+
+ + {/* Description */} + {project.description && ( +

{project.description}

+ )} + + {/* Progress */} +
+
+ + Completion + + {project.completion}% +
+ +
+ + {/* Footer */} + + + {/* Tags */} + {project.tags.length > 0 && ( +
+ {project.tags.map((tag) => ( + + {tag} + + ))} +
+ )} +
+ ); +} diff --git a/ai-tools-dashboard/client/src/components/projects/ProjectForm.tsx b/ai-tools-dashboard/client/src/components/projects/ProjectForm.tsx new file mode 100644 index 0000000..c9a5bd5 --- /dev/null +++ b/ai-tools-dashboard/client/src/components/projects/ProjectForm.tsx @@ -0,0 +1,143 @@ +import { useState } from 'react'; +import type { Project } from '../../types'; +import Button from '../ui/Button'; + +const ACCENT_PRESETS = [ + '#6366f1', '#8b5cf6', '#ec4899', '#f97316', + '#10b981', '#06b6d4', '#eab308', '#ef4444', +]; + +const CATEGORIES = ['General', 'AI/ML', 'Automation', 'Data', 'DevOps', 'Frontend', 'Backend', 'Research', 'Other']; + +interface Props { + initial?: Partial; + onSubmit: (data: Partial) => Promise; + onCancel: () => void; +} + +export default function ProjectForm({ initial, onSubmit, onCancel }: Props) { + const [form, setForm] = useState({ + name: initial?.name || '', + description: initial?.description || '', + category: initial?.category || 'General', + status: initial?.status || 'active', + completion: initial?.completion ?? 0, + external_url: initial?.external_url || '', + drive_url: initial?.drive_url || '', + tags: initial?.tags?.join(', ') || '', + accent_color: initial?.accent_color || '#6366f1', + is_new: initial?.is_new ?? true, + }); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(''); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (!form.name.trim()) { setError('Name is required'); return; } + setLoading(true); + setError(''); + try { + await onSubmit({ + ...form, + tags: form.tags.split(',').map((t) => t.trim()).filter(Boolean), + completion: Number(form.completion), + external_url: form.external_url || null, + drive_url: form.drive_url || null, + }); + } catch (err: any) { + setError(err.message || 'Something went wrong'); + setLoading(false); + } + }; + + const field = 'w-full bg-surface border border-border rounded-lg px-3 py-2 text-sm text-white placeholder:text-muted focus:outline-none focus:border-[var(--accent)] transition-colors'; + + return ( +
+ {error &&

{error}

} + +
+
+ + setForm({ ...form, name: e.target.value })} placeholder="My AI Project" /> +
+ +
+ + +
+ +
+ + +
+ +
+ +