mirror of
https://github.com/Dvorinka/Bookra.git
synced 2026-06-05 04:52:59 +00:00
first commit
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
import { useParams } from "@solidjs/router";
|
||||
import { For, Show, createResource, createSignal } from "solid-js";
|
||||
import { apiClient } from "../lib/api-client";
|
||||
import { useI18n } from "../providers/i18n-provider";
|
||||
import type { components } from "@bookra/api-client/generated/types";
|
||||
|
||||
export function PublicBookingRoute() {
|
||||
const params = useParams();
|
||||
const i18n = useI18n();
|
||||
const tenantSlug = () => params.tenantSlug ?? "studio-atelier";
|
||||
const [bookingResult, setBookingResult] = createSignal<string | null>(null);
|
||||
const [submittingSlot, setSubmittingSlot] = createSignal<string | null>(null);
|
||||
const [availability, { refetch }] = createResource(() =>
|
||||
apiClient.GET("/v1/public/tenants/{tenantSlug}/availability", {
|
||||
params: {
|
||||
path: {
|
||||
tenantSlug: tenantSlug(),
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const bookSlot = async (slot: components["schemas"]["TimeSlot"]) => {
|
||||
setSubmittingSlot(slot.startsAt);
|
||||
setBookingResult(null);
|
||||
|
||||
const response = await apiClient.POST("/v1/public/bookings", {
|
||||
body: {
|
||||
tenantSlug: tenantSlug(),
|
||||
bookingMode: slot.mode,
|
||||
serviceId: slot.serviceId ?? undefined,
|
||||
classSessionId: slot.classSessionId ?? undefined,
|
||||
staffId: slot.staffId ?? undefined,
|
||||
locationId: slot.locationId ?? undefined,
|
||||
customerName: "Demo Customer",
|
||||
customerEmail: "customer@bookra.dev",
|
||||
startsAt: slot.startsAt,
|
||||
endsAt: slot.endsAt,
|
||||
},
|
||||
});
|
||||
|
||||
const payload = response as { data?: components["schemas"]["CreateBookingResponse"]; error?: unknown };
|
||||
if (payload.error) {
|
||||
setBookingResult(`Booking failed: ${String(payload.error)}`);
|
||||
} else if (payload.data) {
|
||||
setBookingResult(`Created ${payload.data.status} booking ${payload.data.reference}`);
|
||||
}
|
||||
|
||||
setSubmittingSlot(null);
|
||||
void refetch();
|
||||
};
|
||||
|
||||
return (
|
||||
<section class="grid gap-8 py-10 lg:grid-cols-[1.1fr_0.9fr]">
|
||||
<div class="rounded-panel border border-black/10 bg-white/70 p-8 shadow-card">
|
||||
<h1 class="text-3xl font-semibold tracking-tight">{i18n.t("booking.title")}</h1>
|
||||
<p class="mt-3 max-w-2xl text-base leading-7 text-slate">{i18n.t("booking.body")}</p>
|
||||
<Show when={bookingResult()}>
|
||||
<div class="mt-6 rounded-panel border border-pine/20 bg-pine/10 px-4 py-3 text-sm text-pine">
|
||||
{bookingResult()}
|
||||
</div>
|
||||
</Show>
|
||||
<div class="mt-8 grid gap-3">
|
||||
<Show
|
||||
when={availability.latest?.data?.slots?.length}
|
||||
fallback={
|
||||
<div class="rounded-panel border border-dashed border-black/10 bg-canvas/70 p-5 text-sm leading-6 text-slate">
|
||||
{i18n.t("booking.empty")}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<For each={availability.latest?.data?.slots ?? []}>
|
||||
{(slot) => (
|
||||
<article class="rounded-panel border border-black/10 bg-canvas/70 p-5">
|
||||
<div class="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
|
||||
<div>
|
||||
<p class="text-sm uppercase tracking-[0.2em] text-slate">{slot.mode}</p>
|
||||
<h2 class="mt-2 text-xl font-semibold">{slot.label ?? "Bookable slot"}</h2>
|
||||
<p class="mt-2 text-sm leading-6 text-slate">
|
||||
{new Date(slot.startsAt).toLocaleString()} - {new Date(slot.endsAt).toLocaleTimeString()}
|
||||
</p>
|
||||
<Show when={typeof slot.remainingCapacity === "number"}>
|
||||
<p class="mt-2 text-sm text-pine">Remaining capacity: {slot.remainingCapacity}</p>
|
||||
</Show>
|
||||
</div>
|
||||
<button
|
||||
class="rounded-full bg-ink px-5 py-3 text-sm font-medium text-canvas disabled:opacity-50"
|
||||
disabled={submittingSlot() === slot.startsAt}
|
||||
onClick={() => void bookSlot(slot)}
|
||||
type="button"
|
||||
>
|
||||
{submittingSlot() === slot.startsAt ? "Booking..." : "Book demo slot"}
|
||||
</button>
|
||||
</div>
|
||||
</article>
|
||||
)}
|
||||
</For>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rounded-panel border border-black/10 bg-pine p-8 text-canvas shadow-card">
|
||||
<p class="text-sm uppercase tracking-[0.2em] text-canvas/70">Tenant slug</p>
|
||||
<p class="mt-4 text-2xl font-semibold">{tenantSlug()}</p>
|
||||
<p class="mt-4 text-sm leading-7 text-canvas/80">
|
||||
This route is ready to consume live availability from the Go API once Neon-backed tenant data is configured.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user