feat: add coffee- abo page dummy data

This commit is contained in:
DeathKaioken 2025-11-13 19:35:12 +01:00
parent 05cbe87d60
commit 757b530e14
2 changed files with 361 additions and 1 deletions

View File

@ -0,0 +1,360 @@
'use client';
import React, { useState, useMemo } from 'react';
import PageLayout from '../components/PageLayout';
interface Coffee {
id: string;
name: string;
description: string;
pricePer10: number; // price for 10 units
image: string; // added
}
const coffees: Coffee[] = [
{ id: 'espresso', name: 'Espresso Roast', description: 'Kräftig und intensiv dunkle Schokolade, geröstete Gewürze und ein langer, satt-cremiger Nachhall. Ideal für morgens oder den energiereichen Nachmittagsschub.', pricePer10: 12, image: 'https://images.unsplash.com/photo-1509042239860-f550ce710b93?auto=format&fit=crop&w=400&q=60' },
{ id: 'crema', name: 'Caffè Crema', description: 'Ausgewogen & mild samtiger Körper mit hellen Honig- und Mandelnoten. Ein sanfter, alltagstauglicher Kaffee für jede Uhrzeit und jeden Geschmack.', pricePer10: 11, image: 'https://images.unsplash.com/photo-1524079429932-1e9f39f1f268?auto=format&fit=crop&w=400&q=60' },
{ id: 'colombia', name: 'Colombia Single Origin', description: 'Fruchtige Noten rote Beeren, leichte Zitrus-Säure und ein sauberer, heller Abgang. Gewaschen auf Höhenlagen, perfekt für Filter und French Press.', pricePer10: 13, image: 'https://images.unsplash.com/photo-1511920170033-f8396924c348?auto=format&fit=crop&w=400&q=60' },
{ id: 'ethiopia', name: 'Ethiopia Sidamo', description: 'Florale Eleganz Jasmin, Bergamotte und eine feine Tee-ähnliche Struktur. Komplex und duftend, ideal für Genießer, die Nuancen entdecken möchten.', pricePer10: 14, image: 'https://images.unsplash.com/photo-1510626176961-4b57d4b273c4?auto=format&fit=crop&w=400&q=60' },
{ id: 'sumatra', name: 'Sumatra Mandheling', description: 'Würzig & erdig tiefer Körper, Kakao, Kräuter und dezente Tabaknoten. Vollmundig und schwer, hervorragend für Espresso-Mischungen.', pricePer10: 13, image: 'https://images.unsplash.com/photo-1495474472287-4d71bcdd2085?auto=format&fit=crop&w=400&q=60' },
{ id: 'kenya', name: 'Kenya AA', description: 'Lebendige Säure schwarze Johannisbeere, saftige Traube und spritzige Frische. Klar konturiert und kraftvoll, toll als Pour Over.', pricePer10: 15, image: 'https://images.unsplash.com/photo-1495474472287-4d71bcdd2085?auto=format&fit=crop&w=400&q=60' },
{ id: 'brazil', name: 'Brazil Santos', description: 'Nussig & schokoladig weiche Textur, geröstete Haselnuss und Milchschokolade. Sehr balanciert und vielseitig für Espresso & Milchgetränke.', pricePer10: 11, image: 'https://images.unsplash.com/photo-1509042239860-f550ce710b93?auto=format&fit=crop&w=400&q=60' },
{ id: 'italian', name: 'Italian Roast', description: 'Dunkel & ölig karamellisierte Bitterstoffe, Rauch und intensive Tiefe. Klassisches Profil für kräftigen Cappuccino oder Latte.', pricePer10: 12, image: 'https://images.unsplash.com/photo-1512568400610-62da28bc8a13?auto=format&fit=crop&w=400&q=60' },
{ id: 'house', name: 'House Blend', description: 'Harmonisch & rund milde Süße, ausgewogene Säure und sanfter Körper. Entwickelt als universeller Alltagskaffee für jeden Zubereitungsstil.', pricePer10: 10, image: 'https://images.unsplash.com/photo-1461988091159-110d53f20702?auto=format&fit=crop&w=400&q=60' },
{ id: 'decaf', name: 'Decaf Blend', description: 'Voller Geschmack ohne Koffein. Kakao, leichte Karamell-Noten und sanfter Abgang. Entkoffeiniert im schonenden CO₂-Verfahren.', pricePer10: 12, image: 'https://images.unsplash.com/photo-1497935586351-b67a49e012bf?auto=format&fit=crop&w=400&q=60' },
{ id: 'guatemala', name: 'Guatemala Huehuetenango', description: 'Karamell & Kakao balanciert mit einer feinen Pflaumenfrucht. Angenehme Süße und klare Struktur großartig als Filter und Chemex.', pricePer10: 14, image: 'https://images.unsplash.com/photo-1509978778153-491c9a09a1ab?auto=format&fit=crop&w=400&q=60' },
{ id: 'limited', name: 'Limited Reserve', description: 'Exklusiv & komplex limitierte Mikrolots mit seltenen floralen und tropischen Noten. Für besondere Momente und anspruchsvolle Tassenprofile.', pricePer10: 18, image: 'https://images.unsplash.com/photo-1541167767034-41d7a96c58f1?auto=format&fit=crop&w=400&q=60' }
];
export default function CoffeeAbonnementPage() {
const [selections, setSelections] = useState<Record<string, number>>({});
const [bump, setBump] = useState<Record<string, boolean>>({}); // added: bump animation flags
const [form, setForm] = useState({
firstName: '',
lastName: '',
email: '',
street: '',
postalCode: '',
city: '',
frequency: 'monatlich',
startDate: ''
});
const TAX_RATE = 0.07; // 7% Steuer
const selectedEntries = useMemo(
() =>
Object.entries(selections).map(([id, qty]) => ({
coffee: coffees.find(c => c.id === id)!,
quantity: qty
})),
[selections]
);
const totalPrice = useMemo(
() =>
selectedEntries.reduce(
(sum, entry) => sum + (entry.quantity / 10) * entry.coffee.pricePer10,
0
),
[selectedEntries]
);
const taxAmount = useMemo(() => totalPrice * TAX_RATE, [totalPrice]);
const totalWithTax = useMemo(() => totalPrice + taxAmount, [totalPrice, taxAmount]);
const canSubmit =
selectedEntries.length > 0 &&
Object.values(form).every(v => v.trim() !== '');
const handleInput = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
const { name, value } = e.target;
setForm(prev => ({ ...prev, [name]: value }));
};
const toggleCoffee = (id: string) => {
setSelections(prev => {
const copy = { ...prev };
if (id in copy) {
delete copy[id];
} else {
copy[id] = 10; // default start quantity
}
return copy;
});
};
const changeQuantity = (id: string, delta: number) => {
setSelections(prev => {
if (!(id in prev)) return prev;
const next = prev[id] + delta;
if (next < 10 || next > 120) return prev;
const updated = { ...prev, [id]: next };
setBump(b => ({ ...b, [id]: true })); // trigger bump animation
setTimeout(() => setBump(b => ({ ...b, [id]: false })), 250);
return updated;
});
};
return (
<PageLayout>
<div className="mx-auto max-w-7xl px-4 py-8 space-y-10 bg-white">
<h1 className="text-3xl font-bold tracking-tight">Kaffee Abonnement konfigurieren</h1>
{/* Section 1: Multi coffee selection + per-coffee quantity */}
<section>
<h2 className="text-xl font-semibold mb-4">1. Kaffeesorten & Mengen wählen</h2>
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
{coffees.map(coffee => {
const active = coffee.id in selections;
const qty = selections[coffee.id] || 0;
return (
<div
key={coffee.id}
className={`group rounded-xl border p-4 shadow-sm transition ${
active ? 'border-emerald-600 bg-emerald-50 shadow-md' : 'border-gray-200 bg-white'
}`}
>
<div className="relative overflow-hidden rounded-md mb-3">
<img
src={coffee.image}
alt={coffee.name}
className="h-36 w-full object-cover transition-transform duration-300 group-hover:scale-105"
loading="lazy"
/>
{/* CHANGED: enhanced price badge */}
<div className="absolute top-2 right-2 flex flex-col items-end gap-1">
<span
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 ${
active ? 'bg-emerald-600' : 'bg-emerald-500'
}`}
>
{coffee.pricePer10}
</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">
pro 10 Stk
</span>
</div>
</div>
<div className="flex items-start justify-between">
<h3 className="font-semibold text-sm">{coffee.name}</h3>
</div>
<p className="mt-2 text-xs text-gray-600 leading-relaxed">
{coffee.description}
</p>
<button
type="button"
onClick={() => toggleCoffee(coffee.id)}
className={`mt-3 w-full text-xs font-medium rounded px-3 py-2 border transition ${
active
? 'border-emerald-600 text-emerald-700 bg-white hover:bg-emerald-100'
: 'border-gray-300 hover:bg-gray-100'
}`}
>
{active ? 'Entfernen' : 'Hinzufügen'}
</button>
{active && (
<div className="mt-4 flex flex-col gap-3">
<div className="flex items-center justify-between">
<span className="text-[11px] font-medium text-gray-500">Menge (10120)</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 ${
bump[coffee.id] ? 'scale-110' : 'scale-100'
}`}
>
{qty} Stk
</span>
</div>
<div className="flex items-center gap-2">
<button
onClick={() => changeQuantity(coffee.id, -10)}
className="h-8 w-14 rounded-full bg-gray-100 hover:bg-gray-200 text-xs font-medium transition active:scale-95"
>
-10
</button>
<div className="flex-1 relative">
<input
type="range"
min={10}
max={120}
step={10}
value={qty}
onChange={e =>
changeQuantity(
coffee.id,
parseInt(e.target.value, 10) - qty
)
}
className="w-full appearance-none cursor-pointer bg-transparent"
style={{
background:
'linear-gradient(to right,#059669 0%,#059669 ' +
((qty - 10) / (120 - 10)) * 100 +
'%,#e5e7eb ' +
((qty - 10) / (120 - 10)) * 100 +
'%,#e5e7eb 100%)',
height: '6px',
borderRadius: '999px'
}}
/>
</div>
<button
onClick={() => changeQuantity(coffee.id, +10)}
className="h-8 w-14 rounded-full bg-gray-100 hover:bg-gray-200 text-xs font-medium transition active:scale-95"
>
+10
</button>
</div>
<div className="flex items-center justify-between text-[11px] text-gray-500">
<span>Subtotal</span>
<span className="font-semibold text-gray-700">
{((qty / 10) * coffee.pricePer10).toFixed(2)}
</span>
</div>
</div>
)}
</div>
);
})}
</div>
</section>
{/* Section 2: Form (light styling) */}
<section>
<h2 className="text-xl font-semibold mb-4">2. Deine Daten</h2>
<div className="grid gap-4 sm:grid-cols-2">
<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 && (
<p className="text-sm text-gray-600">Noch keine Kaffees ausgewählt.</p>
)}
{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-emerald-700">{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-bold">{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>
<button
disabled={!canSubmit}
className={`w-full mt-2 rounded-md px-4 py-3 font-semibold transition ${
canSubmit
? 'bg-emerald-600 text-white hover:bg-emerald-500'
: 'bg-gray-300 text-gray-600 cursor-not-allowed'
}`}
>
Abonnement abschließen
</button>
{!canSubmit && (
<p className="text-xs text-gray-500">
Bitte mindestens eine Kaffeesorte wählen und alle Felder ausfüllen.
</p>
)}
</div>
</section>
</div>
</PageLayout>
);
}

View File

@ -25,7 +25,7 @@ export default function PageLayout({
const isMobile = isMobileDevice(); const isMobile = isMobileDevice();
return ( return (
<div className="min-h-screen w-full flex flex-col bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100 transition-colors"> <div className="min-h-screen w-full flex flex-col bg-white text-gray-900">
{showHeader && ( {showHeader && (
<div className="relative z-50 w-full flex-shrink-0"> <div className="relative z-50 w-full flex-shrink-0">