import { useState, useEffect, useCallback } from 'react' import { Wifi, Copy, RefreshCw, CheckCircle, Eye, EyeOff, ChevronDown, ChevronRight, AlertCircle, Info, Shield, Smartphone, Router, Terminal, ExternalLink, BookOpen, Check, X, Settings, ToggleLeft, ToggleRight, } from 'lucide-react' import { api, type WifiSettings } from '../lib/api' import { useAuth } from '../contexts/AuthContext' import { cn } from '../lib/utils' // ── Copy button helper ──────────────────────────────────────────────────────── function CopyBtn({ text }: { text: string }) { const [copied, setCopied] = useState(false) const copy = () => { navigator.clipboard.writeText(text).catch(() => {}) setCopied(true) setTimeout(() => setCopied(false), 2000) } return ( ) } // ── Code block with copy ────────────────────────────────────────────────────── function CodeBlock({ code, lang = 'text' }: { code: string; lang?: string }) { return (
        {code}
      
) } // ── Setup guide tabs ────────────────────────────────────────────────────────── type GuideTabs = 'mikrotik' | 'unifi' | 'universal' function SetupGuide({ apiToken }: { apiToken: string }) { const [tab, setTab] = useState('mikrotik') const verifyUrl = 'https://api.hotelsync.ru/api/wifi-auth/verify' const portalUrl = 'https://wifisync.ru/portal' const tabs: { id: GuideTabs; label: string }[] = [ { id: 'mikrotik', label: 'MikroTik' }, { id: 'unifi', label: 'Ubiquiti UniFi' }, { id: 'universal', label: 'Универсальная' }, ] return (
{/* Tab bar */}
{tabs.map(t => ( ))}
{/* MikroTik */} {tab === 'mikrotik' && (
Инструкция для RouterOS v7+. Hotspot должен быть уже настроен на интерфейсе.

В Winbox: IP → Hotspot → Hotspot Setup. Или через терминал:

Установите URL страницы входа на адрес wifisync.ru:

Добавьте Walled Garden для доступа к порталу без авторизации:

Portal URL {portalUrl}
API Token {apiToken}
)} {/* UniFi */} {tab === 'unifi' && (
Инструкция для UniFi Network Application (Controller). Требуется UniFi AP с поддержкой Guest Portal.

Создайте гостевую сеть: Networks → Create New Network → включите Guest Network.

Settings → Guest Control → Guest Portal → External Portal Server

Custom Portal URL {portalUrl}

Settings → Guest Control → Pre-Authorization Access → добавьте:

Portal URL {portalUrl}
API Token {apiToken}
)} {/* Universal */} {tab === 'universal' && (
Подходит для любого оборудования, поддерживающего внешний captive portal или редирект HTTP.
  1. Гость подключается к вашей гостевой WiFi сети
  2. Роутер перенаправляет HTTP запрос на страницу авторизации wifisync.ru
  3. Гость вводит номер комнаты и фамилию (или дату рождения)
  4. wifisync.ru проверяет данные через HotelSync API
  5. При успехе гость получает доступ в интернет
{portalUrl}?token={apiToken.slice(0, 8)}...

Укажите этот URL в настройках вашего роутера как «Login URL» или «Redirect URL»

Если вы реализуете собственный портал, используйте этот API:

)}
) } function StepBlock({ n, title, children }: { n: number; title: string; children: React.ReactNode }) { return (
{n}

{title}

{children}
) } // ── Main Page ───────────────────────────────────────────────────────────────── export function WiFiPage() { const { user } = useAuth() const slug = user?.hotelSlug ?? '' const [settings, setSettings] = useState(null) const [loading, setLoading] = useState(true) const [saving, setSaving] = useState(false) const [showToken, setShowToken] = useState(false) const [regenerating, setRegenerating] = useState(false) const [showGuide, setShowGuide] = useState(false) const [activeTab, setActiveTab] = useState<'settings' | 'guide'>('settings') // Form state const [authMethod, setAuthMethod] = useState('room_any') const [ssidName, setSsidName] = useState('') const [welcomeText, setWelcomeText] = useState('') const [sessionHours, setSessionHours] = useState('24') const [enabled, setEnabled] = useState(false) const [saveOk, setSaveOk] = useState(false) const load = useCallback(async () => { if (!slug) return try { setLoading(true) const s = await api.wifi.get(slug) setSettings(s) setAuthMethod(s.authMethod) setSsidName(s.ssidName ?? '') setWelcomeText(s.welcomeText ?? '') setSessionHours(String(s.sessionHours)) setEnabled(s.enabled) } catch { // ignore } finally { setLoading(false) } }, [slug]) useEffect(() => { load() }, [load]) const save = async () => { if (!slug) return setSaving(true) try { const updated = await api.wifi.update(slug, { enabled, authMethod, ssidName: ssidName.trim() || null, welcomeText: welcomeText.trim() || null, sessionHours: Math.max(1, Math.min(720, Number(sessionHours) || 24)), }) setSettings(updated) setSaveOk(true) setTimeout(() => setSaveOk(false), 2500) } catch { // ignore } finally { setSaving(false) } } const regenerate = async () => { if (!slug) return if (!window.confirm('Сгенерировать новый токен? Старый токен перестанет работать.')) return setRegenerating(true) try { const { apiToken } = await api.wifi.regenerateToken(slug) setSettings(prev => prev ? { ...prev, apiToken } : prev) } catch { // ignore } finally { setRegenerating(false) } } const toggleEnabled = async () => { const newVal = !enabled setEnabled(newVal) if (!slug) return try { const updated = await api.wifi.update(slug, { enabled: newVal }) setSettings(updated) } catch { setEnabled(!newVal) } } if (loading) { return (
) } const token = settings?.apiToken ?? '' const maskedToken = token ? token.slice(0, 8) + '••••••••••••••••••••••••' + token.slice(-4) : '' const AUTH_METHODS: { id: WifiSettings['authMethod']; label: string; desc: string }[] = [ { id: 'room_any', label: 'Номер комнаты + Фамилия или Дата рождения', desc: 'Гость вводит то, что удобнее. Рекомендуется.' }, { id: 'room_lastname', label: 'Номер комнаты + Фамилия', desc: 'Латиницей или кириллицей — как в базе.' }, { id: 'room_birthdate',label: 'Номер комнаты + Дата рождения', desc: 'Только цифры, гость не ошибётся.' }, { id: 'room_only', label: 'Только номер комнаты', desc: 'Без пароля. Для отелей без требований к безопасности.' }, ] return (
{/* ── Header ─────────────────────────────────────────────────────────── */}

WiFi авторизация

Автоматический вход гостей по номеру комнаты

{/* Enable toggle */}
{/* ── Promo block (when disabled) ────────────────────────────────────── */} {!enabled && (
{[ { icon: Smartphone, title: 'Вход по данным брони', desc: 'Гость вводит номер комнаты и фамилию — никаких паролей' }, { icon: Shield, title: 'Только реальные гости', desc: 'Доступ только активным гостям, сессия до даты выезда' }, { icon: Router, title: 'Любое оборудование', desc: 'Работает с MikroTik, UniFi, TP-Link и любым роутером' }, ].map(({ icon: Icon, title, desc }) => (

{title}

{desc}

))}

Сервис wifisync.ru предоставляет captive portal страницу и берёт на себя авторизацию гостей через HotelSync. Вам нужно только скопировать токен и настроить роутер.

)} {/* ── Tabs ───────────────────────────────────────────────────────────── */}
{([['settings', 'Настройки'], ['guide', 'Инструкция по оборудованию']] as const).map(([id, label]) => ( ))}
{/* ── Settings tab ───────────────────────────────────────────────────── */} {activeTab === 'settings' && (
{/* Token block */}
{showToken ? token : maskedToken}

Вставьте этот токен в настройки wifisync.ru при подключении отеля

https://api.hotelsync.ru/api/wifi-auth/verify

Этот эндпоинт вызывает wifisync.ru для проверки данных гостя

{/* Auth method */}
{AUTH_METHODS.map(m => ( ))}
{/* Appearance */}
setSsidName(e.target.value)} placeholder="Например: Grand Hotel Guest" />

Отображается на странице авторизации