rates/frontend/src/components/Layout.tsx
jtricerolph e05054172f Add Rate Monitor app — Booking.com + direct booking engine competitor rates
Combines Booking.com Playwright scraper (from forecasting), direct booking
engine scraper (ported from laptop-archive/guestline-monitor), and Newbook
own-hotel rates into one focused tool. Four views: Bookability, Market View
(with price index badges + direct rate sub-rows), Direct Rates (per-competitor
room breakdown, min-stay flags, hotel config/discovery), Rate Analysis
(advance purchase curve, DOW chart, rate timeline, comparison table).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-05 12:06:30 +00:00

70 lines
2.6 KiB
TypeScript

import { NavLink } from 'react-router-dom'
import { TrendingUp, Calendar, Globe, BarChart2, Settings, Building2 } from 'lucide-react'
import { useQuery } from '@tanstack/react-query'
import { useAuth } from './AuthGate'
import { can } from '../types'
import api from '../api'
import type { ReactNode } from 'react'
const ICON = { size: 16, strokeWidth: 1.75 }
const NAV = [
{ to: '/bookability', label: 'Bookability', icon: Calendar, cap: 'view_own_rates' },
{ to: '/market', label: 'Market View', icon: Globe, cap: 'view_competitors' },
{ to: '/direct', label: 'Direct Rates', icon: Building2, cap: 'view_direct_rates' },
{ to: '/analysis', label: 'Rate Analysis', icon: TrendingUp, cap: 'rate_analysis' },
{ to: '/settings', label: 'Settings', icon: Settings, cap: 'manage_scraper' },
]
export default function Layout({ children }: { children: ReactNode }) {
const { user } = useAuth()
const items = NAV.filter(n => can(user, n.cap))
const { data: alertCount } = useQuery<number>({
queryKey: ['parity-alert-count'],
queryFn: () => api.get('/competitors/parity/alerts?status=active').then(r => r.data.length),
refetchInterval: 60_000,
enabled: can(user, 'view_competitors'),
})
return (
<div className="app-shell">
<aside className="sidebar">
<div className="sidebar-logo">
<BarChart2 size={18} strokeWidth={1.75} />
Rate Monitor
</div>
<nav className="sidebar-nav">
{items.map(({ to, label, icon: Icon }) => (
<NavLink key={to} to={to} className={({ isActive }) => isActive ? 'active' : ''}>
<Icon {...ICON} />
{label}
{to === '/market' && alertCount ? (
<span style={{
marginLeft: 'auto', background: 'var(--danger)', color: '#fff',
borderRadius: 10, fontSize: 10, fontWeight: 700,
padding: '1px 6px', lineHeight: '16px',
}}>{alertCount}</span>
) : null}
</NavLink>
))}
</nav>
<div className="sidebar-user">{user.name || user.email}</div>
</aside>
<header className="top-bar">
<BarChart2 size={18} strokeWidth={1.75} color="var(--gold)" />
<span className="top-bar-title">Rate Monitor</span>
<nav className="top-bar-nav">
{items.map(({ to, label }) => (
<NavLink key={to} to={to} className={({ isActive }) => isActive ? 'active' : ''}>
{label}
</NavLink>
))}
</nav>
</header>
<main className="page-content">{children}</main>
</div>
)
}