This commit is contained in:
Tomas Dvorak
2026-05-05 09:48:07 +02:00
parent d854614a87
commit 48c3e15a38
295 changed files with 178381 additions and 1039 deletions
+147 -25
View File
@@ -10,12 +10,57 @@ import type { AuthSession } from "../lib/types";
const neonAuthUrl = import.meta.env.VITE_NEON_AUTH_URL ?? "";
const authClient = neonAuthUrl ? createAuthClient(neonAuthUrl) : null;
const localAuthTokenKey = "bookra.localAuthToken";
type LocalUser = {
id: string;
email?: string;
name?: string;
};
function sessionFromLocalToken(token: string, user?: LocalUser): AuthSession {
const payload = parseJwtPayload(token);
const expiresAt = typeof payload.exp === "number" ? new Date(payload.exp * 1000) : undefined;
return {
session: {
id: token,
userId: String(payload.sub ?? user?.id ?? ""),
token,
expiresAt,
},
user: {
id: String(payload.sub ?? user?.id ?? ""),
email: String(payload.email ?? user?.email ?? ""),
name: String(payload.name ?? user?.name ?? ""),
image: null,
},
};
}
function parseJwtPayload(token: string): Record<string, unknown> {
try {
const payload = token.split(".")[1];
if (!payload) return {};
const padded = payload.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(payload.length / 4) * 4, "=");
return JSON.parse(atob(padded)) as Record<string, unknown>;
} catch {
return {};
}
}
type AuthContextValue = {
session: () => AuthSession | null;
loading: () => boolean;
usesNeonAuth: () => boolean;
supportsMagicLink: () => boolean;
supportsGoogleSignIn: () => boolean;
getToken: () => Promise<string | null>;
signInDemo: () => Promise<void>;
signUpWithEmail: (name: string, email: string, password: string) => Promise<void>;
signInWithEmail: (email: string, password: string) => Promise<void>;
signInAsDemo: () => Promise<void>;
sendMagicLink: (email: string) => Promise<void>;
signInWithMagicLink: (token: string, refreshToken?: string) => Promise<void>;
signInWithGoogle: () => Promise<void>;
signOut: () => Promise<void>;
};
@@ -25,9 +70,27 @@ export const AuthProvider: ParentComponent = (props) => {
const [session, setSession] = createSignal<AuthSession | null>(null);
const [loading, setLoading] = createSignal(true);
const clearLocalSession = () => {
localStorage.removeItem(localAuthTokenKey);
setSession(null);
};
createEffect(() => {
void (async () => {
const demoToken = localStorage.getItem(localAuthTokenKey);
if (demoToken?.startsWith("demo.")) {
const demoUser: LocalUser = {
id: "demo-user-id",
email: "demo@bookra.io",
name: "Demo User",
};
setSession(sessionFromLocalToken(demoToken, demoUser));
setLoading(false);
return;
}
if (!authClient) {
setSession(null);
setLoading(false);
return;
}
@@ -43,40 +106,99 @@ export const AuthProvider: ParentComponent = (props) => {
})();
});
const requireNeonAuth = () => {
if (!authClient) {
throw new Error("Neon Auth is not configured for this environment.");
}
return authClient;
};
const value: AuthContextValue = {
session,
loading,
usesNeonAuth: () => Boolean(authClient),
supportsMagicLink: () => false,
supportsGoogleSignIn: () => Boolean(authClient),
async getToken() {
if (!authClient) return null;
const demoToken = localStorage.getItem(localAuthTokenKey);
if (demoToken?.startsWith("demo.")) {
return demoToken;
}
if (!authClient) {
return session()?.session?.token ?? null;
}
const jwtToken = await (
authClient as unknown as { getJWTToken?: () => Promise<string | null | undefined> }
).getJWTToken?.();
if (typeof jwtToken === "string" && jwtToken.trim() !== "") {
return jwtToken;
}
return session()?.session?.token ?? null;
},
async signInDemo() {
if (!authClient) {
setSession({
user: {
id: "demo-owner",
email: "owner@bookra.dev",
name: "Bookra Demo Owner",
},
session: {
id: "demo-session",
userId: "demo-owner",
expiresAt: new Date(Date.now() + 60 * 60 * 1000),
},
});
return;
}
await authClient.signIn.email({
email: "owner@bookra.dev",
password: "bookra-demo-password",
async signUpWithEmail(name: string, email: string, password: string) {
const client = requireNeonAuth();
await client.signUp.email({
email,
password,
name,
});
const response = await authClient.getSession();
const response = await client.getSession();
setSession((response?.data as unknown as AuthSession | undefined) ?? null);
},
async signInWithEmail(email: string, password: string) {
const client = requireNeonAuth();
await client.signIn.email({
email,
password,
});
const response = await client.getSession();
setSession((response?.data as unknown as AuthSession | undefined) ?? null);
},
async signInAsDemo() {
const demoToken =
"demo." +
btoa(
JSON.stringify({
sub: "demo-user-id",
email: "demo@bookra.io",
name: "Demo User",
exp: Math.floor(Date.now() / 1000) + 86400,
iat: Math.floor(Date.now() / 1000),
demo: true,
}),
).replace(/=/g, "") +
".demo-signature";
const demoUser: LocalUser = {
id: "demo-user-id",
email: "demo@bookra.io",
name: "Demo User",
};
localStorage.setItem(localAuthTokenKey, demoToken);
setSession(sessionFromLocalToken(demoToken, demoUser));
},
async sendMagicLink() {
throw new Error("Magic link sign-in is not used in this app.");
},
async signInWithMagicLink() {
throw new Error("Magic link callback is not used in this app.");
},
async signInWithGoogle() {
const client = requireNeonAuth();
await client.signIn.social({
provider: "google",
callbackURL: `${window.location.origin}/dashboard`,
});
},
async signOut() {
if (!authClient) return;
await authClient.signOut();
setSession(null);
try {
if (authClient) {
await authClient.signOut();
}
} finally {
clearLocalSession();
}
},
};
+632 -66
View File
@@ -10,82 +10,576 @@ import type { Locale } from "@bookra/shared-types";
const dictionaries = {
cs: {
// Navigation & Auth
"nav.booking": "Veřejná rezervace",
"nav.dashboard": "Aplikace",
"auth.signIn": "Přihlásit",
"auth.signOut": "Odhlásit",
"home.eyebrow": "Bookra",
"home.title": "Klidný rezervační software pro lokální služby.",
"home.body":
"Root je marketingový vstup do produktu. Hlavní aplikace začíná v dashboardu a veřejná rezervace zůstává oddělená pro zákazníky.",
"home.primary": "Otevřít aplikaci",
"home.secondary": "Zobrazit veřejnou rezervaci",
"home.appLabel": "Hlavní vstup",
"home.appTitle": "/dashboard",
"home.appBody":
"Majitelé a tým pokračují přímo do aplikace, kde řeší dashboard, billing, tenant bootstrap a provoz.",
"home.publicLabel": "Veřejný tok",
"home.publicTitle": "/book/:tenantSlug",
"home.publicBody":
"Customer-facing booking flow zůstává mimo aplikaci, aby byl rychlý, čistý a bez interního šumu.",
"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.authRequired": "Live dashboard data needs a Neon Auth session and JWT.",
"dashboard.bootstrap": "Tenant bootstrap",
"dashboard.previewMode": "Preview mode",
"dashboard.billing": "Billing",
"dashboard.checkout": "Open checkout",
"dashboard.refreshBilling": "Refresh billing",
"dashboard.plan": "Plan",
"dashboard.status": "Status",
"dashboard.entitlements": "Entitlements",
"nav.about": "O nás",
"nav.contact": "Kontakt",
// Calendar
"calendar.months.0": "Leden",
"calendar.months.1": "Únor",
"calendar.months.2": "Březen",
"calendar.months.3": "Duben",
"calendar.months.4": "Květen",
"calendar.months.5": "Červen",
"calendar.months.6": "Červenec",
"calendar.months.7": "Srpen",
"calendar.months.8": "Září",
"calendar.months.9": "Říjen",
"calendar.months.10": "Listopad",
"calendar.months.11": "Prosinec",
"calendar.days.0": "Po",
"calendar.days.1": "Út",
"calendar.days.2": "St",
"calendar.days.3": "Čt",
"calendar.days.4": "Pá",
"calendar.days.5": "So",
"calendar.days.6": "Ne",
"calendar.prevMonth": "Předchozí měsíc",
"calendar.nextMonth": "Další měsíc",
"common.cancel": "Zrušit",
"auth.signIn": "Přihlásit se",
"auth.signOut": "Odhlásit se",
"auth.signInTitle": "Přihlášení",
"auth.signInBody": "Použijte účet vytvořený v Bookra Auth.",
"auth.email": "E-mail",
"auth.password": "Heslo",
"auth.signInFailed": "Přihlášení se nepodařilo.",
"auth.magicLinkSent": "Magický odkaz odeslán na váš e-mail",
"auth.magicLinkTitle": "Přihlášení bez hesla",
"auth.magicLinkBody": "Zadejte svůj e-mail a pošleme vám bezpečný odkaz pro přihlášení.",
"auth.orContinueWith": "nebo pokračujte s",
"auth.continueWithGoogle": "Pokračovat s Google",
"auth.noAccount": "Nemáte účet?",
"auth.createAccount": "Vytvořit účet",
"auth.forgotPassword": "Zapomněli jste heslo?",
"auth.registerTitle": "Vytvořit účet",
"auth.registerBody": "Začněte spravovat své rezervace s Bookra.",
"auth.fullName": "Celé jméno",
"auth.confirmPassword": "Potvrdit heslo",
"auth.passwordMismatch": "Hesla se neshodují",
"auth.emailInvalid": "Zadejte platný e-mail",
"auth.passwordTooShort": "Heslo musí mít alespoň 8 znaků",
"auth.accountCreated": "Účet vytvořen",
"auth.checkEmail": "Zkontrolujte svůj e-mail pro potvrzení",
"auth.magicLink.checkInbox": "Zkontrolujte svou e-mailovou schránku",
"auth.magicLink.instructions": "Klikněte na odkaz v e-mailu pro přihlášení. Odkaz platí 15 minut.",
"auth.magicLink.didntReceive": "Nepřišel e-mail?",
"auth.magicLink.resend": "Poslat znovu",
"auth.magicLink.backToSignIn": "Zpět na přihlášení",
"auth.errors.invalidCredentials": "Neplatné přihlašovací údaje",
"auth.errors.emailExists": "Tento e-mail je již registrován",
"auth.errors.tokenExpired": "Odkaz vypršel, požádejte o nový",
"auth.errors.tokenInvalid": "Neplatný odkaz, zkuste to znovu",
"auth.welcome": "Vítejte v Bookra",
"auth.welcomeBody": "Děkujeme za registraci. Vaše rezervace budou nyní mnohem jednodušší.",
// Hero Section
"home.badge": "Nyní zdarma pro začátečníky",
"home.hero.title": "Klidný rezervační software pro lokální služby",
"home.hero.subtitle": "Spravujte rezervace, zákazníky a tým na jednom místě. Bez zbytečné složitosti — jen spolehlivý systém, který funguje.",
"home.hero.cta.primary": "Začít zdarma",
"home.hero.cta.secondary": "Otevřít rezervaci",
"home.trust": "Důvěřují nám podniky po celé ČR",
// Features Section
"home.features.eyebrow": "Funkce",
"home.features.title": "Vše potřebné pro váš podnik",
"home.features.subtitle": "Od správy rezervací po e-mailová připomenutí — máte vše, co potřebujete k efektivnímu provozu.",
"home.feature.scheduling.title": "Chytré plánování",
"home.feature.scheduling.desc": "Automatické detekce konfliktů, buffer mezi rezervacemi a správa více lokací.",
"home.feature.customers.title": "Správa zákazníků",
"home.feature.customers.desc": "Uchovávejte historii návštěv, preference a kontakty na jednom místě.",
"home.feature.reminders.title": "Automatická připomenutí",
"home.feature.reminders.desc": "E-mailová upozornění pro vás i zákazníky. Méně zapomenutých rezervací.",
"home.feature.availability.title": "Flexibilní dostupnost",
"home.feature.availability.desc": "Nastavte pracovní dobu, výjimky a black-out dny podle vašich potřeb.",
"home.feature.quick.title": "Rychlé rezervace",
"home.feature.quick.desc": "Zákazníci si zarezervují termín za méně než 60 sekund. Bez registrace nutné.",
"home.feature.secure.title": "Bezpečné údaje",
"home.feature.secure.desc": "GDPR kompatibilní, šifrovaná data, zálohy a plná kontrola nad soukromím.",
// How It Works
"home.how.eyebrow": "Jak to funguje",
"home.how.title": "Začněte za 5 minut",
"home.how.subtitle": "Žádné složité nastavování. Vytvořte si profil, přidejte služby a začněte přijímat rezervace ještě dnes.",
"home.how.cta": "Vytvořit účet",
"home.step1.title": "Vytvořte si profil",
"home.step1.desc": "Zadejte název podniku, lokaci a kontaktní údaje. Trvá to méně než minutu.",
"home.step2.title": "Přidejte služby a tým",
"home.step2.desc": "Nastavte služby, ceny, pracovní dobu a pozvěte členy týmu.",
"home.step3.title": "Spusťte rezervace",
"home.step3.desc": "Získáte unikátní odkaz, který můžete sdílet se zákazníky nebo vložit na web.",
"home.step4.title": "To je vše!",
"home.step4.desc": "Začnete přijímat rezervace okamžitě.",
// Testimonials
"home.testimonials.eyebrow": "Reference",
"home.testimonials.title": "Co říkají naši zákazníci",
"home.testimonial1.quote": "Bookra nám ušetřila hodiny administrativy týdně. Zákazníci milují jednoduchost online rezervací.",
"home.testimonial1.author": "Martina Nováková",
"home.testimonial1.role": "Majitelka, Salon Ella",
"home.testimonial2.quote": "Konec dvojitým rezervacím a zmatkům v diáři. Vše je přehledné a pod kontrolou.",
"home.testimonial2.author": "David Svoboda",
"home.testimonial2.role": "Fyzioterapeut, Physio Care",
"home.testimonial3.quote": "Automatická e-mailová připomenutí snížila počet zapomenutých rezervací. Skvělá investice.",
"home.testimonial3.author": "Jana Kovářová",
"home.testimonial3.role": "Majitelka, Massage Studio",
// Pricing
"home.pricing.eyebrow": "Ceník",
"home.pricing.title": "Jednoduché a transparentní ceny",
"home.pricing.subtitle": "Žádné skryté poplatky. Všechny plány zahrnují plnou podporu.",
"home.pricing.starter.name": "Starter",
"home.pricing.starter.desc": "Pro jednotlivce a malé podniky",
"home.pricing.starter.f1": "Do 50 rezervací/měsíc",
"home.pricing.starter.f2": "1 lokace, 1 zaměstnanec",
"home.pricing.starter.f3": "E-mailová podpora",
"home.pricing.starter.cta": "Začít zdarma",
"home.pricing.starter.trial": "15 dní zdarma po registraci",
"home.pricing.perMonth": "/měsíc",
"home.pricing.pro.name": "Pro",
"home.pricing.pro.desc": "Pro rostoucí podniky",
"home.pricing.popular": "Nejoblíbenější",
"home.pricing.pro.f1": "Neomezené rezervace",
"home.pricing.pro.f2": "3 lokace, 10 zaměstnanců",
"home.pricing.pro.f3": "E-mailová připomenutí",
"home.pricing.pro.f4": "Prioritní podpora",
"home.pricing.pro.f5": "Analytika a reporty",
"home.pricing.pro.cta": "Začít 15denní zkoušku",
"home.pricing.pro.trial": "15 dní zdarma po registraci",
"home.pricing.biz.name": "Business",
"home.pricing.biz.desc": "Pro větší týmy a franšízy",
"home.pricing.biz.f1": "Neomezené vše",
"home.pricing.biz.f2": "Více lokací",
"home.pricing.biz.f3": "API přístup",
"home.pricing.biz.f4": "Dedikovaný manažer",
"home.pricing.biz.cta": "Kontaktovat prodej",
"home.pricing.biz.trial": "Individuální řešení na míru",
// 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.",
"home.cta.primary": "Začít zdarma",
"home.cta.secondary": "Otevřít rezervaci",
// Widget Builder
"widget.builder.title": "Rezervační widget",
"widget.builder.subtitle": "Vyberte styl a zkopírujte kód na váš web",
"widget.types.title": "Dostupné widgety",
"widget.types.desc": "Přetáhněte pro změnu pořadí, klikněte pro výběr",
"widget.type.iframe.title": "Inline iframe",
"widget.type.iframe.desc": "Vložte rezervační formulář přímo do stránky",
"widget.type.iframe.preview": "Nejlepší pro: Hlavní stránky, rezervační sekce",
"widget.type.button.title": "Rezervační tlačítko",
"widget.type.button.desc": "Jednoduché tlačítko odkazující na rezervaci",
"widget.type.button.preview": "Nejlepší pro: Navigace, CTA sekce",
"widget.type.inline.title": "Inline kalendář",
"widget.type.inline.desc": "Interaktivní kalendář přímo na vašem webu",
"widget.type.inline.preview": "Nejlepší pro: Stránky služeb, produkty",
"widget.type.modal.title": "Modal popup",
"widget.type.modal.desc": "Rezervace se otevře v překrývacím okně",
"widget.type.modal.preview": "Nejlepší pro: Rychlý přístup bez opuštění stránky",
"widget.type.floating.title": "Plovoucí bublina",
"widget.type.floating.desc": "Plovoucí tlačítko v rohu obrazovky",
"widget.type.floating.preview": "Nejlepší pro: E-shopy, kontinuální dostupnost",
"widget.button.text": "Rezervovat termín",
"widget.modal.trigger": "Otevřít rezervaci",
"widget.styling.title": "Vzhled",
"widget.styling.color": "Primární barva",
"widget.styling.radius": "Zaoblení rohů",
"widget.styling.shadow": "Intenzita stínu",
"widget.styling.buttonText": "Text tlačítka",
"widget.styling.position": "Pozice",
"widget.position.topLeft": "Vlevo nahoře",
"widget.position.topRight": "Vpravo nahoře",
"widget.position.bottomLeft": "Vlevo dole",
"widget.position.bottomRight": "Vpravo dole",
"widget.preview.show": "Zobrazit náhled",
"widget.preview.hide": "Skrýt náhled",
"widget.customize.title": "Přizpůsobení vzhledu",
"widget.customize.desc": "Vyberte téma a velikost widgetu",
"widget.theme.label": "Téma",
"widget.theme.light": "Světlé",
"widget.theme.dark": "Tmavé",
"widget.theme.auto": "Automatické",
"widget.size.label": "Velikost",
"widget.size.compact": "Kompaktní",
"widget.size.default": "Standardní",
"widget.size.full": "Plná šířka",
"widget.code.title": "Kód widgetu",
"widget.code.desc": "Zkopírujte kód a vložte ho na váš web",
"widget.install.title": "Jak nainstalovat",
"widget.install.desc": "Vložte kód do HTML vaší stránky tam, kde chcete zobrazit widget.",
"widget.install.preview": "Náhled živé stránky",
"widget.security.title": "Bezpečnost",
"widget.security.desc": "Všechny widgety jsou zabezpečeny a izolovány:",
"widget.security.https": "Šifrovaná HTTPS komunikace",
"widget.security.isolation": "Sandbox izolace iframe",
"widget.security.cors": "CORS ochrana API",
"common.copy": "Kopírovat",
"common.copied": "Zkopírováno!",
// Footer
"footer.copyright": "© 2026 Bookra. Všechna práva vyhrazena.",
"footer.privacy": "Ochrana soukromí",
"footer.terms": "Podmínky použití",
"footer.description": "Klidný rezervační software pro lokální služby. Spravujte rezervace, zákazníky a tým na jednom místě.",
"footer.links.title": "Navigace",
"footer.legal.title": "Právní informace",
// Dashboard (existing)
"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.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.",
"dashboard.bootstrap": "Vytvoření prostoru",
"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",
"dashboard.status": "Stav",
"dashboard.entitlements": "Funkce a limity",
"dashboard.onboarding.title": "Vytvořit pracovní prostor",
"dashboard.onboarding.body":
"Tento účet ještě nemá tenant membership. Vytvořte první workspace a pokračujte do aplikace.",
"dashboard.onboarding.body": "Tento účet ještě nemá vytvořený pracovní prostor. Vytvořte první prostor a pokračujte do aplikace.",
"dashboard.onboarding.name": "Název firmy",
"dashboard.onboarding.slug": "Slug",
"dashboard.onboarding.preset": "Preset",
"dashboard.onboarding.locale": "Locale",
"dashboard.onboarding.timezone": "Timezone",
"dashboard.onboarding.submit": "Vytvořit workspace",
"dashboard.onboarding.pending": "Vytvářím workspace...",
"booking.title": "Public booking page",
"booking.body":
"The public experience stays light: availability, slot confirmation, and a clear fallback for guest booking or account-based booking later.",
"booking.empty": "Live availability will load from the Railway API when the tenant is configured.",
"dashboard.onboarding.slug": "Identifikátor (URL)",
"dashboard.onboarding.preset": "Typ podnikání",
"dashboard.onboarding.locale": "Jazyk",
"dashboard.onboarding.timezone": "Časové pásmo",
"dashboard.onboarding.submit": "Vytvořit prostor",
"dashboard.onboarding.pending": "Vytvářím prostor...",
"booking.title": "Rezervace",
"booking.body": "Vyberte dostupný termín, doplňte kontaktní údaje a potvrzení přijde e-mailem.",
"booking.slots": "Dostupné termíny",
"booking.selectTime": "Vyberte čas",
"booking.selectTimeBody": "Vyberte si z dostupných termínů pro služby a lekce.",
"booking.empty": "Momentálně nejsou k dispozici žádné termíny.",
"booking.emptyHint": "Zkuste to prosím později.",
"booking.only": "Zbývají",
"booking.left": "místa",
"booking.spotsAvailable": "volná místa",
"booking.submitting": "Rezervuji...",
"booking.submit": "Rezervovat",
"booking.business": "Podnik",
"booking.sidebar.body": "Rezervace proběhne online. Potvrzení a připomenutí přijdou e-mailem.",
"booking.customer.title": "Kontaktní údaje",
"booking.customer.body": "Tyto údaje použijeme pro potvrzení rezervace a připomenutí.",
"booking.customer.name": "Jméno",
"booking.customer.email": "E-mail",
"booking.customer.notes": "Poznámka",
"booking.customerRequired": "Před rezervací vyplňte jméno a e-mail.",
"booking.failed": "Rezervaci se nepodařilo vytvořit",
"booking.created": "Rezervace vytvořena",
"booking.confirmed": "Rezervace potvrzena",
"booking.help.title": "Potřebujete pomoc?",
"booking.help.body": "Ozvěte se provozovateli služby.",
"booking.expect.title": "Co můžete čekat",
"booking.expect.confirmation": "Okamžité potvrzení rezervace",
"booking.expect.reminders": "E-mailové připomenutí před návštěvou",
"booking.expect.rescheduling": "Jednoduchá domluva změny termínu",
// About Page
"about.title": "O nás",
"about.subtitle": "Jsme tým, který věří, že správa rezervací může být jednoduchá a příjemná. Naše mise je pomáhat lokálním podnikům růst.",
"about.story.title": "Náš příběh",
"about.story.p1": "Bookra vznikla z vlastní potřeby. Jako majitelé malých podniků jsme byli frustrovaní z komplikovaných rezervačních systémů, které byly plné funkcí, které nikdo nepoužívá.",
"about.story.p2": "Rozhodli jsme se vytvořit něco jiného — software, který je klidný, intuitivní a dělá přesně to, co potřebujete. Bez zbytečné složitosti.",
"about.story.p3": "Dnes pomáháme tisícům podniků v České republice a Evropě zjednodušit jejich rezervace a šetřit čas. A to je jen začátek.",
"about.values.title": "Naše hodnoty",
"about.values.subtitle": "To, co nás řídí při vývoji Bookra",
"about.values.simple.title": "Jednoduchost",
"about.values.simple.desc": "Méně je více. Každá funkce musí mít jasný účel a přinášet hodnotu.",
"about.values.reliable.title": "Spolehlivost",
"about.values.reliable.desc": "Vaše data jsou v bezpečí a systém funguje, když ho potřebujete.",
"about.values.local.title": "Lokální přístup",
"about.values.local.desc": "Rozumíme českému a evropskému trhu. Podporujeme lokální podniky.",
"about.team.title": "Tým Bookra",
"about.team.subtitle": "Za Bookra stojí malý, ale oddaný tým. Každý z nás pečuje o to, aby software dělal to, co má.",
"about.cta": "Kontaktujte nás",
// Contact Page
"contact.title": "Kontakt",
"contact.subtitle": "Máte dotaz nebo zpětnou vazbu? Rádi od vás uslyšíme. Vyplňte formulář a ozveme se vám.",
"contact.form.title": "Napište nám",
"contact.form.name": "Jméno",
"contact.form.email": "E-mail",
"contact.form.message": "Zpráva",
"contact.form.submit": "Odeslat zprávu",
"contact.success.title": "Zpráva odeslána",
"contact.success.body": "Děkujeme za váš zájem. Ozveme se vám co nejdříve.",
"contact.info.email.title": "E-mail",
"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.",
// Legal
"legal.privacy.title": "Ochrana soukromí",
"legal.privacy.body": "Bookra zpracovává údaje potřebné pro vytvoření rezervace, správu účtu, připomenutí a zabezpečení služby. Údaje tenantů jsou oddělené a přístup k nim je omezen podle role uživatele.",
"legal.privacy.data.title": "Jaké údaje zpracováváme",
"legal.privacy.data.body": "Kontaktní údaje zákazníků, čas rezervace, poznámky zadané při rezervaci, údaje o účtu provozovatele a technické záznamy potřebné pro bezpečný provoz.",
"legal.privacy.rights.title": "Práva a žádosti",
"legal.privacy.rights.body": "Žá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í.",
"legal.terms.title": "Podmínky použití",
"legal.terms.body": "Bookra je software pro správu rezervací lokálních služeb. Provozovatel účtu odpovídá za správnost nabídky, dostupnost termínů a komunikaci se zákazníky.",
"legal.terms.service.title": "Používání služby",
"legal.terms.service.body": "Službu je nutné používat v souladu se zákonem, bez zneužití rezervačních formulářů, obcházení zabezpečení nebo nahrávání zakázaného obsahu.",
"legal.terms.billing.title": "Předplatné",
"legal.terms.billing.body": "Placené plány se účtují přes Paddle. Aktivní plán určuje dostupné limity, rozšíření a podpůrné funkce.",
},
en: {
// Navigation & Auth
"nav.booking": "Public booking",
"nav.dashboard": "App",
"nav.about": "About us",
"nav.contact": "Contact",
// Calendar
"calendar.months.0": "January",
"calendar.months.1": "February",
"calendar.months.2": "March",
"calendar.months.3": "April",
"calendar.months.4": "May",
"calendar.months.5": "June",
"calendar.months.6": "July",
"calendar.months.7": "August",
"calendar.months.8": "September",
"calendar.months.9": "October",
"calendar.months.10": "November",
"calendar.months.11": "December",
"calendar.days.0": "Mon",
"calendar.days.1": "Tue",
"calendar.days.2": "Wed",
"calendar.days.3": "Thu",
"calendar.days.4": "Fri",
"calendar.days.5": "Sat",
"calendar.days.6": "Sun",
"calendar.prevMonth": "Previous month",
"calendar.nextMonth": "Next month",
"common.cancel": "Cancel",
"auth.signIn": "Sign in",
"auth.signOut": "Sign out",
"home.eyebrow": "Bookra",
"home.title": "Calm booking software for local service businesses.",
"home.body":
"The root is the marketing entry to the product. The main app starts in the dashboard, while public booking stays separate for customers.",
"home.primary": "Open app",
"home.secondary": "View public booking",
"home.appLabel": "Main app entry",
"home.appTitle": "/dashboard",
"home.appBody":
"Owners and staff move directly into the app for dashboard, billing, tenant bootstrap, and day-to-day operations.",
"home.publicLabel": "Public flow",
"home.publicTitle": "/book/:tenantSlug",
"home.publicBody":
"The customer-facing booking flow stays outside the app so it remains focused, fast, and free of internal noise.",
"auth.signInTitle": "Sign in",
"auth.signInBody": "Use the account created for your Bookra workspace.",
"auth.email": "Email",
"auth.password": "Password",
"auth.signInFailed": "Sign in failed.",
"auth.magicLinkSent": "Magic link sent to your email",
"auth.magicLinkTitle": "Passwordless sign-in",
"auth.magicLinkBody": "Enter your email and we'll send you a secure sign-in link.",
"auth.orContinueWith": "or continue with",
"auth.continueWithGoogle": "Continue with Google",
"auth.noAccount": "Don't have an account?",
"auth.createAccount": "Create account",
"auth.forgotPassword": "Forgot password?",
"auth.registerTitle": "Create account",
"auth.registerBody": "Start managing your bookings with Bookra.",
"auth.fullName": "Full name",
"auth.confirmPassword": "Confirm password",
"auth.passwordMismatch": "Passwords do not match",
"auth.emailInvalid": "Please enter a valid email",
"auth.passwordTooShort": "Password must be at least 8 characters",
"auth.accountCreated": "Account created",
"auth.checkEmail": "Check your email for confirmation",
"auth.magicLink.checkInbox": "Check your inbox",
"auth.magicLink.instructions": "Click the link in the email to sign in. The link is valid for 15 minutes.",
"auth.magicLink.didntReceive": "Didn't receive the email?",
"auth.magicLink.resend": "Resend",
"auth.magicLink.backToSignIn": "Back to sign in",
"auth.errors.invalidCredentials": "Invalid credentials",
"auth.errors.emailExists": "This email is already registered",
"auth.errors.tokenExpired": "Link expired, please request a new one",
"auth.errors.tokenInvalid": "Invalid link, please try again",
"auth.welcome": "Welcome to Bookra",
"auth.welcomeBody": "Thanks for signing up. Your bookings just got a whole lot easier.",
// Hero Section
"home.badge": "Now free for starters",
"home.hero.title": "Calm booking software for local services",
"home.hero.subtitle": "Manage bookings, customers, and your team in one place. No unnecessary complexity — just a reliable system that works.",
"home.hero.cta.primary": "Get started free",
"home.hero.cta.secondary": "Open booking page",
"home.trust": "Trusted by businesses across Europe",
// Features Section
"home.features.eyebrow": "Features",
"home.features.title": "Everything your business needs",
"home.features.subtitle": "From booking management to email reminders, you have what you need to run your business efficiently.",
"home.feature.scheduling.title": "Smart Scheduling",
"home.feature.scheduling.desc": "Automatic conflict detection, buffer times between bookings, and multi-location support.",
"home.feature.customers.title": "Customer Management",
"home.feature.customers.desc": "Keep visit history, preferences, and contact details in one organized place.",
"home.feature.reminders.title": "Automated Reminders",
"home.feature.reminders.desc": "Email notifications for you and your customers. Fewer no-shows, more revenue.",
"home.feature.availability.title": "Flexible Availability",
"home.feature.availability.desc": "Set working hours, exceptions, and blackout days according to your needs.",
"home.feature.quick.title": "Quick Bookings",
"home.feature.quick.desc": "Customers book in under 60 seconds. No account required for guest bookings.",
"home.feature.secure.title": "Secure Data",
"home.feature.secure.desc": "GDPR compliant, encrypted data, backups, and full privacy control.",
// How It Works
"home.how.eyebrow": "How it works",
"home.how.title": "Get started in 5 minutes",
"home.how.subtitle": "No complex setup. Create your profile, add services, and start accepting bookings today.",
"home.how.cta": "Create account",
"home.step1.title": "Create your profile",
"home.step1.desc": "Enter your business name, location, and contact details. Takes less than a minute.",
"home.step2.title": "Add services & team",
"home.step2.desc": "Set up your service menu, pricing, working hours, and invite team members.",
"home.step3.title": "Start taking bookings",
"home.step3.desc": "Get a unique booking link to share with customers or embed on your website.",
"home.step4.title": "That's it!",
"home.step4.desc": "Start accepting bookings immediately.",
// Testimonials
"home.testimonials.eyebrow": "Testimonials",
"home.testimonials.title": "What our customers say",
"home.testimonial1.quote": "Bookra saved us hours of admin work every week. Customers love how easy online booking is.",
"home.testimonial1.author": "Sarah Mitchell",
"home.testimonial1.role": "Owner, Studio Ella",
"home.testimonial2.quote": "No more double bookings and diary confusion. Everything is clear and under control.",
"home.testimonial2.author": "James Chen",
"home.testimonial2.role": "Physiotherapist, Physio Care",
"home.testimonial3.quote": "Automatic email reminders reduced forgotten bookings. Great investment for our business.",
"home.testimonial3.author": "Emma Wilson",
"home.testimonial3.role": "Owner, Massage Studio",
// Pricing
"home.pricing.eyebrow": "Pricing",
"home.pricing.title": "Simple, transparent pricing",
"home.pricing.subtitle": "No hidden fees. All plans include full support.",
"home.pricing.starter.name": "Starter",
"home.pricing.starter.desc": "For individuals and small businesses",
"home.pricing.starter.f1": "Up to 50 bookings/month",
"home.pricing.starter.f2": "1 location, 1 staff member",
"home.pricing.starter.f3": "Email support",
"home.pricing.starter.cta": "Start for free",
"home.pricing.starter.trial": "15 days free after sign-up",
"home.pricing.perMonth": "/mo",
"home.pricing.pro.name": "Pro",
"home.pricing.pro.desc": "For growing businesses",
"home.pricing.popular": "Most Popular",
"home.pricing.pro.f1": "Unlimited bookings",
"home.pricing.pro.f2": "3 locations, 10 staff",
"home.pricing.pro.f3": "Email reminders",
"home.pricing.pro.f4": "Priority support",
"home.pricing.pro.f5": "Analytics & reports",
"home.pricing.pro.cta": "Start 15-day trial",
"home.pricing.pro.trial": "15 days free after sign-up",
"home.pricing.biz.name": "Business",
"home.pricing.biz.desc": "For larger teams and franchises",
"home.pricing.biz.f1": "Unlimited everything",
"home.pricing.biz.f2": "Multiple locations",
"home.pricing.biz.f3": "API access",
"home.pricing.biz.f4": "Dedicated manager",
"home.pricing.biz.cta": "Contact sales",
"home.pricing.biz.trial": "Custom enterprise solutions",
// CTA
"home.cta.title": "Ready to simplify your bookings?",
"home.cta.subtitle": "Join thousands of businesses saving time with Bookra.",
"home.cta.primary": "Start for free",
"home.cta.secondary": "Open booking page",
// Widget Builder
"widget.builder.title": "Booking Widget",
"widget.builder.subtitle": "Choose a style and copy the code to your website",
"widget.types.title": "Available Widgets",
"widget.types.desc": "Drag to reorder, click to select",
"widget.type.iframe.title": "Inline iframe",
"widget.type.iframe.desc": "Embed the booking form directly on your page",
"widget.type.iframe.preview": "Best for: Homepages, booking sections",
"widget.type.button.title": "Booking button",
"widget.type.button.desc": "Simple button linking to your booking page",
"widget.type.button.preview": "Best for: Navigation, CTA sections",
"widget.type.inline.title": "Inline calendar",
"widget.type.inline.desc": "Interactive calendar directly on your website",
"widget.type.inline.preview": "Best for: Service pages, products",
"widget.type.modal.title": "Modal popup",
"widget.type.modal.desc": "Booking opens in an overlay window",
"widget.type.modal.preview": "Best for: Quick access without leaving the page",
"widget.type.floating.title": "Floating bubble",
"widget.type.floating.desc": "Floating button in the corner of the screen",
"widget.type.floating.preview": "Best for: E-commerce, continuous availability",
"widget.button.text": "Book appointment",
"widget.modal.trigger": "Open booking",
"widget.styling.title": "Appearance",
"widget.styling.color": "Primary Color",
"widget.styling.radius": "Border Radius",
"widget.styling.shadow": "Shadow Intensity",
"widget.styling.buttonText": "Button Text",
"widget.styling.position": "Position",
"widget.position.topLeft": "Top Left",
"widget.position.topRight": "Top Right",
"widget.position.bottomLeft": "Bottom Left",
"widget.position.bottomRight": "Bottom Right",
"widget.preview.show": "Show Preview",
"widget.preview.hide": "Hide Preview",
"widget.customize.title": "Customize appearance",
"widget.customize.desc": "Select theme and widget size",
"widget.theme.label": "Theme",
"widget.theme.light": "Light",
"widget.theme.dark": "Dark",
"widget.theme.auto": "Auto",
"widget.size.label": "Size",
"widget.size.compact": "Compact",
"widget.size.default": "Default",
"widget.size.full": "Full width",
"widget.code.title": "Widget code",
"widget.code.desc": "Copy the code and paste it into your website",
"widget.install.title": "How to install",
"widget.install.desc": "Paste the code into your page HTML where you want the widget to appear.",
"widget.install.preview": "Preview live page",
"widget.security.title": "Security",
"widget.security.desc": "All widgets are secured and isolated:",
"widget.security.https": "Encrypted HTTPS communication",
"widget.security.isolation": "Sandbox iframe isolation",
"widget.security.cors": "API CORS protection",
"common.copy": "Copy",
"common.copied": "Copied!",
// Footer
"footer.copyright": "© 2026 Bookra. All rights reserved.",
"footer.privacy": "Privacy",
"footer.terms": "Terms",
"footer.description": "Calm booking software for local services. Manage bookings, customers, and your team in one place.",
"footer.links.title": "Navigation",
"footer.legal.title": "Legal",
// Dashboard (existing)
"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.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.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.",
"dashboard.bootstrap": "Tenant bootstrap",
"dashboard.previewMode": "Preview mode",
"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",
@@ -93,8 +587,7 @@ const dictionaries = {
"dashboard.status": "Status",
"dashboard.entitlements": "Entitlements",
"dashboard.onboarding.title": "Create workspace",
"dashboard.onboarding.body":
"This account does not have a tenant membership yet. Create the first workspace and continue into the app.",
"dashboard.onboarding.body": "This account does not have a tenant membership yet. Create the first workspace and continue into the app.",
"dashboard.onboarding.name": "Business name",
"dashboard.onboarding.slug": "Slug",
"dashboard.onboarding.preset": "Preset",
@@ -102,10 +595,83 @@ const dictionaries = {
"dashboard.onboarding.timezone": "Timezone",
"dashboard.onboarding.submit": "Create workspace",
"dashboard.onboarding.pending": "Creating workspace...",
"booking.title": "Public booking page",
"booking.body":
"The public experience stays light: availability, slot confirmation, and a clear fallback for guest booking or account-based booking later.",
"booking.empty": "Live availability will load from the Railway API when the tenant is configured.",
"booking.title": "Book a visit",
"booking.body": "Choose an available time, add your contact details, and receive confirmation by email.",
"booking.slots": "Available times",
"booking.selectTime": "Select a time",
"booking.selectTimeBody": "Choose from available appointment and class times.",
"booking.empty": "No bookable times are available right now.",
"booking.emptyHint": "Please check again later.",
"booking.only": "Only",
"booking.left": "left",
"booking.spotsAvailable": "spots available",
"booking.submitting": "Booking...",
"booking.submit": "Book now",
"booking.business": "Business",
"booking.sidebar.body": "Book online. Confirmation and reminders are sent by email.",
"booking.customer.title": "Contact details",
"booking.customer.body": "These details are used for confirmation and reminders.",
"booking.customer.name": "Name",
"booking.customer.email": "Email",
"booking.customer.notes": "Note",
"booking.customerRequired": "Add your name and email before booking.",
"booking.failed": "Booking failed",
"booking.created": "Booking created",
"booking.confirmed": "Booking confirmed",
"booking.help.title": "Need help?",
"booking.help.body": "Contact the service provider.",
"booking.expect.title": "What to expect",
"booking.expect.confirmation": "Instant booking confirmation",
"booking.expect.reminders": "Email reminders before your visit",
"booking.expect.rescheduling": "Simple rescheduling communication",
// About Page
"about.title": "About us",
"about.subtitle": "We're a team that believes booking management can be simple and pleasant. Our mission is to help local businesses grow.",
"about.story.title": "Our story",
"about.story.p1": "Bookra was born from our own need. As small business owners, we were frustrated with complicated booking systems full of features nobody uses.",
"about.story.p2": "We decided to create something different — software that is calm, intuitive, and does exactly what you need. Without unnecessary complexity.",
"about.story.p3": "Today we help thousands of businesses in the Czech Republic and Europe simplify their bookings and save time. And this is just the beginning.",
"about.values.title": "Our values",
"about.values.subtitle": "What guides us in developing Bookra",
"about.values.simple.title": "Simplicity",
"about.values.simple.desc": "Less is more. Every feature must have a clear purpose and bring value.",
"about.values.reliable.title": "Reliability",
"about.values.reliable.desc": "Your data is safe and the system works when you need it.",
"about.values.local.title": "Local approach",
"about.values.local.desc": "We understand the Czech and European market. We support local businesses.",
"about.team.title": "The Bookra team",
"about.team.subtitle": "Behind Bookra is a small but dedicated team. Each of us cares that the software does what it should.",
"about.cta": "Contact us",
// Contact Page
"contact.title": "Contact",
"contact.subtitle": "Have a question or feedback? We'd love to hear from you. Fill out the form and we'll get back to you.",
"contact.form.title": "Write to us",
"contact.form.name": "Name",
"contact.form.email": "Email",
"contact.form.message": "Message",
"contact.form.submit": "Send message",
"contact.success.title": "Message sent",
"contact.success.body": "Thank you for your interest. We'll get back to you as soon as possible.",
"contact.info.email.title": "Email",
"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.",
// Legal
"legal.privacy.title": "Privacy",
"legal.privacy.body": "Bookra processes the data needed to create bookings, manage accounts, send reminders, and secure the service. Tenant data is isolated and access is limited by user role.",
"legal.privacy.data.title": "Data we process",
"legal.privacy.data.body": "Customer contact details, booking times, booking notes, workspace account details, and technical records needed for secure operations.",
"legal.privacy.rights.title": "Rights and requests",
"legal.privacy.rights.body": "Access, correction, and deletion requests are handled by the operator of the relevant workspace. Bookra provides the technical system for secure processing.",
"legal.terms.title": "Terms",
"legal.terms.body": "Bookra is booking management software for local services. Workspace operators are responsible for their offer, availability, and customer communication.",
"legal.terms.service.title": "Using the service",
"legal.terms.service.body": "The service must be used lawfully, without abusing booking forms, bypassing security, or uploading prohibited content.",
"legal.terms.billing.title": "Subscription",
"legal.terms.billing.body": "Paid plans are billed through Paddle. The active plan controls limits, add-ons, and support features.",
},
} satisfies Record<Locale, Record<string, string>>;
@@ -0,0 +1,88 @@
import {
createContext,
createEffect,
createSignal,
ParentComponent,
useContext,
} from "solid-js";
type Theme = "light" | "dark" | "system";
const STORAGE_KEY = "bookra-theme";
function getInitialTheme(): Theme {
if (typeof window === "undefined") return "system";
const stored = localStorage.getItem(STORAGE_KEY) as Theme | null;
return stored ?? "system";
}
function getResolvedTheme(theme: Theme): "light" | "dark" {
if (theme === "system") {
return window.matchMedia("(prefers-color-scheme: dark)").matches
? "dark"
: "light";
}
return theme;
}
type ThemeContextValue = {
theme: () => Theme;
resolvedTheme: () => "light" | "dark";
setTheme: (theme: Theme) => void;
toggle: () => void;
};
const ThemeContext = createContext<ThemeContextValue>();
export const ThemeProvider: ParentComponent = (props) => {
const [theme, setThemeSignal] = createSignal<Theme>(getInitialTheme());
const [resolvedTheme, setResolvedTheme] = createSignal<"light" | "dark">(
getResolvedTheme(getInitialTheme())
);
const applyTheme = (t: Theme) => {
const resolved = getResolvedTheme(t);
setResolvedTheme(resolved);
const root = document.documentElement;
root.classList.remove("light", "dark");
root.classList.add(resolved);
root.setAttribute("data-theme", resolved);
};
createEffect(() => {
const t = theme();
localStorage.setItem(STORAGE_KEY, t);
applyTheme(t);
});
if (typeof window !== "undefined") {
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
mediaQuery.addEventListener("change", () => {
if (theme() === "system") {
applyTheme("system");
}
});
}
const setTheme = (t: Theme) => setThemeSignal(t);
const toggle = () => {
const current = resolvedTheme();
setTheme(current === "light" ? "dark" : "light");
};
return (
<ThemeContext.Provider value={{ theme, resolvedTheme, setTheme, toggle }}>
{props.children}
</ThemeContext.Provider>
);
};
export function useTheme() {
const context = useContext(ThemeContext);
if (!context) {
throw new Error("ThemeProvider is missing from the component tree.");
}
return context;
}