- TariffsPage (/tariffs): rate plan builder — meal plans (RO/BB/HB/FB/AI), inclusions picker, price modifier (% or ₽), min nights, cancellation policy - DynamicPricingPage (/dynamic-pricing): rule engine for weekends, holidays, seasons, custom date ranges, weather; interactive price calendar preview with colour-coded effective rates per day - LoyaltyPage (/loyalty): program settings (points/₽, point value, expiry), 4-level structure (Bronze/Silver/Gold/Platinum) with editable perks, guest portal preview mockup, top-guests leaderboard - MaintenancePage (/maintenance): scheduled room & service downtime records; status flow (scheduled → in_progress → done); time range support for services - DiscountsPage (/discounts): discount catalogue — % or ₽, category (all / loyalty / corporate / promo), min nights, validity dates, active toggle - BookingModal: discount now selected from dropdown of active discounts only (replaces free-form input); summary shows discount name + saved amount - Sidebar: new "Цены и тарифы" section (Тарифы, Динамические цены, Скидки); Управление extended with Лояльность and Тех. перерывы Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
491 lines
24 KiB
TypeScript
491 lines
24 KiB
TypeScript
import { useState } from 'react'
|
||
import {
|
||
Award, Star, Gift, Percent, CreditCard, Edit2, Save, Info,
|
||
Crown, Shield, User, ChevronRight, Smartphone, Mail, QrCode,
|
||
TrendingUp,
|
||
} from 'lucide-react'
|
||
import { cn } from '../lib/utils'
|
||
|
||
// ─── Types ────────────────────────────────────────────────────────────────────
|
||
|
||
type LevelId = 'bronze' | 'silver' | 'gold' | 'platinum'
|
||
|
||
interface LoyaltyLevel {
|
||
id: LevelId
|
||
name: string
|
||
minPoints: number
|
||
maxPoints: number | null
|
||
discount: number // % discount on stay
|
||
bonusPercent: number // % of spent amount returned as points
|
||
perks: string[]
|
||
color: string
|
||
icon: React.ElementType
|
||
}
|
||
|
||
interface ProgramSettings {
|
||
pointsPerRuble: number // points per 1 ₽ spent
|
||
pointValue: number // 1 point = N rubles
|
||
expiryMonths: number // points expire after N months
|
||
isActive: boolean
|
||
}
|
||
|
||
interface GuestPoints {
|
||
id: string
|
||
name: string
|
||
email: string
|
||
points: number
|
||
totalSpent: number
|
||
level: LevelId
|
||
}
|
||
|
||
// ─── Mock Data ────────────────────────────────────────────────────────────────
|
||
|
||
const DEFAULT_LEVELS: LoyaltyLevel[] = [
|
||
{
|
||
id: 'bronze',
|
||
name: 'Бронза',
|
||
minPoints: 0,
|
||
maxPoints: 999,
|
||
discount: 0,
|
||
bonusPercent: 3,
|
||
perks: ['Накопление бонусных баллов 3%', 'Приоритет на бесплатный Wi-Fi', 'Поздний выезд по запросу'],
|
||
color: 'text-amber-700 dark:text-amber-500',
|
||
icon: User,
|
||
},
|
||
{
|
||
id: 'silver',
|
||
name: 'Серебро',
|
||
minPoints: 1000,
|
||
maxPoints: 4999,
|
||
discount: 5,
|
||
bonusPercent: 5,
|
||
perks: ['Скидка 5% на проживание', 'Накопление 5%', 'Бесплатный ранний заезд', 'Комплимент от отеля'],
|
||
color: 'text-slate-500 dark:text-slate-400',
|
||
icon: Shield,
|
||
},
|
||
{
|
||
id: 'gold',
|
||
name: 'Золото',
|
||
minPoints: 5000,
|
||
maxPoints: 14999,
|
||
discount: 10,
|
||
bonusPercent: 8,
|
||
perks: ['Скидка 10% на проживание', 'Накопление 8%', 'Гарантированный апгрейд номера', 'Приветственный напиток'],
|
||
color: 'text-yellow-500 dark:text-yellow-400',
|
||
icon: Award,
|
||
},
|
||
{
|
||
id: 'platinum',
|
||
name: 'Платина',
|
||
minPoints: 15000,
|
||
maxPoints: null,
|
||
discount: 15,
|
||
bonusPercent: 12,
|
||
perks: ['Скидка 15% на проживание', 'Накопление 12%', 'VIP-заезд/выезд', 'Персональный менеджер', 'Ежегодный бонус 500 баллов'],
|
||
color: 'text-violet-500 dark:text-violet-400',
|
||
icon: Crown,
|
||
},
|
||
]
|
||
|
||
const DEFAULT_SETTINGS: ProgramSettings = {
|
||
pointsPerRuble: 1,
|
||
pointValue: 0.5,
|
||
expiryMonths: 24,
|
||
isActive: true,
|
||
}
|
||
|
||
const MOCK_GUESTS: GuestPoints[] = [
|
||
{ id: 'g-1', name: 'Александр Петров', email: 'a.petrov@gmail.com', points: 16250, totalSpent: 287400, level: 'platinum' },
|
||
{ id: 'g-4', name: 'Елена Новикова', email: 'e.novikova@gmail.com', points: 8100, totalSpent: 76500, level: 'gold' },
|
||
{ id: 'g-6', name: 'Анна Лебедева', email: 'anna.l@mail.ru', points: 3950, totalSpent: 132000, level: 'silver' },
|
||
{ id: 'g-2', name: 'Мария Сидорова', email: 'm.sidorova@mail.ru', points: 2200, totalSpent: 98200, level: 'silver' },
|
||
{ id: 'g-5', name: 'Игорь Волков', email: 'i.volkov@corp.ru', points: 780, totalSpent: 44000, level: 'bronze' },
|
||
{ id: 'g-3', name: 'Дмитрий Козлов', email: 'dkozlov@yandex.ru', points: 340, totalSpent: 18600, level: 'bronze' },
|
||
{ id: 'g-7', name: 'Сергей Морозов', email: '', points: 120, totalSpent: 8400, level: 'bronze' },
|
||
]
|
||
|
||
function levelBadge(levelId: LevelId) {
|
||
const level = DEFAULT_LEVELS.find(l => l.id === levelId)!
|
||
const bgs: Record<LevelId, string> = {
|
||
bronze: 'bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-400',
|
||
silver: 'bg-slate-100 text-slate-600 dark:bg-slate-700 dark:text-slate-300',
|
||
gold: 'bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400',
|
||
platinum: 'bg-violet-100 text-violet-700 dark:bg-violet-900/30 dark:text-violet-400',
|
||
}
|
||
return (
|
||
<span className={cn('inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-semibold', bgs[levelId])}>
|
||
<level.icon size={10} />
|
||
{level.name}
|
||
</span>
|
||
)
|
||
}
|
||
|
||
// ─── Level Editor ─────────────────────────────────────────────────────────────
|
||
|
||
function LevelCard({ level, onEdit }: { level: LoyaltyLevel; onEdit: (l: LoyaltyLevel) => void }) {
|
||
const [editing, setEditing] = useState(false)
|
||
const [form, setForm] = useState(level)
|
||
|
||
const save = () => { onEdit(form); setEditing(false) }
|
||
|
||
const levelBg: Record<LevelId, string> = {
|
||
bronze: 'from-amber-50 to-orange-50 border-amber-200 dark:from-amber-900/10 dark:to-orange-900/10 dark:border-amber-800',
|
||
silver: 'from-slate-50 to-slate-100 border-slate-200 dark:from-slate-800/50 dark:to-slate-700/50 dark:border-slate-600',
|
||
gold: 'from-yellow-50 to-amber-50 border-yellow-200 dark:from-yellow-900/10 dark:to-amber-900/10 dark:border-yellow-800',
|
||
platinum: 'from-violet-50 to-purple-50 border-violet-200 dark:from-violet-900/10 dark:to-purple-900/10 dark:border-violet-800',
|
||
}
|
||
|
||
return (
|
||
<div className={cn('rounded-2xl border bg-gradient-to-br p-4', levelBg[level.id])}>
|
||
<div className="flex items-center justify-between mb-3">
|
||
<div className="flex items-center gap-2">
|
||
<level.icon size={20} className={level.color} />
|
||
<span className={cn('font-bold text-base', level.color)}>{level.name}</span>
|
||
</div>
|
||
<button
|
||
onClick={() => setEditing(!editing)}
|
||
className="p-1.5 rounded-lg text-slate-400 hover:text-brand-600 hover:bg-white/60 transition-colors"
|
||
>
|
||
{editing ? <Save size={15} /> : <Edit2 size={15} />}
|
||
</button>
|
||
</div>
|
||
|
||
{editing ? (
|
||
<div className="space-y-2">
|
||
<div className="grid grid-cols-2 gap-2">
|
||
<div>
|
||
<label className="block text-xs text-slate-500 mb-1">Мин. баллов</label>
|
||
<input type="number" className="input text-sm" value={form.minPoints}
|
||
onChange={e => setForm(p => ({ ...p, minPoints: parseInt(e.target.value) || 0 }))} />
|
||
</div>
|
||
<div>
|
||
<label className="block text-xs text-slate-500 mb-1">Скидка %</label>
|
||
<input type="number" min={0} max={50} className="input text-sm" value={form.discount}
|
||
onChange={e => setForm(p => ({ ...p, discount: parseInt(e.target.value) || 0 }))} />
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<label className="block text-xs text-slate-500 mb-1">Начисление баллов %</label>
|
||
<input type="number" min={0} max={20} className="input text-sm" value={form.bonusPercent}
|
||
onChange={e => setForm(p => ({ ...p, bonusPercent: parseInt(e.target.value) || 0 }))} />
|
||
</div>
|
||
<button onClick={save} className="btn-primary w-full text-sm py-1.5">Сохранить</button>
|
||
</div>
|
||
) : (
|
||
<div className="space-y-1.5">
|
||
<div className="flex justify-between text-xs">
|
||
<span className="text-slate-500">Порог баллов</span>
|
||
<span className="font-semibold text-slate-700 dark:text-slate-300">
|
||
{level.minPoints.toLocaleString('ru-RU')}
|
||
{level.maxPoints ? ` — ${level.maxPoints.toLocaleString('ru-RU')}` : '+'}
|
||
</span>
|
||
</div>
|
||
<div className="flex justify-between text-xs">
|
||
<span className="text-slate-500">Скидка на проживание</span>
|
||
<span className={cn('font-bold', level.color)}>{level.discount}%</span>
|
||
</div>
|
||
<div className="flex justify-between text-xs">
|
||
<span className="text-slate-500">Начисление баллов</span>
|
||
<span className="font-semibold text-slate-700 dark:text-slate-300">{level.bonusPercent}% от суммы</span>
|
||
</div>
|
||
<div className="mt-2 pt-2 border-t border-white/50 dark:border-slate-600/50 space-y-1">
|
||
{level.perks.map((perk, i) => (
|
||
<div key={i} className="flex items-start gap-1.5 text-xs text-slate-600 dark:text-slate-300">
|
||
<span className="text-emerald-500 mt-0.5 shrink-0">✓</span>
|
||
{perk}
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
// ─── Guest Portal Preview ─────────────────────────────────────────────────────
|
||
|
||
function GuestPortalPreview({ settings }: { settings: ProgramSettings }) {
|
||
const guest = MOCK_GUESTS[0]
|
||
const level = DEFAULT_LEVELS.find(l => l.id === guest.level)!
|
||
const nextLevel = DEFAULT_LEVELS.find(l => l.minPoints > guest.points)
|
||
const progressToNext = nextLevel
|
||
? ((guest.points - level.minPoints) / (nextLevel.minPoints - level.minPoints)) * 100
|
||
: 100
|
||
|
||
return (
|
||
<div className="rounded-2xl border-2 border-dashed border-slate-300 dark:border-slate-600 p-4">
|
||
<div className="flex items-center gap-2 mb-3">
|
||
<Smartphone size={14} className="text-slate-400" />
|
||
<p className="text-xs font-medium text-slate-500 dark:text-slate-400 uppercase tracking-wide">
|
||
Личный кабинет гостя (превью)
|
||
</p>
|
||
</div>
|
||
|
||
<div className="bg-gradient-to-br from-slate-800 to-slate-900 rounded-xl p-4 text-white space-y-3">
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<p className="text-xs text-slate-400">Добро пожаловать,</p>
|
||
<p className="font-bold text-sm">{guest.name.split(' ')[0]}</p>
|
||
</div>
|
||
<div className="text-right">
|
||
<p className="text-xs text-slate-400">Уровень</p>
|
||
<div className="flex items-center gap-1">
|
||
<level.icon size={12} className={level.color} />
|
||
<p className={cn('text-sm font-bold', level.color)}>{level.name}</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Points balance */}
|
||
<div className="bg-white/10 rounded-xl p-3">
|
||
<p className="text-xs text-slate-400 mb-0.5">Накопленные баллы</p>
|
||
<p className="text-3xl font-bold">{guest.points.toLocaleString('ru-RU')}</p>
|
||
<p className="text-xs text-slate-400 mt-0.5">
|
||
≈ {Math.round(guest.points * settings.pointValue).toLocaleString('ru-RU')} ₽ к оплате
|
||
</p>
|
||
</div>
|
||
|
||
{/* Progress to next level */}
|
||
{nextLevel && (
|
||
<div>
|
||
<div className="flex justify-between text-xs text-slate-400 mb-1">
|
||
<span>До {nextLevel.name}</span>
|
||
<span>{(nextLevel.minPoints - guest.points).toLocaleString('ru-RU')} баллов</span>
|
||
</div>
|
||
<div className="h-2 rounded-full bg-white/20">
|
||
<div
|
||
className="h-2 rounded-full bg-gradient-to-r from-brand-400 to-brand-600"
|
||
style={{ width: `${Math.min(100, progressToNext)}%` }}
|
||
/>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Quick perks */}
|
||
<div className="grid grid-cols-3 gap-2">
|
||
{[
|
||
{ icon: Percent, label: `${level.discount}% скидка` },
|
||
{ icon: Gift, label: 'Комплимент' },
|
||
{ icon: CreditCard, label: 'Оплата баллами' },
|
||
].map(p => (
|
||
<div key={p.label} className="bg-white/5 rounded-lg p-2 text-center">
|
||
<p.icon size={14} className="mx-auto text-brand-400 mb-1" />
|
||
<p className="text-[10px] text-slate-300">{p.label}</p>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
<div className="flex items-center gap-3 mt-3 pt-3 border-t border-slate-200 dark:border-slate-700">
|
||
<QrCode size={14} className="text-slate-400 shrink-0" />
|
||
<p className="text-xs text-slate-500 dark:text-slate-400">
|
||
Гость получает ссылку на личный кабинет по email после первого бронирования.
|
||
Доступ — без пароля, по magic link.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
// ─── Main Page ────────────────────────────────────────────────────────────────
|
||
|
||
export function LoyaltyPage() {
|
||
const [levels, setLevels] = useState<LoyaltyLevel[]>(DEFAULT_LEVELS)
|
||
const [settings, setSettings] = useState<ProgramSettings>(DEFAULT_SETTINGS)
|
||
const [editSettings, setEditSettings] = useState(false)
|
||
|
||
const updateLevel = (updated: LoyaltyLevel) =>
|
||
setLevels(prev => prev.map(l => l.id === updated.id ? updated : l))
|
||
|
||
const totalGuests = MOCK_GUESTS.length
|
||
const activeGuests = MOCK_GUESTS.filter(g => g.points > 0).length
|
||
const totalPoints = MOCK_GUESTS.reduce((s, g) => s + g.points, 0)
|
||
|
||
return (
|
||
<div className="p-6 space-y-6">
|
||
{/* Header */}
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<h1 className="text-2xl font-bold text-slate-900 dark:text-slate-100">Программа лояльности</h1>
|
||
<p className="text-sm text-slate-500 dark:text-slate-400 mt-0.5">
|
||
Уровни, бонусные баллы, личный кабинет гостя
|
||
</p>
|
||
</div>
|
||
<div className="flex items-center gap-3">
|
||
<span className={cn(
|
||
'px-3 py-1.5 rounded-xl text-sm font-medium border',
|
||
settings.isActive
|
||
? 'bg-emerald-50 text-emerald-700 border-emerald-300 dark:bg-emerald-900/20 dark:text-emerald-400 dark:border-emerald-700'
|
||
: 'bg-slate-100 text-slate-500 border-slate-200 dark:bg-slate-700 dark:text-slate-400 dark:border-slate-600',
|
||
)}>
|
||
{settings.isActive ? '● Программа активна' : '○ Программа отключена'}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Stats */}
|
||
<div className="grid grid-cols-4 gap-4">
|
||
{[
|
||
{ label: 'Участников программы', value: activeGuests, icon: User, color: 'text-brand-600 dark:text-brand-400', bg: 'bg-brand-50 dark:bg-brand-900/20' },
|
||
{ label: 'Всего баллов в обороте', value: totalPoints.toLocaleString('ru-RU'), icon: TrendingUp, color: 'text-amber-600 dark:text-amber-400', bg: 'bg-amber-50 dark:bg-amber-900/20' },
|
||
{ label: 'Платиновых участников', value: MOCK_GUESTS.filter(g => g.level === 'platinum').length, icon: Crown, color: 'text-violet-600 dark:text-violet-400', bg: 'bg-violet-50 dark:bg-violet-900/20' },
|
||
{ label: 'Средний балс/гость', value: Math.round(totalPoints / totalGuests).toLocaleString('ru-RU'), icon: Star, color: 'text-yellow-600 dark:text-yellow-400', bg: 'bg-yellow-50 dark:bg-yellow-900/20' },
|
||
].map(s => (
|
||
<div key={s.label} className="card p-4 flex items-center gap-3">
|
||
<div className={cn('w-10 h-10 rounded-xl flex items-center justify-center shrink-0', s.bg)}>
|
||
<s.icon size={18} className={s.color} />
|
||
</div>
|
||
<div>
|
||
<p className="text-xs text-slate-500 dark:text-slate-400 leading-tight">{s.label}</p>
|
||
<p className="text-xl font-bold text-slate-900 dark:text-slate-100 mt-0.5">{s.value}</p>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
|
||
<div className="grid grid-cols-1 xl:grid-cols-3 gap-6">
|
||
<div className="xl:col-span-2 space-y-5">
|
||
{/* Program settings */}
|
||
<div className="card p-4">
|
||
<div className="flex items-center justify-between mb-4">
|
||
<h2 className="text-sm font-semibold text-slate-700 dark:text-slate-300">Настройки программы</h2>
|
||
<button onClick={() => setEditSettings(!editSettings)} className="btn-secondary flex items-center gap-1.5 text-sm">
|
||
{editSettings ? <><Save size={13} /> Сохранить</> : <><Edit2 size={13} /> Изменить</>}
|
||
</button>
|
||
</div>
|
||
<div className="grid grid-cols-3 gap-4">
|
||
{[
|
||
{
|
||
label: 'Начисление',
|
||
desc: `${settings.pointsPerRuble} балл за 1 ₽`,
|
||
field: 'pointsPerRuble' as const,
|
||
unit: 'балл/₽',
|
||
},
|
||
{
|
||
label: 'Ценность балла',
|
||
desc: `1 балл = ${settings.pointValue} ₽`,
|
||
field: 'pointValue' as const,
|
||
unit: '₽',
|
||
},
|
||
{
|
||
label: 'Срок действия',
|
||
desc: `${settings.expiryMonths} месяцев`,
|
||
field: 'expiryMonths' as const,
|
||
unit: 'мес.',
|
||
},
|
||
].map(item => (
|
||
<div key={item.field}>
|
||
<p className="text-xs text-slate-500 dark:text-slate-400">{item.label}</p>
|
||
{editSettings ? (
|
||
<div className="flex items-center gap-1 mt-1">
|
||
<input
|
||
type="number" step="0.1" min={0}
|
||
className="input text-sm"
|
||
value={settings[item.field]}
|
||
onChange={e => setSettings(prev => ({
|
||
...prev,
|
||
[item.field]: parseFloat(e.target.value) || 0,
|
||
}))}
|
||
/>
|
||
<span className="text-xs text-slate-400 shrink-0">{item.unit}</span>
|
||
</div>
|
||
) : (
|
||
<p className="text-base font-semibold text-slate-900 dark:text-slate-100 mt-0.5">{item.desc}</p>
|
||
)}
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Level cards */}
|
||
<div>
|
||
<h2 className="text-sm font-semibold text-slate-600 dark:text-slate-400 uppercase tracking-wide mb-3">
|
||
Уровни программы
|
||
</h2>
|
||
<div className="grid grid-cols-2 gap-3">
|
||
{levels.map(level => (
|
||
<LevelCard key={level.id} level={level} onEdit={updateLevel} />
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Top guests */}
|
||
<div className="card overflow-hidden">
|
||
<div className="px-4 py-3 border-b border-slate-200 dark:border-slate-700">
|
||
<h3 className="text-sm font-semibold text-slate-700 dark:text-slate-300">Топ участников</h3>
|
||
</div>
|
||
<table className="w-full">
|
||
<thead>
|
||
<tr className="bg-slate-50 dark:bg-slate-700/30">
|
||
<th className="px-4 py-2 text-left text-xs font-semibold text-slate-500 uppercase tracking-wide">Гость</th>
|
||
<th className="px-4 py-2 text-left text-xs font-semibold text-slate-500 uppercase tracking-wide">Уровень</th>
|
||
<th className="px-4 py-2 text-right text-xs font-semibold text-slate-500 uppercase tracking-wide">Баллы</th>
|
||
<th className="px-4 py-2 text-right text-xs font-semibold text-slate-500 uppercase tracking-wide">Потрачено</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody className="divide-y divide-slate-100 dark:divide-slate-700/50">
|
||
{MOCK_GUESTS.sort((a, b) => b.points - a.points).map(g => (
|
||
<tr key={g.id} className="hover:bg-slate-50 dark:hover:bg-slate-700/20 transition-colors">
|
||
<td className="px-4 py-3">
|
||
<div className="flex items-center gap-2">
|
||
<div className="w-7 h-7 rounded-full bg-brand-100 dark:bg-brand-900/30 flex items-center justify-center shrink-0">
|
||
<span className="text-xs font-bold text-brand-600 dark:text-brand-400">
|
||
{g.name.split(' ').map(p => p[0]).join('').slice(0, 2)}
|
||
</span>
|
||
</div>
|
||
<span className="text-sm text-slate-700 dark:text-slate-300">{g.name}</span>
|
||
</div>
|
||
</td>
|
||
<td className="px-4 py-3">{levelBadge(g.level)}</td>
|
||
<td className="px-4 py-3 text-right">
|
||
<span className="text-sm font-semibold text-slate-900 dark:text-slate-100">
|
||
{g.points.toLocaleString('ru-RU')}
|
||
</span>
|
||
</td>
|
||
<td className="px-4 py-3 text-right">
|
||
<span className="text-sm text-slate-600 dark:text-slate-400">
|
||
{g.totalSpent.toLocaleString('ru-RU')} ₽
|
||
</span>
|
||
</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Right column */}
|
||
<div className="space-y-4">
|
||
{/* Guest portal preview */}
|
||
<GuestPortalPreview settings={settings} />
|
||
|
||
{/* Redemption info */}
|
||
<div className="card p-4 space-y-3">
|
||
<h3 className="text-sm font-semibold text-slate-700 dark:text-slate-300">Как работает программа</h3>
|
||
{[
|
||
{ icon: CreditCard, text: 'При каждом бронировании гостю начисляются баллы пропорционально потраченной сумме' },
|
||
{ icon: Percent, text: 'Накопленные баллы можно списать при следующем бронировании' },
|
||
{ icon: Mail, text: 'После первого визита гость получает email с ссылкой на личный кабинет' },
|
||
{ icon: Award, text: 'При достижении нового уровня приходит уведомление с перечнем привилегий' },
|
||
].map((item, i) => (
|
||
<div key={i} className="flex items-start gap-2.5">
|
||
<div className="w-6 h-6 rounded-lg bg-brand-50 dark:bg-brand-900/20 flex items-center justify-center shrink-0 mt-0.5">
|
||
<item.icon size={12} className="text-brand-600 dark:text-brand-400" />
|
||
</div>
|
||
<p className="text-xs text-slate-600 dark:text-slate-400">{item.text}</p>
|
||
</div>
|
||
))}
|
||
</div>
|
||
|
||
<div className="flex items-start gap-3 p-3 rounded-xl bg-brand-50 dark:bg-brand-900/10 border border-brand-200 dark:border-brand-800">
|
||
<Info size={14} className="text-brand-600 dark:text-brand-400 mt-0.5 shrink-0" />
|
||
<p className="text-xs text-brand-700 dark:text-brand-400">
|
||
В будущих версиях баллы будут работать во всех отелях сети HotelSync — единая программа для всего бренда.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|