refactor: setup sidebar header

This commit is contained in:
Haileyesus
2026-03-27 16:44:56 +03:00
parent 186dbcde63
commit b57fec9d66
7 changed files with 183 additions and 54 deletions

View File

@@ -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<Project[]> => {
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 [];
}
};

View File

@@ -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,
};
};

View File

@@ -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,
};
};

View File

@@ -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<Project[]>([]);
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,
};
};

View File

@@ -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);
});
};

View File

@@ -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 && (
<div
className="fixed inset-0 z-40 bg-black/20 backdrop-blur-sm md:hidden"
onClick={() => setIsCollapsed(true)}
/>
)}
<aside
className={cn(
"flex flex-col bg-background/80 backdrop-blur-sm transition-all duration-300 border-r border-border h-full",
"fixed inset-y-0 left-0 z-50 md:relative md:z-0", // Make it fixed drawer on mobile, relative on desktop
isCollapsed
? "-translate-x-full md:translate-x-0 md:w-0 md:opacity-0 md:overflow-hidden md:border-none" // Hide fully on mobile if collapsed
: "translate-x-0 w-[85vw] sm:w-80 md:w-72 opacity-100"
)}
>
<SidebarHeader
isCollapsed={isCollapsed}
onToggleCollapse={() => setIsCollapsed(true)}
/>
{/* Placeholder for the rest of the sidebar content */}
<>
{/* Mobile Backdrop Overlay - allows tapping outside to close */}
{!isCollapsed && (
<div className="flex-1 overflow-y-auto overscroll-contain">
{/* Future list component will go here */}
</div>
<div
className="fixed inset-0 z-40 bg-black/20 backdrop-blur-sm md:hidden"
onClick={() => setCollapsed(true)}
/>
)}
</aside>
{/* 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 && (
<aside className="fixed inset-y-0 left-0 z-40 flex h-full flex-col items-center border-r border-border bg-background/80 px-2 py-4 md:relative">
<Button
<aside
className={cn(
"flex flex-col bg-background/80 backdrop-blur-sm transition-all duration-300 border-r border-border h-full",
"fixed inset-y-0 left-0 z-50 md:relative md:z-0", // Make it fixed drawer on mobile, relative on desktop
isCollapsed
? "-translate-x-full md:translate-x-0 md:w-0 md:opacity-0 md:overflow-hidden md:border-none" // Hide fully on mobile if collapsed
: "translate-x-0 w-[85vw] sm:w-80 md:w-72 opacity-100"
)}
>
<SidebarHeader
isCollapsed={isCollapsed}
onToggleCollapse={toggleCollapse}
isRefreshing={isRefreshing}
onRefresh={refreshWorkspaces}
onNewProject={openNewProject}
/>
{/* Placeholder for the rest of the sidebar content */}
{!isCollapsed && (
<div className="flex-1 overflow-y-auto overscroll-contain">
{/* Future list component will go here */}
{/* Can pass workspaces to the future list component as props */}
</div>
)}
</aside>
{/* 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 && (
<aside className="fixed inset-y-0 left-0 z-40 flex h-full flex-col items-center border-r border-border bg-background/80 px-2 py-4 md:relative">
<Button
variant="ghost"
size="sm"
className="h-8 w-8 rounded-lg p-0 text-muted-foreground hover:bg-accent/80 hover:text-foreground"
onClick={() => setIsCollapsed(false)}
onClick={() => setCollapsed(false)}
title="Show Sidebar"
>
<PanelRightOpen className="h-4 w-4" />
</Button>
</aside>
</aside>
)}
</>
{/* MODALS */}
{showNewProject && (
<ProjectCreationWizard
onClose={closeNewProject}
onProjectCreated={refreshWorkspaces}
/>
)}
</>
);

View File

@@ -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<SearchMode>('projects');
const [searchFilter, setSearchFilter] = useState('');
const [isRefreshing, setIsRefreshing] = useState(false);
const handleRefresh = () => {
setIsRefreshing(true);
setTimeout(() => setIsRefreshing(false), 1000);
};
const LogoBlock = () => (
<div className="flex min-w-0 items-center gap-2.5">
@@ -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"
>
<RefreshCw className={cn("h-3.5 w-3.5", isRefreshing && "animate-spin")} />
<RefreshCw className={cn("h-3.5 w-3.5 transition-opacity", isRefreshing && "animate-spin opacity-50")} />
</Button>
<Button
variant="ghost"
size="sm"
className="h-7 w-7 rounded-lg p-0 text-muted-foreground hover:bg-accent/80 hover:text-foreground"
onClick={onNewProject}
title="New Project"
>
<Plus className="h-3.5 w-3.5" />
@@ -104,14 +108,15 @@ export default function SidebarHeader({ isCollapsed, onToggleCollapse }: Sidebar
<LogoWithLink />
<div className="flex flex-shrink-0 gap-1.5">
<button
className="flex h-8 w-8 items-center justify-center rounded-lg bg-muted/50 transition-all active:scale-95"
onClick={handleRefresh}
className="flex h-8 w-8 items-center justify-center rounded-lg bg-muted/50 transition-all active:scale-95 disabled:opacity-70"
onClick={onRefresh}
disabled={isRefreshing}
>
<RefreshCw className={cn("h-4 w-4 text-muted-foreground", isRefreshing && "animate-spin")} />
<RefreshCw className={cn("h-4 w-4 text-muted-foreground transition-opacity", isRefreshing && "animate-spin opacity-50")} />
</button>
<button
className="flex h-8 w-8 items-center justify-center rounded-lg bg-primary/90 text-primary-foreground transition-all active:scale-95"
onClick={onNewProject}
>
<FolderPlus className="h-4 w-4" />
</button>
@@ -130,4 +135,4 @@ export default function SidebarHeader({ isCollapsed, onToggleCollapse }: Sidebar
<div className="nav-divider md:hidden" />
</div>
);
}
}