mirror of
https://github.com/Dvorinka/Bookra.git
synced 2026-06-03 20:13:00 +00:00
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:
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user