style: remove all emojis, apply saas minimal dark mode design system
This commit is contained in:
@@ -3,95 +3,125 @@ import { useAppStore } from './store/useAppStore';
|
|||||||
import FloorPlan from './components/FloorPlan';
|
import FloorPlan from './components/FloorPlan';
|
||||||
import Timeline from './components/Timeline';
|
import Timeline from './components/Timeline';
|
||||||
import ReservationList from './components/ReservationList';
|
import ReservationList from './components/ReservationList';
|
||||||
import BookingModal from './components/BookingModal';
|
import ReservationModal from './components/BookingModal';
|
||||||
import { ViewMode } from './types';
|
import { ViewMode } from './types';
|
||||||
|
|
||||||
const NAV_ITEMS: { id: ViewMode; label: string; icon: string }[] = [
|
const NAV = [
|
||||||
{ id: 'floor', label: '2D Tischplan', icon: '⬛' },
|
{ id: 'floor' as ViewMode, label: 'Tischplan' },
|
||||||
{ id: 'timeline', label: 'Timeline', icon: '📊' },
|
{ id: 'timeline' as ViewMode, label: 'Timeline' },
|
||||||
{ id: 'list', label: 'Reservierungen', icon: '📋' },
|
{ id: 'list' as ViewMode, label: 'Reservierungen' },
|
||||||
];
|
];
|
||||||
|
|
||||||
function App() {
|
export default function App() {
|
||||||
const [view, setView] = useState<ViewMode>('floor');
|
const [view, setView] = useState<ViewMode>('floor');
|
||||||
const { tables, reservations, showBookingModal, openBookingModal } = useAppStore();
|
const { tables, reservations, showBookingModal, openBookingModal } = useAppStore();
|
||||||
|
|
||||||
const activeCount = reservations.filter((r) => ['PENDING', 'CONFIRMED', 'ARRIVED'].includes(r.status)).length;
|
|
||||||
const freeCount = tables.filter((t) => t.status === 'FREE').length;
|
const freeCount = tables.filter((t) => t.status === 'FREE').length;
|
||||||
|
const activeCount = reservations.filter((r) => ['PENDING', 'CONFIRMED', 'ARRIVED'].includes(r.status)).length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-slate-950 text-slate-100" style={{ fontFamily: "'Inter', system-ui, sans-serif" }}>
|
<div style={{ display: 'flex', flexDirection: 'column', minHeight: '100vh', background: 'var(--color-bg)' }}>
|
||||||
{/* Header */}
|
{/* ── Header ── */}
|
||||||
<header className="sticky top-0 z-40 border-b border-slate-800/80 bg-slate-950/95 backdrop-blur-xl px-4 lg:px-6 py-3">
|
<header style={{
|
||||||
<div className="max-w-7xl mx-auto flex items-center justify-between gap-4">
|
position: 'sticky', top: 0, zIndex: 50,
|
||||||
{/* Brand */}
|
background: 'rgba(9,9,13,0.92)',
|
||||||
<div className="flex items-center gap-3">
|
backdropFilter: 'blur(16px)',
|
||||||
<div className="w-9 h-9 rounded-xl bg-gradient-to-br from-amber-400 to-orange-500 flex items-center justify-center font-black text-slate-950 text-sm shadow-lg shadow-amber-500/25">
|
borderBottom: '1px solid var(--color-border)',
|
||||||
PL
|
padding: '0 1.5rem',
|
||||||
|
height: 56,
|
||||||
|
display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '1rem',
|
||||||
|
}}>
|
||||||
|
{/* Brand */}
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: '0.75rem' }}>
|
||||||
|
<div style={{
|
||||||
|
width: 32, height: 32, borderRadius: 8,
|
||||||
|
background: 'var(--color-accent)',
|
||||||
|
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||||
|
fontSize: 12, fontWeight: 700, color: '#fff',
|
||||||
|
letterSpacing: '-0.5px',
|
||||||
|
boxShadow: '0 0 20px var(--color-accent-glow)',
|
||||||
|
}}>PL</div>
|
||||||
|
<div>
|
||||||
|
<div style={{ fontWeight: 600, fontSize: 14, color: 'var(--color-text)', letterSpacing: '-0.2px' }}>
|
||||||
|
Pizzeria Luigi
|
||||||
</div>
|
</div>
|
||||||
<div className="hidden sm:block">
|
<div style={{ fontSize: 11, color: 'var(--color-muted)', display: 'flex', alignItems: 'center', gap: 5 }}>
|
||||||
<p className="font-bold text-base leading-tight">Pizzeria Luigi</p>
|
<span style={{ width: 5, height: 5, borderRadius: '50%', background: 'var(--color-success)', display: 'inline-block' }} />
|
||||||
<p className="text-xs text-slate-500 flex items-center gap-1.5">
|
{new Date().toLocaleDateString('de-DE', { weekday: 'long', day: '2-digit', month: 'short' })}
|
||||||
<span className="w-1.5 h-1.5 rounded-full bg-emerald-500 animate-pulse" />
|
|
||||||
Live · {new Date().toLocaleDateString('de-DE', { weekday: 'long', day: '2-digit', month: 'long' })}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Stats */}
|
|
||||||
<div className="hidden md:flex items-center gap-4 text-xs">
|
|
||||||
<div className="flex items-center gap-2 px-3 py-1.5 bg-slate-900 rounded-xl border border-slate-800">
|
|
||||||
<span className="w-2 h-2 rounded-full bg-emerald-500" />
|
|
||||||
<span className="text-slate-400">Frei:</span>
|
|
||||||
<span className="font-bold text-emerald-400">{freeCount}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2 px-3 py-1.5 bg-slate-900 rounded-xl border border-slate-800">
|
|
||||||
<span className="w-2 h-2 rounded-full bg-amber-400" />
|
|
||||||
<span className="text-slate-400">Aktiv:</span>
|
|
||||||
<span className="font-bold text-amber-400">{activeCount}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Nav */}
|
|
||||||
<nav className="flex bg-slate-900/80 p-1 rounded-xl border border-slate-800 gap-1">
|
|
||||||
{NAV_ITEMS.map((item) => (
|
|
||||||
<button
|
|
||||||
key={item.id}
|
|
||||||
onClick={() => setView(item.id)}
|
|
||||||
className={`px-3 py-1.5 rounded-lg text-xs font-semibold transition flex items-center gap-1.5 ${
|
|
||||||
view === item.id
|
|
||||||
? 'bg-amber-500 text-slate-950 shadow'
|
|
||||||
: 'text-slate-400 hover:text-slate-200'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<span>{item.icon}</span>
|
|
||||||
<span className="hidden sm:inline">{item.label}</span>
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
{/* CTA */}
|
|
||||||
<button
|
|
||||||
onClick={() => openBookingModal()}
|
|
||||||
className="bg-emerald-500 hover:bg-emerald-400 active:scale-95 text-slate-950 font-bold px-4 py-2 rounded-xl text-xs transition shadow-lg shadow-emerald-500/20 flex items-center gap-1.5"
|
|
||||||
>
|
|
||||||
<span>+</span>
|
|
||||||
<span className="hidden sm:inline">Walk-In</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Stats */}
|
||||||
|
<div style={{ display: 'flex', gap: '0.5rem' }}>
|
||||||
|
<Chip color="success" label="Frei" value={freeCount} />
|
||||||
|
<Chip color="accent" label="Aktiv" value={activeCount} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Nav */}
|
||||||
|
<nav style={{
|
||||||
|
display: 'flex', background: 'var(--color-surface)', borderRadius: 10,
|
||||||
|
border: '1px solid var(--color-border)', padding: 3, gap: 2,
|
||||||
|
}}>
|
||||||
|
{NAV.map((n) => (
|
||||||
|
<button
|
||||||
|
key={n.id}
|
||||||
|
onClick={() => setView(n.id)}
|
||||||
|
style={{
|
||||||
|
padding: '5px 14px', borderRadius: 7, fontSize: 12, fontWeight: 600, cursor: 'pointer',
|
||||||
|
border: 'none', transition: 'all 0.15s',
|
||||||
|
background: view === n.id ? 'var(--color-accent)' : 'transparent',
|
||||||
|
color: view === n.id ? '#fff' : 'var(--color-muted)',
|
||||||
|
boxShadow: view === n.id ? '0 2px 12px var(--color-accent-glow)' : 'none',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{n.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
{/* CTA */}
|
||||||
|
<button
|
||||||
|
onClick={() => openBookingModal()}
|
||||||
|
style={{
|
||||||
|
background: 'var(--color-accent)', color: '#fff',
|
||||||
|
border: 'none', borderRadius: 8, cursor: 'pointer',
|
||||||
|
padding: '7px 16px', fontSize: 12, fontWeight: 600,
|
||||||
|
display: 'flex', alignItems: 'center', gap: 6,
|
||||||
|
boxShadow: '0 2px 16px var(--color-accent-glow)',
|
||||||
|
transition: 'all 0.15s',
|
||||||
|
}}
|
||||||
|
onMouseEnter={(e) => (e.currentTarget.style.background = 'var(--color-accent-hover)')}
|
||||||
|
onMouseLeave={(e) => (e.currentTarget.style.background = 'var(--color-accent)')}
|
||||||
|
>
|
||||||
|
Walk-In
|
||||||
|
</button>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{/* Main */}
|
{/* ── Content ── */}
|
||||||
<main className="max-w-7xl mx-auto p-4 lg:p-6">
|
<main style={{ flex: 1, maxWidth: 1280, width: '100%', margin: '0 auto', padding: '1.5rem' }}>
|
||||||
{view === 'floor' && <FloorPlan tables={tables} />}
|
{view === 'floor' && <FloorPlan tables={tables} />}
|
||||||
{view === 'timeline' && <Timeline />}
|
{view === 'timeline' && <Timeline />}
|
||||||
{view === 'list' && <ReservationList />}
|
{view === 'list' && <ReservationList />}
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
{/* Booking Modal */}
|
{showBookingModal && <ReservationModal />}
|
||||||
{showBookingModal && <BookingModal />}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default App;
|
// ─── Shared atoms ────────────────────────────────────────────────────────────
|
||||||
|
function Chip({ label, value, color }: { label: string; value: number; color: 'success' | 'accent' }) {
|
||||||
|
const c = color === 'success' ? 'var(--color-success)' : 'var(--color-accent)';
|
||||||
|
return (
|
||||||
|
<div style={{
|
||||||
|
display: 'flex', alignItems: 'center', gap: 6,
|
||||||
|
background: 'var(--color-surface)', borderRadius: 8,
|
||||||
|
border: '1px solid var(--color-border)', padding: '4px 10px', fontSize: 11,
|
||||||
|
}}>
|
||||||
|
<span style={{ width: 6, height: 6, borderRadius: '50%', background: c }} />
|
||||||
|
<span style={{ color: 'var(--color-muted)' }}>{label}</span>
|
||||||
|
<span style={{ fontWeight: 700, color: c }}>{value}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -18,12 +18,12 @@ const DURATION_OPTIONS = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const EXTRA_TAGS = [
|
const EXTRA_TAGS = [
|
||||||
{ id: 'Hochstuhl', icon: '🪑', label: 'Hochstuhl' },
|
{ id: 'Hochstuhl', label: 'Hochstuhl' },
|
||||||
{ id: 'Geburtstag', icon: '🎂', label: 'Geburtstag' },
|
{ id: 'Geburtstag', label: 'Geburtstag' },
|
||||||
{ id: 'Fensterplatz', icon: '🪟', label: 'Fensterplatz' },
|
{ id: 'Fensterplatz', label: 'Fensterplatz' },
|
||||||
{ id: 'Hund', icon: '🐕', label: 'Hund' },
|
{ id: 'Hund', label: 'Hund' },
|
||||||
{ id: 'Terrasse', icon: '☀️', label: 'Terrasse / Außen' },
|
{ id: 'Terrasse', label: 'Terrasse / Außen' },
|
||||||
{ id: 'Allergie', icon: '⚠️', label: 'Allergie' },
|
{ id: 'Allergie', label: 'Allergie' },
|
||||||
];
|
];
|
||||||
|
|
||||||
// ─── Helpers ────────────────────────────────────────────────────────────────
|
// ─── Helpers ────────────────────────────────────────────────────────────────
|
||||||
@@ -230,7 +230,6 @@ export default function ReservationModal() {
|
|||||||
: 'bg-slate-800/50 border-slate-700 text-slate-400 hover:border-slate-500 hover:text-slate-300'
|
: 'bg-slate-800/50 border-slate-700 text-slate-400 hover:border-slate-500 hover:text-slate-300'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<span className="text-base">{tag.icon}</span>
|
|
||||||
<span>{tag.label}</span>
|
<span>{tag.label}</span>
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ export default function FloorPlan({ tables }: Props) {
|
|||||||
cursor-pointer shadow-lg`}
|
cursor-pointer shadow-lg`}
|
||||||
>
|
>
|
||||||
<span className="font-bold text-xl">{t.number}</span>
|
<span className="font-bold text-xl">{t.number}</span>
|
||||||
<span className="text-xs opacity-75">👤 {t.maxCapacity}</span>
|
<span className="text-xs opacity-60">{t.maxCapacity} P.</span>
|
||||||
<span className="text-[10px] font-semibold uppercase tracking-wider opacity-80">
|
<span className="text-[10px] font-semibold uppercase tracking-wider opacity-80">
|
||||||
{STATUS_LABEL[t.status]}
|
{STATUS_LABEL[t.status]}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -27,11 +27,13 @@ const NEXT_STATUS: Partial<Record<ReservationStatus, ReservationStatus>> = {
|
|||||||
ARRIVED: 'FINISHED',
|
ARRIVED: 'FINISHED',
|
||||||
};
|
};
|
||||||
|
|
||||||
const TAG_ICONS: Record<string, string> = {
|
const TAG_LABELS: Record<string, string> = {
|
||||||
Hochstuhl: '🪑',
|
Hochstuhl: 'Hochstuhl',
|
||||||
Allergie: '⚠️',
|
Allergie: 'Allergie',
|
||||||
Fensterplatz: '🪟',
|
Fensterplatz: 'Fensterplatz',
|
||||||
Geburtstag: '🎂',
|
Geburtstag: 'Geburtstag',
|
||||||
|
Hund: 'Hund',
|
||||||
|
Terrasse: 'Terrasse',
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function ReservationList() {
|
export default function ReservationList() {
|
||||||
@@ -54,17 +56,17 @@ export default function ReservationList() {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs mt-1 text-current/70 flex flex-wrap gap-x-3 gap-y-0.5">
|
<div className="text-xs mt-1 text-current/70 flex flex-wrap gap-x-3 gap-y-0.5">
|
||||||
<span>👤 {r.guestCount} Pers.</span>
|
<span>{r.guestCount} Pers.</span>
|
||||||
<span>🕐 {r.startTime}–{r.endTime}</span>
|
<span>{r.startTime}–{r.endTime}</span>
|
||||||
<span>📋 Tisch {getTableNames(r.tableIds)}</span>
|
<span>Tisch {getTableNames(r.tableIds)}</span>
|
||||||
{r.guest.phone && <span>📞 {r.guest.phone}</span>}
|
{r.guest.phone && <span>{r.guest.phone}</span>}
|
||||||
</div>
|
</div>
|
||||||
{r.tags.length > 0 && (
|
{r.tags.length > 0 && (
|
||||||
<div className="flex gap-1 mt-2 flex-wrap">
|
<div className="flex gap-1 mt-2 flex-wrap">
|
||||||
{r.tags.map((tag) => (
|
{r.tags.map((tag) => (
|
||||||
<span key={tag} className="text-xs px-2 py-0.5 rounded-full bg-slate-800/80 text-slate-300 border border-slate-700">
|
<span key={tag} className="text-xs px-2 py-0.5 rounded-full bg-slate-800/80 text-slate-300 border border-slate-700">
|
||||||
{TAG_ICONS[tag] ?? '🏷️'} {tag}
|
{TAG_LABELS[tag] ?? tag}
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1 +1,49 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
|
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
|
||||||
|
|
||||||
|
@theme {
|
||||||
|
--font-sans: 'Inter', system-ui, sans-serif;
|
||||||
|
|
||||||
|
--color-bg: #09090d;
|
||||||
|
--color-surface: #111116;
|
||||||
|
--color-surface-2: #1a1a24;
|
||||||
|
--color-border: #1f1f2e;
|
||||||
|
--color-border-2: #2a2a3a;
|
||||||
|
|
||||||
|
--color-accent: #6366f1;
|
||||||
|
--color-accent-hover: #818cf8;
|
||||||
|
--color-accent-dim: rgba(99,102,241,0.12);
|
||||||
|
--color-accent-glow: rgba(99,102,241,0.25);
|
||||||
|
|
||||||
|
--color-success: #22c55e;
|
||||||
|
--color-warning: #f59e0b;
|
||||||
|
--color-danger: #ef4444;
|
||||||
|
--color-sky: #38bdf8;
|
||||||
|
|
||||||
|
--color-text: #f4f4f8;
|
||||||
|
--color-muted: #71717a;
|
||||||
|
--color-subtle: #3f3f52;
|
||||||
|
}
|
||||||
|
|
||||||
|
*, *::before, *::after { box-sizing: border-box; }
|
||||||
|
|
||||||
|
html, body, #root {
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
background-color: var(--color-bg);
|
||||||
|
color: var(--color-text);
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Scrollbar */
|
||||||
|
::-webkit-scrollbar { width: 6px; height: 6px; }
|
||||||
|
::-webkit-scrollbar-track { background: transparent; }
|
||||||
|
::-webkit-scrollbar-thumb { background: var(--color-border-2); border-radius: 3px; }
|
||||||
|
|
||||||
|
/* Selection */
|
||||||
|
::selection { background: var(--color-accent-dim); color: var(--color-text); }
|
||||||
|
|
||||||
|
/* Focus ring */
|
||||||
|
:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }
|
||||||
|
|||||||
Reference in New Issue
Block a user