Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 44 additions & 38 deletions app/components/FolderTree.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@ import { ChevronRight, ChevronDown, Folder } from "lucide-react";
import { SolidStorage } from "../lib/hooks/useSolidStorages";
import { FolderTreeChild, folderUrlsEqual, ensureTrailingSlash, getAuthenticatedSession, fetchContainerListing, foldersFromListing } from "../lib/helpers";
import { getContainerListing, loadContainerListing, subscribeContainerCache, getContainerCacheVersion } from "../lib/cache";
import {
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
SidebarMenuSub,
SidebarMenuSubItem,
SidebarMenuSkeleton,
} from "@/components/ui/sidebar";

interface FolderTreeProps {
storages: SolidStorage[];
Expand All @@ -30,6 +38,8 @@ export default function FolderTree({
// Children come from the shared cache, not a local copy.
// cacheVersion makes this recompute when listings are written or invalidated.
const childrenByUrl = useMemo(() => {
// cacheVersion is read so listings recompute after cache writes/invalidation.
void cacheVersion;
const next: Record<string, FolderTreeChild[]> = {};
for (const url of expandedUrls) {
const cached = getContainerListing(url);
Expand Down Expand Up @@ -124,7 +134,7 @@ export default function FolderTree({
}, [expandedUrls, loadChildren]);

// Render one folder row and its nested children when expanded.
const renderNode = useCallback((node: FolderTreeChild, depth: number) => {
const renderNode = useCallback((node: FolderTreeChild) => {
const nodeUrl = ensureTrailingSlash(node.url);
const isExpanded = expandedUrls.has(nodeUrl);
const isLoading = loadingUrls.has(nodeUrl);
Expand All @@ -133,16 +143,12 @@ export default function FolderTree({
const isCurrent = normalizedCurrentFolderUrl != null && folderUrlsEqual(normalizedCurrentFolderUrl, nodeUrl);

return (
<li key={nodeUrl}>
<div
className={`group flex items-center gap-1 rounded-md px-2 py-1 text-sm ${isCurrent ? "bg-accent text-foreground font-medium" : "text-foreground hover:bg-muted"
}`}
style={{ paddingLeft: `${depth * 12 + 8}px` }}
>
<SidebarMenuItem key={nodeUrl}>
<div className="flex w-full items-center gap-0.5">
<button
type="button"
onClick={() => void toggleExpand(nodeUrl)}
className="rounded p-0.5 hover:bg-muted"
className="flex size-7 shrink-0 items-center justify-center rounded-md text-sidebar-foreground hover:bg-sidebar-accent"
aria-label={isExpanded ? `Collapse ${node.name}` : `Expand ${node.name}`}
aria-expanded={isExpanded}
>
Expand All @@ -153,44 +159,41 @@ export default function FolderTree({
)}
</button>

<button
type="button"
<SidebarMenuButton
isActive={isCurrent}
className="flex-1"
onClick={() => onNavigate(nodeUrl)}
className="flex min-w-0 flex-1 items-center gap-2 text-left"
title={node.name}
aria-current={isCurrent ? "page" : undefined}
>
<Folder className="h-4 w-4 shrink-0" aria-hidden="true" />
<span className="truncate">{node.name}</span>
</button>
<Folder aria-hidden="true" />
<span>{node.name}</span>
</SidebarMenuButton>
</div>

{isExpanded && (
<ul className="space-y-0.5">
{isExpanded && (isLoading || hasError || children.length > 0) && (
<SidebarMenuSub>
{isLoading && (
<li
className="px-2 py-1 text-xs text-muted-foreground"
style={{ paddingLeft: `${(depth + 1) * 12 + 8}px` }}
aria-live="polite"
>
Loading...
</li>
<SidebarMenuSubItem>
<SidebarMenuSkeleton showIcon />
</SidebarMenuSubItem>
)}

{!isLoading && hasError && (
<li
className="px-2 py-1 text-xs text-destructive"
style={{ paddingLeft: `${(depth + 1) * 12 + 8}px` }}
>
Failed to load folders
</li>
<SidebarMenuSubItem>
<p className="px-2 py-1 text-xs text-destructive">
Failed to load folders
</p>
</SidebarMenuSubItem>
)}

{!isLoading && !hasError && children.map((child) => renderNode(child, depth + 1))}
</ul>
{!isLoading &&
!hasError &&
children.map((child) => renderNode(child))}
</SidebarMenuSub>
)}
</li>
)
</SidebarMenuItem>
);
}, [
childrenByUrl,
errorByUrl,
Expand All @@ -212,12 +215,15 @@ export default function FolderTree({
);

if (rootNodes.length === 0) {
return <p className="px-3 py-2 text-sm text-muted-foreground">No storages found</p>
return (
<p className="px-3 py-2 text-sm text-muted-foreground">
No storages found
</p>
);
}

return (
<ul className="space-y-0.5" aria-label="My Storages">
{rootNodes.map((node) => renderNode(node, 0))}
</ul>
<SidebarMenu aria-label="My Storages">
{rootNodes.map((node) => renderNode(node))}
</SidebarMenu>
);
}
47 changes: 7 additions & 40 deletions app/components/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,49 +1,16 @@
"use client";

import Image from "next/image";
import ProfileIcon from "./ProfileIcon";
import ThemeToggle from "./ThemeToggle";
import { Menu } from "lucide-react";
import { SidebarTrigger } from "@/components/ui/sidebar";

interface HeaderProps {
onMenuClick?: () => void;
sidebarOpen?: boolean;
}

export default function Header({ onMenuClick }: HeaderProps) {
export default function Header() {
return (
<header className="sticky top-0 z-10 border-b border-border bg-background">
<div className="flex h-14 items-center">
{/* Desktop: center logo over the w-64 sidebar column */}
<div className="hidden w-64 shrink-0 items-center justify-center lg:flex">
<Image
src="/file-manager-logo.svg"
alt="Solid File Manager"
width={48}
height={48}
className="h-12 w-12"
priority
/>
</div>

{/* Mobile: menu only — logo lives in the sidebar Sheet */}
{onMenuClick && (
<div className="flex items-center px-2 lg:hidden">
<button
type="button"
onClick={onMenuClick}
className="flex h-9 w-9 cursor-pointer items-center justify-center rounded-md text-muted-foreground hover:bg-muted focus:outline-none focus:ring-2 focus:ring-ring"
aria-label="Toggle menu"
>
<Menu className="h-5 w-5" />
</button>
</div>
)}

<div className="ml-auto flex items-center gap-1 px-2 sm:gap-2 sm:px-4">
<ThemeToggle />
<ProfileIcon />
</div>
<header className="sticky top-0 z-10 flex h-14 items-center gap-2 border-b border-border bg-background px-2 sm:px-4">
<SidebarTrigger className="lg:hidden" />
<div className="ml-auto flex items-center gap-1 sm:gap-2">
<ThemeToggle />
<ProfileIcon />
</div>
</header>
);
Expand Down
139 changes: 55 additions & 84 deletions app/components/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,17 @@ import GitHubLinks from "./shared/GitHubLinks";
import FolderTree from "./FolderTree";
import { SolidStorage } from "../lib/hooks/useSolidStorages";
import {
Sheet,
SheetContent,
SheetTitle,
} from "@/components/ui/sheet";
Sidebar as SidebarPrimitive,
SidebarContent,
SidebarFooter,
SidebarHeader,
SidebarGroup,
SidebarGroupContent,
SidebarGroupLabel,
useSidebar,
} from "@/components/ui/sidebar";

interface SidebarProps {
isOpen?: boolean;
onClose?: () => void;
interface AppSidebarProps {
currentContainerUrl?: string | null;
storages?: SolidStorage[];
onFolderNavigate?: (folderUrl: string) => void;
Expand All @@ -22,93 +25,61 @@ interface SidebarProps {
onFolderUploadClick?: () => void;
}

function SidebarNav({
export default function AppSidebar({
currentContainerUrl,
storages,
onFolderNavigate,
onNewFolderClick,
onFileUploadClick,
onFolderUploadClick,
}: Omit<SidebarProps, "isOpen" | "onClose">) {
return (
<nav className="flex h-full flex-col" aria-label="Navigation">
<NewMenuButton
currentContainerUrl={currentContainerUrl || null}
onNewFolderClick={onNewFolderClick}
onFileUploadClick={onFileUploadClick}
onFolderUploadClick={onFolderUploadClick}
/>

<div className="mt-2 flex-1 overflow-y-auto">
<p className="px-3 py-1 text-xs font-medium text-muted-foreground">
My Storages
</p>
{storages && onFolderNavigate ? (
<FolderTree
storages={storages}
currentFolderUrl={currentContainerUrl}
onNavigate={onFolderNavigate}
/>
) : null}
</div>

<div className="mt-auto border-t border-border pt-4">
<GitHubLinks layout="vertical" />
</div>
</nav>
);
}
}: AppSidebarProps) {
const { isMobile, setOpenMobile } = useSidebar();

export default function Sidebar({
isOpen = false,
onClose,
currentContainerUrl,
storages,
onFolderNavigate,
onNewFolderClick,
onFileUploadClick,
onFolderUploadClick,
}: SidebarProps) {
const navProps = {
currentContainerUrl,
storages,
onFolderNavigate,
onNewFolderClick,
onFileUploadClick,
onFolderUploadClick,
const handleNavigate = (folderUrl: string) => {
onFolderNavigate?.(folderUrl);
if (isMobile) {
setOpenMobile(false);
}
};

return (
<>
{/* Desktop: always visible */}
<aside className="hidden h-full w-64 shrink-0 flex-col border-r border-sidebar-border bg-sidebar p-2 text-sidebar-foreground lg:flex">
<SidebarNav {...navProps} />
</aside>
<SidebarPrimitive collapsible="offcanvas">
<SidebarHeader className="items-center py-3">
<Image
src="/file-manager-logo.svg"
alt="Solid File Manager"
width={48}
height={48}
className="h-12 w-12"
priority
/>
</SidebarHeader>

{/* Mobile: Sheet drawer */}
<Sheet
open={isOpen}
onOpenChange={(open) => {
if (!open) onClose?.();
}}
>
<SheetContent
side="left"
className="w-64 gap-0 bg-sidebar p-2 text-sidebar-foreground sm:max-w-64"
>
<SheetTitle className="sr-only">Navigation</SheetTitle>
<div className="flex items-center justify-center py-3">
<Image
src="/file-manager-logo.svg"
alt="Solid File Manager"
width={48}
height={48}
className="h-12 w-12"
/>
</div>
<SidebarNav {...navProps} />
</SheetContent>
</Sheet>
</>
<SidebarContent className="p-2">
<NewMenuButton
currentContainerUrl={currentContainerUrl || null}
onNewFolderClick={onNewFolderClick}
onFileUploadClick={onFileUploadClick}
onFolderUploadClick={onFolderUploadClick}
/>

<SidebarGroup>
<SidebarGroupLabel>My Storages</SidebarGroupLabel>
<SidebarGroupContent>
{storages && onFolderNavigate ? (
<FolderTree
storages={storages}
currentFolderUrl={currentContainerUrl}
onNavigate={handleNavigate}
/>
) : null}
</SidebarGroupContent>
</SidebarGroup>
</SidebarContent>

<SidebarFooter className="border-t border-sidebar-border">
<GitHubLinks layout="vertical" />
</SidebarFooter>
</SidebarPrimitive>
);
}
Loading
Loading