diff --git a/src/components/refactored/sidebar/data/workspacesApi.ts b/src/components/refactored/sidebar/data/workspacesApi.ts new file mode 100644 index 00000000..d7bb56e8 --- /dev/null +++ b/src/components/refactored/sidebar/data/workspacesApi.ts @@ -0,0 +1,23 @@ +import { authenticatedFetch } from '@/utils/api'; +import type { Project } from '@/types/app'; + +/** + * Data Extractor layer + * Handles fetching workspaces from the API and formatting them. + */ +export const fetchWorkspaces = async (): Promise => { + try { + const response = await authenticatedFetch('/api/get-workspaces'); + if (!response.ok) { + throw new Error(`Failed to fetch workspaces: ${response.statusText}`); + } + const data = await response.json(); + + // Normalize response formats depending on the actual backend implementation + return data.projects || data.workspaces || data || []; + } catch (error) { + console.error('Error fetching workspaces:', error); + // Return empty array to gracefully handle failure + return []; + } +}; diff --git a/src/components/refactored/sidebar/hooks/useSidebarModals.ts b/src/components/refactored/sidebar/hooks/useSidebarModals.ts new file mode 100644 index 00000000..7d70b291 --- /dev/null +++ b/src/components/refactored/sidebar/hooks/useSidebarModals.ts @@ -0,0 +1,18 @@ +import { useState } from 'react'; + +/** + * Hook layer (The Manager) + * Manages the open/close states of various sidebar modals. + */ +export const useSidebarModals = () => { + const [showNewProject, setShowNewProject] = useState(false); + + const openNewProject = () => setShowNewProject(true); + const closeNewProject = () => setShowNewProject(false); + + return { + showNewProject, + openNewProject, + closeNewProject, + }; +}; diff --git a/src/components/refactored/sidebar/hooks/useSidebarSettings.ts b/src/components/refactored/sidebar/hooks/useSidebarSettings.ts new file mode 100644 index 00000000..5e5d1fe0 --- /dev/null +++ b/src/components/refactored/sidebar/hooks/useSidebarSettings.ts @@ -0,0 +1,18 @@ +import { useState } from 'react'; + +/** + * Hook layer (The Manager) + * Manages the layout states for the sidebar, such as collapse/open. + */ +export const useSidebarSettings = () => { + const [isCollapsed, setIsCollapsed] = useState(false); + + const toggleCollapse = () => setIsCollapsed((prev) => !prev); + const setCollapsed = (value: boolean) => setIsCollapsed(value); + + return { + isCollapsed, + toggleCollapse, + setCollapsed, + }; +}; diff --git a/src/components/refactored/sidebar/hooks/useWorkspaces.ts b/src/components/refactored/sidebar/hooks/useWorkspaces.ts new file mode 100644 index 00000000..d6983900 --- /dev/null +++ b/src/components/refactored/sidebar/hooks/useWorkspaces.ts @@ -0,0 +1,33 @@ +import { useState, useEffect, useCallback } from 'react'; +import { fetchWorkspaces } from '../data/workspacesApi'; +import type { Project } from '@/types/app'; + +/** + * Hook layer (The Manager) + * Manages fetching workspaces and loading states. + */ +export const useWorkspaces = () => { + const [workspaces, setWorkspaces] = useState([]); + const [isRefreshing, setIsRefreshing] = useState(false); + + const refreshWorkspaces = useCallback(async () => { + setIsRefreshing(true); + try { + const data = await fetchWorkspaces(); + setWorkspaces(data); + } finally { + setIsRefreshing(false); + } + }, []); + + // Fetch on mount + useEffect(() => { + refreshWorkspaces(); + }, [refreshWorkspaces]); + + return { + workspaces, + isRefreshing, + refreshWorkspaces, + }; +}; diff --git a/src/components/refactored/sidebar/utils/search.ts b/src/components/refactored/sidebar/utils/search.ts new file mode 100644 index 00000000..44c13553 --- /dev/null +++ b/src/components/refactored/sidebar/utils/search.ts @@ -0,0 +1,16 @@ +import type { Project } from '@/types/app'; + +/** + * Filters workspaces/projects by matching the search string against + * both `displayName` and `name` (case-insensitive substring match). + */ +export const filterWorkspacesByName = (workspaces: Project[], filter: string): Project[] => { + const normalized = filter.trim().toLowerCase(); + if (!normalized) return workspaces; + + return workspaces.filter((project) => { + const displayName = (project.displayName || project.name).toLowerCase(); + const projectName = project.name.toLowerCase(); + return displayName.includes(normalized) || projectName.includes(normalized); + }); +}; diff --git a/src/components/refactored/sidebar/view/Sidebar.tsx b/src/components/refactored/sidebar/view/Sidebar.tsx index 74e197d7..1dc71cfb 100644 --- a/src/components/refactored/sidebar/view/Sidebar.tsx +++ b/src/components/refactored/sidebar/view/Sidebar.tsx @@ -1,59 +1,75 @@ -import { useState } from 'react'; import { PanelRightOpen } from 'lucide-react'; -import { Button } from '@/shared/view/ui'; +import { useSidebarSettings } from '../hooks/useSidebarSettings'; +import { useWorkspaces } from '../hooks/useWorkspaces'; +import { useSidebarModals } from '../hooks/useSidebarModals'; +import SidebarHeader from './SidebarHeader'; import { cn } from '@/lib/utils'; -import SidebarHeader from '@/components/refactored/sidebar/view/SidebarHeader.js'; - +import { Button } from '@/shared/view/ui'; +import ProjectCreationWizard from '@/components/project-creation-wizard'; export function Sidebar() { - const [isCollapsed, setIsCollapsed] = useState(false); + const { isCollapsed, toggleCollapse, setCollapsed } = useSidebarSettings(); + const { workspaces, isRefreshing, refreshWorkspaces } = useWorkspaces(); + const { showNewProject, openNewProject, closeNewProject } = useSidebarModals(); return ( <> - {/* Mobile Backdrop Overlay - allows tapping outside to close */} - {!isCollapsed && ( -
setIsCollapsed(true)} - /> - )} - - - - {/* Collapsed view handle - Only show on desktop since mobile hides it completely behind a toggle usually, but let's keep it consistent or standard. - Actually, on mobile, if it's completely hidden, we need a way to open it from the main content. For now we show the small bar if it's flex, - but since we made it fixed, let's keep the small bar fixed too. */} - {isCollapsed && ( - + + {/* Collapsed view handle - Only show on desktop since mobile hides it completely behind a toggle usually, but let's keep it consistent or standard. */} + {isCollapsed && ( + + + )} + + + {/* MODALS */} + {showNewProject && ( + )} ); diff --git a/src/components/refactored/sidebar/view/SidebarHeader.tsx b/src/components/refactored/sidebar/view/SidebarHeader.tsx index 99f65fef..96cf68c1 100644 --- a/src/components/refactored/sidebar/view/SidebarHeader.tsx +++ b/src/components/refactored/sidebar/view/SidebarHeader.tsx @@ -9,18 +9,21 @@ import { IS_PLATFORM } from '@/constants/config'; type SidebarHeaderProps = { isCollapsed: boolean; onToggleCollapse: () => void; + isRefreshing: boolean; + onRefresh: () => void; + onNewProject: () => void; }; -export default function SidebarHeader({ isCollapsed, onToggleCollapse }: SidebarHeaderProps) { - // UI States declared here to avoid prop drilling as per instructions +export default function SidebarHeader({ + isCollapsed, + onToggleCollapse, + isRefreshing, + onRefresh, + onNewProject +}: SidebarHeaderProps) { + // UI States for search const [searchMode, setSearchMode] = useState('projects'); const [searchFilter, setSearchFilter] = useState(''); - const [isRefreshing, setIsRefreshing] = useState(false); - - const handleRefresh = () => { - setIsRefreshing(true); - setTimeout(() => setIsRefreshing(false), 1000); - }; const LogoBlock = () => (
@@ -61,16 +64,17 @@ export default function SidebarHeader({ isCollapsed, onToggleCollapse }: Sidebar variant="ghost" size="sm" className="h-7 w-7 rounded-lg p-0 text-muted-foreground hover:bg-accent/80 hover:text-foreground" - onClick={handleRefresh} + onClick={onRefresh} disabled={isRefreshing} title="Refresh" > - + @@ -130,4 +135,4 @@ export default function SidebarHeader({ isCollapsed, onToggleCollapse }: Sidebar
); -} +} \ No newline at end of file