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:
@@ -1,5 +1,6 @@
|
||||
import { useState } from 'react'
|
||||
import { BedDouble, Users, Wifi, Plus, Search, Pencil } from 'lucide-react'
|
||||
import { BedDouble, Users, Wifi, Plus, Search, Pencil, ChevronDown, ChevronUp, Check, Trash2, X as XIcon } from 'lucide-react'
|
||||
import { useAmenities } from '../contexts/AmenitiesContext'
|
||||
import { MOCK_ROOMS } from '../data/mockData'
|
||||
import { MOCK_CATEGORIES } from './RoomCategoriesPage'
|
||||
import type { Room } from '../types'
|
||||
@@ -14,6 +15,12 @@ export function RoomsPage() {
|
||||
const [modalOpen, setModalOpen] = useState(false)
|
||||
const [editingRoom, setEditingRoom] = useState<Room | undefined>(undefined)
|
||||
|
||||
const { amenities, addAmenity, removeAmenity, renameAmenity } = useAmenities()
|
||||
const [showAmenities, setShowAmenities] = useState(false)
|
||||
const [newAmenity, setNewAmenity] = useState('')
|
||||
const [editingAmenity, setEditingAmenity] = useState<string | null>(null)
|
||||
const [editAmenityValue, setEditAmenityValue] = useState('')
|
||||
|
||||
const floors = [...new Set(rooms.map(r => r.floor))].sort()
|
||||
|
||||
const filtered = rooms.filter(r => {
|
||||
@@ -130,6 +137,96 @@ export function RoomsPage() {
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Справочник удобств */}
|
||||
<div className="card overflow-hidden">
|
||||
<button
|
||||
onClick={() => setShowAmenities(v => !v)}
|
||||
className="w-full flex items-center justify-between px-5 py-4 text-left hover:bg-slate-50 dark:hover:bg-slate-700/30 transition-colors"
|
||||
>
|
||||
<div>
|
||||
<p className="font-semibold text-slate-900 dark:text-slate-100">Справочник удобств</p>
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400 mt-0.5">
|
||||
{amenities.length} удобств · используется в номерах и категориях
|
||||
</p>
|
||||
</div>
|
||||
{showAmenities
|
||||
? <ChevronUp size={16} className="text-slate-400" />
|
||||
: <ChevronDown size={16} className="text-slate-400" />}
|
||||
</button>
|
||||
|
||||
{showAmenities && (
|
||||
<div className="border-t border-slate-200 dark:border-slate-700 p-5 space-y-4">
|
||||
{/* Add new */}
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
className="input flex-1 text-sm"
|
||||
placeholder="Добавить удобство (напр. Фен, Сейф, Балкон...)"
|
||||
value={newAmenity}
|
||||
onChange={e => setNewAmenity(e.target.value)}
|
||||
onKeyDown={e => {
|
||||
if (e.key === 'Enter') { e.preventDefault(); if (newAmenity.trim()) { addAmenity(newAmenity.trim()); setNewAmenity('') } }
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
onClick={() => { if (newAmenity.trim()) { addAmenity(newAmenity.trim()); setNewAmenity('') } }}
|
||||
disabled={!newAmenity.trim() || amenities.includes(newAmenity.trim())}
|
||||
className="btn-primary px-4 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
<Plus size={15} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* List */}
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-2">
|
||||
{amenities.map(a => (
|
||||
<div
|
||||
key={a}
|
||||
className="flex items-center gap-1.5 px-2.5 py-2 rounded-xl border border-slate-200 dark:border-slate-600 bg-white dark:bg-slate-800 group"
|
||||
>
|
||||
{editingAmenity === a ? (
|
||||
<>
|
||||
<input
|
||||
autoFocus
|
||||
className="input text-xs flex-1 py-0.5 h-6"
|
||||
value={editAmenityValue}
|
||||
onChange={e => setEditAmenityValue(e.target.value)}
|
||||
onKeyDown={e => {
|
||||
if (e.key === 'Enter') { renameAmenity(a, editAmenityValue); setEditingAmenity(null) }
|
||||
if (e.key === 'Escape') setEditingAmenity(null)
|
||||
}}
|
||||
/>
|
||||
<button onClick={() => { renameAmenity(a, editAmenityValue); setEditingAmenity(null) }}
|
||||
className="text-emerald-500 hover:text-emerald-600 shrink-0"><Check size={13} /></button>
|
||||
<button onClick={() => setEditingAmenity(null)}
|
||||
className="text-slate-400 hover:text-slate-600 shrink-0"><XIcon size={13} /></button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span className="text-xs text-slate-700 dark:text-slate-300 flex-1 truncate">{a}</span>
|
||||
<div className="flex gap-0.5 opacity-0 group-hover:opacity-100 transition-opacity shrink-0">
|
||||
<button
|
||||
onClick={() => { setEditingAmenity(a); setEditAmenityValue(a) }}
|
||||
className="p-0.5 rounded hover:bg-slate-100 dark:hover:bg-slate-700 text-slate-400 hover:text-slate-700"
|
||||
>
|
||||
<Pencil size={11} />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => removeAmenity(a)}
|
||||
className="p-0.5 rounded hover:bg-red-100 dark:hover:bg-red-900/30 text-slate-400 hover:text-red-500"
|
||||
>
|
||||
<Trash2 size={11} />
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Modal */}
|
||||
<RoomModal
|
||||
open={modalOpen}
|
||||
|
||||
Reference in New Issue
Block a user