import React, { useState, useRef, useEffect } from 'react'; import { useAppStore, ProjectData, PostData, MediaData } from '../../store'; import { loadTabsForProject, saveTabsForProject } from '../../utils'; import { showToast } from '../Toast'; import { useI18n } from '../../i18n'; import './ProjectSelector.css'; export const ProjectSelector: React.FC = () => { const { t } = useI18n(); const { projects, activeProject, setProjects, setActiveProject, setPosts, setMedia, setSelectedPost, setSelectedMedia, removeProject, getTabState, restoreTabState, clearTabs } = useAppStore(); const [isOpen, setIsOpen] = useState(false); const [showCreateModal, setShowCreateModal] = useState(false); const [showDeleteModal, setShowDeleteModal] = useState(false); const [projectToDelete, setProjectToDelete] = useState(null); const [deleteConfirmText, setDeleteConfirmText] = useState(''); const [newProjectName, setNewProjectName] = useState(''); const [newProjectDescription, setNewProjectDescription] = useState(''); const [newProjectDataPath, setNewProjectDataPath] = useState(null); const dropdownRef = useRef(null); // Load projects on mount (active project is loaded by App.tsx) useEffect(() => { const loadProjects = async () => { try { const allProjects = await window.electronAPI?.projects.getAll(); if (allProjects) { setProjects(allProjects as ProjectData[]); } } catch (error) { console.error('Failed to load projects:', error); } }; loadProjects(); }, [setProjects]); // Close dropdown when clicking outside useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { setIsOpen(false); } }; document.addEventListener('mousedown', handleClickOutside); return () => document.removeEventListener('mousedown', handleClickOutside); }, []); const handleSwitchProject = async (project: ProjectData) => { if (project.id === activeProject?.id) { setIsOpen(false); return; } try { // Save current project's tab state before switching if (activeProject) { const currentTabState = getTabState(); saveTabsForProject(activeProject.id, currentTabState); } // Clear tabs for the transition clearTabs(); const updatedProject = await window.electronAPI?.projects.setActive(project.id); if (updatedProject) { setActiveProject(updatedProject as ProjectData); // Clear current selection and reload data setSelectedPost(null); setSelectedMedia(null); // Reload posts and media for new project const [postsResult, mediaResult] = await Promise.all([ window.electronAPI?.posts.getAll({ limit: 500, offset: 0 }), window.electronAPI?.media.getAll(), ]); // posts.getAll returns { items, hasMore, total } if (postsResult) { const { items, hasMore, total } = postsResult as { items: PostData[]; hasMore: boolean; total: number }; setPosts(items, hasMore, total); } if (mediaResult) setMedia(mediaResult as MediaData[]); // Restore tabs for the new project const savedTabState = loadTabsForProject(project.id); if (savedTabState) { restoreTabState(savedTabState); } showToast.success(t('projectSelector.toast.switched', { name: project.name })); } } catch (error) { console.error('Failed to switch project:', error); showToast.error(t('projectSelector.toast.switchFailed')); } setIsOpen(false); }; const handleCreateProject = async (e: React.FormEvent) => { e.preventDefault(); if (!newProjectName.trim()) return; try { const newProject = await window.electronAPI?.projects.create({ name: newProjectName.trim(), description: newProjectDescription.trim() || undefined, dataPath: newProjectDataPath || undefined, }); if (newProject) { setProjects([...projects, newProject as ProjectData]); showToast.success(t('projectSelector.toast.created', { name: newProjectName })); setNewProjectName(''); setNewProjectDescription(''); setNewProjectDataPath(null); setShowCreateModal(false); // Optionally switch to the new project await handleSwitchProject(newProject as ProjectData); } } catch (error) { console.error('Failed to create project:', error); showToast.error(t('projectSelector.toast.createFailed')); } }; const handleSelectFolder = async () => { try { const selectedPath = await window.electronAPI?.app.selectFolder(t('projectSelector.selectProjectLocation')); if (selectedPath) { setNewProjectDataPath(selectedPath); // Check if the folder has existing project metadata const existingMetadata = await window.electronAPI?.app.readProjectMetadata(selectedPath); if (existingMetadata) { // Pre-populate form fields from existing project.json (overwrite if found) if (existingMetadata.name) { setNewProjectName(existingMetadata.name); } if (existingMetadata.description) { setNewProjectDescription(existingMetadata.description); } showToast.info(t('projectSelector.toast.existingSettingsFound')); } } } catch (error) { console.error('Failed to select folder:', error); showToast.error(t('projectSelector.toast.selectFolderFailed')); } }; const handleClearFolder = () => { setNewProjectDataPath(null); }; const handleCloseCreateModal = () => { setNewProjectName(''); setNewProjectDescription(''); setNewProjectDataPath(null); setShowCreateModal(false); }; const openDeleteModal = (e: React.MouseEvent, project: ProjectData) => { e.stopPropagation(); setProjectToDelete(project); setDeleteConfirmText(''); setShowDeleteModal(true); setIsOpen(false); }; const handleDeleteProject = async (e: React.FormEvent) => { e.preventDefault(); if (!projectToDelete || deleteConfirmText !== projectToDelete.name) return; try { const success = await window.electronAPI?.projects.deleteWithData(projectToDelete.id); if (success) { removeProject(projectToDelete.id); showToast.success(t('projectSelector.toast.deletedWithData', { name: projectToDelete.name })); setShowDeleteModal(false); setProjectToDelete(null); setDeleteConfirmText(''); } else { showToast.error(t('projectSelector.toast.deleteFailed')); } } catch (error) { console.error('Failed to delete project:', error); showToast.error(t('projectSelector.toast.deleteFailed')); } }; const canDeleteProject = (project: ProjectData) => { // Cannot delete: default project, or the currently active project return project.id !== 'default' && project.id !== activeProject?.id; }; return (
{isOpen && (
{t('projectSelector.projectsHeader')}
{projects.map(project => (
{canDeleteProject(project) && ( )}
))} {projects.length === 0 && (
{t('projectSelector.noProjectsYet')}
)}
)} {showCreateModal && (
e.stopPropagation()}>

{t('projectSelector.createNewProject')}

setNewProjectName(e.target.value)} placeholder={t('projectSelector.projectNamePlaceholder')} autoFocus />