This commit is contained in:
46
src/app/[locale]/settings/billing/page.tsx
Normal file
46
src/app/[locale]/settings/billing/page.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { redirect, notFound } from "next/navigation";
|
||||
import { getSessionUser, canMutate } from "@/lib/session";
|
||||
import { getOrgBilling } from "@/lib/db";
|
||||
import { BillingSettingsForm } from "@/components/settings/billing-settings-form";
|
||||
|
||||
/**
|
||||
* /settings/billing — view and edit org-scoped billing (Bug 34/35).
|
||||
*
|
||||
* Server-side fetches the existing record (if any) and passes it to
|
||||
* the client form. The form posts to PUT /api/billing on submit.
|
||||
*
|
||||
* Access: same gate as the API — owners and platform admins. `user`
|
||||
* role redirects to /settings (which also wouldn't list billing for
|
||||
* them). 403 here would be friendlier than redirect, but the most
|
||||
* likely cause of a `user` landing on this URL is sharing a bookmark
|
||||
* with their owner — silent redirect is gentle.
|
||||
*/
|
||||
export default async function BillingSettingsPage() {
|
||||
const user = await getSessionUser();
|
||||
if (!user) redirect("/login");
|
||||
if (!canMutate(user)) {
|
||||
redirect("/settings");
|
||||
}
|
||||
const t = await getTranslations("settingsBilling");
|
||||
|
||||
const billing = await getOrgBilling(user.orgId);
|
||||
|
||||
return (
|
||||
<main className="max-w-3xl 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>
|
||||
|
||||
<BillingSettingsForm
|
||||
initial={billing}
|
||||
isPersonal={user.isPersonal}
|
||||
orgName={user.orgName}
|
||||
userName={user.name}
|
||||
/>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
76
src/app/[locale]/settings/page.tsx
Normal file
76
src/app/[locale]/settings/page.tsx
Normal file
@@ -0,0 +1,76 @@
|
||||
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. Currently only billing; this
|
||||
// shape leaves headroom for adding more without restructuring.
|
||||
const sections: Array<{
|
||||
key: string;
|
||||
href: string;
|
||||
title: string;
|
||||
description: string;
|
||||
visible: boolean;
|
||||
}> = [
|
||||
{
|
||||
key: "billing",
|
||||
href: "/settings/billing",
|
||||
title: t("billingTitle"),
|
||||
description: 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user