Batch commit of the pending working tree on security/audit-fixes-2026-07. Major areas: - Outbound webhooks / Zapier: schema + signed delivery with retries, public v1 API (REST-hook subscribe/unsubscribe), settings UI, cron drain. - Deploy hardening: email via SMTP2GO (Resend fully removed), verified DB TLS (DATABASE_SSL=require + DATABASE_CA), storage fails loud in production when Spaces is unconfigured instead of silently using ephemeral disk. - Integrations & features (concurrent work): accounting (QuickBooks/Xero), e-signature (DocuSign/Dropbox Sign), PayPal, geocoding/maps, onboarding, expanded legal pages. - DB migrations 0006–0009. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
116 lines
3.6 KiB
TypeScript
116 lines
3.6 KiB
TypeScript
import Link from "next/link"
|
|
import { LEGAL, LEGAL_PAGES } from "@/lib/legal"
|
|
|
|
/**
|
|
* Shared shell for every legal page. Renders a consistent header (title,
|
|
* effective/updated dates), the body, and a cross-link nav to sibling policies.
|
|
* `pt-32` clears the fixed marketing navbar.
|
|
*/
|
|
export function LegalPage({
|
|
title,
|
|
subtitle,
|
|
updated,
|
|
children,
|
|
}: {
|
|
title: string
|
|
subtitle?: string
|
|
/** Optional per-page override; defaults to the global LEGAL.lastUpdated. */
|
|
updated?: string
|
|
children: React.ReactNode
|
|
}) {
|
|
return (
|
|
<div className="bg-[#09090b] text-white min-h-screen">
|
|
<div className="mx-auto max-w-3xl px-6 pt-32 pb-24">
|
|
<nav className="mb-8 text-xs text-white/30">
|
|
<Link href="/" className="hover:text-white/60 transition">Home</Link>
|
|
<span className="mx-1.5">/</span>
|
|
<span className="text-white/50">{title}</span>
|
|
</nav>
|
|
|
|
<header className="mb-10 border-b border-white/[0.06] pb-8">
|
|
<h1 className="text-3xl font-bold text-white mb-3">{title}</h1>
|
|
{subtitle && <p className="text-sm text-white/50 leading-relaxed">{subtitle}</p>}
|
|
<p className="mt-4 text-xs text-white/30">
|
|
Effective date: {LEGAL.effectiveDate} · Last updated: {updated ?? LEGAL.lastUpdated}
|
|
</p>
|
|
</header>
|
|
|
|
<div className="space-y-9">{children}</div>
|
|
|
|
<PolicyNav current={title} />
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
/** A titled section. Pass plain text or JSX children (paragraphs, lists, etc.). */
|
|
export function Section({
|
|
id,
|
|
heading,
|
|
children,
|
|
}: {
|
|
id?: string
|
|
heading: string
|
|
children: React.ReactNode
|
|
}) {
|
|
return (
|
|
<section id={id} className="scroll-mt-32">
|
|
<h2 className="text-lg font-semibold text-white mb-3">{heading}</h2>
|
|
<div className="space-y-3 text-sm text-white/50 leading-relaxed [&_a]:text-indigo-300 [&_a:hover]:text-indigo-200 [&_strong]:text-white/80 [&_ul]:list-disc [&_ul]:pl-5 [&_ul]:space-y-1.5 [&_li]:marker:text-white/30">
|
|
{children}
|
|
</div>
|
|
</section>
|
|
)
|
|
}
|
|
|
|
/** Highlighted note (e.g. a disclaimer or important caveat). */
|
|
export function Callout({ children }: { children: React.ReactNode }) {
|
|
return (
|
|
<div className="rounded-xl border border-amber-500/20 bg-amber-500/[0.06] px-4 py-3.5 text-sm text-amber-100/80 leading-relaxed">
|
|
{children}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
/** Standard "questions / contact us" block for the bottom of a policy. */
|
|
export function LegalContact({
|
|
email = LEGAL.contactEmail,
|
|
children,
|
|
}: {
|
|
email?: string
|
|
children?: React.ReactNode
|
|
}) {
|
|
return (
|
|
<Section heading="Contact us">
|
|
<p>
|
|
{children ?? "If you have questions about this policy, contact us at "}
|
|
<a href={`mailto:${email}`}>{email}</a>.
|
|
</p>
|
|
<p className="text-white/30 text-xs">
|
|
{LEGAL.entity}
|
|
{LEGAL.address && !LEGAL.address.startsWith("[") ? ` · ${LEGAL.address}` : ""}
|
|
</p>
|
|
</Section>
|
|
)
|
|
}
|
|
|
|
/** Cross-links to the other legal documents. */
|
|
function PolicyNav({ current }: { current: string }) {
|
|
return (
|
|
<nav className="mt-14 border-t border-white/[0.06] pt-8">
|
|
<p className="text-xs font-semibold uppercase tracking-wider text-white/30 mb-4">
|
|
More policies
|
|
</p>
|
|
<ul className="grid grid-cols-2 gap-x-6 gap-y-2.5 sm:grid-cols-3">
|
|
{LEGAL_PAGES.filter((p) => p.label !== current).map((p) => (
|
|
<li key={p.href}>
|
|
<Link href={p.href} className="text-sm text-white/50 hover:text-white transition">
|
|
{p.label}
|
|
</Link>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</nav>
|
|
)
|
|
}
|