mirror of
https://github.com/Dvorinka/Bookra.git
synced 2026-06-04 20:43:01 +00:00
first commit
This commit is contained in:
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user