diff --git a/app/globals.css b/app/globals.css index b6f1826..33e95c5 100644 --- a/app/globals.css +++ b/app/globals.css @@ -29,6 +29,8 @@ --chart-5: oklch(0.38 0.14 265.59); --sidebar: oklch(0.98 0 0); --sidebar-foreground: oklch(0.14 0 0); + --sidebar-background: oklch(1.0 0 0); + --sidebar-project-background: oklch(1.0 0 0); --sidebar-primary: oklch(0.20 0 0); --sidebar-primary-foreground: oklch(0.98 0 0); --sidebar-accent: oklch(0.97 0 0); @@ -51,7 +53,7 @@ } .dark { - --background: oklch(0.20 0 0); + --background: oklch(0.3171 0 0); --foreground: oklch(0.92 0 0); --card: oklch(0.27 0 0); --card-foreground: oklch(0.92 0 0); @@ -76,9 +78,11 @@ --chart-5: oklch(0.42 0.18 265.55); --sidebar: oklch(0.21 0.01 285.93); --sidebar-foreground: oklch(0.99 0 0); + --sidebar-background: #333333; + --sidebar-project-background: #242426; --sidebar-primary: oklch(0.49 0.24 264.40); --sidebar-primary-foreground: oklch(0.99 0 0); - --sidebar-accent: oklch(0.27 0.01 286.10); + --sidebar-accent: #2B2D2E; --sidebar-accent-foreground: oklch(0.99 0 0); --sidebar-border: oklch(1.00 0 0 / 10%); --sidebar-ring: oklch(0.55 0.02 285.93); @@ -122,6 +126,8 @@ --color-chart-5: var(--chart-5); --color-sidebar: var(--sidebar); --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar-background: var(--sidebar-background); + --color-sidebar-project-background: var(--sidebar-project-background); --color-sidebar-primary: var(--sidebar-primary); --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); --color-sidebar-accent: var(--sidebar-accent); @@ -165,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 38a750c..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 */} - ([]); const [loading, setLoading] = useState(true); - // Fetch projects list - const fetchProjects = async () => { + // Fetch projects list with AbortController support + const fetchProjects = useCallback(async (signal?: AbortSignal) => { try { - const data = await GET('/api/projects'); + const data = await GET('/api/projects', { signal }); setProjects(data); } catch (error) { + // Ignore AbortError (component was unmounted) + if (error instanceof Error && error.name === 'AbortError') { + return; + } console.error('Failed to fetch projects:', error); } finally { setLoading(false); } - }; - - // Initial load - useEffect(() => { - fetchProjects(); }, []); - // Polling: refresh project status every 3 seconds + // Initial load and polling with proper cleanup useEffect(() => { + const abortController = new AbortController(); + + // Initial fetch + fetchProjects(abortController.signal); + + // Set up polling const interval = setInterval(() => { - fetchProjects(); + fetchProjects(abortController.signal); }, 3000); - return () => clearInterval(interval); - }, []); + // Cleanup function + return () => { + abortController.abort(); // Cancel all ongoing requests + clearInterval(interval); // Clear polling interval + }; + }, [fetchProjects]); if (loading) { return ( @@ -62,22 +64,7 @@ export default function ProjectsPage() { return (
{/* Header Bar */} -
-
- -

Projects

- ({projects.length}) -
- - - -
+ {/* Content */}
@@ -86,50 +73,7 @@ export default function ProjectsPage() { ) : (
{projects.map((project) => ( - -
- {/* Header */} -
-
-

- {project.name} -

-
-
-
- - {/* Description */} -

- {project.description || 'No description'} -

- - {/* Footer */} -
- {project.status} -
- - - {new Date(project.updatedAt).toLocaleDateString('en-US', { - month: 'short', - day: 'numeric', - })} - -
-
-
- + ))}
)} diff --git a/app/settings/layout.tsx b/app/settings/layout.tsx index 8f6e7bd..c2b73d2 100644 --- a/app/settings/layout.tsx +++ b/app/settings/layout.tsx @@ -1,9 +1,8 @@ import { ReactNode } from 'react'; import { redirect } from 'next/navigation'; -import ProjectSidebar from '@/components/project-sidebar'; +import PrimarySidebar from '@/components/primary-sidebar'; import { auth } from '@/lib/auth'; -import { prisma } from '@/lib/db'; export default async function SettingsLayout({ children, @@ -16,21 +15,10 @@ export default async function SettingsLayout({ redirect('/login'); } - // Get user's projects for sidebar - const projects = await prisma.project.findMany({ - where: { - userId: session.user.id, - }, - orderBy: { - createdAt: 'desc', - }, - }); - return (
{/* Primary Sidebar - VSCode style */} - diff --git a/components/features/projectList/PageHeader.tsx b/components/features/projectList/PageHeader.tsx new file mode 100644 index 0000000..36f31d4 --- /dev/null +++ b/components/features/projectList/PageHeader.tsx @@ -0,0 +1,35 @@ +import { memo } from 'react'; +import { Folder, Plus } from 'lucide-react'; +import Link from 'next/link'; + +import { Button } from '@/components/ui/button'; + +interface PageHeaderProps { + projectsCount: number; + className?: string; +} + +const PageHeader = memo(({ projectsCount, className }: PageHeaderProps) => { + return ( +
+
+ +

Projects

+ ({projectsCount}) +
+ + + +
+ ); +}); + +PageHeader.displayName = 'PageHeader'; + +export default PageHeader; \ No newline at end of file diff --git a/components/features/projectList/ProjectCard.tsx b/components/features/projectList/ProjectCard.tsx new file mode 100644 index 0000000..3099565 --- /dev/null +++ b/components/features/projectList/ProjectCard.tsx @@ -0,0 +1,63 @@ +import { memo } from 'react'; +import { Clock } from 'lucide-react'; +import Link from 'next/link'; + +import { cn } from '@/lib/utils'; +import { Project } from '@/types/project'; + +interface ProjectCardProps { + project: Project; +} + +const ProjectCard = memo(({ project }: ProjectCardProps) => { + return ( + +
+ {/* Header */} +
+
+

+ {project.name} +

+
+
+
+ + {/* Description */} +

+ {project.description || 'No description'} +

+ + {/* Footer */} +
+ {project.status} +
+ + + {new Date(project.updatedAt).toLocaleDateString('en-US', { + month: 'short', + day: 'numeric', + })} + +
+
+
+ + ); +}); + +ProjectCard.displayName = 'ProjectCard'; + +export default ProjectCard; \ No newline at end of file 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 499315f..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( - 'w-full flex items-center px-3 py-2 text-sm transition-colors min-h-[32px]', - isActive ? 'bg-sidebar-accent' : 'hover:bg-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( - 'w-full flex items-center px-3 py-2 text-sm transition-colors min-h-[32px]', - isActive ? 'bg-sidebar-accent' : 'hover:bg-accent' - )} - > - - {section.label} - - ); - })} -
- )} -
- ); -} diff --git a/components/project-sidebar.tsx b/components/project-sidebar.tsx index 2dac2ff..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 4526c7d..a4846d8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "fullstack-agent", - "version": "0.2.2", + "version": "0.4.1", "private": true, "scripts": { "prepare": "prisma generate", @@ -25,6 +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", + "@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 bfe42c1..41147d9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -51,6 +51,15 @@ importers: '@radix-ui/react-tooltip': specifier: ^1.2.8 version: 1.2.8(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@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) @@ -978,6 +987,40 @@ packages: '@swc/helpers@0.5.15': resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} + '@t3-oss/env-core@0.13.8': + resolution: {integrity: sha512-L1inmpzLQyYu4+Q1DyrXsGJYCXbtXjC4cICw1uAKv0ppYPQv656lhZPU91Qd1VS6SO/bou1/q5ufVzBGbNsUpw==} + peerDependencies: + arktype: ^2.1.0 + typescript: '>=5.0.0' + valibot: ^1.0.0-beta.7 || ^1.0.0 + zod: ^3.24.0 || ^4.0.0-beta.0 + peerDependenciesMeta: + arktype: + optional: true + typescript: + optional: true + valibot: + optional: true + zod: + optional: true + + '@t3-oss/env-nextjs@0.13.8': + resolution: {integrity: sha512-QmTLnsdQJ8BiQad2W2nvV6oUpH4oMZMqnFEjhVpzU0h3sI9hn8zb8crjWJ1Amq453mGZs6A4v4ihIeBFDOrLeQ==} + peerDependencies: + arktype: ^2.1.0 + typescript: '>=5.0.0' + valibot: ^1.0.0-beta.7 || ^1.0.0 + zod: ^3.24.0 || ^4.0.0-beta.0 + peerDependenciesMeta: + arktype: + optional: true + typescript: + optional: true + valibot: + optional: true + zod: + optional: true + '@tailwindcss/node@4.1.16': resolution: {integrity: sha512-BX5iaSsloNuvKNHRN3k2RcCuTEgASTo77mofW0vmeHkfrDWaoFAFvNHpEgtu0eqyypcyiBkDWzSMxJhp3AUVcw==} @@ -1066,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==} @@ -3785,6 +3845,18 @@ snapshots: dependencies: tslib: 2.8.1 + '@t3-oss/env-core@0.13.8(typescript@5.9.3)(zod@4.1.12)': + optionalDependencies: + typescript: 5.9.3 + zod: 4.1.12 + + '@t3-oss/env-nextjs@0.13.8(typescript@5.9.3)(zod@4.1.12)': + dependencies: + '@t3-oss/env-core': 0.13.8(typescript@5.9.3)(zod@4.1.12) + optionalDependencies: + typescript: 5.9.3 + zod: 4.1.12 + '@tailwindcss/node@4.1.16': dependencies: '@jridgewell/remapping': 2.3.5 @@ -3854,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' && } + ); } diff --git a/types/project.ts b/types/project.ts new file mode 100644 index 0000000..2cf5943 --- /dev/null +++ b/types/project.ts @@ -0,0 +1,8 @@ +export interface Project { + id: string; + name: string; + description: string | null; + status: string; + updatedAt: string; + githubRepo: string | null; +} \ No newline at end of file