Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bff3aad1ca |
@@ -98,6 +98,7 @@ export default async function AdminBillingPage() {
|
||||
<div className="animate-in animate-in-delay-3">
|
||||
<h2 className="text-lg font-semibold mb-3">{t("balancesTitle")}</h2>
|
||||
<Card>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead className="text-xs text-text-muted text-left">
|
||||
<tr>
|
||||
@@ -126,6 +127,7 @@ export default async function AdminBillingPage() {
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -5,6 +5,11 @@ import { listTenants } from "@/lib/k8s";
|
||||
import { countPendingSkillActivationRequests } from "@/lib/db";
|
||||
import { AdminPanel } from "@/components/admin/admin-panel";
|
||||
|
||||
export async function generateMetadata() {
|
||||
const t = await getTranslations("common");
|
||||
return { title: t("admin") };
|
||||
}
|
||||
|
||||
export default async function AdminPage() {
|
||||
const user = await getSessionUser();
|
||||
if (!user) redirect("/login");
|
||||
|
||||
@@ -26,6 +26,11 @@ import { RunningTotalWidget } from "@/components/billing/running-total-widget";
|
||||
* Anyone signed in can view this. The data is org-scoped; even
|
||||
* non-owner team members see the same view.
|
||||
*/
|
||||
export async function generateMetadata() {
|
||||
const t = await getTranslations("common");
|
||||
return { title: t("billing") };
|
||||
}
|
||||
|
||||
export default async function CustomerBillingPage() {
|
||||
const user = await getSessionUser();
|
||||
if (!user) redirect("/login");
|
||||
|
||||
@@ -22,6 +22,11 @@ import { ProvisioningStatus } from "@/components/onboarding/provisioning-status"
|
||||
import { formatDateTime } from "@/lib/format";
|
||||
import Link from "next/link";
|
||||
|
||||
export async function generateMetadata() {
|
||||
const t = await getTranslations("common");
|
||||
return { title: t("dashboard") };
|
||||
}
|
||||
|
||||
export default async function DashboardPage() {
|
||||
const user = await getSessionUser();
|
||||
if (!user) redirect("/login");
|
||||
|
||||
72
src/app/[locale]/error.tsx
Normal file
72
src/app/[locale]/error.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { Link } from "@/i18n/navigation";
|
||||
|
||||
/**
|
||||
* Error boundary for the [locale] segment. Catches render/data errors
|
||||
* thrown by any page below the locale layout (which is where K8s, DB,
|
||||
* LiteLLM and Stripe calls happen). Renders inside NextIntlClientProvider,
|
||||
* so translations are available. Root-layout failures fall through to
|
||||
* global-error.tsx instead.
|
||||
*/
|
||||
export default function LocaleError({
|
||||
error,
|
||||
reset,
|
||||
}: {
|
||||
error: Error & { digest?: string };
|
||||
reset: () => void;
|
||||
}) {
|
||||
const t = useTranslations("errors");
|
||||
|
||||
useEffect(() => {
|
||||
// Surface the error for log scraping; the digest correlates with
|
||||
// the server-side stack in production.
|
||||
console.error("Portal error boundary:", error);
|
||||
}, [error]);
|
||||
|
||||
return (
|
||||
<div className="flex min-h-[60vh] items-center justify-center px-5">
|
||||
<div className="w-full max-w-md text-center">
|
||||
<div className="mx-auto mb-5 flex h-14 w-14 items-center justify-center rounded-xl bg-error/10">
|
||||
<svg
|
||||
className="h-7 w-7 text-error"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={1.75}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M12 9v4M12 17h.01M10.3 3.86l-8.5 14.7A1.5 1.5 0 003.1 21h17.8a1.5 1.5 0 001.3-2.44l-8.5-14.7a1.5 1.5 0 00-2.6 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h1 className="font-display text-xl font-semibold text-text-primary mb-2">
|
||||
{t("title")}
|
||||
</h1>
|
||||
<p className="text-sm text-text-secondary mb-6">{t("description")}</p>
|
||||
{error?.digest && (
|
||||
<p className="text-[11px] font-mono text-text-muted mb-6">
|
||||
{error.digest}
|
||||
</p>
|
||||
)}
|
||||
<div className="flex items-center justify-center gap-3">
|
||||
<button
|
||||
onClick={reset}
|
||||
className="py-2 px-4 rounded-lg bg-accent text-surface-0 text-sm font-medium hover:bg-accent-dim transition-colors cursor-pointer"
|
||||
>
|
||||
{t("retry")}
|
||||
</button>
|
||||
<Link
|
||||
href="/dashboard"
|
||||
className="py-2 px-4 rounded-lg border border-border text-sm font-medium text-text-secondary hover:text-text-primary hover:bg-surface-2 transition-colors"
|
||||
>
|
||||
{t("backToDashboard")}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Metadata, Viewport } from "next";
|
||||
import { NextIntlClientProvider } from "next-intl";
|
||||
import { getMessages } from "next-intl/server";
|
||||
import { getMessages, getTranslations } from "next-intl/server";
|
||||
import { routing } from "@/i18n/routing";
|
||||
import { notFound } from "next/navigation";
|
||||
import { NavShell } from "@/components/layout/nav-shell";
|
||||
@@ -8,6 +9,27 @@ export function generateStaticParams() {
|
||||
return routing.locales.map((locale) => ({ locale }));
|
||||
}
|
||||
|
||||
// Metadata API (Next 15) instead of a hand-rolled <head>. The title
|
||||
// template lets each page export a short `title` (e.g. "Dashboard")
|
||||
// that renders as "Dashboard · PieCed". Pages that export no metadata
|
||||
// fall back to the default below.
|
||||
export async function generateMetadata(): Promise<Metadata> {
|
||||
const t = await getTranslations("common");
|
||||
const appName = t("appName");
|
||||
return {
|
||||
title: {
|
||||
default: `${appName} Portal`,
|
||||
template: `%s · ${appName}`,
|
||||
},
|
||||
description: "PieCed IT — Multi-tenant AI assistant platform",
|
||||
};
|
||||
}
|
||||
|
||||
export const viewport: Viewport = {
|
||||
width: "device-width",
|
||||
initialScale: 1,
|
||||
};
|
||||
|
||||
export default async function LocaleLayout({
|
||||
children,
|
||||
params,
|
||||
@@ -25,14 +47,6 @@ export default async function LocaleLayout({
|
||||
|
||||
return (
|
||||
<html lang={locale} className="dark">
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>PieCed Portal</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="PieCed IT — Multi-tenant AI assistant platform"
|
||||
/>
|
||||
</head>
|
||||
<body className="min-h-screen bg-surface-0 text-text-primary antialiased">
|
||||
<NextIntlClientProvider messages={messages}>
|
||||
<NavShell>{children}</NavShell>
|
||||
|
||||
25
src/app/[locale]/loading.tsx
Normal file
25
src/app/[locale]/loading.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Loading skeleton for the [locale] segment. Shown during navigation
|
||||
* while a server component fetches (the dashboard, for instance, does
|
||||
* listTenants() + one K8s GET per provisioning row). Textless on
|
||||
* purpose so it needs no translations and adds no layout shift.
|
||||
*/
|
||||
export default function LocaleLoading() {
|
||||
return (
|
||||
<div className="animate-pulse" aria-hidden="true">
|
||||
<div className="mb-8">
|
||||
<div className="h-7 w-48 rounded-md bg-surface-2" />
|
||||
<div className="mt-4 h-4 w-72 rounded bg-surface-1" />
|
||||
</div>
|
||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{Array.from({ length: 6 }).map((_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="h-28 rounded-xl border border-border bg-surface-1"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<span className="sr-only">Loading…</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
34
src/app/[locale]/not-found.tsx
Normal file
34
src/app/[locale]/not-found.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { Link } from "@/i18n/navigation";
|
||||
|
||||
/**
|
||||
* 404 for the [locale] segment. Triggered by notFound() calls in pages
|
||||
* below the locale layout. (A notFound() thrown by the locale layout
|
||||
* itself — e.g. an unknown locale — resolves to the framework default,
|
||||
* which is acceptable for that narrow case.)
|
||||
*/
|
||||
export default async function LocaleNotFound() {
|
||||
const t = await getTranslations("errors");
|
||||
|
||||
return (
|
||||
<div className="flex min-h-[60vh] items-center justify-center px-5">
|
||||
<div className="w-full max-w-md text-center">
|
||||
<div className="font-display text-5xl font-semibold text-accent mb-4 tabular-nums">
|
||||
404
|
||||
</div>
|
||||
<h1 className="font-display text-xl font-semibold text-text-primary mb-2">
|
||||
{t("notFoundTitle")}
|
||||
</h1>
|
||||
<p className="text-sm text-text-secondary mb-6">
|
||||
{t("notFoundDescription")}
|
||||
</p>
|
||||
<Link
|
||||
href="/dashboard"
|
||||
className="inline-flex py-2 px-4 rounded-lg bg-accent text-surface-0 text-sm font-medium hover:bg-accent-dim transition-colors"
|
||||
>
|
||||
{t("backToDashboard")}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -14,6 +14,11 @@ import { Card } from "@/components/ui/card";
|
||||
* Access: any authenticated user (the cards themselves gate further;
|
||||
* non-owner users would not see "Billing" as actionable, etc.).
|
||||
*/
|
||||
export async function generateMetadata() {
|
||||
const t = await getTranslations("common");
|
||||
return { title: t("settings") };
|
||||
}
|
||||
|
||||
export default async function SettingsPage() {
|
||||
const user = await getSessionUser();
|
||||
if (!user) redirect("/login");
|
||||
|
||||
@@ -24,6 +24,11 @@ import { TicketCategoryLabel } from "@/components/support/ticket-category-label"
|
||||
* having recent activity, but we don't sort by status; that's a
|
||||
* filter the admin can add later if the queue grows.
|
||||
*/
|
||||
export async function generateMetadata() {
|
||||
const t = await getTranslations("common");
|
||||
return { title: t("support") };
|
||||
}
|
||||
|
||||
export default async function SupportListPage() {
|
||||
const user = await getSessionUser();
|
||||
if (!user) redirect("/login");
|
||||
|
||||
@@ -17,6 +17,11 @@ import { InviteForm } from "@/components/team/invite-form";
|
||||
* `<TeamList>` and `<InviteForm>` client components handle live
|
||||
* updates after invites and refreshes.
|
||||
*/
|
||||
export async function generateMetadata() {
|
||||
const t = await getTranslations("common");
|
||||
return { title: t("team") };
|
||||
}
|
||||
|
||||
export default async function TeamPage() {
|
||||
const user = await getSessionUser();
|
||||
if (!user) redirect("/login");
|
||||
|
||||
78
src/app/global-error.tsx
Normal file
78
src/app/global-error.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
|
||||
/**
|
||||
* Last-resort boundary for errors thrown in the root layout itself
|
||||
* (before the locale layout / intl provider mount). It replaces the
|
||||
* entire document, so it must render its own <html>/<body> and cannot
|
||||
* use translations or rely on the app stylesheet being applied — styles
|
||||
* are inlined with the palette's hex values so it renders correctly in
|
||||
* isolation. Everything below the locale layout is handled by
|
||||
* [locale]/error.tsx instead; this should almost never be seen.
|
||||
*/
|
||||
export default function GlobalError({
|
||||
error,
|
||||
reset,
|
||||
}: {
|
||||
error: Error & { digest?: string };
|
||||
reset: () => void;
|
||||
}) {
|
||||
useEffect(() => {
|
||||
console.error("Portal global error:", error);
|
||||
}, [error]);
|
||||
|
||||
return (
|
||||
<html lang="en" className="dark">
|
||||
<body
|
||||
style={{
|
||||
margin: 0,
|
||||
minHeight: "100vh",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
background: "#0a0c10",
|
||||
color: "#e8ecf4",
|
||||
fontFamily: "system-ui, sans-serif",
|
||||
padding: "20px",
|
||||
}}
|
||||
>
|
||||
<div style={{ maxWidth: "28rem", textAlign: "center" }}>
|
||||
<h1 style={{ fontSize: "1.25rem", fontWeight: 600, margin: "0 0 0.5rem" }}>
|
||||
Something went wrong
|
||||
</h1>
|
||||
<p style={{ fontSize: "0.875rem", color: "#8892a4", margin: "0 0 1.5rem" }}>
|
||||
An unexpected error occurred. Please try again.
|
||||
</p>
|
||||
{error?.digest && (
|
||||
<p
|
||||
style={{
|
||||
fontSize: "11px",
|
||||
fontFamily: "monospace",
|
||||
color: "#565e6e",
|
||||
margin: "0 0 1.5rem",
|
||||
}}
|
||||
>
|
||||
{error.digest}
|
||||
</p>
|
||||
)}
|
||||
<button
|
||||
onClick={reset}
|
||||
style={{
|
||||
padding: "0.5rem 1rem",
|
||||
borderRadius: "0.5rem",
|
||||
border: "none",
|
||||
background: "#00d4aa",
|
||||
color: "#0a0c10",
|
||||
fontSize: "0.875rem",
|
||||
fontWeight: 500,
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
Try again
|
||||
</button>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -336,6 +336,7 @@ export function CustomInvoiceEditor({ draft, orgBilling }: Props) {
|
||||
<Card>
|
||||
<CardHeader>{t("editorLinesHeading")}</CardHeader>
|
||||
<div className="p-4">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead className="text-xs text-text-muted text-left">
|
||||
<tr>
|
||||
@@ -420,6 +421,7 @@ export function CustomInvoiceEditor({ draft, orgBilling }: Props) {
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div className="flex gap-2 mt-3">
|
||||
<button
|
||||
onClick={addLine}
|
||||
|
||||
@@ -76,6 +76,7 @@ export function DraftList({ drafts, orgNameMap }: Props) {
|
||||
{t("newInvoiceBtn")}
|
||||
</Link>
|
||||
</div>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead className="text-xs text-text-muted text-left">
|
||||
<tr>
|
||||
@@ -140,6 +141,7 @@ export function DraftList({ drafts, orgNameMap }: Props) {
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -265,6 +265,7 @@ function DraftPreview({ draft }: { draft: InvoiceDraft }) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead className="text-xs text-text-muted text-left">
|
||||
<tr>
|
||||
@@ -323,6 +324,7 @@ function DraftPreview({ draft }: { draft: InvoiceDraft }) {
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 pt-3 border-t border-border space-y-1 text-sm">
|
||||
<div className="flex justify-between">
|
||||
|
||||
@@ -463,6 +463,7 @@ export function InvoiceDetailView({ detail, creditNotes = [] }: Props) {
|
||||
{creditNotes.length > 0 && (
|
||||
<Card>
|
||||
<CardHeader>{t("creditNotesPanelTitle")}</CardHeader>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead className="text-xs text-text-muted text-left">
|
||||
<tr>
|
||||
@@ -518,12 +519,14 @@ export function InvoiceDetailView({ detail, creditNotes = [] }: Props) {
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Lines */}
|
||||
<Card>
|
||||
<CardHeader>{t("lineItemsTitle")}</CardHeader>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead className="text-xs text-text-muted text-left">
|
||||
<tr>
|
||||
@@ -572,6 +575,7 @@ export function InvoiceDetailView({ detail, creditNotes = [] }: Props) {
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div className="mt-4 pt-3 border-t border-border space-y-1 text-sm">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-text-muted">{t("subtotal")}</span>
|
||||
|
||||
@@ -126,6 +126,7 @@ export function InvoicesTable({ initialInvoices }: Props) {
|
||||
{t("noInvoicesFound")}
|
||||
</p>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead className="text-xs text-text-muted text-left">
|
||||
<tr>
|
||||
@@ -178,6 +179,7 @@ export function InvoicesTable({ initialInvoices }: Props) {
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
@@ -76,6 +76,7 @@ export function OrgPaymentModeList({ orgs }: Props) {
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead className="text-xs text-text-muted text-left">
|
||||
<tr>
|
||||
@@ -153,6 +154,7 @@ export function OrgPaymentModeList({ orgs }: Props) {
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -255,6 +255,7 @@ export function PricingEditor({
|
||||
<p className="text-sm text-text-muted mb-4">{t("skillPricingDesc")}</p>
|
||||
|
||||
{initialSkillPricing.length > 0 ? (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm mb-6">
|
||||
<thead className="text-xs text-text-muted text-left">
|
||||
<tr>
|
||||
@@ -319,6 +320,7 @@ export function PricingEditor({
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-text-muted italic mb-4">{t("noSkillsPriced")}</p>
|
||||
)}
|
||||
|
||||
@@ -194,6 +194,7 @@ export function CronControls({ initialRecent, initialLastSuccess }: Props) {
|
||||
{t("noRunsYet")}
|
||||
</p>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead className="text-xs text-text-muted text-left">
|
||||
<tr>
|
||||
@@ -241,6 +242,7 @@ export function CronControls({ initialRecent, initialLastSuccess }: Props) {
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
</section>
|
||||
|
||||
@@ -99,6 +99,7 @@ export function PendingSkillRequests({ initialRows }: Props) {
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead className="text-xs text-text-muted text-left">
|
||||
<tr>
|
||||
@@ -199,6 +200,7 @@ export function PendingSkillRequests({ initialRows }: Props) {
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ export function CustomerCreditNoteList({ creditNotes }: Props) {
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead className="text-xs text-text-muted text-left">
|
||||
<tr>
|
||||
@@ -96,6 +97,7 @@ export function CustomerCreditNoteList({ creditNotes }: Props) {
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -107,6 +107,7 @@ export function CustomerInvoiceDetail({ invoice, lines }: Props) {
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead className="text-xs text-text-muted text-left">
|
||||
<tr>
|
||||
@@ -160,6 +161,7 @@ export function CustomerInvoiceDetail({ invoice, lines }: Props) {
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -46,6 +46,7 @@ export function CustomerInvoiceList({ invoices }: Props) {
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead className="text-xs text-text-muted text-left">
|
||||
<tr>
|
||||
@@ -104,6 +105,7 @@ export function CustomerInvoiceList({ invoices }: Props) {
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -160,6 +160,7 @@ export function RunningTotalWidget({ isOwner }: Props) {
|
||||
<summary className="cursor-pointer text-text-muted hover:text-text-secondary">
|
||||
{t("breakdownToggle", { count: draft.lines.length })}
|
||||
</summary>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full mt-2 text-xs">
|
||||
<tbody>
|
||||
{draft.lines.map((ln, i) => (
|
||||
@@ -188,6 +189,7 @@ export function RunningTotalWidget({ isOwner }: Props) {
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</details>
|
||||
)}
|
||||
<p className="text-[10px] text-text-muted mt-3 italic">{t("draftNote")}</p>
|
||||
|
||||
@@ -964,5 +964,13 @@
|
||||
"saving": "Speichern…",
|
||||
"saved": "Gespeichert.",
|
||||
"missingRequired": "Vor- und Nachname sind erforderlich."
|
||||
},
|
||||
"errors": {
|
||||
"title": "Etwas ist schiefgelaufen",
|
||||
"description": "Beim Laden dieser Seite ist ein Fehler aufgetreten. Bitte versuchen Sie es erneut.",
|
||||
"retry": "Erneut versuchen",
|
||||
"backToDashboard": "Zurück zum Dashboard",
|
||||
"notFoundTitle": "Seite nicht gefunden",
|
||||
"notFoundDescription": "Die angeforderte Seite existiert nicht oder wurde verschoben."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -964,5 +964,13 @@
|
||||
"saving": "Saving…",
|
||||
"saved": "Saved.",
|
||||
"missingRequired": "First and last name are required."
|
||||
},
|
||||
"errors": {
|
||||
"title": "Something went wrong",
|
||||
"description": "An error occurred while loading this page. Please try again.",
|
||||
"retry": "Try again",
|
||||
"backToDashboard": "Back to dashboard",
|
||||
"notFoundTitle": "Page not found",
|
||||
"notFoundDescription": "The page you're looking for doesn't exist or has moved."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -964,5 +964,13 @@
|
||||
"saving": "Enregistrement…",
|
||||
"saved": "Enregistré.",
|
||||
"missingRequired": "Le prénom et le nom sont obligatoires."
|
||||
},
|
||||
"errors": {
|
||||
"title": "Une erreur est survenue",
|
||||
"description": "Une erreur s'est produite lors du chargement de cette page. Veuillez réessayer.",
|
||||
"retry": "Réessayer",
|
||||
"backToDashboard": "Retour au tableau de bord",
|
||||
"notFoundTitle": "Page introuvable",
|
||||
"notFoundDescription": "La page que vous recherchez n'existe pas ou a été déplacée."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -964,5 +964,13 @@
|
||||
"saving": "Salvataggio…",
|
||||
"saved": "Salvato.",
|
||||
"missingRequired": "Nome e cognome sono obbligatori."
|
||||
},
|
||||
"errors": {
|
||||
"title": "Si è verificato un errore",
|
||||
"description": "Si è verificato un errore durante il caricamento di questa pagina. Riprova.",
|
||||
"retry": "Riprova",
|
||||
"backToDashboard": "Torna alla dashboard",
|
||||
"notFoundTitle": "Pagina non trovata",
|
||||
"notFoundDescription": "La pagina che stai cercando non esiste o è stata spostata."
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user