import React from 'react'; import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { toast } from '@/components/ui'; import { cn } from '@/lib/utils'; import { PROJECT_ICONS, PROJECT_COLORS, PROJECT_COLOR_MAP, getProjectIconImageUrl } from '@/lib/projectMeta'; import { useProjectsStore } from '@/stores/useProjectsStore'; interface ProjectEditDialogProps { open: boolean; onOpenChange: (open: boolean) => void; projectId: string; projectName: string; projectPath: string; initialIcon?: string | null; initialColor?: string | null; initialIconBackground?: string | null; onSave: (data: { label: string; icon: string | null; color: string | null; iconBackground: string | null }) => void; } const HEX_COLOR_PATTERN = /^#(?:[\da-fA-F]{3}|[\da-fA-F]{6})$/; const normalizeIconBackground = (value: string | null): string | null => { if (!value) { return null; } const trimmed = value.trim(); if (!trimmed) { return null; } return HEX_COLOR_PATTERN.test(trimmed) ? trimmed.toLowerCase() : null; }; export const ProjectEditDialog: React.FC = ({ open, onOpenChange, projectId, projectName, projectPath, initialIcon = null, initialColor = null, initialIconBackground = null, onSave, }) => { const uploadProjectIcon = useProjectsStore((state) => state.uploadProjectIcon); const removeProjectIcon = useProjectsStore((state) => state.removeProjectIcon); const discoverProjectIcon = useProjectsStore((state) => state.discoverProjectIcon); const currentIconImage = useProjectsStore((state) => state.projects.find((project) => project.id === projectId)?.iconImage ?? null); const [name, setName] = React.useState(projectName); const [icon, setIcon] = React.useState(initialIcon); const [color, setColor] = React.useState(initialColor); const [iconBackground, setIconBackground] = React.useState(normalizeIconBackground(initialIconBackground)); const [isUploadingIcon, setIsUploadingIcon] = React.useState(false); const [isRemovingCustomIcon, setIsRemovingCustomIcon] = React.useState(false); const [isDiscoveringIcon, setIsDiscoveringIcon] = React.useState(false); const [previewImageFailed, setPreviewImageFailed] = React.useState(false); const fileInputRef = React.useRef(null); React.useEffect(() => { if (open) { setName(projectName); setIcon(initialIcon); setColor(initialColor); setIconBackground(normalizeIconBackground(initialIconBackground)); } }, [open, projectName, initialIcon, initialColor, initialIconBackground]); const handleSave = () => { const trimmed = name.trim(); if (!trimmed) return; onSave({ label: trimmed, icon, color, iconBackground: normalizeIconBackground(iconBackground) }); onOpenChange(false); }; const currentColorVar = color ? (PROJECT_COLOR_MAP[color] ?? null) : null; const hasImageIcon = Boolean(currentIconImage); const hasCustomIcon = currentIconImage?.source === 'custom'; const iconPreviewUrl = hasImageIcon && !previewImageFailed ? getProjectIconImageUrl({ id: projectId, iconImage: currentIconImage ?? null }) : null; React.useEffect(() => { setPreviewImageFailed(false); }, [projectId, currentIconImage?.updatedAt]); const handleUploadIcon = React.useCallback(async (file: File | null) => { if (!projectId || !file || isUploadingIcon) { return; } setIsUploadingIcon(true); void uploadProjectIcon(projectId, file) .then((result) => { if (!result.ok) { toast.error(result.error || 'Failed to upload project icon'); return; } toast.success('Project icon updated'); }) .finally(() => { setIsUploadingIcon(false); }); }, [isUploadingIcon, projectId, uploadProjectIcon]); const handleRemoveCustomIcon = React.useCallback(async () => { if (!projectId || !hasCustomIcon || isRemovingCustomIcon) { return; } setIsRemovingCustomIcon(true); void removeProjectIcon(projectId) .then((result) => { if (!result.ok) { toast.error(result.error || 'Failed to remove project icon'); return; } toast.success('Custom project icon removed'); }) .finally(() => { setIsRemovingCustomIcon(false); }); }, [hasCustomIcon, isRemovingCustomIcon, projectId, removeProjectIcon]); const handleDiscoverIcon = React.useCallback(async () => { if (!projectId || isDiscoveringIcon) { return; } setIsDiscoveringIcon(true); void discoverProjectIcon(projectId) .then((result) => { if (!result.ok) { toast.error(result.error || 'Failed to discover project icon'); return; } if (result.skipped) { toast.success('Custom icon already set for this project'); return; } toast.success('Project icon discovered'); }) .finally(() => { setIsDiscoveringIcon(false); }); }, [discoverProjectIcon, isDiscoveringIcon, projectId]); return ( Edit project
{/* Name */}
setName(e.target.value)} placeholder="Project name" onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); handleSave(); } }} autoFocus />

{projectPath}

{/* Color */}
{/* No color option */} {PROJECT_COLORS.map((c) => (
{/* Icon */}
{ const file = event.target.files?.[0] ?? null; void handleUploadIcon(file); event.currentTarget.value = ''; }} />
{/* No icon option */} {PROJECT_ICONS.map((i) => { const IconComponent = i.Icon; return ( ); })}
{hasImageIcon && iconPreviewUrl && (
Preview setPreviewImageFailed(true)} />
)}
{!hasCustomIcon && ( <> )} {hasCustomIcon && ( )}
{hasImageIcon && (
setIconBackground(event.target.value)} className="h-8 w-10 cursor-pointer rounded border border-border bg-transparent p-1" aria-label="Project icon background color" /> setIconBackground(event.target.value)} placeholder="#000000" className="h-8 w-[8.5rem]" />
)}
); };