Add Stargil Nails logo and favicon

- Replace emoji icons with Stargil Nails logo in header and loading spinner
- Add favicon.png to public directory
- Copy logo to public/assets for browser access
- Update vite.config.ts to serve public directory
- Add favicon link to HTML head section
This commit is contained in:
2025-09-29 19:50:10 +02:00
parent b33036300f
commit ab96114295
10 changed files with 175 additions and 88 deletions

View File

@@ -8,10 +8,13 @@ export function AdminAvailability() {
const [time, setTime] = useState<string>("09:00");
const [duration, setDuration] = useState<number>(30);
const { data: slots } = useQuery(
queryClient.availability.live.byDate.experimental_liveOptions(selectedDate)
const { data: allSlots } = useQuery(
queryClient.availability.live.list.experimental_liveOptions()
);
const [errorMsg, setErrorMsg] = useState<string>("");
const [successMsg, setSuccessMsg] = useState<string>("");
const { mutate: createSlot, isPending: isCreating } = useMutation(
queryClient.availability.create.mutationOptions()
);
@@ -20,8 +23,38 @@ export function AdminAvailability() {
);
const addSlot = () => {
if (!selectedDate || !time || !duration) return;
createSlot({ sessionId: localStorage.getItem("sessionId") || "", date: selectedDate, time, durationMinutes: duration });
setErrorMsg("");
setSuccessMsg("");
if (!selectedDate || !time || !duration) {
setErrorMsg("Bitte Datum, Uhrzeit und Dauer angeben.");
return;
}
const sessionId = localStorage.getItem("sessionId") || "";
if (!sessionId) {
setErrorMsg("Nicht eingeloggt. Bitte als Inhaber anmelden.");
return;
}
createSlot(
{ sessionId, date: selectedDate, time, durationMinutes: duration },
{
onSuccess: () => {
setSuccessMsg("Slot angelegt.");
// advance time to next 30-minute step
const [hStr, mStr] = time.split(":");
let h = parseInt(hStr, 10);
let m = parseInt(mStr, 10);
m += 30;
if (m >= 60) { h += 1; m -= 60; }
if (h >= 24) { h = 0; }
const next = `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}`;
setTime(next);
},
onError: (err: any) => {
const msg = (err && (err.message || (err as any).toString())) || "Fehler beim Anlegen.";
setErrorMsg(msg);
},
}
);
};
return (
@@ -58,32 +91,43 @@ export function AdminAvailability() {
</button>
</div>
{(errorMsg || successMsg) && (
<div className="text-sm">
{errorMsg && <div className="text-red-600">{errorMsg}</div>}
{successMsg && <div className="text-green-700">{successMsg}</div>}
</div>
)}
<div className="space-y-2">
<h3 className="font-medium">Slots am {selectedDate}</h3>
<h3 className="font-medium">Alle freien Slots</h3>
<div className="grid grid-cols-1 gap-2">
{slots?.sort((a, b) => a.time.localeCompare(b.time)).map((slot) => (
<div key={slot.id} className="flex items-center justify-between border rounded px-3 py-2">
<div className="flex items-center gap-3">
<span className="font-mono">{slot.time}</span>
<span className="text-sm text-gray-600">{slot.durationMinutes} Min</span>
<span className={`text-xs px-2 py-1 rounded ${slot.status === "free" ? "bg-green-100 text-green-800" : "bg-yellow-100 text-yellow-800"}`}>
{slot.status === "free" ? "frei" : "reserviert"}
</span>
{allSlots
?.filter((s) => s.status === "free")
.sort((a, b) => (a.date === b.date ? a.time.localeCompare(b.time) : a.date.localeCompare(b.date)))
.map((slot) => (
<div key={slot.id} className="flex items-center justify-between border rounded px-3 py-2">
<div className="flex items-center gap-3">
<span className="text-sm text-gray-600">{slot.date}</span>
<span className="font-mono">{slot.time}</span>
<span className="text-sm text-gray-600">{slot.durationMinutes} Min</span>
<span className={`text-xs px-2 py-1 rounded ${slot.status === "free" ? "bg-green-100 text-green-800" : "bg-yellow-100 text-yellow-800"}`}>
{slot.status === "free" ? "frei" : "reserviert"}
</span>
</div>
<div className="flex items-center gap-2">
<button
onClick={() => removeSlot({ sessionId: localStorage.getItem("sessionId") || "", id: slot.id })}
className="text-red-600 hover:underline"
disabled={slot.status === "reserved"}
title={slot.status === "reserved" ? "Slot ist reserviert" : "Slot löschen"}
>
Löschen
</button>
</div>
</div>
<div className="flex items-center gap-2">
<button
onClick={() => removeSlot({ sessionId: localStorage.getItem("sessionId") || "", id: slot.id })}
className="text-red-600 hover:underline"
disabled={slot.status === "reserved"}
title={slot.status === "reserved" ? "Slot ist reserviert" : "Slot löschen"}
>
Löschen
</button>
</div>
</div>
))}
{slots?.length === 0 && (
<div className="text-sm text-gray-600">Keine Slots vorhanden.</div>
))}
{allSlots?.filter((s) => s.status === "free").length === 0 && (
<div className="text-sm text-gray-600">Keine freien Slots vorhanden.</div>
)}
</div>
</div>

View File

@@ -14,18 +14,23 @@ export function BookingForm() {
const { data: treatments } = useQuery(
queryClient.treatments.live.list.experimental_liveOptions()
);
const { data: slotsByDate } = useQuery(
appointmentDate
? queryClient.availability.live.byDate.experimental_liveOptions(appointmentDate)
: queryClient.availability.live.byDate.experimental_liveOptions("")
// Lade alle Slots live und filtere freie Slots
const { data: allSlots } = useQuery(
queryClient.availability.live.list.experimental_liveOptions()
);
const freeSlots = (allSlots || []).filter((s) => s.status === "free");
const availableDates = Array.from(new Set(freeSlots.map((s) => s.date))).sort();
const slotsByDate = appointmentDate
? freeSlots.filter((s) => s.date === appointmentDate)
: [];
const { mutate: createBooking, isPending } = useMutation(
queryClient.bookings.create.mutationOptions()
);
const selectedTreatmentData = treatments?.find(t => t.id === selectedTreatment);
const availableSlots = (slotsByDate || []).filter(s => s.status === "free");
const selectedTreatmentData = treatments?.find((t) => t.id === selectedTreatment);
const availableSlots = (slotsByDate || []).filter((s) => s.status === "free");
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
@@ -33,33 +38,36 @@ export function BookingForm() {
alert("Bitte fülle alle erforderlichen Felder aus");
return;
}
const slot = availableSlots.find(s => s.id === selectedSlotId);
const slot = availableSlots.find((s) => s.id === selectedSlotId);
const appointmentTime = slot?.time || "";
createBooking({
treatmentId: selectedTreatment,
customerName,
customerEmail,
customerPhone,
appointmentDate,
appointmentTime,
notes,
slotId: selectedSlotId,
}, {
onSuccess: () => {
setSelectedTreatment("");
setCustomerName("");
setCustomerEmail("");
setCustomerPhone("");
setAppointmentDate("");
setSelectedSlotId("");
setNotes("");
alert("Buchung erfolgreich erstellt! Wir werden dich kontaktieren, um deinen Termin zu bestätigen.");
createBooking(
{
treatmentId: selectedTreatment,
customerName,
customerEmail,
customerPhone,
appointmentDate,
appointmentTime,
notes,
slotId: selectedSlotId,
},
{
onSuccess: () => {
setSelectedTreatment("");
setCustomerName("");
setCustomerEmail("");
setCustomerPhone("");
setAppointmentDate("");
setSelectedSlotId("");
setNotes("");
alert("Buchung erfolgreich erstellt! Wir werden dich kontaktieren, um deinen Termin zu bestätigen.");
},
}
});
);
};
// Get minimum date (today)
const today = new Date().toISOString().split('T')[0];
// Get minimum date (today) nicht mehr genutzt, Datumsauswahl erfolgt aus freien Slots
const today = new Date().toISOString().split("T")[0];
return (
<div className="max-w-2xl mx-auto bg-white rounded-lg shadow-lg p-6">
@@ -134,16 +142,22 @@ export function BookingForm() {
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Gewünschtes Datum *
Datum (nur freie Termine) *
</label>
<input
type="date"
<select
value={appointmentDate}
onChange={(e) => setAppointmentDate(e.target.value)}
min={today}
onChange={(e) => { setAppointmentDate(e.target.value); setSelectedSlotId(""); }}
className="w-full p-3 border border-gray-300 rounded-md focus:ring-2 focus:ring-pink-500 focus:border-pink-500"
required
/>
>
<option value="">Datum auswählen</option>
{availableDates.map((d) => (
<option key={d} value={d}>{d}</option>
))}
</select>
{availableDates.length === 0 && (
<p className="mt-2 text-sm text-gray-500">Aktuell keine freien Termine verfügbar.</p>
)}
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">