mirror of
https://github.com/Dvorinka/Bookra.git
synced 2026-06-04 04:22:59 +00:00
feat(core): consolidate auth service into backend and implement stripe billing
This commit performs a major architectural refactor by migrating the standalone `auth-service` into the main `backend` application, enabling a unified codebase and simplified deployment. It also introduces comprehensive Stripe billing support and a new administrative dashboard.
Key changes:
- **Architecture**: Deleted `apps/auth-service` and integrated its functionality (JWT, magic links, OAuth, user management) into `apps/backend`.
- **Billing**: Added Stripe integration to `backend`, supporting both monthly and yearly subscription cycles with automatic plan entitlement enforcement (e.g., location limits).
- **Admin Dashboard**: Implemented a new administrative service and API endpoints to manage tenants, users, and view platform-wide statistics.
- **Frontend**:
- Added a new pricing page with monthly/yearly toggle and comparison table.
- Integrated Stripe and Sentry for payments and error tracking.
- Improved dashboard UX/UI and added i18n support for new features.
- Enhanced the public booking flow with better validation and contact form integration.
- **Database**: Added migrations for users, magic links, password resets, OAuth states, admin audit logs, and refresh tokens.
- **DevOps**: Updated environment configurations for Railway and Vercel, and streamlined the project's `package.json` scripts.
This commit is contained in:
@@ -19,13 +19,20 @@ export function IntegrationModal(props: IntegrationModalProps) {
|
||||
setTimeout(() => setCopiedSnippet(null), 2000);
|
||||
};
|
||||
|
||||
const hostedPageUrl = `https://bookra.eu/book/${props.tenantSlug}`;
|
||||
const hostedPageUrl = props.publicBookingUrl;
|
||||
const baseUrl = (() => {
|
||||
try {
|
||||
return new URL(props.publicBookingUrl).origin;
|
||||
} catch {
|
||||
return "https://bookra.eu";
|
||||
}
|
||||
})();
|
||||
|
||||
const htmlWidgetCode = `<div id="bookra-widget"></div>
|
||||
<script>
|
||||
(function() {
|
||||
var script = document.createElement('script');
|
||||
script.src = "https://bookra.eu/widget.js";
|
||||
script.src = "${baseUrl}/widget.js";
|
||||
script.async = true;
|
||||
script.onload = function() {
|
||||
BookraWidget.init({
|
||||
@@ -65,7 +72,7 @@ function App() {
|
||||
add_action('wp_footer', function() {
|
||||
?>
|
||||
<div id="bookra-widget"></div>
|
||||
<script src="https://bookra.eu/widget.js" async></script>
|
||||
<script src="${baseUrl}/widget.js" async></script>
|
||||
<script>
|
||||
window.addEventListener('load', function() {
|
||||
BookraWidget.init({
|
||||
|
||||
@@ -76,6 +76,7 @@ export const Shell: ParentComponent = (props) => {
|
||||
const navigate = useNavigate();
|
||||
const [mobileMenuOpen, setMobileMenuOpen] = createSignal(false);
|
||||
const hideHeader = () => location.pathname.startsWith("/dashboard");
|
||||
const hideFooter = () => location.pathname.startsWith("/dashboard");
|
||||
const [signInOpen, setSignInOpen] = createSignal(false);
|
||||
const [authMode, setAuthMode] = createSignal<"sign-in" | "register">("sign-in");
|
||||
const [name, setName] = createSignal("");
|
||||
@@ -88,6 +89,7 @@ export const Shell: ParentComponent = (props) => {
|
||||
const showGoogleSignIn = () => auth.supportsGoogleSignIn() && authMode() === "sign-in";
|
||||
|
||||
const navLinks = [
|
||||
{ href: "/pricing", label: i18n.t("nav.pricing") },
|
||||
{ href: "/about", label: i18n.t("nav.about") },
|
||||
{ href: "/contact", label: i18n.t("nav.contact") },
|
||||
];
|
||||
@@ -409,6 +411,7 @@ export const Shell: ParentComponent = (props) => {
|
||||
|
||||
<main class="flex-1">{props.children}</main>
|
||||
|
||||
<Show when={!hideFooter()}>
|
||||
{/* Footer */}
|
||||
<footer class="border-t border-border/60 py-16 bg-canvas-subtle/40">
|
||||
<div class="section-container">
|
||||
@@ -480,6 +483,7 @@ export const Shell: ParentComponent = (props) => {
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</Show>
|
||||
|
||||
<Dialog open={signInOpen()} onClose={() => setSignInOpen(false)}>
|
||||
<DialogHeader>
|
||||
|
||||
@@ -215,6 +215,7 @@ export function WidgetBuilder(props: WidgetBuilderProps) {
|
||||
const [selectedSize, setSelectedSize] = createSignal<WidgetSize>("default");
|
||||
const [selectedPosition, setSelectedPosition] = createSignal<WidgetPosition>("bottom-right");
|
||||
const [copiedSnippet, setCopiedSnippet] = createSignal<string | null>(null);
|
||||
const [copyError, setCopyError] = createSignal<string | null>(null);
|
||||
const [showPreview, setShowPreview] = createSignal(true);
|
||||
const [draggedIndex, setDraggedIndex] = createSignal<number | null>(null);
|
||||
const [customColor, setCustomColor] = createSignal(props.config.primaryColor || "#a65c3e");
|
||||
@@ -1077,9 +1078,11 @@ export class BookraWidgetComponent implements OnInit {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
setCopiedSnippet(snippetId);
|
||||
setCopyError(null);
|
||||
setTimeout(() => setCopiedSnippet(null), 2000);
|
||||
} catch (err) {
|
||||
console.error("Failed to copy:", err);
|
||||
setCopyError(i18n.locale() === 'cs' ? 'Kopírování se nezdařilo. Zkuste to znovu.' : 'Copy failed. Please try again.');
|
||||
setTimeout(() => setCopyError(null), 3000);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1445,6 +1448,11 @@ export class BookraWidgetComponent implements OnInit {
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Show when={copyError()}>
|
||||
<div class="mb-4 p-3 rounded-xl bg-[hsl(var(--error-soft))] border border-[hsl(var(--error))/20] text-[hsl(var(--error))] text-sm font-medium animate-fade-in">
|
||||
{copyError()}
|
||||
</div>
|
||||
</Show>
|
||||
<Tabs defaultValue="html">
|
||||
<TabsList class="mb-4 flex-wrap">
|
||||
<TabsTrigger value="html">HTML</TabsTrigger>
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import * as Sentry from "@sentry/react";
|
||||
|
||||
export function initSentry() {
|
||||
const dsn = import.meta.env.VITE_SENTRY_DSN;
|
||||
|
||||
if (!dsn) {
|
||||
console.log("Sentry DSN not configured - skipping initialization");
|
||||
return;
|
||||
}
|
||||
|
||||
Sentry.init({
|
||||
dsn,
|
||||
integrations: [
|
||||
Sentry.browserTracingIntegration(),
|
||||
Sentry.replayIntegration({
|
||||
maskAllText: false,
|
||||
blockAllMedia: false,
|
||||
}),
|
||||
],
|
||||
tracesSampleRate: 1.0,
|
||||
replaysSessionSampleRate: 0.1,
|
||||
replaysOnErrorSampleRate: 1.0,
|
||||
enableLogs: true,
|
||||
environment: import.meta.env.MODE,
|
||||
release: `bookra@${import.meta.env.VITE_APP_VERSION || "1.0.0"}`,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { loadStripe, type Stripe } from "@stripe/stripe-js";
|
||||
|
||||
const stripePublishableKey = import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY ?? "";
|
||||
|
||||
let stripePromise: Promise<Stripe | null> | null = null;
|
||||
|
||||
export function stripeConfigured() {
|
||||
return stripePublishableKey.trim() !== "";
|
||||
}
|
||||
|
||||
export async function getStripe() {
|
||||
if (!stripeConfigured()) {
|
||||
return null;
|
||||
}
|
||||
if (!stripePromise) {
|
||||
stripePromise = loadStripe(stripePublishableKey);
|
||||
}
|
||||
return stripePromise;
|
||||
}
|
||||
@@ -3,8 +3,12 @@ import { lazy } from "solid-js";
|
||||
import { Route, Router } from "@solidjs/router";
|
||||
import App from "./App";
|
||||
import "./styles/index.css";
|
||||
import { initSentry } from "./lib/sentry";
|
||||
|
||||
initSentry();
|
||||
|
||||
const HomeRoute = lazy(() => import("./routes/home-route").then((module) => ({ default: module.HomeRoute })));
|
||||
const PricingRoute = lazy(() => import("./routes/pricing-route"));
|
||||
const AboutRoute = lazy(() => import("./routes/about-route").then((module) => ({ default: module.AboutRoute })));
|
||||
const AuthCallbackRoute = lazy(() => import("./routes/auth-callback-route").then((module) => ({ default: module.AuthCallbackRoute })));
|
||||
const ContactRoute = lazy(() => import("./routes/contact-route").then((module) => ({ default: module.ContactRoute })));
|
||||
@@ -18,6 +22,7 @@ render(
|
||||
() => (
|
||||
<Router root={App}>
|
||||
<Route path="/" component={HomeRoute} />
|
||||
<Route path="/pricing" component={PricingRoute} />
|
||||
<Route path="/about" component={AboutRoute} />
|
||||
<Route path="/auth/callback" component={AuthCallbackRoute} />
|
||||
<Route path="/contact" component={ContactRoute} />
|
||||
|
||||
@@ -13,6 +13,7 @@ const dictionaries = {
|
||||
// Navigation & Auth
|
||||
"nav.booking": "Veřejná rezervace",
|
||||
"nav.dashboard": "Aplikace",
|
||||
"nav.pricing": "Ceník",
|
||||
"nav.about": "O nás",
|
||||
"nav.contact": "Kontakt",
|
||||
|
||||
@@ -164,6 +165,26 @@ const dictionaries = {
|
||||
"home.pricing.biz.cta": "Kontaktovat prodej",
|
||||
"home.pricing.biz.trial": "Individuální řešení na míru",
|
||||
|
||||
// Comparison
|
||||
"pricing.compare.eyebrow": "Detailní srovnání",
|
||||
"pricing.compare.title": "Porovnání plánů",
|
||||
"pricing.compare.feature": "Funkce",
|
||||
"pricing.compare.locations": "Lokace",
|
||||
"pricing.compare.staff": "Zaměstnanci",
|
||||
"pricing.compare.bookings": "Rezervací/měsíc",
|
||||
"pricing.compare.emailSupport": "E-mailová podpora",
|
||||
"pricing.compare.reminders": "E-mailová připomenutí",
|
||||
"pricing.compare.analytics": "Analytika",
|
||||
"pricing.compare.api": "API přístup",
|
||||
"pricing.compare.branding": "Vlastní branding",
|
||||
"pricing.compare.whiteLabel": "Bílý labeling",
|
||||
"pricing.compare.manager": "Dedikovaný manažer",
|
||||
"pricing.compare.priority": "Prioritní",
|
||||
"pricing.compare.dedicated": "Dedikovaný",
|
||||
"pricing.compare.advanced": "Pokročilá",
|
||||
"pricing.compare.yes": "Ano",
|
||||
"pricing.compare.no": "Ne",
|
||||
|
||||
// CTA
|
||||
"home.cta.title": "Připraveni zjednodušit své rezervace?",
|
||||
"home.cta.subtitle": "Připojte se k tisícům podniků, které šetří čas s Bookra.",
|
||||
@@ -235,12 +256,51 @@ const dictionaries = {
|
||||
"footer.links.title": "Navigace",
|
||||
"footer.legal.title": "Právní informace",
|
||||
|
||||
// Dashboard (existing)
|
||||
// Dashboard
|
||||
"dashboard.title": "Přehled podniku",
|
||||
"dashboard.body": "Sledujte rezervace, nastavení, předplatné a rezervační widget na jednom místě.",
|
||||
"dashboard.kpi.bookings": "Rezervace tento týden",
|
||||
"dashboard.kpi.cancellations": "Zrušení",
|
||||
"dashboard.kpi.utilization": "Vytížení",
|
||||
"dashboard.overview": "Přehled",
|
||||
"dashboard.bookings": "Rezervace",
|
||||
"dashboard.customers": "Zákazníci",
|
||||
"dashboard.zones": "Zóny a dostupnost",
|
||||
"dashboard.billing": "Platby",
|
||||
"dashboard.settings": "Nastavení",
|
||||
"dashboard.welcome": "Dobrý den,",
|
||||
"dashboard.overviewFor": "Přehled pro",
|
||||
"dashboard.kpi.bookings": "Celkem rezervací",
|
||||
"dashboard.kpi.cancelled": "Zrušených",
|
||||
"dashboard.kpi.completed": "Dokončených",
|
||||
"dashboard.kpi.newClients": "Noví klienti",
|
||||
"dashboard.recentActivity": "Nedávná aktivita",
|
||||
"dashboard.upcomingBookings": "Nadcházející rezervace",
|
||||
"dashboard.viewAll": "Zobrazit vše",
|
||||
"dashboard.locationLimitReached": "Dosáhli jste limitu lokací!",
|
||||
"dashboard.nearingLocationLimit": "Blížíte se limitu lokací",
|
||||
"dashboard.locationsUsed": "lokací použito",
|
||||
"dashboard.upgrade": "Upgrade",
|
||||
"dashboard.shareManage": "Sdílet/Spravit",
|
||||
"dashboard.notifications": "Oznámení",
|
||||
"dashboard.bookingManagement": "Správa rezervací",
|
||||
"dashboard.totalBookings": "celkem rezervací",
|
||||
"dashboard.newBooking": "Nová rezervace",
|
||||
"dashboard.bookingDetails": "Detail rezervace",
|
||||
"dashboard.customerDetails": "Detail zákazníka",
|
||||
"dashboard.close": "Zavřít",
|
||||
"dashboard.edit": "Upravit",
|
||||
"dashboard.cancel": "Zrušit",
|
||||
"dashboard.details": "Detail",
|
||||
"dashboard.saveChanges": "Uložit změny",
|
||||
"dashboard.createBooking": "Vytvořit rezervaci",
|
||||
"dashboard.preview": "Zobrazit náhled",
|
||||
"dashboard.saveEmailSettings": "Uložit nastavení emailů",
|
||||
"dashboard.saving": "Ukládání...",
|
||||
"dashboard.creating": "Vytváření...",
|
||||
"dashboard.prevMonth": "Předchozí měsíc",
|
||||
"dashboard.nextMonth": "Další měsíc",
|
||||
"dashboard.confirmed": "Potvrzeno",
|
||||
"dashboard.pending": "Čeká",
|
||||
"dashboard.cancelled": "Zrušeno",
|
||||
"dashboard.completed": "Dokončeno",
|
||||
"dashboard.welcome.title": "Vítejte v Bookra",
|
||||
"dashboard.welcome.body": "Zjednodušte své rezervace a mějte více času na to, co vás baví.",
|
||||
"dashboard.authRequired": "Pro vstup do aplikace se přihlaste nebo si vytvořte účet.",
|
||||
@@ -248,7 +308,6 @@ const dictionaries = {
|
||||
"dashboard.liveData": "Živá data",
|
||||
"dashboard.liveDataBody": "Dashboard, nastavení a předplatné se načítají z API pro přihlášený účet.",
|
||||
"dashboard.apiReady": "API připojení aktivní",
|
||||
"dashboard.billing": "Předplatné",
|
||||
"dashboard.checkout": "Otevřít platbu",
|
||||
"dashboard.refreshBilling": "Obnovit předplatné",
|
||||
"dashboard.plan": "Plán",
|
||||
@@ -326,6 +385,12 @@ const dictionaries = {
|
||||
"contact.info.email.desc": "Preferujete psát? Jsme tu pro vás.",
|
||||
"contact.info.hours.title": "Pracovní doba",
|
||||
"contact.info.hours.desc": "Odpovídáme během pracovních dní 9:00 — 17:00 CET.",
|
||||
"contact.story.heading": "Proč nás kontaktovat?",
|
||||
"contact.story.p1": "Ať už máte dotaz ohledně funkcí, potřebujete pomoci s nastavením, nebo chcete sdílet zpětnou vazbu — rádi vám pomůžeme.",
|
||||
"contact.story.p2": "Naším cílem je, aby správa rezervací byla pro vás bezstarostná. Ozvěte se nám a najdeme řešení společně.",
|
||||
"contact.error.title": "Nepodařilo se odeslat",
|
||||
"contact.error.body": "Zkuste to prosím později, nebo nám napište přímo na hello@bookra.eu.",
|
||||
"contact.email.address": "hello@bookra.eu",
|
||||
|
||||
// Legal
|
||||
"legal.privacy.title": "Ochrana soukromí",
|
||||
@@ -345,6 +410,7 @@ const dictionaries = {
|
||||
// Navigation & Auth
|
||||
"nav.booking": "Public booking",
|
||||
"nav.dashboard": "App",
|
||||
"nav.pricing": "Pricing",
|
||||
"nav.about": "About us",
|
||||
"nav.contact": "Contact",
|
||||
|
||||
@@ -496,6 +562,26 @@ const dictionaries = {
|
||||
"home.pricing.biz.cta": "Contact sales",
|
||||
"home.pricing.biz.trial": "Custom enterprise solutions",
|
||||
|
||||
// Comparison
|
||||
"pricing.compare.eyebrow": "Detailed comparison",
|
||||
"pricing.compare.title": "Compare plans",
|
||||
"pricing.compare.feature": "Feature",
|
||||
"pricing.compare.locations": "Locations",
|
||||
"pricing.compare.staff": "Staff members",
|
||||
"pricing.compare.bookings": "Bookings/month",
|
||||
"pricing.compare.emailSupport": "Email support",
|
||||
"pricing.compare.reminders": "Email reminders",
|
||||
"pricing.compare.analytics": "Analytics",
|
||||
"pricing.compare.api": "API access",
|
||||
"pricing.compare.branding": "Custom branding",
|
||||
"pricing.compare.whiteLabel": "White labeling",
|
||||
"pricing.compare.manager": "Dedicated manager",
|
||||
"pricing.compare.priority": "Priority",
|
||||
"pricing.compare.dedicated": "Dedicated",
|
||||
"pricing.compare.advanced": "Advanced",
|
||||
"pricing.compare.yes": "Yes",
|
||||
"pricing.compare.no": "No",
|
||||
|
||||
// CTA
|
||||
"home.cta.title": "Ready to simplify your bookings?",
|
||||
"home.cta.subtitle": "Join thousands of businesses saving time with Bookra.",
|
||||
@@ -567,12 +653,51 @@ const dictionaries = {
|
||||
"footer.links.title": "Navigation",
|
||||
"footer.legal.title": "Legal",
|
||||
|
||||
// Dashboard (existing)
|
||||
// Dashboard
|
||||
"dashboard.title": "Owner dashboard",
|
||||
"dashboard.body": "Track weekly bookings, cancellations, utilization, and subscription state with a tenant-aware shell ready for Neon-backed data.",
|
||||
"dashboard.kpi.bookings": "Bookings this week",
|
||||
"dashboard.kpi.cancellations": "Cancellations",
|
||||
"dashboard.kpi.utilization": "Utilization",
|
||||
"dashboard.overview": "Overview",
|
||||
"dashboard.bookings": "Bookings",
|
||||
"dashboard.customers": "Customers",
|
||||
"dashboard.zones": "Zones & Availability",
|
||||
"dashboard.billing": "Billing",
|
||||
"dashboard.settings": "Settings",
|
||||
"dashboard.welcome": "Welcome back,",
|
||||
"dashboard.overviewFor": "Overview for",
|
||||
"dashboard.kpi.bookings": "Total Bookings",
|
||||
"dashboard.kpi.cancelled": "Cancelled",
|
||||
"dashboard.kpi.completed": "Completed",
|
||||
"dashboard.kpi.newClients": "New Clients",
|
||||
"dashboard.recentActivity": "Recent Activity",
|
||||
"dashboard.upcomingBookings": "Upcoming Bookings",
|
||||
"dashboard.viewAll": "View all",
|
||||
"dashboard.locationLimitReached": "You've reached your location limit!",
|
||||
"dashboard.nearingLocationLimit": "You're nearing your location limit",
|
||||
"dashboard.locationsUsed": "locations used",
|
||||
"dashboard.upgrade": "Upgrade",
|
||||
"dashboard.shareManage": "Share/Manage",
|
||||
"dashboard.notifications": "Notifications",
|
||||
"dashboard.bookingManagement": "Booking Management",
|
||||
"dashboard.totalBookings": "total bookings",
|
||||
"dashboard.newBooking": "New Booking",
|
||||
"dashboard.bookingDetails": "Booking Details",
|
||||
"dashboard.customerDetails": "Customer Details",
|
||||
"dashboard.close": "Close",
|
||||
"dashboard.edit": "Edit",
|
||||
"dashboard.cancel": "Cancel",
|
||||
"dashboard.details": "Details",
|
||||
"dashboard.saveChanges": "Save Changes",
|
||||
"dashboard.createBooking": "Create Booking",
|
||||
"dashboard.preview": "Preview",
|
||||
"dashboard.saveEmailSettings": "Save Email Settings",
|
||||
"dashboard.saving": "Saving...",
|
||||
"dashboard.creating": "Creating...",
|
||||
"dashboard.prevMonth": "Previous month",
|
||||
"dashboard.nextMonth": "Next month",
|
||||
"dashboard.confirmed": "Confirmed",
|
||||
"dashboard.pending": "Pending",
|
||||
"dashboard.cancelled": "Cancelled",
|
||||
"dashboard.completed": "Completed",
|
||||
"dashboard.welcome.title": "Welcome to Bookra",
|
||||
"dashboard.welcome.body": "Simplify your bookings and spend more time doing what you love.",
|
||||
"dashboard.authRequired": "Live dashboard data needs a Neon Auth session and JWT.",
|
||||
@@ -580,7 +705,6 @@ const dictionaries = {
|
||||
"dashboard.liveData": "Live data",
|
||||
"dashboard.liveDataBody": "Dashboard, tenant, and billing data are loaded from the API for the signed-in workspace.",
|
||||
"dashboard.apiReady": "API connection active",
|
||||
"dashboard.billing": "Billing",
|
||||
"dashboard.checkout": "Open checkout",
|
||||
"dashboard.refreshBilling": "Refresh billing",
|
||||
"dashboard.plan": "Plan",
|
||||
@@ -658,6 +782,12 @@ const dictionaries = {
|
||||
"contact.info.email.desc": "Prefer to write? We're here for you.",
|
||||
"contact.info.hours.title": "Working hours",
|
||||
"contact.info.hours.desc": "We respond on business days 9:00 — 17:00 CET.",
|
||||
"contact.story.heading": "Why reach out?",
|
||||
"contact.story.p1": "Whether you have questions about features, need help with setup, or want to share feedback — we're happy to help.",
|
||||
"contact.story.p2": "Our goal is to make booking management effortless for you. Get in touch and we'll find a solution together.",
|
||||
"contact.error.title": "Failed to send",
|
||||
"contact.error.body": "Please try again later, or email us directly at hello@bookra.eu.",
|
||||
"contact.email.address": "hello@bookra.eu",
|
||||
|
||||
// Legal
|
||||
"legal.privacy.title": "Privacy",
|
||||
|
||||
@@ -40,6 +40,7 @@ export function BookingManageRoute() {
|
||||
customerEmail: "alice@example.com",
|
||||
service: "Yoga Flow Class",
|
||||
businessName: "Serenity Wellness Studio",
|
||||
businessEmail: "support@bookra.eu",
|
||||
startsAt: new Date(Date.now() + 86400000).toISOString(),
|
||||
endsAt: new Date(Date.now() + 86400000 + 3600000).toISOString(),
|
||||
location: "Main Studio, 123 Wellness Street",
|
||||
@@ -331,7 +332,7 @@ export function BookingManageRoute() {
|
||||
: 'Have questions or need special arrangements? Contact the business directly.'}
|
||||
</p>
|
||||
<a
|
||||
href={`mailto:support@bookra.eu?subject=Booking ${b().reference}`}
|
||||
href={`mailto:${b().businessEmail || 'support@bookra.eu'}?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'}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Show, createSignal } from "solid-js";
|
||||
import { Show, createSignal, Match, Switch } from "solid-js";
|
||||
import { useI18n } from "../providers/i18n-provider";
|
||||
import { BookraCharacter } from "../components/bookra-character";
|
||||
import {
|
||||
@@ -17,38 +17,48 @@ export function ContactRoute() {
|
||||
const [message, setMessage] = createSignal("");
|
||||
const [submitted, setSubmitted] = createSignal(false);
|
||||
const [submitting, setSubmitting] = createSignal(false);
|
||||
const [error, setError] = createSignal("");
|
||||
|
||||
const apiUrl = import.meta.env.VITE_BOOKRA_API_URL ?? "http://localhost:8080";
|
||||
|
||||
const handleSubmit = async (e: Event) => {
|
||||
e.preventDefault();
|
||||
setError("");
|
||||
setSubmitting(true);
|
||||
// Simulate API call
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
setSubmitting(false);
|
||||
setSubmitted(true);
|
||||
|
||||
try {
|
||||
const res = await fetch(`${apiUrl}/v1/public/contact`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
name: name(),
|
||||
email: email(),
|
||||
message: message(),
|
||||
}),
|
||||
});
|
||||
if (!res.ok) throw new Error("Failed to send");
|
||||
setSubmitted(true);
|
||||
} catch {
|
||||
setError(i18n.t("contact.error.body"));
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
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="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" />
|
||||
<span class="w-2 h-2 rounded-full bg-success" />
|
||||
{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>
|
||||
@@ -59,162 +69,147 @@ export function ContactRoute() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Contact Form Section */}
|
||||
{/* Story + Form split */}
|
||||
<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 class="max-w-5xl mx-auto grid lg:grid-cols-[1fr_1.2fr] gap-12 lg:gap-16 items-start">
|
||||
{/* Story side */}
|
||||
<div class="space-y-8">
|
||||
<div>
|
||||
<h2 class="text-display-sm font-semibold text-ink mb-4">
|
||||
{i18n.t("contact.story.heading")}
|
||||
</h2>
|
||||
<div class="space-y-4 text-ink-muted leading-relaxed">
|
||||
<p>{i18n.t("contact.story.p1")}</p>
|
||||
<p>{i18n.t("contact.story.p2")}</p>
|
||||
</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>
|
||||
</div>
|
||||
|
||||
{/* 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:hello@bookra.cz"
|
||||
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"/>
|
||||
<div class="grid sm:grid-cols-2 gap-4">
|
||||
<Card class="surface-elevated group hover:shadow-lg transition-all">
|
||||
<CardContent class="p-5">
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="w-10 h-10 rounded-lg 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="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="text-accent">
|
||||
<path d="M22 17a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V9.5A2.5 2.5 0 0 1 4.5 7h15A2.5 2.5 0 0 1 22 9.5z"/>
|
||||
<polyline points="22 9.5 12 14 2 9.5"/>
|
||||
</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>
|
||||
<h3 class="font-display font-semibold text-ink text-sm mb-1">{i18n.t("contact.info.email.title")}</h3>
|
||||
<a href={`mailto:${i18n.t("contact.email.address")}`} class="text-accent text-sm font-medium hover:underline">
|
||||
{i18n.t("contact.email.address")}
|
||||
</a>
|
||||
</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">
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card class="surface-elevated group hover:shadow-lg transition-all">
|
||||
<CardContent class="p-5">
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="w-10 h-10 rounded-lg 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="20" height="20" 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 text-sm mb-1">{i18n.t("contact.info.hours.title")}</h3>
|
||||
<p class="text-ink-muted text-xs">{i18n.t("contact.info.hours.desc")}</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-4 surface-elevated px-5 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'
|
||||
{i18n.locale() === 'cs'
|
||||
? 'Odpovídáme obvykle do 24 hodin'
|
||||
: 'We usually respond within 24 hours'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Form side */}
|
||||
<div>
|
||||
<Switch>
|
||||
<Match when={submitted()}>
|
||||
<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>
|
||||
<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>
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<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-5">
|
||||
<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
|
||||
minLength={10}
|
||||
placeholder={i18n.locale() === 'cs' ? "Napište nám, jak vám můžeme pomoci..." : "Tell us how we can help you..."}
|
||||
/>
|
||||
<Show when={error()}>
|
||||
<p class="text-sm text-danger">{error()}</p>
|
||||
</Show>
|
||||
<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>
|
||||
</Match>
|
||||
</Switch>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
||||
import { A } from "@solidjs/router";
|
||||
import { createSignal, onMount, createMemo } from "solid-js";
|
||||
import { createSignal, onMount, createMemo, Show, For } from "solid-js";
|
||||
import { useI18n } from "../providers/i18n-provider";
|
||||
import { BookraCharacter } from "../components/bookra-character";
|
||||
|
||||
@@ -107,6 +107,105 @@ const StepCard = (props: StepCardProps) => (
|
||||
// Main home route component
|
||||
export function HomeRoute() {
|
||||
const i18n = useI18n();
|
||||
const [billingInterval, setBillingInterval] = createSignal<"monthly" | "yearly">("monthly");
|
||||
|
||||
const isYearly = () => billingInterval() === "yearly";
|
||||
const isCs = () => i18n.locale() === "cs";
|
||||
|
||||
const ComparisonValue = (props: { value: string; highlight?: boolean }) => {
|
||||
const v = props.value.toLowerCase();
|
||||
const yesLabel = i18n.t("pricing.compare.yes").toLowerCase();
|
||||
const noLabel = i18n.t("pricing.compare.no").toLowerCase();
|
||||
if (v === "yes" || v === yesLabel) {
|
||||
return (
|
||||
<span class={`inline-flex items-center justify-center w-6 h-6 rounded-full ${props.highlight ? 'bg-accent/15 text-accent' : 'bg-success/15 text-success'}`}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M20 6 9 17l-5-5"/>
|
||||
</svg>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
if (v === "no" || v === noLabel) {
|
||||
return (
|
||||
<span class="inline-flex items-center justify-center w-6 h-6 rounded-full bg-ink/5 text-ink-muted">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M18 6 6 18"/>
|
||||
<path d="m6 6 12 12"/>
|
||||
</svg>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<span class={`text-sm font-medium ${props.highlight ? 'text-accent' : 'text-ink'}`}>
|
||||
{props.value}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
// Pricing data with monthly and yearly options
|
||||
const plans = createMemo(() => [
|
||||
{
|
||||
id: "starter",
|
||||
name: isCs() ? "Starter" : "Starter",
|
||||
desc: isCs() ? "Pro jednotlivce a malé podniky" : "For individuals and small businesses",
|
||||
monthly: isCs() ? "199 Kč" : "$9",
|
||||
yearly: isCs() ? "1 990 Kč" : "$90",
|
||||
period: isCs() ? "/měsíc" : "/mo",
|
||||
yearlyPeriod: isCs() ? "/rok" : "/yr",
|
||||
savings: isCs() ? "Ušetřete 398 Kč" : "Save $18",
|
||||
savingsPercent: "17%",
|
||||
trial: isCs() ? "15 dní zdarma po registraci" : "15 days free after sign-up",
|
||||
features: [
|
||||
isCs() ? "Do 50 rezervací/měsíc" : "Up to 50 bookings/month",
|
||||
isCs() ? "1 lokace, 1 zaměstnanec" : "1 location, 1 staff member",
|
||||
isCs() ? "E-mailová podpora" : "Email support",
|
||||
],
|
||||
cta: isCs() ? "Začít zdarma" : "Start for free",
|
||||
popular: false,
|
||||
},
|
||||
{
|
||||
id: "pro",
|
||||
name: isCs() ? "Pro" : "Pro",
|
||||
desc: isCs() ? "Pro rostoucí podniky" : "For growing businesses",
|
||||
monthly: isCs() ? "399 Kč" : "$19",
|
||||
yearly: isCs() ? "3 990 Kč" : "$190",
|
||||
period: isCs() ? "/měsíc" : "/mo",
|
||||
yearlyPeriod: isCs() ? "/rok" : "/yr",
|
||||
savings: isCs() ? "Ušetřete 798 Kč" : "Save $38",
|
||||
savingsPercent: "17%",
|
||||
trial: isCs() ? "15 dní zdarma po registraci" : "15 days free after sign-up",
|
||||
features: [
|
||||
isCs() ? "Neomezené rezervace" : "Unlimited bookings",
|
||||
isCs() ? "3 lokace, 10 zaměstnanců" : "3 locations, 10 staff",
|
||||
isCs() ? "E-mailová připomenutí" : "Email reminders",
|
||||
isCs() ? "Prioritní podpora" : "Priority support",
|
||||
isCs() ? "Analytika a reporty" : "Analytics & reports",
|
||||
],
|
||||
cta: isCs() ? "Začít 15denní zkoušku" : "Start 15-day trial",
|
||||
popular: true,
|
||||
},
|
||||
{
|
||||
id: "business",
|
||||
name: isCs() ? "Business" : "Business",
|
||||
desc: isCs() ? "Pro větší týmy a franšízy" : "For larger teams and franchises",
|
||||
monthly: isCs() ? "799 Kč" : "$39",
|
||||
yearly: isCs() ? "7 990 Kč" : "$390",
|
||||
period: isCs() ? "/měsíc" : "/mo",
|
||||
yearlyPeriod: isCs() ? "/rok" : "/yr",
|
||||
savings: isCs() ? "Ušetřete 1 598 Kč" : "Save $78",
|
||||
savingsPercent: "17%",
|
||||
trial: isCs() ? "Individuální řešení na míru" : "Custom enterprise solutions",
|
||||
features: [
|
||||
isCs() ? "Neomezené vše" : "Unlimited everything",
|
||||
isCs() ? "Více lokací" : "Multiple locations",
|
||||
isCs() ? "API přístup" : "API access",
|
||||
isCs() ? "Dedikovaný manažer" : "Dedicated manager",
|
||||
],
|
||||
cta: isCs() ? "Kontaktovat prodej" : "Contact sales",
|
||||
popular: false,
|
||||
},
|
||||
]);
|
||||
|
||||
const [isVisible, setIsVisible] = createSignal(false);
|
||||
const [currentDate, setCurrentDate] = createSignal(new Date());
|
||||
|
||||
@@ -512,125 +611,168 @@ export function HomeRoute() {
|
||||
{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"
|
||||
|
||||
{/* Billing Toggle */}
|
||||
<div class="flex items-center justify-center mb-12 animate-slide-up" style={{ "animation-delay": "0.25s" }}>
|
||||
<div class="relative inline-flex items-center gap-4">
|
||||
<span class={`text-sm font-semibold transition-colors ${!isYearly() ? 'text-ink' : 'text-ink-muted'}`}>
|
||||
{isCs() ? "Měsíčně" : "Monthly"}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setBillingInterval(isYearly() ? "monthly" : "yearly")}
|
||||
class={`relative w-14 h-7 rounded-full transition-colors duration-300 cursor-pointer ${isYearly() ? 'bg-accent' : 'bg-ink/30'}`}
|
||||
role="switch"
|
||||
aria-checked={isYearly()}
|
||||
aria-label={isYearly() ? (isCs() ? "Ročně" : "Yearly") : (isCs() ? "Měsíčně" : "Monthly")}
|
||||
>
|
||||
{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
|
||||
class={`absolute top-1 left-1 w-5 h-5 bg-white rounded-full shadow-md transition-transform duration-300 ease-spring ${isYearly() ? 'translate-x-7' : 'translate-x-0'}`}
|
||||
/>
|
||||
</button>
|
||||
<span class={`text-sm font-semibold transition-colors ${isYearly() ? 'text-ink' : 'text-ink-muted'}`}>
|
||||
{isCs() ? "Ročně" : "Yearly"}
|
||||
</span>
|
||||
<div class="absolute left-full ml-3 top-1/2 -translate-y-1/2">
|
||||
<Show when={isYearly()}>
|
||||
<span class="inline-flex items-center gap-1 px-2 py-0.5 bg-accent text-white text-xs font-bold rounded-full shadow-sm whitespace-nowrap">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="m12 3-1.912 5.813a2 2 0 0 1-1.275 1.275L3 12l5.813 1.912a2 2 0 0 1 1.275 1.275L12 21l1.912-5.813a2 2 0 0 1 1.275-1.275L21 12l-5.813-1.912a2 2 0 0 1-1.275-1.275L12 3Z"/>
|
||||
</svg>
|
||||
{isCs() ? "-17%" : "-17%"}
|
||||
</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>
|
||||
</Show>
|
||||
</div>
|
||||
</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"
|
||||
<div class="grid md:grid-cols-3 gap-6 lg:gap-8 max-w-5xl mx-auto items-center">
|
||||
{plans().map((plan, index) => (
|
||||
<div
|
||||
class={`group relative p-6 lg:p-8 rounded-card transition-all duration-500 animate-slide-up ${plan.popular ? 'hover:shadow-2xl hover:-translate-y-2 lg:scale-105' : 'hover:shadow-xl hover:-translate-y-1'}`}
|
||||
style={{ "animation-delay": `${0.3 + index * 0.1}s` }}
|
||||
>
|
||||
{i18n.t("home.pricing.biz.cta")}
|
||||
</A>
|
||||
{/* Gradient background for popular card */}
|
||||
<Show when={plan.popular}>
|
||||
<div class="absolute inset-0 bg-gradient-to-b from-ink to-ink/95 rounded-card" />
|
||||
</Show>
|
||||
<Show when={!plan.popular}>
|
||||
<div class="absolute inset-0 surface-elevated rounded-card" />
|
||||
</Show>
|
||||
|
||||
{/* Popular badge */}
|
||||
<Show when={plan.popular}>
|
||||
<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>
|
||||
</Show>
|
||||
|
||||
<div class="relative z-10">
|
||||
<div class="mb-6">
|
||||
<h3 class={`font-display text-lg font-semibold mb-1 ${plan.popular ? 'text-canvas' : 'text-ink'}`}>
|
||||
{plan.name}
|
||||
</h3>
|
||||
<p class={plan.popular ? 'text-canvas/70' : 'text-ink-muted'}>{plan.desc}</p>
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<div class="flex items-baseline gap-1">
|
||||
<span class={`font-display text-4xl font-semibold ${plan.popular ? 'text-canvas' : 'text-ink'}`}>
|
||||
{isYearly() ? plan.yearly : plan.monthly}
|
||||
</span>
|
||||
<span class={plan.popular ? 'text-canvas/60' : 'text-ink-muted'}>
|
||||
{isYearly() ? plan.yearlyPeriod : plan.period}
|
||||
</span>
|
||||
</div>
|
||||
<Show when={isYearly()}>
|
||||
<div class="mt-2 flex items-center gap-1.5">
|
||||
<span class="inline-flex items-center gap-1 px-2 py-0.5 bg-accent-subtle text-accent text-xs font-semibold rounded-full border border-accent/10">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="m12 3-1.912 5.813a2 2 0 0 1-1.275 1.275L3 12l5.813 1.912a2 2 0 0 1 1.275 1.275L12 21l1.912-5.813a2 2 0 0 1 1.275-1.275L21 12l-5.813-1.912a2 2 0 0 1-1.275-1.275L12 3Z"/></svg>
|
||||
{plan.savingsPercent}
|
||||
</span>
|
||||
<span class="text-xs text-ink-subtle">{isCs() ? 'sleva při roční platbě' : 'discount on yearly billing'}</span>
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={!isYearly()}>
|
||||
<p class="mt-1 text-xs text-accent font-medium">{plan.trial}</p>
|
||||
</Show>
|
||||
</div>
|
||||
<ul class="space-y-3 mb-8">
|
||||
{plan.features.map((feature) => (
|
||||
<li class={`flex items-start gap-3 ${plan.popular ? 'text-canvas/80' : '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 w-full ${plan.popular ? 'bg-canvas text-ink hover:bg-canvas-subtle shadow-lg group-hover:shadow-xl' : 'btn-secondary'}`}
|
||||
>
|
||||
{plan.cta}
|
||||
</A>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Comparison Table */}
|
||||
<section class="py-16 px-4">
|
||||
<div class="section-container">
|
||||
<div class="max-w-4xl mx-auto">
|
||||
<div class="text-center mb-10">
|
||||
<span class="text-sm font-medium text-accent mb-2 block tracking-wide uppercase">
|
||||
{i18n.t("pricing.compare.eyebrow")}
|
||||
</span>
|
||||
<h2 class="text-2xl sm:text-3xl font-display font-bold text-ink">
|
||||
{i18n.t("pricing.compare.title")}
|
||||
</h2>
|
||||
</div>
|
||||
<div class="surface-elevated rounded-card overflow-hidden border border-border/50 shadow-sm">
|
||||
{/* Header */}
|
||||
<div class="grid grid-cols-[1fr_100px_100px_100px] sm:grid-cols-[1.5fr_1fr_1fr_1fr] gap-2 p-4 sm:p-5 bg-canvas-subtle/60 border-b border-border/50">
|
||||
<div class="text-sm font-semibold text-ink-muted self-center">{i18n.t("pricing.compare.feature")}</div>
|
||||
<div class="text-center font-display font-semibold text-ink text-sm">Starter</div>
|
||||
<div class="text-center">
|
||||
<span class="inline-block px-3 py-1 bg-accent/10 text-accent font-display font-semibold text-sm rounded-full">
|
||||
Pro
|
||||
</span>
|
||||
</div>
|
||||
<div class="text-center font-display font-semibold text-ink text-sm">Business</div>
|
||||
</div>
|
||||
{/* Rows */}
|
||||
<For each={[
|
||||
{ key: "pricing.compare.locations", starter: "1", pro: "3", business: "∞" },
|
||||
{ key: "pricing.compare.staff", starter: "1", pro: "10", business: "∞" },
|
||||
{ key: "pricing.compare.bookings", starter: "50", pro: "∞", business: "∞" },
|
||||
{ key: "pricing.compare.emailSupport", starter: i18n.t("pricing.compare.yes"), pro: i18n.t("pricing.compare.priority"), business: i18n.t("pricing.compare.dedicated") },
|
||||
{ key: "pricing.compare.reminders", starter: "no", pro: "yes", business: "yes" },
|
||||
{ key: "pricing.compare.analytics", starter: "no", pro: "yes", business: i18n.t("pricing.compare.advanced") },
|
||||
{ key: "pricing.compare.api", starter: "no", pro: "no", business: "yes" },
|
||||
{ key: "pricing.compare.branding", starter: "no", pro: "yes", business: "yes" },
|
||||
{ key: "pricing.compare.whiteLabel", starter: "no", pro: "no", business: "yes" },
|
||||
{ key: "pricing.compare.manager", starter: "no", pro: "no", business: "yes" },
|
||||
]}>
|
||||
{(feature, i) => (
|
||||
<div class={`grid grid-cols-[1fr_100px_100px_100px] sm:grid-cols-[1.5fr_1fr_1fr_1fr] gap-2 p-4 sm:p-5 items-center border-b border-border/30 last:border-0 transition-colors ${i() % 2 === 0 ? 'bg-canvas/30' : ''} hover:bg-canvas-subtle/40`}>
|
||||
<span class="text-sm text-ink font-medium">{i18n.t(feature.key)}</span>
|
||||
<div class="flex justify-center">
|
||||
<ComparisonValue value={feature.starter} />
|
||||
</div>
|
||||
<div class="flex justify-center">
|
||||
<ComparisonValue value={feature.pro} highlight />
|
||||
</div>
|
||||
<div class="flex justify-center">
|
||||
<ComparisonValue value={feature.business} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -9,29 +9,80 @@ export function LegalRoute() {
|
||||
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"),
|
||||
},
|
||||
];
|
||||
const isCs = () => i18n.locale() === "cs";
|
||||
|
||||
const companyInfo = () =>
|
||||
isCs()
|
||||
? "Provozovatel: Bookra, IČO 24330621. Sídlo: Česká republika."
|
||||
: "Operator: Bookra, Business ID 24330621. Registered in the Czech Republic.";
|
||||
|
||||
const termsSections = () => [
|
||||
{
|
||||
title: isCs() ? "1. Úvod a předmět smlouvy" : "1. Introduction and subject",
|
||||
body: isCs()
|
||||
? "Tyto podmínky upravují používání služby Bookra — online rezervačního systému pro lokální služby. Poskytovatelem je Bookra , IČO 24330621. Službu mohou využívat podnikatelé a právnické osoby k správě rezervací, zákazníků a dostupnosti. Uživatel se zavazuje používat službu v souladu s právními předpisy, bez zneužívání rezervačních formulářů, obcházení zabezpečení nebo ukládání zakázaného obsahu."
|
||||
: "These terms govern the use of Bookra — an online booking system for local services. The provider is Bookra , Business ID 24330621. Entrepreneurs and legal entities may use the service to manage bookings, customers, and availability. The user agrees to use the service lawfully, without abusing booking forms, bypassing security, or storing prohibited content.",
|
||||
},
|
||||
{
|
||||
title: isCs() ? "2. Registrace a účet" : "2. Registration and account",
|
||||
body: isCs()
|
||||
? "Pro plné využití služby je nutná registrace. Uživatel zodpovídá za správnost údajů uvedených při registraci a za bezpečnost přihlašovacích údajů. Provozovatel účtu odpovídá za správnost nabídky, dostupnost termínů a komunikaci se zákazníky. Bookra nezodpovídá za obsah rezervací a komunikaci mezi provozovatelem a zákazníkem."
|
||||
: "Full use of the service requires registration. The user is responsible for the accuracy of registration details and the security of login credentials. The workspace operator is responsible for the accuracy of their offer, availability of times, and customer communication. Bookra is not liable for booking content or communication between operators and customers.",
|
||||
},
|
||||
{
|
||||
title: isCs() ? "3. Předplatné a platby" : "3. Subscription and payments",
|
||||
body: isCs()
|
||||
? "Placené plány se účtují předem prostřednictvím platební brány Paddle nebo Stripe. Aktivní plán určuje dostupné limity, rozšíření a podpůrné funkce. Při roční platbě je poskytována sleva oproti měsíčnímu zúčtování. Uživatel může předplatné kdykoliv zrušit; přístup zůstává do konce zaplaceného období. Neposkytujeme refundace za již zaplacená období."
|
||||
: "Paid plans are billed in advance through Paddle or Stripe. The active plan determines available limits, add-ons, and support features. Annual billing includes a discount compared to monthly billing. Users may cancel anytime; access continues until the end of the paid period. No refunds are provided for already-paid periods.",
|
||||
},
|
||||
{
|
||||
title: isCs() ? "4. Odpovědnost a omezení" : "4. Liability and limitations",
|
||||
body: isCs()
|
||||
? "Bookra se snaží zajistit nepřetržitý provoz, ale nezaručuje 100% dostupnost. Neneseme odpovědnost za přímé ani nepřímé škody způsobené výpadkem služby, ztrátou dat způsobenou uživatelem nebo technickými problémy třetích stran. Doporučujeme pravidelnou zálohu důležitých dat."
|
||||
: "Bookra strives to ensure uninterrupted service but does not guarantee 100% uptime. We are not liable for direct or indirect damages caused by service outages, data loss caused by the user, or technical issues from third parties. We recommend regular backups of important data.",
|
||||
},
|
||||
{
|
||||
title: isCs() ? "5. Ukončení a výpověď" : "5. Termination",
|
||||
body: isCs()
|
||||
? "Uživatel může účet zrušit kdykoliv v nastavení. Při dlouhodobé neaktivitě (12 měsíců bez přihlášení) si vyhrazujeme právo účet deaktivovat po předchozím upozornění. Při porušení podmínek může být účet ukončen okamžitě."
|
||||
: "Users may cancel their account anytime in settings. After prolonged inactivity (12 months without login), we reserve the right to deactivate the account after prior notice. Accounts violating these terms may be terminated immediately.",
|
||||
},
|
||||
];
|
||||
|
||||
const privacySections = () => [
|
||||
{
|
||||
title: isCs() ? "1. Jaké údaje zpracováváme a proč" : "1. What data we process and why",
|
||||
body: isCs()
|
||||
? "Zpracováváme minimální množství dat nezbytných pro fungování služby: kontaktní údaje zákazníků (jméno, e-mail) pro potvrzení rezervace, čas rezervace a poznámky zadané při rezervaci, údaje o účtu provozovatele (e-mail, jméno) pro správu účtu, a technické záznamy (IP adresa, čas požadavku) pro zabezpečení. Údaje tenantů jsou oddělené a přístup k nim je omezen podle role uživatele."
|
||||
: "We process the minimum data necessary for the service: customer contact details (name, email) for booking confirmation, booking times and notes, workspace account details (email, name) for account management, and technical records (IP address, request time) for security. Tenant data is isolated and access is limited by user role.",
|
||||
},
|
||||
{
|
||||
title: isCs() ? "2. Cookies a sledování" : "2. Cookies and tracking",
|
||||
body: isCs()
|
||||
? "Bookra nepoužívá žádné sledovací cookies pro marketingové ani analytické účely. Jediné cookies, které ukládáme, jsou technicky nezbytné pro přihlášení a správu relace. Pro anonymní statistiky využíváme Rybbit — nástroj, který pracuje bez cookies a neukládá osobní údaje návštěvníků."
|
||||
: "Bookra does not use any tracking cookies for marketing or analytics purposes. The only cookies we store are technically necessary for login and session management. For anonymous statistics, we use Rybbit — a tool that operates without cookies and does not store visitors' personal data.",
|
||||
},
|
||||
{
|
||||
title: isCs() ? "3. Údaje registrovaných uživatelů" : "3. Registered user data",
|
||||
body: isCs()
|
||||
? "Při registraci shromažďujeme e-mailovou adresu a jméno uživatele. Tato data slouží výhradně k autentizaci, správě účtu a komunikaci ohledně služby (připomenutí, oznámení o změnách). Vaše data neprodáváme, nepronajímáme a nesdílíme s třetími stranami pro marketingové účely. Přístup mají pouze oprávnění zaměstnanci Bookry a to pouze v nezbytném rozsahu pro technickou podporu."
|
||||
: "During registration, we collect the user's email address and name. This data is used solely for authentication, account management, and service-related communication (reminders, change notifications). We do not sell, rent, or share your data with third parties for marketing purposes. Only authorized Bookra employees have access, and only to the extent necessary for technical support.",
|
||||
},
|
||||
{
|
||||
title: isCs() ? "4. Práva a žádosti" : "4. Rights and requests",
|
||||
body: isCs()
|
||||
? "V souladu s GDPR máte právo na přístup ke svým údajům, jejich opravu, výmaz nebo omezení zpracování. Žádosti o přístup, opravu nebo výmaz údajů řeší provozovatel konkrétního účtu. Bookra poskytuje technické prostředky pro bezpečné zpracování. Svá práva můžete uplatnit e-mailem na hello@bookra.eu."
|
||||
: "In accordance with GDPR, you have the right to access, correct, delete, or restrict processing of your data. Access, correction, and deletion requests are handled by the operator of the relevant workspace. Bookra provides the technical system for secure processing. You may exercise your rights by emailing hello@bookra.eu.",
|
||||
},
|
||||
{
|
||||
title: isCs() ? "5. Doba uchování a zabezpečení" : "5. Retention and security",
|
||||
body: isCs()
|
||||
? "Rezervační údaje uchováváme po dobu existence účtu provozovatele, pokud není smazány dříve. Technické záznamy uchováváme po dobu 90 dnů. Všechna data jsou přenášena šifrovaně (TLS), uchovávána v zabezpečených datových centrech v EU a pravidelně zálohována."
|
||||
: "Booking data is retained for the lifetime of the operator's account unless deleted earlier. Technical records are kept for 90 days. All data is transmitted encrypted (TLS), stored in secure EU data centers, and regularly backed up.",
|
||||
},
|
||||
];
|
||||
|
||||
const sections = () => (kind() === "terms" ? termsSections() : privacySections());
|
||||
|
||||
return (
|
||||
<section class="section-container py-16">
|
||||
@@ -42,23 +93,32 @@ export function LegalRoute() {
|
||||
{i18n.t(`legal.${kind()}.title`)}
|
||||
</h1>
|
||||
<p class="text-lg text-ink-muted">{i18n.t(`legal.${kind()}.body`)}</p>
|
||||
<p class="text-sm text-ink-subtle">{companyInfo()}</p>
|
||||
</div>
|
||||
|
||||
<For each={sections()}>
|
||||
{(section) => (
|
||||
<Card class="surface-elevated">
|
||||
{(section, i) => (
|
||||
<Card class="surface-elevated hover:shadow-md transition-shadow">
|
||||
<CardHeader>
|
||||
<CardTitle>{section.title}</CardTitle>
|
||||
<CardTitle class="text-lg">{section.title}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p class="text-ink-muted">{section.body}</p>
|
||||
<p class="text-ink-muted leading-relaxed">{section.body}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</For>
|
||||
|
||||
<div class="pt-4 border-t border-border">
|
||||
<p class="text-sm text-ink-subtle">
|
||||
{isCs()
|
||||
? "Poslední aktualizace: květen 2026. V případě dotazů nás kontaktujte na hello@bookra.eu."
|
||||
: "Last updated: May 2026. For questions, contact us at hello@bookra.eu."}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<aside class="lg:sticky lg:top-24">
|
||||
<aside class="lg:sticky lg:top-24 h-fit space-y-6">
|
||||
<Card class="surface-elevated overflow-hidden">
|
||||
<CardContent class="p-8 text-center">
|
||||
<div class="mb-6 flex justify-center">
|
||||
@@ -71,15 +131,28 @@ export function LegalRoute() {
|
||||
/>
|
||||
<p class="text-sm leading-relaxed text-ink-muted">
|
||||
{kind() === "terms"
|
||||
? i18n.locale() === "cs"
|
||||
? isCs()
|
||||
? "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"
|
||||
: isCs()
|
||||
? "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} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card class="surface-elevated">
|
||||
<CardContent class="p-6">
|
||||
<h3 class="font-display font-semibold text-ink text-sm mb-3">
|
||||
{isCs() ? "Kontakt" : "Contact"}
|
||||
</h3>
|
||||
<div class="space-y-2 text-sm text-ink-muted">
|
||||
<p>Bookra </p>
|
||||
<p>IČO: 24330621</p>
|
||||
<p>Česká republika</p>
|
||||
<a href="mailto:hello@bookra.eu" class="text-accent hover:underline block mt-2">
|
||||
hello@bookra.eu
|
||||
</a>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -0,0 +1,424 @@
|
||||
import { createSignal, createMemo, Show, For } from "solid-js";
|
||||
import { useNavigate } from "@solidjs/router";
|
||||
import { useI18n } from "../providers/i18n-provider";
|
||||
|
||||
const PricingRoute = () => {
|
||||
const navigate = useNavigate();
|
||||
const { locale, toggleLocale } = useI18n();
|
||||
const isCs = () => locale() === "cs";
|
||||
const [billingInterval, setBillingInterval] = createSignal<"monthly" | "yearly">("monthly");
|
||||
const isYearly = () => billingInterval() === "yearly";
|
||||
const [openFaq, setOpenFaq] = createSignal<number | null>(0);
|
||||
|
||||
const plans = createMemo(() => [
|
||||
{
|
||||
id: "starter",
|
||||
name: "Starter",
|
||||
desc: isCs() ? "Pro jednotlivce a malé podniky" : "For individuals and small businesses",
|
||||
monthly: isCs() ? "199 Kč" : "$9",
|
||||
yearly: isCs() ? "1 990 Kč" : "$90",
|
||||
period: isCs() ? "/měsíc" : "/mo",
|
||||
yearlyPeriod: isCs() ? "/rok" : "/yr",
|
||||
savings: isCs() ? "Ušetřete 398 Kč" : "Save $18",
|
||||
savingsPercent: "17%",
|
||||
trial: isCs() ? "15 dní zdarma po registraci" : "15 days free after sign-up",
|
||||
features: [
|
||||
isCs() ? "Do 50 rezervací/měsíc" : "Up to 50 bookings/month",
|
||||
isCs() ? "1 lokace, 1 zaměstnanec" : "1 location, 1 staff member",
|
||||
isCs() ? "E-mailová podpora" : "Email support",
|
||||
isCs() ? "Základní rezervační widget" : "Basic booking widget",
|
||||
isCs() ? "Potvrzení e-mailem" : "Email confirmations",
|
||||
],
|
||||
cta: isCs() ? "Začít zdarma" : "Start for free",
|
||||
popular: false,
|
||||
},
|
||||
{
|
||||
id: "pro",
|
||||
name: "Pro",
|
||||
desc: isCs() ? "Pro rostoucí podniky" : "For growing businesses",
|
||||
monthly: isCs() ? "399 Kč" : "$19",
|
||||
yearly: isCs() ? "3 990 Kč" : "$190",
|
||||
period: isCs() ? "/měsíc" : "/mo",
|
||||
yearlyPeriod: isCs() ? "/rok" : "/yr",
|
||||
savings: isCs() ? "Ušetřete 798 Kč" : "Save $38",
|
||||
savingsPercent: "17%",
|
||||
trial: isCs() ? "15 dní zdarma po registraci" : "15 days free after sign-up",
|
||||
features: [
|
||||
isCs() ? "Neomezené rezervace" : "Unlimited bookings",
|
||||
isCs() ? "3 lokace, 10 zaměstnanců" : "3 locations, 10 staff",
|
||||
isCs() ? "E-mailová připomenutí" : "Email reminders",
|
||||
isCs() ? "Prioritní podpora" : "Priority support",
|
||||
isCs() ? "Analytika a reporty" : "Analytics & reports",
|
||||
isCs() ? "Vlastní branding widgetu" : "Custom widget branding",
|
||||
isCs() ? "Rozšířené nastavení dostupnosti" : "Advanced availability",
|
||||
],
|
||||
cta: isCs() ? "Začít 15denní zkoušku" : "Start 15-day trial",
|
||||
popular: true,
|
||||
},
|
||||
{
|
||||
id: "business",
|
||||
name: "Business",
|
||||
desc: isCs() ? "Pro větší týmy a franšízy" : "For larger teams and franchises",
|
||||
monthly: isCs() ? "799 Kč" : "$39",
|
||||
yearly: isCs() ? "7 990 Kč" : "$390",
|
||||
period: isCs() ? "/měsíc" : "/mo",
|
||||
yearlyPeriod: isCs() ? "/rok" : "/yr",
|
||||
savings: isCs() ? "Ušetřete 1 598 Kč" : "Save $78",
|
||||
savingsPercent: "17%",
|
||||
trial: "",
|
||||
features: [
|
||||
isCs() ? "Neomezené vše" : "Unlimited everything",
|
||||
isCs() ? "Neomezené lokace a zaměstnanci" : "Unlimited locations & staff",
|
||||
isCs() ? "API přístup" : "API access",
|
||||
isCs() ? "Dedikovaný manažer" : "Dedicated manager",
|
||||
isCs() ? "Bílý labeling" : "White labeling",
|
||||
isCs() ? "Pokročilá analytika" : "Advanced analytics",
|
||||
isCs() ? "Integrace s externími systémy" : "External system integrations",
|
||||
],
|
||||
cta: isCs() ? "Kontaktovat nás" : "Contact us",
|
||||
popular: false,
|
||||
},
|
||||
]);
|
||||
|
||||
const handleSelectPlan = (planId: string) => {
|
||||
// Redirect to signup with plan selection
|
||||
navigate("/?signup=true&plan=" + planId + "&billing=" + billingInterval());
|
||||
};
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const comparisonFeatures = [
|
||||
{ key: "pricing.compare.locations", starter: "1", pro: "3", business: "∞" },
|
||||
{ key: "pricing.compare.staff", starter: "1", pro: "10", business: "∞" },
|
||||
{ key: "pricing.compare.bookings", starter: "50", pro: "∞", business: "∞" },
|
||||
{ key: "pricing.compare.emailSupport", starter: t("pricing.compare.yes"), pro: t("pricing.compare.priority"), business: t("pricing.compare.dedicated") },
|
||||
{ key: "pricing.compare.reminders", starter: "no", pro: "yes", business: "yes" },
|
||||
{ key: "pricing.compare.analytics", starter: "no", pro: "yes", business: t("pricing.compare.advanced") },
|
||||
{ key: "pricing.compare.api", starter: "no", pro: "no", business: "yes" },
|
||||
{ key: "pricing.compare.branding", starter: "no", pro: "yes", business: "yes" },
|
||||
{ key: "pricing.compare.whiteLabel", starter: "no", pro: "no", business: "yes" },
|
||||
{ key: "pricing.compare.manager", starter: "no", pro: "no", business: "yes" },
|
||||
];
|
||||
|
||||
const ComparisonValue = (props: { value: string; highlight?: boolean }) => {
|
||||
const v = props.value.toLowerCase();
|
||||
if (v === "yes" || v === t("pricing.compare.yes").toLowerCase()) {
|
||||
return (
|
||||
<span class={`inline-flex items-center justify-center w-6 h-6 rounded-full ${props.highlight ? 'bg-accent/15 text-accent' : 'bg-success/15 text-success'}`}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M20 6 9 17l-5-5"/>
|
||||
</svg>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
if (v === "no" || v === t("pricing.compare.no").toLowerCase()) {
|
||||
return (
|
||||
<span class="inline-flex items-center justify-center w-6 h-6 rounded-full bg-ink/5 text-ink-muted">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M18 6 6 18"/>
|
||||
<path d="m6 6 12 12"/>
|
||||
</svg>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<span class={`text-sm font-medium ${props.highlight ? 'text-accent' : 'text-ink'}`}>
|
||||
{props.value}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
const faqs = [
|
||||
{
|
||||
enQ: "Can I cancel anytime?",
|
||||
csQ: "Mohu kdykoliv zrušit?",
|
||||
enA: "Yes, you can cancel anytime. If you have an annual subscription, you'll have access until the end of the period. No cancellation fees.",
|
||||
csA: "Ano, můžete zrušit kdykoliv. Pokud máte roční předplatné, budete mít přístup do konce období. Žádné poplatky za zrušení.",
|
||||
},
|
||||
{
|
||||
enQ: "What if I exceed my limits?",
|
||||
csQ: "Co když překročím limity?",
|
||||
enA: "You'll be notified and prompted to upgrade to a higher plan. Your data will be preserved and you can continue using Bookra seamlessly.",
|
||||
csA: "Budete upozorněni a vyzváni k upgradu na vyšší plán. Vaše data zůstanou zachována a můžete dál používat Bookra bez přerušení.",
|
||||
},
|
||||
{
|
||||
enQ: "What payment methods do you accept?",
|
||||
csQ: "Jaké platební metody přijímáte?",
|
||||
enA: "We accept all major credit cards through Stripe. Payments are secure, encrypted, and PCI compliant.",
|
||||
csA: "Přijímáme všechny hlavní kreditní karty přes Stripe. Platby jsou zabezpečené, šifrované a splňují PCI standard.",
|
||||
},
|
||||
{
|
||||
enQ: "Can I switch plans?",
|
||||
csQ: "Můžu změnit plán?",
|
||||
enA: "Yes, you can upgrade or downgrade at any time. When upgrading, we'll prorate the difference. When downgrading, the new rate applies at the next billing cycle.",
|
||||
csA: "Ano, můžete kdykoliv upgradovat nebo downgradovat. Při upgradu doplatíte poměrnou částku. Při downgradu se nová cena aplikuje od dalšího fakturačního období.",
|
||||
},
|
||||
{
|
||||
enQ: "Is there a free trial?",
|
||||
csQ: "Je k dispozici bezplatná zkouška?",
|
||||
enA: "Yes, every plan includes a 15-day free trial. No credit card required to start.",
|
||||
csA: "Ano, každý plán obsahuje 15denní bezplatnou zkoušku. Není potřeba zadávat platební kartu.",
|
||||
},
|
||||
{
|
||||
enQ: "Do you offer support?",
|
||||
csQ: "Poskytujete podporu?",
|
||||
enA: "Absolutely. Starter includes email support, Pro gets priority support, and Business includes a dedicated account manager.",
|
||||
csA: "Samozřejmě. Starter obsahuje e-mailovou podporu, Pro má prioritní podporu a Business zahrnuje dedikovaného account managera.",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div class="min-h-screen bg-gradient-to-br from-canvas-subtle via-canvas to-canvas-subtle">
|
||||
{/* Hero */}
|
||||
<section class="pt-16 pb-12 sm:pt-24 sm:pb-16 text-center px-4">
|
||||
<h1 class="text-4xl sm:text-5xl font-display font-bold text-ink mb-4 animate-slide-up">
|
||||
{isCs() ? "Jednoduché a férové ceny" : "Simple, fair pricing"}
|
||||
</h1>
|
||||
<p class="text-lg text-ink-muted max-w-2xl mx-auto mb-8 animate-slide-up" style={{ "animation-delay": "0.1s" }}>
|
||||
{isCs()
|
||||
? "Vyberte si plán, který vyhovuje vašemu podnikání. Žádné skryté poplatky, žádné překvapení."
|
||||
: "Choose a plan that fits your business. No hidden fees, no surprises."}
|
||||
</p>
|
||||
|
||||
{/* Billing Toggle */}
|
||||
<div class="flex items-center justify-center mb-8 animate-slide-up" style={{ "animation-delay": "0.2s" }}>
|
||||
<div class="relative inline-flex items-center gap-4">
|
||||
<span class={`text-sm font-semibold transition-colors ${!isYearly() ? 'text-ink' : 'text-ink-muted'}`}>
|
||||
{isCs() ? "Měsíčně" : "Monthly"}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setBillingInterval(isYearly() ? "monthly" : "yearly")}
|
||||
class={`relative w-14 h-7 rounded-full transition-colors duration-300 cursor-pointer ${isYearly() ? 'bg-accent' : 'bg-ink/30'}`}
|
||||
role="switch"
|
||||
aria-checked={isYearly()}
|
||||
>
|
||||
<span class={`absolute top-1 left-1 w-5 h-5 bg-white rounded-full shadow-md transition-transform duration-300 ${isYearly() ? 'translate-x-7' : 'translate-x-0'}`} />
|
||||
</button>
|
||||
<span class={`text-sm font-semibold transition-colors ${isYearly() ? 'text-ink' : 'text-ink-muted'}`}>
|
||||
{isCs() ? "Ročně" : "Yearly"}
|
||||
</span>
|
||||
<div class="absolute left-full ml-3 top-1/2 -translate-y-1/2">
|
||||
<Show when={isYearly()}>
|
||||
<span class="inline-flex items-center gap-1 px-2 py-0.5 bg-accent text-white text-xs font-bold rounded-full shadow-sm whitespace-nowrap">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="m12 3-1.912 5.813a2 2 0 0 1-1.275 1.275L3 12l5.813 1.912a2 2 0 0 1 1.275 1.275L12 21l1.912-5.813a2 2 0 0 1 1.275-1.275L21 12l-5.813-1.912a2 2 0 0 1-1.275-1.275L12 3Z"/></svg>
|
||||
-17%
|
||||
</span>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Pricing Cards */}
|
||||
<section class="pb-16 px-4">
|
||||
<div class="grid md:grid-cols-3 gap-6 lg:gap-8 max-w-5xl mx-auto items-center">
|
||||
<For each={plans()}>
|
||||
{(plan, index) => (
|
||||
<div
|
||||
class={`group relative p-6 lg:p-8 rounded-card transition-all duration-500 animate-slide-up ${plan.popular ? 'hover:shadow-2xl hover:-translate-y-2 lg:scale-105' : 'hover:shadow-xl hover:-translate-y-1'}`}
|
||||
style={{ "animation-delay": `${0.3 + index() * 0.1}s` }}
|
||||
>
|
||||
<Show when={plan.popular}>
|
||||
<div class="absolute inset-0 bg-gradient-to-b from-ink to-ink/95 rounded-card" />
|
||||
</Show>
|
||||
<Show when={!plan.popular}>
|
||||
<div class="absolute inset-0 surface-elevated rounded-card" />
|
||||
</Show>
|
||||
|
||||
<Show when={plan.popular}>
|
||||
<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">
|
||||
{t("home.pricing.popular")}
|
||||
</span>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<div class="relative z-10">
|
||||
<div class="mb-6">
|
||||
<h3 class={`font-display text-lg font-semibold mb-1 ${plan.popular ? 'text-canvas' : 'text-ink'}`}>
|
||||
{plan.name}
|
||||
</h3>
|
||||
<p class={plan.popular ? 'text-canvas/70' : 'text-ink-muted'}>{plan.desc}</p>
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<div class="flex items-baseline gap-1">
|
||||
<span class={`font-display text-4xl font-semibold ${plan.popular ? 'text-canvas' : 'text-ink'}`}>
|
||||
{isYearly() ? plan.yearly : plan.monthly}
|
||||
</span>
|
||||
<span class={plan.popular ? 'text-canvas/60' : 'text-ink-muted'}>
|
||||
{isYearly() ? plan.yearlyPeriod : plan.period}
|
||||
</span>
|
||||
</div>
|
||||
<Show when={isYearly()}>
|
||||
<div class="mt-2 flex items-center gap-1.5">
|
||||
<span class="inline-flex items-center gap-1 px-2 py-0.5 bg-accent-subtle text-accent text-xs font-semibold rounded-full border border-accent/10">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="m12 3-1.912 5.813a2 2 0 0 1-1.275 1.275L3 12l5.813 1.912a2 2 0 0 1 1.275 1.275L12 21l1.912-5.813a2 2 0 0 1 1.275-1.275L21 12l-5.813-1.912a2 2 0 0 1-1.275-1.275L12 3Z"/></svg>
|
||||
{plan.savingsPercent}
|
||||
</span>
|
||||
<span class="text-xs text-ink-subtle">{isCs() ? 'sleva při roční platbě' : 'discount on yearly billing'}</span>
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={!isYearly() && plan.trial}>
|
||||
<p class="mt-1 text-xs text-accent font-medium">{plan.trial}</p>
|
||||
</Show>
|
||||
</div>
|
||||
<ul class="space-y-3 mb-8">
|
||||
<For each={plan.features}>
|
||||
{(feature) => (
|
||||
<li class={`flex items-start gap-3 ${plan.popular ? 'text-canvas/80' : 'text-ink-muted'}`}>
|
||||
<span class="mt-0.5 text-accent shrink-0">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M20 6 9 17l-5-5"/>
|
||||
</svg>
|
||||
</span>
|
||||
<span class="text-sm">{feature}</span>
|
||||
</li>
|
||||
)}
|
||||
</For>
|
||||
</ul>
|
||||
<button
|
||||
onClick={() => handleSelectPlan(plan.id)}
|
||||
class={`w-full py-3 px-4 rounded-xl font-semibold text-sm transition-all duration-300 ${
|
||||
plan.popular
|
||||
? 'bg-accent text-white hover:bg-accent/90 hover:shadow-lg hover:-translate-y-0.5'
|
||||
: 'bg-ink text-canvas hover:bg-ink/90 hover:shadow-lg hover:-translate-y-0.5'
|
||||
}`}
|
||||
>
|
||||
{plan.cta}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Comparison Table */}
|
||||
<section class="py-16 px-4">
|
||||
<div class="max-w-4xl mx-auto">
|
||||
<div class="text-center mb-10">
|
||||
<span class="text-sm font-medium text-accent mb-2 block tracking-wide uppercase">
|
||||
{isCs() ? "Detailní srovnání" : "Detailed comparison"}
|
||||
</span>
|
||||
<h2 class="text-2xl sm:text-3xl font-display font-bold text-ink">
|
||||
{isCs() ? "Porovnání plánů" : "Compare plans"}
|
||||
</h2>
|
||||
</div>
|
||||
<div class="surface-elevated rounded-card overflow-hidden border border-border/50 shadow-sm">
|
||||
{/* Header */}
|
||||
<div class="grid grid-cols-[1fr_100px_100px_100px] sm:grid-cols-[1.5fr_1fr_1fr_1fr] gap-2 p-4 sm:p-5 bg-canvas-subtle/60 border-b border-border/50">
|
||||
<div class="text-sm font-semibold text-ink-muted self-center">{isCs() ? "Funkce" : "Feature"}</div>
|
||||
<div class="text-center font-display font-semibold text-ink text-sm">Starter</div>
|
||||
<div class="text-center">
|
||||
<span class="inline-block px-3 py-1 bg-accent/10 text-accent font-display font-semibold text-sm rounded-full">
|
||||
Pro
|
||||
</span>
|
||||
</div>
|
||||
<div class="text-center font-display font-semibold text-ink text-sm">Business</div>
|
||||
</div>
|
||||
{/* Rows */}
|
||||
<For each={comparisonFeatures}>
|
||||
{(feature, i) => (
|
||||
<div class={`grid grid-cols-[1fr_100px_100px_100px] sm:grid-cols-[1.5fr_1fr_1fr_1fr] gap-2 p-4 sm:p-5 items-center border-b border-border/30 last:border-0 transition-colors ${i() % 2 === 0 ? 'bg-canvas/30' : ''} hover:bg-canvas-subtle/40`}>
|
||||
<span class="text-sm text-ink font-medium">{t(feature.key)}</span>
|
||||
<div class="flex justify-center">
|
||||
<ComparisonValue value={feature.starter} />
|
||||
</div>
|
||||
<div class="flex justify-center">
|
||||
<ComparisonValue value={feature.pro} highlight />
|
||||
</div>
|
||||
<div class="flex justify-center">
|
||||
<ComparisonValue value={feature.business} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* FAQ */}
|
||||
<section class="py-16 px-4 bg-canvas-subtle/30">
|
||||
<div class="max-w-2xl mx-auto">
|
||||
<div class="text-center mb-10">
|
||||
<span class="text-sm font-medium text-accent mb-2 block tracking-wide uppercase">
|
||||
{isCs() ? "Máte otázky?" : "Got questions?"}
|
||||
</span>
|
||||
<h2 class="text-2xl sm:text-3xl font-display font-bold text-ink">
|
||||
{isCs() ? "Časté dotazy" : "Frequently asked questions"}
|
||||
</h2>
|
||||
</div>
|
||||
<div class="space-y-3">
|
||||
<For each={faqs}>
|
||||
{(faq, i) => (
|
||||
<div class="surface-elevated rounded-card border border-border/40 overflow-hidden transition-all duration-300 hover:border-border/70 hover:shadow-md">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setOpenFaq(openFaq() === i() ? null : i())}
|
||||
class="w-full flex items-center justify-between p-5 text-left group"
|
||||
>
|
||||
<span class="font-semibold text-ink text-sm sm:text-base pr-4 group-hover:text-accent transition-colors">
|
||||
{isCs() ? faq.csQ : faq.enQ}
|
||||
</span>
|
||||
<span class={`shrink-0 w-8 h-8 rounded-full bg-canvas-subtle flex items-center justify-center text-ink-muted group-hover:bg-accent/10 group-hover:text-accent transition-all duration-300 ${openFaq() === i() ? 'rotate-180' : ''}`}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="m6 9 6 6 6-6"/>
|
||||
</svg>
|
||||
</span>
|
||||
</button>
|
||||
<div class={`grid transition-all duration-300 ${openFaq() === i() ? 'grid-rows-[1fr] opacity-100' : 'grid-rows-[0fr] opacity-0'}`}>
|
||||
<div class="overflow-hidden">
|
||||
<div class="px-5 pb-5 text-sm text-ink-muted leading-relaxed border-t border-border/30 pt-4">
|
||||
{isCs() ? faq.csA : faq.enA}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* CTA */}
|
||||
<section class="py-16 px-4">
|
||||
<div class="max-w-2xl mx-auto text-center">
|
||||
<h2 class="text-2xl font-display font-bold text-ink mb-4">
|
||||
{isCs() ? "Stále si nejste jistí?" : "Still not sure?"}
|
||||
</h2>
|
||||
<p class="text-ink-muted mb-6">
|
||||
{isCs()
|
||||
? "Začněte s bezplatným 15denním trial a rozhodněte se později."
|
||||
: "Start with a free 15-day trial and decide later."}
|
||||
</p>
|
||||
<a
|
||||
href="/dashboard?signup=true"
|
||||
class="inline-block px-8 py-3 bg-accent text-white font-semibold rounded-xl hover:bg-accent/90 hover:shadow-lg hover:-translate-y-0.5 transition-all duration-300"
|
||||
>
|
||||
{isCs() ? "Začít zdarma" : "Start for free"}
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Footer */}
|
||||
<footer class="border-t border-border py-8 px-4">
|
||||
<div class="max-w-6xl mx-auto flex flex-col sm:flex-row items-center justify-between gap-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="w-6 h-6 rounded bg-gradient-to-br from-accent to-accent/70 flex items-center justify-center">
|
||||
<span class="text-white font-bold text-xs">B</span>
|
||||
</div>
|
||||
<span class="text-ink-muted text-sm">© 2024 Bookra</span>
|
||||
</div>
|
||||
<nav class="flex items-center gap-6">
|
||||
<a href="/privacy" class="text-ink-muted hover:text-ink text-sm transition-colors">{isCs() ? "Ochrana soukromí" : "Privacy"}</a>
|
||||
<a href="/terms" class="text-ink-muted hover:text-ink text-sm transition-colors">{isCs() ? "Podmínky" : "Terms"}</a>
|
||||
<a href="/contact" class="text-ink-muted hover:text-ink text-sm transition-colors">{isCs() ? "Kontakt" : "Contact"}</a>
|
||||
</nav>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PricingRoute;
|
||||
@@ -16,6 +16,8 @@ export function PublicBookingRoute() {
|
||||
const [customerName, setCustomerName] = createSignal("");
|
||||
const [customerEmail, setCustomerEmail] = createSignal("");
|
||||
const [notes, setNotes] = createSignal("");
|
||||
const [highlightContact, setHighlightContact] = createSignal(false);
|
||||
let contactFormRef: HTMLDivElement | undefined;
|
||||
const [availability, { refetch }] = createResource(() => {
|
||||
const slug = tenantSlug();
|
||||
if (!slug) return null;
|
||||
@@ -31,6 +33,9 @@ export function PublicBookingRoute() {
|
||||
const bookSlot = async (slot: components["schemas"]["TimeSlot"]) => {
|
||||
if (!customerName().trim() || !customerEmail().trim()) {
|
||||
setBookingError(i18n.t("booking.customerRequired"));
|
||||
setHighlightContact(true);
|
||||
contactFormRef?.scrollIntoView({ behavior: "smooth", block: "center" });
|
||||
setTimeout(() => setHighlightContact(false), 2000);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -105,8 +110,8 @@ export function PublicBookingRoute() {
|
||||
<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" }}>
|
||||
<div class="space-y-6 order-1 lg:order-2" ref={(el) => { contactFormRef = el; }}>
|
||||
<Card class={`surface-elevated animate-slide-up transition-all duration-300 ${highlightContact() ? 'ring-2 ring-accent shadow-lg' : ''}`} 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>
|
||||
|
||||
Reference in New Issue
Block a user