91 lines
3.1 KiB
TypeScript
91 lines
3.1 KiB
TypeScript
import { getTranslations } from "next-intl/server";
|
|
import { redirect } from "next/navigation";
|
|
import Link from "next/link";
|
|
import { getSessionUser, canMutate } from "@/lib/session";
|
|
import { Card } from "@/components/ui/card";
|
|
|
|
/**
|
|
* /settings — landing page for user/org-level configuration (Bug 35
|
|
* intentionally landed billing here rather than at /billing because we
|
|
* expect more settings categories: notifications, API keys, default
|
|
* workspace templates, etc.). Currently lists a single category card;
|
|
* the layout scales to a sidebar nav once there are 3+.
|
|
*
|
|
* Access: any authenticated user (the cards themselves gate further;
|
|
* non-owner users would not see "Billing" as actionable, etc.).
|
|
*/
|
|
export default async function SettingsPage() {
|
|
const user = await getSessionUser();
|
|
if (!user) redirect("/login");
|
|
const t = await getTranslations("settings");
|
|
|
|
// Build the list of settings cards. Each entry has a stable key, a
|
|
// route, and a visibility predicate. Phase 6 fix5: profile is
|
|
// visible to every signed-in user (it's their own identity).
|
|
// Billing stays gated behind canMutate.
|
|
const sections: Array<{
|
|
key: string;
|
|
href: string;
|
|
title: string;
|
|
description: string;
|
|
visible: boolean;
|
|
}> = [
|
|
{
|
|
key: "profile",
|
|
href: "/settings/profile",
|
|
title: t("profileTitle"),
|
|
description: t("profileDescription"),
|
|
// Every signed-in user can edit their own first/last name.
|
|
visible: true,
|
|
},
|
|
{
|
|
key: "billing",
|
|
href: "/settings/billing",
|
|
title: t("billingTitle"),
|
|
// Personal customers (B2C) don't have a VAT number; the
|
|
// description shouldn't mention one. Same pattern used in the
|
|
// form itself (label/field gating).
|
|
description: user.isPersonal
|
|
? t("billingDescriptionPersonal")
|
|
: t("billingDescription"),
|
|
// Owners and platform admins can edit billing. `user` role
|
|
// can't even view it — billing details aren't useful to them.
|
|
visible: canMutate(user),
|
|
},
|
|
];
|
|
|
|
const visibleSections = sections.filter((s) => s.visible);
|
|
|
|
return (
|
|
<main className="max-w-4xl mx-auto px-6 py-8">
|
|
<div className="mb-8 animate-in">
|
|
<h1 className="font-display text-2xl font-semibold accent-rule">
|
|
{t("title")}
|
|
</h1>
|
|
<p className="text-sm text-text-secondary mt-3">{t("subtitle")}</p>
|
|
</div>
|
|
|
|
{visibleSections.length === 0 && (
|
|
<Card className="animate-in animate-in-delay-1">
|
|
<p className="text-sm text-text-secondary">{t("nothingForYou")}</p>
|
|
</Card>
|
|
)}
|
|
|
|
<div className="grid gap-3 animate-in animate-in-delay-1">
|
|
{visibleSections.map((s) => (
|
|
<Link
|
|
key={s.key}
|
|
href={s.href}
|
|
className="block rounded-xl border border-border bg-surface-1 p-4 hover:border-text-secondary transition-colors"
|
|
>
|
|
<div className="font-medium text-text-primary">{s.title}</div>
|
|
<div className="text-xs text-text-secondary mt-1">
|
|
{s.description}
|
|
</div>
|
|
</Link>
|
|
))}
|
|
</div>
|
|
</main>
|
|
);
|
|
}
|