- GuestReviewPage: fix hotel config lookup type (slug && obj returns '' | obj)
- PosPage, ReviewsPage: change ComponentType<{size?:number}> to ElementType for Lucide icon compatibility
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
205 lines
8.2 KiB
TypeScript
205 lines
8.2 KiB
TypeScript
import { useState } from 'react'
|
||
import { useParams } from 'react-router-dom'
|
||
import { Star, ExternalLink, CheckCircle2, Heart } from 'lucide-react'
|
||
import { cn } from '../lib/utils'
|
||
|
||
// ── Mock hotel config (in real app — fetched by slug from API) ────────────────
|
||
|
||
interface HotelConfig {
|
||
name: string
|
||
color: string
|
||
logoLetter: string
|
||
redirectThreshold: number // min rating (1–5) to show redirect buttons
|
||
platforms: { name: string; url: string }[]
|
||
welcomeText: string
|
||
showTextField: boolean
|
||
}
|
||
|
||
const MOCK_HOTELS: Record<string, HotelConfig> = {
|
||
'grand-palace': {
|
||
name: 'Гранд Палас',
|
||
color: '#2563eb',
|
||
logoLetter: 'Г',
|
||
redirectThreshold: 4,
|
||
welcomeText: 'Как вам у нас?',
|
||
showTextField: true,
|
||
platforms: [
|
||
{ name: 'Booking.com', url: 'https://www.booking.com/hotel/ru/' },
|
||
{ name: 'Яндекс Путешествия', url: 'https://travel.yandex.ru/hotels/' },
|
||
{ name: 'Google', url: 'https://g.page/r/' },
|
||
],
|
||
},
|
||
}
|
||
|
||
const DEFAULT_CONFIG: HotelConfig = {
|
||
name: 'Отель',
|
||
color: '#2563eb',
|
||
logoLetter: 'О',
|
||
redirectThreshold: 4,
|
||
welcomeText: 'Как вам у нас?',
|
||
showTextField: true,
|
||
platforms: [
|
||
{ name: 'Booking.com', url: 'https://www.booking.com' },
|
||
{ name: 'Google', url: 'https://google.com' },
|
||
],
|
||
}
|
||
|
||
// ── Component ─────────────────────────────────────────────────────────────────
|
||
|
||
export function GuestReviewPage() {
|
||
const { slug } = useParams<{ slug: string }>()
|
||
const hotel: HotelConfig = (slug ? MOCK_HOTELS[slug] : null) ?? DEFAULT_CONFIG
|
||
|
||
const [rating, setRating] = useState(0)
|
||
const [hover, setHover] = useState(0)
|
||
const [text, setText] = useState('')
|
||
const [submitted, setSubmitted] = useState(false)
|
||
|
||
const isPositive = rating >= hotel.redirectThreshold
|
||
|
||
const handleSubmit = () => {
|
||
if (rating === 0) return
|
||
setSubmitted(true)
|
||
// In real app: POST /api/reviews { slug, rating, text }
|
||
}
|
||
|
||
return (
|
||
<div className="min-h-screen bg-gradient-to-br from-slate-50 to-slate-100 flex items-center justify-center p-4">
|
||
<div className="w-full max-w-sm">
|
||
|
||
{/* Card */}
|
||
<div className="bg-white rounded-3xl shadow-2xl overflow-hidden">
|
||
|
||
{/* Top color bar */}
|
||
<div className="h-2" style={{ backgroundColor: hotel.color }} />
|
||
|
||
<div className="px-8 py-8 text-center space-y-6">
|
||
|
||
{/* Logo */}
|
||
<div
|
||
className="w-20 h-20 rounded-2xl mx-auto flex items-center justify-center text-3xl font-bold text-white shadow-lg"
|
||
style={{ backgroundColor: hotel.color }}
|
||
>
|
||
{hotel.logoLetter}
|
||
</div>
|
||
|
||
{/* Hotel name */}
|
||
<div>
|
||
<h1 className="text-xl font-bold text-slate-900">{hotel.name}</h1>
|
||
{!submitted && (
|
||
<p className="text-slate-500 mt-1 text-base">{hotel.welcomeText}</p>
|
||
)}
|
||
</div>
|
||
|
||
{/* ── NOT YET SUBMITTED ── */}
|
||
{!submitted && (
|
||
<>
|
||
{/* Stars */}
|
||
<div className="flex justify-center gap-2">
|
||
{Array.from({ length: 5 }, (_, i) => (
|
||
<button
|
||
key={i}
|
||
onMouseEnter={() => setHover(i + 1)}
|
||
onMouseLeave={() => setHover(0)}
|
||
onClick={() => setRating(i + 1)}
|
||
className="transition-transform hover:scale-110 active:scale-95"
|
||
>
|
||
<Star
|
||
size={40}
|
||
className={cn(
|
||
'transition-colors drop-shadow-sm',
|
||
(hover || rating) > i
|
||
? 'text-amber-400 fill-amber-400'
|
||
: 'text-slate-200 fill-slate-200',
|
||
)}
|
||
/>
|
||
</button>
|
||
))}
|
||
</div>
|
||
|
||
{/* Star label */}
|
||
{(hover || rating) > 0 && (
|
||
<p className="text-sm font-medium text-slate-600 -mt-2">
|
||
{['', 'Очень плохо', 'Плохо', 'Нормально', 'Хорошо', 'Отлично!'][hover || rating]}
|
||
</p>
|
||
)}
|
||
|
||
{/* Text field */}
|
||
{rating > 0 && hotel.showTextField && (
|
||
<textarea
|
||
className="w-full border border-slate-200 rounded-2xl p-3.5 text-sm text-slate-700 bg-slate-50 resize-none focus:outline-none focus:border-blue-400 transition-colors placeholder:text-slate-400"
|
||
rows={3}
|
||
placeholder="Расскажите подробнее... (необязательно)"
|
||
value={text}
|
||
onChange={e => setText(e.target.value)}
|
||
/>
|
||
)}
|
||
|
||
{/* Submit */}
|
||
<button
|
||
onClick={handleSubmit}
|
||
disabled={rating === 0}
|
||
className="w-full py-3.5 rounded-2xl text-white font-semibold text-base transition-all hover:opacity-90 active:scale-95 disabled:opacity-30 disabled:cursor-not-allowed shadow-lg"
|
||
style={{ backgroundColor: hotel.color }}
|
||
>
|
||
Отправить отзыв
|
||
</button>
|
||
</>
|
||
)}
|
||
|
||
{/* ── SUBMITTED: NEGATIVE ── */}
|
||
{submitted && !isPositive && (
|
||
<div className="space-y-4 py-2">
|
||
<div className="w-16 h-16 rounded-full bg-slate-100 flex items-center justify-center mx-auto">
|
||
<Heart size={28} className="text-slate-400" />
|
||
</div>
|
||
<div>
|
||
<p className="font-bold text-slate-900 text-lg">Спасибо за честность</p>
|
||
<p className="text-slate-500 text-sm mt-1.5 leading-relaxed">
|
||
Мы сожалеем, что что-то пошло не так. Ваш отзыв поможет нам стать лучше.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* ── SUBMITTED: POSITIVE ── */}
|
||
{submitted && isPositive && (
|
||
<div className="space-y-4 py-2">
|
||
<div className="w-16 h-16 rounded-full bg-emerald-50 flex items-center justify-center mx-auto">
|
||
<CheckCircle2 size={32} className="text-emerald-500" />
|
||
</div>
|
||
<div>
|
||
<p className="font-bold text-slate-900 text-lg">Спасибо за отзыв!</p>
|
||
<p className="text-slate-500 text-sm mt-1.5 leading-relaxed">
|
||
Если вам не сложно — поделитесь впечатлениями на одной из площадок:
|
||
</p>
|
||
</div>
|
||
|
||
<div className="space-y-2.5">
|
||
{hotel.platforms.map(p => (
|
||
<a
|
||
key={p.name}
|
||
href={p.url}
|
||
target="_blank"
|
||
rel="noopener noreferrer"
|
||
className="flex items-center justify-between w-full px-4 py-3 rounded-2xl border-2 border-slate-100 hover:border-blue-200 bg-slate-50 hover:bg-blue-50 text-slate-700 font-medium text-sm transition-all group"
|
||
>
|
||
<span>{p.name}</span>
|
||
<ExternalLink size={14} className="text-slate-400 group-hover:text-blue-500 transition-colors" />
|
||
</a>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* Footer */}
|
||
<div className="px-8 pb-6 text-center">
|
||
<p className="text-xs text-slate-300">HotelSync · система сбора отзывов</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|