style: remove all emojis, apply saas minimal dark mode design system
This commit is contained in:
@@ -3,69 +3,79 @@ import { useAppStore } from './store/useAppStore';
|
||||
import FloorPlan from './components/FloorPlan';
|
||||
import Timeline from './components/Timeline';
|
||||
import ReservationList from './components/ReservationList';
|
||||
import BookingModal from './components/BookingModal';
|
||||
import ReservationModal from './components/BookingModal';
|
||||
import { ViewMode } from './types';
|
||||
|
||||
const NAV_ITEMS: { id: ViewMode; label: string; icon: string }[] = [
|
||||
{ id: 'floor', label: '2D Tischplan', icon: '⬛' },
|
||||
{ id: 'timeline', label: 'Timeline', icon: '📊' },
|
||||
{ id: 'list', label: 'Reservierungen', icon: '📋' },
|
||||
const NAV = [
|
||||
{ id: 'floor' as ViewMode, label: 'Tischplan' },
|
||||
{ id: 'timeline' as ViewMode, label: 'Timeline' },
|
||||
{ id: 'list' as ViewMode, label: 'Reservierungen' },
|
||||
];
|
||||
|
||||
function App() {
|
||||
export default function App() {
|
||||
const [view, setView] = useState<ViewMode>('floor');
|
||||
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 activeCount = reservations.filter((r) => ['PENDING', 'CONFIRMED', 'ARRIVED'].includes(r.status)).length;
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-slate-950 text-slate-100" style={{ fontFamily: "'Inter', system-ui, sans-serif" }}>
|
||||
{/* 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">
|
||||
<div className="max-w-7xl mx-auto flex items-center justify-between gap-4">
|
||||
<div style={{ display: 'flex', flexDirection: 'column', minHeight: '100vh', background: 'var(--color-bg)' }}>
|
||||
{/* ── Header ── */}
|
||||
<header style={{
|
||||
position: 'sticky', top: 0, zIndex: 50,
|
||||
background: 'rgba(9,9,13,0.92)',
|
||||
backdropFilter: 'blur(16px)',
|
||||
borderBottom: '1px solid var(--color-border)',
|
||||
padding: '0 1.5rem',
|
||||
height: 56,
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '1rem',
|
||||
}}>
|
||||
{/* Brand */}
|
||||
<div className="flex items-center gap-3">
|
||||
<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">
|
||||
PL
|
||||
<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 style={{ fontSize: 11, color: 'var(--color-muted)', display: 'flex', alignItems: 'center', gap: 5 }}>
|
||||
<span style={{ width: 5, height: 5, borderRadius: '50%', background: 'var(--color-success)', display: 'inline-block' }} />
|
||||
{new Date().toLocaleDateString('de-DE', { weekday: 'long', day: '2-digit', month: 'short' })}
|
||||
</div>
|
||||
<div className="hidden sm:block">
|
||||
<p className="font-bold text-base leading-tight">Pizzeria Luigi</p>
|
||||
<p className="text-xs text-slate-500 flex items-center gap-1.5">
|
||||
<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>
|
||||
|
||||
{/* 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 style={{ display: 'flex', gap: '0.5rem' }}>
|
||||
<Chip color="success" label="Frei" value={freeCount} />
|
||||
<Chip color="accent" label="Aktiv" value={activeCount} />
|
||||
</div>
|
||||
|
||||
{/* Nav */}
|
||||
<nav className="flex bg-slate-900/80 p-1 rounded-xl border border-slate-800 gap-1">
|
||||
{NAV_ITEMS.map((item) => (
|
||||
<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={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'
|
||||
}`}
|
||||
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',
|
||||
}}
|
||||
>
|
||||
<span>{item.icon}</span>
|
||||
<span className="hidden sm:inline">{item.label}</span>
|
||||
{n.label}
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
@@ -73,25 +83,45 @@ function App() {
|
||||
{/* 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"
|
||||
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)')}
|
||||
>
|
||||
<span>+</span>
|
||||
<span className="hidden sm:inline">Walk-In</span>
|
||||
Walk-In
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Main */}
|
||||
<main className="max-w-7xl mx-auto p-4 lg:p-6">
|
||||
{/* ── Content ── */}
|
||||
<main style={{ flex: 1, maxWidth: 1280, width: '100%', margin: '0 auto', padding: '1.5rem' }}>
|
||||
{view === 'floor' && <FloorPlan tables={tables} />}
|
||||
{view === 'timeline' && <Timeline />}
|
||||
{view === 'list' && <ReservationList />}
|
||||
</main>
|
||||
|
||||
{/* Booking Modal */}
|
||||
{showBookingModal && <BookingModal />}
|
||||
{showBookingModal && <ReservationModal />}
|
||||
</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 = [
|
||||
{ id: 'Hochstuhl', icon: '🪑', label: 'Hochstuhl' },
|
||||
{ id: 'Geburtstag', icon: '🎂', label: 'Geburtstag' },
|
||||
{ id: 'Fensterplatz', icon: '🪟', label: 'Fensterplatz' },
|
||||
{ id: 'Hund', icon: '🐕', label: 'Hund' },
|
||||
{ id: 'Terrasse', icon: '☀️', label: 'Terrasse / Außen' },
|
||||
{ id: 'Allergie', icon: '⚠️', label: 'Allergie' },
|
||||
{ id: 'Hochstuhl', label: 'Hochstuhl' },
|
||||
{ id: 'Geburtstag', label: 'Geburtstag' },
|
||||
{ id: 'Fensterplatz', label: 'Fensterplatz' },
|
||||
{ id: 'Hund', label: 'Hund' },
|
||||
{ id: 'Terrasse', label: 'Terrasse / Außen' },
|
||||
{ id: 'Allergie', label: 'Allergie' },
|
||||
];
|
||||
|
||||
// ─── 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'
|
||||
}`}
|
||||
>
|
||||
<span className="text-base">{tag.icon}</span>
|
||||
<span>{tag.label}</span>
|
||||
</button>
|
||||
))}
|
||||
|
||||
@@ -62,7 +62,7 @@ export default function FloorPlan({ tables }: Props) {
|
||||
cursor-pointer shadow-lg`}
|
||||
>
|
||||
<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">
|
||||
{STATUS_LABEL[t.status]}
|
||||
</span>
|
||||
|
||||
@@ -27,11 +27,13 @@ const NEXT_STATUS: Partial<Record<ReservationStatus, ReservationStatus>> = {
|
||||
ARRIVED: 'FINISHED',
|
||||
};
|
||||
|
||||
const TAG_ICONS: Record<string, string> = {
|
||||
Hochstuhl: '🪑',
|
||||
Allergie: '⚠️',
|
||||
Fensterplatz: '🪟',
|
||||
Geburtstag: '🎂',
|
||||
const TAG_LABELS: Record<string, string> = {
|
||||
Hochstuhl: 'Hochstuhl',
|
||||
Allergie: 'Allergie',
|
||||
Fensterplatz: 'Fensterplatz',
|
||||
Geburtstag: 'Geburtstag',
|
||||
Hund: 'Hund',
|
||||
Terrasse: 'Terrasse',
|
||||
};
|
||||
|
||||
export default function ReservationList() {
|
||||
@@ -54,16 +56,16 @@ export default function ReservationList() {
|
||||
</span>
|
||||
</div>
|
||||
<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.startTime}–{r.endTime}</span>
|
||||
<span>📋 Tisch {getTableNames(r.tableIds)}</span>
|
||||
{r.guest.phone && <span>📞 {r.guest.phone}</span>}
|
||||
<span>{r.guestCount} Pers.</span>
|
||||
<span>{r.startTime}–{r.endTime}</span>
|
||||
<span>Tisch {getTableNames(r.tableIds)}</span>
|
||||
{r.guest.phone && <span>{r.guest.phone}</span>}
|
||||
</div>
|
||||
{r.tags.length > 0 && (
|
||||
<div className="flex gap-1 mt-2 flex-wrap">
|
||||
{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">
|
||||
{TAG_ICONS[tag] ?? '🏷️'} {tag}
|
||||
{TAG_LABELS[tag] ?? tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -1 +1,49 @@
|
||||
@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