+
๐ {buildLogsData.version}
-
+
{buildLogsData.status}
)}
-
+
{buildLogsData?.buildLog || (
buildLogsData?.status === 'building'
? 'Build in progress... Logs will appear when complete.'
diff --git a/frontend/src/app/dashboard/apps/page.tsx b/frontend/src/app/dashboard/apps/page.tsx
index 116b3dc..9d08415 100644
--- a/frontend/src/app/dashboard/apps/page.tsx
+++ b/frontend/src/app/dashboard/apps/page.tsx
@@ -7,13 +7,13 @@ import toast from 'react-hot-toast';
import type { Application } from '@/types';
const statusColors: Record = {
- running: 'bg-green-100 text-green-700',
- pending: 'bg-yellow-100 text-yellow-700',
- building: 'bg-blue-100 text-blue-700',
- deploying: 'bg-blue-100 text-blue-700',
- failed: 'bg-red-100 text-red-700',
- build_failed: 'bg-red-100 text-red-700',
- stopped: 'bg-gray-100 text-gray-700',
+ running: 'badge-green',
+ pending: 'badge-yellow',
+ building: 'badge-blue',
+ deploying: 'badge-blue',
+ failed: 'badge-red',
+ build_failed: 'badge-red',
+ stopped: 'badge-gray',
};
export default function AppsPage() {
@@ -34,73 +34,140 @@ export default function AppsPage() {
});
if (isLoading) {
- return Loading applications...
;
+ return (
+
+
+
+ {[1, 2, 3, 4].map((i) => (
+
+ ))}
+
+
+ );
}
return (
-
-
My Applications
+
+
+
My Applications
+
{apps.length} application{apps.length !== 1 ? 's' : ''}
+
- + New Application
+ ๐ New Application
{apps.length === 0 ? (
-
No applications yet
-
- Deploy your first app
+
๐ฆ
+
No applications yet
+
Deploy your first application to get started.
+
+ ๐ Deploy your first app
) : (
-
-
-
-
- | Name |
- Runtime |
- Database |
- Status |
- Replicas |
- Actions |
-
-
-
- {apps.map((app) => {
- const latestStatus = app.deployments?.[0]?.status || 'pending';
- return (
-
- |
-
- {app.name}
-
- |
- {app.runtime} |
- {app.databaseType} |
-
-
- {latestStatus}
-
- |
- {app.replicas} |
-
-
- View
-
-
- |
-
- );
- })}
-
-
-
+ <>
+ {/* Desktop Table */}
+
+
+
+
+ | Application |
+ Runtime |
+ Database |
+ Status |
+ Replicas |
+ Actions |
+
+
+
+ {apps.map((app) => {
+ const latestStatus = app.deployments?.[0]?.status || 'pending';
+ return (
+
+ |
+
+
+ {app.runtime === 'nodejs' ? '๐ฉ' : '๐ง'}
+
+
+ {app.name}
+
+
+ |
+ {app.runtime} |
+ {app.databaseType} |
+
+
+ {latestStatus}
+
+ |
+ {app.replicas} |
+
+
+
+ View
+
+
+
+ |
+
+ );
+ })}
+
+
+
+
+ {/* Mobile Cards */}
+
+ {apps.map((app) => {
+ const latestStatus = app.deployments?.[0]?.status || 'pending';
+ return (
+
+
+
+
+ {app.runtime === 'nodejs' ? '๐ฉ' : '๐ง'}
+
+
+
{app.name}
+
{app.runtime}
+
+
+
+ {latestStatus}
+
+
+
+ ๐พ {app.databaseType}
+ ๐ฆ {app.replicas} replica{app.replicas > 1 ? 's' : ''}
+
+
+ );
+ })}
+
+ >
)}
);
diff --git a/frontend/src/app/dashboard/deploy/page.tsx b/frontend/src/app/dashboard/deploy/page.tsx
index 45a5345..17a5253 100644
--- a/frontend/src/app/dashboard/deploy/page.tsx
+++ b/frontend/src/app/dashboard/deploy/page.tsx
@@ -140,25 +140,37 @@ export default function DeployPage() {
};
return (
-
+
-
Deploy New Application
-
Follow the steps to deploy your app to the cloud.
+
Deploy New Application
+
Follow the steps to deploy your app to the cloud.
{/* Step indicator */}
-
+
{steps.map((label, i) => (
-
-
- {i + 1}
+
+
+
+ {i < step ? 'โ' : i + 1}
+
+
+ {label}
+
-
- {label}
-
- {i < steps.length - 1 &&
}
+ {i < steps.length - 1 && (
+
+ )}
))}
@@ -166,8 +178,8 @@ export default function DeployPage() {
{/* Step 0: Basic Info */}
{step === 0 && (
-
-
Basic Information
+
+
Basic Information
-
Runtime & Database
+
Runtime & Database
-
+
{[
{ value: 'nodejs', label: 'Node.js', icon: '๐ฉ', desc: 'Express, NestJS, Fastify...' },
{ value: 'laravel', label: 'Laravel', icon: '๐ง', desc: 'PHP 8.3, Composer, Artisan' },
@@ -351,7 +363,7 @@ export default function DeployPage() {
-
+
{[
{ value: 'none', label: 'None', icon: 'โ' },
{ value: 'postgresql', label: 'PostgreSQL', icon: '๐' },
@@ -377,12 +389,12 @@ export default function DeployPage() {
{/* Step 2: Resources */}
{step === 2 && (
-
Resources & Configuration
+
Resources & Configuration
{/* Cluster Assignment Mode */}
-
+
-
+
-
+
-
+
setEnvVal(e.target.value)}
/>
-
+
{Object.entries(form.envVars || {}).map(([key, value]) => (
@@ -636,8 +648,8 @@ export default function DeployPage() {
{/* Step 3: Review */}
{step === 3 && (
-
Review & Deploy
-
+
Review & Deploy
+
Name
{form.name}
@@ -709,11 +721,11 @@ export default function DeployPage() {
)}
{/* Navigation */}
-
+
@@ -721,7 +733,7 @@ export default function DeployPage() {
@@ -729,12 +741,12 @@ export default function DeployPage() {
)}
diff --git a/frontend/src/app/dashboard/layout.tsx b/frontend/src/app/dashboard/layout.tsx
index 16f1708..eb2f39d 100644
--- a/frontend/src/app/dashboard/layout.tsx
+++ b/frontend/src/app/dashboard/layout.tsx
@@ -1,6 +1,6 @@
'use client';
-import { useEffect } from 'react';
+import { useEffect, useState } from 'react';
import { useRouter, usePathname } from 'next/navigation';
import Link from 'next/link';
import { useAuthStore } from '@/lib/store';
@@ -21,6 +21,7 @@ export default function DashboardLayout({ children }: { children: React.ReactNod
const { user, isAuthenticated, isLoading, logout } = useAuthStore();
const router = useRouter();
const pathname = usePathname();
+ const [sidebarOpen, setSidebarOpen] = useState(false);
useEffect(() => {
if (!isLoading && !isAuthenticated) {
@@ -28,95 +29,154 @@ export default function DashboardLayout({ children }: { children: React.ReactNod
}
}, [isLoading, isAuthenticated, router]);
+ // Close sidebar on route change (mobile)
+ useEffect(() => {
+ setSidebarOpen(false);
+ }, [pathname]);
+
if (isLoading) {
return (
-
-
+
);
}
if (!isAuthenticated) return null;
+ const NavLink = ({ item }: { item: { href: string; label: string; icon: string } }) => {
+ const isActive = pathname === item.href || (item.href !== '/dashboard' && pathname.startsWith(item.href + '/'));
+ return (
+
+
{item.icon}
+
{item.label}
+
+ );
+ };
+
+ const SidebarContent = () => (
+
+
+ {userNavItems.map((item) => (
+
+ ))}
+
+ {user?.role === 'admin' && (
+ <>
+
+ {adminNavItems.map((item) => (
+
+ ))}
+ >
+ )}
+
+
+ {/* Sidebar footer */}
+
+
+
+ {user?.firstName} {user?.lastName}
+
+
{user?.email}
+
+
+
+ );
+
return (
+ {/* Mobile overlay */}
+ {sidebarOpen && (
+
setSidebarOpen(false)}
+ />
+ )}
+
+ {/* Mobile sidebar */}
+
+
{/* Header */}
-
+
-
-
- โ๏ธ CloudHost
+
+ {/* Mobile hamburger */}
+
+
+
โ๏ธ
+
CloudHost
-
-
- {user?.firstName} {user?.lastName}
+
+
+
+ {user?.firstName} {user?.lastName}
+
{user?.role === 'admin' && (
-
- Admin
-
+ Admin
)}
-
+
-
+
- {/* Sidebar */}
-
diff --git a/frontend/src/app/dashboard/page.tsx b/frontend/src/app/dashboard/page.tsx
index 13aab14..5f94b86 100644
--- a/frontend/src/app/dashboard/page.tsx
+++ b/frontend/src/app/dashboard/page.tsx
@@ -7,13 +7,18 @@ import { useAuthStore } from '@/lib/store';
import type { Application } from '@/types';
const statusColors: Record
= {
- running: 'bg-green-100 text-green-700',
- pending: 'bg-yellow-100 text-yellow-700',
+ running: 'bg-emerald-100 text-emerald-700',
+ pending: 'bg-amber-100 text-amber-700',
building: 'bg-blue-100 text-blue-700',
deploying: 'bg-blue-100 text-blue-700',
failed: 'bg-red-100 text-red-700',
build_failed: 'bg-red-100 text-red-700',
- stopped: 'bg-gray-100 text-gray-700',
+ stopped: 'bg-gray-100 text-gray-600',
+};
+
+const statusIcons: Record = {
+ running: '๐ข', pending: '๐ก', building: '๐ต', deploying: '๐ต',
+ failed: '๐ด', build_failed: '๐ด', stopped: 'โช',
};
export default function DashboardPage() {
@@ -27,59 +32,79 @@ export default function DashboardPage() {
const runningApps = apps.filter(
(a) => a.deployments?.some((d) => d.status === 'running'),
);
+ const failedApps = apps.filter(
+ (a) => a.deployments?.some((d) => d.status === 'failed' || d.status === 'build_failed'),
+ );
return (
+ {/* Welcome */}
-
+
Welcome back, {user?.firstName}! ๐
-
+
Here's an overview of your applications.
{/* Stats */}
-
-
-
Total Apps
-
{apps.length}
+
+
+
Total Apps
+
{isLoading ? 'โ' : apps.length}
-
-
Running
-
{runningApps.length}
+
+
Running
+
{isLoading ? 'โ' : runningApps.length}
-
-
Deployments (7d)
-
- {apps.reduce((sum, a) => sum + (a.deployments?.length || 0), 0)}
-
+
+
Failed
+
{isLoading ? 'โ' : failedApps.length}
+
+
+
Total Deploys
+
+ {isLoading ? 'โ' : apps.reduce((sum, a) => sum + (a.deployments?.length || 0), 0)}
+
{/* Recent Applications */}
-
+
Recent Applications
- + New Application
+ ๐ New Application
{isLoading ? (
-
Loading...
+
+ {[1, 2, 3].map((i) => (
+
+ ))}
+
) : apps.length === 0 ? (
-
-
No applications yet
-
+
+
๐ฆ
+
No applications yet
+
Deploy your first application to get started.
-
- Deploy your first app
+
+ ๐ Deploy your first app
) : (
-
+
{apps.slice(0, 5).map((app) => {
const latestDeploy = app.deployments?.[0];
const status = latestDeploy?.status || 'pending';
@@ -87,30 +112,39 @@ export default function DashboardPage() {
-
-
+
+
{app.runtime === 'nodejs' ? '๐ฉ' : '๐ง'}
-
-
{app.name}
-
+
+
+ {app.name}
+
+
{app.runtime} ยท {app.replicas} replica{app.replicas > 1 ? 's' : ''}
{app.databaseType !== 'none' && ` ยท ${app.databaseType}`}
-
- {status}
-
+
+ {statusIcons[status]}
+
+ {status}
+
+
);
})}
+ {apps.length > 5 && (
+
+ View all {apps.length} applications โ
+
+ )}
)}
diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css
index 6d6f94f..4a18367 100644
--- a/frontend/src/app/globals.css
+++ b/frontend/src/app/globals.css
@@ -6,34 +6,135 @@
body {
@apply bg-gray-50 text-gray-900 antialiased;
}
+
+ /* Smooth scrollbar */
+ ::-webkit-scrollbar {
+ @apply w-1.5 h-1.5;
+ }
+ ::-webkit-scrollbar-track {
+ @apply bg-transparent;
+ }
+ ::-webkit-scrollbar-thumb {
+ @apply bg-gray-300 rounded-full hover:bg-gray-400;
+ }
}
@layer components {
+ /* โโโ Buttons โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
.btn-primary {
- @apply bg-primary-600 text-white px-4 py-2 rounded-lg font-medium
- hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-primary-500
- focus:ring-offset-2 transition-colors disabled:opacity-50 disabled:cursor-not-allowed;
+ @apply inline-flex items-center justify-center gap-2 bg-primary-600 text-white
+ px-4 py-2.5 rounded-xl font-semibold text-sm
+ hover:bg-primary-700 active:scale-[0.98]
+ focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2
+ transition-all duration-150 disabled:opacity-50 disabled:cursor-not-allowed
+ shadow-sm hover:shadow-md;
}
.btn-secondary {
- @apply bg-white text-gray-700 px-4 py-2 rounded-lg font-medium border border-gray-300
- hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-primary-500
- focus:ring-offset-2 transition-colors;
+ @apply inline-flex items-center justify-center gap-2 bg-white text-gray-700
+ px-4 py-2.5 rounded-xl font-semibold text-sm border border-gray-300
+ hover:bg-gray-50 active:scale-[0.98]
+ focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2
+ transition-all duration-150 shadow-sm;
}
.btn-danger {
- @apply bg-red-600 text-white px-4 py-2 rounded-lg font-medium
- hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500
- focus:ring-offset-2 transition-colors;
+ @apply inline-flex items-center justify-center gap-2 bg-red-600 text-white
+ px-4 py-2.5 rounded-xl font-semibold text-sm
+ hover:bg-red-700 active:scale-[0.98]
+ focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2
+ transition-all duration-150 shadow-sm hover:shadow-md
+ disabled:opacity-50 disabled:cursor-not-allowed;
}
+ .btn-ghost {
+ @apply inline-flex items-center justify-center gap-2 text-gray-600
+ px-3 py-2 rounded-xl font-medium text-sm
+ hover:bg-gray-100 active:scale-[0.98]
+ focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2
+ transition-all duration-150;
+ }
+
+ .btn-icon {
+ @apply inline-flex items-center justify-center w-9 h-9 rounded-xl
+ text-gray-500 hover:bg-gray-100 hover:text-gray-700
+ active:scale-[0.95] transition-all duration-150;
+ }
+
+ /* โโโ Inputs โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
.input-field {
- @apply w-full px-3 py-2 border border-gray-300 rounded-lg shadow-sm
- placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-primary-500
- focus:border-primary-500 transition-colors;
+ @apply w-full px-3.5 py-2.5 border border-gray-300 rounded-xl shadow-sm text-sm
+ placeholder-gray-400 bg-white
+ focus:outline-none focus:ring-2 focus:ring-primary-500/20 focus:border-primary-500
+ transition-all duration-150;
}
+ /* โโโ Cards โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
.card {
- @apply bg-white rounded-xl shadow-sm border border-gray-200 p-6;
+ @apply bg-white rounded-2xl shadow-sm border border-gray-200/80 p-6
+ transition-shadow duration-200;
+ }
+ .card-hover {
+ @apply card hover:shadow-md hover:border-gray-300;
+ }
+
+ /* โโโ Badges โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
+ .badge {
+ @apply inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-semibold;
+ }
+ .badge-green { @apply badge bg-emerald-100 text-emerald-700; }
+ .badge-red { @apply badge bg-red-100 text-red-700; }
+ .badge-blue { @apply badge bg-blue-100 text-blue-700; }
+ .badge-yellow { @apply badge bg-amber-100 text-amber-700; }
+ .badge-gray { @apply badge bg-gray-100 text-gray-600; }
+ .badge-purple { @apply badge bg-purple-100 text-purple-700; }
+
+ /* โโโ Stat card โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
+ .stat-card {
+ @apply bg-white rounded-2xl border border-gray-200/80 p-5 shadow-sm
+ flex flex-col gap-1;
+ }
+ .stat-value {
+ @apply text-3xl font-bold tracking-tight;
+ }
+ .stat-label {
+ @apply text-sm font-medium text-gray-500;
+ }
+
+ /* โโโ Table โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
+ .table-wrapper {
+ @apply overflow-hidden rounded-2xl border border-gray-200/80 bg-white shadow-sm;
+ }
+
+ /* โโโ Loading skeleton โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
+ .skeleton {
+ @apply animate-pulse bg-gray-200 rounded-xl;
+ }
+
+ /* โโโ Page header โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
+ .page-header {
+ @apply flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4;
+ }
+ .page-title {
+ @apply text-2xl font-bold text-gray-900 tracking-tight;
+ }
+ .page-subtitle {
+ @apply text-sm text-gray-500 mt-0.5;
}
}
+
+/* โโโ Animations โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
+@keyframes fadeIn {
+ from { opacity: 0; transform: translateY(8px); }
+ to { opacity: 1; transform: translateY(0); }
+}
+@keyframes slideUp {
+ from { opacity: 0; transform: translateY(16px); }
+ to { opacity: 1; transform: translateY(0); }
+}
+.animate-fade-in {
+ animation: fadeIn 0.3s ease-out;
+}
+.animate-slide-up {
+ animation: slideUp 0.4s ease-out;
+}
diff --git a/frontend/src/app/login/page.tsx b/frontend/src/app/login/page.tsx
index 136a5fd..1cdb305 100644
--- a/frontend/src/app/login/page.tsx
+++ b/frontend/src/app/login/page.tsx
@@ -28,22 +28,21 @@ export default function LoginPage() {
};
return (
-
-
+
+
-
โ๏ธ CloudHost
-
Sign in to your account
-
- Or{' '}
-
- create a new account
-
+
+ โ๏ธ
+
+
Welcome back
+
+ Sign in to your CloudHost account
-