diff --git a/dashboard/app/dashboard/clients/page.tsx b/dashboard/app/dashboard/clients/page.tsx index afe34a8..162e120 100644 --- a/dashboard/app/dashboard/clients/page.tsx +++ b/dashboard/app/dashboard/clients/page.tsx @@ -1,8 +1,8 @@ "use client"; import { useState, useEffect } from "react"; +import { toast } from "sonner"; import { Plus, ExternalLink, X } from "lucide-react"; -import DemoBadge from "@/components/DemoBadge"; import { api } from "@/lib/api"; import { stubClients } from "@/lib/stubs"; import type { Client } from "@/lib/stubs"; @@ -81,21 +81,21 @@ export default function ClientsPage() {
-

Clients

+

Clients

Manage your client roster

-
+
- + diff --git a/dashboard/app/dashboard/growth/page.tsx b/dashboard/app/dashboard/growth/page.tsx new file mode 100644 index 0000000..8fb2ae2 --- /dev/null +++ b/dashboard/app/dashboard/growth/page.tsx @@ -0,0 +1,283 @@ +"use client"; + +import { useState } from "react"; +import { toast } from "sonner"; +import { + Mail, + Search, + MapPin, + TrendingUp, + DollarSign, + BookOpen, + Clock, + ChevronRight, +} from "lucide-react"; +import DualRankingTable from "@/components/DualRankingTable"; +import { stubDualRankings, stubCompetitorChanges } from "@/lib/stubs"; +import type { StructuredDiff } from "@/lib/stubs"; +import { dualRankingStats } from "@/lib/stats"; + +const threatColors: Record = { + low: "bg-chart-3/10 text-chart-3", + medium: "bg-muted text-chart-2", + high: "bg-destructive/10 text-destructive", +}; + +const diffTypeMeta: Record< + StructuredDiff["type"], + { icon: typeof DollarSign; chipBg: string; pillBg: string } +> = { + price: { + icon: DollarSign, + chipBg: "bg-destructive/10 text-destructive", + pillBg: "bg-destructive/10 text-destructive", + }, + menu: { + icon: BookOpen, + chipBg: "bg-chart-4/10 text-chart-4", + pillBg: "bg-chart-4/10 text-chart-4", + }, + hours: { + icon: Clock, + chipBg: "bg-chart-3/10 text-chart-3", + pillBg: "bg-chart-3/10 text-chart-3", + }, +}; + +const legendItems: { + type: StructuredDiff["type"]; + label: string; + pillClass: string; +}[] = [ + { type: "price", label: "Price change", pillClass: "bg-destructive/10 text-destructive" }, + { type: "menu", label: "Menu / service change", pillClass: "bg-chart-4/10 text-chart-4" }, + { type: "hours", label: "Hours change", pillClass: "bg-chart-3/10 text-chart-3" }, +]; + +export default function GrowthPage() { + const [tab, setTab] = useState<"seo" | "competitors">("seo"); + + const rankingStats = dualRankingStats(stubDualRankings); + const totalChanges = stubCompetitorChanges.reduce( + (s, c) => s + c.changes.length, + 0, + ); + + const seoStats = [ + { + label: "Avg organic rank", + value: rankingStats.avgOrganic.toFixed(1), + icon: Search, + chipClass: "bg-muted text-muted-foreground", + }, + { + label: "Avg Local Pack rank", + value: rankingStats.avgLocalPack.toFixed(1), + icon: MapPin, + chipClass: "bg-chart-4/10 text-chart-4", + }, + { + label: "Keywords improving", + value: rankingStats.improving.toString(), + icon: TrendingUp, + chipClass: "bg-chart-3/10 text-chart-3", + }, + { + label: "In top 3 (Maps)", + value: rankingStats.topThreeMaps.toString(), + icon: MapPin, + chipClass: "bg-accent/10 text-accent", + }, + ]; + + return ( +
+ {/* Page header */} +
+
+

Growth

+

+ SEO rankings and competitor intelligence +

+
+
+ +
+
+ + {/* Tabs */} +
+ + +
+ + {tab === "seo" ? ( + <> + {/* Stat cards */} +
+ {seoStats.map((stat) => { + const Icon = stat.icon; + return ( +
+
+ + + +
+

+ {stat.value} +

+

+ {stat.label} +

+
+
+
+ ); + })} +
+ + {/* Rankings table */} +
+
+

+ Keyword Rankings +

+ + Updated 21 Jul 2026, 6:00 AM + +
+ +
+ + ) : ( + <> + {/* Legend */} +
+
+ {legendItems.map((item) => { + const Icon = diffTypeMeta[item.type].icon; + return ( + + + + {item.label} + + + ); + })} + + {totalChanges} structured changes across{" "} + {stubCompetitorChanges.length} competitors this week + +
+
+ + {/* Competitor cards */} +
+ {stubCompetitorChanges.map((competitor) => ( +
+
+
+
+

+ {competitor.name} +

+

+ {competitor.domain} +

+
+ + {competitor.threat.toUpperCase()} + +
+
+
+ {competitor.changes.map((change, idx) => { + const meta = diffTypeMeta[change.type]; + const Icon = meta.icon; + return ( +
+
+ + + +
+

+ {change.description} +

+
+ + {change.oldValue} + + + + {change.newValue} + +
+
+
+ + {change.timestamp} + +
+ ); + })} +
+
+ ))} +
+ + )} +
+ ); +} diff --git a/dashboard/app/dashboard/layout.tsx b/dashboard/app/dashboard/layout.tsx index 5ea4b2f..6ad687a 100644 --- a/dashboard/app/dashboard/layout.tsx +++ b/dashboard/app/dashboard/layout.tsx @@ -1,20 +1,9 @@ -import Sidebar from "@/components/Sidebar"; -import TrialBanner from "@/components/TrialBanner"; +import { DashboardShell } from "@/components/DashboardShell"; export default function DashboardLayout({ children, }: { children: React.ReactNode; }) { - return ( -
- -
- -
- {children} -
-
-
- ); + return {children}; } diff --git a/dashboard/app/dashboard/locations/page.tsx b/dashboard/app/dashboard/locations/page.tsx index 2497a2f..9d01fd5 100644 --- a/dashboard/app/dashboard/locations/page.tsx +++ b/dashboard/app/dashboard/locations/page.tsx @@ -1,6 +1,7 @@ "use client"; import { useState } from "react"; +import { toast } from "sonner"; import { Building2, Store, @@ -13,7 +14,6 @@ import { Stethoscope, Globe, } from "lucide-react"; -import DemoBadge from "@/components/DemoBadge"; import Toggle from "@/components/Toggle"; import { stubLocations } from "@/lib/stubs"; import type { Location } from "@/lib/stubs"; @@ -61,14 +61,30 @@ export default function LocationsPage() { }; const statCards = [ - { label: "Locations", value: stats.total.toString(), icon: Building2 }, + { + label: "Locations", + value: stats.total.toString(), + icon: Building2, + chipClass: "bg-accent/10 text-accent", + }, { label: "Menus synced", value: `${stats.synced}/${stats.total}`, icon: Store, + chipClass: "bg-chart-3/10 text-chart-3", + }, + { + label: "Reviews this week", + value: "23", + icon: Star, + chipClass: "bg-muted text-chart-2", + }, + { + label: "Avg local rank", + value: "4.2", + icon: MapPin, + chipClass: "bg-chart-4/10 text-chart-4", }, - { label: "Reviews this week", value: "23", icon: Star }, - { label: "Avg local rank", value: "4.2", icon: MapPin }, ]; const columns = [selected.targets.slice(0, 2), selected.targets.slice(2, 4)]; @@ -77,13 +93,12 @@ export default function LocationsPage() {
-

Locations

+

Locations

Manage venues and per-location menu-sync targets

-
Bondi Dental @@ -92,27 +107,35 @@ export default function LocationsPage() {
-
+ {/* Stat cards */}
{statCards.map((stat) => { const Icon = stat.icon; return (
- + + +
-

+

{stat.value}

-

+

{stat.label}

@@ -122,10 +145,11 @@ export default function LocationsPage() { })}
-
+ {/* Locations table */} +
Business Type Trial
- + @@ -174,7 +198,8 @@ export default function LocationsPage() {
Location Menu-sync target Status
-
+ {/* Menu-sync targets for selected location */} +

Menu-sync targets — {venueName(selected)} @@ -189,7 +214,7 @@ export default function LocationsPage() { {columns.map((col, colIdx) => (
{col.map((target, idx) => { const Icon = targetIcons[target.key] ?? Store; @@ -227,7 +252,10 @@ export default function LocationsPage() { ))}
-
diff --git a/dashboard/app/dashboard/page.tsx b/dashboard/app/dashboard/page.tsx index 78b185f..b57c3ed 100644 --- a/dashboard/app/dashboard/page.tsx +++ b/dashboard/app/dashboard/page.tsx @@ -1,6 +1,7 @@ "use client"; import { useState, useEffect } from "react"; +import { toast } from "sonner"; import { Clock, CheckCircle2, BarChart3 } from "lucide-react"; import ReviewCard from "@/components/ReviewCard"; import { api } from "@/lib/api"; @@ -24,19 +25,19 @@ export default function DashboardPage() { label: "Pending", value: drafts.length.toString(), icon: Clock, - color: "text-accent bg-accent/10", + chipClass: "bg-accent/10 text-accent", }, { label: "Posted today", value: "2", icon: CheckCircle2, - color: "text-chart-3 bg-chart-3/10", + chipClass: "bg-chart-3/10 text-chart-3", }, { label: "Avg SLA", value: "3.2h", icon: BarChart3, - color: "text-chart-2 bg-muted", + chipClass: "bg-muted text-chart-2", }, ]; @@ -51,7 +52,7 @@ export default function DashboardPage() { return (
-

+

Review Approval Queue

@@ -59,16 +60,28 @@ export default function DashboardPage() {

+ {/* Stat cards */}
{stats.map((stat) => { const Icon = stat.icon; return ( -
+
- + + +
-

{stat.value}

-

{stat.label}

+

+ {stat.value} +

+

+ {stat.label} +

@@ -77,7 +90,7 @@ export default function DashboardPage() {
{drafts.length === 0 ? ( -
+

All caught up! @@ -85,6 +98,9 @@ export default function DashboardPage() {

Check back later for new reviews needing approval.

+

+ New reviews appear here automatically when customers leave feedback. +

) : (
diff --git a/dashboard/app/dashboard/rebook/page.tsx b/dashboard/app/dashboard/rebook/page.tsx index f406f8c..1a8bfe5 100644 --- a/dashboard/app/dashboard/rebook/page.tsx +++ b/dashboard/app/dashboard/rebook/page.tsx @@ -1,6 +1,7 @@ "use client"; import { useState } from "react"; +import { toast } from "sonner"; import { Users, Clock, @@ -10,7 +11,6 @@ import { Building2, ChevronDown, } from "lucide-react"; -import DemoBadge from "@/components/DemoBadge"; import Toggle from "@/components/Toggle"; import { stubPractitioners } from "@/lib/stubs"; import type { Practitioner, LapsedPatient } from "@/lib/stubs"; @@ -80,28 +80,26 @@ export default function RebookPage() {
-

Rebook

+

Rebook

Lapsed-patient follow-up by practitioner

-
- -
- - Bondi Dental Clinic - -
+
+ + Bondi Dental Clinic +
+ {/* Stat cards */}
{statCards.map((stat, i) => { const Icon = stat.icon; return (
-

+

{stat.value}

-

+

{stat.label}

@@ -123,10 +121,11 @@ export default function RebookPage() { })}
-
+ {/* Practitioners table */} +
- + @@ -232,7 +231,7 @@ function PractitionerDetail({ ]; return ( -
+

@@ -257,22 +256,22 @@ function PractitionerDetail({ {detailStats.map((stat) => (
-

+

{stat.value}

-

+

{stat.label}

))}

-
+
Practitioner Lapsed patients Follow-ups sent
- + diff --git a/dashboard/app/dashboard/reports/page.tsx b/dashboard/app/dashboard/reports/page.tsx index 18c5063..07ac29d 100644 --- a/dashboard/app/dashboard/reports/page.tsx +++ b/dashboard/app/dashboard/reports/page.tsx @@ -2,7 +2,6 @@ import { useState } from "react"; import { - RefreshCw, Mail, Search, MapPin, @@ -13,7 +12,7 @@ import { ChevronRight, } from "lucide-react"; import DualRankingTable from "@/components/DualRankingTable"; -import DemoBadge from "@/components/DemoBadge"; +import { toast } from "sonner"; import { stubDualRankings, stubCompetitorChanges } from "@/lib/stubs"; import type { StructuredDiff } from "@/lib/stubs"; import { dualRankingStats } from "@/lib/stats"; @@ -101,13 +100,12 @@ export default function ReportsPage() {

- - -
diff --git a/dashboard/app/dashboard/settings/page.tsx b/dashboard/app/dashboard/settings/page.tsx index f035e06..35de710 100644 --- a/dashboard/app/dashboard/settings/page.tsx +++ b/dashboard/app/dashboard/settings/page.tsx @@ -1,8 +1,36 @@ "use client"; -import { useState } from "react"; -import { Save, X } from "lucide-react"; -import DemoBadge from "@/components/DemoBadge"; +import { useEffect, useState } from "react"; +import { toast } from "sonner"; +import { + CreditCard, + Gauge, + Settings, + Save, + X, + ExternalLink, + Download, + Check, + ShieldCheck, + FileText, + Search, + MessageSquare, + ChevronRight, + TrendingUp, +} from "lucide-react"; +import Toggle from "@/components/Toggle"; +import { api } from "@/lib/api"; +import { stubBillingUsage } from "@/lib/stubs"; +import type { BillingUsage, Invoice, UsageBar } from "@/lib/stubs"; +import { + usagePercent, + usageStatus, + usageRemaining, + STATUS_STYLES, +} from "@/lib/usage"; +import type { LucideIcon } from "lucide-react"; + +type SettingsTab = "preferences" | "billing" | "usage"; const JOBS = [ "Review Guard", @@ -12,7 +40,25 @@ const JOBS = [ "Menu Sync", ]; -export default function SettingsPage() { +const JOB_ICONS: Record = { + "Review drafts": ShieldCheck, + "SEO reports": FileText, + "Competitor briefs": Search, + "Follow-up messages": MessageSquare, + "Menu syncs": Settings, +}; + +const INVOICE_STATUS_STYLES: Record = { + paid: "bg-chart-3/10 text-chart-3", + open: "bg-muted text-chart-2", + void: "bg-destructive/10 text-destructive", +}; + +/* ------------------------------------------------------------------ */ +/* Tab: Preferences */ +/* ------------------------------------------------------------------ */ + +function PreferencesTab() { const [jobToggles, setJobToggles] = useState>({ "Review Guard": true, "Rank Report": true, @@ -42,31 +88,22 @@ export default function SettingsPage() { if (kw && !keywords.includes(kw)) setKeywords([...keywords, kw]); setKeywordInput(""); }; - const removeKeyword = (kw: string) => setKeywords(keywords.filter((k) => k !== kw)); - const addUrl = () => { const url = urlInput.trim(); if (url && !competitorUrls.includes(url)) setCompetitorUrls([...competitorUrls, url]); setUrlInput(""); }; - const removeUrl = (url: string) => setCompetitorUrls(competitorUrls.filter((u) => u !== url)); return (
-
-

Settings

-

- Configure your automation preferences -

-
-
-
+ {/* Active Jobs */} +

Active Jobs

Toggle automations on/off @@ -80,29 +117,18 @@ export default function SettingsPage() { {job} - + setJobToggles({ ...jobToggles, [job]: v })} + aria-label={`Toggle ${job}`} + /> ))}

-
+ {/* Voice Sample */} +

Voice Sample

@@ -110,14 +136,15 @@ export default function SettingsPage() { Used for AI-generated phone scripts

Patient Last visit Channel