Initial commit: HotelSync PMS v0.1.0
- React 18 + TypeScript + Vite + Tailwind CSS - Шахматка бронирований (drag-to-book) - Страницы: Calendar, Bookings, Rooms, Housekeeping, Channels, API Docs, Settings - Роли: super_admin, hotel_manager, housekeeper - Светлая/тёмная тема - Docker + Nginx конфигурация - Лендинг hotelsync.ru
This commit is contained in:
14
src/components/ui/Badge.tsx
Normal file
14
src/components/ui/Badge.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import { cn } from '../../lib/utils'
|
||||
|
||||
interface BadgeProps {
|
||||
children: React.ReactNode
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function Badge({ children, className }: BadgeProps) {
|
||||
return (
|
||||
<span className={cn('badge', className)}>
|
||||
{children}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
75
src/components/ui/Modal.tsx
Normal file
75
src/components/ui/Modal.tsx
Normal file
@@ -0,0 +1,75 @@
|
||||
import { useEffect } from 'react'
|
||||
import { X } from 'lucide-react'
|
||||
import { cn } from '../../lib/utils'
|
||||
|
||||
interface ModalProps {
|
||||
open: boolean
|
||||
onClose: () => void
|
||||
title: string
|
||||
children: React.ReactNode
|
||||
size?: 'sm' | 'md' | 'lg' | 'xl'
|
||||
footer?: React.ReactNode
|
||||
}
|
||||
|
||||
const SIZES = {
|
||||
sm: 'max-w-sm',
|
||||
md: 'max-w-md',
|
||||
lg: 'max-w-lg',
|
||||
xl: 'max-w-2xl',
|
||||
}
|
||||
|
||||
export function Modal({ open, onClose, title, children, size = 'md', footer }: ModalProps) {
|
||||
useEffect(() => {
|
||||
const handler = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') onClose()
|
||||
}
|
||||
if (open) document.addEventListener('keydown', handler)
|
||||
return () => document.removeEventListener('keydown', handler)
|
||||
}, [open, onClose])
|
||||
|
||||
useEffect(() => {
|
||||
document.body.style.overflow = open ? 'hidden' : ''
|
||||
return () => { document.body.style.overflow = '' }
|
||||
}, [open])
|
||||
|
||||
if (!open) return null
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
||||
{/* Backdrop */}
|
||||
<div
|
||||
className="absolute inset-0 bg-black/50 backdrop-blur-sm animate-fade-in"
|
||||
onClick={onClose}
|
||||
/>
|
||||
{/* Panel */}
|
||||
<div className={cn(
|
||||
'relative w-full bg-white dark:bg-slate-800 rounded-2xl shadow-2xl',
|
||||
'flex flex-col max-h-[90vh] animate-fade-in',
|
||||
SIZES[size],
|
||||
)}>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between px-6 py-4 border-b border-slate-200 dark:border-slate-700 shrink-0">
|
||||
<h2 className="text-lg font-semibold text-slate-900 dark:text-slate-100">
|
||||
{title}
|
||||
</h2>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="p-1.5 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-700 text-slate-500 transition-colors"
|
||||
>
|
||||
<X size={18} />
|
||||
</button>
|
||||
</div>
|
||||
{/* Body */}
|
||||
<div className="flex-1 overflow-y-auto px-6 py-4">
|
||||
{children}
|
||||
</div>
|
||||
{/* Footer */}
|
||||
{footer && (
|
||||
<div className="shrink-0 px-6 py-4 border-t border-slate-200 dark:border-slate-700 flex justify-end gap-3">
|
||||
{footer}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user