mirror of
https://github.com/Dvorinka/Bookra.git
synced 2026-10-28 07:41:16 +00:00
feat(site): landing-only on www, app gated to demo subdomain (#20)
* feat(site): landing-only on www host, app gated to demo subdomain - RequireAppHost redirects app routes to / on non-demo hosts - Signup CTAs swapped for launch-notification mailto on www - Pricing marked as indicative pre-launch, no subscription purchasable - SEO: OG/Twitter cards, og.png, JSON-LD, robots.txt, sitemap.xml, per-route document titles, SPA rewrite for Vercel Authored By: TDvorak <[email protected]> Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * chore: ignore vercel link artifacts Authored By: TDvorak <[email protected]> * docs: document www/demo host split in deployment targets Authored By: TDvorak <[email protected]> --------- Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
27a51c38b8
commit
7a31f55f81
15 files changed
+171
-65
No files matched your search
@@ -85,3 +85,5 @@ desloppify-out/
|
|||||||
# Analysis output
|
# Analysis output
|
||||||
graphify-out/
|
graphify-out/
|
||||||
output/
|
output/
|
||||||
|
.vercel
|
||||||
|
.env*
|
||||||
@@ -6,7 +6,10 @@ billing machinery underneath.
|
|||||||
|
|
||||||
Deployment targets:
|
Deployment targets:
|
||||||
|
|
||||||
- frontend on Vercel
|
- frontend on Vercel — `bookra-frontend` serves `www.bookra.eu` (landing only,
|
||||||
|
pre-launch: signup CTAs are mailto and app routes redirect to `/`);
|
||||||
|
`bookrademo` serves `demo.bookra.eu` (full app). The split is enforced by
|
||||||
|
hostname in `apps/frontend/src/lib/app-host.ts`.
|
||||||
- backend on Railway
|
- backend on Railway
|
||||||
- Neon Postgres + Neon Auth
|
- Neon Postgres + Neon Auth
|
||||||
- Stripe billing (checkout, customer portal, webhooks, SMS usage invoices)
|
- Stripe billing (checkout, customer portal, webhooks, SMS usage invoices)
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
.vercel
|
||||||
|
.env*
|
||||||
@@ -10,12 +10,40 @@
|
|||||||
<link rel="canonical" href="https://bookra.eu" />
|
<link rel="canonical" href="https://bookra.eu" />
|
||||||
<title>Bookra — Jednoduchý rezervační software</title>
|
<title>Bookra — Jednoduchý rezervační software</title>
|
||||||
|
|
||||||
|
<meta name="robots" content="index,follow" />
|
||||||
|
<link rel="apple-touch-icon" href="/favicon.svg" />
|
||||||
|
|
||||||
<!-- Open Graph -->
|
<!-- Open Graph -->
|
||||||
<meta property="og:type" content="website" />
|
<meta property="og:type" content="website" />
|
||||||
<meta property="og:url" content="https://bookra.eu" />
|
<meta property="og:url" content="https://bookra.eu" />
|
||||||
<meta property="og:title" content="Bookra — Jednoduchý rezervační software" />
|
<meta property="og:title" content="Bookra — Jednoduchý rezervační software" />
|
||||||
<meta property="og:description" content="Spravujte rezervace, zákazníky a tým na jednom místě. Bez zbytečné složitosti." />
|
<meta property="og:description" content="Spravujte rezervace, zákazníky a tým na jednom místě. Bez zbytečné složitosti." />
|
||||||
<meta property="og:locale" content="cs_CZ" />
|
<meta property="og:locale" content="cs_CZ" />
|
||||||
|
<meta property="og:site_name" content="Bookra" />
|
||||||
|
<meta property="og:image" content="https://bookra.eu/og.png" />
|
||||||
|
<meta property="og:image:width" content="1200" />
|
||||||
|
<meta property="og:image:height" content="630" />
|
||||||
|
|
||||||
|
<!-- Twitter -->
|
||||||
|
<meta name="twitter:card" content="summary_large_image" />
|
||||||
|
<meta name="twitter:title" content="Bookra — Jednoduchý rezervační software" />
|
||||||
|
<meta name="twitter:description" content="Spravujte rezervace, zákazníky a tým na jednom místě. Bez zbytečné složitosti." />
|
||||||
|
<meta name="twitter:image" content="https://bookra.eu/og.png" />
|
||||||
|
|
||||||
|
<!-- Structured data -->
|
||||||
|
<script type="application/ld+json">
|
||||||
|
{
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@type": "SoftwareApplication",
|
||||||
|
"name": "Bookra",
|
||||||
|
"url": "https://bookra.eu",
|
||||||
|
"applicationCategory": "BusinessApplication",
|
||||||
|
"operatingSystem": "Web",
|
||||||
|
"inLanguage": ["cs", "en"],
|
||||||
|
"description": "Jednoduchý rezervační software pro salony, kliniky a lokální služby. Rezervace, zákazníci, tým a připomenutí na jednom místě.",
|
||||||
|
"offers": { "@type": "Offer", "price": "0", "priceCurrency": "CZK", "availability": "https://schema.org/PreOrder" }
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<!-- Preconnect to Google Fonts -->
|
<!-- Preconnect to Google Fonts -->
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 203 KiB |
@@ -0,0 +1,8 @@
|
|||||||
|
User-agent: *
|
||||||
|
Allow: /
|
||||||
|
Disallow: /dashboard
|
||||||
|
Disallow: /book/
|
||||||
|
Disallow: /manage/
|
||||||
|
Disallow: /auth/
|
||||||
|
|
||||||
|
Sitemap: https://bookra.eu/sitemap.xml
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||||
|
<url><loc>https://bookra.eu/</loc><priority>1.0</priority></url>
|
||||||
|
<url><loc>https://bookra.eu/pricing</loc><priority>0.9</priority></url>
|
||||||
|
<url><loc>https://bookra.eu/about</loc></url>
|
||||||
|
<url><loc>https://bookra.eu/contact</loc></url>
|
||||||
|
<url><loc>https://bookra.eu/privacy</loc></url>
|
||||||
|
<url><loc>https://bookra.eu/terms</loc></url>
|
||||||
|
</urlset>
|
||||||
@@ -6,14 +6,25 @@ import { I18nProvider } from "./providers/i18n-provider";
|
|||||||
import { ThemeProvider } from "./providers/theme-provider";
|
import { ThemeProvider } from "./providers/theme-provider";
|
||||||
import { Shell } from "./components/shell";
|
import { Shell } from "./components/shell";
|
||||||
|
|
||||||
|
const ROUTE_TITLES: Record<string, string> = {
|
||||||
|
"/pricing": "Bookra - Ceník",
|
||||||
|
"/about": "Bookra - O nás",
|
||||||
|
"/contact": "Bookra - Kontakt",
|
||||||
|
"/privacy": "Bookra - Ochrana soukromí",
|
||||||
|
"/terms": "Bookra - Obchodní podmínky",
|
||||||
|
};
|
||||||
|
|
||||||
// ScrollToTop component that resets scroll position on route change
|
// ScrollToTop component that resets scroll position on route change
|
||||||
const ScrollToTop = () => {
|
const ScrollToTop = () => {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
// Reset scroll position whenever the pathname changes
|
// Reset scroll position and document title whenever the pathname changes
|
||||||
location.pathname;
|
const path = location.pathname;
|
||||||
window.scrollTo({ top: 0, left: 0, behavior: "instant" });
|
window.scrollTo({ top: 0, left: 0, behavior: "instant" });
|
||||||
|
document.title = ROUTE_TITLES[path] ?? (path.startsWith("/dashboard") || path.startsWith("/book") || path.startsWith("/manage")
|
||||||
|
? "Bookra"
|
||||||
|
: "Bookra - Jednoduchý rezervační software");
|
||||||
});
|
});
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { ParentComponent, Show, createSignal, onMount, onCleanup } from "solid-j
|
|||||||
import { useAuth } from "../providers/auth-provider";
|
import { useAuth } from "../providers/auth-provider";
|
||||||
import { useI18n } from "../providers/i18n-provider";
|
import { useI18n } from "../providers/i18n-provider";
|
||||||
import { useTheme } from "../providers/theme-provider";
|
import { useTheme } from "../providers/theme-provider";
|
||||||
|
import { isAppHost } from "../lib/app-host";
|
||||||
import { BookraCharacter } from "./bookra-character";
|
import { BookraCharacter } from "./bookra-character";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
@@ -96,10 +97,7 @@ export const Shell: ParentComponent = (props) => {
|
|||||||
];
|
];
|
||||||
|
|
||||||
// Demo mode detection
|
// Demo mode detection
|
||||||
const isDemoMode = () => {
|
const isDemoMode = isAppHost;
|
||||||
const hostname = window.location.hostname;
|
|
||||||
return hostname.includes("demo") || hostname === "localhost" || hostname === "127.0.0.1";
|
|
||||||
};
|
|
||||||
|
|
||||||
// Listen for custom event to open auth dialog from dashboard
|
// Listen for custom event to open auth dialog from dashboard
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
@@ -284,12 +282,9 @@ export const Shell: ParentComponent = (props) => {
|
|||||||
<Show when={isDemoMode()} fallback={
|
<Show when={isDemoMode()} fallback={
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => {
|
onClick={() => { window.location.href = "https://demo.bookra.eu"; }}
|
||||||
setAuthMode("sign-in");
|
|
||||||
setSignInOpen(true);
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{i18n.t("auth.signIn")}
|
{i18n.locale() === 'cs' ? 'Vyzkoušet demo' : 'Try demo'}
|
||||||
</Button>
|
</Button>
|
||||||
}>
|
}>
|
||||||
<Button
|
<Button
|
||||||
@@ -379,12 +374,11 @@ export const Shell: ParentComponent = (props) => {
|
|||||||
<Button
|
<Button
|
||||||
fullWidth
|
fullWidth
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setAuthMode("sign-in");
|
|
||||||
setSignInOpen(true);
|
|
||||||
setMobileMenuOpen(false);
|
setMobileMenuOpen(false);
|
||||||
|
window.location.href = "https://demo.bookra.eu";
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{i18n.t("auth.signIn")}
|
{i18n.locale() === 'cs' ? 'Vyzkoušet demo' : 'Try demo'}
|
||||||
</Button>
|
</Button>
|
||||||
}>
|
}>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
// The public www host serves the landing only until launch; the app lives on the demo subdomain.
|
||||||
|
export const isAppHost = () => {
|
||||||
|
const hostname = window.location.hostname;
|
||||||
|
return hostname.includes("demo") || hostname === "localhost" || hostname === "127.0.0.1";
|
||||||
|
};
|
||||||
|
|
||||||
|
export const appUrl = (path = "") => (isAppHost() ? path : `https://demo.bookra.eu${path}`);
|
||||||
|
|
||||||
|
export const WAITLIST_MAILTO =
|
||||||
|
"mailto:[email protected]?subject=" + encodeURIComponent("Bookra - oznamte mi spuštění");
|
||||||
+25
-13
@@ -1,12 +1,24 @@
|
|||||||
import { render } from "solid-js/web";
|
import { render } from "solid-js/web";
|
||||||
import { lazy } from "solid-js";
|
import { lazy, onMount, type Component, type ParentProps } from "solid-js";
|
||||||
import { Route, Router } from "@solidjs/router";
|
import { Route, Router, useNavigate } from "@solidjs/router";
|
||||||
import App from "./App";
|
import App from "./App";
|
||||||
import "./styles/index.css";
|
import "./styles/index.css";
|
||||||
import { initSentry } from "./lib/sentry";
|
import { initSentry } from "./lib/sentry";
|
||||||
|
import { isAppHost } from "./lib/app-host";
|
||||||
|
|
||||||
initSentry();
|
initSentry();
|
||||||
|
|
||||||
|
// App-only routes bounce to the landing on the public www host; the app is served on the app/demo subdomain.
|
||||||
|
const RequireAppHost = (props: ParentProps) => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
onMount(() => {
|
||||||
|
if (!isAppHost()) navigate("/", { replace: true });
|
||||||
|
});
|
||||||
|
return isAppHost() ? props.children : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const gated = (RouteComponent: Component) => () => <RequireAppHost><RouteComponent /></RequireAppHost>;
|
||||||
|
|
||||||
const HomeRoute = lazy(() => import("./routes/home-route").then((module) => ({ default: module.HomeRoute })));
|
const HomeRoute = lazy(() => import("./routes/home-route").then((module) => ({ default: module.HomeRoute })));
|
||||||
const PricingRoute = lazy(() => import("./routes/pricing-route"));
|
const PricingRoute = lazy(() => import("./routes/pricing-route"));
|
||||||
const AboutRoute = lazy(() => import("./routes/about-route").then((module) => ({ default: module.AboutRoute })));
|
const AboutRoute = lazy(() => import("./routes/about-route").then((module) => ({ default: module.AboutRoute })));
|
||||||
@@ -31,18 +43,18 @@ render(
|
|||||||
<Route path="/" component={HomeRoute} />
|
<Route path="/" component={HomeRoute} />
|
||||||
<Route path="/pricing" component={PricingRoute} />
|
<Route path="/pricing" component={PricingRoute} />
|
||||||
<Route path="/about" component={AboutRoute} />
|
<Route path="/about" component={AboutRoute} />
|
||||||
<Route path="/auth/callback" component={AuthCallbackRoute} />
|
<Route path="/auth/callback" component={gated(AuthCallbackRoute)} />
|
||||||
<Route path="/contact" component={ContactRoute} />
|
<Route path="/contact" component={ContactRoute} />
|
||||||
<Route path="/dashboard" component={DashboardRoute} />
|
<Route path="/dashboard" component={gated(DashboardRoute)} />
|
||||||
<Route path="/dashboard/bookings" component={DashboardBookingsRoute} />
|
<Route path="/dashboard/bookings" component={gated(DashboardBookingsRoute)} />
|
||||||
<Route path="/dashboard/customers" component={DashboardCustomersRoute} />
|
<Route path="/dashboard/customers" component={gated(DashboardCustomersRoute)} />
|
||||||
<Route path="/dashboard/zones" component={DashboardZonesRoute} />
|
<Route path="/dashboard/zones" component={gated(DashboardZonesRoute)} />
|
||||||
<Route path="/dashboard/services" component={DashboardServicesRoute} />
|
<Route path="/dashboard/services" component={gated(DashboardServicesRoute)} />
|
||||||
<Route path="/dashboard/staff" component={DashboardStaffRoute} />
|
<Route path="/dashboard/staff" component={gated(DashboardStaffRoute)} />
|
||||||
<Route path="/dashboard/billing" component={DashboardBillingRoute} />
|
<Route path="/dashboard/billing" component={gated(DashboardBillingRoute)} />
|
||||||
<Route path="/dashboard/settings" component={DashboardSettingsRoute} />
|
<Route path="/dashboard/settings" component={gated(DashboardSettingsRoute)} />
|
||||||
<Route path="/book/:tenantSlug" component={PublicBookingRoute} />
|
<Route path="/book/:tenantSlug" component={gated(PublicBookingRoute)} />
|
||||||
<Route path="/manage/:reference" component={BookingManageRoute} />
|
<Route path="/manage/:reference" component={gated(BookingManageRoute)} />
|
||||||
<Route path="/:kind" component={LegalRoute} matchFilters={{ kind: ["privacy", "terms"] }} />
|
<Route path="/:kind" component={LegalRoute} matchFilters={{ kind: ["privacy", "terms"] }} />
|
||||||
<Route path="*404" component={NotFoundRoute} />
|
<Route path="*404" component={NotFoundRoute} />
|
||||||
</Router>
|
</Router>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { A, useNavigate } from "@solidjs/router";
|
import { useNavigate } from "@solidjs/router";
|
||||||
import { createSignal, onMount, createMemo, Show, For } from "solid-js";
|
import { createSignal, onMount, createMemo, Show, For } from "solid-js";
|
||||||
import { useI18n } from "../providers/i18n-provider";
|
import { useI18n } from "../providers/i18n-provider";
|
||||||
import { useTheme } from "../providers/theme-provider";
|
import { useTheme } from "../providers/theme-provider";
|
||||||
@@ -8,6 +8,7 @@ import { DashboardMockup } from "../components/dashboard-mockup";
|
|||||||
import { VideoPlayer } from "../components/video-player";
|
import { VideoPlayer } from "../components/video-player";
|
||||||
import { AnimatedList } from "../components/animated-list";
|
import { AnimatedList } from "../components/animated-list";
|
||||||
import { FloatingDock } from "../components/floating-dock";
|
import { FloatingDock } from "../components/floating-dock";
|
||||||
|
import { isAppHost, appUrl, WAITLIST_MAILTO } from "../lib/app-host";
|
||||||
|
|
||||||
const HomeLogo = () => {
|
const HomeLogo = () => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
@@ -228,13 +229,16 @@ export function HomeRoute() {
|
|||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
// Pre-launch: signup CTAs collect launch notifications on www; on the app host they enter the app.
|
||||||
|
const primaryHref = () => (isAppHost() ? "/dashboard" : WAITLIST_MAILTO);
|
||||||
|
const primaryLabel = () =>
|
||||||
|
isAppHost() ? i18n.t("home.hero.cta.primary") : isCs() ? "Oznamte mi spuštění" : "Notify me at launch";
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
setIsVisible(true);
|
setIsVisible(true);
|
||||||
|
|
||||||
// Demo mode: redirect landing page to dashboard immediately
|
// Demo mode: redirect landing page to dashboard immediately
|
||||||
const hostname = window.location.hostname;
|
if (isAppHost()) {
|
||||||
const isDemo = hostname.includes("demo") || hostname === "localhost" || hostname === "127.0.0.1";
|
|
||||||
if (isDemo) {
|
|
||||||
navigate("/dashboard");
|
navigate("/dashboard");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -379,15 +383,15 @@ export function HomeRoute() {
|
|||||||
{i18n.t("home.hero.subtitle")}
|
{i18n.t("home.hero.subtitle")}
|
||||||
</p>
|
</p>
|
||||||
<div class="flex flex-col sm:flex-row items-center justify-center gap-4 animate-slide-up" style={{ "animation-delay": "0.3s" }}>
|
<div class="flex flex-col sm:flex-row items-center justify-center gap-4 animate-slide-up" style={{ "animation-delay": "0.3s" }}>
|
||||||
<A
|
<a
|
||||||
href="/dashboard"
|
href={primaryHref()}
|
||||||
class="btn-primary inline-flex items-center gap-2 text-base px-8 py-4 group"
|
class="btn-primary inline-flex items-center gap-2 text-base px-8 py-4 group"
|
||||||
>
|
>
|
||||||
{i18n.t("home.hero.cta.primary")}
|
{primaryLabel()}
|
||||||
<span class="transition-transform duration-200 group-hover:translate-x-1">
|
<span class="transition-transform duration-200 group-hover:translate-x-1">
|
||||||
<ArrowRightIcon />
|
<ArrowRightIcon />
|
||||||
</span>
|
</span>
|
||||||
</A>
|
</a>
|
||||||
<a
|
<a
|
||||||
href="https://demo.bookra.eu"
|
href="https://demo.bookra.eu"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
@@ -560,7 +564,7 @@ export function HomeRoute() {
|
|||||||
description: isCs()
|
description: isCs()
|
||||||
? "Sledujte KPI, trendy rezervací a dnešní agenda v reálném čase."
|
? "Sledujte KPI, trendy rezervací a dnešní agenda v reálném čase."
|
||||||
: "Track KPIs, booking trends, and today's agenda in real time.",
|
: "Track KPIs, booking trends, and today's agenda in real time.",
|
||||||
href: "/dashboard",
|
href: appUrl("/dashboard"),
|
||||||
children: <DashboardMockup />,
|
children: <DashboardMockup />,
|
||||||
fadeBottom: true,
|
fadeBottom: true,
|
||||||
},
|
},
|
||||||
@@ -569,7 +573,7 @@ export function HomeRoute() {
|
|||||||
description: isCs()
|
description: isCs()
|
||||||
? "Intuitivní týdenní a měsíční pohled s drag-and-drop plánováním."
|
? "Intuitivní týdenní a měsíční pohled s drag-and-drop plánováním."
|
||||||
: "Intuitive weekly and monthly views with drag-and-drop scheduling.",
|
: "Intuitive weekly and monthly views with drag-and-drop scheduling.",
|
||||||
href: "/dashboard/bookings",
|
href: appUrl("/dashboard/bookings"),
|
||||||
children: (
|
children: (
|
||||||
<div class="w-full h-full flex flex-col gap-2">
|
<div class="w-full h-full flex flex-col gap-2">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
@@ -620,7 +624,7 @@ export function HomeRoute() {
|
|||||||
description: isCs()
|
description: isCs()
|
||||||
? "Historie návštěv, poznámky a preference na jednom místě."
|
? "Historie návštěv, poznámky a preference na jednom místě."
|
||||||
: "Visit history, notes, and preferences all in one place.",
|
: "Visit history, notes, and preferences all in one place.",
|
||||||
href: "/dashboard/customers",
|
href: appUrl("/dashboard/customers"),
|
||||||
children: (
|
children: (
|
||||||
<div class="w-full h-full flex flex-col gap-2">
|
<div class="w-full h-full flex flex-col gap-2">
|
||||||
<div class="flex items-center gap-2 mb-1">
|
<div class="flex items-center gap-2 mb-1">
|
||||||
@@ -654,7 +658,7 @@ export function HomeRoute() {
|
|||||||
description: isCs()
|
description: isCs()
|
||||||
? "Podrobné statistiky, exporty a přehledy pro váš podnik."
|
? "Podrobné statistiky, exporty a přehledy pro váš podnik."
|
||||||
: "Detailed statistics, exports, and business insights.",
|
: "Detailed statistics, exports, and business insights.",
|
||||||
href: "/dashboard?tab=analytics",
|
href: appUrl("/dashboard"),
|
||||||
children: (
|
children: (
|
||||||
<div class="w-full h-full flex flex-col gap-3">
|
<div class="w-full h-full flex flex-col gap-3">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
@@ -709,13 +713,13 @@ export function HomeRoute() {
|
|||||||
{i18n.t("home.how.subtitle")}
|
{i18n.t("home.how.subtitle")}
|
||||||
</p>
|
</p>
|
||||||
<div class="flex items-center gap-4 mb-8">
|
<div class="flex items-center gap-4 mb-8">
|
||||||
<A
|
<a
|
||||||
href="/dashboard"
|
href={primaryHref()}
|
||||||
class="btn-primary inline-flex items-center gap-2"
|
class="btn-primary inline-flex items-center gap-2"
|
||||||
>
|
>
|
||||||
{i18n.t("home.how.cta")}
|
{primaryLabel()}
|
||||||
<ArrowRightIcon />
|
<ArrowRightIcon />
|
||||||
</A>
|
</a>
|
||||||
{/* Character guide */}
|
{/* Character guide */}
|
||||||
<div class="hidden sm:block">
|
<div class="hidden sm:block">
|
||||||
<BookraCharacter pose="walk" size="sm" animate={true} />
|
<BookraCharacter pose="walk" size="sm" animate={true} />
|
||||||
@@ -801,6 +805,13 @@ export function HomeRoute() {
|
|||||||
<p class="text-lg text-ink-muted animate-slide-up" style={{ "animation-delay": "0.2s" }}>
|
<p class="text-lg text-ink-muted animate-slide-up" style={{ "animation-delay": "0.2s" }}>
|
||||||
{i18n.t("home.pricing.subtitle")}
|
{i18n.t("home.pricing.subtitle")}
|
||||||
</p>
|
</p>
|
||||||
|
<Show when={!isAppHost()}>
|
||||||
|
<p class="text-sm text-ink-subtle mt-4 animate-slide-up" style={{ "animation-delay": "0.25s" }}>
|
||||||
|
{isCs()
|
||||||
|
? "Bookra se spouští brzy. Předplatné zatím nelze zakoupit, ceny jsou orientační."
|
||||||
|
: "Bookra launches soon. Subscriptions are not yet available; prices are indicative."}
|
||||||
|
</p>
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Billing Toggle */}
|
{/* Billing Toggle */}
|
||||||
@@ -899,12 +910,12 @@ export function HomeRoute() {
|
|||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
<A
|
<a
|
||||||
href="/dashboard"
|
href={isAppHost() ? "/dashboard" : WAITLIST_MAILTO}
|
||||||
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'}`}
|
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}
|
{isAppHost() || plan.id === "business" ? plan.cta : primaryLabel()}
|
||||||
</A>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@@ -984,15 +995,15 @@ export function HomeRoute() {
|
|||||||
{i18n.t("home.cta.subtitle")}
|
{i18n.t("home.cta.subtitle")}
|
||||||
</p>
|
</p>
|
||||||
<div class="flex flex-col sm:flex-row items-center justify-center gap-4 animate-slide-up" style={{ "animation-delay": "0.2s" }}>
|
<div class="flex flex-col sm:flex-row items-center justify-center gap-4 animate-slide-up" style={{ "animation-delay": "0.2s" }}>
|
||||||
<A
|
<a
|
||||||
href="/dashboard"
|
href={primaryHref()}
|
||||||
class="btn-primary inline-flex items-center gap-2 text-base px-8 py-4 group shadow-lg hover:shadow-xl transition-all duration-300"
|
class="btn-primary inline-flex items-center gap-2 text-base px-8 py-4 group shadow-lg hover:shadow-xl transition-all duration-300"
|
||||||
>
|
>
|
||||||
{i18n.t("home.cta.primary")}
|
{primaryLabel()}
|
||||||
<span class="transition-transform duration-200 group-hover:translate-x-1">
|
<span class="transition-transform duration-200 group-hover:translate-x-1">
|
||||||
<ArrowRightIcon />
|
<ArrowRightIcon />
|
||||||
</span>
|
</span>
|
||||||
</A>
|
</a>
|
||||||
<a
|
<a
|
||||||
href="https://demo.bookra.eu"
|
href="https://demo.bookra.eu"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
@@ -1053,10 +1064,10 @@ export function HomeRoute() {
|
|||||||
: "New bookings, reminders, and alerts — everything at a glance."}
|
: "New bookings, reminders, and alerts — everything at a glance."}
|
||||||
</p>
|
</p>
|
||||||
<div class="flex items-center gap-4 animate-slide-up" style={{ "animation-delay": "0.3s" }}>
|
<div class="flex items-center gap-4 animate-slide-up" style={{ "animation-delay": "0.3s" }}>
|
||||||
<A href="/dashboard" class="btn-primary inline-flex items-center gap-2">
|
<a href={primaryHref()} class="btn-primary inline-flex items-center gap-2">
|
||||||
{i18n.t("home.cta.primary")}
|
{primaryLabel()}
|
||||||
<ArrowRightIcon />
|
<ArrowRightIcon />
|
||||||
</A>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="animate-slide-up" style={{ "animation-delay": "0.3s" }}>
|
<div class="animate-slide-up" style={{ "animation-delay": "0.3s" }}>
|
||||||
@@ -1213,10 +1224,10 @@ export function HomeRoute() {
|
|||||||
: "Manage bookings, customers, and your team from anywhere. An intuitive interface designed for mobile."}
|
: "Manage bookings, customers, and your team from anywhere. An intuitive interface designed for mobile."}
|
||||||
</p>
|
</p>
|
||||||
<div class="flex items-center gap-4 animate-slide-up" style={{ "animation-delay": "0.3s" }}>
|
<div class="flex items-center gap-4 animate-slide-up" style={{ "animation-delay": "0.3s" }}>
|
||||||
<A href="/dashboard" class="btn-primary inline-flex items-center gap-2">
|
<a href={primaryHref()} class="btn-primary inline-flex items-center gap-2">
|
||||||
{i18n.t("home.cta.primary")}
|
{primaryLabel()}
|
||||||
<ArrowRightIcon />
|
<ArrowRightIcon />
|
||||||
</A>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { createSignal, createMemo, Show, For } from "solid-js";
|
import { createSignal, createMemo, Show, For } from "solid-js";
|
||||||
import { useNavigate } from "@solidjs/router";
|
import { useNavigate } from "@solidjs/router";
|
||||||
import { useI18n } from "../providers/i18n-provider";
|
import { useI18n } from "../providers/i18n-provider";
|
||||||
|
import { isAppHost, WAITLIST_MAILTO } from "../lib/app-host";
|
||||||
|
|
||||||
const PricingRoute = () => {
|
const PricingRoute = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -81,9 +82,14 @@ const PricingRoute = () => {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
const handleSelectPlan = (planId: string) => {
|
const handleSelectPlan = (planId: string) => {
|
||||||
// Redirect to signup with plan selection
|
// Pre-launch: www collects launch notifications; the app host keeps the signup flow.
|
||||||
|
if (!isAppHost()) {
|
||||||
|
window.location.href = WAITLIST_MAILTO;
|
||||||
|
return;
|
||||||
|
}
|
||||||
navigate("/?signup=true&plan=" + planId + "&billing=" + billingInterval());
|
navigate("/?signup=true&plan=" + planId + "&billing=" + billingInterval());
|
||||||
};
|
};
|
||||||
|
const waitlistLabel = () => (isCs() ? "Oznamte mi spuštění" : "Notify me at launch");
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
@@ -179,6 +185,13 @@ const PricingRoute = () => {
|
|||||||
? "Vyberte si plán, který vyhovuje vašemu podnikání. Žádné skryté poplatky, žádné překvapení."
|
? "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."}
|
: "Choose a plan that fits your business. No hidden fees, no surprises."}
|
||||||
</p>
|
</p>
|
||||||
|
<Show when={!isAppHost()}>
|
||||||
|
<p class="text-sm text-ink-subtle mb-8 animate-slide-up" style={{ "animation-delay": "0.15s" }}>
|
||||||
|
{isCs()
|
||||||
|
? "Bookra se spouští brzy. Předplatné zatím nelze zakoupit, ceny jsou orientační."
|
||||||
|
: "Bookra launches soon. Subscriptions are not yet available; prices are indicative."}
|
||||||
|
</p>
|
||||||
|
</Show>
|
||||||
|
|
||||||
{/* Billing Toggle */}
|
{/* Billing Toggle */}
|
||||||
<div class="flex items-center justify-center mb-8 animate-slide-up" style={{ "animation-delay": "0.2s" }}>
|
<div class="flex items-center justify-center mb-8 animate-slide-up" style={{ "animation-delay": "0.2s" }}>
|
||||||
@@ -285,7 +298,7 @@ const PricingRoute = () => {
|
|||||||
: 'bg-ink text-canvas hover:bg-ink/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}
|
{isAppHost() || plan.id === "business" ? plan.cta : waitlistLabel()}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -389,14 +402,14 @@ const PricingRoute = () => {
|
|||||||
</h2>
|
</h2>
|
||||||
<p class="text-ink-muted mb-6">
|
<p class="text-ink-muted mb-6">
|
||||||
{isCs()
|
{isCs()
|
||||||
? "Začněte s bezplatným 15denním trial a rozhodněte se později."
|
? "Bookra se spouští brzy. Nechte nám e-mail a po spuštění dostanete 15denní trial zdarma."
|
||||||
: "Start with a free 15-day trial and decide later."}
|
: "Bookra launches soon. Leave us your email and get a free 15-day trial at launch."}
|
||||||
</p>
|
</p>
|
||||||
<a
|
<a
|
||||||
href="/dashboard?signup=true"
|
href={isAppHost() ? "/dashboard?signup=true" : WAITLIST_MAILTO}
|
||||||
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"
|
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"}
|
{isAppHost() ? (isCs() ? "Začít zdarma" : "Start for free") : waitlistLabel()}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
{"root":["./src/App.tsx","./src/main.tsx","./src/components/animated-list.tsx","./src/components/bookra-character.tsx","./src/components/calendar-feed.tsx","./src/components/dashboard-mockup.tsx","./src/components/floating-dock.tsx","./src/components/hover-feature-cards.tsx","./src/components/index.ts","./src/components/integration-modal.tsx","./src/components/location-map.tsx","./src/components/pinned-list.tsx","./src/components/shell.tsx","./src/components/sms-settings.tsx","./src/components/video-player.tsx","./src/components/widget-builder.tsx","./src/components/dashboard/activity-timeline.tsx","./src/components/dashboard/calendar-view.tsx","./src/components/dashboard/export-csv-button.tsx","./src/components/dashboard/icons.tsx","./src/components/dashboard/kpi-card.tsx","./src/components/dashboard/notification-dropdown.tsx","./src/components/dashboard/revenue-chart.tsx","./src/components/dashboard/types.ts","./src/components/ui/avatar.tsx","./src/components/ui/badge.tsx","./src/components/ui/button.tsx","./src/components/ui/card.tsx","./src/components/ui/dialog.tsx","./src/components/ui/index.ts","./src/components/ui/input.tsx","./src/components/ui/select.tsx","./src/components/ui/skeleton.tsx","./src/components/ui/tabs.tsx","./src/components/ui/textarea.tsx","./src/components/ui/tooltip.tsx","./src/lib/api-client.ts","./src/lib/map.ts","./src/lib/sentry.ts","./src/lib/types.ts","./src/providers/auth-provider.tsx","./src/providers/i18n-provider.tsx","./src/providers/theme-provider.tsx","./src/routes/about-route.tsx","./src/routes/auth-callback-route.tsx","./src/routes/booking-manage-route.tsx","./src/routes/contact-route.tsx","./src/routes/home-route.tsx","./src/routes/legal-route.tsx","./src/routes/not-found-route.tsx","./src/routes/pricing-route.tsx","./src/routes/public-booking-route.tsx","./src/routes/dashboard/billing-page.tsx","./src/routes/dashboard/bookings-page.tsx","./src/routes/dashboard/customers-page.tsx","./src/routes/dashboard/dashboard-data.ts","./src/routes/dashboard/layout.tsx","./src/routes/dashboard/overview-page.tsx","./src/routes/dashboard/services-page.tsx","./src/routes/dashboard/settings-page.tsx","./src/routes/dashboard/staff-page.tsx","./src/routes/dashboard/zones-page.tsx","./vite.config.ts"],"version":"5.9.3"}
|
{"root":["./src/App.tsx","./src/main.tsx","./src/components/animated-list.tsx","./src/components/bookra-character.tsx","./src/components/calendar-feed.tsx","./src/components/dashboard-mockup.tsx","./src/components/floating-dock.tsx","./src/components/hover-feature-cards.tsx","./src/components/index.ts","./src/components/integration-modal.tsx","./src/components/location-map.tsx","./src/components/pinned-list.tsx","./src/components/shell.tsx","./src/components/sms-settings.tsx","./src/components/video-player.tsx","./src/components/widget-builder.tsx","./src/components/dashboard/activity-timeline.tsx","./src/components/dashboard/calendar-view.tsx","./src/components/dashboard/export-csv-button.tsx","./src/components/dashboard/icons.tsx","./src/components/dashboard/kpi-card.tsx","./src/components/dashboard/notification-dropdown.tsx","./src/components/dashboard/revenue-chart.tsx","./src/components/dashboard/types.ts","./src/components/ui/avatar.tsx","./src/components/ui/badge.tsx","./src/components/ui/button.tsx","./src/components/ui/card.tsx","./src/components/ui/dialog.tsx","./src/components/ui/index.ts","./src/components/ui/input.tsx","./src/components/ui/select.tsx","./src/components/ui/skeleton.tsx","./src/components/ui/tabs.tsx","./src/components/ui/textarea.tsx","./src/components/ui/tooltip.tsx","./src/lib/api-client.ts","./src/lib/app-host.ts","./src/lib/map.ts","./src/lib/sentry.ts","./src/lib/types.ts","./src/providers/auth-provider.tsx","./src/providers/i18n-provider.tsx","./src/providers/theme-provider.tsx","./src/routes/about-route.tsx","./src/routes/auth-callback-route.tsx","./src/routes/booking-manage-route.tsx","./src/routes/contact-route.tsx","./src/routes/home-route.tsx","./src/routes/legal-route.tsx","./src/routes/not-found-route.tsx","./src/routes/pricing-route.tsx","./src/routes/public-booking-route.tsx","./src/routes/dashboard/billing-page.tsx","./src/routes/dashboard/bookings-page.tsx","./src/routes/dashboard/customers-page.tsx","./src/routes/dashboard/dashboard-data.ts","./src/routes/dashboard/layout.tsx","./src/routes/dashboard/overview-page.tsx","./src/routes/dashboard/services-page.tsx","./src/routes/dashboard/settings-page.tsx","./src/routes/dashboard/staff-page.tsx","./src/routes/dashboard/zones-page.tsx","./vite.config.ts"],"version":"5.9.3"}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"rewrites": [{ "source": "/(.*)", "destination": "/index.html" }]
|
||||||
|
}
|
||||||
Reference in new issue
Block a user