Skip to content
Closed
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
19 changes: 17 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;
--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 Expand Up @@ -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;
}
}
19 changes: 5 additions & 14 deletions app/projects/[id]/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 (
<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} />
<PrimarySidebar currentProjectId={id} userId={session.user.id} />

{/* Secondary Sidebar - Project Settings */}
<ProjectSecondarySidebar
<ProjectSidebar
project={project}
sandboxes={project.sandboxes}
envVars={project.environments}
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
16 changes: 2 additions & 14 deletions app/settings/layout.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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 (
<div className="h-screen flex bg-[#1e1e1e] text-white overflow-hidden">
{/* Primary Sidebar - VSCode style */}
<ProjectSidebar
projects={projects}
<PrimarySidebar
currentProjectId="" // No current project in settings
userId={session.user.id}
/>
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;
Loading