diff --git a/app/globals.css b/app/globals.css index 2310d02..33e95c5 100644 --- a/app/globals.css +++ b/app/globals.css @@ -171,4 +171,13 @@ span { @apply text-muted-foreground; } + h1 { + @apply scroll-m-20 text-4xl tracking-tight text-balance; + } + h2 { + @apply scroll-m-20 text-3xl tracking-tight first:mt-0; + } + p { + @apply leading-7 [&:not(:first-child)]:mt-6 text-muted-foreground; + } } diff --git a/app/projects/[id]/layout.tsx b/app/projects/[id]/layout.tsx index f83b23e..7219e79 100644 --- a/app/projects/[id]/layout.tsx +++ b/app/projects/[id]/layout.tsx @@ -3,7 +3,7 @@ import { notFound } from 'next/navigation'; import ContentWrapper from '@/components/content-wrapper'; import PersistentTerminal from '@/components/persistent-terminal'; -import ProjectSecondarySidebar from '@/components/project-secondary-sidebar'; +import PrimarySidebar from '@/components/primary-sidebar'; import ProjectSidebar from '@/components/project-sidebar'; import { TerminalProvider } from '@/components/terminal-provider'; import { auth } from '@/lib/auth'; @@ -40,24 +40,15 @@ export default async function ProjectLayout({ notFound(); } - // Get all user projects for sidebar - const projects = await prisma.project.findMany({ - where: { - userId: session.user.id, - }, - orderBy: { - createdAt: 'desc', - }, - }); - + return (
{/* Primary Sidebar - VSCode style */} - + {/* Secondary Sidebar - Project Settings */} - {/* Primary Sidebar - VSCode style */} - diff --git a/components/primary-sidebar.tsx b/components/primary-sidebar.tsx new file mode 100644 index 0000000..1d2f9bf --- /dev/null +++ b/components/primary-sidebar.tsx @@ -0,0 +1,173 @@ +'use client'; + +import { useState } from 'react'; +import { useQuery } from '@tanstack/react-query'; +import { Circle, FolderOpen, GitBranch, Home, Plus, Settings } from 'lucide-react'; +import Link from 'next/link'; + +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; +import { cn } from '@/lib/utils'; + +interface Project { + id: string; + name: string; + status: string; + updatedAt: string; +} + +interface PrimarySidebarProps { + currentProjectId: string; + userId: string; +} + +export default function PrimarySidebar({ + currentProjectId, +}: PrimarySidebarProps) { + // Fetch projects list with TanStack Query, polling every 5 seconds + const { data: projects } = useQuery({ + queryKey: ['projects'], + queryFn: async () => { + const response = await fetch('/api/projects'); + if (!response.ok) { + throw new Error('Failed to fetch projects'); + } + return response.json(); + }, + refetchInterval: 5000, // Poll every 5 seconds + staleTime: 4000, // Data is fresh for 4 seconds + retry: 2, + }); + const [isExpanded, setIsExpanded] = useState(false); + + const getStatusColor = (status: string) => { + switch (status) { + case 'READY': + case 'DEPLOYED': + return 'text-green-600 dark:text-green-500'; + case 'INITIALIZING': + case 'DEPLOYING': + return 'text-yellow-600 dark:text-yellow-500'; + case 'ERROR': + return 'text-destructive'; + default: + return 'text-muted-foreground'; + } + }; + + return ( +
setIsExpanded(true)} + onMouseLeave={() => setIsExpanded(false)} + > + + {/* Header */} +
+ + {isExpanded && Projects} +
+ + {/* Navigation Items */} +
+ {/* Home Link */} + + + + + {isExpanded && ( + All Projects + )} + + + {!isExpanded && ( + +

All Projects

+
+ )} +
+ + {/* New Project */} + + + + + {isExpanded && ( + New Project + )} + + + {!isExpanded && ( + +

New Project

+
+ )} +
+ +
+ + {/* Project List */} +
+ {projects?.map((project) => ( + + + +
+ + +
+ {isExpanded && ( + {project.name} + )} + +
+ {!isExpanded && ( + +

{project.name}

+
+ )} +
+ ))} +
+
+ + {/* Settings */} +
+ + + + + {isExpanded && Settings} + + + {!isExpanded && ( + +

Settings

+
+ )} +
+
+ +
+ ); +} diff --git a/components/project-secondary-sidebar.tsx b/components/project-secondary-sidebar.tsx deleted file mode 100644 index f681b27..0000000 --- a/components/project-secondary-sidebar.tsx +++ /dev/null @@ -1,205 +0,0 @@ -'use client'; - -import { useState } from 'react'; -import { Environment, Project, Sandbox } from '@prisma/client'; -import { - ChevronDown, - ChevronLeft, - ChevronRight, - CreditCard, - Database, - Github, - Key, - Package, - Settings, - Shield, - Terminal, -} from 'lucide-react'; -import { usePathname, useRouter } from 'next/navigation'; - -import { useTerminal } from '@/components/terminal-provider'; -import { cn } from '@/lib/utils'; - -interface ProjectSecondarySidebarProps { - project: Project; - sandboxes: Sandbox[]; - envVars: Environment[]; -} - -export default function ProjectSecondarySidebar({ project }: ProjectSecondarySidebarProps) { - const [isCollapsed, setIsCollapsed] = useState(false); - const [isConfigExpanded, setIsConfigExpanded] = useState(true); - const { hideTerminal, isTerminalVisible } = useTerminal(); - const pathname = usePathname(); - const router = useRouter(); - - const handleSectionClick = (e: React.MouseEvent, sectionId: string, href: string) => { - e.preventDefault(); - - if (sectionId === 'terminal') { - // For terminal, navigate without hiding (terminal page will show it) - router.push(href); - } else { - // For other sections, hide terminal and navigate - hideTerminal(); - router.push(href); - } - }; - - const topSections = [ - { - id: 'terminal', - label: 'Web Terminal', - icon: Terminal, - href: `/projects/${project.id}/terminal`, - }, - { id: 'database', label: 'Database', icon: Database, href: `/projects/${project.id}/database` }, - ]; - - const configSections = [ - { - id: 'environment', - label: 'Environment Variables', - icon: Package, - href: `/projects/${project.id}/environment`, - }, - { - id: 'secrets', - label: 'Secret Configuration', - icon: Key, - href: `/projects/${project.id}/secrets`, - }, - { id: 'auth', label: 'Auth Configuration', icon: Shield, href: `/projects/${project.id}/auth` }, - { - id: 'payment', - label: 'Payment Configuration', - icon: CreditCard, - href: `/projects/${project.id}/payment`, - }, - ]; - - const bottomSections = [ - { - id: 'github', - label: 'GitHub Repository', - icon: Github, - href: `/projects/${project.id}/github`, - }, - ]; - - // Check if any config section is active - const isConfigActive = configSections.some((section) => pathname === section.href); - - return ( -
- {/* Header */} -
- {!isCollapsed && ( - Project {project.name} - )} - -
- - {!isCollapsed && ( -
- {/* Top sections */} - {topSections.map((section) => { - const Icon = section.icon; - const isActive = - pathname === section.href || (section.id === 'terminal' && isTerminalVisible); - - return ( - handleSectionClick(e, section.id, section.href)} - className={cn( - 'group w-full flex items-center px-3 py-2 text-sm transition-colors min-h-[32px]', - isActive ? 'bg-sidebar-accent' : 'hover:bg-sidebar-accent' - )} - > - - {section.label} - - ); - })} - - {/* Configuration Group */} -
- - - {isConfigExpanded && ( - - )} -
- - {/* Bottom sections */} - {bottomSections.map((section) => { - const Icon = section.icon; - const isActive = pathname === section.href; - - return ( - handleSectionClick(e, section.id, section.href)} - className={cn( - 'group w-full flex items-center px-3 py-2 text-sm transition-colors min-h-[32px]', - isActive ? 'bg-sidebar-accent' : 'hover:bg-sidebar-accent' - )} - > - - {section.label} - - ); - })} -
- )} -
- ); -} diff --git a/components/project-sidebar.tsx b/components/project-sidebar.tsx index 290da85..34b6937 100644 --- a/components/project-sidebar.tsx +++ b/components/project-sidebar.tsx @@ -1,154 +1,205 @@ 'use client'; import { useState } from 'react'; -import { Project } from '@prisma/client'; -import { Circle, FolderOpen, GitBranch, Home, Plus, Settings } from 'lucide-react'; -import Link from 'next/link'; +import { Environment, Project, Sandbox } from '@prisma/client'; +import { + ChevronDown, + ChevronLeft, + ChevronRight, + CreditCard, + Database, + Github, + Key, + Package, + Settings, + Shield, + Terminal, +} from 'lucide-react'; +import { usePathname, useRouter } from 'next/navigation'; -import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; +import { useTerminal } from '@/components/terminal-provider'; import { cn } from '@/lib/utils'; interface ProjectSidebarProps { - projects: Project[]; - currentProjectId: string; - userId: string; + project: Project; + sandboxes: Sandbox[]; + envVars: Environment[]; } -export default function ProjectSidebar({ - projects, - currentProjectId, -}: ProjectSidebarProps) { - const [isExpanded, setIsExpanded] = useState(false); - - const getStatusColor = (status: string) => { - switch (status) { - case 'READY': - case 'DEPLOYED': - return 'text-green-600 dark:text-green-500'; - case 'INITIALIZING': - case 'DEPLOYING': - return 'text-yellow-600 dark:text-yellow-500'; - case 'ERROR': - return 'text-destructive'; - default: - return 'text-muted-foreground'; +export default function ProjectSidebar({ project }: ProjectSidebarProps) { + const [isCollapsed, setIsCollapsed] = useState(false); + const [isConfigExpanded, setIsConfigExpanded] = useState(true); + const { hideTerminal, isTerminalVisible } = useTerminal(); + const pathname = usePathname(); + const router = useRouter(); + + const handleSectionClick = (e: React.MouseEvent, sectionId: string, href: string) => { + e.preventDefault(); + + if (sectionId === 'terminal') { + // For terminal, navigate without hiding (terminal page will show it) + router.push(href); + } else { + // For other sections, hide terminal and navigate + hideTerminal(); + router.push(href); } }; + const topSections = [ + { + id: 'terminal', + label: 'Web Terminal', + icon: Terminal, + href: `/projects/${project.id}/terminal`, + }, + { id: 'database', label: 'Database', icon: Database, href: `/projects/${project.id}/database` }, + ]; + + const configSections = [ + { + id: 'environment', + label: 'Environment Variables', + icon: Package, + href: `/projects/${project.id}/environment`, + }, + { + id: 'secrets', + label: 'Secret Configuration', + icon: Key, + href: `/projects/${project.id}/secrets`, + }, + { id: 'auth', label: 'Auth Configuration', icon: Shield, href: `/projects/${project.id}/auth` }, + { + id: 'payment', + label: 'Payment Configuration', + icon: CreditCard, + href: `/projects/${project.id}/payment`, + }, + ]; + + const bottomSections = [ + { + id: 'github', + label: 'GitHub Repository', + icon: Github, + href: `/projects/${project.id}/github`, + }, + ]; + + // Check if any config section is active + const isConfigActive = configSections.some((section) => pathname === section.href); + return (
setIsExpanded(true)} - onMouseLeave={() => setIsExpanded(false)} > - - {/* Header */} -
- - {isExpanded && Projects} -
+ {/* Header */} +
+ {!isCollapsed && ( + Project {project.name} + )} + +
- {/* Navigation Items */} -
- {/* Home Link */} - - - - - {isExpanded && ( - All Projects + {!isCollapsed && ( +
+ {/* Top sections */} + {topSections.map((section) => { + const Icon = section.icon; + const isActive = + pathname === section.href || (section.id === 'terminal' && isTerminalVisible); + + return ( + handleSectionClick(e, section.id, section.href)} + className={cn( + 'group w-full flex items-center px-3 py-2 text-sm transition-colors min-h-[32px]', + isActive ? 'bg-sidebar-accent' : 'hover:bg-sidebar-accent' )} - - - {!isExpanded && ( - -

All Projects

-
- )} - - - {/* New Project */} - - - - - {isExpanded && ( - New Project + + {section.label} +
+ ); + })} + + {/* Configuration Group */} +
+ + + {isConfigExpanded && ( + )} - - -
- - {/* Project List */} -
- {projects.map((project) => ( - - - -
- - -
- {isExpanded && ( - {project.name} - )} - -
- {!isExpanded && ( - -

{project.name}

-
- )} -
- ))}
-
- {/* Settings */} - - + )}
); -} +} \ No newline at end of file diff --git a/package.json b/package.json index 9370b9e..a4846d8 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,9 @@ "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-tooltip": "^1.2.8", - "@t3-oss/env-nextjs": "^0.13.8", + "@t3-oss/env-nextjs": "^0.13.8", + "@tanstack/react-query": "^5.90.7", + "@tanstack/react-query-devtools": "^5.90.2", "@zjy365/sealos-desktop-sdk": "^0.1.20", "bcryptjs": "^3.0.2", "class-variance-authority": "^0.7.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0fe639a..41147d9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -54,6 +54,12 @@ importers: '@t3-oss/env-nextjs': specifier: ^0.13.8 version: 0.13.8(typescript@5.9.3)(zod@4.1.12) + '@tanstack/react-query': + specifier: ^5.90.7 + version: 5.90.7(react@19.1.0) + '@tanstack/react-query-devtools': + specifier: ^5.90.2 + version: 5.90.2(@tanstack/react-query@5.90.7(react@19.1.0))(react@19.1.0) '@zjy365/sealos-desktop-sdk': specifier: ^0.1.20 version: 0.1.20(@kubernetes/client-node@1.4.0) @@ -1103,6 +1109,23 @@ packages: '@tailwindcss/postcss@4.1.16': resolution: {integrity: sha512-Qn3SFGPXYQMKR/UtqS+dqvPrzEeBZHrFA92maT4zijCVggdsXnDBMsPFJo1eArX3J+O+Gi+8pV4PkqjLCNBk3A==} + '@tanstack/query-core@5.90.7': + resolution: {integrity: sha512-6PN65csiuTNfBMXqQUxQhCNdtm1rV+9kC9YwWAIKcaxAauq3Wu7p18j3gQY3YIBJU70jT/wzCCZ2uqto/vQgiQ==} + + '@tanstack/query-devtools@5.90.1': + resolution: {integrity: sha512-GtINOPjPUH0OegJExZ70UahT9ykmAhmtNVcmtdnOZbxLwT7R5OmRztR5Ahe3/Cu7LArEmR6/588tAycuaWb1xQ==} + + '@tanstack/react-query-devtools@5.90.2': + resolution: {integrity: sha512-vAXJzZuBXtCQtrY3F/yUNJCV4obT/A/n81kb3+YqLbro5Z2+phdAbceO+deU3ywPw8B42oyJlp4FhO0SoivDFQ==} + peerDependencies: + '@tanstack/react-query': ^5.90.2 + react: ^18 || ^19 + + '@tanstack/react-query@5.90.7': + resolution: {integrity: sha512-wAHc/cgKzW7LZNFloThyHnV/AX9gTg3w5yAv0gvQHPZoCnepwqCMtzbuPbb2UvfvO32XZ46e8bPOYbfZhzVnnQ==} + peerDependencies: + react: ^18 || ^19 + '@tybys/wasm-util@0.10.1': resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} @@ -3903,6 +3926,21 @@ snapshots: postcss: 8.5.6 tailwindcss: 4.1.16 + '@tanstack/query-core@5.90.7': {} + + '@tanstack/query-devtools@5.90.1': {} + + '@tanstack/react-query-devtools@5.90.2(@tanstack/react-query@5.90.7(react@19.1.0))(react@19.1.0)': + dependencies: + '@tanstack/query-devtools': 5.90.1 + '@tanstack/react-query': 5.90.7(react@19.1.0) + react: 19.1.0 + + '@tanstack/react-query@5.90.7(react@19.1.0)': + dependencies: + '@tanstack/query-core': 5.90.7 + react: 19.1.0 + '@tybys/wasm-util@0.10.1': dependencies: tslib: 2.8.1 diff --git a/provider/providers.tsx b/provider/providers.tsx index 827d736..8bd470e 100644 --- a/provider/providers.tsx +++ b/provider/providers.tsx @@ -1,13 +1,32 @@ 'use client'; +import { useState } from 'react'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; import { SessionProvider } from 'next-auth/react'; import { SealosProvider } from './sealos'; export function Providers({ children }: { children: React.ReactNode }) { + const [queryClient] = useState( + () => + new QueryClient({ + defaultOptions: { + queries: { + staleTime: 5 * 1000, // 5 seconds + retry: 2, + refetchOnWindowFocus: false, + }, + }, + }), + ); + return ( - {children} + + {children} + {process.env.NODE_ENV === 'development' && } + ); }