dev #21
@ -13,6 +13,17 @@ import { useAboContractTemplateHtml } from './hooks/useAboContractTemplateHtml'
|
|||||||
import SignaturePad from './components/SignaturePad'
|
import SignaturePad from './components/SignaturePad'
|
||||||
import { Dialog, DialogActions, DialogBody, DialogTitle } from '../../components/dialog'
|
import { Dialog, DialogActions, DialogBody, DialogTitle } from '../../components/dialog'
|
||||||
import { createReferralLink } from '../../referral-management/hooks/generateReferralLink'
|
import { createReferralLink } from '../../referral-management/hooks/generateReferralLink'
|
||||||
|
import SubscribeGuard from '../components/SubscribeGuard'
|
||||||
|
import {
|
||||||
|
COFFEE_SELECTIONS_STORAGE_KEY,
|
||||||
|
COFFEE_SELECTIONS_UNIT_STORAGE_KEY,
|
||||||
|
getOrderPackError,
|
||||||
|
getRemainingMinPacks,
|
||||||
|
MAX_ABO_PACKS,
|
||||||
|
MIN_ABO_PACKS,
|
||||||
|
normalizeStoredSelections,
|
||||||
|
packsToCapsules,
|
||||||
|
} from '../lib/orderRules'
|
||||||
|
|
||||||
const COLORS = ['#1C2B4A', '#233357', '#2A3B66', '#314475', '#3A4F88', '#5B6C9A'];
|
const COLORS = ['#1C2B4A', '#233357', '#2A3B66', '#314475', '#3A4F88', '#5B6C9A'];
|
||||||
|
|
||||||
@ -65,8 +76,17 @@ function pickFirstString(...values: unknown[]): string {
|
|||||||
// ── shared input class
|
// ── shared input class
|
||||||
const inputCls = 'block w-full rounded-xl border border-slate-200 bg-white px-4 py-3 text-sm text-slate-900 shadow-sm placeholder:text-slate-400 focus:ring-2 focus:ring-slate-900 focus:border-transparent';
|
const inputCls = 'block w-full rounded-xl border border-slate-200 bg-white px-4 py-3 text-sm text-slate-900 shadow-sm placeholder:text-slate-400 focus:ring-2 focus:ring-slate-900 focus:border-transparent';
|
||||||
const labelCls = 'block text-sm font-semibold text-slate-700 mb-1';
|
const labelCls = 'block text-sm font-semibold text-slate-700 mb-1';
|
||||||
|
const requiredMarkCls = 'ml-1 text-red-500';
|
||||||
|
|
||||||
export default function SummaryPage() {
|
export default function SummaryPage() {
|
||||||
|
return (
|
||||||
|
<SubscribeGuard>
|
||||||
|
<SummaryPageContent />
|
||||||
|
</SubscribeGuard>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SummaryPageContent() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { coffees, loading, error } = useActiveCoffees();
|
const { coffees, loading, error } = useActiveCoffees();
|
||||||
@ -79,7 +99,6 @@ export default function SummaryPage() {
|
|||||||
const [contractPdfLoading, setContractPdfLoading] = useState(false)
|
const [contractPdfLoading, setContractPdfLoading] = useState(false)
|
||||||
const [contractPdfError, setContractPdfError] = useState<string | null>(null)
|
const [contractPdfError, setContractPdfError] = useState<string | null>(null)
|
||||||
const [selections, setSelections] = useState<Record<string, number>>({});
|
const [selections, setSelections] = useState<Record<string, number>>({});
|
||||||
const [selectedPlanCapsules, setSelectedPlanCapsules] = useState<number>(60);
|
|
||||||
const [signatureDataUrl, setSignatureDataUrl] = useState('')
|
const [signatureDataUrl, setSignatureDataUrl] = useState('')
|
||||||
const [form, setForm] = useState({
|
const [form, setForm] = useState({
|
||||||
firstName: '',
|
firstName: '',
|
||||||
@ -103,6 +122,7 @@ export default function SummaryPage() {
|
|||||||
signingCity: '',
|
signingCity: '',
|
||||||
});
|
});
|
||||||
const [showThanks, setShowThanks] = useState(false);
|
const [showThanks, setShowThanks] = useState(false);
|
||||||
|
const [hasAttemptedSubmit, setHasAttemptedSubmit] = useState(false);
|
||||||
const [guestMailtoHref, setGuestMailtoHref] = useState<string>('')
|
const [guestMailtoHref, setGuestMailtoHref] = useState<string>('')
|
||||||
const [guestInviteLink, setGuestInviteLink] = useState<string>('')
|
const [guestInviteLink, setGuestInviteLink] = useState<string>('')
|
||||||
const [confetti, setConfetti] = useState<{ left: number; delay: number; color: string }[]>([]);
|
const [confetti, setConfetti] = useState<{ left: number; delay: number; color: string }[]>([]);
|
||||||
@ -316,11 +336,14 @@ export default function SummaryPage() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
try {
|
try {
|
||||||
const raw = sessionStorage.getItem('coffeeSelections');
|
const raw = sessionStorage.getItem(COFFEE_SELECTIONS_STORAGE_KEY);
|
||||||
if (raw) setSelections(JSON.parse(raw));
|
const unit = sessionStorage.getItem(COFFEE_SELECTIONS_UNIT_STORAGE_KEY);
|
||||||
const rawPlan = sessionStorage.getItem('coffeeAboSizeCapsules');
|
if (raw) {
|
||||||
const parsedPlan = rawPlan ? Number(rawPlan) : null;
|
const parsed = JSON.parse(raw);
|
||||||
if (parsedPlan && Number.isInteger(parsedPlan) && parsedPlan >= 60 && parsedPlan % 10 === 0) setSelectedPlanCapsules(parsedPlan);
|
if (parsed && typeof parsed === 'object') {
|
||||||
|
setSelections(normalizeStoredSelections(parsed, unit));
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@ -340,9 +363,10 @@ export default function SummaryPage() {
|
|||||||
[selections, coffees]
|
[selections, coffees]
|
||||||
);
|
);
|
||||||
|
|
||||||
const totalCapsules = useMemo(() => selectedEntries.reduce((sum, e) => sum + e.quantity, 0), [selectedEntries])
|
const totalPacks = useMemo(() => selectedEntries.reduce((sum, e) => sum + e.quantity, 0), [selectedEntries])
|
||||||
const totalPacks = totalCapsules / 10
|
const totalCapsules = useMemo(() => packsToCapsules(totalPacks), [totalPacks])
|
||||||
const requiredPacks = selectedPlanCapsules / 10
|
const orderPackError = useMemo(() => getOrderPackError(totalPacks), [totalPacks])
|
||||||
|
const remainingMinPacks = useMemo(() => getRemainingMinPacks(totalPacks), [totalPacks])
|
||||||
|
|
||||||
const rawUserId = user?.id
|
const rawUserId = user?.id
|
||||||
const currentUserId = typeof rawUserId === 'number' ? rawUserId : (typeof rawUserId === 'string' && /^\d+$/.test(rawUserId) ? Number(rawUserId) : undefined)
|
const currentUserId = typeof rawUserId === 'number' ? rawUserId : (typeof rawUserId === 'string' && /^\d+$/.test(rawUserId) ? Number(rawUserId) : undefined)
|
||||||
@ -381,8 +405,8 @@ export default function SummaryPage() {
|
|||||||
return () => { active = false; };
|
return () => { active = false; };
|
||||||
}, [form.country, vatRates]);
|
}, [form.country, vatRates]);
|
||||||
|
|
||||||
const totalPrice = useMemo(() => selectedEntries.reduce((sum, e) => sum + (e.quantity / 10) * e.coffee.pricePer10, 0), [selectedEntries]);
|
const totalPrice = useMemo(() => selectedEntries.reduce((sum, e) => sum + e.quantity * e.coffee.pricePer10, 0), [selectedEntries]);
|
||||||
const shippingFee = useMemo(() => resolveShippingFee(selectedPlanCapsules), [resolveShippingFee, selectedPlanCapsules]);
|
const shippingFee = useMemo(() => resolveShippingFee(totalCapsules), [resolveShippingFee, totalCapsules]);
|
||||||
const netWithShipping = useMemo(() => totalPrice + shippingFee, [totalPrice, shippingFee]);
|
const netWithShipping = useMemo(() => totalPrice + shippingFee, [totalPrice, shippingFee]);
|
||||||
const effectiveTaxRate = isReverseCharge ? 0 : taxRate
|
const effectiveTaxRate = isReverseCharge ? 0 : taxRate
|
||||||
const taxAmount = useMemo(() => totalPrice * effectiveTaxRate, [totalPrice, effectiveTaxRate]);
|
const taxAmount = useMemo(() => totalPrice * effectiveTaxRate, [totalPrice, effectiveTaxRate]);
|
||||||
@ -420,20 +444,50 @@ export default function SummaryPage() {
|
|||||||
const hasRequiredInvoiceFields = form.invoiceSameAsShipping || form.invoiceEmail.trim() !== ''
|
const hasRequiredInvoiceFields = form.invoiceSameAsShipping || form.invoiceEmail.trim() !== ''
|
||||||
const hasSigningCity = form.signingCity.trim() !== ''
|
const hasSigningCity = form.signingCity.trim() !== ''
|
||||||
const hasSignature = signatureDataUrl.trim() !== ''
|
const hasSignature = signatureDataUrl.trim() !== ''
|
||||||
const canSubmit = selectedEntries.length > 0 && totalPacks === requiredPacks && hasRequiredSelfFields && hasRequiredInvoiceFields && hasSigningCity && hasSignature;
|
const canSubmit = selectedEntries.length > 0 && !orderPackError && hasRequiredSelfFields && hasRequiredInvoiceFields && hasSigningCity && hasSignature;
|
||||||
|
const canAttemptSubmit = selectedEntries.length > 0 && !orderPackError && !submitLoading
|
||||||
|
const firstNameError = hasAttemptedSubmit && form.firstName.trim() === ''
|
||||||
|
const lastNameError = hasAttemptedSubmit && form.lastName.trim() === ''
|
||||||
|
const emailError = hasAttemptedSubmit && form.email.trim() === ''
|
||||||
|
const streetError = hasAttemptedSubmit && form.street.trim() === ''
|
||||||
|
const postalCodeError = hasAttemptedSubmit && form.postalCode.trim() === ''
|
||||||
|
const cityError = hasAttemptedSubmit && form.city.trim() === ''
|
||||||
|
const countryError = hasAttemptedSubmit && form.country.trim() === ''
|
||||||
|
const invoiceEmailError = hasAttemptedSubmit && !form.invoiceSameAsShipping && form.invoiceEmail.trim() === ''
|
||||||
|
const signingCityError = hasAttemptedSubmit && !hasSigningCity
|
||||||
|
const signatureError = hasAttemptedSubmit && !hasSignature
|
||||||
|
const getFieldClassName = (hasError: boolean, extraClassName = '') => {
|
||||||
|
const errorClassName = hasError ? 'border-red-400 focus:ring-red-400' : ''
|
||||||
|
return `${inputCls} ${errorClassName} ${extraClassName}`.trim()
|
||||||
|
}
|
||||||
|
const renderLabel = (label: string, required = false) => (
|
||||||
|
<>
|
||||||
|
{label}
|
||||||
|
{required && <span className={requiredMarkCls}>*</span>}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
|
||||||
const backToSelection = () => router.push('/coffee-abonnements');
|
const backToSelection = () => router.push('/coffee-abonnements');
|
||||||
|
|
||||||
const submit = async () => {
|
const submit = async () => {
|
||||||
if (!canSubmit || submitLoading) return
|
if (submitLoading) return
|
||||||
if (totalPacks !== requiredPacks) { setSubmitError(`Order must contain exactly ${requiredPacks} packs (${selectedPlanCapsules} capsules).`); return }
|
setHasAttemptedSubmit(true)
|
||||||
|
if (selectedEntries.length === 0) {
|
||||||
|
setSubmitError(`Order must contain at least ${MIN_ABO_PACKS} packs (${packsToCapsules(MIN_ABO_PACKS)} capsules).`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (orderPackError) { setSubmitError(orderPackError); return }
|
||||||
|
if (!hasRequiredSelfFields || !hasRequiredInvoiceFields) {
|
||||||
|
setSubmitError('Please fill in all required fields.')
|
||||||
|
return
|
||||||
|
}
|
||||||
if (!hasSigningCity) { setSubmitError('Signing city is required.'); return }
|
if (!hasSigningCity) { setSubmitError('Signing city is required.'); return }
|
||||||
if (!hasSignature) { setSubmitError('Signature is required.'); return }
|
if (!hasSignature) { setSubmitError('Signature is required.'); return }
|
||||||
setSubmitError(null)
|
setSubmitError(null)
|
||||||
setSubmitLoading(true)
|
setSubmitLoading(true)
|
||||||
try {
|
try {
|
||||||
const payload: SubscribeAboInput = {
|
const payload: SubscribeAboInput = {
|
||||||
items: selectedEntries.map(entry => ({ coffeeId: entry.coffee.id, quantity: Math.round(entry.quantity / 10) })),
|
items: selectedEntries.map(entry => ({ coffeeId: entry.coffee.id, quantity: entry.quantity })),
|
||||||
billing_interval: 'month', interval_count: 1, is_auto_renew: true, is_for_self: true,
|
billing_interval: 'month', interval_count: 1, is_auto_renew: true, is_for_self: true,
|
||||||
firstName: form.firstName.trim(), lastName: form.lastName.trim(), email: form.email.trim(),
|
firstName: form.firstName.trim(), lastName: form.lastName.trim(), email: form.email.trim(),
|
||||||
street: form.street.trim(), postalCode: form.postalCode.trim(), city: form.city.trim(), country: form.country.trim(),
|
street: form.street.trim(), postalCode: form.postalCode.trim(), city: form.city.trim(), country: form.country.trim(),
|
||||||
@ -451,9 +505,10 @@ export default function SummaryPage() {
|
|||||||
referred_by: typeof currentUserId === 'number' ? currentUserId : undefined,
|
referred_by: typeof currentUserId === 'number' ? currentUserId : undefined,
|
||||||
}
|
}
|
||||||
await subscribeAbo(payload)
|
await subscribeAbo(payload)
|
||||||
|
setHasAttemptedSubmit(false)
|
||||||
setGuestMailtoHref(''); setGuestInviteLink(''); setShowThanks(true);
|
setGuestMailtoHref(''); setGuestInviteLink(''); setShowThanks(true);
|
||||||
try { sessionStorage.removeItem('coffeeSelections'); } catch {}
|
try { sessionStorage.removeItem(COFFEE_SELECTIONS_STORAGE_KEY); } catch {}
|
||||||
try { sessionStorage.removeItem('coffeeAboSizeCapsules'); } catch {}
|
try { sessionStorage.removeItem(COFFEE_SELECTIONS_UNIT_STORAGE_KEY); } catch {}
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
setSubmitError(e?.message || 'Subscription could not be created.');
|
setSubmitError(e?.message || 'Subscription could not be created.');
|
||||||
} finally {
|
} finally {
|
||||||
@ -467,7 +522,7 @@ export default function SummaryPage() {
|
|||||||
className="min-h-screen"
|
className="min-h-screen"
|
||||||
style={{ background: 'radial-gradient(circle at top left,rgba(251,191,36,0.10),transparent 22%),radial-gradient(circle at top right,rgba(56,189,248,0.10),transparent 24%),linear-gradient(180deg,#f8fafc 0%,#f8fafc 50%,#eef2ff 100%)' }}
|
style={{ background: 'radial-gradient(circle at top left,rgba(251,191,36,0.10),transparent 22%),radial-gradient(circle at top right,rgba(56,189,248,0.10),transparent 24%),linear-gradient(180deg,#f8fafc 0%,#f8fafc 50%,#eef2ff 100%)' }}
|
||||||
>
|
>
|
||||||
<div className="max-w-[1820px] mx-auto px-4 sm:px-6 xl:px-10 py-8 space-y-5">
|
<div className="max-w-455 mx-auto px-4 sm:px-6 xl:px-10 py-8 space-y-5">
|
||||||
|
|
||||||
{/* Header card */}
|
{/* Header card */}
|
||||||
<div className="rounded-[28px] border border-white/80 bg-white/90 px-8 py-6 shadow-[0_24px_70px_-40px_rgba(15,23,42,0.3)] backdrop-blur flex flex-wrap items-center justify-between gap-4">
|
<div className="rounded-[28px] border border-white/80 bg-white/90 px-8 py-6 shadow-[0_24px_70px_-40px_rgba(15,23,42,0.3)] backdrop-blur flex flex-wrap items-center justify-between gap-4">
|
||||||
@ -541,32 +596,32 @@ export default function SummaryPage() {
|
|||||||
|
|
||||||
<div className="grid gap-4 sm:grid-cols-2">
|
<div className="grid gap-4 sm:grid-cols-2">
|
||||||
<div>
|
<div>
|
||||||
<label className={labelCls}>{t('autofix.kfe9527d8')}</label>
|
<label className={labelCls}>{renderLabel(t('autofix.kfe9527d8'), true)}</label>
|
||||||
<input name="firstName" value={form.firstName} onChange={handleInput} className={inputCls} />
|
<input name="firstName" value={form.firstName} onChange={handleInput} className={getFieldClassName(firstNameError)} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className={labelCls}>{t('autofix.k6a2c64e8')}</label>
|
<label className={labelCls}>{renderLabel(t('autofix.k6a2c64e8'), true)}</label>
|
||||||
<input name="lastName" value={form.lastName} onChange={handleInput} className={inputCls} />
|
<input name="lastName" value={form.lastName} onChange={handleInput} className={getFieldClassName(lastNameError)} />
|
||||||
</div>
|
</div>
|
||||||
<div className="sm:col-span-2">
|
<div className="sm:col-span-2">
|
||||||
<label className={labelCls}>Email</label>
|
<label className={labelCls}>{renderLabel('Email', true)}</label>
|
||||||
<input type="email" name="email" value={form.email} onChange={handleInput} className={inputCls} />
|
<input type="email" name="email" value={form.email} onChange={handleInput} className={getFieldClassName(emailError)} />
|
||||||
</div>
|
</div>
|
||||||
<div className="sm:col-span-2">
|
<div className="sm:col-span-2">
|
||||||
<label className={labelCls}>{t('autofix.kd1a2772d')}</label>
|
<label className={labelCls}>{renderLabel(t('autofix.kd1a2772d'), true)}</label>
|
||||||
<input name="street" value={form.street} onChange={handleInput} className={inputCls} />
|
<input name="street" value={form.street} onChange={handleInput} className={getFieldClassName(streetError)} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className={labelCls}>ZIP</label>
|
<label className={labelCls}>{renderLabel('ZIP', true)}</label>
|
||||||
<input name="postalCode" value={form.postalCode} onChange={handleInput} className={inputCls} />
|
<input name="postalCode" value={form.postalCode} onChange={handleInput} className={getFieldClassName(postalCodeError)} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className={labelCls}>City</label>
|
<label className={labelCls}>{renderLabel('City', true)}</label>
|
||||||
<input name="city" value={form.city} onChange={handleInput} className={inputCls} />
|
<input name="city" value={form.city} onChange={handleInput} className={getFieldClassName(cityError)} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className={labelCls}>Country</label>
|
<label className={labelCls}>{renderLabel('Country', true)}</label>
|
||||||
<select name="country" value={form.country} onChange={handleInput} className={inputCls}>
|
<select name="country" value={form.country} onChange={handleInput} className={getFieldClassName(countryError)}>
|
||||||
{countryOptions.map(code => <option key={code} value={code}>{code}</option>)}
|
{countryOptions.map(code => <option key={code} value={code}>{code}</option>)}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
@ -635,8 +690,8 @@ export default function SummaryPage() {
|
|||||||
<input name="invoicePhone" value={form.invoicePhone} onChange={handleInput} className={inputCls} />
|
<input name="invoicePhone" value={form.invoicePhone} onChange={handleInput} className={inputCls} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className={labelCls}>Email</label>
|
<label className={labelCls}>{renderLabel('Email', true)}</label>
|
||||||
<input type="email" name="invoiceEmail" value={form.invoiceEmail} onChange={handleInput} className={inputCls} />
|
<input type="email" name="invoiceEmail" value={form.invoiceEmail} onChange={handleInput} className={getFieldClassName(invoiceEmailError)} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -666,27 +721,27 @@ export default function SummaryPage() {
|
|||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<label className={labelCls}>Ort (Signing City) *</label>
|
<label className={labelCls}>{renderLabel('Ort (Signing City)', true)}</label>
|
||||||
<input type="text" name="signingCity" value={form.signingCity} onChange={handleInput}
|
<input type="text" name="signingCity" value={form.signingCity} onChange={handleInput}
|
||||||
className={`${inputCls} max-w-xs ${!hasSigningCity && submitError ? 'border-red-400 focus:ring-red-400' : ''}`}
|
className={getFieldClassName(signingCityError, 'max-w-xs')}
|
||||||
placeholder={t('autofix.k1f0b2c48')} />
|
placeholder={t('autofix.k1f0b2c48')} />
|
||||||
{!hasSigningCity && submitError && <p className="mt-1 text-xs text-red-600">{t('autofix.k516705dd')}</p>}
|
{signingCityError && <p className="mt-1 text-xs text-red-600">{t('autofix.k516705dd')}</p>}
|
||||||
</div>
|
</div>
|
||||||
<SignaturePad value={signatureDataUrl} onChange={setSignatureDataUrl} required error={!hasSignature && submitError ? 'Signature is required.' : null} />
|
<SignaturePad value={signatureDataUrl} onChange={setSignatureDataUrl} required error={signatureError ? 'Signature is required.' : null} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={submit}
|
onClick={submit}
|
||||||
disabled={!canSubmit || submitLoading}
|
disabled={!canAttemptSubmit}
|
||||||
className={`w-full rounded-xl px-5 py-3.5 text-sm font-semibold transition inline-flex items-center justify-center gap-2 shadow-sm ${
|
className={`w-full rounded-xl px-5 py-3.5 text-sm font-semibold transition inline-flex items-center justify-center gap-2 shadow-sm ${
|
||||||
canSubmit && !submitLoading ? 'bg-slate-900 text-white hover:bg-slate-700' : 'bg-slate-100 text-slate-400 cursor-not-allowed'
|
canAttemptSubmit ? 'bg-slate-900 text-white hover:bg-slate-700' : 'bg-slate-100 text-slate-400 cursor-not-allowed'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{submitLoading && <svg className="w-4 h-4 animate-spin" fill="none" viewBox="0 0 24 24"><circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" /><path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v8z" /></svg>}
|
{submitLoading && <svg className="w-4 h-4 animate-spin" fill="none" viewBox="0 0 24 24"><circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" /><path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v8z" /></svg>}
|
||||||
{submitLoading ? t('autofix.k27b5b842') : t('autofix.k737db983')}
|
{submitLoading ? t('autofix.k27b5b842') : t('autofix.k737db983')}
|
||||||
{!submitLoading && <svg className="w-4 h-4" 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>}
|
{!submitLoading && <svg className="w-4 h-4" 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 && <p className="text-xs text-slate-500 text-center">{t('autofix.k1824f78d')}</p>}
|
{!canSubmit && <p className="text-xs text-slate-500 text-center">{orderPackError ?? 'Fill in all required fields marked with * and provide your signature to finish the subscription.'}</p>}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@ -700,7 +755,7 @@ export default function SummaryPage() {
|
|||||||
{selectedEntries.map(entry => (
|
{selectedEntries.map(entry => (
|
||||||
<div key={entry.coffee.id} className="flex gap-3 items-start">
|
<div key={entry.coffee.id} className="flex gap-3 items-start">
|
||||||
{/* Coffee picture */}
|
{/* Coffee picture */}
|
||||||
<div className="flex-shrink-0 w-16 h-16 rounded-xl overflow-hidden border border-slate-100 bg-slate-50">
|
<div className="shrink-0 w-16 h-16 rounded-xl overflow-hidden border border-slate-100 bg-slate-50">
|
||||||
{entry.coffee.image ? (
|
{entry.coffee.image ? (
|
||||||
<img src={entry.coffee.image} alt={entry.coffee.name} className="w-full h-full object-cover" />
|
<img src={entry.coffee.image} alt={entry.coffee.name} className="w-full h-full object-cover" />
|
||||||
) : (
|
) : (
|
||||||
@ -761,10 +816,13 @@ export default function SummaryPage() {
|
|||||||
|
|
||||||
{/* Pack validation */}
|
{/* Pack validation */}
|
||||||
<div className="rounded-xl border border-slate-100 bg-slate-50 px-3 py-2 text-xs text-slate-600">
|
<div className="rounded-xl border border-slate-100 bg-slate-50 px-3 py-2 text-xs text-slate-600">
|
||||||
<span className="font-semibold">{totalCapsules}</span> capsules selected ({totalPacks} packs). Target: <span className="font-semibold">{selectedPlanCapsules}</span> ({requiredPacks} packs).
|
<span className="font-semibold">{totalPacks.toLocaleString('en-US')}</span> packs selected.
|
||||||
{totalPacks !== requiredPacks && (
|
<div className="text-slate-500">{totalCapsules.toLocaleString('en-US')} capsules total · minimum {MIN_ABO_PACKS} packs · maximum {MAX_ABO_PACKS.toLocaleString('en-US')} packs.</div>
|
||||||
|
{orderPackError && (
|
||||||
<div className="mt-1 rounded-lg bg-red-50 border border-red-200 text-red-700 px-2 py-1 font-medium">
|
<div className="mt-1 rounded-lg bg-red-50 border border-red-200 text-red-700 px-2 py-1 font-medium">
|
||||||
Exactly {requiredPacks} packs required.
|
{remainingMinPacks > 0
|
||||||
|
? `${remainingMinPacks} more pack${remainingMinPacks === 1 ? '' : 's'} needed to reach the minimum order.`
|
||||||
|
: orderPackError}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user