- Refactored JSX elements to improve readability by collapsing multi-line props and attributes into single lines if their length permits. - Improved consistency in component imports by grouping and consolidating them. - No functional changes, purely restructuring for clarity and maintainability.
186 lines
5.2 KiB
TypeScript
186 lines
5.2 KiB
TypeScript
/**
|
|
* Project Switcher Component
|
|
* Dropdown selector for switching between projects
|
|
* Displays current project and allows quick project navigation
|
|
*/
|
|
|
|
'use client';
|
|
|
|
import { useState, useCallback } from 'react';
|
|
import { useRouter } from '@/lib/i18n/routing';
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from '@/components/ui/select';
|
|
import { Button } from '@/components/ui/button';
|
|
import { FolderKanban, Plus, ChevronDown } from 'lucide-react';
|
|
import { cn } from '@/lib/utils';
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuLabel,
|
|
DropdownMenuSeparator,
|
|
DropdownMenuTrigger,
|
|
} from '@/components/ui/dropdown-menu';
|
|
|
|
interface Project {
|
|
id: string;
|
|
slug: string;
|
|
name: string;
|
|
/** Optional description */
|
|
description?: string;
|
|
}
|
|
|
|
interface ProjectSwitcherProps {
|
|
/** Currently selected project */
|
|
currentProject?: Project;
|
|
/** List of available projects */
|
|
projects: Project[];
|
|
/** Callback when project is changed */
|
|
onProjectChange?: (projectSlug: string) => void;
|
|
/** Additional className */
|
|
className?: string;
|
|
}
|
|
|
|
export function ProjectSwitcher({
|
|
currentProject,
|
|
projects,
|
|
onProjectChange,
|
|
className,
|
|
}: ProjectSwitcherProps) {
|
|
const router = useRouter();
|
|
const [open, setOpen] = useState(false);
|
|
|
|
const handleProjectChange = useCallback(
|
|
(projectSlug: string) => {
|
|
if (onProjectChange) {
|
|
onProjectChange(projectSlug);
|
|
} else {
|
|
// Default behavior: navigate to project dashboard
|
|
router.push(`/projects/${projectSlug}`);
|
|
}
|
|
setOpen(false);
|
|
},
|
|
[onProjectChange, router]
|
|
);
|
|
|
|
const handleCreateProject = useCallback(() => {
|
|
router.push('/projects/new');
|
|
setOpen(false);
|
|
}, [router]);
|
|
|
|
// If no projects, show create button
|
|
if (projects.length === 0) {
|
|
return (
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={handleCreateProject}
|
|
className={cn('gap-2', className)}
|
|
data-testid="create-project-button"
|
|
>
|
|
<Plus className="h-4 w-4" aria-hidden="true" />
|
|
<span>Create Project</span>
|
|
</Button>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<DropdownMenu open={open} onOpenChange={setOpen}>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
className={cn('gap-2 min-w-[160px] justify-between', className)}
|
|
data-testid="project-switcher-trigger"
|
|
aria-label={
|
|
currentProject ? `Switch project, current: ${currentProject.name}` : 'Select project'
|
|
}
|
|
>
|
|
<div className="flex items-center gap-2">
|
|
<FolderKanban className="h-4 w-4" aria-hidden="true" />
|
|
<span className="truncate max-w-[120px]">
|
|
{currentProject?.name ?? 'Select Project'}
|
|
</span>
|
|
</div>
|
|
<ChevronDown className="h-4 w-4 opacity-50" aria-hidden="true" />
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="start" className="w-[200px]" data-testid="project-switcher-menu">
|
|
<DropdownMenuLabel>Projects</DropdownMenuLabel>
|
|
<DropdownMenuSeparator />
|
|
{projects.map((project) => (
|
|
<DropdownMenuItem
|
|
key={project.id}
|
|
onSelect={() => handleProjectChange(project.slug)}
|
|
className="cursor-pointer"
|
|
data-testid={`project-option-${project.slug}`}
|
|
>
|
|
<FolderKanban className="mr-2 h-4 w-4" aria-hidden="true" />
|
|
<span className="truncate">{project.name}</span>
|
|
{currentProject?.id === project.id && (
|
|
<span className="ml-auto text-xs text-muted-foreground">Current</span>
|
|
)}
|
|
</DropdownMenuItem>
|
|
))}
|
|
<DropdownMenuSeparator />
|
|
<DropdownMenuItem
|
|
onSelect={handleCreateProject}
|
|
className="cursor-pointer"
|
|
data-testid="create-project-option"
|
|
>
|
|
<Plus className="mr-2 h-4 w-4" aria-hidden="true" />
|
|
<span>Create New Project</span>
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Alternative version using Select component for simpler use cases
|
|
*/
|
|
interface ProjectSelectProps {
|
|
/** Currently selected project slug */
|
|
value?: string;
|
|
/** List of available projects */
|
|
projects: Project[];
|
|
/** Callback when project is selected */
|
|
onValueChange: (projectSlug: string) => void;
|
|
/** Placeholder text */
|
|
placeholder?: string;
|
|
/** Additional className */
|
|
className?: string;
|
|
}
|
|
|
|
export function ProjectSelect({
|
|
value,
|
|
projects,
|
|
onValueChange,
|
|
placeholder = 'Select a project',
|
|
className,
|
|
}: ProjectSelectProps) {
|
|
return (
|
|
<Select value={value} onValueChange={onValueChange}>
|
|
<SelectTrigger className={cn('w-[200px]', className)} data-testid="project-select">
|
|
<SelectValue placeholder={placeholder} />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{projects.map((project) => (
|
|
<SelectItem
|
|
key={project.id}
|
|
value={project.slug}
|
|
data-testid={`project-select-option-${project.slug}`}
|
|
>
|
|
{project.name}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
);
|
|
}
|