This commit is contained in:
Tomas Dvorak committed 2026-05-05 09:48:07 +02:00
1 parent d854614a87
commit 48c3e15a38
295 files changed
+178378 -1036

No files matched your search

+115
View File
@@ -0,0 +1,115 @@
import { A } from "@solidjs/router";
import { useI18n } from "../providers/i18n-provider";
import { BookraCharacter } from "../components/bookra-character";
import { Card, CardContent, CardHeader, CardTitle } from "../components/ui";
export function AboutRoute() {
const i18n = useI18n();
const values = [
{
pose: "educate" as const,
title: i18n.t("about.values.simple.title"),
description: i18n.t("about.values.simple.desc"),
},
{
pose: "happy_note" as const,
title: i18n.t("about.values.reliable.title"),
description: i18n.t("about.values.reliable.desc"),
},
{
pose: "flag" as const,
title: i18n.t("about.values.local.title"),
description: i18n.t("about.values.local.desc"),
},
];
return (
<div class="animate-fade-in">
{/* Hero Section */}
<section class="relative pt-16 pb-12 lg:pt-24 lg:pb-16 overflow-hidden">
<div class="section-container">
<div class="max-w-3xl mx-auto text-center">
<div class="flex justify-center mb-8">
<BookraCharacter pose="hello" size="lg" animate={true} />
</div>
<h1 class="text-display-xl font-semibold text-ink mb-6 tracking-tight">
{i18n.t("about.title")}
</h1>
<p class="text-lg lg:text-xl text-ink-muted leading-relaxed">
{i18n.t("about.subtitle")}
</p>
</div>
</div>
</section>
{/* Story Section */}
<section class="py-16 lg:py-24 bg-canvas-subtle/30">
<div class="section-container">
<div class="max-w-3xl mx-auto">
<h2 class="text-display-md font-semibold text-ink mb-8 text-center">
{i18n.t("about.story.title")}
</h2>
<div class="space-y-6 text-ink-muted leading-relaxed text-lg">
<p>{i18n.t("about.story.p1")}</p>
<p>{i18n.t("about.story.p2")}</p>
<p>{i18n.t("about.story.p3")}</p>
</div>
</div>
</div>
</section>
{/* Values Section */}
<section class="py-16 lg:py-24">
<div class="section-container">
<div class="max-w-2xl mx-auto text-center mb-12">
<h2 class="text-display-md font-semibold text-ink mb-4">
{i18n.t("about.values.title")}
</h2>
<p class="text-lg text-ink-muted">
{i18n.t("about.values.subtitle")}
</p>
</div>
<div class="grid md:grid-cols-3 gap-6 lg:gap-8 max-w-4xl mx-auto">
{values.map((value) => (
<Card class="surface-elevated">
<CardHeader>
<div class="mb-4 flex justify-center">
<BookraCharacter pose={value.pose} size="sm" animate={true} />
</div>
<CardTitle>{value.title}</CardTitle>
</CardHeader>
<CardContent>
<p class="text-ink-muted">{value.description}</p>
</CardContent>
</Card>
))}
</div>
</div>
</section>
{/* Team/Mascot Section */}
<section class="py-16 lg:py-24 bg-canvas-subtle/30">
<div class="section-container">
<div class="max-w-3xl mx-auto text-center">
<div class="flex justify-center mb-8">
<BookraCharacter pose="laptop" size="xl" animate={true} />
</div>
<h2 class="text-display-md font-semibold text-ink mb-4">
{i18n.t("about.team.title")}
</h2>
<p class="text-lg text-ink-muted mb-8">
{i18n.t("about.team.subtitle")}
</p>
<A
href="/contact"
class="btn-primary inline-flex items-center gap-2"
>
{i18n.t("about.cta")}
</A>
</div>
</div>
</section>
</div>
);
}
@@ -0,0 +1,27 @@
import { onMount } from "solid-js";
import { useNavigate } from "@solidjs/router";
import { BookraCharacter } from "../components/bookra-character";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../components/ui";
export function AuthCallbackRoute() {
const navigate = useNavigate();
onMount(() => {
window.setTimeout(() => navigate("/dashboard", { replace: true }), 250);
});
return (
<section class="section-container py-16">
<Card class="surface-elevated mx-auto max-w-xl text-center">
<CardHeader>
<div class="mb-4 flex justify-center">
<BookraCharacter pose="hello" size="lg" animate />
</div>
<CardTitle>Finishing sign-in</CardTitle>
<CardDescription>Redirecting you to your dashboard.</CardDescription>
</CardHeader>
<CardContent />
</Card>
</section>
);
}
@@ -0,0 +1,451 @@
import { A, useParams, useSearchParams } from "@solidjs/router";
import { createSignal, createResource, Show, Match, Switch } from "solid-js";
import { apiClient } from "../lib/api-client";
import { useI18n } from "../providers/i18n-provider";
import { BookraCharacter } from "../components/bookra-character";
import { Button, Card, CardHeader, CardTitle, CardDescription, CardContent, Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, Input, Textarea } from "../components/ui";
export function BookingManageRoute() {
const params = useParams();
const [searchParams] = useSearchParams();
const i18n = useI18n();
const [isRescheduling, setIsRescheduling] = createSignal(false);
const [isCancelling, setIsCancelling] = createSignal(false);
const [newDate, setNewDate] = createSignal("");
const [newTime, setNewTime] = createSignal("");
const [message, setMessage] = createSignal("");
const [actionSuccess, setActionSuccess] = createSignal<string | null>(null);
const [actionError, setActionError] = createSignal<string | null>(null);
const token = () => searchParams.token || "";
const reference = () => params.reference || "";
const [booking] = createResource(token, async (t) => {
if (!t) return null;
const ref = reference();
if (!ref) return null;
// Fetch from real API
const response = await (apiClient as any).GET(`/v1/public/bookings/${ref}`, {
params: { query: { token: t } }
});
if (response.error) {
return null;
}
return response.data ?? {
reference: ref || "BK-001",
customerName: "Alice Johnson",
customerEmail: "[email protected]",
service: "Yoga Flow Class",
businessName: "Serenity Wellness Studio",
startsAt: new Date(Date.now() + 86400000).toISOString(),
endsAt: new Date(Date.now() + 86400000 + 3600000).toISOString(),
location: "Main Studio, 123 Wellness Street",
status: "confirmed",
notes: "",
};
});
const handleReschedule = async () => {
setActionSuccess(null);
setActionError(null);
if (!newDate() || !newTime()) {
setActionError(i18n.locale() === 'cs' ? 'Vyberte prosím nové datum a čas' : 'Please select a new date and time');
return;
}
const ref = reference();
const t = token();
if (!ref || !t) {
setActionError(i18n.locale() === 'cs' ? 'Chybí referenční číslo nebo token' : 'Missing reference or token');
return;
}
// Calculate new start and end times
const newStart = new Date(`${newDate()}T${newTime()}`);
const newEnd = new Date(newStart.getTime() + 60 * 60 * 1000); // Default 1 hour duration
try {
const response = await (apiClient as any).POST(`/v1/public/bookings/${ref}/reschedule`, {
params: { query: { token: t } },
body: {
newStartsAt: newStart.toISOString(),
newEndsAt: newEnd.toISOString(),
reason: message()
}
});
if (response.error) {
setActionError(i18n.locale() === 'cs' ? 'Změna termínu selhala' : 'Reschedule failed');
return;
}
setActionSuccess(i18n.locale() === 'cs' ? 'Rezervace byla přeplánována' : 'Booking rescheduled successfully');
setIsRescheduling(false);
setNewDate("");
setNewTime("");
setMessage("");
} catch (err) {
setActionError(i18n.locale() === 'cs' ? 'Změna termínu selhala' : 'Reschedule failed');
}
};
const handleCancel = async () => {
setActionSuccess(null);
setActionError(null);
const ref = reference();
const t = token();
if (!ref || !t) {
setActionError(i18n.locale() === 'cs' ? 'Chybí referenční číslo nebo token' : 'Missing reference or token');
return;
}
try {
const response = await (apiClient as any).POST(`/v1/public/bookings/${ref}/cancel`, {
params: { query: { token: t } },
body: { reason: message() }
});
if (response.error) {
setActionError(i18n.locale() === 'cs' ? 'Zrušení rezervace selhalo' : 'Cancellation failed');
return;
}
setActionSuccess(i18n.locale() === 'cs' ? 'Rezervace byla zrušena' : 'Booking cancelled successfully');
setIsCancelling(false);
setMessage("");
} catch (err) {
setActionError(i18n.locale() === 'cs' ? 'Zrušení rezervace selhalo' : 'Cancellation failed');
}
};
const formatDate = (dateString: string) => {
const date = new Date(dateString);
return new Intl.DateTimeFormat(i18n.locale() === 'cs' ? 'cs-CZ' : 'en-US', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric',
}).format(date);
};
const formatTime = (dateString: string) => {
const date = new Date(dateString);
return new Intl.DateTimeFormat(i18n.locale() === 'cs' ? 'cs-CZ' : 'en-US', {
hour: '2-digit',
minute: '2-digit',
}).format(date);
};
return (
<section class="section-container py-12 min-h-screen">
{/* Header */}
<div class="max-w-2xl mx-auto text-center mb-10">
<BookraCharacter pose="hello" size="lg" animate={true} class="mx-auto mb-6" />
<h1 class="text-display-lg font-semibold text-ink mb-4">
{i18n.locale() === 'cs' ? 'Správa rezervace' : 'Manage Your Booking'}
</h1>
<p class="text-lg text-ink-muted">
{i18n.locale() === 'cs'
? 'Zde můžete zobrazit, změnit nebo zrušit svou rezervaci'
: 'View, modify or cancel your booking here'}
</p>
</div>
{/* Success/Error Messages */}
<Show when={actionSuccess()}>
<div class="max-w-2xl mx-auto mb-6 p-4 rounded-xl bg-[hsl(var(--success-soft))] border border-[hsl(var(--success))/20] text-[hsl(var(--success))] animate-fade-in">
<div class="flex items-center gap-3">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/>
<polyline points="22 4 12 14.01 9 11.01"/>
</svg>
<span class="font-medium">{actionSuccess()}</span>
</div>
</div>
</Show>
<Show when={actionError()}>
<div class="max-w-2xl mx-auto mb-6 p-4 rounded-xl bg-[hsl(var(--error-soft))] border border-[hsl(var(--error))/20] text-[hsl(var(--error))] animate-fade-in">
<div class="flex items-center gap-3">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"/>
<line x1="12" x2="12" y1="8" y2="12"/>
<line x1="12" x2="12.01" y1="16" y2="16"/>
</svg>
<span class="font-medium">{actionError()}</span>
</div>
</div>
</Show>
{/* Booking Details Card */}
<Show when={booking()}>
{(b) => (
<div class="max-w-2xl mx-auto space-y-6">
<Card class="surface-elevated">
<CardHeader>
<div class="flex items-center justify-between">
<div>
<CardTitle class="font-display text-2xl">{b().service}</CardTitle>
<CardDescription class="mt-1">{b().businessName}</CardDescription>
</div>
<span class={`inline-flex items-center px-3 py-1 rounded-full text-sm font-semibold ${
b().status === 'confirmed'
? 'bg-[hsl(var(--success-soft))] text-[hsl(var(--success))]'
: 'bg-[hsl(var(--warning-soft))] text-[hsl(var(--warning))]'
}`}>
{b().status === 'confirmed'
? (i18n.locale() === 'cs' ? 'Potvrzeno' : 'Confirmed')
: (i18n.locale() === 'cs' ? 'Čeká se' : 'Pending')
}
</span>
</div>
</CardHeader>
<CardContent class="space-y-6">
{/* Reference */}
<div class="flex items-center gap-3 p-3 bg-canvas-subtle rounded-xl">
<div class="w-10 h-10 rounded-lg bg-[hsl(var(--info-subtle))] flex items-center justify-center text-[hsl(var(--info))]">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M4 7V4h16v3"/>
<path d="M9 20h6"/>
<path d="M12 4v16"/>
</svg>
</div>
<div>
<p class="text-sm text-ink-muted">{i18n.locale() === 'cs' ? 'Referenční číslo' : 'Reference'}</p>
<p class="font-mono font-medium text-ink">{b().reference}</p>
</div>
</div>
{/* Date & Time */}
<div class="grid grid-cols-2 gap-4">
<div class="flex items-center gap-3 p-4 bg-canvas-subtle rounded-xl">
<div class="w-10 h-10 rounded-lg bg-accent-subtle flex items-center justify-center text-accent">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="4" width="18" height="18" rx="2" ry="2"/>
<line x1="16" y1="2" x2="16" y2="6"/>
<line x1="8" y1="2" x2="8" y2="6"/>
<line x1="3" y1="10" x2="21" y2="10"/>
</svg>
</div>
<div>
<p class="text-sm text-ink-muted">{i18n.locale() === 'cs' ? 'Datum' : 'Date'}</p>
<p class="font-medium text-ink">{formatDate(b().startsAt)}</p>
</div>
</div>
<div class="flex items-center gap-3 p-4 bg-canvas-subtle rounded-xl">
<div class="w-10 h-10 rounded-lg bg-accent-subtle flex items-center justify-center text-accent">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"/>
<polyline points="12 6 12 12 16 14"/>
</svg>
</div>
<div>
<p class="text-sm text-ink-muted">{i18n.locale() === 'cs' ? 'Čas' : 'Time'}</p>
<p class="font-medium text-ink">{formatTime(b().startsAt)} - {formatTime(b().endsAt)}</p>
</div>
</div>
</div>
{/* Location */}
<div class="flex items-center gap-3 p-4 bg-canvas-subtle rounded-xl">
<div class="w-10 h-10 rounded-lg bg-[hsl(var(--info-subtle))] flex items-center justify-center text-[hsl(var(--info))]">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"/>
<circle cx="12" cy="10" r="3"/>
</svg>
</div>
<div>
<p class="text-sm text-ink-muted">{i18n.locale() === 'cs' ? 'Místo' : 'Location'}</p>
<p class="font-medium text-ink">{b().location}</p>
</div>
</div>
{/* Customer Info */}
<div class="p-4 bg-canvas-subtle rounded-xl">
<p class="text-sm text-ink-muted mb-3">{i18n.locale() === 'cs' ? 'Vaše údaje' : 'Your Details'}</p>
<div class="space-y-2">
<p class="font-medium text-ink">{b().customerName}</p>
<p class="text-sm text-ink-muted">{b().customerEmail}</p>
</div>
<Show when={b().notes}>
<div class="mt-3 pt-3 border-t border-border">
<p class="text-sm text-ink-muted">{i18n.locale() === 'cs' ? 'Poznámky' : 'Notes'}</p>
<p class="text-sm text-ink mt-1">{b().notes}</p>
</div>
</Show>
</div>
{/* Action Buttons */}
<div class="flex flex-col sm:flex-row gap-3 pt-4">
<Button
variant="secondary"
class="flex-1"
onClick={() => setIsRescheduling(true)}
>
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="mr-2">
<rect x="3" y="4" width="18" height="18" rx="2" ry="2"/>
<line x1="16" y1="2" x2="16" y2="6"/>
<line x1="8" y1="2" x2="8" y2="6"/>
<line x1="3" y1="10" x2="21" y2="10"/>
</svg>
{i18n.locale() === 'cs' ? 'Změnit termín' : 'Reschedule'}
</Button>
<Button
variant="secondary"
class="flex-1 text-[hsl(var(--error))] hover:bg-[hsl(var(--error-soft))]"
onClick={() => setIsCancelling(true)}
>
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="mr-2">
<circle cx="12" cy="12" r="10"/>
<line x1="15" y1="9" x2="9" y2="15"/>
<line x1="9" y1="9" x2="15" y2="15"/>
</svg>
{i18n.locale() === 'cs' ? 'Zrušit rezervaci' : 'Cancel Booking'}
</Button>
</div>
</CardContent>
</Card>
{/* Contact Business Card */}
<Card class="surface-card">
<CardContent class="p-6">
<div class="flex items-start gap-4">
<div class="w-12 h-12 rounded-full bg-accent-subtle flex items-center justify-center text-accent shrink-0">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>
</svg>
</div>
<div>
<h3 class="font-display font-semibold text-ink mb-1">
{i18n.locale() === 'cs' ? 'Potřebujete pomoci?' : 'Need help?'}
</h3>
<p class="text-sm text-ink-muted mb-3">
{i18n.locale() === 'cs'
? 'Máte dotazy nebo potřebujete speciální úpravy? Kontaktujte přímo podnik.'
: 'Have questions or need special arrangements? Contact the business directly.'}
</p>
<a
href={`mailto:[email protected]?subject=Booking ${b().reference}`}
class="text-accent hover:text-accent-hover font-medium text-sm inline-flex items-center gap-2"
>
{i18n.locale() === 'cs' ? 'Poslat zprávu' : 'Send message'}
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M5 12h14"/>
<path d="m12 5 7 7-7 7"/>
</svg>
</a>
</div>
</div>
</CardContent>
</Card>
</div>
)}
</Show>
{/* Reschedule Dialog */}
<Dialog open={isRescheduling()} onClose={() => setIsRescheduling(false)}>
<DialogContent>
<DialogHeader>
<DialogTitle>
{i18n.locale() === 'cs' ? 'Změnit termín rezervace' : 'Reschedule Booking'}
</DialogTitle>
<DialogDescription>
{i18n.locale() === 'cs'
? 'Vyberte nové datum a čas pro vaši rezervaci'
: 'Select a new date and time for your booking'}
</DialogDescription>
</DialogHeader>
<div class="space-y-4 mt-4">
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-ink-muted mb-1.5">
{i18n.locale() === 'cs' ? 'Nové datum' : 'New Date'}
</label>
<Input
type="date"
value={newDate()}
onInput={(e) => setNewDate(e.currentTarget.value)}
/>
</div>
<div>
<label class="block text-sm font-medium text-ink-muted mb-1.5">
{i18n.locale() === 'cs' ? 'Nový čas' : 'New Time'}
</label>
<Input
type="time"
value={newTime()}
onInput={(e) => setNewTime(e.currentTarget.value)}
/>
</div>
</div>
<div>
<label class="block text-sm font-medium text-ink-muted mb-1.5">
{i18n.locale() === 'cs' ? 'Zpráva pro podnik (volitelné)' : 'Message to business (optional)'}
</label>
<Textarea
rows={3}
value={message()}
onInput={(e) => setMessage(e.currentTarget.value)}
placeholder={i18n.locale() === 'cs' ? 'Důvod změny termínu...' : 'Reason for rescheduling...'}
/>
</div>
<div class="flex gap-3 pt-4">
<Button variant="secondary" class="flex-1" onClick={() => setIsRescheduling(false)}>
{i18n.locale() === 'cs' ? 'Zrušit' : 'Cancel'}
</Button>
<Button class="flex-1" onClick={handleReschedule}>
{i18n.locale() === 'cs' ? 'Potvrdit změnu' : 'Confirm Change'}
</Button>
</div>
</div>
</DialogContent>
</Dialog>
{/* Cancel Dialog */}
<Dialog open={isCancelling()} onClose={() => setIsCancelling(false)}>
<DialogContent>
<DialogHeader>
<DialogTitle>
{i18n.locale() === 'cs' ? 'Zrušit rezervaci' : 'Cancel Booking'}
</DialogTitle>
<DialogDescription>
{i18n.locale() === 'cs'
? 'Opravdu chcete zrušit tuto rezervaci? Tuto akci nelze vrátit zpět.'
: 'Are you sure you want to cancel this booking? This action cannot be undone.'}
</DialogDescription>
</DialogHeader>
<div class="space-y-4 mt-4">
<div>
<label class="block text-sm font-medium text-ink-muted mb-1.5">
{i18n.locale() === 'cs' ? 'Důvod zrušení (volitelné)' : 'Cancellation reason (optional)'}
</label>
<Textarea
rows={3}
value={message()}
onInput={(e) => setMessage(e.currentTarget.value)}
placeholder={i18n.locale() === 'cs' ? 'Proč rušíte rezervaci...' : 'Why are you cancelling...'}
/>
</div>
<div class="flex gap-3 pt-4">
<Button variant="secondary" class="flex-1" onClick={() => setIsCancelling(false)}>
{i18n.locale() === 'cs' ? 'Nemazat' : 'Keep Booking'}
</Button>
<Button
class="flex-1 bg-[hsl(var(--error))] hover:bg-[hsl(var(--error-hover))]"
onClick={handleCancel}
>
{i18n.locale() === 'cs' ? 'Zrušit rezervaci' : 'Cancel Booking'}
</Button>
</div>
</div>
</DialogContent>
</Dialog>
</section>
);
}
+216
View File
@@ -0,0 +1,216 @@
import { Show, createSignal } from "solid-js";
import { useI18n } from "../providers/i18n-provider";
import { BookraCharacter } from "../components/bookra-character";
import { Button, Card, CardContent, CardHeader, CardTitle, Input, Textarea } from "../components/ui";
export function ContactRoute() {
const i18n = useI18n();
const [name, setName] = createSignal("");
const [email, setEmail] = createSignal("");
const [message, setMessage] = createSignal("");
const [submitted, setSubmitted] = createSignal(false);
const [submitting, setSubmitting] = createSignal(false);
const handleSubmit = async (e: Event) => {
e.preventDefault();
setSubmitting(true);
// Simulate API call
await new Promise((resolve) => setTimeout(resolve, 1000));
setSubmitting(false);
setSubmitted(true);
};
return (
<div class="animate-fade-in">
{/* Hero Section */}
<section class="relative pt-16 pb-12 lg:pt-24 lg:pb-16 overflow-hidden">
{/* Background gradient */}
<div
class="absolute inset-0 pointer-events-none"
style={{ background: "var(--gradient-hero)" }}
/>
<div class="section-container relative">
<div class="max-w-3xl mx-auto text-center">
{/* Character at top */}
<div class="flex justify-center mb-8">
<BookraCharacter pose="headphones" size="xl" animate={true} />
</div>
<span class="inline-flex items-center gap-2 px-4 py-1.5 mb-6 text-sm font-medium tracking-wide text-accent bg-accent-subtle/80 rounded-full border border-accent/10 backdrop-blur-sm">
<span class="w-2 h-2 rounded-full bg-accent animate-pulse" />
{i18n.locale() === 'cs' ? 'Jsme tu pro vás' : 'We are here for you'}
</span>
<h1 class="text-display-xl font-semibold text-ink mb-6 tracking-tight animate-slide-up">
{i18n.t("contact.title")}
</h1>
<p class="text-lg lg:text-xl text-ink-muted leading-relaxed max-w-2xl mx-auto animate-slide-up" style={{ "animation-delay": "0.1s" }}>
{i18n.t("contact.subtitle")}
</p>
</div>
</div>
</section>
{/* Contact Form Section */}
<section class="py-16 lg:py-24 bg-canvas-subtle/30">
<div class="section-container">
<div class="max-w-2xl mx-auto">
<Show when={!submitted()} fallback={
<Card class="surface-elevated border-success/20">
<CardContent class="py-12">
<div class="flex flex-col items-center text-center">
<div class="relative mb-6">
<BookraCharacter pose="success" size="xl" animate={true} />
<div class="absolute -top-2 -right-2 text-3xl animate-bounce">🎉</div>
</div>
<h2 class="text-display-md font-semibold text-ink mb-4">
{i18n.t("contact.success.title")}
</h2>
<p class="text-ink-muted max-w-sm">
{i18n.t("contact.success.body")}
</p>
</div>
</CardContent>
</Card>
}>
<Card class="surface-elevated overflow-hidden">
<div class="flex items-center gap-3 p-6 border-b border-border/50 bg-canvas-subtle/30">
<div class="w-10 h-10 rounded-xl bg-accent/10 flex items-center justify-center">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="text-accent">
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>
</svg>
</div>
<CardTitle class="text-xl">{i18n.t("contact.form.title")}</CardTitle>
</div>
<CardContent class="p-6">
<form onSubmit={handleSubmit} class="space-y-6">
<Input
label={i18n.t("contact.form.name")}
type="text"
value={name()}
onInput={(e) => setName(e.currentTarget.value)}
required
autocomplete="name"
/>
<Input
label={i18n.t("contact.form.email")}
type="email"
value={email()}
onInput={(e) => setEmail(e.currentTarget.value)}
required
autocomplete="email"
/>
<Textarea
label={i18n.t("contact.form.message")}
value={message()}
onInput={(e) => setMessage(e.currentTarget.value)}
rows={5}
required
placeholder={i18n.locale() === 'cs' ? "Napište nám, jak vám můžeme pomoci..." : "Tell us how we can help you..."}
/>
<Button
type="submit"
fullWidth
isLoading={submitting()}
class="shadow-lg hover:shadow-xl transition-all"
>
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="mr-2">
<line x1="22" y1="2" x2="11" y2="13"/>
<polygon points="22 2 15 22 11 13 2 9 22 2"/>
</svg>
{i18n.t("contact.form.submit")}
</Button>
</form>
</CardContent>
</Card>
</Show>
</div>
</div>
</section>
{/* Contact Info Section */}
<section class="py-16 lg:py-24">
<div class="section-container">
<div class="max-w-4xl mx-auto">
{/* Section title */}
<div class="text-center mb-12">
<h2 class="text-display-md font-semibold text-ink mb-3">
{i18n.locale() === 'cs' ? 'Další způsoby kontaktu' : 'Other ways to reach us'}
</h2>
<p class="text-ink-muted">
{i18n.locale() === 'cs' ? 'Vyberte si, co vám nejvíce vyhovuje' : 'Choose what works best for you'}
</p>
</div>
<div class="grid md:grid-cols-2 gap-8">
{/* Email Card */}
<Card class="surface-elevated group hover:shadow-xl transition-all duration-500">
<CardContent class="p-6">
<div class="flex items-start gap-4">
<div class="w-12 h-12 rounded-xl bg-accent/10 flex items-center justify-center shrink-0 group-hover:bg-accent/20 transition-colors">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="text-accent">
<path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/>
<polyline points="22,6 12,13 2,6"/>
</svg>
</div>
<div>
<h3 class="font-display font-semibold text-ink mb-1">{i18n.t("contact.info.email.title")}</h3>
<p class="text-ink-muted text-sm mb-3">{i18n.t("contact.info.email.desc")}</p>
<a
href="mailto:[email protected]"
class="inline-flex items-center gap-2 text-accent hover:text-accent/80 font-medium transition-colors group/link"
>
hello@bookra.cz
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="transition-transform group-hover/link:translate-x-1">
<line x1="5" y1="12" x2="19" y2="12"/>
<polyline points="12 5 19 12 12 19"/>
</svg>
</a>
</div>
</div>
</CardContent>
</Card>
{/* Hours Card */}
<Card class="surface-elevated group hover:shadow-xl transition-all duration-500">
<CardContent class="p-6">
<div class="flex items-start gap-4">
<div class="w-12 h-12 rounded-xl bg-accent/10 flex items-center justify-center shrink-0 group-hover:bg-accent/20 transition-colors">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="text-accent">
<circle cx="12" cy="12" r="10"/>
<polyline points="12 6 12 12 16 14"/>
</svg>
</div>
<div>
<h3 class="font-display font-semibold text-ink mb-1">{i18n.t("contact.info.hours.title")}</h3>
<p class="text-ink-muted text-sm">{i18n.t("contact.info.hours.desc")}</p>
<div class="mt-3 flex items-center gap-2">
<span class="w-2 h-2 rounded-full bg-success animate-pulse"/>
<span class="text-xs text-success font-medium">
{i18n.locale() === 'cs' ? 'Aktuálně online' : 'Currently online'}
</span>
</div>
</div>
</div>
</CardContent>
</Card>
</div>
{/* Helpful mascot at bottom */}
<div class="mt-16 flex justify-center">
<div class="flex items-center gap-4 surface-elevated px-6 py-4 rounded-2xl">
<BookraCharacter pose="main" size="sm" animate={true} />
<p class="text-ink-muted text-sm">
{i18n.locale() === 'cs'
? 'Odpovídáme obvykle do 24 hodin'
: 'We usually respond within 24 hours'}
</p>
</div>
</div>
</div>
</div>
</section>
</div>
);
}
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+672 -27
View File
@@ -1,40 +1,685 @@
import { A } from "@solidjs/router";
import { createSignal, onMount, JSX, createMemo } from "solid-js";
import { useI18n } from "../providers/i18n-provider";
import { BookraCharacter } from "../components/bookra-character";
// Lucide-like icon components (lightweight SVG)
const CalendarIcon = () => (
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="4" width="18" height="18" rx="2" ry="2" />
<line x1="16" y1="2" x2="16" y2="6" />
<line x1="8" y1="2" x2="8" y2="6" />
<line x1="3" y1="10" x2="21" y2="10" />
</svg>
);
const ClockIcon = () => (
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10" />
<polyline points="12 6 12 12 16 14" />
</svg>
);
const UsersIcon = () => (
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" />
<circle cx="9" cy="7" r="4" />
<path d="M23 21v-2a4 4 0 0 0-3-3.87" />
<path d="M16 3.13a4 4 0 0 1 0 7.75" />
</svg>
);
const BellIcon = () => (
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9" />
<path d="M13.73 21a2 2 0 0 1-3.46 0" />
</svg>
);
const ShieldIcon = () => (
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" />
</svg>
);
const ZapIcon = () => (
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2" />
</svg>
);
const ArrowRightIcon = () => (
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<line x1="5" y1="12" x2="19" y2="12" />
<polyline points="12 5 19 12 12 19" />
</svg>
);
const CheckIcon = () => (
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="20 6 9 17 4 12" />
</svg>
);
const SunIcon = () => (
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="5" />
<line x1="12" y1="1" x2="12" y2="3" />
<line x1="12" y1="21" x2="12" y2="23" />
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64" />
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78" />
<line x1="1" y1="12" x2="3" y2="12" />
<line x1="21" y1="12" x2="23" y2="12" />
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36" />
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22" />
</svg>
);
const MoonIcon = () => (
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" />
</svg>
);
// Step card component
interface StepCardProps {
number: number;
title: string;
description: string;
}
const StepCard = (props: StepCardProps) => (
<div class="relative flex gap-5 lg:gap-6">
<div class="flex flex-col items-center">
<div class="w-10 h-10 rounded-full bg-ink text-canvas flex items-center justify-center text-sm font-semibold shrink-0 font-display">
{props.number}
</div>
<div class="w-px flex-1 bg-border my-3" />
</div>
<div class="pb-10">
<h3 class="font-display text-lg font-semibold text-ink mb-2">{props.title}</h3>
<p class="text-ink-muted leading-relaxed">{props.description}</p>
</div>
</div>
);
// Main home route component
export function HomeRoute() {
const i18n = useI18n();
const [isVisible, setIsVisible] = createSignal(false);
const [currentDate, setCurrentDate] = createSignal(new Date());
onMount(() => {
setIsVisible(true);
});
// Calendar helpers
const calendarMonth = createMemo(() => currentDate().getMonth());
const calendarYear = createMemo(() => currentDate().getFullYear());
const prevMonth = () => {
const newDate = new Date(currentDate());
newDate.setMonth(newDate.getMonth() - 1);
setCurrentDate(newDate);
};
const nextMonth = () => {
const newDate = new Date(currentDate());
newDate.setMonth(newDate.getMonth() + 1);
setCurrentDate(newDate);
};
// Generate calendar days for current month
const calendarDays = createMemo(() => {
const year = calendarYear();
const month = calendarMonth();
const firstDay = new Date(year, month, 1);
const lastDay = new Date(year, month + 1, 0);
const daysInMonth = lastDay.getDate();
const startingDay = firstDay.getDay(); // 0 = Sunday, 1 = Monday, etc.
// Adjust for Monday start (0 = Monday)
const adjustedStart = startingDay === 0 ? 6 : startingDay - 1;
const days = [];
// Empty slots for days before the first day of month
for (let i = 0; i < adjustedStart; i++) {
days.push({ day: 0, isCurrentMonth: false });
}
// Days of the month
const today = new Date();
for (let i = 1; i <= daysInMonth; i++) {
const isToday = today.getDate() === i &&
today.getMonth() === month &&
today.getFullYear() === year;
// Generate some mock bookings (deterministic based on day)
const hasBooking = [3, 7, 12, 15, 18, 22, 26, 29].includes(i);
const hasMultiple = [7, 15, 22].includes(i);
days.push({
day: i,
isCurrentMonth: true,
isToday,
hasBooking,
hasMultiple
});
}
return days;
});
const features = [
{
icon: CalendarIcon,
title: i18n.t("home.feature.scheduling.title"),
description: i18n.t("home.feature.scheduling.desc"),
},
{
icon: UsersIcon,
title: i18n.t("home.feature.customers.title"),
description: i18n.t("home.feature.customers.desc"),
},
{
icon: BellIcon,
title: i18n.t("home.feature.reminders.title"),
description: i18n.t("home.feature.reminders.desc"),
},
{
icon: ClockIcon,
title: i18n.t("home.feature.availability.title"),
description: i18n.t("home.feature.availability.desc"),
},
{
icon: ZapIcon,
title: i18n.t("home.feature.quick.title"),
description: i18n.t("home.feature.quick.desc"),
},
{
icon: ShieldIcon,
title: i18n.t("home.feature.secure.title"),
description: i18n.t("home.feature.secure.desc"),
},
];
const steps = [
{
title: i18n.t("home.step1.title"),
description: i18n.t("home.step1.desc"),
},
{
title: i18n.t("home.step2.title"),
description: i18n.t("home.step2.desc"),
},
{
title: i18n.t("home.step3.title"),
description: i18n.t("home.step3.desc"),
},
];
const testimonials = [
{
quote: i18n.t("home.testimonial1.quote"),
author: i18n.t("home.testimonial1.author"),
role: i18n.t("home.testimonial1.role"),
},
{
quote: i18n.t("home.testimonial2.quote"),
author: i18n.t("home.testimonial2.author"),
role: i18n.t("home.testimonial2.role"),
},
{
quote: i18n.t("home.testimonial3.quote"),
author: i18n.t("home.testimonial3.author"),
role: i18n.t("home.testimonial3.role"),
},
];
return (
<section class="grid gap-8 py-10 lg:grid-cols-[1.4fr_0.8fr]">
<div class="rounded-panel border border-black/10 bg-white/70 p-8 shadow-card backdrop-blur">
<p class="mb-4 text-xs uppercase tracking-[0.24em] text-slate">{i18n.t("home.eyebrow")}</p>
<h1 class="max-w-3xl text-4xl font-semibold tracking-tight text-ink md:text-6xl">
{i18n.t("home.title")}
</h1>
<p class="mt-6 max-w-2xl text-lg leading-8 text-slate">{i18n.t("home.body")}</p>
<div class="mt-8 flex flex-wrap gap-3">
<A class="rounded-full bg-ink px-5 py-3 text-sm font-medium text-canvas" href="/dashboard">
{i18n.t("home.primary")}
</A>
<A class="rounded-full border border-black/10 px-5 py-3 text-sm font-medium" href="/book/studio-atelier">
{i18n.t("home.secondary")}
</A>
<div class="animate-fade-in">
{/* Hero Section */}
<section class="relative pt-16 pb-20 lg:pt-24 lg:pb-32 overflow-hidden">
<div
class="absolute inset-0 pointer-events-none animate-pulse-soft"
style={{ background: "var(--gradient-hero)" }}
/>
<div class="section-container relative">
<div class="max-w-3xl mx-auto text-center">
<span class="inline-flex items-center gap-2 px-4 py-1.5 mb-8 text-sm font-medium tracking-wide text-accent bg-accent-subtle/80 rounded-full border border-accent/10 backdrop-blur-sm">
<span class="w-2 h-2 rounded-full bg-accent animate-pulse" />
{i18n.t("home.badge")}
</span>
<h1 class="text-display-xl font-semibold text-ink mb-6 tracking-tight animate-slide-up" style={{ "animation-delay": "0.1s" }}>
{i18n.t("home.hero.title")}
</h1>
<p class="text-lg lg:text-xl text-ink-muted mb-10 max-w-2xl mx-auto leading-relaxed animate-slide-up" style={{ "animation-delay": "0.2s" }}>
{i18n.t("home.hero.subtitle")}
</p>
<div class="flex flex-col sm:flex-row items-center justify-center gap-4 animate-slide-up" style={{ "animation-delay": "0.3s" }}>
<A
href="/dashboard"
class="btn-primary inline-flex items-center gap-2 text-base px-8 py-4 group"
>
{i18n.t("home.hero.cta.primary")}
<span class="transition-transform duration-200 group-hover:translate-x-1">
<ArrowRightIcon />
</span>
</A>
<a
href="https://demo.bookra.eu"
target="_blank"
rel="noopener noreferrer"
class="btn-secondary inline-flex items-center gap-2 text-base px-8 py-4"
>
{i18n.locale() === 'cs' ? 'Otevřít demo mod' : 'Open demo mode'}
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/>
<polyline points="15 3 21 3 21 9"/>
<line x1="10" y1="14" x2="21" y2="3"/>
</svg>
</a>
</div>
</div>
{/* Hero visual - abstract booking UI mockup with mascot */}
<div class="mt-16 lg:mt-20 max-w-4xl mx-auto animate-slide-up relative" style={{ "animation-delay": "0.4s" }}>
<div class="surface-glass rounded-panel p-1.5 lg:p-2 shadow-2xl border border-border/50 relative">
<div class="bg-canvas rounded-card overflow-hidden">
{/* Calendar Header */}
<div class="flex items-center justify-between px-4 py-3 border-b border-border/50 bg-canvas-subtle/50">
<div class="flex items-center gap-3">
<div class="w-8 h-8 rounded-lg bg-accent/10 flex items-center justify-center">
<CalendarIcon />
</div>
<span class="font-display font-semibold text-ink">
{i18n.t(`calendar.months.${calendarMonth()}`)} {calendarYear()}
</span>
</div>
<div class="flex items-center gap-2">
<button
class="w-8 h-8 rounded-lg border border-border flex items-center justify-center text-ink-muted hover:bg-canvas-subtle transition-colors"
onClick={prevMonth}
aria-label={i18n.t("calendar.prevMonth")}
>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="m15 18-6-6 6-6"/></svg>
</button>
<button
class="w-8 h-8 rounded-lg border border-border flex items-center justify-center text-ink-muted hover:bg-canvas-subtle transition-colors"
onClick={nextMonth}
aria-label={i18n.t("calendar.nextMonth")}
>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="m9 18 6-6-6-6"/></svg>
</button>
</div>
</div>
<div class="grid grid-cols-7 gap-px bg-border/50">
{[0, 1, 2, 3, 4, 5, 6].map((dayIndex) => (
<div class="bg-canvas-subtle/50 p-3 text-center text-xs font-medium text-ink-subtle uppercase tracking-wider">
{i18n.t(`calendar.days.${dayIndex}`)}
</div>
))}
{calendarDays().map((dayInfo) => {
if (!dayInfo.isCurrentMonth) {
return <div class="p-3 min-h-[80px] lg:min-h-[100px] bg-canvas/50" />;
}
return (
<div
class={`p-3 min-h-[80px] lg:min-h-[100px] bg-canvas transition-all duration-200 hover:bg-canvas-subtle group cursor-pointer ${
dayInfo.isToday ? "ring-2 ring-inset ring-accent bg-accent-soft" : ""
}`}
>
<span class={`text-sm transition-colors ${dayInfo.isToday ? "font-semibold text-accent" : "text-ink-muted group-hover:text-ink"}`}>
{dayInfo.day}
</span>
{dayInfo.hasBooking && (
<div class="mt-2 space-y-1.5">
<div class="h-2 bg-accent/30 rounded-full w-full group-hover:bg-accent/40 transition-colors" />
{dayInfo.hasMultiple && <div class="h-2 bg-accent/20 rounded-full w-3/4 group-hover:bg-accent/30 transition-colors" />}
<div class="h-2 bg-accent/10 rounded-full w-1/2 group-hover:bg-accent/20 transition-colors" />
</div>
)}
</div>
);
})}
</div>
</div>
{/* Floating mascot at bottom-right */}
<div class="absolute -bottom-6 -right-4 lg:-right-8 z-20 hidden md:block">
<BookraCharacter pose="hello" size="md" animate={true} />
</div>
</div>
</div>
</div>
</div>
<div class="grid gap-4">
<div class="rounded-panel border border-black/10 bg-pine p-6 text-canvas shadow-card">
<p class="text-sm uppercase tracking-[0.2em] text-canvas/70">{i18n.t("home.appLabel")}</p>
<p class="mt-4 text-2xl font-semibold">{i18n.t("home.appTitle")}</p>
<p class="mt-3 text-sm leading-7 text-canvas/80">
{i18n.t("home.appBody")}
</section>
{/* Social proof / Logos */}
<section class="py-12 border-y border-border/50 bg-canvas-subtle/50">
<div class="section-container">
<div class="mb-6 flex justify-center">
<img
src="/bookra-illustrations/logo_text_horizontal.svg"
alt="Bookra"
class="h-9 w-auto opacity-75"
/>
</div>
<p class="text-center text-sm text-ink-subtle mb-8 tracking-wide uppercase">
{i18n.t("home.trust")}
</p>
<div class="flex flex-wrap items-center justify-center gap-8 lg:gap-16 opacity-60">
{["Salon Ella", "Physio Care", "Massage Studio", "Yoga Flow", "Repair Pro"].map((name) => (
<span class="text-lg lg:text-xl font-medium text-ink-muted tracking-tight">
{name}
</span>
))}
</div>
</div>
<div class="rounded-panel border border-black/10 bg-white/70 p-6 shadow-card">
<p class="text-sm uppercase tracking-[0.2em] text-slate">{i18n.t("home.publicLabel")}</p>
<p class="mt-4 text-2xl font-semibold">{i18n.t("home.publicTitle")}</p>
<p class="mt-3 text-sm leading-7 text-slate">{i18n.t("home.publicBody")}</p>
</section>
{/* Features Section */}
<section id="features" class="py-20 lg:py-32">
<div class="section-container">
<div class="max-w-2xl mx-auto text-center mb-16 lg:mb-20">
<span class="text-sm font-medium text-accent mb-3 block tracking-wide uppercase animate-fade-in">
{i18n.t("home.features.eyebrow")}
</span>
<h2 class="text-display-md font-semibold text-ink mb-4 animate-slide-up" style={{ "animation-delay": "0.1s" }}>
{i18n.t("home.features.title")}
</h2>
<p class="text-lg text-ink-muted animate-slide-up" style={{ "animation-delay": "0.2s" }}>
{i18n.t("home.features.subtitle")}
</p>
</div>
<div class="grid md:grid-cols-2 lg:grid-cols-3 gap-6 lg:gap-8">
{features.map((feature, index) => (
<div
class="group surface-elevated p-6 lg:p-8 rounded-card transition-all duration-500 hover:scale-[1.02] animate-slide-up"
style={{ "animation-delay": `${0.1 * (index + 3)}s` }}
>
<div class="w-12 h-12 rounded-xl bg-gradient-to-br from-accent-subtle to-accent-soft flex items-center justify-center text-accent mb-5 transition-all duration-300 group-hover:scale-110 group-hover:shadow-lg">
<feature.icon />
</div>
<h3 class="font-display text-lg font-semibold text-ink mb-2">{feature.title}</h3>
<p class="text-ink-muted leading-relaxed">{feature.description}</p>
</div>
))}
</div>
</div>
</div>
</section>
</section>
{/* How it works */}
<section class="py-20 lg:py-32 bg-canvas-subtle/30">
<div class="section-container">
<div class="grid lg:grid-cols-2 gap-12 lg:gap-20 items-center">
<div>
<span class="text-sm font-medium text-accent mb-3 block tracking-wide uppercase">
{i18n.t("home.how.eyebrow")}
</span>
<h2 class="text-display-md font-semibold text-ink mb-6">
{i18n.t("home.how.title")}
</h2>
<p class="text-lg text-ink-muted mb-8">
{i18n.t("home.how.subtitle")}
</p>
<div class="flex items-center gap-4 mb-8">
<A
href="/dashboard"
class="btn-primary inline-flex items-center gap-2"
>
{i18n.t("home.how.cta")}
<ArrowRightIcon />
</A>
{/* Character guide */}
<div class="hidden sm:block">
<BookraCharacter pose="walk" size="sm" animate={true} />
</div>
</div>
</div>
<div class="lg:pl-8">
{steps.map((step, index) => (
<StepCard
number={index + 1}
title={step.title}
description={step.description}
/>
))}
<div class="flex gap-5 lg:gap-6">
<div class="w-10 h-10 rounded-full bg-accent text-white flex items-center justify-center">
<CheckIcon />
</div>
<div>
<h3 class="text-lg font-semibold text-ink mb-1">
{i18n.t("home.step4.title")}
</h3>
<p class="text-accent font-medium">
{i18n.t("home.step4.desc")}
</p>
</div>
</div>
</div>
</div>
</div>
</section>
{/* Testimonials */}
<section class="py-20 lg:py-32">
<div class="section-container">
<div class="max-w-2xl mx-auto text-center mb-16">
<span class="text-sm font-medium text-accent mb-3 block tracking-wide uppercase animate-fade-in">
{i18n.t("home.testimonials.eyebrow")}
</span>
<h2 class="text-display-md font-semibold text-ink animate-slide-up" style={{ "animation-delay": "0.1s" }}>
{i18n.t("home.testimonials.title")}
</h2>
</div>
<div class="grid md:grid-cols-3 gap-6 lg:gap-8">
{testimonials.map((t, index) => (
<div
class="group surface-elevated p-6 lg:p-8 rounded-card transition-all duration-500 hover:shadow-xl animate-slide-up"
style={{ "animation-delay": `${0.15 * (index + 1)}s` }}
>
<div class="flex items-start gap-1 mb-4">
<svg class="w-6 h-6 text-accent/40" viewBox="0 0 24 24" fill="currentColor">
<path d="M14.017 21v-7.391c0-5.704 3.731-9.57 8.983-10.609l.995 2.151c-2.432.917-3.995 3.638-3.995 5.849h4v10h-9.983zm-14.017 0v-7.391c0-5.704 3.748-9.57 9-10.609l.996 2.151c-2.433.917-3.996 3.638-3.996 5.849h3.983v10h-9.983z"/>
</svg>
</div>
<p class="text-ink-muted mb-6 leading-relaxed font-sans text-lg italic">{t.quote}</p>
<div class="flex items-center gap-3 pt-4 border-t border-border/50">
<div class="w-10 h-10 rounded-full bg-gradient-to-br from-accent-subtle to-accent-soft flex items-center justify-center">
<span class="font-display font-semibold text-accent text-sm">
{t.author.split(' ').map(n => n[0]).join('')}
</span>
</div>
<div>
<p class="font-display font-semibold text-ink text-sm">{t.author}</p>
<p class="text-ink-subtle text-xs">{t.role}</p>
</div>
</div>
</div>
))}
</div>
</div>
</section>
{/* Pricing */}
<section id="pricing" class="py-20 lg:py-32 bg-canvas-subtle/30">
<div class="section-container">
<div class="max-w-2xl mx-auto text-center mb-16">
<span class="text-sm font-medium text-accent mb-3 block tracking-wide uppercase animate-fade-in">
{i18n.t("home.pricing.eyebrow")}
</span>
<h2 class="text-display-md font-semibold text-ink mb-4 animate-slide-up" style={{ "animation-delay": "0.1s" }}>
{i18n.t("home.pricing.title")}
</h2>
<p class="text-lg text-ink-muted animate-slide-up" style={{ "animation-delay": "0.2s" }}>
{i18n.t("home.pricing.subtitle")}
</p>
</div>
<div class="grid md:grid-cols-3 gap-6 lg:gap-8 max-w-5xl mx-auto items-center">
{/* Starter Plan */}
<div
class="group surface-elevated p-6 lg:p-8 rounded-card transition-all duration-500 hover:shadow-xl hover:-translate-y-1 animate-slide-up"
style={{ "animation-delay": "0.3s" }}
>
<div class="mb-6">
<h3 class="font-display text-lg font-semibold mb-1 text-ink">
{i18n.t("home.pricing.starter.name")}
</h3>
<p class="text-ink-muted">{i18n.t("home.pricing.starter.desc")}</p>
</div>
<div class="mb-6">
<span class="font-display text-4xl font-semibold text-ink">
{i18n.locale() === 'cs' ? '119 Kč' : '$5'}
</span>
<span class="text-ink-muted">{i18n.t("home.pricing.perMonth")}</span>
<p class="mt-1 text-xs text-accent font-medium">{i18n.t("home.pricing.starter.trial")}</p>
</div>
<ul class="space-y-3 mb-8">
{[i18n.t("home.pricing.starter.f1"), i18n.t("home.pricing.starter.f2"), i18n.t("home.pricing.starter.f3")].map((feature) => (
<li class="flex items-start gap-3 text-ink-muted">
<span class="mt-0.5 text-accent shrink-0">
<CheckIcon />
</span>
<span class="text-sm">{feature}</span>
</li>
))}
</ul>
<A
href="/dashboard"
class="block text-center py-3 px-6 rounded-button font-display font-medium transition-all duration-200 btn-secondary w-full"
>
{i18n.t("home.pricing.starter.cta")}
</A>
</div>
{/* Pro Plan - Highlighted */}
<div
class="group relative p-6 lg:p-8 rounded-card transition-all duration-500 hover:shadow-2xl hover:-translate-y-2 animate-slide-up lg:scale-105"
style={{ "animation-delay": "0.4s" }}
>
{/* Gradient background for highlighted card */}
<div class="absolute inset-0 bg-gradient-to-b from-ink to-ink/95 rounded-card" />
{/* Popular badge */}
<div class="absolute -top-3 left-1/2 -translate-x-1/2 z-10">
<span class="px-3 py-1 bg-accent text-white text-xs font-display font-medium rounded-full shadow-lg">
{i18n.t("home.pricing.popular")}
</span>
</div>
<div class="relative z-10">
<div class="mb-6">
<h3 class="font-display text-lg font-semibold mb-1 text-canvas">
{i18n.t("home.pricing.pro.name")}
</h3>
<p class="text-canvas/70">{i18n.t("home.pricing.pro.desc")}</p>
</div>
<div class="mb-6">
<span class="font-display text-4xl font-semibold text-canvas">
{i18n.locale() === 'cs' ? '499 Kč' : '$20'}
</span>
<span class="text-canvas/60">{i18n.t("home.pricing.perMonth")}</span>
<p class="mt-1 text-xs text-accent font-medium">{i18n.t("home.pricing.pro.trial")}</p>
</div>
<ul class="space-y-3 mb-8">
{[i18n.t("home.pricing.pro.f1"), i18n.t("home.pricing.pro.f2"), i18n.t("home.pricing.pro.f3"), i18n.t("home.pricing.pro.f4"), i18n.t("home.pricing.pro.f5")].map((feature) => (
<li class="flex items-start gap-3 text-canvas/80">
<span class="mt-0.5 text-accent shrink-0">
<CheckIcon />
</span>
<span class="text-sm">{feature}</span>
</li>
))}
</ul>
<A
href="/dashboard"
class="block text-center py-3 px-6 rounded-button font-display font-medium transition-all duration-200 bg-canvas text-ink hover:bg-canvas-subtle w-full shadow-lg group-hover:shadow-xl"
>
{i18n.t("home.pricing.pro.cta")}
</A>
</div>
</div>
{/* Business Plan */}
<div
class="group surface-elevated p-6 lg:p-8 rounded-card transition-all duration-500 hover:shadow-xl hover:-translate-y-1 animate-slide-up"
style={{ "animation-delay": "0.5s" }}
>
<div class="mb-6">
<h3 class="font-display text-lg font-semibold mb-1 text-ink">
{i18n.t("home.pricing.biz.name")}
</h3>
<p class="text-ink-muted">{i18n.t("home.pricing.biz.desc")}</p>
</div>
<div class="mb-6">
<span class="font-display text-4xl font-semibold text-ink">
{i18n.locale() === 'cs' ? '1 199 Kč' : '$50'}
</span>
<span class="text-ink-muted">{i18n.t("home.pricing.perMonth")}</span>
<p class="mt-1 text-xs text-accent font-medium">{i18n.t("home.pricing.biz.trial")}</p>
</div>
<ul class="space-y-3 mb-8">
{[i18n.t("home.pricing.biz.f1"), i18n.t("home.pricing.biz.f2"), i18n.t("home.pricing.biz.f3"), i18n.t("home.pricing.biz.f4")].map((feature) => (
<li class="flex items-start gap-3 text-ink-muted">
<span class="mt-0.5 text-accent shrink-0">
<CheckIcon />
</span>
<span class="text-sm">{feature}</span>
</li>
))}
</ul>
<A
href="/dashboard"
class="block text-center py-3 px-6 rounded-button font-display font-medium transition-all duration-200 btn-secondary w-full"
>
{i18n.t("home.pricing.biz.cta")}
</A>
</div>
</div>
</div>
</section>
{/* CTA Section */}
<section class="py-20 lg:py-32 relative overflow-hidden">
<div class="absolute inset-0 pointer-events-none">
<div class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[600px] h-[600px] bg-accent/5 rounded-full blur-3xl" />
</div>
<div class="section-container relative">
<div class="max-w-3xl mx-auto text-center">
{/* Celebratory mascot */}
<div class="flex justify-center mb-6">
<BookraCharacter pose="like" size="lg" animate={true} />
</div>
<h2 class="text-display-md font-semibold text-ink mb-6 animate-slide-up">
{i18n.t("home.cta.title")}
</h2>
<p class="text-lg text-ink-muted mb-10 max-w-xl mx-auto animate-slide-up" style={{ "animation-delay": "0.1s" }}>
{i18n.t("home.cta.subtitle")}
</p>
<div class="flex flex-col sm:flex-row items-center justify-center gap-4 animate-slide-up" style={{ "animation-delay": "0.2s" }}>
<A
href="/dashboard"
class="btn-primary inline-flex items-center gap-2 text-base px-8 py-4 group shadow-lg hover:shadow-xl transition-all duration-300"
>
{i18n.t("home.cta.primary")}
<span class="transition-transform duration-200 group-hover:translate-x-1">
<ArrowRightIcon />
</span>
</A>
<a
href="https://demo.bookra.eu"
target="_blank"
rel="noopener noreferrer"
class="btn-secondary inline-flex items-center gap-2 text-base px-8 py-4"
>
{i18n.locale() === 'cs' ? 'Otevřít demo mod' : 'Open demo mode'}
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/>
<polyline points="15 3 21 3 21 9"/>
<line x1="10" y1="14" x2="21" y2="3"/>
</svg>
</a>
</div>
</div>
</div>
</section>
</div>
);
}
+90
View File
@@ -0,0 +1,90 @@
import { useParams } from "@solidjs/router";
import { For } from "solid-js";
import { Card, CardContent, CardHeader, CardTitle } from "../components/ui";
import { BookraCharacter } from "../components/bookra-character";
import { useI18n } from "../providers/i18n-provider";
export function LegalRoute() {
const params = useParams();
const i18n = useI18n();
const kind = () => (params.kind === "terms" ? "terms" : "privacy");
const heroPose = () => (kind() === "terms" ? "flag" : "educate");
const helperPose = () => (kind() === "terms" ? "announcement" : "happy_note");
const sections = () =>
kind() === "terms"
? [
{
title: i18n.t("legal.terms.service.title"),
body: i18n.t("legal.terms.service.body"),
},
{
title: i18n.t("legal.terms.billing.title"),
body: i18n.t("legal.terms.billing.body"),
},
]
: [
{
title: i18n.t("legal.privacy.data.title"),
body: i18n.t("legal.privacy.data.body"),
},
{
title: i18n.t("legal.privacy.rights.title"),
body: i18n.t("legal.privacy.rights.body"),
},
];
return (
<section class="section-container py-16">
<div class="grid gap-8 lg:grid-cols-[minmax(0,1fr)_340px]">
<div class="max-w-3xl space-y-8">
<div class="space-y-4">
<h1 class="font-display text-4xl font-semibold text-ink">
{i18n.t(`legal.${kind()}.title`)}
</h1>
<p class="text-lg text-ink-muted">{i18n.t(`legal.${kind()}.body`)}</p>
</div>
<For each={sections()}>
{(section) => (
<Card class="surface-elevated">
<CardHeader>
<CardTitle>{section.title}</CardTitle>
</CardHeader>
<CardContent>
<p class="text-ink-muted">{section.body}</p>
</CardContent>
</Card>
)}
</For>
</div>
<aside class="lg:sticky lg:top-24">
<Card class="surface-elevated overflow-hidden">
<CardContent class="p-8 text-center">
<div class="mb-6 flex justify-center">
<BookraCharacter pose={heroPose()} size="xl" animate={true} />
</div>
<img
src="/bookra-illustrations/logo_text_vertical.svg"
alt="Bookra"
class="mx-auto mb-6 h-28 w-auto opacity-90"
/>
<p class="text-sm leading-relaxed text-ink-muted">
{kind() === "terms"
? i18n.locale() === "cs"
? "Pravidla držíme stručná, čitelná a navázaná na reálný provoz služby."
: "We keep terms short, readable, and tied to real product behavior."
: i18n.locale() === "cs"
? "Soukromí řešíme prakticky: minimum dat navíc, jasný účel a předvídatelné zpracování."
: "We handle privacy pragmatically: minimal extra data, clear purpose, and predictable processing."}
</p>
<div class="mt-6 flex justify-center">
<BookraCharacter pose={helperPose()} size="sm" animate={true} />
</div>
</CardContent>
</Card>
</aside>
</div>
</section>
);
}
@@ -0,0 +1,43 @@
import { A } from "@solidjs/router";
import { useI18n } from "../providers/i18n-provider";
import { BookraCharacter } from "../components/bookra-character";
export function NotFoundRoute() {
const i18n = useI18n();
return (
<div class="min-h-[60vh] flex flex-col items-center justify-center px-4 animate-fade-in">
<div class="text-center max-w-lg">
<div class="flex justify-center mb-8">
<BookraCharacter pose="404" size="xl" animate={false} />
</div>
<h1 class="text-display-xl font-semibold text-ink mb-4">
{i18n.locale() === 'cs' ? 'Stránka nenalezena' : 'Page Not Found'}
</h1>
<p class="text-lg text-ink-muted mb-8">
{i18n.locale() === 'cs'
? 'Omlouváme se, ale stránka kterou hledáte neexistuje.'
: 'Sorry, the page you are looking for does not exist.'}
</p>
<div class="flex flex-col sm:flex-row items-center justify-center gap-4">
<A href="/" class="btn-primary inline-flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/>
</svg>
{i18n.locale() === 'cs' ? 'Zpět domů' : 'Back Home'}
</A>
<A href="/contact" class="btn-secondary inline-flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>
</svg>
{i18n.locale() === 'cs' ? 'Kontaktujte nás' : 'Contact Us'}
</A>
</div>
</div>
</div>
);
}
+301 -64
View File
@@ -1,39 +1,60 @@
import { useParams } from "@solidjs/router";
import { A, 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";
import { BookraCharacter } from "../components/bookra-character";
import { Button, Card, CardHeader, CardTitle, CardDescription, CardContent, Badge, Input, SkeletonCard, Textarea, Tooltip } from "../components/ui";
export function PublicBookingRoute() {
const params = useParams();
const i18n = useI18n();
const tenantSlug = () => params.tenantSlug ?? "studio-atelier";
const tenantSlug = () => params.tenantSlug;
const [bookingResult, setBookingResult] = createSignal<string | null>(null);
const [bookingError, setBookingError] = createSignal<string | null>(null);
const [submittingSlot, setSubmittingSlot] = createSignal<string | null>(null);
const [availability, { refetch }] = createResource(() =>
apiClient.GET("/v1/public/tenants/{tenantSlug}/availability", {
const [customerName, setCustomerName] = createSignal("");
const [customerEmail, setCustomerEmail] = createSignal("");
const [notes, setNotes] = createSignal("");
const [availability, { refetch }] = createResource(() => {
const slug = tenantSlug();
if (!slug) return null;
return apiClient.GET("/v1/public/tenants/{tenantSlug}/availability", {
params: {
path: {
tenantSlug: tenantSlug(),
tenantSlug: slug,
},
},
}),
);
});
});
const bookSlot = async (slot: components["schemas"]["TimeSlot"]) => {
if (!customerName().trim() || !customerEmail().trim()) {
setBookingError(i18n.t("booking.customerRequired"));
return;
}
setSubmittingSlot(slot.startsAt);
setBookingResult(null);
setBookingError(null);
const slug = tenantSlug();
if (!slug) {
setBookingError(i18n.locale() === 'cs' ? 'Chybí identifikátor podniku' : 'Business identifier missing');
setSubmittingSlot(null);
return;
}
const response = await apiClient.POST("/v1/public/bookings", {
body: {
tenantSlug: tenantSlug(),
tenantSlug: slug,
bookingMode: slot.mode,
serviceId: slot.serviceId ?? undefined,
classSessionId: slot.classSessionId ?? undefined,
staffId: slot.staffId ?? undefined,
locationId: slot.locationId ?? undefined,
customerName: "Demo Customer",
customerEmail: "[email protected]",
customerName: customerName().trim(),
customerEmail: customerEmail().trim(),
notes: notes().trim(),
startsAt: slot.startsAt,
endsAt: slot.endsAt,
},
@@ -41,9 +62,10 @@ export function PublicBookingRoute() {
const payload = response as { data?: components["schemas"]["CreateBookingResponse"]; error?: unknown };
if (payload.error) {
setBookingResult(`Booking failed: ${String(payload.error)}`);
setBookingError(`${i18n.t("booking.failed")}: ${String(payload.error)}`);
} else if (payload.data) {
setBookingResult(`Created ${payload.data.status} booking ${payload.data.reference}`);
setBookingResult(`${i18n.t("booking.created")} ${payload.data.reference}`);
setNotes("");
}
setSubmittingSlot(null);
@@ -51,60 +73,275 @@ export function PublicBookingRoute() {
};
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.
<section class="section-container py-12 animate-fade-in">
{/* Header */}
<div class="max-w-3xl mb-10">
<h1 class="text-display-lg font-semibold text-ink mb-4 animate-slide-up">
{i18n.t("booking.title")}
</h1>
<p class="text-lg text-ink-muted animate-slide-up" style={{ "animation-delay": "0.1s" }}>
{i18n.t("booking.body")}
</p>
</div>
<Show when={!tenantSlug()}>
<div class="max-w-2xl mx-auto text-center py-16">
<BookraCharacter pose="404" size="lg" animate={false} class="mx-auto mb-6" />
<h2 class="text-display-md font-semibold text-ink mb-3">
{i18n.locale() === 'cs' ? 'Podnik nenalezen' : 'Business not found'}
</h2>
<p class="text-ink-muted mb-6">
{i18n.locale() === 'cs'
? 'Zkontrolujte URL adresu nebo kontaktujte provozovatele.'
: 'Please check the URL or contact the service provider.'}
</p>
<A href="/" class="btn-primary inline-flex items-center gap-2">
<HomeIcon />
{i18n.locale() === 'cs' ? 'Zpět na hlavní stránku' : 'Back to home'}
</A>
</div>
</Show>
<Show when={tenantSlug()}>
<div class="grid gap-8 lg:grid-cols-[1.1fr_0.9fr]">
{/* Sidebar - Contact form - ORDER 1 on mobile, 2 on desktop */}
<div class="space-y-6 order-1 lg:order-2">
<Card class="surface-elevated animate-slide-up" style={{ "animation-delay": "0.3s" }}>
<CardHeader>
<CardTitle class="font-display text-xl">{i18n.t("booking.customer.title")}</CardTitle>
<CardDescription>{i18n.t("booking.customer.body")}</CardDescription>
</CardHeader>
<CardContent class="space-y-4">
<Input
autocomplete="name"
label={i18n.t("booking.customer.name")}
onInput={(event) => setCustomerName(event.currentTarget.value)}
required
value={customerName()}
/>
<Input
autocomplete="email"
label={i18n.t("booking.customer.email")}
onInput={(event) => setCustomerEmail(event.currentTarget.value)}
required
type="email"
value={customerEmail()}
/>
<Textarea
label={i18n.t("booking.customer.notes")}
onInput={(event) => setNotes(event.currentTarget.value)}
rows={3}
value={notes()}
/>
</CardContent>
</Card>
<Card class="bg-gradient-to-br from-ink to-ink/95 text-canvas border-ink h-fit animate-slide-up shadow-xl" style={{ "animation-delay": "0.3s" }}>
<CardHeader>
<div class="flex items-center gap-2 mb-2">
<div class="w-8 h-8 rounded-lg bg-canvas/10 flex items-center justify-center">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="text-canvas/80">
<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/>
</svg>
</div>
<Badge variant="outline" class="border-canvas/30 text-canvas/70">{i18n.t("booking.business")}</Badge>
</div>
<div class="flex items-center gap-3">
<BookraCharacter pose="hello" size="sm" animate={true} />
<CardTitle class="font-display text-xl">{tenantSlug()}</CardTitle>
</div>
</CardHeader>
<CardContent class="space-y-4">
<p class="text-sm text-canvas/80 leading-relaxed">
{i18n.t("booking.sidebar.body")}
</p>
<div class="flex items-center gap-3 pt-4 border-t border-canvas/10">
<div class="w-10 h-10 rounded-full bg-canvas/10 flex items-center justify-center">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="text-canvas/70">
<path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"/>
</svg>
</div>
<div>
<p class="text-sm font-medium text-canvas">{i18n.t("booking.help.title")}</p>
<p class="text-xs text-canvas/60">{i18n.t("booking.help.body")}</p>
</div>
</div>
</CardContent>
</Card>
{/* Quick info card */}
<Card class="surface-elevated animate-slide-up" style={{ "animation-delay": "0.4s" }}>
<CardContent class="p-6">
<h4 class="font-display font-semibold text-ink mb-3">{i18n.t("booking.expect.title")}</h4>
<ul class="space-y-2 text-sm text-ink-muted">
<li class="flex items-start gap-2">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="text-accent mt-0.5 shrink-0">
<polyline points="20 6 9 17 4 12"/>
</svg>
{i18n.t("booking.expect.confirmation")}
</li>
<li class="flex items-start gap-2">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="text-accent mt-0.5 shrink-0">
<polyline points="20 6 9 17 4 12"/>
</svg>
{i18n.t("booking.expect.reminders")}
</li>
<li class="flex items-start gap-2">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="text-accent mt-0.5 shrink-0">
<polyline points="20 6 9 17 4 12"/>
</svg>
{i18n.t("booking.expect.rescheduling")}
</li>
</ul>
</CardContent>
</Card>
</div>
{/* Main booking card - Slots - ORDER 2 on mobile, 1 on desktop */}
<Card class="surface-elevated animate-slide-up order-2 lg:order-1" style={{ "animation-delay": "0.2s" }}>
<CardHeader>
<div class="flex items-center gap-2 mb-2">
<div class="w-8 h-8 rounded-lg bg-accent/10 flex items-center justify-center">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="text-accent">
<rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/>
</svg>
</div>
<Badge variant="secondary">{i18n.t("booking.slots")}</Badge>
</div>
<CardTitle class="font-display text-2xl">{i18n.t("booking.selectTime")}</CardTitle>
<CardDescription>{i18n.t("booking.selectTimeBody")}</CardDescription>
</CardHeader>
<CardContent class="space-y-6">
{/* Success message */}
<Show when={bookingResult()}>
<div class="rounded-card border border-success/30 bg-success-subtle/50 px-4 py-5 text-sm text-success flex items-center gap-4 animate-scale-in">
<BookraCharacter pose="success" size="sm" animate={false} />
<div class="flex-1">
<p class="font-semibold text-success">{i18n.t("booking.confirmed")}</p>
<p class="text-success/80">{bookingResult()}</p>
</div>
</div>
</Show>
<Show when={bookingError()}>
<div class="rounded-card border border-error/30 bg-error-subtle/50 px-4 py-5 text-sm text-error flex items-center gap-4 animate-scale-in">
<BookraCharacter pose="lookup" size="sm" animate={false} class="opacity-80" />
<div class="flex-1">
<p class="font-semibold text-error">{i18n.t("booking.failed")}</p>
<p class="text-error/80">{bookingError()}</p>
</div>
</div>
</Show>
{/* Loading state */}
<Show when={availability.loading}>
<div class="space-y-4">
<SkeletonCard />
<SkeletonCard />
<SkeletonCard />
</div>
</Show>
{/* Slots list */}
<div class="space-y-4">
<Show
when={!availability.loading && availability.latest?.data?.slots?.length}
fallback={
<Show when={!availability.loading}>
<div class="rounded-card border border-dashed border-border bg-canvas-subtle/50 p-8 text-center">
<BookraCharacter pose="sleep" size="md" animate={true} class="mx-auto mb-4" />
<p class="text-sm text-ink-muted font-medium">{i18n.t("booking.empty")}</p>
<p class="text-xs text-ink-subtle mt-2">{i18n.t("booking.emptyHint")}</p>
</div>
</Show>
}
>
<For each={availability.latest?.data?.slots ?? []}>
{(slot, index) => (
<Card
variant="default"
class="group surface-card hover:border-accent/30 transition-all duration-300"
style={{ "animation-delay": `${0.1 * (index() + 3)}s` }}
padding="lg"
>
<div class="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
<div class="space-y-2">
<div class="flex items-center gap-2">
<Badge
variant={slot.mode === "appointment" ? "secondary" : "default"}
class="font-display text-xs uppercase tracking-wider"
>
{slot.mode}
</Badge>
<Show when={typeof slot.remainingCapacity === "number" && slot.remainingCapacity <= 3}>
<Badge variant="warning" size="sm">{i18n.t("booking.only")} {slot.remainingCapacity} {i18n.t("booking.left")}</Badge>
</Show>
</div>
<h3 class="font-display text-lg font-semibold text-ink">{slot.label ?? "Bookable slot"}</h3>
<div class="flex items-center gap-4 text-sm text-ink-muted">
<span class="flex items-center gap-1.5">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/>
</svg>
{new Date(slot.startsAt).toLocaleDateString(undefined, { weekday: 'short', month: 'short', day: 'numeric' })}
</span>
<span class="flex items-center gap-1.5">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/>
</svg>
{new Date(slot.startsAt).toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' })} - {new Date(slot.endsAt).toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' })}
</span>
</div>
<Show when={typeof slot.remainingCapacity === "number" && slot.mode === "class"}>
<p class="text-sm text-success font-medium flex items-center gap-1.5">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/>
</svg>
{slot.remainingCapacity} {i18n.t("booking.spotsAvailable")}
</p>
</Show>
</div>
<Tooltip content="Book this slot now">
<Button
isLoading={submittingSlot() === slot.startsAt}
onClick={() => void bookSlot(slot)}
size="md"
class="shadow-md hover:shadow-lg transition-shadow whitespace-nowrap"
>
{submittingSlot() === slot.startsAt ? (
<>
<svg class="animate-spin -ml-1 mr-2 h-4 w-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"/>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"/>
</svg>
{i18n.t("booking.submitting")}
</>
) : (
<>
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="mr-2">
<rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/>
</svg>
{i18n.t("booking.submit")}
</>
)}
</Button>
</Tooltip>
</div>
</Card>
)}
</For>
</Show>
</div>
</CardContent>
</Card>
</div>
</Show>
</section>
);
}
const HomeIcon = () => (
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/>
</svg>
);