feat: add user coffee abo site
This commit is contained in:
parent
886919e4dc
commit
aa447348b2
@ -1,6 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import React, { useState, useMemo } from 'react';
|
import React, { useState, useMemo } from 'react';
|
||||||
import PageLayout from '../components/PageLayout';
|
import PageLayout from '../components/PageLayout';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
|
||||||
interface Coffee {
|
interface Coffee {
|
||||||
id: string;
|
id: string;
|
||||||
@ -27,18 +28,11 @@ const coffees: Coffee[] = [
|
|||||||
|
|
||||||
export default function CoffeeAbonnementPage() {
|
export default function CoffeeAbonnementPage() {
|
||||||
const [selections, setSelections] = useState<Record<string, number>>({});
|
const [selections, setSelections] = useState<Record<string, number>>({});
|
||||||
const [bump, setBump] = useState<Record<string, boolean>>({}); // added: bump animation flags
|
const [bump, setBump] = useState<Record<string, boolean>>({});
|
||||||
const [form, setForm] = useState({
|
// removed: form state and handlers (moved to summary page)
|
||||||
firstName: '',
|
// removed: canSubmit depending on form
|
||||||
lastName: '',
|
|
||||||
email: '',
|
const router = useRouter(); // added
|
||||||
street: '',
|
|
||||||
postalCode: '',
|
|
||||||
city: '',
|
|
||||||
frequency: 'monatlich',
|
|
||||||
startDate: ''
|
|
||||||
});
|
|
||||||
const TAX_RATE = 0.07; // 7% Steuer
|
|
||||||
|
|
||||||
const selectedEntries = useMemo(
|
const selectedEntries = useMemo(
|
||||||
() =>
|
() =>
|
||||||
@ -57,16 +51,18 @@ export default function CoffeeAbonnementPage() {
|
|||||||
),
|
),
|
||||||
[selectedEntries]
|
[selectedEntries]
|
||||||
);
|
);
|
||||||
|
const TAX_RATE = 0.07; // 7% Steuer
|
||||||
const taxAmount = useMemo(() => totalPrice * TAX_RATE, [totalPrice]);
|
const taxAmount = useMemo(() => totalPrice * TAX_RATE, [totalPrice]);
|
||||||
const totalWithTax = useMemo(() => totalPrice + taxAmount, [totalPrice, taxAmount]);
|
const totalWithTax = useMemo(() => totalPrice + taxAmount, [totalPrice, taxAmount]);
|
||||||
|
|
||||||
const canSubmit =
|
const canProceed = selectedEntries.length > 0; // added
|
||||||
selectedEntries.length > 0 &&
|
|
||||||
Object.values(form).every(v => v.trim() !== '');
|
|
||||||
|
|
||||||
const handleInput = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
|
const proceedToSummary = () => {
|
||||||
const { name, value } = e.target;
|
if (!canProceed) return;
|
||||||
setForm(prev => ({ ...prev, [name]: value }));
|
try {
|
||||||
|
sessionStorage.setItem('coffeeSelections', JSON.stringify(selections));
|
||||||
|
} catch {}
|
||||||
|
router.push('/coffee-abonnements/summary');
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleCoffee = (id: string) => {
|
const toggleCoffee = (id: string) => {
|
||||||
@ -95,8 +91,25 @@ export default function CoffeeAbonnementPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<PageLayout>
|
<PageLayout>
|
||||||
<div className="mx-auto max-w-7xl px-4 py-8 space-y-10 bg-white">
|
<div className="mx-auto max-w-7xl px-4 py-10 space-y-10 bg-gradient-to-b from-white to-[#1C2B4A0D]">
|
||||||
<h1 className="text-3xl font-bold tracking-tight">Kaffee Abonnement konfigurieren</h1>
|
<h1 className="text-3xl font-bold tracking-tight">
|
||||||
|
<span className="text-[#1C2B4A]">
|
||||||
|
Kaffee Abonnement konfigurieren
|
||||||
|
</span>
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
{/* Stepper */}
|
||||||
|
<div className="flex items-center gap-3 text-sm text-gray-600">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<span className="h-8 w-8 rounded-full bg-[#1C2B4A] text-white flex items-center justify-center font-semibold">1</span>
|
||||||
|
<span className="ml-2 font-medium">Auswahl</span>
|
||||||
|
</div>
|
||||||
|
<div className="h-px flex-1 bg-gray-200" />
|
||||||
|
<div className="flex items-center opacity-60">
|
||||||
|
<span className="h-8 w-8 rounded-full bg-gray-200 text-gray-600 flex items-center justify-center font-semibold">2</span>
|
||||||
|
<span className="ml-2 font-medium">Zusammenfassung</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Section 1: Multi coffee selection + per-coffee quantity */}
|
{/* Section 1: Multi coffee selection + per-coffee quantity */}
|
||||||
<section>
|
<section>
|
||||||
@ -109,7 +122,7 @@ export default function CoffeeAbonnementPage() {
|
|||||||
<div
|
<div
|
||||||
key={coffee.id}
|
key={coffee.id}
|
||||||
className={`group rounded-xl border p-4 shadow-sm transition ${
|
className={`group rounded-xl border p-4 shadow-sm transition ${
|
||||||
active ? 'border-emerald-600 bg-emerald-50 shadow-md' : 'border-gray-200 bg-white'
|
active ? 'border-[#1C2B4A] bg-[#1C2B4A]/5 shadow-md' : 'border-gray-200 bg-white'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="relative overflow-hidden rounded-md mb-3">
|
<div className="relative overflow-hidden rounded-md mb-3">
|
||||||
@ -119,17 +132,18 @@ export default function CoffeeAbonnementPage() {
|
|||||||
className="h-36 w-full object-cover transition-transform duration-300 group-hover:scale-105"
|
className="h-36 w-full object-cover transition-transform duration-300 group-hover:scale-105"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
/>
|
/>
|
||||||
{/* CHANGED: enhanced price badge */}
|
{/* price badge */}
|
||||||
<div className="absolute top-2 right-2 flex flex-col items-end gap-1">
|
<div className="absolute top-2 right-2 flex flex-col items-end gap-1">
|
||||||
<span
|
<span
|
||||||
aria-label={`Preis €${coffee.pricePer10} pro 10 Stück`}
|
aria-label={`Preis €${coffee.pricePer10} pro 10 Stück`}
|
||||||
className={`relative inline-flex items-center justify-center rounded-full text-white text-[11px] font-bold px-3 py-1 shadow-lg ring-2 ring-white/50 backdrop-blur-sm transition-transform group-hover:scale-105 ${
|
className={`relative inline-flex items-center justify-center rounded-full text-white text-[11px] font-bold px-3 py-1 shadow-lg ring-2 ring-white/50 backdrop-blur-sm transition-transform group-hover:scale-105 ${
|
||||||
active ? 'bg-emerald-600' : 'bg-emerald-500'
|
active ? 'bg-[#1C2B4A]' : 'bg-[#1C2B4A]/80'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
€{coffee.pricePer10}
|
€{coffee.pricePer10}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-[10px] font-medium px-2 py-0.5 rounded-full bg-emerald-100 text-emerald-700 border border-emerald-300 shadow-sm">
|
{/* CHANGED: solid, readable over images */}
|
||||||
|
<span className="text-[10px] font-medium px-2 py-0.5 rounded-full bg-[#1C2B4A]/90 text-white border border-white/20 shadow-sm backdrop-blur-sm">
|
||||||
pro 10 Stk
|
pro 10 Stk
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@ -145,7 +159,7 @@ export default function CoffeeAbonnementPage() {
|
|||||||
onClick={() => toggleCoffee(coffee.id)}
|
onClick={() => toggleCoffee(coffee.id)}
|
||||||
className={`mt-3 w-full text-xs font-medium rounded px-3 py-2 border transition ${
|
className={`mt-3 w-full text-xs font-medium rounded px-3 py-2 border transition ${
|
||||||
active
|
active
|
||||||
? 'border-emerald-600 text-emerald-700 bg-white hover:bg-emerald-100'
|
? 'border-[#1C2B4A] text-[#1C2B4A] bg-white hover:bg-[#1C2B4A]/10'
|
||||||
: 'border-gray-300 hover:bg-gray-100'
|
: 'border-gray-300 hover:bg-gray-100'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
@ -156,9 +170,7 @@ export default function CoffeeAbonnementPage() {
|
|||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span className="text-[11px] font-medium text-gray-500">Menge (10–120)</span>
|
<span className="text-[11px] font-medium text-gray-500">Menge (10–120)</span>
|
||||||
<span
|
<span
|
||||||
className={`inline-flex items-center justify-center rounded-full bg-emerald-600 text-white px-3 py-1 text-xs font-semibold transition-transform duration-300 ${
|
className={`inline-flex items-center justify-center rounded-full bg-[#1C2B4A] text-white px-3 py-1 text-xs font-semibold transition-transform duration-300 ${bump[coffee.id] ? 'scale-110' : 'scale-100'}`}
|
||||||
bump[coffee.id] ? 'scale-110' : 'scale-100'
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
{qty} Stk
|
{qty} Stk
|
||||||
</span>
|
</span>
|
||||||
@ -186,7 +198,7 @@ export default function CoffeeAbonnementPage() {
|
|||||||
className="w-full appearance-none cursor-pointer bg-transparent"
|
className="w-full appearance-none cursor-pointer bg-transparent"
|
||||||
style={{
|
style={{
|
||||||
background:
|
background:
|
||||||
'linear-gradient(to right,#059669 0%,#059669 ' +
|
'linear-gradient(to right,#1C2B4A 0%,#1C2B4A ' +
|
||||||
((qty - 10) / (120 - 10)) * 100 +
|
((qty - 10) / (120 - 10)) * 100 +
|
||||||
'%,#e5e7eb ' +
|
'%,#e5e7eb ' +
|
||||||
((qty - 10) / (120 - 10)) * 100 +
|
((qty - 10) / (120 - 10)) * 100 +
|
||||||
@ -217,107 +229,19 @@ export default function CoffeeAbonnementPage() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Section 2: Form (light styling) */}
|
{/* Section 2: Compact preview + next steps */}
|
||||||
<section>
|
<section>
|
||||||
<h2 className="text-xl font-semibold mb-4">2. Deine Daten</h2>
|
<h2 className="text-xl font-semibold mb-4">2. Vorschau</h2>
|
||||||
<div className="grid gap-4 sm:grid-cols-2">
|
<div className="rounded-xl border border-[#1C2B4A]/20 p-6 bg-white/80 backdrop-blur-sm space-y-4 shadow-lg">
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium mb-1">Vorname</label>
|
|
||||||
<input
|
|
||||||
name="firstName"
|
|
||||||
value={form.firstName}
|
|
||||||
onChange={handleInput}
|
|
||||||
className="w-full rounded border px-3 py-2 bg-white border-gray-300 focus:outline-none focus:ring-2 focus:ring-emerald-500"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium mb-1">Nachname</label>
|
|
||||||
<input
|
|
||||||
name="lastName"
|
|
||||||
value={form.lastName}
|
|
||||||
onChange={handleInput}
|
|
||||||
className="w-full rounded border px-3 py-2 bg-white border-gray-300 focus:outline-none focus:ring-2 focus:ring-emerald-500"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="sm:col-span-2">
|
|
||||||
<label className="block text-sm font-medium mb-1">E-Mail</label>
|
|
||||||
<input
|
|
||||||
type="email"
|
|
||||||
name="email"
|
|
||||||
value={form.email}
|
|
||||||
onChange={handleInput}
|
|
||||||
className="w-full rounded border px-3 py-2 bg-white border-gray-300 focus:outline-none focus:ring-2 focus:ring-emerald-500"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="sm:col-span-2">
|
|
||||||
<label className="block text-sm font-medium mb-1">Straße & Nr.</label>
|
|
||||||
<input
|
|
||||||
name="street"
|
|
||||||
value={form.street}
|
|
||||||
onChange={handleInput}
|
|
||||||
className="w-full rounded border px-3 py-2 bg-white border-gray-300 focus:outline-none focus:ring-2 focus:ring-emerald-500"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium mb-1">PLZ</label>
|
|
||||||
<input
|
|
||||||
name="postalCode"
|
|
||||||
value={form.postalCode}
|
|
||||||
onChange={handleInput}
|
|
||||||
className="w-full rounded border px-3 py-2 bg-white border-gray-300 focus:outline-none focus:ring-2 focus:ring-emerald-500"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium mb-1">Stadt</label>
|
|
||||||
<input
|
|
||||||
name="city"
|
|
||||||
value={form.city}
|
|
||||||
onChange={handleInput}
|
|
||||||
className="w-full rounded border px-3 py-2 bg-white border-gray-300 focus:outline-none focus:ring-2 focus:ring-emerald-500"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium mb-1">Lieferintervall</label>
|
|
||||||
<select
|
|
||||||
name="frequency"
|
|
||||||
value={form.frequency}
|
|
||||||
onChange={handleInput}
|
|
||||||
className="w-full rounded border px-3 py-2 bg-white border-gray-300 focus:outline-none focus:ring-2 focus:ring-emerald-500"
|
|
||||||
>
|
|
||||||
<option value="monatlich">Monatlich</option>
|
|
||||||
<option value="zweimonatlich">Alle 2 Monate</option>
|
|
||||||
<option value="vierteljährlich">Vierteljährlich</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium mb-1">Startdatum</label>
|
|
||||||
<input
|
|
||||||
type="date"
|
|
||||||
name="startDate"
|
|
||||||
value={form.startDate}
|
|
||||||
onChange={handleInput}
|
|
||||||
className="w-full rounded border px-3 py-2 bg-white border-gray-300 focus:outline-none focus:ring-2 focus:ring-emerald-500"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Section 3: Summary (light styling) */}
|
|
||||||
<section>
|
|
||||||
<h2 className="text-xl font-semibold mb-4">3. Zusammenfassung</h2>
|
|
||||||
<div className="rounded-lg border border-gray-200 p-6 bg-white space-y-4 shadow-sm">
|
|
||||||
{selectedEntries.length === 0 && (
|
{selectedEntries.length === 0 && (
|
||||||
<p className="text-sm text-gray-600">Noch keine Kaffees ausgewählt.</p>
|
<p className="text-sm text-gray-600">Noch keine Kaffees ausgewählt.</p>
|
||||||
)}
|
)}
|
||||||
{selectedEntries.map(entry => (
|
{selectedEntries.map(entry => (
|
||||||
<div
|
<div key={entry.coffee.id} className="flex justify-between text-sm border-b last:border-b-0 pb-2 last:pb-0">
|
||||||
key={entry.coffee.id}
|
|
||||||
className="flex justify-between text-sm border-b last:border-b-0 pb-2 last:pb-0"
|
|
||||||
>
|
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<span className="font-medium">{entry.coffee.name}</span>
|
<span className="font-medium">{entry.coffee.name}</span>
|
||||||
<span className="text-xs text-gray-500">
|
<span className="text-xs text-gray-500">
|
||||||
{entry.quantity} Stk • <span className="inline-flex items-center font-semibold text-emerald-700">€{entry.coffee.pricePer10}/10</span>
|
{entry.quantity} Stk • <span className="inline-flex items-center font-semibold text-[#1C2B4A]">€{entry.coffee.pricePer10}/10</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-right font-semibold">
|
<div className="text-right font-semibold">
|
||||||
@ -327,31 +251,23 @@ export default function CoffeeAbonnementPage() {
|
|||||||
))}
|
))}
|
||||||
<div className="flex justify-between pt-2 border-t">
|
<div className="flex justify-between pt-2 border-t">
|
||||||
<span className="text-sm font-semibold">Gesamt (Netto)</span>
|
<span className="text-sm font-semibold">Gesamt (Netto)</span>
|
||||||
<span className="text-lg font-bold">€{totalPrice.toFixed(2)}</span>
|
<span className="text-lg font-extrabold tracking-tight text-[#1C2B4A]">€{totalPrice.toFixed(2)}</span>
|
||||||
</div>
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<span className="text-sm">Steuer ({(TAX_RATE * 100).toFixed(0)}%)</span>
|
|
||||||
<span className="text-sm font-medium">€{taxAmount.toFixed(2)}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<span className="text-sm font-semibold">Gesamt inkl. Steuer</span>
|
|
||||||
<span className="text-lg font-bold">€{totalWithTax.toFixed(2)}</span>
|
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
disabled={!canSubmit}
|
onClick={proceedToSummary}
|
||||||
className={`w-full mt-2 rounded-md px-4 py-3 font-semibold transition ${
|
disabled={!canProceed}
|
||||||
canSubmit
|
className={`group w-full mt-2 rounded-lg px-4 py-3 font-semibold transition inline-flex items-center justify-center ${
|
||||||
? 'bg-emerald-600 text-white hover:bg-emerald-500'
|
canProceed
|
||||||
: 'bg-gray-300 text-gray-600 cursor-not-allowed'
|
? 'bg-[#1C2B4A] text-white hover:bg-[#1C2B4A]/90 shadow-md hover:shadow-lg'
|
||||||
|
: 'bg-gray-200 text-gray-600 cursor-not-allowed'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
Abonnement abschließen
|
Nächste Schritte
|
||||||
|
<svg className={`ml-2 h-5 w-5 transition-transform ${canProceed ? 'group-hover:translate-x-0.5' : ''}`} viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||||
|
<path fillRule="evenodd" d="M10.293 3.293a1 1 0 011.414 0l5.999 6a1 1 0 010 1.414l-6 6a1 1 0 11-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-4.293-4.293a1 1 0 010-1.414z" clipRule="evenodd" />
|
||||||
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
{!canSubmit && (
|
{!canProceed && <p className="text-xs text-gray-500">Bitte mindestens eine Kaffeesorte wählen.</p>}
|
||||||
<p className="text-xs text-gray-500">
|
|
||||||
Bitte mindestens eine Kaffeesorte wählen und alle Felder ausfüllen.
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
286
src/app/coffee-abonnements/summary/page.tsx
Normal file
286
src/app/coffee-abonnements/summary/page.tsx
Normal file
@ -0,0 +1,286 @@
|
|||||||
|
'use client';
|
||||||
|
import React, { useEffect, useMemo, useState } from 'react';
|
||||||
|
import PageLayout from '../../components/PageLayout';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
|
||||||
|
interface Coffee {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
pricePer10: number;
|
||||||
|
image: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// duplicated from selection page to resolve details by id
|
||||||
|
const coffees: Coffee[] = [
|
||||||
|
{ id: 'espresso', name: 'Espresso Roast', description: '', pricePer10: 12, image: '' },
|
||||||
|
{ id: 'crema', name: 'Caffè Crema', description: '', pricePer10: 11, image: '' },
|
||||||
|
{ id: 'colombia', name: 'Colombia Single Origin', description: '', pricePer10: 13, image: '' },
|
||||||
|
{ id: 'ethiopia', name: 'Ethiopia Sidamo', description: '', pricePer10: 14, image: '' },
|
||||||
|
{ id: 'sumatra', name: 'Sumatra Mandheling', description: '', pricePer10: 13, image: '' },
|
||||||
|
{ id: 'kenya', name: 'Kenya AA', description: '', pricePer10: 15, image: '' },
|
||||||
|
{ id: 'brazil', name: 'Brazil Santos', description: '', pricePer10: 11, image: '' },
|
||||||
|
{ id: 'italian', name: 'Italian Roast', description: '', pricePer10: 12, image: '' },
|
||||||
|
{ id: 'house', name: 'House Blend', description: '', pricePer10: 10, image: '' },
|
||||||
|
{ id: 'decaf', name: 'Decaf Blend', description: '', pricePer10: 12, image: '' },
|
||||||
|
{ id: 'guatemala', name: 'Guatemala Huehuetenango', description: '', pricePer10: 14, image: '' },
|
||||||
|
{ id: 'limited', name: 'Limited Reserve', description: '', pricePer10: 18, image: '' }
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function SummaryPage() {
|
||||||
|
const router = useRouter();
|
||||||
|
const [selections, setSelections] = useState<Record<string, number>>({});
|
||||||
|
const [form, setForm] = useState({
|
||||||
|
firstName: '',
|
||||||
|
lastName: '',
|
||||||
|
email: '',
|
||||||
|
street: '',
|
||||||
|
postalCode: '',
|
||||||
|
city: '',
|
||||||
|
frequency: 'monatlich',
|
||||||
|
startDate: ''
|
||||||
|
});
|
||||||
|
const [showThanks, setShowThanks] = useState(false);
|
||||||
|
const [confetti, setConfetti] = useState<{ left: number; delay: number; color: string }[]>([]);
|
||||||
|
const COLORS = ['#1C2B4A', '#233357', '#2A3B66', '#314475', '#3A4F88', '#5B6C9A']; // dark blue palette
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
try {
|
||||||
|
const raw = sessionStorage.getItem('coffeeSelections');
|
||||||
|
if (raw) setSelections(JSON.parse(raw));
|
||||||
|
} catch {}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!showThanks) return;
|
||||||
|
const items = Array.from({ length: 40 }).map(() => ({
|
||||||
|
left: Math.random() * 100,
|
||||||
|
delay: Math.random() * 0.6,
|
||||||
|
color: COLORS[Math.floor(Math.random() * COLORS.length)],
|
||||||
|
}));
|
||||||
|
setConfetti(items);
|
||||||
|
}, [showThanks]);
|
||||||
|
|
||||||
|
const selectedEntries = useMemo(
|
||||||
|
() =>
|
||||||
|
Object.entries(selections).map(([id, qty]) => {
|
||||||
|
const coffee = coffees.find(c => c.id === id);
|
||||||
|
return coffee ? { coffee, quantity: qty } : null;
|
||||||
|
}).filter(Boolean) as { coffee: Coffee; quantity: number }[],
|
||||||
|
[selections]
|
||||||
|
);
|
||||||
|
|
||||||
|
const TAX_RATE = 0.07;
|
||||||
|
const totalPrice = useMemo(
|
||||||
|
() => selectedEntries.reduce((sum, e) => sum + (e.quantity / 10) * e.coffee.pricePer10, 0),
|
||||||
|
[selectedEntries]
|
||||||
|
);
|
||||||
|
const taxAmount = useMemo(() => totalPrice * TAX_RATE, [totalPrice]);
|
||||||
|
const totalWithTax = useMemo(() => totalPrice + taxAmount, [totalPrice, taxAmount]);
|
||||||
|
|
||||||
|
const handleInput = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
|
||||||
|
const { name, value } = e.target;
|
||||||
|
setForm(prev => ({ ...prev, [name]: value }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const canSubmit =
|
||||||
|
selectedEntries.length > 0 &&
|
||||||
|
Object.values(form).every(v => (typeof v === 'string' ? v.trim() !== '' : true));
|
||||||
|
|
||||||
|
const backToSelection = () => router.push('/coffee-abonnements');
|
||||||
|
|
||||||
|
const submit = () => {
|
||||||
|
if (!canSubmit) return;
|
||||||
|
setShowThanks(true);
|
||||||
|
try { sessionStorage.removeItem('coffeeSelections'); } catch {}
|
||||||
|
// here you would post data to your API
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageLayout>
|
||||||
|
<div className="mx-auto max-w-6xl px-4 py-10 space-y-8 bg-gradient-to-b from-white to-[#1C2B4A0D]">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h1 className="text-3xl font-bold tracking-tight">
|
||||||
|
<span className="text-[#1C2B4A]">Zusammenfassung & Daten</span>
|
||||||
|
</h1>
|
||||||
|
<button
|
||||||
|
onClick={backToSelection}
|
||||||
|
className="rounded-md border border-gray-300 px-3 py-2 text-sm hover:bg-gray-100"
|
||||||
|
>
|
||||||
|
Zurück zur Auswahl
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Stepper */}
|
||||||
|
<div className="flex items-center gap-3 text-sm text-gray-600">
|
||||||
|
<div className="flex items-center opacity-60">
|
||||||
|
<span className="h-8 w-8 rounded-full bg-gray-200 text-gray-600 flex items-center justify-center font-semibold">1</span>
|
||||||
|
<span className="ml-2 font-medium">Auswahl</span>
|
||||||
|
</div>
|
||||||
|
<div className="h-px flex-1 bg-gray-200" />
|
||||||
|
<div className="flex items-center">
|
||||||
|
<span className="h-8 w-8 rounded-full bg-[#1C2B4A] text-white flex items-center justify-center font-semibold">2</span>
|
||||||
|
<span className="ml-2 font-medium">Zusammenfassung</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{selectedEntries.length === 0 ? (
|
||||||
|
<div className="rounded-xl border p-6 bg-white shadow-sm">
|
||||||
|
<p className="text-sm text-gray-600 mb-4">Keine Auswahl gefunden.</p>
|
||||||
|
<button
|
||||||
|
onClick={backToSelection}
|
||||||
|
className="rounded-md bg-[#1C2B4A] text-white px-4 py-2 font-semibold hover:bg-[#1C2B4A]/90"
|
||||||
|
>
|
||||||
|
Zur Auswahl
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="grid gap-8 lg:grid-cols-3">
|
||||||
|
{/* Left: Customer data */}
|
||||||
|
<section className="lg:col-span-2">
|
||||||
|
<h2 className="text-xl font-semibold mb-4">1. Deine Daten</h2>
|
||||||
|
<div className="rounded-xl border border-[#1C2B4A]/20 bg-white/80 backdrop-blur-sm p-6 shadow-lg">
|
||||||
|
<div className="grid gap-4 sm:grid-cols-2">
|
||||||
|
{/* replace focus rings */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium mb-1">Vorname</label>
|
||||||
|
<input name="firstName" value={form.firstName} onChange={handleInput} className="w-full rounded border px-3 py-2 bg-white border-gray-300 focus:outline-none focus:ring-2 focus:ring-[#1C2B4A]" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium mb-1">Nachname</label>
|
||||||
|
<input name="lastName" value={form.lastName} onChange={handleInput} className="w-full rounded border px-3 py-2 bg-white border-gray-300 focus:outline-none focus:ring-2 focus:ring-[#1C2B4A]" />
|
||||||
|
</div>
|
||||||
|
<div className="sm:col-span-2">
|
||||||
|
<label className="block text-sm font-medium mb-1">E-Mail</label>
|
||||||
|
<input type="email" name="email" value={form.email} onChange={handleInput} className="w-full rounded border px-3 py-2 bg-white border-gray-300 focus:outline-none focus:ring-2 focus:ring-[#1C2B4A]" />
|
||||||
|
</div>
|
||||||
|
<div className="sm:col-span-2">
|
||||||
|
<label className="block text-sm font-medium mb-1">Straße & Nr.</label>
|
||||||
|
<input name="street" value={form.street} onChange={handleInput} className="w-full rounded border px-3 py-2 bg-white border-gray-300 focus:outline-none focus:ring-2 focus:ring-[#1C2B4A]" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium mb-1">PLZ</label>
|
||||||
|
<input name="postalCode" value={form.postalCode} onChange={handleInput} className="w-full rounded border px-3 py-2 bg-white border-gray-300 focus:outline-none focus:ring-2 focus:ring-[#1C2B4A]" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium mb-1">Stadt</label>
|
||||||
|
<input name="city" value={form.city} onChange={handleInput} className="w-full rounded border px-3 py-2 bg-white border-gray-300 focus:outline-none focus:ring-2 focus:ring-[#1C2B4A]" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium mb-1">Lieferintervall</label>
|
||||||
|
<select name="frequency" value={form.frequency} onChange={handleInput} className="w-full rounded border px-3 py-2 bg-white border-gray-300 focus:outline-none focus:ring-2 focus:ring-[#1C2B4A]">
|
||||||
|
<option value="monatlich">Monatlich</option>
|
||||||
|
<option value="zweimonatlich">Alle 2 Monate</option>
|
||||||
|
<option value="vierteljährlich">Vierteljährlich</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium mb-1">Startdatum</label>
|
||||||
|
<input type="date" name="startDate" value={form.startDate} onChange={handleInput} className="w-full rounded border px-3 py-2 bg-white border-gray-300 focus:outline-none focus:ring-2 focus:ring-[#1C2B4A]" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={submit}
|
||||||
|
disabled={!canSubmit}
|
||||||
|
className={`group w-full mt-6 rounded-lg px-4 py-3 font-semibold transition inline-flex items-center justify-center ${
|
||||||
|
canSubmit ? 'bg-[#1C2B4A] text-white hover:bg-[#1C2B4A]/90 shadow-md hover:shadow-lg' : 'bg-gray-200 text-gray-600 cursor-not-allowed'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Abonnement abschließen
|
||||||
|
<svg className={`ml-2 h-5 w-5 transition-transform ${canSubmit ? 'group-hover:translate-x-0.5' : ''}`} viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||||
|
<path fillRule="evenodd" d="M10.293 3.293a1 1 0 011.414 0l5.999 6a1 1 0 010 1.414l-6 6a1 1 0 11-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-4.293-4.293a1 1 0 010-1.414z" clipRule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
{!canSubmit && <p className="text-xs text-gray-500 mt-2">Bitte mindestens eine Kaffeesorte wählen und alle Felder ausfüllen.</p>}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Right: Order summary */}
|
||||||
|
<section className="lg:col-span-1">
|
||||||
|
<h2 className="text-xl font-semibold mb-4">2. Deine Auswahl</h2>
|
||||||
|
<div className="rounded-xl border border-[#1C2B4A]/20 bg-white/80 backdrop-blur-sm p-6 shadow-lg lg:sticky lg:top-6">
|
||||||
|
{selectedEntries.map(entry => (
|
||||||
|
<div key={entry.coffee.id} className="flex justify-between text-sm border-b last:border-b-0 pb-2 last:pb-0">
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="font-medium">{entry.coffee.name}</span>
|
||||||
|
<span className="text-xs text-gray-500">
|
||||||
|
{entry.quantity} Stk • <span className="inline-flex items-center font-semibold text-[#1C2B4A]">€{entry.coffee.pricePer10}/10</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-right font-semibold">€{((entry.quantity / 10) * entry.coffee.pricePer10).toFixed(2)}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<div className="flex justify-between pt-2 border-t">
|
||||||
|
<span className="text-sm font-semibold">Gesamt (Netto)</span>
|
||||||
|
<span className="text-lg font-extrabold tracking-tight text-[#1C2B4A]">€{totalPrice.toFixed(2)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-sm">Steuer ({(TAX_RATE * 100).toFixed(0)}%)</span>
|
||||||
|
<span className="text-sm font-medium">€{taxAmount.toFixed(2)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-sm font-semibold">Gesamt inkl. Steuer</span>
|
||||||
|
<span className="text-xl font-extrabold text-[#1C2B4A]">€{totalWithTax.toFixed(2)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Thank you overlay */}
|
||||||
|
{showThanks && (
|
||||||
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm">
|
||||||
|
<div className="relative mx-4 w-full max-w-md rounded-2xl bg-white p-8 text-center shadow-2xl">
|
||||||
|
<div className="pointer-events-none absolute inset-0 overflow-hidden">
|
||||||
|
{confetti.map((c, i) => (
|
||||||
|
<span key={i} className="confetti" style={{ left: `${c.left}%`, animationDelay: `${c.delay}s`, background: c.color }} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-[#1C2B4A]/10 text-[#1C2B4A] pop">
|
||||||
|
<svg viewBox="0 0 24 24" className="h-9 w-9" fill="none" stroke="currentColor" strokeWidth="2">
|
||||||
|
<path d="M20 6L9 17l-5-5" strokeLinecap="round" strokeLinejoin="round" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h3 className="text-2xl font-bold">Danke für dein Abo!</h3>
|
||||||
|
<p className="mt-1 text-sm text-gray-600">Wir haben deine Bestellung erhalten.</p>
|
||||||
|
|
||||||
|
<div className="mt-6 grid gap-3 sm:grid-cols-2">
|
||||||
|
<button onClick={() => { setShowThanks(false); backToSelection(); }} className="rounded-lg bg-[#1C2B4A] text-white px-4 py-2 font-semibold hover:bg-[#1C2B4A]/90">
|
||||||
|
Zur Auswahl
|
||||||
|
</button>
|
||||||
|
<button onClick={() => setShowThanks(false)} className="rounded-lg border border-gray-300 px-4 py-2 font-semibold hover:bg-gray-50">
|
||||||
|
Schließen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style jsx>{`
|
||||||
|
.confetti {
|
||||||
|
position: absolute;
|
||||||
|
top: -10%;
|
||||||
|
width: 8px;
|
||||||
|
height: 12px;
|
||||||
|
border-radius: 2px;
|
||||||
|
opacity: 0.9;
|
||||||
|
animation: fall 1.8s linear forwards;
|
||||||
|
}
|
||||||
|
@keyframes fall {
|
||||||
|
0% { transform: translateY(0) rotate(0deg); }
|
||||||
|
100% { transform: translateY(110vh) rotate(720deg); }
|
||||||
|
}
|
||||||
|
.pop {
|
||||||
|
animation: pop 450ms ease-out forwards;
|
||||||
|
}
|
||||||
|
@keyframes pop {
|
||||||
|
0% { transform: scale(0.6); opacity: 0; }
|
||||||
|
60% { transform: scale(1.08); opacity: 1; }
|
||||||
|
100% { transform: scale(1); }
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</PageLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user