import { useState } from "react"; import { open } from "@tauri-apps/plugin-dialog"; import type { Project, AuthMode } from "../../lib/types"; import { useProjects } from "../../hooks/useProjects"; import { useTerminal } from "../../hooks/useTerminal"; import { useAppState } from "../../store/appState"; interface Props { project: Project; } export default function ProjectCard({ project }: Props) { const { selectedProjectId, setSelectedProject } = useAppState(); const { start, stop, rebuild, remove, update } = useProjects(); const { open: openTerminal } = useTerminal(); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [showConfig, setShowConfig] = useState(false); const isSelected = selectedProjectId === project.id; const isStopped = project.status === "stopped" || project.status === "error"; const handleStart = async () => { setLoading(true); setError(null); try { await start(project.id); } catch (e) { setError(String(e)); } finally { setLoading(false); } }; const handleStop = async () => { setLoading(true); setError(null); try { await stop(project.id); } catch (e) { setError(String(e)); } finally { setLoading(false); } }; const handleOpenTerminal = async () => { try { await openTerminal(project.id, project.name); } catch (e) { setError(String(e)); } }; const handleAuthModeChange = async (mode: AuthMode) => { try { await update({ ...project, auth_mode: mode }); } catch (e) { setError(String(e)); } }; const handleBrowseSSH = async () => { const selected = await open({ directory: true, multiple: false }); if (selected) { try { await update({ ...project, ssh_key_path: selected as string }); } catch (e) { setError(String(e)); } } }; const statusColor = { stopped: "bg-[var(--text-secondary)]", starting: "bg-[var(--warning)]", running: "bg-[var(--success)]", stopping: "bg-[var(--warning)]", error: "bg-[var(--error)]", }[project.status]; return (
setSelectedProject(project.id)} className={`px-3 py-2 rounded cursor-pointer transition-colors ${ isSelected ? "bg-[var(--bg-tertiary)]" : "hover:bg-[var(--bg-tertiary)]" }`} >
{project.name}
{project.path}
{isSelected && (
{/* Auth mode selector */}
Auth:
{/* Action buttons */}
{isStopped ? ( ) : project.status === "running" ? ( <> { setLoading(true); try { await rebuild(project.id); } catch (e) { setError(String(e)); } setLoading(false); }} disabled={loading} label="Reset" /> ) : ( {project.status}... )} { e?.stopPropagation?.(); setShowConfig(!showConfig); }} disabled={false} label={showConfig ? "Hide" : "Config"} /> { if (confirm(`Remove project "${project.name}"?`)) { await remove(project.id); } }} disabled={loading} label="Remove" danger />
{/* Config panel */} {showConfig && (
e.stopPropagation()}> {/* SSH Key */}
{ try { await update({ ...project, ssh_key_path: e.target.value || null }); } catch {} }} placeholder="~/.ssh" disabled={!isStopped} className="flex-1 px-2 py-1 bg-[var(--bg-primary)] border border-[var(--border-color)] rounded text-xs text-[var(--text-primary)] focus:outline-none focus:border-[var(--accent)] disabled:opacity-50" />
{/* Git Name */}
{ try { await update({ ...project, git_user_name: e.target.value || null }); } catch {} }} placeholder="Your Name" disabled={!isStopped} className="w-full px-2 py-1 bg-[var(--bg-primary)] border border-[var(--border-color)] rounded text-xs text-[var(--text-primary)] focus:outline-none focus:border-[var(--accent)] disabled:opacity-50" />
{/* Git Email */}
{ try { await update({ ...project, git_user_email: e.target.value || null }); } catch {} }} placeholder="you@example.com" disabled={!isStopped} className="w-full px-2 py-1 bg-[var(--bg-primary)] border border-[var(--border-color)] rounded text-xs text-[var(--text-primary)] focus:outline-none focus:border-[var(--accent)] disabled:opacity-50" />
{/* Git Token (HTTPS) */}
{ try { await update({ ...project, git_token: e.target.value || null }); } catch {} }} placeholder="ghp_..." disabled={!isStopped} className="w-full px-2 py-1 bg-[var(--bg-primary)] border border-[var(--border-color)] rounded text-xs text-[var(--text-primary)] focus:outline-none focus:border-[var(--accent)] disabled:opacity-50" />
{/* Docker access toggle */}
)}
)} {error && (
{error}
)}
); } function ActionButton({ onClick, disabled, label, accent, danger, }: { onClick: (e?: React.MouseEvent) => void; disabled: boolean; label: string; accent?: boolean; danger?: boolean; }) { let color = "text-[var(--text-secondary)] hover:text-[var(--text-primary)]"; if (accent) color = "text-[var(--accent)] hover:text-[var(--accent-hover)]"; if (danger) color = "text-[var(--error)] hover:text-[var(--error)]"; return ( ); }