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,223 @@
import { A, useNavigate } from "@solidjs/router";
import { Show } from "solid-js";
import { CalendarView } from "../../components/dashboard/calendar-view";
import { RevenueChart } from "../../components/dashboard/revenue-chart";
import { KpiCard } from "../../components/dashboard/kpi-card";
import { ActivityTimeline } from "../../components/dashboard/activity-timeline";
import { NotificationDropdown } from "../../components/dashboard/notification-dropdown";
import { getInitials } from "../../components/dashboard/types";
import { ChevronRightIcon, MailIcon, SparklesIcon } from "../../components/dashboard/icons";
import { PinnedList } from "../../components/pinned-list";
import { DashboardLayout, useDashboardData } from "./layout";
function OverviewPage() {
const data = useDashboardData();
const navigate = useNavigate();
return (
<div class="space-y-6">
{/* Header */}
<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 tracking-tight font-display">
{data.i18n.t("dashboard.welcome")} <span class="text-accent">{data.auth.session()?.user?.name}</span>
</h1>
<p class="text-ink-muted mt-1">{data.i18n.t("dashboard.overviewFor")} {data.resolvedBootstrap()?.tenantName}</p>
</div>
<div class="flex items-center gap-3 shrink-0">
<button onClick={() => data.setShowIntegrationModal(true)} class="btn-secondary text-sm">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/>
</svg>
{data.i18n.t("dashboard.shareManage")}
</button>
<span class="text-sm text-ink-subtle hidden sm:inline">{new Date().toLocaleDateString(data.i18n.locale() === "cs" ? "cs-CZ" : "en-US", { weekday: "long", month: "long", day: "numeric", hour: "2-digit", minute: "2-digit" })}</span>
<NotificationDropdown bookings={data.normalizedAllBookings()} billing={data.resolvedBilling()} />
</div>
</div>
{/* KPI Cards */}
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
{data.normalizedKpis().map((kpi: any, index: number) => (
<KpiCard kpi={kpi} index={index} />
))}
</div>
{/* Setup Guide */}
<div class="grid lg:grid-cols-3 gap-6">
<div class="lg:col-span-1 surface-card p-5">
<div class="flex items-center gap-3 mb-4">
<div class="w-9 h-9 rounded-lg bg-accent-subtle flex items-center justify-center text-accent">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5"/></svg>
</div>
<div>
<h3 class="font-semibold text-ink">{data.i18n.locale() === "cs" ? "Začínáme" : "Getting Started"}</h3>
<p class="text-xs text-ink-muted">{data.i18n.locale() === "cs" ? "Dokončete nastavení" : "Complete your setup"}</p>
</div>
</div>
<PinnedList
items={[
{
id: "setup-locations",
name: data.i18n.locale() === "cs" ? "Přidat lokace" : "Add locations",
subtitle: data.i18n.locale() === "cs" ? "Salon, klinika, studio" : "Salon, clinic, studio",
icon: <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0Z"/><circle cx="12" cy="10" r="3"/></svg>,
href: "/dashboard/zones",
},
{
id: "setup-hours",
name: data.i18n.locale() === "cs" ? "Nastavit provozní dobu" : "Set business hours",
subtitle: data.i18n.locale() === "cs" ? "Po–Ne, svátky" : "Mon–Sun, holidays",
icon: <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>,
href: "/dashboard/zones",
},
{
id: "setup-services",
name: data.i18n.locale() === "cs" ? "Definovat služby" : "Define services",
subtitle: data.i18n.locale() === "cs" ? "Ceník, délka, kapacita" : "Pricing, duration, capacity",
icon: <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/></svg>,
href: "/dashboard/settings",
},
{
id: "setup-widget",
name: data.i18n.locale() === "cs" ? "Vložit rezervační widget" : "Embed booking widget",
subtitle: data.i18n.locale() === "cs" ? "Web, Instagram, email" : "Website, Instagram, email",
icon: <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg>,
href: "/dashboard/settings",
},
{
id: "setup-team",
name: data.i18n.locale() === "cs" ? "Přidat členy týmu" : "Add team members",
subtitle: data.i18n.locale() === "cs" ? "Role, oprávnění" : "Roles, permissions",
icon: <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>,
href: "/dashboard/settings",
},
{
id: "setup-branding",
name: data.i18n.locale() === "cs" ? "Upravit branding" : "Customize branding",
subtitle: data.i18n.locale() === "cs" ? "Barva, logo, jazyk" : "Color, logo, language",
icon: <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="13.5" cy="6.5" r=".5" fill="currentColor"/><circle cx="17.5" cy="10.5" r=".5" fill="currentColor"/><circle cx="6.5" cy="12.5" r=".5" fill="currentColor"/><path d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10c.926 0 1.648-.746 1.648-1.688 0-.437-.18-.835-.437-1.125-.29-.289-.438-.652-.438-1.125a1.64 1.64 0 0 1 1.668-1.668h1.996c3.051 0 5.555-2.503 5.555-5.554C21.965 6.01 17.461 2 12 2z"/></svg>,
href: "/dashboard/settings",
},
]}
pinnedLabel={data.i18n.locale() === "cs" ? "Připnuto" : "Pinned"}
allLabel={data.i18n.locale() === "cs" ? "Zbývá" : "Remaining"}
initialPinned={["setup-locations"]}
/>
</div>
<div class="lg:col-span-2">
<RevenueChart />
</div>
</div>
{/* SMS Usage */}
{(() => {
const usage = data.smsUsage();
if (!usage || usage.messageCount === 0) return null;
return (
<div class="surface-card p-5 border-l-4 border-accent">
<div class="flex items-center justify-between">
<div class="flex items-center gap-4">
<div class="w-10 h-10 rounded-full bg-accent-subtle flex items-center justify-center text-accent">
<MailIcon />
</div>
<div>
<p class="font-semibold text-ink">SMS {data.i18n.t("dashboard.billing.planUsage")}</p>
<p class="text-sm text-ink-muted">{usage.messageCount} messages &mdash; {(usage.totalCostCents / 100).toFixed(2)} Kc</p>
</div>
</div>
<A href="/dashboard/settings" class="text-sm font-medium text-accent hover:text-accent-hover transition-colors">
{data.i18n.t("dashboard.settings")}
</A>
</div>
</div>
);
})()}
{/* Plan Limit Warning */}
{(() => {
const entitlements = data.resolvedBilling()?.entitlements;
const limit = entitlements?.maxLocations ?? -1;
const count = data.locationData() ?? 0;
const isNear = limit > 0 && count >= limit * 0.8;
const isAt = limit > 0 && count >= limit;
if (!isNear && !isAt) return null;
return (
<div class={`p-4 rounded-xl border ${isAt ? "bg-canvas-subtle border-ink/10" : "bg-accent/5 border-accent/20"}`}>
<div class="flex items-center gap-3">
<div class={`w-10 h-10 rounded-full flex items-center justify-center ${isAt ? "bg-canvas-muted" : "bg-accent/10"}`}>
<SparklesIcon />
</div>
<div class="flex-1 min-w-0">
<p class="font-medium text-ink">{isAt ? data.i18n.t("dashboard.locationLimitReached") : data.i18n.t("dashboard.nearingLocationLimit")}</p>
<p class="text-sm text-ink-muted">{data.i18n.t("dashboard.locationsUsed")} {count}/{limit === -1 ? "\u221e" : limit}</p>
</div>
<button onClick={() => void data.openCheckout()} class="px-4 py-2 text-sm font-medium rounded-lg bg-accent text-canvas hover:bg-accent-hover transition-colors shrink-0">
{data.i18n.t("dashboard.upgrade")}
</button>
</div>
</div>
);
})()}
{/* Revenue Chart + Calendar + Activity */}
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div class="lg:col-span-2 space-y-6">
<RevenueChart bookings={data.normalizedAllBookings()} />
<CalendarView bookings={data.normalizedUpcomingBookings()} locale={data.i18n.locale()} onBookingClick={data.openBookingDetail} />
</div>
<div class="space-y-6">
<ActivityTimeline activities={data.normalizedRecentActivity()} />
</div>
</div>
{/* Upcoming Bookings */}
<div class="surface-card p-6 shadow-sm">
<div class="flex items-center justify-between mb-5">
<div>
<h3 class="text-lg font-semibold text-ink">{data.i18n.t("dashboard.upcomingBookings")}</h3>
<p class="text-sm text-ink-muted mt-0.5">{data.normalizedUpcomingBookings().length} total</p>
</div>
<A href="/dashboard/bookings" class="inline-flex items-center gap-2 px-4 py-2 text-sm font-medium text-accent hover:text-accent-hover hover:bg-accent-subtle rounded-xl transition-all">
{data.i18n.t("dashboard.viewAll")} <ChevronRightIcon />
</A>
</div>
<div class="space-y-3">
{data.normalizedUpcomingBookings().slice(0, 5).map((booking: any) => (
<button
onClick={() => data.openBookingDetail(booking)}
class="w-full text-left flex items-center gap-4 p-4 rounded-xl border border-border/60 hover:border-accent/30 hover:bg-accent-subtle/20 transition-all group"
>
<div class="w-11 h-11 rounded-full bg-accent-subtle flex items-center justify-center text-sm font-bold text-accent shrink-0">
{getInitials(booking.customerName)}
</div>
<div class="flex-1 min-w-0">
<p class="font-medium text-ink group-hover:text-accent transition-colors">{booking.customerName}</p>
<p class="text-sm text-ink-muted">{booking.service} &bull; {booking.duration}</p>
</div>
<div class="text-right shrink-0">
<p class="text-sm font-medium text-ink">{new Date(booking.startsAt).toLocaleDateString(undefined, { month: "short", day: "numeric" })}</p>
<span class={`inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium ${
booking.status === "confirmed" ? "bg-accent/10 text-accent"
: booking.status === "pending" ? "bg-canvas-muted text-ink-muted"
: "bg-canvas-subtle text-ink-subtle"
}`}>
{data.i18n.t(`dashboard.${booking.status}`)}
</span>
</div>
</button>
))}
</div>
</div>
</div>
);
}
export default function OverviewRoute() {
return (
<DashboardLayout>
<OverviewPage />
</DashboardLayout>
);
}