import React, { useState, useEffect, useMemo } from 'react';
import {
Users, Check, ArrowLeft, Plus, Trash2, Settings, X, MessageSquare, AlertCircle
} from 'lucide-react';
const KEY_EVENTS = 'reservation:events:v1';
const KEY_RESERVATIONS = 'reservation:reservations:v1';
const ADMIN_KEY = 'admin';
const DEFAULT_EVENTS = [
{ id: 'e1', date: '2026-05-30', time: '19:00', title: 'NEON Chefs — Frühling', capacity: 12 },
{ id: 'e2', date: '2026-06-06', time: '19:00', title: 'Pop-up Dinner', capacity: 14 },
{ id: 'e3', date: '2026-06-20', time: '19:00', title: 'NEON Chefs — Sommer', capacity: 12 },
];
// ─── Helpers ──────────────────────────────────────────────────────────────────
function formatDate(iso) {
const d = new Date(iso + 'T12:00:00');
return d.toLocaleDateString('de-CH', {
weekday: 'long', day: 'numeric', month: 'long', year: 'numeric',
});
}
function formatDateShort(iso) {
const d = new Date(iso + 'T12:00:00');
const wd = d.toLocaleDateString('de-CH', { weekday: 'short' }).replace('.', '');
const day = d.getDate();
const mo = d.toLocaleDateString('de-CH', { month: 'short' }).replace('.', '');
return `${wd} ${day}. ${mo}`;
}
function uid() { return Math.random().toString(36).slice(2, 10) + Date.now().toString(36); }
function todayISO() { return new Date().toISOString().slice(0, 10); }
async function safeGet(key) {
try { const r = await window.storage.get(key, true); return r ? JSON.parse(r.value) : null; }
catch { return null; }
}
async function safeSet(key, value) {
try { await window.storage.set(key, JSON.stringify(value), true); return true; }
catch (e) { console.error(e); return false; }
}
// ─── App ──────────────────────────────────────────────────────────────────────
export default function App() {
const [view, setView] = useState('form');
const [loading, setLoading] = useState(true);
const [events, setEvents] = useState([]);
const [reservations, setReservations] = useState([]);
const [lastReservation, setLastReservation] = useState(null);
const [selectedEventId, setSelectedEventId] = useState('');
const [partySize, setPartySize] = useState(2);
const [vorname, setVorname] = useState('');
const [nachname, setNachname] = useState('');
const [email, setEmail] = useState('');
const [phone, setPhone] = useState('');
const [comments, setComments] = useState('');
const [errors, setErrors] = useState({});
// Admin: type "admin" anywhere
useEffect(() => {
let buf = '';
const onKey = (e) => {
if (e.key.length !== 1) return;
buf = (buf + e.key.toLowerCase()).slice(-ADMIN_KEY.length);
if (buf === ADMIN_KEY) setView('admin');
};
window.addEventListener('keydown', onKey);
return () => window.removeEventListener('keydown', onKey);
}, []);
useEffect(() => {
(async () => {
let evts = await safeGet(KEY_EVENTS);
if (!evts) { evts = DEFAULT_EVENTS; await safeSet(KEY_EVENTS, evts); }
const resvs = (await safeGet(KEY_RESERVATIONS)) || [];
setEvents(evts);
setReservations(resvs);
setLoading(false);
})();
}, []);
const bookedByEvent = useMemo(() => {
const m = {};
for (const r of reservations) m[r.eventId] = (m[r.eventId] || 0) + Number(r.partySize || 0);
return m;
}, [reservations]);
const upcomingEvents = useMemo(() => {
const t = todayISO();
return events.filter((e) => e.date >= t)
.sort((a, b) => a.date.localeCompare(b.date) || a.time.localeCompare(b.time));
}, [events]);
const selectedEvent = events.find((e) => e.id === selectedEventId);
const remainingForSelected = selectedEvent ? selectedEvent.capacity - (bookedByEvent[selectedEvent.id] || 0) : 0;
function validate() {
const errs = {};
if (!selectedEventId) errs.event = 'Bitte ein Datum wählen';
if (selectedEvent) {
const remaining = selectedEvent.capacity - (bookedByEvent[selectedEvent.id] || 0);
if (partySize < 1) errs.partySize = 'Mindestens 1 Person';
else if (partySize > remaining) errs.partySize = `Nur noch ${remaining} Plätze frei`;
}
if (!vorname.trim()) errs.vorname = 'Pflichtfeld';
if (!nachname.trim()) errs.nachname = 'Pflichtfeld';
if (!email.trim()) errs.email = 'Pflichtfeld';
else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) errs.email = 'Ungültige E-Mail';
if (!phone.trim()) errs.phone = 'Pflichtfeld';
setErrors(errs);
return Object.keys(errs).length === 0;
}
async function handleSubmit() {
if (!validate()) return;
const reservation = {
id: uid(),
eventId: selectedEventId,
eventDate: selectedEvent.date,
eventTime: selectedEvent.time,
eventTitle: selectedEvent.title,
partySize: Number(partySize),
vorname: vorname.trim(), nachname: nachname.trim(),
email: email.trim(), phone: phone.trim(),
comments: comments.trim(),
createdAt: new Date().toISOString(),
};
const next = [...reservations, reservation];
setReservations(next);
await safeSet(KEY_RESERVATIONS, next);
setLastReservation(reservation);
setView('confirmed');
}
function resetForm() {
setSelectedEventId(''); setPartySize(2);
setVorname(''); setNachname(''); setEmail(''); setPhone(''); setComments('');
setErrors({}); setLastReservation(null); setView('form');
}
async function deleteReservation(id) {
const next = reservations.filter((r) => r.id !== id);
setReservations(next); await safeSet(KEY_RESERVATIONS, next);
}
async function addEvent(evt) {
const next = [...events, { ...evt, id: uid() }];
setEvents(next); await safeSet(KEY_EVENTS, next);
}
async function deleteEvent(id) {
if (!confirm('Anlass wirklich löschen?')) return;
const next = events.filter((e) => e.id !== id);
setEvents(next); await safeSet(KEY_EVENTS, next);
}
if (loading) {
return (
);
}
return (
{view === 'form' && (
{ setSelectedEventId(id); setErrors((e) => ({ ...e, event: undefined })); }}
partySize={partySize} setPartySize={setPartySize}
vorname={vorname} setVorname={setVorname}
nachname={nachname} setNachname={setNachname}
email={email} setEmail={setEmail}
phone={phone} setPhone={setPhone}
comments={comments} setComments={setComments}
errors={errors}
remainingForSelected={remainingForSelected} selectedEvent={selectedEvent}
onSubmit={handleSubmit} onAdmin={() => setView('admin')}
/>
)}
{view === 'confirmed' && lastReservation && }
{view === 'admin' && (
setView('form')} onAddEvent={addEvent}
onDeleteEvent={deleteEvent} onDeleteReservation={deleteReservation}
/>
)}
);
}
// ─── Reservation Form ─────────────────────────────────────────────────────────
function ReservationForm(props) {
const {
events, bookedByEvent, selectedEventId, setSelectedEventId,
partySize, setPartySize, vorname, setVorname, nachname, setNachname,
email, setEmail, phone, setPhone, comments, setComments, errors,
remainingForSelected, selectedEvent, onSubmit, onAdmin,
} = props;
return (
{/* Header */}
Reservation
Tisch reservieren
{/* Datum */}
{events.length === 0 ? (
Zurzeit sind keine Anlässe ausgeschrieben.
) : (
{events.map((evt) => {
const booked = bookedByEvent[evt.id] || 0;
const remaining = evt.capacity - booked;
const soldOut = remaining <= 0;
const selected = evt.id === selectedEventId;
return (
-
);
})}
)}
{errors.event && }
{/* Personen */}
{/* Angaben */}
{/* Submit */}
Mit dem Absenden stimmen Sie der Verarbeitung Ihrer Daten zu.
{/* Footer */}
);
}
// ─── Confirmation ─────────────────────────────────────────────────────────────
function Confirmation({ reservation, onNew }) {
return (
Bestätigt
Vielen Dank, {reservation.vorname}.
Ihre Reservation wurde aufgenommen. Sie erhalten in Kürze eine Bestätigung
an {reservation.email}.
{reservation.comments && }
);
}
function DetailRow({ label, value }) {
return (
{label}
{value}
);
}
// ─── Admin View ───────────────────────────────────────────────────────────────
function AdminView({ events, reservations, bookedByEvent, onBack, onAddEvent, onDeleteEvent, onDeleteReservation }) {
const [showAddEvent, setShowAddEvent] = useState(false);
const t = todayISO();
const sortedEvents = [...events].sort((a, b) => a.date.localeCompare(b.date) || a.time.localeCompare(b.time));
const grouped = useMemo(() => {
const map = new Map();
for (const r of reservations) {
if (!map.has(r.eventDate)) map.set(r.eventDate, []);
map.get(r.eventDate).push(r);
}
return Array.from(map.entries())
.sort((a, b) => a[0].localeCompare(b[0]))
.map(([date, list]) => ({
date,
list: list.sort((a, b) => a.eventTime.localeCompare(b.eventTime) || a.createdAt.localeCompare(b.createdAt)),
}));
}, [reservations]);
return (
{/* Anlässe */}
{showAddEvent && (
{ await onAddEvent(e); setShowAddEvent(false); }} onCancel={() => setShowAddEvent(false)} />
)}
{sortedEvents.length === 0 ? (
- Keine Anlässe.
) : sortedEvents.map((evt) => {
const booked = bookedByEvent[evt.id] || 0;
const remaining = evt.capacity - booked;
const isPast = evt.date < t;
const pct = Math.min(100, (booked / evt.capacity) * 100);
return (
-
{formatDateShort(evt.date)}
{evt.title}
{evt.time}{isPast && ' · vergangen'}
{booked}/{evt.capacity}
{remaining <= 0 ? 'Voll' : `${remaining} frei`}
);
})}
{/* Reservationen */}
{grouped.length === 0 ? (
Noch keine Reservationen.
) : grouped.map((group) => {
const totalGuests = group.list.reduce((s, r) => s + Number(r.partySize || 0), 0);
return (
{formatDate(group.date)}
{group.list.length} Res. · {totalGuests} Gäste
{group.list.map((r) => onDeleteReservation(r.id)} />)}
);
})}
);
}
function ReservationRow({ r, onDelete }) {
const [expanded, setExpanded] = useState(false);
return (
{r.eventTime}
{r.vorname} {r.nachname}
{r.partySize}
{r.comments && (
)}
{expanded && r.comments && (
{r.comments}
)}
);
}
// ─── Add Event Form ───────────────────────────────────────────────────────────
function AddEventForm({ onAdd, onCancel }) {
const [date, setDate] = useState(todayISO());
const [time, setTime] = useState('19:00');
const [title, setTitle] = useState('');
const [capacity, setCapacity] = useState(12);
const [err, setErr] = useState('');
function submit() {
if (!title.trim()) { setErr('Titel fehlt'); return; }
if (!date) { setErr('Datum fehlt'); return; }
if (capacity < 1) { setErr('Kapazität muss > 0 sein'); return; }
onAdd({ date, time, title: title.trim(), capacity: Number(capacity) });
setTitle(''); setCapacity(12); setErr('');
}
return (
);
}
// ─── Shared ───────────────────────────────────────────────────────────────────
function Label({ children, inline }) {
return
{children}
;
}
function Field({ label, children, error }) {
return (
);
}
function ErrorLine({ msg }) {
return (
);
}