'use client'; import { useEffect, useMemo, useRef, useState, type FormEvent } from 'react'; import type { Flat } from '../lib/flats'; // Страница /ru/apartments: квартиры AREV из CRM, галерея фото, описание и запись на просмотр. // Страница только на русском (решение владельца), поэтому тексты здесь, а не в словарях i18n. const TIMES = [ { id: 'morning', label: 'Утро', hint: '10:00–13:00' }, { id: 'day', label: 'День', hint: '13:00–17:00' }, { id: 'evening', label: 'Вечер', hint: '17:00–20:00' }, ] as const; type TimeId = (typeof TIMES)[number]['id']; const ERRORS: Record = { CONTACT_REQUIRED: 'Укажите телефон или Telegram — так агент сможет с вами связаться.', BAD_PHONE: 'Похоже, в телефоне ошибка. Пример: +374 91 123456.', BAD_TELEGRAM: 'Ник в Telegram — латиницей, например @anna_ru.', BAD_DAY: 'Этот день уже недоступен — выберите другой.', OBJECT_NOT_FOUND: 'Эту квартиру только что сняли с показа. Выберите другую.', NOT_FOUND: 'Эту квартиру только что сняли с показа. Выберите другую.', TOO_MANY: 'Слишком много заявок подряд. Попробуйте через несколько минут.', }; const FALLBACK_ERROR = 'Не получилось отправить заявку. Попробуйте ещё раз или напишите нам в Telegram.'; const SYMBOL: Record = { USD: '$', AMD: '֏', RUB: '₽', EUR: '€', }; function price(f: Flat) { if (!f.price) return 'Цена по запросу'; const n = Math.round(f.price).toLocaleString('ru-RU'); const c = f.currency ?? 'AMD'; const sum = c === 'USD' || c === 'EUR' ? `${SYMBOL[c]}${n}` : `${n} ${SYMBOL[c] ?? c}`; return f.dealType === 'rent' ? `${sum} / мес` : sum; } const KIND: Record = { apartment: 'квартира', house: 'дом', commercial: 'помещение', }; function title(f: Flat) { const kind = KIND[f.propertyType ?? 'apartment'] ?? 'объект'; return f.rooms ? `${f.rooms}-комн. ${kind}` : kind[0]!.toUpperCase() + kind.slice(1); } function facts(f: Flat) { return [f.area ? `${Math.round(f.area)} м²` : null, f.floor ? (f.floorsTotal ? `этаж ${f.floor}/${f.floorsTotal}` : `этаж ${f.floor}`) : null].filter( Boolean, ) as string[]; } const photo = (f: Flat, i: number) => `/api/flats/photo/${f.id}/${i}`; /** Ближайшие 7 дней, начиная с завтра, по Еревану */ function nextDays() { const ymd = new Intl.DateTimeFormat('en-CA', { timeZone: 'Asia/Yerevan' }); const week = new Intl.DateTimeFormat('ru-RU', { weekday: 'short', timeZone: 'Asia/Yerevan', }); const date = new Intl.DateTimeFormat('ru-RU', { day: 'numeric', month: 'short', timeZone: 'Asia/Yerevan', }); return Array.from({ length: 7 }, (_, k) => { const d = new Date(Date.now() + (k + 1) * 86_400_000); return { id: ymd.format(d), week: k === 0 ? 'завтра' : week.format(d), date: date.format(d).replace('.', ''), }; }); } const ROOMS = [ { id: 0, label: 'Все' }, { id: 1, label: '1 комн.' }, { id: 2, label: '2 комн.' }, { id: 3, label: '3 комн.' }, { id: 4, label: '4+' }, ]; export function Flats({ flats, telegramUrl }: { flats: Flat[]; telegramUrl: string }) { const [rooms, setRooms] = useState(0); const [booking, setBooking] = useState(null); const [viewer, setViewer] = useState<{ flat: Flat; index: number } | null>(null); const shown = useMemo(() => flats.filter((f) => !rooms || (rooms === 4 ? (f.rooms ?? 0) >= 4 : f.rooms === rooms)), [flats, rooms]); const available = useMemo(() => new Set(flats.map((f) => Math.min(f.rooms ?? 0, 4))), [flats]); return ( <>
{ROOMS.filter((r) => !r.id || available.has(r.id)).map((r) => ( ))} {countLabel(shown.length)}
    {shown.map((f) => ( setBooking(f)} onOpen={(index) => setViewer({ flat: f, index })} /> ))}
{viewer && ( setViewer(null)} onBook={() => { setBooking(viewer.flat); setViewer(null); }} /> )} {booking && setBooking(null)} />} ); } function countLabel(n: number) { const m10 = n % 10; const m100 = n % 100; const word = m10 === 1 && m100 !== 11 ? 'вариант' : m10 >= 2 && m10 <= 4 && (m100 < 12 || m100 > 14) ? 'варианта' : 'вариантов'; return `${n} ${word}`; } function FlatCard({ flat: f, onBook, onOpen }: { flat: Flat; onBook: () => void; onOpen: (index: number) => void }) { const [open, setOpen] = useState(false); return (
  • {title(f)}

    {[f.district, ...facts(f)].filter(Boolean).join(' · ')}

    {price(f)}

    {f.text}

  • ); } /** Лента фото: листается пальцем, стрелками и колесом; по нажатию — на весь экран */ function Gallery({ flat: f, onOpen, start = 0, full = false }: { flat: Flat; onOpen?: (index: number) => void; start?: number; full?: boolean }) { const ref = useRef(null); const [index, setIndex] = useState(start); const count = f.photos; useEffect(() => { const el = ref.current; if (el && start) el.scrollTo({ left: start * el.clientWidth, behavior: 'instant' as ScrollBehavior, }); }, [start]); const go = (to: number) => { const el = ref.current; if (!el) return; const next = Math.max(0, Math.min(count - 1, to)); el.scrollTo({ left: next * el.clientWidth, behavior: 'smooth' }); }; useEffect(() => { if (!full) return; const onKey = (e: KeyboardEvent) => { if (e.key === 'ArrowRight') go(index + 1); if (e.key === 'ArrowLeft') go(index - 1); }; window.addEventListener('keydown', onKey); return () => window.removeEventListener('keydown', onKey); }); if (!count) return
    Фото скоро появятся
    ; return (
    { const el = e.currentTarget; setIndex(Math.round(el.scrollLeft / el.clientWidth)); }} > {Array.from({ length: count }, (_, i) => ( ))}
    {count > 1 && ( <> )} {index + 1} / {count}
    ); } function useDialog(onClose: () => void) { const ref = useRef(null); const latest = useRef(onClose); latest.current = onClose; useEffect(() => { const d = ref.current; if (!d) return; if (!d.open) d.showModal(); document.body.style.overflow = 'hidden'; // Esc и закрытие формой — тот же выход, что и крестик const close = () => latest.current(); d.addEventListener('close', close); return () => { d.removeEventListener('close', close); document.body.style.overflow = ''; }; }, []); return ref; } function Viewer({ flat, start, onClose, onBook }: { flat: Flat; start: number; onClose: () => void; onBook: () => void }) { const ref = useDialog(onClose); return ( e.target === e.currentTarget && onClose()}>
    {title(flat)} · {price(flat)}
    ); } function Booking({ flat, telegramUrl, onClose }: { flat: Flat; telegramUrl: string; onClose: () => void }) { const ref = useDialog(onClose); const days = useMemo(nextDays, []); const [day, setDay] = useState(days[0]!.id); const [time, setTime] = useState('evening'); const [state, setState] = useState<'form' | 'sending' | 'done'>('form'); const [error, setError] = useState(null); const submit = async (e: FormEvent) => { e.preventDefault(); const data = new FormData(e.currentTarget); const phone = String(data.get('phone') ?? '').trim(); const telegram = String(data.get('telegram') ?? '').trim(); if (!phone && !telegram) return setError(ERRORS.CONTACT_REQUIRED!); setError(null); setState('sending'); try { const res = await fetch('/api/flats/request', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ leadId: flat.id, day, time, name: data.get('name'), phone, telegram, website: data.get('website'), }), }); const body = (await res.json().catch(() => ({}))) as { code?: string }; if (!res.ok) { setError(ERRORS[body.code ?? ''] ?? FALLBACK_ERROR); return setState('form'); } setState('done'); } catch { setError(FALLBACK_ERROR); setState('form'); } }; const chosen = days.find((d) => d.id === day)!; const slot = TIMES.find((t) => t.id === time)!; return ( e.target === e.currentTarget && onClose()}>
    {state === 'done' ? (

    Заявка принята

    Агент свяжется с вами, чтобы подтвердить просмотр: {chosen.week}, {chosen.date}, {slot.label.toLowerCase()} ({slot.hint}).

    ) : (

    Запись на просмотр

    {title(flat)} {flat.district ? `, ${flat.district}` : ''} · {price(flat)}

    День
    {days.map((d) => ( ))}
    Время
    {TIMES.map((t) => ( ))}
    {error && (

    {error} {error === FALLBACK_ERROR && ( <> {' '} Написать в Telegram )}

    )}

    Нажимая «Записаться», вы соглашаетесь с политикой конфиденциальности. Мы передадим контакты только агенту, который ведёт эту квартиру.

    )}
    ); }