feat(ui): implement comprehensive dashboard and enhance frontend experience

This commit introduces a major overhaul of the user interface, transitioning from a basic structure to a feature-rich dashboard system. Key improvements include:

- **Dashboard Implementation**: Added a complete dashboard routing system with dedicated pages for Overview, Bookings, Customers, Zones, Billing, and Settings.
- **New UI Components**: Introduced a variety of high-quality components including `AnimatedList`, `FloatingDock`, `HoverFeatureCards`, `VideoPlayer` (with ambient glow effect), `PinnedList`, and `DashboardMockup`.
- **Enhanced Dashboard Features**:
    - Integrated real-time KPI cards and activity timelines.
    - Implemented a multi-view calendar system.
    - Added customer and booking management interfaces with filtering and search capabilities.
    - Added a zone/location management view with map integration.
- **Branding & Visuals**: Updated the application with new SVG logos (horizontal and vertical variants) and implemented dark/light mode optimized branding.
- **Internationalization**: Expanded i18n support with comprehensive Czech and English translations for the new dashboard and integration modules.
- **Integration Tools**: Added a new `IntegrationModal` allowing users to easily embed Bookra widgets via HTML, React, SolidJS, or PHP.
- **Backend Support**: Updated the booking service to provide comprehensive dashboard summary data, including historical booking records for charts.
This commit is contained in:
Tomas Dvorak
2026-05-18 14:31:20 +02:00
parent 9d63fa7620
commit da5ba13eab
41 changed files with 8761 additions and 184 deletions
@@ -0,0 +1,101 @@
import { Show, createMemo } from "solid-js";
import { SparklesIcon, XIcon } from "../../components/dashboard/icons";
import { DashboardLayout, useDashboardData } from "./layout";
function BillingPage() {
const data = useDashboardData();
const current = data.resolvedBilling();
const entitlements = current?.entitlements;
const isTrialing = current?.subscriptionStatus === "trialing";
const isActive = current?.subscriptionStatus === "active";
const planCode = current?.planCode ?? "starter";
const planName = current?.planName ?? "Starter";
const trialDays = current?.trialDaysRemaining ?? 0;
const planFeatures = createMemo(() => {
const cs = data.i18n.locale() === "cs";
const base = [
{ label: data.i18n.t("dashboard.billing.locations"), value: entitlements?.maxLocations ?? 1, limit: entitlements?.maxLocations ?? 1 },
{ label: data.i18n.t("dashboard.billing.bookings"), value: entitlements?.maxBookings === -1 ? "\u221e" : (entitlements?.maxBookings ?? 50), limit: entitlements?.maxBookings ?? 50 },
{ label: data.i18n.t("dashboard.billing.staff"), value: entitlements?.maxStaff ?? 1, limit: entitlements?.maxStaff ?? 1 },
];
if (planCode === "pro" || planCode === "business") {
base.push({ label: cs ? "Emailove pripominky" : "Email reminders", value: "\u2713", limit: "\u2713" });
base.push({ label: cs ? "Analytika" : "Analytics", value: "\u2713", limit: "\u2713" });
}
if (planCode === "business") {
base.push({ label: cs ? "API pristup" : "API access", value: "\u2713", limit: "\u2713" });
}
return base;
});
return (
<div class="space-y-6">
<div>
<h1 class="text-2xl sm:text-3xl font-bold text-ink font-display">{data.i18n.t("dashboard.billing")}</h1>
<p class="text-ink-muted mt-1">{data.i18n.t("dashboard.billing.currentPlan")}: {planName}</p>
</div>
{/* Billing notice/error */}
<Show when={data.billingNotice()}>
<div class="p-4 rounded-xl bg-accent-subtle border border-accent/20 flex items-center justify-between animate-fade-in">
<div class="flex items-center gap-3"><SparklesIcon /><p class="text-sm font-medium text-accent">{data.billingNotice()}</p></div>
<button onClick={() => data.setBillingNotice(null)} class="p-1.5 hover:bg-accent/10 rounded-lg text-accent"><XIcon /></button>
</div>
</Show>
<Show when={data.billingError()}>
<div class="p-4 rounded-xl bg-canvas-subtle border border-ink/10 flex items-center justify-between animate-fade-in">
<p class="text-sm font-medium text-ink">{data.billingError()}</p>
<button onClick={() => data.setBillingError(null)} class="p-1.5 hover:bg-canvas-muted rounded-lg text-ink-muted"><XIcon /></button>
</div>
</Show>
{/* Current Plan Card */}
<div class="surface-card p-6">
<div class="flex items-start justify-between gap-4 mb-6">
<div>
<div class="flex items-center gap-3 mb-2">
<h3 class="text-xl font-bold text-ink font-display">{planName}</h3>
{isTrialing && <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-accent/10 text-accent">{data.i18n.locale() === "cs" ? `Zkusebni doba (${trialDays} dnu)` : `Trial (${trialDays} days)`}</span>}
{isActive && <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-accent/10 text-accent">{data.i18n.t("dashboard.confirmed")}</span>}
</div>
<p class="text-sm text-ink-muted">{data.i18n.t("dashboard.billing.period")}: {data.billingInterval() === "monthly" ? (data.i18n.locale() === "cs" ? "Mesicne" : "Monthly") : (data.i18n.locale() === "cs" ? "Rocne" : "Yearly")}</p>
</div>
<div class="flex gap-2 shrink-0">
<button onClick={() => data.setBillingInterval("monthly")} class={`px-3 py-1.5 rounded-lg text-sm font-medium transition-all ${data.billingInterval() === "monthly" ? "bg-accent text-canvas" : "bg-canvas-subtle text-ink-muted hover:text-ink"}`}>{data.i18n.locale() === "cs" ? "Mesicne" : "Monthly"}</button>
<button onClick={() => data.setBillingInterval("yearly")} class={`px-3 py-1.5 rounded-lg text-sm font-medium transition-all ${data.billingInterval() === "yearly" ? "bg-accent text-canvas" : "bg-canvas-subtle text-ink-muted hover:text-ink"}`}>{data.i18n.locale() === "cs" ? "Rocne" : "Yearly"}</button>
</div>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
{planFeatures().map((feature: any) => (
<div class="p-4 bg-canvas-subtle rounded-xl">
<p class="text-xs text-ink-muted uppercase tracking-wider mb-1">{feature.label}</p>
<p class="text-lg font-semibold text-ink">{typeof feature.value === "number" ? `${feature.value} / ${feature.limit}` : feature.value}</p>
</div>
))}
</div>
<div class="flex flex-wrap gap-3 mt-6">
<button onClick={() => void data.openCheckout()} disabled={data.billingAction() === "checkout"} class="btn-primary disabled:opacity-50">
{data.billingAction() === "checkout" ? (data.i18n.locale() === "cs" ? "Otevirani..." : "Opening...") : (isTrialing ? data.i18n.t("dashboard.upgrade") : data.i18n.t("dashboard.checkout"))}
</button>
<Show when={isActive || isTrialing}>
<button onClick={() => void data.openPortal()} class="btn-secondary">{data.i18n.locale() === "cs" ? "Spravovat predplatne" : "Manage subscription"}</button>
</Show>
<button onClick={() => void data.refreshBilling()} disabled={data.billingAction() === "refresh"} class="btn-secondary disabled:opacity-50">
{data.billingAction() === "refresh" ? (data.i18n.locale() === "cs" ? "Obnovovani..." : "Refreshing...") : data.i18n.t("dashboard.refreshBilling")}
</button>
</div>
</div>
</div>
);
}
export default function BillingRoute() {
return (
<DashboardLayout>
<BillingPage />
</DashboardLayout>
);
}