Localize dashboard shell and home page.
Convert the dashboard layout (sidebar nav, header, role badges, wallet, sign-out, language switcher) and the dashboard home page to the i18n dictionaries, including a shared deployment-status label map. Switch their links/router to the locale-aware helpers and add RTL-aware spacing. Remaining dashboard page bodies and shared components still render English copy and will be localized in follow-up commits. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,11 +1,14 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffect, useState, type ReactNode } from 'react';
|
import { useEffect, useState, type ReactNode } from 'react';
|
||||||
import { useRouter, usePathname } from 'next/navigation';
|
|
||||||
import Link from 'next/link';
|
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { useAuthStore } from '@/lib/store';
|
import { useAuthStore } from '@/lib/store';
|
||||||
import api from '@/lib/api';
|
import api from '@/lib/api';
|
||||||
|
import { Link } from '@/i18n/Link';
|
||||||
|
import { useLocalizedRouter, usePathname } from '@/i18n/navigation';
|
||||||
|
import { useT } from '@/i18n/I18nProvider';
|
||||||
|
import { LanguageSwitcher } from '@/i18n/LanguageSwitcher';
|
||||||
|
import type { Dictionary } from '@/i18n/dictionaries/fa';
|
||||||
import { DeploymentProgressManager } from '@/components/deployment-progress-manager';
|
import { DeploymentProgressManager } from '@/components/deployment-progress-manager';
|
||||||
import { useDeployProgressStore } from '@/lib/deploy-progress-store';
|
import { useDeployProgressStore } from '@/lib/deploy-progress-store';
|
||||||
import {
|
import {
|
||||||
@@ -31,43 +34,45 @@ import {
|
|||||||
Database,
|
Database,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
type NavItem = { href: string; label: string; icon: ReactNode };
|
type NavKey = keyof Dictionary['nav'];
|
||||||
|
type NavItem = { href: string; labelKey: NavKey; icon: ReactNode };
|
||||||
|
|
||||||
const userNavItems: NavItem[] = [
|
const userNavItems: NavItem[] = [
|
||||||
{ href: '/dashboard', label: 'Dashboard', icon: <LayoutDashboard className="w-4 h-4" /> },
|
{ href: '/dashboard', labelKey: 'dashboard', icon: <LayoutDashboard className="w-4 h-4" /> },
|
||||||
{ href: '/dashboard/apps', label: 'Applications', icon: <Package className="w-4 h-4" /> },
|
{ href: '/dashboard/apps', labelKey: 'applications', icon: <Package className="w-4 h-4" /> },
|
||||||
{ href: '/dashboard/services', label: 'Databases & Services', icon: <Database className="w-4 h-4" /> },
|
{ href: '/dashboard/services', labelKey: 'services', icon: <Database className="w-4 h-4" /> },
|
||||||
{ href: '/dashboard/logs', label: 'Logs', icon: <ScrollText className="w-4 h-4" /> },
|
{ href: '/dashboard/logs', labelKey: 'logs', icon: <ScrollText className="w-4 h-4" /> },
|
||||||
{ href: '/dashboard/deploy', label: 'New Deploy', icon: <Rocket className="w-4 h-4" /> },
|
{ href: '/dashboard/deploy', labelKey: 'newDeploy', icon: <Rocket className="w-4 h-4" /> },
|
||||||
{ href: '/dashboard/wallet', label: 'Wallet', icon: <Wallet className="w-4 h-4" /> },
|
{ href: '/dashboard/wallet', labelKey: 'wallet', icon: <Wallet className="w-4 h-4" /> },
|
||||||
{ href: '/dashboard/invoices', label: 'Invoices', icon: <FileText className="w-4 h-4" /> },
|
{ href: '/dashboard/invoices', labelKey: 'invoices', icon: <FileText className="w-4 h-4" /> },
|
||||||
{ href: '/dashboard/tickets', label: 'Tickets', icon: <Ticket className="w-4 h-4" /> },
|
{ href: '/dashboard/tickets', labelKey: 'tickets', icon: <Ticket className="w-4 h-4" /> },
|
||||||
];
|
];
|
||||||
|
|
||||||
const adminNavItems: NavItem[] = [
|
const adminNavItems: NavItem[] = [
|
||||||
{ href: '/dashboard/admin/users', label: 'Users', icon: <Users className="w-4 h-4" /> },
|
{ href: '/dashboard/admin/users', labelKey: 'users', icon: <Users className="w-4 h-4" /> },
|
||||||
{ href: '/dashboard/admin/apps', label: 'All Applications', icon: <Boxes className="w-4 h-4" /> },
|
{ href: '/dashboard/admin/apps', labelKey: 'allApplications', icon: <Boxes className="w-4 h-4" /> },
|
||||||
{ href: '/dashboard/admin/billing', label: 'Billing Plans', icon: <CreditCard className="w-4 h-4" /> },
|
{ href: '/dashboard/admin/billing', labelKey: 'billingPlans', icon: <CreditCard className="w-4 h-4" /> },
|
||||||
{ href: '/dashboard/admin/invoices', label: 'Invoices', icon: <FileText className="w-4 h-4" /> },
|
{ href: '/dashboard/admin/invoices', labelKey: 'invoices', icon: <FileText className="w-4 h-4" /> },
|
||||||
{ href: '/dashboard/admin/clusters', label: 'Clusters', icon: <Server className="w-4 h-4" /> },
|
{ href: '/dashboard/admin/clusters', labelKey: 'clusters', icon: <Server className="w-4 h-4" /> },
|
||||||
{ href: '/dashboard/admin/pools', label: 'Cluster Pools', icon: <Scale className="w-4 h-4" /> },
|
{ href: '/dashboard/admin/pools', labelKey: 'clusterPools', icon: <Scale className="w-4 h-4" /> },
|
||||||
{ href: '/dashboard/admin/tickets', label: 'All Tickets', icon: <ClipboardList className="w-4 h-4" /> },
|
{ href: '/dashboard/admin/tickets', labelKey: 'allTickets', icon: <ClipboardList className="w-4 h-4" /> },
|
||||||
];
|
];
|
||||||
|
|
||||||
const technicalNavItems: NavItem[] = [
|
const technicalNavItems: NavItem[] = [
|
||||||
{ href: '/dashboard/admin/users', label: 'Users', icon: <Users className="w-4 h-4" /> },
|
{ href: '/dashboard/admin/users', labelKey: 'users', icon: <Users className="w-4 h-4" /> },
|
||||||
{ href: '/dashboard/admin/apps', label: 'All Applications', icon: <Boxes className="w-4 h-4" /> },
|
{ href: '/dashboard/admin/apps', labelKey: 'allApplications', icon: <Boxes className="w-4 h-4" /> },
|
||||||
{ href: '/dashboard/staff/tickets', label: 'Technical Tickets', icon: <Wrench className="w-4 h-4" /> },
|
{ href: '/dashboard/staff/tickets', labelKey: 'technicalTickets', icon: <Wrench className="w-4 h-4" /> },
|
||||||
];
|
];
|
||||||
|
|
||||||
const salesNavItems: NavItem[] = [
|
const salesNavItems: NavItem[] = [
|
||||||
{ href: '/dashboard/admin/users', label: 'Users', icon: <Users className="w-4 h-4" /> },
|
{ href: '/dashboard/admin/users', labelKey: 'users', icon: <Users className="w-4 h-4" /> },
|
||||||
{ href: '/dashboard/staff/tickets', label: 'Sales Tickets', icon: <Briefcase className="w-4 h-4" /> },
|
{ href: '/dashboard/staff/tickets', labelKey: 'salesTickets', icon: <Briefcase className="w-4 h-4" /> },
|
||||||
];
|
];
|
||||||
|
|
||||||
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
|
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
|
||||||
|
const t = useT();
|
||||||
const { user, isAuthenticated, isLoading, logout } = useAuthStore();
|
const { user, isAuthenticated, isLoading, logout } = useAuthStore();
|
||||||
const router = useRouter();
|
const router = useLocalizedRouter();
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const [sidebarOpen, setSidebarOpen] = useState(false);
|
const [sidebarOpen, setSidebarOpen] = useState(false);
|
||||||
const deployBarMinimized = useDeployProgressStore((s) => s.minimized);
|
const deployBarMinimized = useDeployProgressStore((s) => s.minimized);
|
||||||
@@ -106,7 +111,7 @@ export default function DashboardLayout({ children }: { children: React.ReactNod
|
|||||||
<div className="min-h-screen flex items-center justify-center bg-gray-50">
|
<div className="min-h-screen flex items-center justify-center bg-gray-50">
|
||||||
<div className="flex flex-col items-center gap-3">
|
<div className="flex flex-col items-center gap-3">
|
||||||
<div className="animate-spin rounded-full h-10 w-10 border-2 border-primary-600 border-t-transparent" />
|
<div className="animate-spin rounded-full h-10 w-10 border-2 border-primary-600 border-t-transparent" />
|
||||||
<span className="text-sm text-gray-500">Loading...</span>
|
<span className="text-sm text-gray-500">{t.common.loading}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -126,7 +131,7 @@ export default function DashboardLayout({ children }: { children: React.ReactNod
|
|||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{item.icon}
|
{item.icon}
|
||||||
<span className="flex-1">{item.label}</span>
|
<span className="flex-1">{t.nav[item.labelKey]}</span>
|
||||||
{badge !== undefined && badge > 0 && (
|
{badge !== undefined && badge > 0 && (
|
||||||
<span className="min-w-[20px] h-5 flex items-center justify-center px-1.5 text-xs font-bold rounded-full bg-red-500 text-white">
|
<span className="min-w-[20px] h-5 flex items-center justify-center px-1.5 text-xs font-bold rounded-full bg-red-500 text-white">
|
||||||
{badge}
|
{badge}
|
||||||
@@ -160,7 +165,7 @@ export default function DashboardLayout({ children }: { children: React.ReactNod
|
|||||||
<>
|
<>
|
||||||
<div className="pt-5 pb-2">
|
<div className="pt-5 pb-2">
|
||||||
<p className="px-3 text-[11px] font-bold text-gray-400 uppercase tracking-widest">
|
<p className="px-3 text-[11px] font-bold text-gray-400 uppercase tracking-widest">
|
||||||
Admin
|
{t.nav.sectionAdmin}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{adminNavItems.map((item) => (
|
{adminNavItems.map((item) => (
|
||||||
@@ -173,7 +178,7 @@ export default function DashboardLayout({ children }: { children: React.ReactNod
|
|||||||
<>
|
<>
|
||||||
<div className="pt-5 pb-2">
|
<div className="pt-5 pb-2">
|
||||||
<p className="px-3 text-[11px] font-bold text-gray-400 uppercase tracking-widest">
|
<p className="px-3 text-[11px] font-bold text-gray-400 uppercase tracking-widest">
|
||||||
Technical
|
{t.nav.sectionTechnical}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{technicalNavItems.map((item) => (
|
{technicalNavItems.map((item) => (
|
||||||
@@ -186,7 +191,7 @@ export default function DashboardLayout({ children }: { children: React.ReactNod
|
|||||||
<>
|
<>
|
||||||
<div className="pt-5 pb-2">
|
<div className="pt-5 pb-2">
|
||||||
<p className="px-3 text-[11px] font-bold text-gray-400 uppercase tracking-widest">
|
<p className="px-3 text-[11px] font-bold text-gray-400 uppercase tracking-widest">
|
||||||
Sales
|
{t.nav.sectionSales}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{salesNavItems.map((item) => (
|
{salesNavItems.map((item) => (
|
||||||
@@ -221,13 +226,13 @@ export default function DashboardLayout({ children }: { children: React.ReactNod
|
|||||||
|
|
||||||
{/* Mobile sidebar */}
|
{/* Mobile sidebar */}
|
||||||
<aside
|
<aside
|
||||||
className={`fixed inset-y-0 left-0 z-50 w-64 bg-white border-r border-gray-200 p-4 transform transition-transform duration-200 ease-in-out lg:hidden ${
|
className={`fixed inset-y-0 left-0 rtl:left-auto rtl:right-0 z-50 w-64 bg-white border-r rtl:border-r-0 rtl:border-l border-gray-200 p-4 transform transition-transform duration-200 ease-in-out lg:hidden ${
|
||||||
sidebarOpen ? 'translate-x-0' : '-translate-x-full'
|
sidebarOpen ? 'translate-x-0' : '-translate-x-full rtl:translate-x-full'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between mb-6">
|
<div className="flex items-center justify-between mb-6">
|
||||||
<Link href="/dashboard" className="text-lg font-bold text-primary-600">
|
<Link href="/dashboard" className="text-lg font-bold text-primary-600">
|
||||||
<Cloud className="w-5 h-5 inline mr-1" /> CloudHost
|
<Cloud className="w-5 h-5 inline mr-1 rtl:mr-0 rtl:ml-1" /> {t.common.appName}
|
||||||
</Link>
|
</Link>
|
||||||
<button onClick={() => setSidebarOpen(false)} className="btn-icon">
|
<button onClick={() => setSidebarOpen(false)} className="btn-icon">
|
||||||
<X className="w-4 h-4" />
|
<X className="w-4 h-4" />
|
||||||
@@ -254,7 +259,7 @@ export default function DashboardLayout({ children }: { children: React.ReactNod
|
|||||||
</button>
|
</button>
|
||||||
<Link href="/dashboard" className="text-xl font-bold text-primary-600 flex items-center gap-2">
|
<Link href="/dashboard" className="text-xl font-bold text-primary-600 flex items-center gap-2">
|
||||||
<Cloud className="w-6 h-6" />
|
<Cloud className="w-6 h-6" />
|
||||||
<span className="hidden sm:inline">CloudHost</span>
|
<span className="hidden sm:inline">{t.common.appName}</span>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
@@ -262,27 +267,29 @@ export default function DashboardLayout({ children }: { children: React.ReactNod
|
|||||||
<Link
|
<Link
|
||||||
href="/dashboard/wallet"
|
href="/dashboard/wallet"
|
||||||
className="hidden sm:flex items-center gap-1.5 px-3 py-1.5 rounded-xl bg-gray-100 hover:bg-gray-200 transition-colors text-sm"
|
className="hidden sm:flex items-center gap-1.5 px-3 py-1.5 rounded-xl bg-gray-100 hover:bg-gray-200 transition-colors text-sm"
|
||||||
title="Wallet Balance"
|
title={t.nav.walletBalanceTitle}
|
||||||
>
|
>
|
||||||
<Wallet className="w-3.5 h-3.5 text-primary-600" />
|
<Wallet className="w-3.5 h-3.5 text-primary-600" />
|
||||||
<span className="font-bold text-gray-800">
|
<span className="font-bold text-gray-800">
|
||||||
{walletData ? Number(walletData.balance).toLocaleString('en-US') : '...'}
|
{walletData ? Number(walletData.balance).toLocaleString('en-US') : '...'}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-gray-500 text-xs">T</span>
|
<span className="text-gray-500 text-xs">{t.common.currencyShort}</span>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
|
<LanguageSwitcher className="text-gray-600 hover:bg-gray-100 hover:text-gray-900" />
|
||||||
|
|
||||||
<div className="hidden sm:flex items-center gap-2 text-sm">
|
<div className="hidden sm:flex items-center gap-2 text-sm">
|
||||||
<span className="text-gray-600 font-medium">
|
<span className="text-gray-600 font-medium">
|
||||||
{user?.firstName} {user?.lastName}
|
{user?.firstName} {user?.lastName}
|
||||||
</span>
|
</span>
|
||||||
{user?.role === 'admin' && (
|
{user?.role === 'admin' && (
|
||||||
<span className="badge-purple">Admin</span>
|
<span className="badge-purple">{t.roles.admin}</span>
|
||||||
)}
|
)}
|
||||||
{user?.role === 'technical' && (
|
{user?.role === 'technical' && (
|
||||||
<span className="px-2 py-0.5 text-xs font-semibold rounded-full bg-blue-100 text-blue-700">Technical</span>
|
<span className="px-2 py-0.5 text-xs font-semibold rounded-full bg-blue-100 text-blue-700">{t.roles.technical}</span>
|
||||||
)}
|
)}
|
||||||
{user?.role === 'sales' && (
|
{user?.role === 'sales' && (
|
||||||
<span className="px-2 py-0.5 text-xs font-semibold rounded-full bg-green-100 text-green-700">Sales</span>
|
<span className="px-2 py-0.5 text-xs font-semibold rounded-full bg-green-100 text-green-700">{t.roles.sales}</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
@@ -290,7 +297,7 @@ export default function DashboardLayout({ children }: { children: React.ReactNod
|
|||||||
className="btn-ghost text-gray-500 hover:text-red-600"
|
className="btn-ghost text-gray-500 hover:text-red-600"
|
||||||
>
|
>
|
||||||
<LogOut className="w-4 h-4 sm:hidden" />
|
<LogOut className="w-4 h-4 sm:hidden" />
|
||||||
<span className="hidden sm:inline">Sign out</span>
|
<span className="hidden sm:inline">{t.common.signOut}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import Link from 'next/link';
|
|
||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
|
import { Link } from '@/i18n/Link';
|
||||||
|
import { useT } from '@/i18n/I18nProvider';
|
||||||
|
import type { Dictionary } from '@/i18n/dictionaries/fa';
|
||||||
import api from '@/lib/api';
|
import api from '@/lib/api';
|
||||||
import { useAuthStore } from '@/lib/store';
|
import { useAuthStore } from '@/lib/store';
|
||||||
import type { Application } from '@/types';
|
import type { Application } from '@/types';
|
||||||
@@ -34,6 +36,10 @@ const statusIcons: Record<string, ReactNode> = {
|
|||||||
stopped: <Circle className="w-3 h-3 fill-gray-400 text-gray-400" />,
|
stopped: <Circle className="w-3 h-3 fill-gray-400 text-gray-400" />,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function statusLabel(status: string, t: Dictionary): string {
|
||||||
|
return (t.dashboard.status as Record<string, string>)[status] ?? status;
|
||||||
|
}
|
||||||
|
|
||||||
function countRunning(apps: Application[]) {
|
function countRunning(apps: Application[]) {
|
||||||
return apps.filter((a) => a.deployments?.some((d) => d.status === 'running')).length;
|
return apps.filter((a) => a.deployments?.some((d) => d.status === 'running')).length;
|
||||||
}
|
}
|
||||||
@@ -58,6 +64,8 @@ function serviceSubtitle(app: Application): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function DashboardPage() {
|
export default function DashboardPage() {
|
||||||
|
const t = useT();
|
||||||
|
const h = t.dashboard.home;
|
||||||
const user = useAuthStore((s) => s.user);
|
const user = useAuthStore((s) => s.user);
|
||||||
|
|
||||||
const { data: appsRaw = [], isLoading: appsLoading } = useQuery<Application[]>({
|
const { data: appsRaw = [], isLoading: appsLoading } = useQuery<Application[]>({
|
||||||
@@ -87,8 +95,8 @@ export default function DashboardPage() {
|
|||||||
return (
|
return (
|
||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="page-title">Welcome back, {user?.firstName}!</h1>
|
<h1 className="page-title">{h.welcome.replace('{name}', user?.firstName ?? '')}</h1>
|
||||||
<p className="page-subtitle">Overview of your applications and managed services.</p>
|
<p className="page-subtitle">{h.overview}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{resourceCredits.length > 0 && (
|
{resourceCredits.length > 0 && (
|
||||||
@@ -96,39 +104,36 @@ export default function DashboardPage() {
|
|||||||
<div className="flex items-start gap-3">
|
<div className="flex items-start gap-3">
|
||||||
<Wallet className="w-5 h-5 text-indigo-600 shrink-0 mt-0.5" />
|
<Wallet className="w-5 h-5 text-indigo-600 shrink-0 mt-0.5" />
|
||||||
<div>
|
<div>
|
||||||
<h2 className="font-semibold text-indigo-900">Prepaid resource credits</h2>
|
<h2 className="font-semibold text-indigo-900">{h.creditsTitle}</h2>
|
||||||
<p className="text-sm text-indigo-700 mt-1">
|
<p className="text-sm text-indigo-700 mt-1">{h.creditsBody}</p>
|
||||||
If you delete an app before your plan ends, you can deploy a new app with the same resources at no
|
|
||||||
extra charge until the credit expires.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid gap-3 sm:grid-cols-2">
|
<div className="grid gap-3 sm:grid-cols-2">
|
||||||
{resourceCredits.map((credit) => (
|
{resourceCredits.map((credit) => (
|
||||||
<div key={credit.id} className="rounded-xl border border-indigo-200 bg-white p-4 text-sm">
|
<div key={credit.id} className="rounded-xl border border-indigo-200 bg-white p-4 text-sm">
|
||||||
<p className="font-semibold text-gray-900">
|
<p className="font-semibold text-gray-900">
|
||||||
{credit.sourceAppName ? `From app “${credit.sourceAppName}”` : 'Resource credit'}
|
{credit.sourceAppName ? h.fromApp.replace('{name}', credit.sourceAppName) : h.resourceCredit}
|
||||||
</p>
|
</p>
|
||||||
<p className="mt-2 inline-flex items-center gap-1 text-indigo-700 font-medium">
|
<p className="mt-2 inline-flex items-center gap-1 text-indigo-700 font-medium">
|
||||||
<Clock className="w-3.5 h-3.5" />
|
<Clock className="w-3.5 h-3.5" />
|
||||||
{credit.remainingLabel} remaining
|
{h.remaining.replace('{label}', credit.remainingLabel)}
|
||||||
</p>
|
</p>
|
||||||
<p className="mt-1 text-xs text-gray-500">
|
<p className="mt-1 text-xs text-gray-500">
|
||||||
Expires {formatExpiresAtLocal(credit.expiresAt)}
|
{h.expires.replace('{date}', formatExpiresAtLocal(credit.expiresAt))}
|
||||||
</p>
|
</p>
|
||||||
<ul className="mt-3 grid grid-cols-2 gap-x-3 gap-y-1 text-xs text-gray-600">
|
<ul className="mt-3 grid grid-cols-2 gap-x-3 gap-y-1 text-xs text-gray-600">
|
||||||
<li>CPU: {credit.cpuLimit}</li>
|
<li>{h.cpu}: {credit.cpuLimit}</li>
|
||||||
<li>RAM: {credit.memoryLimit}</li>
|
<li>{h.ram}: {credit.memoryLimit}</li>
|
||||||
<li>Replicas: {credit.replicas}</li>
|
<li>{h.replicas}: {credit.replicas}</li>
|
||||||
<li>DB: {credit.databaseType}</li>
|
<li>{h.db}: {credit.databaseType}</li>
|
||||||
<li>DB disk: {credit.dbStorageSize}</li>
|
<li>{h.dbDisk}: {credit.dbStorageSize}</li>
|
||||||
<li>App disk: {credit.appStorageSize}</li>
|
<li>{h.appDisk}: {credit.appStorageSize}</li>
|
||||||
{credit.enableRedis && <li>Redis</li>}
|
{credit.enableRedis && <li>Redis</li>}
|
||||||
{credit.enableRabbitmq && <li>RabbitMQ</li>}
|
{credit.enableRabbitmq && <li>RabbitMQ</li>}
|
||||||
{credit.enableElasticsearch && <li>Elasticsearch</li>}
|
{credit.enableElasticsearch && <li>Elasticsearch</li>}
|
||||||
</ul>
|
</ul>
|
||||||
<Link href="/dashboard/deploy" className="mt-3 inline-block text-xs font-medium text-primary-600 hover:underline">
|
<Link href="/dashboard/deploy" className="mt-3 inline-block text-xs font-medium text-primary-600 hover:underline">
|
||||||
Use on new app →
|
{h.useOnNewApp}
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@@ -138,37 +143,37 @@ export default function DashboardPage() {
|
|||||||
|
|
||||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
||||||
<div className="stat-card">
|
<div className="stat-card">
|
||||||
<div className="stat-label">Applications</div>
|
<div className="stat-label">{h.statApplications}</div>
|
||||||
<div className="stat-value text-gray-900">{isLoading ? '—' : apps.length}</div>
|
<div className="stat-value text-gray-900">{isLoading ? '—' : apps.length}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="stat-card">
|
<div className="stat-card">
|
||||||
<div className="stat-label">Apps running</div>
|
<div className="stat-label">{h.statAppsRunning}</div>
|
||||||
<div className="stat-value text-emerald-600">{isLoading ? '—' : runningApps}</div>
|
<div className="stat-value text-emerald-600">{isLoading ? '—' : runningApps}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="stat-card">
|
<div className="stat-card">
|
||||||
<div className="stat-label">Managed services</div>
|
<div className="stat-label">{h.statManagedServices}</div>
|
||||||
<div className="stat-value text-gray-900">{isLoading ? '—' : services.length}</div>
|
<div className="stat-value text-gray-900">{isLoading ? '—' : services.length}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="stat-card">
|
<div className="stat-card">
|
||||||
<div className="stat-label">Services running</div>
|
<div className="stat-label">{h.statServicesRunning}</div>
|
||||||
<div className="stat-value text-emerald-600">{isLoading ? '—' : runningServices}</div>
|
<div className="stat-value text-emerald-600">{isLoading ? '—' : runningServices}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{(failedApps > 0 || failedServices > 0) && (
|
{(failedApps > 0 || failedServices > 0) && (
|
||||||
<div className="text-sm text-red-600 font-medium">
|
<div className="text-sm text-red-600 font-medium">
|
||||||
{failedApps > 0 && `${failedApps} application${failedApps !== 1 ? 's' : ''} failed`}
|
{failedApps > 0 && h.appsFailed.replace('{n}', String(failedApps))}
|
||||||
{failedApps > 0 && failedServices > 0 && ' · '}
|
{failedApps > 0 && failedServices > 0 && ' · '}
|
||||||
{failedServices > 0 && `${failedServices} service${failedServices !== 1 ? 's' : ''} failed`}
|
{failedServices > 0 && h.servicesFailed.replace('{n}', String(failedServices))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Applications */}
|
{/* Applications */}
|
||||||
<div>
|
<div>
|
||||||
<div className="page-header mb-4">
|
<div className="page-header mb-4">
|
||||||
<h2 className="text-lg font-semibold text-gray-900">Recent applications</h2>
|
<h2 className="text-lg font-semibold text-gray-900">{h.recentApplications}</h2>
|
||||||
<Link href="/dashboard/deploy" className="btn-primary text-sm">
|
<Link href="/dashboard/deploy" className="btn-primary text-sm">
|
||||||
<Rocket className="w-4 h-4 mr-1 inline" /> New application
|
<Rocket className="w-4 h-4 mr-1 rtl:mr-0 rtl:ml-1 inline" /> {h.newApplication}
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -187,9 +192,9 @@ export default function DashboardPage() {
|
|||||||
) : apps.length === 0 ? (
|
) : apps.length === 0 ? (
|
||||||
<div className="card text-center py-12">
|
<div className="card text-center py-12">
|
||||||
<Package className="w-10 h-10 mx-auto mb-3 text-gray-300" />
|
<Package className="w-10 h-10 mx-auto mb-3 text-gray-300" />
|
||||||
<p className="text-gray-600 font-medium">No applications yet</p>
|
<p className="text-gray-600 font-medium">{h.noApplications}</p>
|
||||||
<Link href="/dashboard/deploy" className="btn-primary mt-4 inline-flex items-center gap-1 text-sm">
|
<Link href="/dashboard/deploy" className="btn-primary mt-4 inline-flex items-center gap-1 text-sm">
|
||||||
<Rocket className="w-4 h-4" /> Deploy your first app
|
<Rocket className="w-4 h-4" /> {h.deployFirst}
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@@ -212,21 +217,21 @@ export default function DashboardPage() {
|
|||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<h3 className="font-semibold text-gray-900 group-hover:text-primary-700 truncate">{app.name}</h3>
|
<h3 className="font-semibold text-gray-900 group-hover:text-primary-700 truncate">{app.name}</h3>
|
||||||
<p className="text-sm text-gray-500 truncate">
|
<p className="text-sm text-gray-500 truncate">
|
||||||
{app.runtime} · {app.replicas} replica{app.replicas > 1 ? 's' : ''}
|
{app.runtime} · {app.replicas} {app.replicas > 1 ? h.replicaPlural : h.replica}
|
||||||
{app.databaseType !== 'none' && ` · ${app.databaseType}`}
|
{app.databaseType !== 'none' && ` · ${app.databaseType}`}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 shrink-0">
|
<div className="flex items-center gap-2 shrink-0">
|
||||||
{statusIcons[status]}
|
{statusIcons[status]}
|
||||||
<span className={`badge ${statusColors[status] || 'badge-gray'}`}>{status}</span>
|
<span className={`badge ${statusColors[status] || 'badge-gray'}`}>{statusLabel(status, t)}</span>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{apps.length > 5 && (
|
{apps.length > 5 && (
|
||||||
<Link href="/dashboard/apps" className="text-center text-sm text-primary-600 hover:text-primary-700 font-medium py-2">
|
<Link href="/dashboard/apps" className="text-center text-sm text-primary-600 hover:text-primary-700 font-medium py-2">
|
||||||
View all {apps.length} applications →
|
{h.viewAllApplications.replace('{n}', String(apps.length))}
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -236,9 +241,9 @@ export default function DashboardPage() {
|
|||||||
{/* Managed services */}
|
{/* Managed services */}
|
||||||
<div>
|
<div>
|
||||||
<div className="page-header mb-4">
|
<div className="page-header mb-4">
|
||||||
<h2 className="text-lg font-semibold text-gray-900">Recent databases & services</h2>
|
<h2 className="text-lg font-semibold text-gray-900">{h.recentServices}</h2>
|
||||||
<Link href="/dashboard/services/new" className="btn-primary text-sm">
|
<Link href="/dashboard/services/new" className="btn-primary text-sm">
|
||||||
<Plus className="w-4 h-4 mr-1 inline" /> New service
|
<Plus className="w-4 h-4 mr-1 rtl:mr-0 rtl:ml-1 inline" /> {h.newService}
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -257,9 +262,9 @@ export default function DashboardPage() {
|
|||||||
) : services.length === 0 ? (
|
) : services.length === 0 ? (
|
||||||
<div className="card text-center py-12">
|
<div className="card text-center py-12">
|
||||||
<Database className="w-10 h-10 mx-auto mb-3 text-gray-300" />
|
<Database className="w-10 h-10 mx-auto mb-3 text-gray-300" />
|
||||||
<p className="text-gray-600 font-medium">No managed services yet</p>
|
<p className="text-gray-600 font-medium">{h.noServices}</p>
|
||||||
<Link href="/dashboard/services/new" className="btn-primary mt-4 inline-flex items-center gap-1 text-sm">
|
<Link href="/dashboard/services/new" className="btn-primary mt-4 inline-flex items-center gap-1 text-sm">
|
||||||
<Plus className="w-4 h-4" /> Create database or service
|
<Plus className="w-4 h-4" /> {h.createServiceCta}
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@@ -286,7 +291,7 @@ export default function DashboardPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 shrink-0">
|
<div className="flex items-center gap-2 shrink-0">
|
||||||
{statusIcons[status]}
|
{statusIcons[status]}
|
||||||
<span className={`badge ${statusColors[status] || 'badge-gray'}`}>{status}</span>
|
<span className={`badge ${statusColors[status] || 'badge-gray'}`}>{statusLabel(status, t)}</span>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
@@ -296,7 +301,7 @@ export default function DashboardPage() {
|
|||||||
href="/dashboard/services"
|
href="/dashboard/services"
|
||||||
className="text-center text-sm text-primary-600 hover:text-primary-700 font-medium py-2"
|
className="text-center text-sm text-primary-600 hover:text-primary-700 font-medium py-2"
|
||||||
>
|
>
|
||||||
View all {services.length} services →
|
{h.viewAllServices.replace('{n}', String(services.length))}
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -158,6 +158,55 @@ const en: Dictionary = {
|
|||||||
technicalTickets: 'Technical Tickets',
|
technicalTickets: 'Technical Tickets',
|
||||||
salesTickets: 'Sales Tickets',
|
salesTickets: 'Sales Tickets',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
dashboard: {
|
||||||
|
status: {
|
||||||
|
running: 'Running',
|
||||||
|
pending: 'Pending',
|
||||||
|
building: 'Building',
|
||||||
|
deploying: 'Deploying',
|
||||||
|
failed: 'Failed',
|
||||||
|
build_failed: 'Build failed',
|
||||||
|
cancelled: 'Cancelled',
|
||||||
|
stopped: 'Stopped',
|
||||||
|
},
|
||||||
|
home: {
|
||||||
|
welcome: 'Welcome back, {name}!',
|
||||||
|
overview: 'Overview of your applications and managed services.',
|
||||||
|
creditsTitle: 'Prepaid resource credits',
|
||||||
|
creditsBody:
|
||||||
|
'If you delete an app before your plan ends, you can deploy a new app with the same resources at no extra charge until the credit expires.',
|
||||||
|
fromApp: 'From app “{name}”',
|
||||||
|
resourceCredit: 'Resource credit',
|
||||||
|
remaining: '{label} remaining',
|
||||||
|
expires: 'Expires {date}',
|
||||||
|
cpu: 'CPU',
|
||||||
|
ram: 'RAM',
|
||||||
|
replicas: 'Replicas',
|
||||||
|
db: 'DB',
|
||||||
|
dbDisk: 'DB disk',
|
||||||
|
appDisk: 'App disk',
|
||||||
|
useOnNewApp: 'Use on new app →',
|
||||||
|
statApplications: 'Applications',
|
||||||
|
statAppsRunning: 'Apps running',
|
||||||
|
statManagedServices: 'Managed services',
|
||||||
|
statServicesRunning: 'Services running',
|
||||||
|
appsFailed: '{n} application(s) failed',
|
||||||
|
servicesFailed: '{n} service(s) failed',
|
||||||
|
recentApplications: 'Recent applications',
|
||||||
|
newApplication: 'New application',
|
||||||
|
noApplications: 'No applications yet',
|
||||||
|
deployFirst: 'Deploy your first app',
|
||||||
|
viewAllApplications: 'View all {n} applications →',
|
||||||
|
recentServices: 'Recent databases & services',
|
||||||
|
newService: 'New service',
|
||||||
|
noServices: 'No managed services yet',
|
||||||
|
createServiceCta: 'Create database or service',
|
||||||
|
viewAllServices: 'View all {n} services →',
|
||||||
|
replica: 'replica',
|
||||||
|
replicaPlural: 'replicas',
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default en;
|
export default en;
|
||||||
|
|||||||
@@ -157,6 +157,55 @@ const fa = {
|
|||||||
technicalTickets: 'تیکتهای فنی',
|
technicalTickets: 'تیکتهای فنی',
|
||||||
salesTickets: 'تیکتهای فروش',
|
salesTickets: 'تیکتهای فروش',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
dashboard: {
|
||||||
|
status: {
|
||||||
|
running: 'در حال اجرا',
|
||||||
|
pending: 'در انتظار',
|
||||||
|
building: 'در حال بیلد',
|
||||||
|
deploying: 'در حال انتشار',
|
||||||
|
failed: 'ناموفق',
|
||||||
|
build_failed: 'خطای بیلد',
|
||||||
|
cancelled: 'لغو شده',
|
||||||
|
stopped: 'متوقف',
|
||||||
|
},
|
||||||
|
home: {
|
||||||
|
welcome: 'خوش آمدی، {name}!',
|
||||||
|
overview: 'نمای کلیِ اپلیکیشنها و سرویسهای مدیریتشدهٔ تو.',
|
||||||
|
creditsTitle: 'اعتبارِ پیشپرداختِ منابع',
|
||||||
|
creditsBody:
|
||||||
|
'اگر اپی را پیش از پایان پلنت حذف کنی، میتوانی تا زمان انقضای اعتبار، اپ جدیدی با همان منابع و بدون هزینهٔ اضافه منتشر کنی.',
|
||||||
|
fromApp: 'از اپِ «{name}»',
|
||||||
|
resourceCredit: 'اعتبارِ منابع',
|
||||||
|
remaining: '{label} باقیمانده',
|
||||||
|
expires: 'انقضا {date}',
|
||||||
|
cpu: 'پردازنده',
|
||||||
|
ram: 'حافظه',
|
||||||
|
replicas: 'تعداد نمونه',
|
||||||
|
db: 'دیتابیس',
|
||||||
|
dbDisk: 'دیسکِ دیتابیس',
|
||||||
|
appDisk: 'دیسکِ اپ',
|
||||||
|
useOnNewApp: 'استفاده روی اپ جدید →',
|
||||||
|
statApplications: 'اپلیکیشنها',
|
||||||
|
statAppsRunning: 'اپهای در حال اجرا',
|
||||||
|
statManagedServices: 'سرویسهای مدیریتشده',
|
||||||
|
statServicesRunning: 'سرویسهای در حال اجرا',
|
||||||
|
appsFailed: '{n} اپلیکیشن ناموفق',
|
||||||
|
servicesFailed: '{n} سرویس ناموفق',
|
||||||
|
recentApplications: 'اپلیکیشنهای اخیر',
|
||||||
|
newApplication: 'اپلیکیشن جدید',
|
||||||
|
noApplications: 'هنوز اپلیکیشنی نداری',
|
||||||
|
deployFirst: 'اولین اپت را منتشر کن',
|
||||||
|
viewAllApplications: 'مشاهدهٔ همهٔ {n} اپلیکیشن →',
|
||||||
|
recentServices: 'دیتابیسها و سرویسهای اخیر',
|
||||||
|
newService: 'سرویس جدید',
|
||||||
|
noServices: 'هنوز سرویس مدیریتشدهای نداری',
|
||||||
|
createServiceCta: 'ساخت دیتابیس یا سرویس',
|
||||||
|
viewAllServices: 'مشاهدهٔ همهٔ {n} سرویس →',
|
||||||
|
replica: 'نمونه',
|
||||||
|
replicaPlural: 'نمونه',
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Dictionary = typeof fa;
|
export type Dictionary = typeof fa;
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user