Files
vibe-kanban/frontend/src/components/projects/ProjectList.tsx
Solomon a282bbdae4 Real-time sync for Projects (#1512)
* Real-time sync for Projects

* Do not create project in a transaction

Update hooks trigger before the transaction is commited, which causes insert events to be dismissed because the row is isn't found
2025-12-17 18:25:34 +00:00

109 lines
3.7 KiB
TypeScript

import { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { Project } from 'shared/types';
import { ProjectFormDialog } from '@/components/dialogs/projects/ProjectFormDialog';
import { AlertCircle, Loader2, Plus } from 'lucide-react';
import ProjectCard from '@/components/projects/ProjectCard.tsx';
import { useKeyCreate, Scope } from '@/keyboard';
import { useProjects } from '@/hooks/useProjects';
export function ProjectList() {
const navigate = useNavigate();
const { t } = useTranslation('projects');
const { projects, isLoading, error: projectsError } = useProjects();
const [error, setError] = useState('');
const [focusedProjectId, setFocusedProjectId] = useState<string | null>(null);
const handleCreateProject = async () => {
try {
const result = await ProjectFormDialog.show({});
if (result === 'saved') return;
} catch (error) {
// User cancelled - do nothing
}
};
// Semantic keyboard shortcut for creating new project
useKeyCreate(handleCreateProject, { scope: Scope.PROJECTS });
const handleEditProject = (project: Project) => {
navigate(`/settings/projects?projectId=${project.id}`);
};
// Set initial focus when projects are loaded
useEffect(() => {
if (projects.length === 0) {
setFocusedProjectId(null);
return;
}
if (!focusedProjectId || !projects.some((p) => p.id === focusedProjectId)) {
setFocusedProjectId(projects[0].id);
}
}, [projects, focusedProjectId]);
return (
<div className="space-y-6 p-8 pb-16 md:pb-8 h-full overflow-auto">
<div className="flex justify-between items-center">
<div>
<h1 className="text-3xl font-bold tracking-tight">{t('title')}</h1>
<p className="text-muted-foreground">{t('subtitle')}</p>
</div>
<Button onClick={handleCreateProject}>
<Plus className="mr-2 h-4 w-4" />
{t('createProject')}
</Button>
</div>
{(error || projectsError) && (
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertDescription>
{error || projectsError?.message || t('errors.fetchFailed')}
</AlertDescription>
</Alert>
)}
{isLoading ? (
<div className="flex items-center justify-center py-12">
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
{t('loading')}
</div>
) : projects.length === 0 ? (
<Card>
<CardContent className="py-12 text-center">
<div className="mx-auto flex h-12 w-12 items-center justify-center rounded-lg bg-muted">
<Plus className="h-6 w-6" />
</div>
<h3 className="mt-4 text-lg font-semibold">{t('empty.title')}</h3>
<p className="mt-2 text-sm text-muted-foreground">
{t('empty.description')}
</p>
<Button className="mt-4" onClick={handleCreateProject}>
<Plus className="mr-2 h-4 w-4" />
{t('empty.createFirst')}
</Button>
</CardContent>
</Card>
) : (
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
{projects.map((project) => (
<ProjectCard
key={project.id}
project={project}
isFocused={focusedProjectId === project.id}
setError={setError}
onEdit={handleEditProject}
/>
))}
</div>
)}
</div>
);
}