From dc3e68dbe2065178fa4dde1a43843ae0d58a402e Mon Sep 17 00:00:00 2001 From: HotelSync Date: Mon, 16 Mar 2026 19:27:34 +0300 Subject: [PATCH] Add guest review page + switch to 5-star rating system MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - New public page /review/:slug — guest-facing review form with 5-star selector, optional comment, positive flow (redirect buttons to platforms), negative flow (thank you message) - Switch all rating scales from 10-point to 5-star in ReviewsPage: mock data, StarRating component, avgRating sub-label, NPS thresholds, threshold slider (1–4), settings cards - Rating badge in review list now shows number + star icon - Register route in App.tsx as public (no auth required) Co-Authored-By: Claude Sonnet 4.6 --- src/App.tsx | 2 + src/pages/GuestReviewPage.tsx | 204 ++++++++++++++++++++++++++++++++++ src/pages/ReviewsPage.tsx | 64 ++++++----- 3 files changed, 237 insertions(+), 33 deletions(-) create mode 100644 src/pages/GuestReviewPage.tsx diff --git a/src/App.tsx b/src/App.tsx index a54ecc4..0103b37 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -34,6 +34,7 @@ import { DynamicPricingPage } from './pages/DynamicPricingPage' import { LoyaltyPage } from './pages/LoyaltyPage' import { MaintenancePage } from './pages/MaintenancePage' import { DiscountsPage } from './pages/DiscountsPage' +import { GuestReviewPage } from './pages/GuestReviewPage' export default function App() { return ( @@ -46,6 +47,7 @@ export default function App() { {/* Public */} } /> + } /> {/* App routes */} }> diff --git a/src/pages/GuestReviewPage.tsx b/src/pages/GuestReviewPage.tsx new file mode 100644 index 0000000..abbc835 --- /dev/null +++ b/src/pages/GuestReviewPage.tsx @@ -0,0 +1,204 @@ +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 = { + '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 = (slug && MOCK_HOTELS[slug]) ?? 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 ( +
+
+ + {/* Card */} +
+ + {/* Top color bar */} +
+ +
+ + {/* Logo */} +
+ {hotel.logoLetter} +
+ + {/* Hotel name */} +
+

{hotel.name}

+ {!submitted && ( +

{hotel.welcomeText}

+ )} +
+ + {/* ── NOT YET SUBMITTED ── */} + {!submitted && ( + <> + {/* Stars */} +
+ {Array.from({ length: 5 }, (_, i) => ( + + ))} +
+ + {/* Star label */} + {(hover || rating) > 0 && ( +

+ {['', 'Очень плохо', 'Плохо', 'Нормально', 'Хорошо', 'Отлично!'][hover || rating]} +

+ )} + + {/* Text field */} + {rating > 0 && hotel.showTextField && ( +