Add guest review page + switch to 5-star rating system
- 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 <noreply@anthropic.com>
This commit is contained in:
@@ -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() {
|
||||
<Routes>
|
||||
{/* Public */}
|
||||
<Route path="/login" element={<LoginPage />} />
|
||||
<Route path="/review/:slug" element={<GuestReviewPage />} />
|
||||
|
||||
{/* App routes */}
|
||||
<Route element={<AppLayout />}>
|
||||
|
||||
204
src/pages/GuestReviewPage.tsx
Normal file
204
src/pages/GuestReviewPage.tsx
Normal file
@@ -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<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 = (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 (
|
||||
<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>
|
||||
)
|
||||
}
|
||||
@@ -49,7 +49,7 @@ const DEFAULT_PLATFORMS: ReviewPlatform[] = [
|
||||
const MOCK_REVIEWS: Review[] = [
|
||||
{
|
||||
id: 'rv1', guestName: 'Дмитрий Волков', roomNumber: '101',
|
||||
rating: 9, source: 'email', bookingSource: 'booking',
|
||||
rating: 5, source: 'email', bookingSource: 'booking',
|
||||
createdAt: new Date(Date.now() - 3600000 * 5),
|
||||
text: 'Замечательный отель! Персонал очень вежливый. Номер чистый, завтрак вкусный. Обязательно вернёмся.',
|
||||
status: 'published',
|
||||
@@ -57,14 +57,14 @@ const MOCK_REVIEWS: Review[] = [
|
||||
},
|
||||
{
|
||||
id: 'rv2', guestName: 'Анна Козлова', roomNumber: '202',
|
||||
rating: 2, source: 'qr', bookingSource: 'direct',
|
||||
rating: 1, source: 'qr', bookingSource: 'direct',
|
||||
createdAt: new Date(Date.now() - 3600000 * 12),
|
||||
text: 'Шум из соседнего номера мешал спать. Кондиционер плохо работал. Разочарована.',
|
||||
status: 'pending',
|
||||
},
|
||||
{
|
||||
id: 'rv3', guestName: 'Наталья Александрова', roomNumber: '301',
|
||||
rating: 10, source: 'sms', bookingSource: 'widget',
|
||||
rating: 5, source: 'sms', bookingSource: 'widget',
|
||||
createdAt: new Date(Date.now() - 3600000 * 36),
|
||||
text: 'Лучший отель! Вид из окна потрясающий, кровать удобная, всё стильно.',
|
||||
status: 'published',
|
||||
@@ -72,7 +72,7 @@ const MOCK_REVIEWS: Review[] = [
|
||||
},
|
||||
{
|
||||
id: 'rv4', guestName: 'Игорь Соколов', roomNumber: '201',
|
||||
rating: 7, source: 'email', bookingSource: 'yandex',
|
||||
rating: 4, source: 'email', bookingSource: 'yandex',
|
||||
createdAt: new Date(Date.now() - 3600000 * 48),
|
||||
text: 'В целом хорошо. Немного дорого для такого номера. Расположение отличное.',
|
||||
status: 'published',
|
||||
@@ -80,14 +80,14 @@ const MOCK_REVIEWS: Review[] = [
|
||||
},
|
||||
{
|
||||
id: 'rv5', guestName: 'Виктор Громов', roomNumber: '402',
|
||||
rating: 3, source: 'qr', bookingSource: 'direct',
|
||||
rating: 2, source: 'qr', bookingSource: 'direct',
|
||||
createdAt: new Date(Date.now() - 3600000 * 60),
|
||||
text: 'Долго ждали заселения. Номер убран с опозданием. Разочарованы.',
|
||||
status: 'pending',
|
||||
},
|
||||
{
|
||||
id: 'rv6', guestName: 'Михаил Орлов', roomNumber: '401',
|
||||
rating: 8, source: 'email', bookingSource: 'google',
|
||||
rating: 4, source: 'email', bookingSource: 'google',
|
||||
createdAt: new Date(Date.now() - 3600000 * 100),
|
||||
text: 'Очень доволен. Персонал внимательный, номер чистый.',
|
||||
status: 'published',
|
||||
@@ -112,22 +112,21 @@ const PREVIEW_COLORS = ['#2563eb', '#16a34a', '#9333ea', '#dc2626']
|
||||
|
||||
// ── Helper components ─────────────────────────────────────────────────────────
|
||||
|
||||
function StarRating({ rating, max = 10, size = 14 }: { rating: number; max?: number; size?: number }) {
|
||||
const stars = Math.round((rating / max) * 5)
|
||||
function StarRating({ rating, size = 14 }: { rating: number; size?: number }) {
|
||||
return (
|
||||
<div className="flex items-center gap-0.5">
|
||||
{Array.from({ length: 5 }, (_, i) => (
|
||||
<Star key={i} size={size} className={i < stars ? 'text-amber-400 fill-amber-400' : 'text-slate-300 dark:text-slate-600'} />
|
||||
<Star key={i} size={size} className={i < rating ? 'text-amber-400 fill-amber-400' : 'text-slate-300 dark:text-slate-600'} />
|
||||
))}
|
||||
<span className="ml-1.5 text-sm font-semibold text-slate-700 dark:text-slate-300">{rating}/10</span>
|
||||
<span className="ml-1.5 text-sm font-semibold text-slate-700 dark:text-slate-300">{rating}/5</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function ratingColor(r: number, threshold: number): string {
|
||||
if (r >= 9) return 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-300'
|
||||
if (r >= 7) return 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-300'
|
||||
if (r >= threshold) return 'bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-300'
|
||||
if (r >= 5) return 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-300'
|
||||
if (r >= 4) return 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-300'
|
||||
if (r >= threshold) return 'bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-300'
|
||||
return 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-300'
|
||||
}
|
||||
|
||||
@@ -143,7 +142,7 @@ export function ReviewsPage() {
|
||||
const [copied, setCopied] = useState(false)
|
||||
|
||||
// Settings
|
||||
const [threshold, setThreshold] = useState(6)
|
||||
const [threshold, setThreshold] = useState(4)
|
||||
const [platforms, setPlatforms] = useState<ReviewPlatform[]>(DEFAULT_PLATFORMS)
|
||||
const [newPlatformName, setNewPlatformName] = useState('')
|
||||
const [newPlatformUrl, setNewPlatformUrl] = useState('')
|
||||
@@ -171,8 +170,8 @@ export function ReviewsPage() {
|
||||
: '—'
|
||||
const nps = published.length
|
||||
? Math.round(
|
||||
(published.filter(r => r.rating >= 9).length / published.length * 100) -
|
||||
(published.filter(r => r.rating <= 6).length / published.length * 100),
|
||||
(published.filter(r => r.rating >= 5).length / published.length * 100) -
|
||||
(published.filter(r => r.rating <= 2).length / published.length * 100),
|
||||
)
|
||||
: 0
|
||||
|
||||
@@ -211,7 +210,6 @@ export function ReviewsPage() {
|
||||
}
|
||||
|
||||
const enabledPlatforms = platforms.filter(p => p.enabled)
|
||||
const thresholdStars = Math.round((threshold / 10) * 5)
|
||||
|
||||
// Determine redirect platform based on booking source
|
||||
const getRedirectPlatform = (source?: BookingSource): string | null => {
|
||||
@@ -237,14 +235,14 @@ export function ReviewsPage() {
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-slate-900 dark:text-slate-100">Отзывы гостей</h1>
|
||||
<p className="text-sm text-slate-500 dark:text-slate-400">
|
||||
Отрицательные (< {thresholdStars} звёзд) → модерация. Положительные → автоматический редирект на площадки.
|
||||
Отрицательные (< {threshold} звёзд) → модерация. Положительные → автоматический редирект на площадки.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
||||
{[
|
||||
{ label: 'Средняя оценка', value: avgRating, sub: 'из 10', color: 'text-amber-600 dark:text-amber-400' },
|
||||
{ label: 'Средняя оценка', value: avgRating, sub: 'из 5 звёзд', color: 'text-amber-600 dark:text-amber-400' },
|
||||
{ label: 'NPS', value: nps >= 0 ? `+${nps}` : `${nps}`, sub: 'лояльность', color: 'text-emerald-600 dark:text-emerald-400' },
|
||||
{ label: 'В архиве', value: published.length, sub: 'опубликованных', color: 'text-brand-600 dark:text-brand-400' },
|
||||
{ label: 'Ждут ответа', value: pending.length, sub: 'негативных', color: pending.length > 0 ? 'text-red-600 dark:text-red-400' : 'text-slate-500' },
|
||||
@@ -262,7 +260,7 @@ export function ReviewsPage() {
|
||||
<div className="card p-3 border-red-200 dark:border-red-700/50 bg-red-50/50 dark:bg-red-900/10 flex items-start gap-3">
|
||||
<ThumbsDown size={14} className="text-red-600 mt-0.5 shrink-0" />
|
||||
<div>
|
||||
<p className="font-semibold text-slate-900 dark:text-slate-100 text-sm">Отрицательные (< {thresholdStars} звёзд)</p>
|
||||
<p className="font-semibold text-slate-900 dark:text-slate-100 text-sm">Отрицательные (< {threshold} звёзд)</p>
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400 mt-0.5">
|
||||
Модерация — менеджер пишет личный ответ гостю. Публично не публикуется.
|
||||
</p>
|
||||
@@ -271,7 +269,7 @@ export function ReviewsPage() {
|
||||
<div className="card p-3 border-blue-200 dark:border-blue-700/50 bg-blue-50/50 dark:bg-blue-900/10 flex items-start gap-3">
|
||||
<ArrowUpRight size={14} className="text-blue-600 mt-0.5 shrink-0" />
|
||||
<div>
|
||||
<p className="font-semibold text-slate-900 dark:text-slate-100 text-sm">Положительные (≥ {thresholdStars} звёзд)</p>
|
||||
<p className="font-semibold text-slate-900 dark:text-slate-100 text-sm">Положительные (≥ {threshold} звёзд)</p>
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400 mt-0.5">
|
||||
Авторедирект на площадку источника бронирования или выбор из: {enabledPlatforms.map(p => p.name).join(', ') || '(настройте)'}
|
||||
</p>
|
||||
@@ -392,14 +390,14 @@ export function ReviewsPage() {
|
||||
)}
|
||||
|
||||
{/* After submit: negative flow */}
|
||||
{previewSubmitted && previewRating < thresholdStars + 1 && (
|
||||
{previewSubmitted && previewRating < threshold && (
|
||||
<div className="p-3 rounded-xl bg-slate-50 dark:bg-slate-800 border border-slate-200 dark:border-slate-600 text-sm text-slate-600 dark:text-slate-400">
|
||||
Спасибо за честный отзыв. Мы уже работаем над улучшениями.
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* After submit: positive flow — redirect buttons */}
|
||||
{previewSubmitted && previewRating >= thresholdStars + 1 && (
|
||||
{previewSubmitted && previewRating >= threshold && (
|
||||
<div className="space-y-2">
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400">Поделитесь на площадках:</p>
|
||||
{enabledPlatforms.map(p => (
|
||||
@@ -499,9 +497,9 @@ export function ReviewsPage() {
|
||||
|
||||
{/* Threshold info */}
|
||||
<div className="card p-4 bg-amber-50/50 dark:bg-amber-900/10 border-amber-200 dark:border-amber-700/50">
|
||||
<p className="text-sm font-semibold text-slate-800 dark:text-slate-200 mb-1">Текущий порог: {thresholdStars} из 5 звёзд ({threshold}/10)</p>
|
||||
<p className="text-sm font-semibold text-slate-800 dark:text-slate-200 mb-1">Текущий порог: {threshold} из 5 звёзд</p>
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400">
|
||||
При оценке ≥ {thresholdStars} звёзд гость видит кнопки площадок. Ниже — страницу благодарности.
|
||||
При оценке ≥ {threshold} звёзд гость видит кнопки площадок. Ниже — страницу благодарности.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -586,9 +584,9 @@ export function ReviewsPage() {
|
||||
</p>
|
||||
<div className="flex items-center gap-4">
|
||||
<div>
|
||||
<p className="text-xs text-slate-500 mb-2">Порог (из 10 баллов)</p>
|
||||
<p className="text-xs text-slate-500 mb-2">Порог (из 5 звёзд)</p>
|
||||
<input
|
||||
type="range" min={2} max={9} value={threshold}
|
||||
type="range" min={1} max={4} value={threshold}
|
||||
onChange={e => setThreshold(parseInt(e.target.value))}
|
||||
className="w-48"
|
||||
/>
|
||||
@@ -597,19 +595,19 @@ export function ReviewsPage() {
|
||||
<div className="text-3xl font-bold text-brand-600 dark:text-brand-400">{threshold}</div>
|
||||
<div className="flex gap-0.5 mt-1">
|
||||
{Array.from({ length: 5 }, (_, i) => (
|
||||
<Star key={i} size={14} className={i < thresholdStars ? 'text-amber-400 fill-amber-400' : 'text-slate-300'} />
|
||||
<Star key={i} size={14} className={i < threshold ? 'text-amber-400 fill-amber-400' : 'text-slate-300'} />
|
||||
))}
|
||||
</div>
|
||||
<p className="text-xs text-slate-400 mt-0.5">{thresholdStars} из 5 звёзд</p>
|
||||
<p className="text-xs text-slate-400 mt-0.5">{threshold} из 5 звёзд</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-3 text-xs">
|
||||
<div className="flex-1 p-2.5 rounded-lg bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-700">
|
||||
<p className="font-medium text-red-700 dark:text-red-300">Рейтинг < {threshold}/10</p>
|
||||
<p className="font-medium text-red-700 dark:text-red-300">Рейтинг < {threshold} звёзд</p>
|
||||
<p className="text-slate-500 mt-0.5">→ Модерация + личный ответ</p>
|
||||
</div>
|
||||
<div className="flex-1 p-2.5 rounded-lg bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-700">
|
||||
<p className="font-medium text-blue-700 dark:text-blue-300">Рейтинг ≥ {threshold}/10</p>
|
||||
<p className="font-medium text-blue-700 dark:text-blue-300">Рейтинг ≥ {threshold} звёзд</p>
|
||||
<p className="text-slate-500 mt-0.5">→ Авторедирект на площадки</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -738,8 +736,8 @@ export function ReviewsPage() {
|
||||
)}
|
||||
>
|
||||
<div className="flex items-start gap-3 flex-wrap">
|
||||
<div className={cn('w-10 h-10 rounded-full flex items-center justify-center text-base font-bold shrink-0', ratingColor(review.rating, threshold))}>
|
||||
{review.rating}
|
||||
<div className={cn('w-10 h-10 rounded-full flex items-center justify-center gap-0.5 text-sm font-bold shrink-0', ratingColor(review.rating, threshold))}>
|
||||
{review.rating}<Star size={10} className="fill-current" />
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
|
||||
Reference in New Issue
Block a user