mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-05-16 01:12:46 +00:00
refactor: setup sidebar header
This commit is contained in:
23
src/components/refactored/sidebar/data/workspacesApi.ts
Normal file
23
src/components/refactored/sidebar/data/workspacesApi.ts
Normal 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 [];
|
||||
}
|
||||
};
|
||||
18
src/components/refactored/sidebar/hooks/useSidebarModals.ts
Normal file
18
src/components/refactored/sidebar/hooks/useSidebarModals.ts
Normal 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,
|
||||
};
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
33
src/components/refactored/sidebar/hooks/useWorkspaces.ts
Normal file
33
src/components/refactored/sidebar/hooks/useWorkspaces.ts
Normal 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,
|
||||
};
|
||||
};
|
||||
16
src/components/refactored/sidebar/utils/search.ts
Normal file
16
src/components/refactored/sidebar/utils/search.ts
Normal 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);
|
||||
});
|
||||
};
|
||||
@@ -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}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user