This commit is contained in:
2026-05-14 19:24:50 +00:00
parent af0628867f
commit a7f218e182
63 changed files with 10539 additions and 0 deletions

View File

@@ -0,0 +1,120 @@
import { useEffect, useState } from 'react';
import { api } from '@/api/client';
import type { AvailableProject } from '@/api/types';
import { Button } from '@/components/ui/button';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
interface Props {
open: boolean;
onOpenChange: (open: boolean) => void;
onAdded: () => void;
}
export function AddProjectModal({ open, onOpenChange, onAdded }: Props) {
const [available, setAvailable] = useState<AvailableProject[] | null>(null);
const [customPath, setCustomPath] = useState('');
const [error, setError] = useState<string | null>(null);
const [busy, setBusy] = useState(false);
useEffect(() => {
if (!open) return;
setError(null);
setCustomPath('');
setAvailable(null);
api.projects
.available()
.then(setAvailable)
.catch((err) =>
setError(err instanceof Error ? err.message : 'failed to list available projects')
);
}, [open]);
async function add(path: string) {
setBusy(true);
setError(null);
try {
await api.projects.add({ path });
onAdded();
onOpenChange(false);
} catch (err) {
setError(err instanceof Error ? err.message : 'failed to add');
} finally {
setBusy(false);
}
}
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>Add project</DialogTitle>
<DialogDescription>
Pick from detected repos in /opt or type a path.
</DialogDescription>
</DialogHeader>
<div className="space-y-3">
<div className="rounded-md border max-h-64 overflow-y-auto">
{available === null && (
<div className="px-3 py-2 text-sm text-muted-foreground">Loading</div>
)}
{available && available.length === 0 && (
<div className="px-3 py-2 text-sm text-muted-foreground">
No undiscovered repos in /opt.
</div>
)}
{available?.map((p) => (
<button
key={p.path}
disabled={busy}
onClick={() => void add(p.path)}
className="w-full text-left px-3 py-2 hover:bg-muted disabled:opacity-50 border-b last:border-b-0"
>
<div className="text-sm font-medium">{p.name}</div>
<div className="text-xs text-muted-foreground font-mono">{p.path}</div>
</button>
))}
</div>
<div className="space-y-1.5">
<Label htmlFor="custom-path">Custom path</Label>
<div className="flex gap-2">
<Input
id="custom-path"
placeholder="/opt/some-repo"
value={customPath}
onChange={(e) => setCustomPath(e.target.value)}
disabled={busy}
/>
<Button
onClick={() => void add(customPath.trim())}
disabled={busy || !customPath.trim()}
>
Add
</Button>
</div>
</div>
{error && (
<div className="text-sm text-destructive">{error}</div>
)}
</div>
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)} disabled={busy}>
Cancel
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}