diff --git a/src/app/(frontend)/korzyna/page.tsx b/src/app/(frontend)/korzyna/page.tsx new file mode 100644 index 0000000..553e04d --- /dev/null +++ b/src/app/(frontend)/korzyna/page.tsx @@ -0,0 +1,323 @@ +'use client' + +import { useState } from 'react' +import Link from 'next/link' +import { useCart } from '@/context/CartContext' + +const FONT_MONT = { fontFamily: 'var(--font-montserrat, Montserrat), sans-serif' } + +function getUtmParams(): Record { + if (typeof window === 'undefined') return {} + const p = new URLSearchParams(window.location.search) + return { + utmSource: p.get('utm_source') ?? undefined, + utmMedium: p.get('utm_medium') ?? undefined, + utmCampaign: p.get('utm_campaign') ?? undefined, + utmContent: p.get('utm_content') ?? undefined, + utmTerm: p.get('utm_term') ?? undefined, + gclid: p.get('gclid') ?? undefined, + } +} + +export default function KorzynaPage() { + const { items, totalCount, totalPrice, updateCount, removeItem, clearCart, hydrated } = useCart() + + const [name, setName] = useState('') + const [phone, setPhone] = useState('') + const [email, setEmail] = useState('') + const [terms, setTerms] = useState(false) + const [errors, setErrors] = useState>({}) + const [submitting, setSubmitting] = useState(false) + const [globalError, setGlobalError] = useState('') + + function validate() { + const e: Record = {} + if (name.trim().length < 2) e.name = "Введіть ваше ім'я" + if (!/^[+\d\s\-().]{9,20}$/.test(phone.trim())) e.phone = 'Введіть коректний номер телефону' + if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.trim())) e.email = 'Введіть коректний email' + return e + } + + async function handleSubmit(e: React.FormEvent) { + e.preventDefault() + setGlobalError('') + const errs = validate() + if (Object.keys(errs).length) { + setErrors(errs) + return + } + setErrors({}) + setSubmitting(true) + + try { + const utm = getUtmParams() + const checkoutItems = items.map((i) => ({ tariff: i.tariffId, count: String(i.count) })) + + const [, checkoutRes] = await Promise.allSettled([ + fetch('/api/leads', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + name: name.trim(), + phone: phone.trim(), + email: email.trim(), + formSource: 'ticket-purchase', + ...utm, + }), + }), + fetch('/api/tickets/checkout', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email: email.trim(), items: checkoutItems }), + }), + ]) + + if (checkoutRes.status === 'rejected') { + throw new Error('Network error') + } + const checkoutResult = checkoutRes.value + if (!checkoutResult.ok) { + const body = await checkoutResult.json().catch(() => ({})) + throw new Error((body as { error?: string }).error ?? 'Помилка оплати') + } + + const { url } = (await checkoutResult.json()) as { url: string } + clearCart() + window.location.href = url + } catch (err) { + setSubmitting(false) + setGlobalError(err instanceof Error ? err.message : 'Щось пішло не так. Спробуйте ще раз.') + } + } + + // Empty state + if (hydrated && items.length === 0) { + return ( +
+
+
🛒
+

+ Кошик порожній +

+

+ Оберіть квитки та додайте їх у кошик +

+ + Обрати квитки + +
+
+ ) + } + + return ( +
+
+

+ Ваш кошик +

+ + {/* Items */} +
+ {items.map((item) => ( +
+ {item.icon && ( + + {item.icon} + + )} +
+

+ {item.name} +

+

+ {item.price} ₴ / шт +

+
+ + {/* Counter */} +
+ + + {item.count} + + +
+ + + {item.price * item.count} ₴ + + + +
+ ))} +
+ + {/* Total */} +
+ + Разом: {totalCount} {totalCount === 1 ? 'квиток' : totalCount < 5 ? 'квитки' : 'квитків'} + + + {totalPrice} ₴ + +
+ + {/* Form */} +
+

+ Контактні дані +

+ +
+ + setName(e.target.value)} + placeholder="Іван Петренко" + className={`w-full rounded-[10px] border px-4 py-3 text-[14px] text-[#272727] outline-none transition-colors focus:border-[#f28b4a] ${errors.name ? 'border-red-400 bg-red-50' : 'border-[#e0d8ce] bg-[#fafaf8]'}`} + style={FONT_MONT} + /> + {errors.name &&

{errors.name}

} +
+ +
+ + setPhone(e.target.value)} + placeholder="+380 67 123 45 67" + className={`w-full rounded-[10px] border px-4 py-3 text-[14px] text-[#272727] outline-none transition-colors focus:border-[#f28b4a] ${errors.phone ? 'border-red-400 bg-red-50' : 'border-[#e0d8ce] bg-[#fafaf8]'}`} + style={FONT_MONT} + /> + {errors.phone &&

{errors.phone}

} +
+ +
+ + setEmail(e.target.value)} + placeholder="ivan@example.com" + className={`w-full rounded-[10px] border px-4 py-3 text-[14px] text-[#272727] outline-none transition-colors focus:border-[#f28b4a] ${errors.email ? 'border-red-400 bg-red-50' : 'border-[#e0d8ce] bg-[#fafaf8]'}`} + style={FONT_MONT} + /> + {errors.email &&

{errors.email}

} +
+ + {/* Terms */} + + + {globalError && ( +
+ {globalError} +
+ )} + + + + {!terms && ( +

+ Погодьтесь з умовами, щоб продовжити +

+ )} + +

+ Після оплати квитки надійдуть на email протягом 5 хвилин +

+
+
+
+ ) +} diff --git a/src/app/(frontend)/kvytky/page.tsx b/src/app/(frontend)/kvytky/page.tsx index 5bdae07..8c8ac0e 100644 --- a/src/app/(frontend)/kvytky/page.tsx +++ b/src/app/(frontend)/kvytky/page.tsx @@ -1,6 +1,6 @@ import type { Metadata } from 'next' -import Link from 'next/link' import { PageHero } from '@/components/ui/PageHero' +import { TariffCardClient } from '@/components/ui/TariffCardClient' export const metadata: Metadata = { title: 'Купити квиток — Шуміленд', @@ -93,7 +93,14 @@ export default async function TicketsPage() {
{items.map((tariff) => ( - + ))}
@@ -105,38 +112,3 @@ export default async function TicketsPage() { ) } -function TariffCard({ tariff }: { tariff: Tariff }) { - return ( -
- {tariff.icon && {tariff.icon}} -
-

- {tariff.name} -

-

- {tariff.price} ₴ -

-
- - -
- ) -} - -function CheckoutButton({ tariffId }: { tariffId: string }) { - return ( - - Придбати - - ) -} diff --git a/src/app/(frontend)/layout.tsx b/src/app/(frontend)/layout.tsx index 51747c9..7058279 100644 --- a/src/app/(frontend)/layout.tsx +++ b/src/app/(frontend)/layout.tsx @@ -7,6 +7,7 @@ import { Footer } from '@/components/layout/Footer' import { GoogleAnalytics } from '@/components/analytics/GoogleAnalytics' import { BinotelWidget } from '@/components/analytics/BinotelWidget' import { getSiteSettings } from '@/lib/getSiteSettings' +import { CartProvider } from '@/context/CartContext' const montserrat = Montserrat({ subsets: ['latin', 'cyrillic'], @@ -51,9 +52,11 @@ export default async function FrontendLayout({ children }: { children: React.Rea > {settings.ga4Id && } {settings.binotelId && } -
-
{children}
-