Makelore 2.0 initial clean snapshot
This commit is contained in:
164
src/pages/Projects/index.tsx
Normal file
164
src/pages/Projects/index.tsx
Normal file
@@ -0,0 +1,164 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Check, FolderOpen, FolderPlus, RefreshCw } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { useOpencodeStore } from '@/stores/opencode';
|
||||
import { useProjectConfigStore } from '@/stores/project-config';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
function formatOpenedAt(value: string): string {
|
||||
const date = new Date(value);
|
||||
if (Number.isNaN(date.getTime())) return value;
|
||||
return date.toLocaleString();
|
||||
}
|
||||
|
||||
export function Projects() {
|
||||
const projects = useOpencodeStore((state) => state.projects);
|
||||
const activeProject = useOpencodeStore((state) => state.activeProject);
|
||||
const loading = useOpencodeStore((state) => state.loading);
|
||||
const error = useOpencodeStore((state) => state.error);
|
||||
const loadProjects = useOpencodeStore((state) => state.loadProjects);
|
||||
const pickAndOpenProject = useOpencodeStore((state) => state.pickAndOpenProject);
|
||||
const setActiveProject = useOpencodeStore((state) => state.setActiveProject);
|
||||
const loadProjectConfig = useProjectConfigStore((state) => state.load);
|
||||
const [projectEntryError, setProjectEntryError] = useState<string | null>(null);
|
||||
const availableProjects = projects;
|
||||
|
||||
useEffect(() => {
|
||||
void loadProjects();
|
||||
}, [loadProjects]);
|
||||
|
||||
const activateVerifiedProject = async (project: { id: string }) => {
|
||||
try {
|
||||
setProjectEntryError(null);
|
||||
const result = await loadProjectConfig(project.id);
|
||||
if (result.status === 'valid') {
|
||||
await setActiveProject(project.id);
|
||||
return;
|
||||
}
|
||||
if (result.status === 'missing') {
|
||||
setProjectEntryError('Project configuration is missing. Create the project from the sidebar first.');
|
||||
return;
|
||||
}
|
||||
setProjectEntryError(result.error ?? 'Project configuration is invalid.');
|
||||
} catch (error) {
|
||||
setProjectEntryError(error instanceof Error ? error.message : String(error));
|
||||
}
|
||||
};
|
||||
|
||||
const handleOpenFolder = async () => {
|
||||
const project = await pickAndOpenProject();
|
||||
if (!project) return;
|
||||
await activateVerifiedProject(project);
|
||||
};
|
||||
|
||||
return (
|
||||
<main className="flex h-full min-h-0 flex-col bg-background">
|
||||
<div className="flex shrink-0 items-center justify-between border-b px-6 py-4">
|
||||
<div className="min-w-0">
|
||||
<h1 className="text-xl font-semibold tracking-normal">Projects</h1>
|
||||
<div className="mt-1 flex min-w-0 items-center gap-2 text-sm text-muted-foreground">
|
||||
<FolderOpen className="h-4 w-4 shrink-0" />
|
||||
<span className="truncate">
|
||||
{activeProject ? activeProject.path : 'No project selected'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex shrink-0 items-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => void loadProjects()}
|
||||
disabled={loading}
|
||||
>
|
||||
<RefreshCw className={cn('mr-2 h-4 w-4', loading && 'animate-spin')} />
|
||||
Refresh
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => void handleOpenFolder()}
|
||||
disabled={loading}
|
||||
>
|
||||
<FolderPlus className="mr-2 h-4 w-4" />
|
||||
Open Folder
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="min-h-0 flex-1 overflow-auto p-6">
|
||||
{error && (
|
||||
<div className="mb-4 rounded-md border border-destructive/30 bg-destructive/10 px-4 py-3 text-sm text-destructive">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
{projectEntryError && (
|
||||
<div className="mb-4 rounded-md border border-destructive/30 bg-destructive/10 px-4 py-3 text-sm text-destructive">
|
||||
{projectEntryError}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{availableProjects.length === 0 ? (
|
||||
<div className="flex min-h-[240px] items-center justify-center rounded-md border border-dashed">
|
||||
<div className="text-center">
|
||||
<FolderOpen className="mx-auto h-9 w-9 text-muted-foreground" />
|
||||
<p className="mt-3 text-sm font-medium">No projects yet</p>
|
||||
<Button
|
||||
className="mt-4"
|
||||
size="sm"
|
||||
onClick={() => void handleOpenFolder()}
|
||||
disabled={loading}
|
||||
>
|
||||
<FolderPlus className="mr-2 h-4 w-4" />
|
||||
Open Folder
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{availableProjects.map((project) => {
|
||||
const active = activeProject?.id === project.id;
|
||||
return (
|
||||
<div
|
||||
key={project.id}
|
||||
className={cn(
|
||||
'flex items-center gap-3 rounded-md border bg-card px-4 py-3',
|
||||
active && 'border-primary/40 bg-primary/5',
|
||||
)}
|
||||
>
|
||||
<div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-md bg-muted">
|
||||
<FolderOpen className="h-5 w-5 text-muted-foreground" />
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<span className="truncate text-sm font-medium">{project.name}</span>
|
||||
{active && (
|
||||
<Badge variant="secondary" className="shrink-0">
|
||||
Active
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-0.5 truncate text-xs text-muted-foreground">{project.path}</div>
|
||||
<div className="mt-1 text-xs text-muted-foreground">
|
||||
Last opened {formatOpenedAt(project.lastOpenedAt)}
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant={active ? 'secondary' : 'outline'}
|
||||
size="sm"
|
||||
disabled={active || loading}
|
||||
onClick={() => void activateVerifiedProject(project)}
|
||||
>
|
||||
{active ? <Check className="mr-2 h-4 w-4" /> : null}
|
||||
{active ? 'Active' : 'Use'}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
export default Projects;
|
||||
Reference in New Issue
Block a user