mirror of
https://github.com/Dvorinka/Bookra.git
synced 2026-10-29 16:04:45 +00:00
cleanup
This commit is contained in:
1 parent
d854614a87
commit
48c3e15a38
295 files changed
+178378
-1036
No files matched your search
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
Reference in new issue
Block a user