Repository Public
1'use client';2import { Suspense, useEffect, useRef, useState } from 'react';3import { useRouter, useSearchParams } from 'next/navigation';4import { Group, Panel, Separator } from 'react-resizable-panels';5import { FolderTree } from 'lucide-react';6import { useProjects } from '@/lib/queries';7import { useBreakpoint } from '@/hooks/use-viewport';8import { useCurrentProject } from '@/hooks/use-current';9import { useUi } from '@/stores/ui';10import { Select } from '@/components/ui/select';11import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';12import { EmptyState, Spinner } from '@/components/ui/misc';13import { FilesBrowser } from '@/components/files/files-browser';14import { GitPanel } from '@/components/files/git-panel';15import { EditorPane } from '@/components/editor/editor-pane';16import { useEditor } from '@/components/editor/editor-store';1718function FilesInner() {19 const params = useSearchParams();20 const router = useRouter();21 const bp = useBreakpoint();22 const projects = useProjects();23 const fallback = useCurrentProject();24 const pid = params.get('p');25 const pathParam = params.get('path');26 const project = projects.data?.find((p) => p.id === pid) ?? (pid ? null : fallback);27 const { sizes, setSizes } = useUi();28 const hasTabs = useEditor((s) => s.tabs.length > 0);29 const activePath = useEditor((s) => s.tabs.find((t) => t.key === s.activeKey)?.path ?? null);30 const [mobileEditor, setMobileEditor] = useState(false);31 const opened = useRef<string | null>(null);3233 // `?path=` opens that file once (deep links from the palette, the terminal side panel…).34 useEffect(() => {35 if (!project || !pathParam) return;36 const key = `${project.id}:${pathParam}`;37 if (opened.current === key) return;38 opened.current = key;39 void useEditor.getState().open(project.id, pathParam);40 setMobileEditor(true);41 }, [project, pathParam]);4243 if (projects.isPending) return <div className="flex h-full items-center justify-center"><Spinner /></div>;44 if (!project) return <EmptyState icon={<FolderTree />} title={pid ? 'Project not found' : 'No projects yet'} description="Register a project to browse its files." className="h-full" />;4546 const openFile = (path: string) => {47 void useEditor.getState().open(project.id, path);48 setMobileEditor(true);49 };50 const openDiff = (path: string) => {51 void useEditor.getState().openGitDiff(project.id, path);52 setMobileEditor(true);53 };54 const side = (55 <Tabs defaultValue="files" className={bp === 'mobile' ? 'flex h-full min-w-0 flex-col bg-bg' : 'flex h-full min-w-0 flex-col bg-surface'}>56 <div className="p-2 pointer-coarse:px-4">57 <Select label="Project" value={project.id} onValueChange={(v) => router.replace(`/files/?p=${v}`)} options={(projects.data ?? []).map((p) => ({ value: p.id, label: p.name }))} />58 </div>59 <TabsList>60 <TabsTrigger value="files">Files</TabsTrigger>61 <TabsTrigger value="git">Git</TabsTrigger>62 </TabsList>63 <TabsContent value="files" className="overflow-hidden"><FilesBrowser projectId={project.id} mode={bp === 'mobile' ? 'drill' : 'tree'} wide={bp !== 'mobile'} onOpenFile={openFile} activePath={activePath} /></TabsContent>64 <TabsContent value="git" className="overflow-hidden"><GitPanel projectId={project.id} onOpenDiff={openDiff} /></TabsContent>65 </Tabs>66 );67 if (bp === 'mobile') return mobileEditor && hasTabs ? <EditorPane mobile onBack={() => setMobileEditor(false)} /> : side;68 return (69 <Group id="files" orientation="horizontal" defaultLayout={sizes.files} onLayoutChanged={(l) => setSizes('files', l)} className="h-full">70 <Panel id="tree" defaultSize="28%" minSize="16%" maxSize="55%">{side}</Panel>71 <Separator className="w-px bg-line-2 transition-colors data-[separator=hover]:bg-accent data-[separator=active]:bg-accent focus-visible:bg-accent focus-visible:outline-none" />72 <Panel id="editor" minSize="30%"><EditorPane /></Panel>73 </Group>74 );75}76export default function FilesPage() {77 return <Suspense><FilesInner /></Suspense>;78}79