106 lines
5.9 KiB
TypeScript
106 lines
5.9 KiB
TypeScript
import { useState } from 'react'
|
|
import { useNavigate } from 'react-router-dom'
|
|
import { motion } from 'framer-motion'
|
|
import { useTranslation } from 'react-i18next'
|
|
import { Flower2, MapPin, Truck, Store as StoreIcon, Star, ArrowRight } from 'lucide-react'
|
|
import { zoneLookup } from '../api/client'
|
|
import { useShop } from '../store/shop'
|
|
import { PetalLayer, Pressable } from './components/motion'
|
|
import { BRAND } from './brand'
|
|
import LanguageSwitcher from '../i18n/LanguageSwitcher'
|
|
|
|
// Floom 패턴: 진입 즉시 ZIP 입력 → 배송 가능 매장 필터링
|
|
export default function ZipGate() {
|
|
const { t } = useTranslation()
|
|
const [zip, setZip] = useState('')
|
|
const [result, setResult] = useState<any>(null)
|
|
const [loading, setLoading] = useState(false)
|
|
const [err, setErr] = useState('')
|
|
const { setZone } = useShop()
|
|
const nav = useNavigate()
|
|
|
|
const lookup = async (e: React.FormEvent) => {
|
|
e.preventDefault(); setErr(''); setResult(null)
|
|
if (!/^\d{5}$/.test(zip)) { setErr(t('zip.errInvalid')); return }
|
|
setLoading(true)
|
|
try {
|
|
const r = await zoneLookup(zip)
|
|
setResult(r)
|
|
if (!r?.deliverable) setErr(t('zip.errNotDeliverable'))
|
|
} catch { setErr(t('zip.errFailed')) }
|
|
finally { setLoading(false) }
|
|
}
|
|
|
|
const choose = (s: any) => { setZone(zip, s.storeId, s.storeName); nav('/home') }
|
|
|
|
return (
|
|
<div className="relative min-h-screen bg-gradient-to-br from-blush-100 via-cream to-sage-100 flex items-center justify-center px-4 overflow-hidden">
|
|
<div className="absolute top-5 right-5 z-20"><LanguageSwitcher variant="shop" /></div>
|
|
<PetalLayer count={18} />
|
|
|
|
{/* 배경 보태니컬 글로우 */}
|
|
<motion.div aria-hidden className="absolute -top-20 -left-20 text-blush-200/40"
|
|
animate={{ rotate: [0, 360] }} transition={{ duration: 80, repeat: Infinity, ease: 'linear' }}>
|
|
<Flower2 size={320} strokeWidth={0.5} />
|
|
</motion.div>
|
|
<motion.div aria-hidden className="absolute -bottom-24 -right-16 text-sage-300/40"
|
|
animate={{ rotate: [360, 0] }} transition={{ duration: 90, repeat: Infinity, ease: 'linear' }}>
|
|
<Flower2 size={260} strokeWidth={0.5} />
|
|
</motion.div>
|
|
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 24 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.8, ease: [0.22, 1, 0.36, 1] }}
|
|
className="relative z-10 w-full max-w-lg text-center">
|
|
<div className="flex flex-col items-center mb-5">
|
|
<motion.span animate={{ rotate: [0, 8, -6, 0] }} transition={{ duration: 7, repeat: Infinity, ease: 'easeInOut' }}>
|
|
<Flower2 className="text-blush-500" size={44} />
|
|
</motion.span>
|
|
<h1 className="font-serif text-4xl font-bold text-blush-900 mt-3">Montvale Florist</h1>
|
|
<p className="font-display text-[13px] tracking-[0.35em] uppercase text-sage-600 mt-1">{t('zip.since', { year: BRAND.founded })}</p>
|
|
</div>
|
|
|
|
<p className="font-display text-2xl text-[#6b5258] mb-1">{BRAND.tagline}</p>
|
|
<p className="text-[#8a7077] text-sm mb-8">{t('zip.lead')}</p>
|
|
|
|
<form onSubmit={lookup} className="bg-white/90 backdrop-blur rounded-4xl shadow-bloom p-7 border border-blush-100">
|
|
<label className="flex items-center gap-2 text-sm text-blush-700 font-medium mb-3 justify-center"><MapPin size={16} /> {t('zip.enterZip')}</label>
|
|
<div className="flex gap-2">
|
|
<input value={zip} onChange={e => setZip(e.target.value.replace(/\D/g, '').slice(0, 5))}
|
|
placeholder={t('zip.placeholder')} inputMode="numeric" autoFocus
|
|
className="flex-1 px-4 py-3.5 rounded-2xl bg-blush-50 border border-blush-100 text-center text-lg tracking-[0.3em] outline-none focus:border-blush-400 transition-colors" />
|
|
<Pressable type="submit" disabled={loading}
|
|
className="px-7 rounded-2xl bg-blush-500 text-white font-semibold hover:bg-blush-600 disabled:opacity-60 flex items-center gap-1">
|
|
{loading ? '…' : <>{t('zip.go')} <ArrowRight size={16} /></>}
|
|
</Pressable>
|
|
</div>
|
|
{err && <p className="text-blush-500 text-xs mt-3">{err}</p>}
|
|
|
|
{result?.deliverable && (
|
|
<motion.div initial={{ opacity: 0, height: 0 }} animate={{ opacity: 1, height: 'auto' }} className="mt-6 text-left overflow-hidden">
|
|
<div className="flex items-center gap-1.5 text-sm text-sage-700 font-medium mb-3"><Truck size={15} /> {t('zip.availableStores')}</div>
|
|
<div className="space-y-2">
|
|
{(result.stores || []).map((s: any) => (
|
|
<Pressable key={s.storeId} onClick={() => choose(s)}
|
|
className="w-full flex items-center justify-between bg-blush-50 hover:bg-blush-100 border border-blush-100 rounded-2xl px-4 py-3.5 text-left">
|
|
<span>
|
|
<span className="font-medium text-sm flex items-center gap-1.5 text-blush-900"><StoreIcon size={14} className="text-blush-500" />{s.storeName}</span>
|
|
<span className="block text-xs text-[#8a7077] mt-0.5">{t('zip.radiusSameDay', { radius: s.radiusMi, cutoff: s.sameDayCutoff, tz: s.timezone })}</span>
|
|
</span>
|
|
<ArrowRight size={16} className="text-blush-500" />
|
|
</Pressable>
|
|
))}
|
|
</div>
|
|
</motion.div>
|
|
)}
|
|
</form>
|
|
|
|
<div className="flex items-center justify-center gap-2 mt-6 text-[12px] text-[#8a7077]">
|
|
<span className="flex text-gold">{[1, 2, 3, 4, 5].map(i => <Star key={i} size={13} fill="currentColor" />)}</span>
|
|
{BRAND.rating}★ · {t('zip.reviews', { n: BRAND.reviewCount.toLocaleString() })}
|
|
</div>
|
|
<button onClick={() => nav('/home')} className="text-xs text-[#a08a90] hover:text-blush-600 mt-4 underline-offset-2 hover:underline">{t('zip.browsePickup')}</button>
|
|
</motion.div>
|
|
</div>
|
|
)
|
|
}
|