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 (
{copied ? : }
)
}
// ── Code block with copy ──────────────────────────────────────────────────────
function CodeBlock({ code, lang = 'text' }: { code: string; lang?: string }) {
return (
)
}
// ── 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 => (
setTab(t.id)}
className={cn(
'px-4 py-2 text-sm font-medium border-b-2 -mb-px transition-colors',
tab === t.id
? 'border-brand-600 text-brand-600 dark:text-brand-400'
: 'border-transparent text-slate-500 hover:text-slate-700 dark:text-slate-400',
)}
>
{t.label}
))}
{/* 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.
Гость подключается к вашей гостевой WiFi сети
Роутер перенаправляет HTTP запрос на страницу авторизации wifisync.ru
Гость вводит номер комнаты и фамилию (или дату рождения)
wifisync.ru проверяет данные через HotelSync API
При успехе гость получает доступ в интернет
{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 (
)
}
// ── 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 */}
{enabled ? : }
{enabled ? 'Включён' : 'Выключен'}
{/* ── 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 }) => (
))}
Сервис wifisync.ru предоставляет captive portal страницу и берёт на себя авторизацию гостей через HotelSync.
Вам нужно только скопировать токен и настроить роутер.
)}
{/* ── Tabs ───────────────────────────────────────────────────────────── */}
{([['settings', 'Настройки'], ['guide', 'Инструкция по оборудованию']] as const).map(([id, label]) => (
setActiveTab(id)}
className={cn(
'px-4 py-2.5 text-sm font-medium border-b-2 -mb-px transition-colors',
activeTab === id
? 'border-brand-600 text-brand-600 dark:text-brand-400'
: 'border-transparent text-slate-500 hover:text-slate-700 dark:text-slate-400',
)}
>
{label}
))}
{/* ── Settings tab ───────────────────────────────────────────────────── */}
{activeTab === 'settings' && (
{/* Token block */}
API токен
{showToken ? token : maskedToken}
setShowToken(v => !v)} className="btn-ghost p-1.5 text-slate-400 hover:text-slate-600" title={showToken ? 'Скрыть' : 'Показать'}>
{showToken ? : }
Новый токен
Вставьте этот токен в настройки wifisync.ru при подключении отеля
URL проверки гостей
https://api.hotelsync.ru/api/wifi-auth/verify
Этот эндпоинт вызывает wifisync.ru для проверки данных гостя
{/* Auth method */}
{/* Appearance */}
{/* Save button */}
{saving ? : null}
{saving ? 'Сохранение...' : 'Сохранить'}
{saveOk && (
Сохранено
)}
)}
{/* ── Guide tab ──────────────────────────────────────────────────────── */}
{activeTab === 'guide' && (
)}
)
}
// ── Section wrapper ───────────────────────────────────────────────────────────
function Section({
title, icon: Icon, children,
}: {
title: string
icon: React.ElementType
children: React.ReactNode
}) {
return (
)
}