Files

397 lines
16 KiB
TypeScript
Raw Permalink 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 { useEffect, useState } from 'react';
import Link from 'next/link';
import { MessageCircle } from 'lucide-react';
import {
buildDeliveryInquiryMessage,
buildPickupInquiryMessage,
} from '@/application/messaging/whatsapp-message-builder';
import type { FulfillmentMode } from '@/domain/cart/inquiry';
import type { DeliveryInquiryDetails, PickupInquiryDetails } from '@/domain/cart/inquiry';
import {
calculateLineTotal,
formatLineQuantityLabel,
} from '@/domain/shared/order-line';
import { RESTAURANT_CONTACT } from '@/domain/shared/constants';
import { container } from '@/infrastructure/di/container';
import { useCart } from '@/presentation/providers/cart-provider';
import { useLanguage } from '@/presentation/providers/language-provider';
import { getTranslation } from '@/presentation/i18n/translations';
import { getMenuItemName } from '@/application/i18n/menu-localization';
import { useMenu } from '@/presentation/providers/menu-provider';
import CartInquiryModal from '@/components/CartInquiryModal';
import {
buildCartInquiryCopy,
getWhatsAppInquiryLanguage,
getWhatsAppInquiryTranslation,
} from '@/application/messaging/inquiry-language';
import { useCustomerAuth } from '@/presentation/providers/customer-auth-provider';
const CART_INQUIRY_INTENT_KEY = 'shahi-cart-inquiry-intent';
function GoogleIcon({ className }: { className?: string }) {
return (
<svg className={className} viewBox="0 0 24 24" aria-hidden="true">
<path
fill="#4285F4"
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92a5.06 5.06 0 0 1-2.2 3.32v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.1z"
/>
<path
fill="#34A853"
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
/>
<path
fill="#FBBC05"
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
/>
<path
fill="#EA4335"
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
/>
</svg>
);
}
export default function CartDrawer() {
const {
items,
isOpen,
closeCart,
openCart,
totalPrice,
removeFromCart,
updateQuantity,
clearCart,
} = useCart();
const { language, isRtl } = useLanguage();
const { getItemById } = useMenu();
const {
email: customerEmail,
name: customerName,
isAuthenticated,
isLoading: isAuthLoading,
} = useCustomerAuth();
const t = getTranslation(language);
const [inquiryMode, setInquiryMode] = useState<FulfillmentMode | null>(null);
const [authGateMode, setAuthGateMode] = useState<FulfillmentMode | null>(null);
useEffect(() => {
if (isAuthLoading || !isAuthenticated) return;
const intent = sessionStorage.getItem(CART_INQUIRY_INTENT_KEY) as FulfillmentMode | null;
if (intent !== 'pickup' && intent !== 'delivery') return;
sessionStorage.removeItem(CART_INQUIRY_INTENT_KEY);
openCart();
setAuthGateMode(null);
setInquiryMode(intent);
}, [isAuthenticated, isAuthLoading, openCart]);
const openWhatsApp = (message: string | null) => {
if (!message) return;
container.messagingGateway.openWhatsApp(message);
setInquiryMode(null);
};
const startInquiry = (mode: FulfillmentMode) => {
if (isAuthLoading) return;
if (!isAuthenticated) {
setAuthGateMode(mode);
setInquiryMode(null);
return;
}
setAuthGateMode(null);
setInquiryMode(mode);
};
const handleGoogleSignInForInquiry = () => {
if (!authGateMode) return;
try {
sessionStorage.setItem(CART_INQUIRY_INTENT_KEY, authGateMode);
} catch {
// Ignore storage errors.
}
const returnTo = encodeURIComponent(`${window.location.pathname}${window.location.search}`);
window.location.href = `/api/auth/google?returnTo=${returnTo}`;
};
const handlePickupSubmit = (details: PickupInquiryDetails) => {
const inquiryLang = getWhatsAppInquiryLanguage(language);
const inquiryCopy = buildCartInquiryCopy(getWhatsAppInquiryTranslation(language));
openWhatsApp(buildPickupInquiryMessage(items, details, inquiryCopy, inquiryLang));
};
const handleDeliverySubmit = (details: DeliveryInquiryDetails) => {
const inquiryLang = getWhatsAppInquiryLanguage(language);
const inquiryCopy = buildCartInquiryCopy(getWhatsAppInquiryTranslation(language));
openWhatsApp(buildDeliveryInquiryMessage(items, details, inquiryCopy, inquiryLang));
};
if (!isOpen) return null;
return (
<>
<div
className="fixed inset-0 bg-black/40 z-[980]"
onClick={closeCart}
/>
<div
className={`fixed top-0 h-[100dvh] w-full max-w-md bg-[#F8F5F0] z-[990] shadow-2xl flex flex-col ${
isRtl ? 'left-0 border-r border-[#EDE6D9]' : 'right-0 border-l border-[#EDE6D9]'
}`}
>
<div className="flex shrink-0 items-center justify-between border-b border-[#EDE6D9] px-4 py-4 sm:px-6 sm:py-5">
<div className="flex items-center gap-3">
<h2 className="text-2xl tracking-[-0.5px]">{t.cartDrawer.title}</h2>
{items.length > 0 && (
<span className="text-xs px-2.5 py-0.5 rounded-full bg-[#EDE6D9] text-[#6B665F]">
{items.length} {items.length === 1 ? t.cartDrawer.item : t.cartDrawer.items}
</span>
)}
</div>
<div className="flex items-center gap-3">
{items.length > 0 && (
<button
onClick={clearCart}
className="text-xs text-[#B38B4D] hover:underline"
>
{t.cartDrawer.clear}
</button>
)}
<button
onClick={closeCart}
aria-label="Close cart"
className="flex min-h-11 min-w-11 items-center justify-center text-[#6B665F] hover:text-[#2C2A26] text-2xl leading-none touch-manipulation"
>
×
</button>
</div>
</div>
<div className="min-h-0 flex-1 overflow-y-auto overscroll-y-contain px-4 py-4 sm:px-6 sm:py-6">
{items.length === 0 ? (
<div className="flex flex-col items-center justify-center h-full text-center">
<div className="text-6xl mb-4">🛒</div>
<p className="text-lg text-[#6B665F]">{t.cartDrawer.empty}</p>
<Link
href="/menu"
onClick={closeCart}
className="mt-6 text-[#B38B4D] hover:underline"
>
{t.cartDrawer.browseMenu}
</Link>
</div>
) : (
<div className="space-y-6">
{items.map((item) => (
<div key={item.id} className="flex gap-4 border-b border-[#EDE6D9] pb-6">
<div className="flex-1">
<div className="flex justify-between">
<div>
<h4 className="font-medium tracking-[-0.3px]">
{getMenuItemName(language, {
id: item.id,
name: getItemById(item.id)?.name ?? item.name,
})}
</h4>
<p className="text-sm text-[#6B665F]">
{item.pricingMode === 'weight'
? `${item.pricePerHalfKg ?? item.price} kr / ½ kg`
: `${item.price} kr × ${item.quantity}`}
</p>
</div>
<div className="text-right font-medium">
{calculateLineTotal(item)} kr
</div>
</div>
<div className="flex items-center gap-3 mt-3">
<button
onClick={() => updateQuantity(item.id, item.quantity - 1)}
className="w-11 h-11 flex items-center justify-center border border-[#EDE6D9] rounded hover:bg-white transition touch-manipulation"
>
</button>
<span className="min-w-12 text-center font-medium tabular-nums">
{formatLineQuantityLabel(item)}
</span>
<button
onClick={() => updateQuantity(item.id, item.quantity + 1)}
className="w-11 h-11 flex items-center justify-center border border-[#EDE6D9] rounded hover:bg-white transition touch-manipulation"
>
+
</button>
<button
onClick={() => removeFromCart(item.id)}
className="ml-auto text-xs text-[#B38B4D] hover:underline"
>
{t.cartDrawer.remove}
</button>
</div>
</div>
</div>
))}
</div>
)}
</div>
{items.length > 0 && (
<div className="shrink-0 border-t border-[#EDE6D9] bg-white px-4 py-4 pb-[max(1rem,env(safe-area-inset-bottom))] sm:px-6 sm:py-6">
<button
onClick={clearCart}
className="text-xs text-[#8A8478] hover:text-[#B38B4D] mb-3 underline"
>
{t.cartDrawer.clearBasket}
</button>
<div className="flex justify-between text-lg font-medium mb-4">
<span>{t.cartDrawer.total}</span>
<span>{totalPrice.toFixed(0)} kr</span>
</div>
{authGateMode ? (
<div className="mb-3 rounded-2xl border border-[#EDE6D9] bg-gradient-to-br from-[#FFFCF7] to-[#FFF6DC]/50 p-4 text-center sm:p-5">
<h3 className="mb-2 font-serif text-base tracking-tight text-[#101724] sm:text-lg">
{t.cartDrawer.signInRequiredTitle}
</h3>
<p className="mb-4 text-sm leading-relaxed text-[#6B665F]">
{t.cartDrawer.signInRequiredSubtitle}
</p>
<button
type="button"
onClick={handleGoogleSignInForInquiry}
className="mb-3 flex w-full items-center justify-center gap-3 rounded-2xl border border-[#EDE6D9] bg-white px-4 py-3.5 text-sm font-semibold text-[#101724] shadow-sm transition hover:border-[#c99a2e]/40 hover:bg-[#FFFCF7] active:scale-[0.985] min-h-[52px] touch-manipulation"
>
<GoogleIcon className="h-5 w-5 shrink-0" />
<span className="text-left leading-snug">{t.auth.customer.signInWithGoogle}</span>
</button>
<button
type="button"
onClick={() => setAuthGateMode(null)}
className="inline-flex min-h-[44px] w-full items-center justify-center text-sm font-medium text-[#6B665F] hover:text-[#101724] active:underline touch-manipulation"
>
{t.cartDrawer.inquiryCancel}
</button>
</div>
) : (
<>
<p className="text-xs text-center text-[#6B665F] mb-4">
{t.cartDrawer.inquiryHint}
</p>
<button
type="button"
onClick={() => startInquiry('pickup')}
disabled={isAuthLoading}
className="btn-primary mb-2 flex w-full min-h-[52px] items-center justify-center gap-2 rounded-full py-4 text-base font-medium tracking-[0.5px] touch-manipulation active:scale-[0.985] disabled:opacity-60"
>
<MessageCircle className="h-5 w-5" aria-hidden />
{t.cartDrawer.pickupInquiry}
</button>
<button
type="button"
onClick={() => startInquiry('delivery')}
disabled={isAuthLoading}
className="btn-outline mb-2 flex w-full min-h-[52px] items-center justify-center gap-2 rounded-full border-[#25D366] py-4 text-base font-medium tracking-[0.5px] text-[#128C7E] hover:bg-[#25D366]/10 touch-manipulation active:scale-[0.985] disabled:opacity-60"
>
<MessageCircle className="h-5 w-5" aria-hidden />
{t.cartDrawer.deliveryInquiry}
</button>
</>
)}
<button
onClick={() => window.open(`tel:${RESTAURANT_CONTACT.phonePrimary}`, '_self')}
className="btn-outline w-full py-3 rounded-full text-sm tracking-[0.5px] font-medium mb-3"
>
{t.cartDrawer.call}
</button>
<button
onClick={closeCart}
className="w-full text-sm text-[#6B665F] hover:text-[#2C2A26] pt-1"
>
{t.cartDrawer.continueBrowsing}
</button>
</div>
)}
</div>
{inquiryMode && isAuthenticated && (
<CartInquiryModal
mode={inquiryMode}
isOpen={!!inquiryMode}
language={language}
customerEmail={customerEmail}
customerName={customerName}
onClose={() => setInquiryMode(null)}
onSubmitPickup={handlePickupSubmit}
onSubmitDelivery={handleDeliverySubmit}
labels={{
titlePickup: t.cartDrawer.inquiryModalTitlePickup,
titleDelivery: t.cartDrawer.inquiryModalTitleDelivery,
signedInAs: t.cartDrawer.signedInAs,
nameLabel: t.cartDrawer.nameLabel,
namePlaceholder: t.cartDrawer.namePlaceholder,
emailLabel: t.cartDrawer.emailLabel,
emailPlaceholder: t.cartDrawer.emailPlaceholder,
emailRequired: t.cartDrawer.emailRequired,
branchLabel: t.cartDrawer.branchLabel,
branchAskim: t.cartDrawer.branchAskim,
branchBackaplan: t.cartDrawer.branchBackaplan,
askimOnlineUnavailable: t.cartDrawer.askimOnlineUnavailable,
addressLabel: t.cartDrawer.addressLabel,
addressPlaceholder: t.cartDrawer.addressPlaceholder,
addressHint:
(t.cartDrawer as { addressHint?: string }).addressHint ??
'Only Gothenburg addresses can be selected. Pick one from the suggestions.',
addressFallbackPlaceholder:
(t.cartDrawer as { addressFallbackPlaceholder?: string }).addressFallbackPlaceholder ??
'Street, postcode, city…',
addressFallbackHint:
(t.cartDrawer as { addressFallbackHint?: string }).addressFallbackHint ??
'Address search is temporarily unavailable. Type your full delivery address manually.',
addressSearching:
(t.cartDrawer as { addressSearching?: string }).addressSearching ??
'Searching addresses…',
addressNoResults:
(t.cartDrawer as { addressNoResults?: string }).addressNoResults ??
'No matching addresses. Try street name and number.',
addressOutsideGothenburg:
(t.cartDrawer as { addressOutsideGothenburg?: string }).addressOutsideGothenburg ??
'This address is outside Gothenburg. We only deliver within the city.',
addressSelectSuggestion:
(t.cartDrawer as { addressSelectSuggestion?: string }).addressSelectSuggestion ??
'Please pick an address from the suggestions.',
addressInvalid:
(t.cartDrawer as { addressInvalid?: string }).addressInvalid ??
'Select a valid Gothenburg address from the list.',
preferredDateLabel: t.cartDrawer.preferredDateLabel,
preferredDatePlaceholder: t.cartDrawer.preferredDatePlaceholder,
dateToday: t.cartDrawer.dateToday,
dateTomorrow: t.cartDrawer.dateTomorrow,
preferredTimeLabel: t.cartDrawer.preferredTimeLabel,
scheduleOptionalHint: t.cartDrawer.scheduleOptionalHint,
submitPickup: t.cartDrawer.submitPickup,
submitDelivery: t.cartDrawer.submitDelivery,
cancel: t.cartDrawer.inquiryCancel,
close: t.cartDrawer.inquiryClose,
nameRequired: t.cartDrawer.nameRequired,
branchRequired: t.cartDrawer.branchRequired,
scheduleIncomplete: t.cartDrawer.scheduleIncomplete,
scheduleTooSoon: t.cartDrawer.scheduleTooSoon,
}}
/>
)}
</>
);
}