Add password reset, order confirmation email with editable templates, and fix missing account entry points
Password reset uses Payload's built-in forgot/reset-password flow, customized to link to this app instead of the Payload admin. Order confirmation email and the password-reset email's wording both come from a new Payload email-templates collection, editable without a deploy and previewable via Live Preview at /email-preview/[type] (same mechanism as Posts/LegalPages/Testimonials, sample data instead of a real document). Also: order numbers get a random suffix (prevents guessing, motivated by a considered-and-deferred guest order-lookup feature); the discount code field only shows in the cart when a code is actually active (codes now apply via a ?code= link instead of manual entry); and three navigation gaps found while testing — no reachable login link with an empty cart, no logout link anywhere, no way back from profile to order history. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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 `<Suspense>`, 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 `<Suspense>` 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 (`<table>`
|
||||
layout, `style` attributes, no Tailwind/`<style>` block) — most email
|
||||
clients strip external/embedded CSS.
|
||||
- **`/email-preview/[type]/page.tsx`** — entered exclusively from Payload's
|
||||
admin iframe (`EmailTemplates.ts`'s `admin.livePreview.url`), never a
|
||||
real visitor destination (`noindex`). Always reads with `draft: true` so
|
||||
an unsaved admin edit shows immediately. Renders against **sample data**
|
||||
(`SAMPLE_ORDER` in `emailTemplates.ts`) — unlike the other three Live
|
||||
Preview targets, there's no "current" real order/reset-link to preview
|
||||
against generically.
|
||||
- The *real* send always reads the **published** template
|
||||
(`getEmailTemplate()` in `app/lib/payload.ts`, `draft` unset) — a Live
|
||||
Preview edit never affects a live customer email until actually saved.
|
||||
- `npx payload run src/seed-email-templates.ts` (Payload repo) seeds
|
||||
sensible defaults for both rows; `sendOrderConfirmationEmail()` also has
|
||||
a hardcoded fallback for the rare case a fresh install's order arrives
|
||||
before that seed has run.
|
||||
|
||||
### GDPR self-service
|
||||
|
||||
`/konto/profil`'s "Konto & Daten" section:
|
||||
|
||||
Reference in New Issue
Block a user