Skip to content

Commit 21a2642

Browse files
Che-Zhuclaude
andauthored
feat: Home page UI overhaul with project management dashboard (#132)
* docs: Remove various legacy documentation files and add design-system.md. * feat: Add homepage with sidebar and update design system - New homepage with sidebar + hero layout - Sidebar component with nav, logo, version display - New fonts: Space Grotesk + Noto Sans - Update theme border colors - Add shadcn Separator * feat: add home layout with sidebar and search bar - Create dedicated layout for /home route with Sidebar + SearchBar - Update background color to #121416 in modern theme - Add SearchBar component using shadcn/ui (Input, Button, Kbd) - Update design-system.md with font application guidelines - Simplify home page by extracting layout concerns Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * feat: Implement home page project listing and creation UI with new toggle components. * feat: introduce and apply design system color variables for page header styling and document new tokens. * feat: Enhance project cards with navigation to detail pages and direct public URL access, refactoring component to use shadcn/ui. * refactor: remove Hero component * refactor: centralize project status configuration, use Prisma types for project status, and add relative time formatting utility. * feat: implement project actions menu and integrate it into project cards * refactor: Remove `alert-dialog-vscode` and enhance `alert-dialog` with size options and improved overlay interaction. * feat: Integrate `CreateProjectDialog` into `SearchBar` and `CreateProjectCard` components to handle new project creation.feat: Integrate `CreateProjectDialog` into `SearchBar` and `CreateProjectCard` components to handle new project creation * feat: add FullScreenDialog component and use it to replace AlertDialog for project deletion confirmation. * feat: introduce `createProject` server action and refactor project creation dialog to utilize it * fix lint issue * chore: Reset package version to 2.0.0-alpha.1 --------- Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent dc99f0d commit 21a2642

58 files changed

Lines changed: 2494 additions & 10447 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

app/globals.css

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@
176176
/* Modern Theme - Dark Mode (default) */
177177
.modern {
178178
/* === Base Colors === */
179-
--background: #09090b;
179+
--background: #121416;
180180
--foreground: #fafafa;
181181

182182
/* === UI Components === */
@@ -203,7 +203,7 @@
203203
--destructive-foreground: #ffffff;
204204

205205
/* === Borders & Inputs === */
206-
--border: #27272a;
206+
--border: #2d3136;
207207
--input: #27272a;
208208
--ring: #3b82f6;
209209

@@ -213,7 +213,7 @@
213213
--sidebar-background: #18181b;
214214
--sidebar-project-background: #09090b;
215215
--sidebar-accent: #27272a;
216-
--sidebar-border: #27272a;
216+
--sidebar-border: #2d3136;
217217

218218
/* === Charts === */
219219
--chart-1: #3b82f6;
@@ -371,6 +371,11 @@
371371

372372
body {
373373
@apply bg-background text-foreground;
374+
font-family: var(--font-body), sans-serif;
375+
}
376+
377+
h1, h2, h3, h4, h5, h6 {
378+
font-family: var(--font-heading), sans-serif;
374379
}
375380

376381
h1 {
@@ -388,6 +393,10 @@
388393
span {
389394
@apply text-muted-foreground;
390395
}
396+
397+
code, pre, .font-mono {
398+
font-family: var(--font-mono), monospace;
399+
}
391400
}
392401

393402
/* Terminal scroll indicator animation */
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
'use client'
2+
3+
import { useState } from 'react'
4+
import { MdAdd } from 'react-icons/md'
5+
6+
import CreateProjectDialog from '@/components/dialog/create-project-dialog'
7+
import { cn } from '@/lib/utils'
8+
9+
export function CreateProjectCard() {
10+
const [isDialogOpen, setIsDialogOpen] = useState(false)
11+
12+
return (
13+
<>
14+
<button
15+
onClick={() => setIsDialogOpen(true)}
16+
className={cn(
17+
'group bg-card/30 border border-dashed border-border rounded-xl',
18+
'flex flex-col items-center justify-center p-8',
19+
'hover:bg-card/50 hover:border-primary/50',
20+
'transition-all cursor-pointer h-full min-h-[280px]'
21+
)}
22+
>
23+
<div
24+
className={cn(
25+
'w-16 h-16 rounded-full bg-card border border-border',
26+
'flex items-center justify-center',
27+
'group-hover:scale-110 transition-transform duration-300',
28+
'mb-4 shadow-lg group-hover:shadow-primary/20'
29+
)}
30+
>
31+
<MdAdd className="w-8 h-8 text-muted-foreground group-hover:text-primary transition-colors" />
32+
</div>
33+
<h3 className="text-lg font-bold font-display text-white mb-2">
34+
Create New Project
35+
</h3>
36+
<p className="text-sm text-muted-foreground text-center max-w-[200px]">
37+
Start from scratch or use one of our templates.
38+
</p>
39+
</button>
40+
41+
<CreateProjectDialog
42+
open={isDialogOpen}
43+
onOpenChange={setIsDialogOpen}
44+
/>
45+
</>
46+
)
47+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
'use client'
2+
3+
import { useState } from 'react'
4+
import { ProjectStatus } from '@prisma/client'
5+
6+
import type { ProjectWithRelations } from '@/lib/data/project'
7+
8+
import { PageHeaderWithFilter } from './page-header-with-filter'
9+
import { ProjectList } from './project-list'
10+
11+
interface HomePageContentProps {
12+
projects: ProjectWithRelations<{ sandboxes: true }>[]
13+
}
14+
15+
export function HomePageContent({ projects }: HomePageContentProps) {
16+
const [activeFilter, setActiveFilter] = useState<'ALL' | ProjectStatus>('ALL')
17+
18+
return (
19+
<>
20+
<PageHeaderWithFilter activeFilter={activeFilter} onFilterChange={setActiveFilter} />
21+
<ProjectList projects={projects} activeFilter={activeFilter} />
22+
</>
23+
)
24+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
'use client'
2+
3+
import { ProjectStatus } from '@prisma/client'
4+
5+
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group'
6+
import { cn } from '@/lib/utils'
7+
8+
type FilterStatus = 'ALL' | ProjectStatus
9+
10+
interface PageHeaderWithFilterProps {
11+
activeFilter: FilterStatus
12+
onFilterChange: (filter: FilterStatus) => void
13+
}
14+
15+
const filters: { label: string; value: FilterStatus }[] = [
16+
{ label: 'All', value: 'ALL' },
17+
{ label: 'Running', value: 'RUNNING' },
18+
{ label: 'Stopped', value: 'STOPPED' },
19+
]
20+
21+
export function PageHeaderWithFilter({ activeFilter, onFilterChange }: PageHeaderWithFilterProps) {
22+
return (
23+
<div className="flex flex-col sm:flex-row sm:items-end justify-between gap-4 mb-8">
24+
<div>
25+
<h1 className="text-3xl font-heading font-bold text-foreground tracking-tight">
26+
My Projects
27+
</h1>
28+
<p className="text-muted-foreground mt-1">
29+
Manage and monitor your full stack apps and deployments.
30+
</p>
31+
</div>
32+
<ToggleGroup
33+
type="single"
34+
value={activeFilter}
35+
onValueChange={(value) => value && onFilterChange(value as FilterStatus)}
36+
className="flex bg-sidebar p-1 rounded-lg border border-border"
37+
spacing={1}
38+
>
39+
{filters.map((filter) => (
40+
<ToggleGroupItem
41+
key={filter.value}
42+
value={filter.value}
43+
className={cn(
44+
'px-4 py-1.5 text-xs font-bold font-heading rounded border-0 shadow-none',
45+
'text-muted-foreground hover:text-white hover:bg-transparent',
46+
'data-[state=on]:bg-accent data-[state=on]:text-foreground data-[state=on]:shadow-sm',
47+
'transition-colors duration-200'
48+
)}
49+
>
50+
{filter.label}
51+
</ToggleGroupItem>
52+
))}
53+
</ToggleGroup>
54+
</div>
55+
)
56+
}
Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
'use client';
2+
3+
import { useState } from 'react';
4+
import { MdDeleteOutline, MdMoreHoriz, MdPause, MdPlayArrow, MdRefresh, MdSettings } from 'react-icons/md';
5+
import { ProjectStatus } from '@prisma/client';
6+
import { useRouter } from 'next/navigation';
7+
8+
import { Button } from '@/components/ui/button';
9+
import {
10+
DropdownMenu,
11+
DropdownMenuContent,
12+
DropdownMenuItem,
13+
DropdownMenuSeparator,
14+
DropdownMenuTrigger,
15+
} from '@/components/ui/dropdown-menu';
16+
import {
17+
FullScreenDialog,
18+
FullScreenDialogAction,
19+
FullScreenDialogClose,
20+
FullScreenDialogContent,
21+
FullScreenDialogDescription,
22+
FullScreenDialogFooter,
23+
FullScreenDialogHeader,
24+
FullScreenDialogTitle,
25+
} from '@/components/ui/fullscreen-dialog';
26+
import { Input } from '@/components/ui/input';
27+
import { Label } from '@/components/ui/label';
28+
import { useProjectOperations } from '@/hooks/use-project-operations';
29+
30+
interface ProjectActionsMenuProps {
31+
projectId: string;
32+
projectName: string;
33+
status: ProjectStatus;
34+
}
35+
36+
export function ProjectActionsMenu({ projectId, projectName, status }: ProjectActionsMenuProps) {
37+
const router = useRouter();
38+
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
39+
const [confirmInput, setConfirmInput] = useState('');
40+
const { executeOperation, loading } = useProjectOperations(projectId);
41+
42+
// Determine available actions based on status
43+
const showStart = status === 'STOPPED';
44+
const showStop = status !== 'STOPPED';
45+
46+
// Check if the confirmation input matches the project name
47+
const isConfirmValid = confirmInput === projectName;
48+
49+
const handleDeleteClick = () => {
50+
setShowDeleteDialog(true);
51+
};
52+
53+
const handleDeleteConfirm = () => {
54+
if (!isConfirmValid) return;
55+
setShowDeleteDialog(false);
56+
setConfirmInput('');
57+
executeOperation('DELETE');
58+
};
59+
60+
const handleDialogOpenChange = (open: boolean) => {
61+
setShowDeleteDialog(open);
62+
if (!open) {
63+
setConfirmInput('');
64+
}
65+
};
66+
67+
const handleSettingsClick = () => {
68+
router.push(`/projects/${projectId}/settings`);
69+
};
70+
71+
return (
72+
<>
73+
<DropdownMenu>
74+
<DropdownMenuTrigger asChild>
75+
<Button
76+
variant="ghost"
77+
size="icon"
78+
className="absolute top-3 right-3 h-8 w-8 text-muted-foreground hover:text-white"
79+
onClick={(e) => e.stopPropagation()}
80+
>
81+
<MdMoreHoriz className="w-5 h-5" />
82+
<span className="sr-only">More options</span>
83+
</Button>
84+
</DropdownMenuTrigger>
85+
<DropdownMenuContent
86+
align="end"
87+
className="w-32 bg-popover/90 backdrop-blur-md"
88+
>
89+
{/* Start/Stop based on status */}
90+
{showStart && (
91+
<DropdownMenuItem
92+
onClick={(e) => {
93+
e.stopPropagation();
94+
executeOperation('START');
95+
}}
96+
disabled={loading !== null}
97+
className="gap-3 px-3 py-2 text-xs font-medium text-muted-foreground hover:text-white hover:bg-white/5"
98+
>
99+
{loading === 'START' ? (
100+
<>
101+
<MdRefresh className="h-[18px] w-[18px] animate-spin" />
102+
Starting...
103+
</>
104+
) : (
105+
<>
106+
<MdPlayArrow className="h-[18px] w-[18px]" />
107+
Start
108+
</>
109+
)}
110+
</DropdownMenuItem>
111+
)}
112+
{showStop && (
113+
<DropdownMenuItem
114+
onClick={(e) => {
115+
e.stopPropagation();
116+
executeOperation('STOP');
117+
}}
118+
disabled={loading !== null}
119+
className="gap-3 px-3 py-2 text-xs font-medium text-muted-foreground hover:text-white hover:bg-white/5"
120+
>
121+
{loading === 'STOP' ? (
122+
<>
123+
<MdRefresh className="h-[18px] w-[18px] animate-spin" />
124+
Stopping...
125+
</>
126+
) : (
127+
<>
128+
<MdPause className="h-[18px] w-[18px]" />
129+
Stop
130+
</>
131+
)}
132+
</DropdownMenuItem>
133+
)}
134+
135+
{/* Settings */}
136+
<DropdownMenuItem
137+
onClick={(e) => {
138+
e.stopPropagation();
139+
handleSettingsClick();
140+
}}
141+
className="gap-3 px-3 py-2 text-xs font-medium text-muted-foreground hover:text-white hover:bg-white/5"
142+
>
143+
<MdSettings className="h-[18px] w-[18px]" />
144+
Settings
145+
</DropdownMenuItem>
146+
147+
<DropdownMenuSeparator className="bg-border/60 mx-2 my-1" />
148+
149+
{/* Delete */}
150+
<DropdownMenuItem
151+
onClick={(e) => {
152+
e.stopPropagation();
153+
handleDeleteClick();
154+
}}
155+
disabled={loading !== null}
156+
className="gap-3 px-3 py-2 text-xs font-medium text-red-500 hover:text-red-400 hover:bg-red-500/10"
157+
>
158+
<MdDeleteOutline className="h-[18px] w-[18px] text-red-500" />
159+
Delete
160+
</DropdownMenuItem>
161+
</DropdownMenuContent>
162+
</DropdownMenu>
163+
164+
{/* Delete Confirmation Dialog */}
165+
<FullScreenDialog open={showDeleteDialog} onOpenChange={handleDialogOpenChange}>
166+
<FullScreenDialogContent>
167+
<FullScreenDialogHeader>
168+
<FullScreenDialogTitle>
169+
Are you sure you want to delete <br />
170+
<span className="text-white">&quot;{projectName}&quot;</span>?
171+
</FullScreenDialogTitle>
172+
<FullScreenDialogDescription>
173+
This will terminate all resources (databases, sandboxes) and cannot be undone.
174+
</FullScreenDialogDescription>
175+
</FullScreenDialogHeader>
176+
177+
{/* Confirmation Input */}
178+
<div className="space-y-2">
179+
<Label className="text-xs font-semibold text-muted-foreground uppercase tracking-wider pl-1">
180+
Type <span className="text-white select-all">{projectName}</span> to confirm
181+
</Label>
182+
<Input
183+
value={confirmInput}
184+
onChange={(e) => setConfirmInput(e.target.value)}
185+
placeholder={projectName}
186+
className="bg-background border-border rounded-xl px-4 py-3 text-white placeholder:text-muted-foreground/30 focus-visible:border-red-500 focus-visible:ring-red-500/50 font-mono text-sm shadow-inner"
187+
/>
188+
</div>
189+
190+
<FullScreenDialogFooter>
191+
<FullScreenDialogClose>
192+
Cancel
193+
</FullScreenDialogClose>
194+
<FullScreenDialogAction
195+
onClick={handleDeleteConfirm}
196+
disabled={!isConfirmValid}
197+
variant="destructive"
198+
>
199+
Permanently Delete
200+
</FullScreenDialogAction>
201+
</FullScreenDialogFooter>
202+
</FullScreenDialogContent>
203+
</FullScreenDialog>
204+
</>
205+
);
206+
}

0 commit comments

Comments
 (0)