diff --git a/README.md b/README.md index d42fa65..25f033e 100644 --- a/README.md +++ b/README.md @@ -167,6 +167,16 @@ Applied in `/cart` only (`/checkout` displays the already-applied result, no second input) — real server-side validation, not just a client-side check against Payload's public API, unlike most content on this site. +- **No manual input field anymore** — the Rabattcode section on `/cart` + (`CartContent.tsx`) only renders at all when a code is actually applied; + there's no open "enter a code" box for every visitor (Nutzer-Entscheidung: + less visual noise, and codes are meant to be shared as marketing links, + not guessed/typed in). Instead, `?code=SAVE10` on the `/cart` URL + auto-applies once on arrival (a `useEffect` reading `useSearchParams()` — + requires `/cart`'s `page.tsx` to wrap `CartContent` in ``, a + Next.js requirement for any `useSearchParams()` consumer). A code that + arrives via the URL but turns out invalid/expired still shows an inline + error, just without an input box to attach it to. - **`app/lib/discountServer.ts`** (server-only, imported exclusively by the two route handlers below — never by a `"use client"` component, same reasoning as Live Preview's `next/headers` lesson above) talks to @@ -217,11 +227,18 @@ check against Payload's public API, unlike most content on this site. written to `sessionStorage` for `/bestellbestaetigung` to read once, but its `orderNumber`/`orderDateIso` now come back from that Payload create call, not generated client-side. -- Still not built: real payment processing (the checkout button is - labelled "zahlungspflichtig" but nothing captures a payment) and a - transactional confirmation email — see `project_backend_checkout_plan` - in the assistant's own memory for what's deliberately deferred to a - later stage. +- **Order confirmation email** is sent from `/api/checkout/route.ts` + right after a successful `createOrder()` — fire-and-forget + (`app/lib/orderEmail.ts`'s `sendOrderConfirmationEmail()`), never blocks + or fails the checkout response itself; a send failure alerts admin + instead (`sendCriticalAlert`, lower severity than the "order not + persisted" alert, since the order itself is safe either way). Content + comes from the **published** `order-confirmation` row in Payload's + `email-templates` collection — see "Email templates & Live Preview" + below for how that's edited/previewed. +- Still not built: real payment processing — the checkout button is + labelled "zahlungspflichtig" but nothing actually captures a payment + yet. See `project_backend_checkout_plan` in the assistant's own memory. ## Orders & customer accounts @@ -246,15 +263,31 @@ without leaving the page. the current password via a real login attempt before changing it, doesn't just trust the caller), `cart` (GET/POST, see below), `verify-email`, `resend-verification`, `delete`, `export` (see "Email - verification" and "GDPR self-service" below), and + verification" and "GDPR self-service" below), `forgot-password`, + `reset-password` (see "Password reset" below), and `orders/[orderNumber]` (PATCH — cancel/return-request, see "Order cancellation & returns" below). -- **`/konto/bestellungen`** lists a customer's own orders; - **`/konto/bestellungen/[orderNumber]`** shows one order's full detail - (items, address, totals, `status`). `status` (`received` → `processing` - → `shipped` → `delivered`, plus `cancelled`/`return_requested`/`returned`) - is maintained by hand in the Payload admin for the shipping states — no - shipping-carrier API integration. +- **`/konto/bestellungen`** lists a customer's own orders (status shown as + a colored `OrderStatusBadge.tsx`, plus an "Abmelden" link — + `LogoutButton.tsx`); **`/konto/bestellungen/[orderNumber]`** shows one + order's full detail (items, address, totals, `status`). `status` + (`received` → `processing` → `shipped` → `delivered`, plus + `cancelled`/`return_requested`/`returned`) is maintained by hand in the + Payload admin for the shipping states — no shipping-carrier API + integration. +- **`Navbar.tsx`'s `AccountLink`** (account icon, desktop; "Anmelden"/"Mein + Konto" text link, mobile drawer) is the only *always*-reachable way into + `/konto/*` — added after discovering there previously wasn't one: + `/checkout`'s own login toggle only renders once the cart already has + items (its empty-cart state is an early return with no such toggle), and + `/bestellbestaetigung`'s "Meine Bestellungen ansehen" link only exists + after a completed order. A returning customer with an empty cart and no + recent order had no way to reach the login page at all before this. + Fetches auth state client-side via `/api/account/me` (not through the + server-rendered root layout) specifically so `app/layout.tsx` — otherwise + static/ISR-cacheable — doesn't get forced into per-request dynamic + rendering just to know one icon's href; briefly shows the logged-out + state on first paint until that fetch resolves. - **`/konto/profil`** edits name + the one saved default address (deliberately a single address, not a full address book — see the assistant's memory note on optionally expanding this later), changes the password, shows @@ -316,6 +349,60 @@ a resend link when `!profile.emailVerified`; resending instead (`app/lib/alertAdmin.ts`'s `sendVerificationEmail()` — same Hostinger SMTP, no Payload hook to piggyback on for a plain field update). +### Password reset + +Unlike email verification, this needed no custom flag — `forgotPassword` +doesn't block login, so it's Payload's built-in flow as-is (see the +Payload README's `customers.auth.forgotPassword` section), just with the +email content/destination swapped so the link points here instead of the +Payload admin. `/konto/passwort-vergessen` (`ForgotPasswordForm.tsx`) → +`POST /api/account/forgot-password` → always responds `{ok:true}` +regardless of whether the email exists (same anti-enumeration reasoning as +Payload's own operation — the route must not leak a different response +shape for "no such account", see its own comment). `/konto/passwort-zuruecksetzen?token=...` +(`ResetPasswordForm.tsx`, token read server-side from `searchParams` — +avoids needing a `` boundary, unlike the `?code=` cart case +above which genuinely needs client-side `useSearchParams()`) → +`POST /api/account/reset-password` → Payload logs the customer in on a +successful reset (returns the same `{token, user}` shape as login), so the +session cookie is set immediately, no separate login step. `LoginForm.tsx` +links to `/konto/passwort-vergessen`. + +### Email templates & Live Preview + +Both transactional emails (order confirmation, password reset) read their +subject/heading/body/footer wording from Payload's `email-templates` +collection — editable in the admin without a deploy, with a Live Preview +button using the exact same mechanism as Posts/LegalPages/Testimonials +(`useLivePreview()` from `@payloadcms/live-preview-react`, already a +dependency here for `LivePostContent.tsx`). + +- **`app/lib/emailTemplates.ts`** — pure string-building functions + (`renderOrderConfirmationHtml()`, `renderPasswordResetHtml()`), no + server-only or client-only imports. Used **both** server-side for the + real send (`orderEmail.ts`) **and** client-side for the Live Preview + page — same function, same inputs, so a Live Preview edit and the real + sent email are guaranteed to render identically for order-confirmation + (password-reset's actual send uses Payload's own simple inline template + instead — see that repo's README for why — so its Live Preview + approximates rather than pixel-matches). Inline-styled HTML (`` + layout, `style` attributes, no Tailwind/`