Setzt Teilprojekte 1-5 der Spec 2026-10-16 um: - Urkunden-Engine (lib/certificate): ein Layout fuer PDF, Vorschau und 3D-Textur, Vorlage classic@1, eingebettete OFL-Schriften, Testmatrix - Einmal-Patenschaft ueber Traegerprodukte (Kategorie PATENSCHAFT), Paket wird im Checkout in Patenschaft (Regelsatz) und Honig aufgeteilt, Kontopflicht, rein digitale Bestellungen (FulfillmentMethod DIGITAL) - Ausstellung nach Zahlung idempotent, Wiederholung im App-Prozess (instrumentation + background-jobs), Ablage in CERTIFICATE_DIR - Verkaufsseite /patenschaft mit Konfigurator und Live-Vorschau, Warenkorb mit Schluessel je Position - Kundenkonto: 3D-Ansicht von Urkunde und Medaille, Downloads, ZIP, Uebertragung per befristetem Einmal-Link, Treuestufen bis VIP - Admin: Angebote, Patenschaften, Druck, Korrektur mit Revisionen, Abos, Erntefenster, Deckung, SEPA-Frist, Treuestufen - Abo ueber Stripe Billing: Lieferung erst nach bezahlten Monaten, SEPA-Neukunden-Frist und Rueckbuchungsschutz, Honigreservierung, Kuendigungsbutton, Monatsrechnungen, Stripe-Einrichtungsskript Startangebote werden inaktiv angelegt; Livebetrieb erst nach Freigabe. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
80 lines
3.4 KiB
TypeScript
80 lines
3.4 KiB
TypeScript
import type { Metadata } from "next";
|
|
import { redirect } from "next/navigation";
|
|
import { Container, SectionHeading, LinkButton } from "@/components/ui";
|
|
import { BackLink } from "@/components/back-link";
|
|
import { AccountEmptyState } from "@/components/account-ui";
|
|
import { getCurrentCustomer } from "@/lib/auth";
|
|
import { listOwnedSponsorships } from "@/lib/sponsorship/access";
|
|
import { hasActiveOffers } from "@/lib/sponsorship/offers";
|
|
import { getCustomerLoyalty } from "@/lib/loyalty";
|
|
import { getLocale } from "@/i18n/locale";
|
|
import { getAccountSponsorshipStrings } from "@/i18n/sponsorship/account";
|
|
import { MedalShelf } from "./_components/medal-shelf";
|
|
import { SponsorshipRow } from "./_components/sponsorship-row";
|
|
import { PendingRefresher } from "./_components/pending-refresher";
|
|
import { BeeSealIcon } from "./_components/icons";
|
|
|
|
export const dynamic = "force-dynamic";
|
|
|
|
export async function generateMetadata(): Promise<Metadata> {
|
|
return { title: getAccountSponsorshipStrings(await getLocale()).overview.metaTitle };
|
|
}
|
|
|
|
export default async function KontoPatenschaftPage() {
|
|
const locale = await getLocale();
|
|
const t = getAccountSponsorshipStrings(locale);
|
|
const customer = await getCurrentCustomer();
|
|
if (!customer) redirect("/konto");
|
|
|
|
const [sponsorships, loyalty] = await Promise.all([
|
|
listOwnedSponsorships(customer.id, locale),
|
|
getCustomerLoyalty(customer.id, locale),
|
|
]);
|
|
const hasPending = sponsorships.some((sponsorship) => sponsorship.revision === null);
|
|
|
|
return (
|
|
<div className="py-20">
|
|
<Container className="max-w-4xl">
|
|
<BackLink href="/konto" label={t.overview.back} />
|
|
<SectionHeading level="h1" title={t.overview.title} subtitle={t.overview.subtitle} />
|
|
|
|
{sponsorships.length === 0 ? (
|
|
<AccountEmptyState
|
|
icon={<BeeSealIcon />}
|
|
title={t.overview.emptyTitle}
|
|
description={t.overview.emptyDescription}
|
|
action={(await hasActiveOffers()) ? <LinkButton href="/patenschaft">{t.overview.emptyAction}</LinkButton> : undefined}
|
|
/>
|
|
) : null}
|
|
|
|
{sponsorships.length > 0 ? (
|
|
<section aria-labelledby="sponsorship-list-heading" className="mt-12">
|
|
<h2 id="sponsorship-list-heading" className="sr-only">
|
|
{t.overview.listTitle}
|
|
</h2>
|
|
{hasPending ? (
|
|
<p role="status" className="mb-5 flex items-start gap-3 border border-honey/30 bg-honey/5 px-4 py-3 text-sm text-ink/75">
|
|
<span aria-hidden="true" className="motion-pulse mt-1.5 h-1.5 w-1.5 shrink-0 rounded-full bg-honey animate-[wh-pulse_2s_ease-out_infinite]" />
|
|
<span>
|
|
<span className="text-heading">{t.overview.pendingTitle}.</span> {t.overview.pendingHint}
|
|
</span>
|
|
</p>
|
|
) : null}
|
|
<ul className="space-y-5">
|
|
{sponsorships.map((sponsorship, index) => (
|
|
<SponsorshipRow key={sponsorship.id} sponsorship={sponsorship} locale={locale} t={t} index={index} />
|
|
))}
|
|
</ul>
|
|
</section>
|
|
) : null}
|
|
|
|
{sponsorships.length > 0 || loyalty.regular.level || loyalty.subscription?.level ? (
|
|
<MedalShelf sponsorships={sponsorships} loyalty={loyalty} t={t} />
|
|
) : null}
|
|
|
|
{hasPending ? <PendingRefresher /> : null}
|
|
</Container>
|
|
</div>
|
|
);
|
|
}
|