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
10 changes: 8 additions & 2 deletions app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);
Expand All @@ -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;
Comment thread
HUAHUAI23 marked this conversation as resolved.
--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);
Expand Down Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion app/projects/[id]/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ export default async function ProjectLayout({

return (
<TerminalProvider>
<div className="h-screen flex bg-background text-foreground overflow-hidden">
<div className="h-screen flex text-foreground overflow-hidden">
{/* Primary Sidebar - VSCode style */}
<ProjectSidebar projects={projects} currentProjectId={id} userId={session.user.id} />

Expand Down
114 changes: 29 additions & 85 deletions app/projects/page.tsx
Original file line number Diff line number Diff line change
@@ -1,52 +1,54 @@
'use client';

import { useEffect, useState } from 'react';
import { Clock, Folder, Loader2, Plus } from 'lucide-react';
import Link from 'next/link';
import { useCallback, useEffect, useState } from 'react';
import { Loader2 } from 'lucide-react';

import NoProject from '@/components/features/projectList/NoProject';
import { Button } from '@/components/ui/button';
import PageHeader from '@/components/features/projectList/PageHeader';
import ProjectCard from '@/components/features/projectList/ProjectCard';
import { GET } from '@/lib/fetch-client';
import { cn } from '@/lib/utils';
import { Project } from '@/types/project';

interface Project {
id: string;
name: string;
description: string | null;
status: string;
updatedAt: string;
githubRepo: string | null;
}
// TODO: convert this page to ssr, add loading and error status, add a ProjectGrid UI, and handle data fetching there

export default function ProjectsPage() {
const [projects, setProjects] = useState<Project[]>([]);
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<Project[]>('/api/projects');
const data = await GET<Project[]>('/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 (
Expand All @@ -62,22 +64,7 @@ export default function ProjectsPage() {
return (
<div className="min-h-screen bg-background">
{/* Header Bar */}
<div className="h-12 bg-card border-b border-border flex items-center justify-between px-4">
<div className="flex items-center gap-3">
<Folder className="h-4 w-4 text-muted-foreground" />
<h1 className="text-sm font-medium text-foreground">Projects</h1>
<span className="text-xs text-muted-foreground">({projects.length})</span>
</div>
<Link href="/projects/new">
<Button
size="sm"
className="h-7 bg-primary hover:bg-primary/90 text-primary-foreground text-xs px-3"
>
<Plus className="mr-1 h-3 w-3" />
New Project
</Button>
</Link>
</div>
<PageHeader projectsCount={projects.length} />

{/* Content */}
<div className="p-6">
Expand All @@ -86,50 +73,7 @@ export default function ProjectsPage() {
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-3">
{projects.map((project) => (
<Link key={project.id} href={`/projects/${project.id}/terminal`}>
<div className="group bg-card border border-border hover:border-primary rounded-lg transition-all cursor-pointer p-3">
{/* Header */}
<div className="flex items-start justify-between mb-2">
<div className="flex-1 min-w-0">
<h3 className="text-sm font-medium text-foreground truncate">
{project.name}
</h3>
</div>
<div
className={cn(
'h-2 w-2 rounded-full shrink-0 ml-2 mt-1',
project.status === 'RUNNING' && 'bg-green-600 dark:bg-green-500',
project.status === 'STOPPED' && 'bg-muted-foreground',
project.status === 'STARTING' && 'bg-yellow-600 dark:bg-yellow-500 animate-pulse',
project.status === 'STOPPING' && 'bg-yellow-600 dark:bg-yellow-500 animate-pulse',
project.status === 'CREATING' && 'bg-blue-600 dark:bg-blue-500 animate-pulse',
project.status === 'TERMINATING' && 'bg-red-600 dark:bg-red-500 animate-pulse',
project.status === 'ERROR' && 'bg-destructive',
project.status === 'PARTIAL' && 'bg-orange-600 dark:bg-orange-500'
)}
/>
</div>

{/* Description */}
<p className="text-xs text-muted-foreground line-clamp-2 mb-3 min-h-[2.5rem]">
{project.description || 'No description'}
</p>

{/* Footer */}
<div className="flex items-center justify-between pt-2 border-t border-border">
<span className="text-xs text-muted-foreground">{project.status}</span>
<div className="flex items-center gap-1 text-xs text-muted-foreground">
<Clock className="h-3 w-3" />
<span>
{new Date(project.updatedAt).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
})}
</span>
</div>
</div>
</div>
</Link>
<ProjectCard key={project.id} project={project} />
))}
</div>
)}
Expand Down
35 changes: 35 additions & 0 deletions components/features/projectList/PageHeader.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className={`h-12 bg-card border-b border-border flex items-center justify-between px-4 ${className || ''}`}>
<div className="flex items-center gap-3">
<Folder className="h-4 w-4 text-muted-foreground" />
<h1 className="text-sm font-medium text-foreground">Projects</h1>
<span className="text-xs text-muted-foreground">({projectsCount})</span>
</div>
<Link href="/projects/new">
<Button
size="sm"
className="h-7 bg-primary hover:bg-primary/90 text-primary-foreground text-xs px-3"
>
<Plus className="mr-1 h-3 w-3" />
New Project
</Button>
</Link>
</div>
);
});

PageHeader.displayName = 'PageHeader';

export default PageHeader;
63 changes: 63 additions & 0 deletions components/features/projectList/ProjectCard.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Link href={`/projects/${project.id}`}>
<div className="group bg-card border border-border hover:border-primary rounded-lg transition-all cursor-pointer p-3">
{/* Header */}
<div className="flex items-start justify-between mb-2">
<div className="flex-1 min-w-0">
<h3 className="text-sm font-medium text-foreground truncate">
{project.name}
</h3>
</div>
<div
className={cn(
'h-2 w-2 rounded-full shrink-0 ml-2 mt-1',
project.status === 'RUNNING' && 'bg-green-600 dark:bg-green-500',
project.status === 'STOPPED' && 'bg-muted-foreground',
project.status === 'STARTING' && 'bg-yellow-600 dark:bg-yellow-500 animate-pulse',
project.status === 'STOPPING' && 'bg-yellow-600 dark:bg-yellow-500 animate-pulse',
project.status === 'CREATING' && 'bg-blue-600 dark:bg-blue-500 animate-pulse',
project.status === 'TERMINATING' && 'bg-red-600 dark:bg-red-500 animate-pulse',
project.status === 'ERROR' && 'bg-destructive',
project.status === 'PARTIAL' && 'bg-orange-600 dark:bg-orange-500'
)}
/>
</div>

{/* Description */}
<p className="text-xs text-muted-foreground line-clamp-2 mb-3 min-h-[2.5rem]">
{project.description || 'No description'}
</p>

{/* Footer */}
<div className="flex items-center justify-between pt-2 border-t border-border">
<span className="text-xs text-muted-foreground">{project.status}</span>
<div className="flex items-center gap-1 text-xs text-muted-foreground">
<Clock className="h-3 w-3" />
<span>
{new Date(project.updatedAt).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
})}
</span>
</div>
</div>
</div>
</Link>
);
});

ProjectCard.displayName = 'ProjectCard';

export default ProjectCard;
28 changes: 14 additions & 14 deletions components/project-secondary-sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ export default function ProjectSecondarySidebar({ project }: ProjectSecondarySid
return (
<div
className={cn(
'bg-background border-r border-border flex flex-col transition-all duration-200',
'bg-sidebar-project-background border-r border-border flex flex-col transition-all duration-200',
isCollapsed ? 'w-10' : 'w-72'
)}
>
Expand Down Expand Up @@ -124,11 +124,11 @@ export default function ProjectSecondarySidebar({ project }: ProjectSecondarySid
href={section.href}
onClick={(e) => 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'
'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'
)}
>
<Icon className="h-4 w-4 mr-2 text-muted-foreground flex-shrink-0" />
<Icon className="h-4 w-4 mr-2 text-muted-foreground group-hover:text-sidebar-foreground flex-shrink-0 transition-colors" />
<span className="text-foreground truncate flex-1">{section.label}</span>
</a>
);
Expand All @@ -139,15 +139,15 @@ export default function ProjectSecondarySidebar({ project }: ProjectSecondarySid
<button
onClick={() => setIsConfigExpanded(!isConfigExpanded)}
className={cn(
'w-full flex items-center px-3 py-2 text-sm transition-colors min-h-[32px]',
isConfigActive ? 'bg-sidebar-accent' : 'hover:bg-accent'
'group w-full flex items-center px-3 py-2 text-sm transition-colors min-h-[32px]',
isConfigActive ? 'bg-sidebar-accent' : 'hover:bg-sidebar-accent'
)}
>
<Settings className="h-4 w-4 mr-2 text-muted-foreground flex-shrink-0" />
<Settings className="h-4 w-4 mr-2 text-muted-foreground group-hover:text-sidebar-foreground flex-shrink-0 transition-colors" />
<span className="text-foreground flex-1 text-left truncate">Configuration</span>
<ChevronDown
className={cn(
'h-4 w-4 text-muted-foreground transition-transform flex-shrink-0',
'h-4 w-4 text-muted-foreground group-hover:text-sidebar-foreground transition-transform flex-shrink-0 transition-colors',
Comment thread
HUAHUAI23 marked this conversation as resolved.
!isConfigExpanded && '-rotate-90'
)}
/>
Expand All @@ -165,11 +165,11 @@ export default function ProjectSecondarySidebar({ project }: ProjectSecondarySid
href={section.href}
onClick={(e) => handleSectionClick(e, section.id, section.href)}
className={cn(
'w-full flex items-center pl-9 pr-3 py-2 text-sm transition-colors min-h-[32px]',
isActive ? 'bg-sidebar-accent' : 'hover:bg-accent'
'group w-full flex items-center pl-9 pr-3 py-2 text-sm transition-colors min-h-[32px]',
isActive ? 'bg-sidebar-accent' : 'hover:bg-sidebar-accent'
)}
>
<Icon className="h-4 w-4 mr-2 text-muted-foreground flex-shrink-0" />
<Icon className="h-4 w-4 mr-2 text-muted-foreground group-hover:text-sidebar-foreground flex-shrink-0 transition-colors" />
<span className="text-foreground truncate flex-1">{section.label}</span>
</a>
);
Expand All @@ -189,11 +189,11 @@ export default function ProjectSecondarySidebar({ project }: ProjectSecondarySid
href={section.href}
onClick={(e) => 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'
'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'
)}
>
<Icon className="h-4 w-4 mr-2 text-muted-foreground flex-shrink-0" />
<Icon className="h-4 w-4 mr-2 text-muted-foreground group-hover:text-sidebar-foreground flex-shrink-0 transition-colors" />
<span className="text-foreground truncate flex-1">{section.label}</span>
</a>
);
Expand Down
Loading
Loading