Makelore 2.0 initial clean snapshot

This commit is contained in:
inman
2026-07-29 17:22:35 +08:00
commit b8ca3f8eea
694 changed files with 139782 additions and 0 deletions

View 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;