feat: Kaufjourney-Optimierungen fuer Kund:innen (Shop, Produktseite, Warenkorb, Checkout)
Konkrete Reibungspunkte entlang der Kauf-Journey behoben: - Shop-Suche als sichtbares, editierbares Feld im Filter-Formular statt nur in der kleinen Kopfzeilen-Suche — Suche/Kategorie/Sortierung/Verfuegbarkeit lassen sich jetzt in beide Richtungen kombinieren, ohne sich gegenseitig zurueckzusetzen; plus "Suche zuruecksetzen"-Link. - Versandkosten- und Lieferzeit-Hinweis direkt auf der Produktdetailseite (bisher erst im Warenkorb/Checkout sichtbar). - Produktgalerie per Touch-Wisch bedienbar, nicht nur ueber die Vorschau-Kacheln (mobile Kaufentscheidung). - Immer sichtbare mobile Kaufleiste auf der Produktseite, damit der Warenkorb-Button auf langen Seiten (Bewertungen, verwandte Produkte) nicht wieder erst erscrollt werden muss. - Live-Lagerbestand (Ausverkauft/Nur noch N) jetzt auch im Cart-Drawer und auf der Warenkorb-Seite sichtbar, nicht nur auf der Produktkarte — neuer Endpunkt /api/shop/cart-stock + useCartStock-Hook. - Checkout- und Adressformulare: autoComplete/inputMode/type auf allen Feldern (given-name, tel, postal-code, ...) fuer Mobil-Autofill und passende Bildschirmtastaturen. - Bestellpositionen in "Meine Bestellungen" und der Bestellverfolgung verlinken jetzt zur Produktseite (samt Bewertungs-Anker) — bisher war die Bewertungsfunktion nach dem Kauf nur durch manuelles Zurueeknavigieren auffindbar. Neue Tests fuer die Lagerbestand-Hinweise im Warenkorb; tsc/vitest/build bleiben gruen (142 Tests).
This commit is contained in:
1 parent
54919d82ed
commit
0b2196460a
16 files changed
+445
-84
No files matched your search
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useState, type FormEvent } from "react";
|
||||
import { useState, type FormEvent, type HTMLAttributes } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Container, SectionHeading, Button } from "@/components/ui";
|
||||
import { TrustBadges } from "@/components/trust-badges";
|
||||
@@ -115,15 +115,37 @@ export function CheckoutForm({
|
||||
<SectionHeading eyebrow={t.checkoutPage.eyebrow} title={t.checkoutPage.yourDetails} />
|
||||
|
||||
<div className="grid sm:grid-cols-2 gap-4">
|
||||
<Field name="firstName" label={t.checkoutPage.firstName} defaultValue={initialValues?.firstName} required />
|
||||
<Field name="lastName" label={t.checkoutPage.lastName} defaultValue={initialValues?.lastName} required />
|
||||
<Field
|
||||
name="firstName"
|
||||
label={t.checkoutPage.firstName}
|
||||
defaultValue={initialValues?.firstName}
|
||||
autoComplete="given-name"
|
||||
required
|
||||
/>
|
||||
<Field
|
||||
name="lastName"
|
||||
label={t.checkoutPage.lastName}
|
||||
defaultValue={initialValues?.lastName}
|
||||
autoComplete="family-name"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="grid sm:grid-cols-2 gap-4">
|
||||
<Field name="email" label={t.checkoutPage.email} type="email" defaultValue={initialValues?.email} required />
|
||||
<Field
|
||||
name="email"
|
||||
label={t.checkoutPage.email}
|
||||
type="email"
|
||||
defaultValue={initialValues?.email}
|
||||
autoComplete="email"
|
||||
required
|
||||
/>
|
||||
<Field
|
||||
name="phone"
|
||||
label={t.checkoutPage.phoneOptional}
|
||||
type="tel"
|
||||
inputMode="tel"
|
||||
defaultValue={initialValues?.phone}
|
||||
autoComplete="tel"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -158,16 +180,35 @@ export function CheckoutForm({
|
||||
name="street"
|
||||
label={t.checkoutPage.streetLabel}
|
||||
defaultValue={initialValues?.street}
|
||||
autoComplete="street-address"
|
||||
required
|
||||
/>
|
||||
<div className="grid sm:grid-cols-2 gap-4">
|
||||
<Field name="zip" label={t.checkoutPage.zipLabel} defaultValue={initialValues?.zip} required />
|
||||
<Field name="city" label={t.checkoutPage.cityLabel} defaultValue={initialValues?.city} required />
|
||||
<Field
|
||||
name="zip"
|
||||
label={t.checkoutPage.zipLabel}
|
||||
defaultValue={initialValues?.zip}
|
||||
autoComplete="postal-code"
|
||||
inputMode="numeric"
|
||||
required
|
||||
/>
|
||||
<Field
|
||||
name="city"
|
||||
label={t.checkoutPage.cityLabel}
|
||||
defaultValue={initialValues?.city}
|
||||
autoComplete="address-level2"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<Field name="discountCode" label={t.checkoutPage.discountCodeLabel} placeholder={t.checkoutPage.discountCodePlaceholder} />
|
||||
<Field
|
||||
name="discountCode"
|
||||
label={t.checkoutPage.discountCodeLabel}
|
||||
placeholder={t.checkoutPage.discountCodePlaceholder}
|
||||
autoComplete="off"
|
||||
/>
|
||||
|
||||
<div>
|
||||
<label className="flex items-center gap-2 text-sm text-ink/70 cursor-pointer">
|
||||
@@ -252,15 +293,21 @@ function Field({
|
||||
name,
|
||||
label,
|
||||
type = "text",
|
||||
inputMode,
|
||||
defaultValue,
|
||||
placeholder,
|
||||
autoComplete,
|
||||
required = false,
|
||||
}: {
|
||||
name: string;
|
||||
label: string;
|
||||
type?: string;
|
||||
/** Zeigt auf Mobilgeraeten die passende Bildschirmtastatur (z.B. Ziffernblock fuer die PLZ). */
|
||||
inputMode?: HTMLAttributes<HTMLInputElement>["inputMode"];
|
||||
defaultValue?: string;
|
||||
placeholder?: string;
|
||||
/** Browser-/Mobil-Autofill fuer Adress- und Kontaktfelder (siehe MDN autofill-Tokens) — ohne das muss jede Bestellung komplett von Hand eingetippt werden. */
|
||||
autoComplete?: string;
|
||||
required?: boolean;
|
||||
}) {
|
||||
return (
|
||||
@@ -269,8 +316,10 @@ function Field({
|
||||
<input
|
||||
name={name}
|
||||
type={type}
|
||||
inputMode={inputMode}
|
||||
defaultValue={defaultValue}
|
||||
placeholder={placeholder}
|
||||
autoComplete={autoComplete}
|
||||
required={required}
|
||||
className="mt-1 w-full border border-ink/20 bg-background px-4 py-3 focus:outline-none focus:border-forest"
|
||||
/>
|
||||
|
||||
Reference in new issue
Block a user