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 committed 2026-05-18 14:31:20 +02:00
1 parent 9d63fa7620
commit da5ba13eab
41 files changed
+8761 -184

No files matched your search

@@ -0,0 +1,156 @@
import { Show, createSignal } from "solid-js";
import { Input } from "../../components/ui/input";
import { Select } from "../../components/ui/select";
import { LocationMap } from "../../components/location-map";
import { MapPinIcon, PlusIcon, XIcon, BanIcon, ClockIcon, SparklesIcon } from "../../components/dashboard/icons";
import { DashboardLayout, useDashboardData } from "./layout";
function ZonesPage() {
const data = useDashboardData();
const [activeTab, setActiveTab] = createSignal<"zones" | "blocked" | "hours">("zones");
const [showAddZone, setShowAddZone] = createSignal(false);
const [newZoneName, setNewZoneName] = createSignal("");
const [newZoneType, setNewZoneType] = createSignal("room");
const [newZoneCapacity, setNewZoneCapacity] = createSignal(10);
const [showUpgradePrompt, setShowUpgradePrompt] = createSignal(false);
const resolvedLocations = () => [
{ id: "z1", name: "Main Hall", type: "hall", capacity: 20, bookingsToday: 5 },
{ id: "z2", name: "Private Room A", type: "private", capacity: 4, bookingsToday: 2 },
{ id: "z3", name: "Treatment Room", type: "room", capacity: 2, bookingsToday: 8 },
];
const locationLimit = () => data.resolvedBilling()?.entitlements?.maxLocations ?? 3;
const canAddLocation = () => resolvedLocations().length < locationLimit();
const handleAddZone = () => {
if (!canAddLocation()) { setShowUpgradePrompt(true); setShowAddZone(false); return; }
setNewZoneName(""); setNewZoneType("room"); setNewZoneCapacity(10); setShowAddZone(false);
};
const typeLabel = (type: string) => {
const map: Record<string, string> = { room: data.i18n.t("dashboard.zone.rooms"), private: data.i18n.t("dashboard.zone.private"), hall: data.i18n.t("dashboard.zone.hall") };
return map[type] || type;
};
return (
<div class="space-y-6">
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<div>
<h1 class="text-2xl sm:text-3xl font-bold text-ink font-display">{data.i18n.t("dashboard.zones")}</h1>
<p class="text-ink-muted mt-1">{resolvedLocations().length} {data.i18n.locale() === "cs" ? "zon" : "zones"}</p>
</div>
</div>
<div class="flex gap-2 p-1 bg-canvas-subtle rounded-xl w-fit">
{[
{ id: "zones", label: data.i18n.t("dashboard.zones"), icon: MapPinIcon },
{ id: "blocked", label: data.i18n.t("dashboard.zone.blockedDays"), icon: BanIcon },
{ id: "hours", label: data.i18n.t("dashboard.zone.workingHours"), icon: ClockIcon },
].map((tab) => (
<button onClick={() => setActiveTab(tab.id as any)} class={`flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-all ${activeTab() === tab.id ? "bg-canvas text-accent shadow-sm" : "text-ink-muted hover:text-ink"}`}>
<tab.icon /> {tab.label}
</button>
))}
</div>
{activeTab() === "zones" && (
<div class="space-y-6">
<LocationMap
coordinates={{ latitude: 50.0755, longitude: 14.4378, zoom: 14, address: data.i18n.locale() === "cs" ? "Praha, Česko" : "Prague, Czechia", source: "coordinates" }}
markerColor={data.resolvedBootstrap()?.brand?.primaryColor}
height={320}
class="shadow-sm"
/>
<div class="surface-card overflow-hidden">
<div class="p-6 border-b border-border flex items-center justify-between">
<h3 class="text-lg font-display font-semibold text-ink">{data.i18n.t("dashboard.zones")}</h3>
<Show when={canAddLocation()} fallback={
<button onClick={() => setShowUpgradePrompt(true)} class="flex items-center gap-2 px-4 py-2 bg-canvas-subtle text-ink-muted border border-border rounded-lg text-sm opacity-60 cursor-not-allowed">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect width="18" height="11" x="3" y="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
{data.i18n.t("dashboard.zone.limitReached")}
</button>
}>
<button onClick={() => setShowAddZone(true)} class="btn-primary text-sm"><PlusIcon /> {data.i18n.t("dashboard.zone.add")}</button>
</Show>
</div>
<Show when={showAddZone()}>
<div class="p-6 border-b border-border bg-canvas-subtle/50">
<h4 class="font-medium text-ink mb-4">{data.i18n.t("dashboard.zone.add")}</h4>
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4">
<Input type="text" label={data.i18n.t("dashboard.zone.name")} value={newZoneName()} onInput={(e) => setNewZoneName(e.currentTarget.value)} placeholder={data.i18n.locale() === "cs" ? "např. Hlavní sál" : "e.g. Main Hall"} />
<Select label={data.i18n.t("dashboard.zone.type")} value={newZoneType()} onChange={(v) => setNewZoneType(v)} options={[{ value: "room", label: data.i18n.t("dashboard.zone.rooms") }, { value: "private", label: data.i18n.t("dashboard.zone.private") }, { value: "hall", label: data.i18n.t("dashboard.zone.hall") }]} />
<Input type="number" label={data.i18n.t("dashboard.zone.capacity")} value={newZoneCapacity()} onInput={(e) => setNewZoneCapacity(parseInt(e.currentTarget.value) || 10)} />
</div>
<div class="flex gap-3 mt-4">
<button onClick={handleAddZone} class="btn-primary text-sm">{data.i18n.t("dashboard.settings.save")}</button>
<button onClick={() => setShowAddZone(false)} class="btn-secondary text-sm">{data.i18n.t("common.cancel")}</button>
</div>
</div>
</Show>
<div class="divide-y divide-border/60">
{resolvedLocations().map((zone: any) => (
<div class="flex items-center justify-between p-6 hover:bg-canvas-subtle/30 transition-colors">
<div class="flex items-center gap-4">
<div class="w-12 h-12 rounded-xl bg-accent-subtle/50 flex items-center justify-center text-accent"><MapPinIcon /></div>
<div>
<h4 class="font-medium text-ink">{zone.name}</h4>
<p class="text-sm text-ink-muted">{typeLabel(zone.type)} &bull; {data.i18n.t("dashboard.zone.capacity")}: {zone.capacity}</p>
</div>
</div>
<div class="text-right">
<p class="text-sm font-medium text-ink">{zone.bookingsToday}</p>
<p class="text-sm text-ink-muted">{zone.bookingsToday} {data.i18n.locale() === "cs" ? "rezervací dnes" : "bookings today"}</p>
</div>
</div>
))}
</div>
</div>
</div>
)}
{activeTab() === "blocked" && (
<div class="surface-card p-12 text-center">
<div class="w-16 h-16 rounded-full bg-canvas-subtle flex items-center justify-center mx-auto mb-4"><BanIcon /></div>
<p class="text-ink-muted font-medium">{data.i18n.t("dashboard.empty.title")}</p>
<p class="text-sm text-ink-muted">{data.i18n.locale() === "cs" ? "Spravujte zóny, blokované dny a pracovní dobu." : "Manage locations, blocked days and working hours."}</p>
</div>
)}
{activeTab() === "hours" && (
<div class="surface-card p-12 text-center">
<div class="w-16 h-16 rounded-full bg-canvas-subtle flex items-center justify-center mx-auto mb-4"><ClockIcon /></div>
<p class="text-ink-muted font-medium">{data.i18n.t("dashboard.empty.title")}</p>
<p class="text-ink-muted text-sm">{data.i18n.locale() === "cs" ? "Nastavení pracovní doby bude dostupné brzy." : "Working hours settings coming soon."}</p>
</div>
)}
<Show when={showUpgradePrompt()}>
<div class="fixed inset-0 z-50 flex items-center justify-center p-4">
<div class="absolute inset-0 bg-ink/50 backdrop-blur-sm" onClick={() => setShowUpgradePrompt(false)} />
<div class="relative bg-canvas rounded-2xl shadow-2xl w-full max-w-md p-6 animate-scale-in">
<div class="flex items-start gap-4">
<div class="w-10 h-10 rounded-full bg-accent-subtle flex items-center justify-center shrink-0"><SparklesIcon /></div>
<div class="flex-1">
<h4 class="font-medium text-ink mb-1">{data.i18n.t("dashboard.locationLimitReached")}</h4>
<p class="text-sm text-ink-muted">{data.i18n.locale() === "cs" ? "Váš plán umožňuje pouze omezený počet lokací. Pro více lokací upgradujte." : "Your plan only allows a limited number of locations. Upgrade for more."}</p>
<div class="flex gap-3">
<button onClick={() => { setShowUpgradePrompt(false); void data.openCheckout(); }} class="px-4 py-2 bg-accent text-canvas text-sm font-medium rounded-lg hover:bg-accent-hover transition-colors">{data.i18n.t("dashboard.upgrade")}</button>
<button onClick={() => setShowUpgradePrompt(false)} class="px-4 py-2 bg-canvas-subtle text-ink-muted text-sm font-medium rounded-lg hover:bg-canvas transition-colors">{data.i18n.t("dashboard.close")}</button>
</div>
</div>
</div>
</div>
</div>
</Show>
</div>
);
}
export default function ZonesRoute() {
return (
<DashboardLayout>
<ZonesPage />
</DashboardLayout>
);
}