first commit

This commit is contained in:
Tomas Dvorak
2026-04-10 12:01:36 +02:00
commit 035ac8ddb5
61 changed files with 6600 additions and 0 deletions
@@ -0,0 +1,92 @@
import {
createContext,
createEffect,
createSignal,
ParentComponent,
useContext,
} from "solid-js";
import { createAuthClient } from "@neondatabase/neon-js/auth";
import type { AuthSession } from "../lib/types";
const neonAuthUrl = import.meta.env.VITE_NEON_AUTH_URL ?? "";
const authClient = neonAuthUrl ? createAuthClient(neonAuthUrl) : null;
type AuthContextValue = {
session: () => AuthSession | null;
loading: () => boolean;
getToken: () => Promise<string | null>;
signInDemo: () => Promise<void>;
signOut: () => Promise<void>;
};
const AuthContext = createContext<AuthContextValue>();
export const AuthProvider: ParentComponent = (props) => {
const [session, setSession] = createSignal<AuthSession | null>(null);
const [loading, setLoading] = createSignal(true);
createEffect(() => {
void (async () => {
if (!authClient) {
setLoading(false);
return;
}
try {
const response = await authClient.getSession();
setSession((response?.data as unknown as AuthSession | undefined) ?? null);
} catch {
setSession(null);
} finally {
setLoading(false);
}
})();
});
const value: AuthContextValue = {
session,
loading,
async getToken() {
if (!authClient) return null;
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",
});
const response = await authClient.getSession();
setSession((response?.data as unknown as AuthSession | undefined) ?? null);
},
async signOut() {
if (!authClient) return;
await authClient.signOut();
setSession(null);
},
};
return <AuthContext.Provider value={value}>{props.children}</AuthContext.Provider>;
};
export function useAuth() {
const context = useContext(AuthContext);
if (!context) {
throw new Error("AuthProvider is missing from the component tree.");
}
return context;
}
@@ -0,0 +1,148 @@
import {
createContext,
createMemo,
createSignal,
ParentComponent,
useContext,
} from "solid-js";
import { defaultLocale, locales } from "@bookra/shared-types";
import type { Locale } from "@bookra/shared-types";
const dictionaries = {
cs: {
"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",
"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.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.",
},
en: {
"nav.booking": "Public booking",
"nav.dashboard": "App",
"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.",
"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",
"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.name": "Business name",
"dashboard.onboarding.slug": "Slug",
"dashboard.onboarding.preset": "Preset",
"dashboard.onboarding.locale": "Locale",
"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.",
},
} satisfies Record<Locale, Record<string, string>>;
type I18nContextValue = {
locale: () => Locale;
t: (key: string) => string;
toggleLocale: () => void;
};
const I18nContext = createContext<I18nContextValue>();
export const I18nProvider: ParentComponent = (props) => {
const initial = (import.meta.env.VITE_DEFAULT_LOCALE as Locale | undefined) ?? defaultLocale;
const [locale, setLocale] = createSignal<Locale>(locales.includes(initial) ? initial : defaultLocale);
const dictionary = createMemo(() => dictionaries[locale()]);
return (
<I18nContext.Provider
value={{
locale,
t(key: string) {
return dictionary()[key as keyof (typeof dictionaries)[Locale]] ?? key;
},
toggleLocale() {
setLocale((value) => (value === "cs" ? "en" : "cs"));
},
}}
>
{props.children}
</I18nContext.Provider>
);
};
export function useI18n() {
const context = useContext(I18nContext);
if (!context) {
throw new Error("I18nProvider is missing from the component tree.");
}
return context;
}