profit-planet-frontend/src/app/coffee-abonnements/page.tsx
2026-02-18 10:24:31 +01:00

355 lines
16 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use client';
import React, { useState, useMemo } from 'react';
import PageLayout from '../components/PageLayout';
import { useRouter } from 'next/navigation';
import { useActiveCoffees } from './hooks/getActiveCoffees';
export default function CoffeeAbonnementPage() {
const [selections, setSelections] = useState<Record<string, number>>({});
const [bump, setBump] = useState<Record<string, boolean>>({});
const [selectedPlanCapsules, setSelectedPlanCapsules] = useState<60 | 120>(120);
const router = useRouter();
// Fetch active coffees from the backend
const { coffees, loading, error } = useActiveCoffees();
const selectedEntries = useMemo(
() =>
Object.entries(selections).map(([id, qty]) => {
const coffee = coffees.find((c) => c.id === id);
if (!coffee) return null;
return { coffee, quantity: qty };
}).filter(Boolean) as { coffee: ReturnType<typeof useActiveCoffees>['coffees'][number]; quantity: number }[],
[selections, coffees]
);
const totalPrice = useMemo(
() =>
selectedEntries.reduce(
(sum, entry) => sum + (entry.quantity / 10) * entry.coffee.pricePer10,
0
),
[selectedEntries]
);
// NEW: enforce selected plan size (60 or 120 capsules)
const totalCapsules = useMemo(
() => selectedEntries.reduce((sum, entry) => sum + entry.quantity, 0),
[selectedEntries]
);
const packsSelected = totalCapsules / 10;
const requiredPacks = selectedPlanCapsules / 10;
const canProceed = packsSelected === requiredPacks;
const proceedToSummary = () => {
if (!canProceed) return;
try {
sessionStorage.setItem('coffeeSelections', JSON.stringify(selections));
sessionStorage.setItem('coffeeAboSizeCapsules', String(selectedPlanCapsules));
} catch {}
router.push('/coffee-abonnements/summary');
};
const toggleCoffee = (id: string) => {
setSelections((prev) => {
const copy = { ...prev };
if (id in copy) {
delete copy[id];
} else {
const total = Object.values(copy).reduce((sum, qty) => sum + qty, 0);
if (total + 10 > selectedPlanCapsules) return prev;
copy[id] = 10;
}
return copy;
});
};
const changeQuantity = (id: string, delta: number) => {
setSelections((prev) => {
if (!(id in prev)) return prev;
const otherTotal = Object.entries(prev).reduce((sum, [key, qty]) => key === id ? sum : sum + qty, 0);
const maxForCoffee = Math.min(120, selectedPlanCapsules - otherTotal);
const next = prev[id] + delta;
if (next < 10 || next > maxForCoffee) return prev;
const updated = { ...prev, [id]: next };
setBump((b) => ({ ...b, [id]: true }));
setTimeout(() => setBump((b) => ({ ...b, [id]: false })), 250);
return updated;
});
};
return (
<PageLayout>
<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">
<span className="text-[#1C2B4A]">Configure Coffee Subscription</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">Selection</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">Summary</span>
</div>
</div>
{/* Section 1: Multi coffee selection + per-coffee quantity */}
<section>
<h2 className="text-xl font-semibold mb-4">1. Select subscription size</h2>
<div className="mb-6 rounded-xl border border-[#1C2B4A]/20 p-4 bg-white/80 backdrop-blur-sm shadow-lg">
<div className="grid gap-3 sm:grid-cols-2">
<button
type="button"
onClick={() => setSelectedPlanCapsules(60)}
className={`rounded-lg border px-4 py-3 text-left transition ${
selectedPlanCapsules === 60
? 'border-[#1C2B4A] bg-[#1C2B4A]/5 text-[#1C2B4A]'
: 'border-gray-300 hover:bg-gray-50'
}`}
>
<div className="font-semibold">60 piece abo</div>
<div className="text-xs text-gray-600">6 packs of 10 capsules</div>
</button>
<button
type="button"
onClick={() => setSelectedPlanCapsules(120)}
className={`rounded-lg border px-4 py-3 text-left transition ${
selectedPlanCapsules === 120
? 'border-[#1C2B4A] bg-[#1C2B4A]/5 text-[#1C2B4A]'
: 'border-gray-300 hover:bg-gray-50'
}`}
>
<div className="font-semibold">120 piece abo</div>
<div className="text-xs text-gray-600">12 packs of 10 capsules</div>
</button>
</div>
</div>
<h2 className="text-xl font-semibold mb-4">2. Choose coffees & quantities</h2>
{error && (
<div className="mb-4 rounded-md border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">
{error}
</div>
)}
{loading ? (
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
<div className="h-44 rounded-xl bg-gray-100 animate-pulse" />
<div className="h-44 rounded-xl bg-gray-100 animate-pulse" />
<div className="h-44 rounded-xl bg-gray-100 animate-pulse" />
</div>
) : (
<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;
const remainingCapsules = selectedPlanCapsules - totalCapsules;
const maxForCoffee = active
? Math.min(120, qty + remainingCapsules)
: 0;
const sliderMax = Math.max(10, maxForCoffee);
const sliderProgress = sliderMax <= 10
? 100
: Math.min(100, Math.max(0, ((qty - 10) / (sliderMax - 10)) * 100));
const canAddCoffee = active || remainingCapsules >= 10;
return (
<div
key={coffee.id}
className={`group rounded-xl border p-4 shadow-sm transition ${
active ? 'border-[#1C2B4A] bg-[#1C2B4A]/5 shadow-md' : 'border-gray-200 bg-white'
}`}
>
<div className="relative overflow-hidden rounded-md mb-3">
{coffee.image ? (
<img
src={coffee.image}
alt={coffee.name}
className="h-36 w-full object-cover transition-transform duration-300 group-hover:scale-105"
loading="lazy"
/>
) : (
<div className="h-36 w-full bg-gray-100 rounded-md" />
)}
{/* price badge (per 10) */}
<div className="absolute top-2 right-2 flex flex-col items-end gap-1">
<span
aria-label={`Price €${coffee.pricePer10} per 10 capsules`}
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-[#1C2B4A]' : 'bg-[#1C2B4A]/80'
}`}
>
{coffee.pricePer10}
</span>
<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">
per 10 pcs
</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)}
disabled={!canAddCoffee}
className={`mt-3 w-full text-xs font-medium rounded px-3 py-2 border transition ${
active
? 'border-[#1C2B4A] text-[#1C2B4A] bg-white hover:bg-[#1C2B4A]/10'
: canAddCoffee
? 'border-gray-300 hover:bg-gray-100'
: 'border-gray-200 bg-gray-100 text-gray-400 cursor-not-allowed'
}`}
>
{active ? 'Remove' : 'Add'}
</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">Quantity (10120)</span>
<span
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'}`}
>
{qty} pcs
</span>
</div>
<div className="flex items-center gap-2">
<button
onClick={() => changeQuantity(coffee.id, -10)}
disabled={qty <= 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={sliderMax}
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,#1C2B4A 0%,#1C2B4A ' +
sliderProgress +
'%,#e5e7eb ' +
sliderProgress +
'%,#e5e7eb 100%)',
height: '6px',
borderRadius: '999px',
}}
/>
</div>
<button
onClick={() => changeQuantity(coffee.id, +10)}
disabled={qty + 10 > maxForCoffee}
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: Compact preview + next steps */}
<section>
<h2 className="text-xl font-semibold mb-4">3. Preview</h2>
<div className="rounded-xl border border-[#1C2B4A]/20 p-6 bg-white/80 backdrop-blur-sm space-y-4 shadow-lg">
{selectedEntries.length === 0 && (
<p className="text-sm text-gray-600">No coffees selected yet.</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-[#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">Total (net)</span>
<span className="text-lg font-extrabold tracking-tight text-[#1C2B4A]">
{totalPrice.toFixed(2)}
</span>
</div>
{/* Packs/capsules summary and validation hint (refined design) */}
<div className="text-xs text-gray-700">
Selected: {totalCapsules} capsules ({packsSelected} packs of 10). Target: {selectedPlanCapsules} capsules ({requiredPacks} packs).
{packsSelected !== requiredPacks && (
<span className="ml-2 inline-flex items-center rounded-md bg-red-50 text-red-700 px-2 py-1 border border-red-200">
Please select exactly {selectedPlanCapsules} capsules ({requiredPacks} packs).
{packsSelected < requiredPacks ? ` ${requiredPacks - packsSelected} packs missing.` : ` ${packsSelected - requiredPacks} packs too many.`}
</span>
)}
</div>
<button
onClick={proceedToSummary}
disabled={!canProceed}
className={`group w-full mt-2 rounded-lg px-4 py-3 font-semibold transition inline-flex items-center justify-center ${
canProceed
? 'bg-[#1C2B4A] text-white hover:bg-[#1C2B4A]/90 shadow-md hover:shadow-lg'
: 'bg-gray-200 text-gray-600 cursor-not-allowed'
}`}
>
Next steps
<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>
{!canProceed && (
<p className="text-xs text-gray-600">
You can continue once exactly {selectedPlanCapsules} capsules ({requiredPacks} packs) are selected.
</p>
)}
</div>
</section>
</div>
</PageLayout>
);
}