Add centralized amenities directory and hourly booking example

- AmenitiesContext: shared amenities list with add/edit/delete, available app-wide
- RoomsPage: collapsible Справочник удобств section (add/rename/delete)
- RoomModal + RoomCategoriesPage: use amenities from context, no hardcoded lists
- RoomCategoriesPage: removed custom amenity input (use справочник instead)
- mockData: room 103 now has allowHourly=true, added sample hourly booking (today, 10-13ч)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-16 15:40:01 +03:00
parent c347c99228
commit 87455c2710
6 changed files with 156 additions and 59 deletions

View File

@@ -1,7 +1,8 @@
import { useState, useRef } from 'react'
import { Plus, Pencil, Trash2, ImagePlus, X as XIcon, Tag, ChevronRight, Check } from 'lucide-react'
import { Plus, Pencil, Trash2, ImagePlus, X as XIcon, Tag, ChevronRight } from 'lucide-react'
import { cn } from '../lib/utils'
import type { RoomCategory } from '../types'
import { useAmenities } from '../contexts/AmenitiesContext'
// ── Mock data ─────────────────────────────────────────────────────────────────
@@ -10,11 +11,6 @@ const CATEGORY_COLORS = [
'#DC2626', '#D97706', '#DB2777', '#475569',
]
const AMENITY_LIST = [
'Wi-Fi', 'TV', 'AC', 'Mini-bar', 'Bathrobe', 'Jacuzzi',
'Panoramic view', 'Kitchen', 'Terrace', 'Butler',
]
export const MOCK_CATEGORIES: RoomCategory[] = [
{
id: 'cat1',
@@ -62,6 +58,7 @@ interface CategoryFormProps {
function CategoryForm({ category, onClose, onSave }: CategoryFormProps) {
const isEdit = !!category
const { amenities: allAmenities } = useAmenities()
const [tab, setTab] = useState<'main' | 'photos'>('main')
const [name, setName] = useState(category?.name ?? '')
const [description, setDescription] = useState(category?.description ?? '')
@@ -71,21 +68,9 @@ function CategoryForm({ category, onClose, onSave }: CategoryFormProps) {
const [photoIdx, setPhotoIdx] = useState(0)
const fileRef = useRef<HTMLInputElement>(null)
const [customInput, setCustomInput] = useState('')
const toggleAmenity = (a: string) =>
setAmenities(prev => prev.includes(a) ? prev.filter(x => x !== a) : [...prev, a])
const addCustom = () => {
const v = customInput.trim()
if (!v || amenities.includes(v)) return
setAmenities(prev => [...prev, v])
setCustomInput('')
}
// All amenities to show as toggles: predefined + any custom ones already in the list
const customAmenities = amenities.filter(a => !AMENITY_LIST.includes(a))
const handlePhotoUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
Array.from(e.target.files ?? []).forEach(file => {
const reader = new FileReader()
@@ -190,9 +175,12 @@ function CategoryForm({ category, onClose, onSave }: CategoryFormProps) {
</div>
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">Удобства в номере</label>
<div className="flex items-center justify-between mb-2">
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300">Удобства в номере</label>
<span className="text-xs text-slate-400">Справочник удобств на странице «Номера»</span>
</div>
<div className="flex flex-wrap gap-2">
{AMENITY_LIST.map(a => {
{allAmenities.map(a => {
const sel = amenities.includes(a)
return (
<button
@@ -210,38 +198,10 @@ function CategoryForm({ category, onClose, onSave }: CategoryFormProps) {
</button>
)
})}
{/* Custom amenities */}
{customAmenities.map(a => (
<span
key={a}
className="flex items-center gap-1 pl-3 pr-1 py-1.5 rounded-lg text-xs font-medium border bg-brand-600 text-white border-brand-600"
>
{a}
<button type="button" onClick={() => toggleAmenity(a)} className="w-4 h-4 rounded-full hover:bg-brand-700 flex items-center justify-center">
<XIcon size={10} />
</button>
</span>
))}
</div>
{/* Add custom amenity */}
<div className="flex gap-2 mt-2">
<input
type="text"
className="input flex-1 text-sm"
placeholder="Добавить своё удобство..."
value={customInput}
onChange={e => setCustomInput(e.target.value)}
onKeyDown={e => e.key === 'Enter' && (e.preventDefault(), addCustom())}
/>
<button
type="button"
onClick={addCustom}
disabled={!customInput.trim() || amenities.includes(customInput.trim())}
className="btn-secondary px-3 disabled:opacity-40 disabled:cursor-not-allowed"
>
<Check size={14} />
</button>
</div>
{amenities.length > 0 && (
<p className="text-xs text-slate-400 mt-1.5">Выбрано: {amenities.length}</p>
)}
</div>
</>
)}