From 11a188c68e3910d32fd6def081b33436b467aa3e Mon Sep 17 00:00:00 2001 From: Michael Freno Date: Sun, 26 Apr 2026 07:56:17 -0400 Subject: [PATCH] FRE-623: Build KPI dashboard with Mixpanel, GA4, Stripe panels and unified report - Created KPIDashboard component with tab navigation (product/acquisition/revenue/report) - Created MixpanelPanel for product KPIs linking to Mixpanel - Created GA4Panel for acquisition KPIs linking to GA4 - Created StripePanel for revenue KPIs linking to Stripe dashboard - Created UnifiedReport with KPI thresholds table and reporting schedule - Added KPI dashboard route (/app/kpi) and sidebar navigation link - Added KPI dashboard CSS styles (metric cards, tabs, table, info cards) - Fixed pre-existing parse errors in Faq.tsx (unescaped apostrophes) - Fixed pre-existing CSS import paths in routes.tsx Co-Authored-By: Paperclip --- src/components/dashboard/GA4Panel.tsx | 49 +++++ src/components/dashboard/KPIDashboard.tsx | 57 +++++ src/components/dashboard/MixpanelPanel.tsx | 50 +++++ src/components/dashboard/StripePanel.tsx | 50 +++++ src/components/dashboard/UnifiedReport.tsx | 96 +++++++++ src/components/layout/AppLayout.tsx | 5 + src/routes.tsx | 14 +- src/routes/faq/Faq.tsx | 10 +- src/styles/components.css | 237 +++++++++++++++++++++ 9 files changed, 558 insertions(+), 10 deletions(-) create mode 100644 src/components/dashboard/GA4Panel.tsx create mode 100644 src/components/dashboard/KPIDashboard.tsx create mode 100644 src/components/dashboard/MixpanelPanel.tsx create mode 100644 src/components/dashboard/StripePanel.tsx create mode 100644 src/components/dashboard/UnifiedReport.tsx diff --git a/src/components/dashboard/GA4Panel.tsx b/src/components/dashboard/GA4Panel.tsx new file mode 100644 index 000000000..4c345c97a --- /dev/null +++ b/src/components/dashboard/GA4Panel.tsx @@ -0,0 +1,49 @@ +import { Component } from 'solid-js'; + +const ACQUISITION_KPIS = [ + { key: 'cac', label: 'Customer Acquisition Cost', target: '<$15', unit: 'USD' }, + { key: 'traffic_sources', label: 'Traffic by Source', target: 'Diversified', unit: '%' }, + { key: 'signup_rate', label: 'Signup Conversion Rate', target: '>5%', unit: '%' }, + { key: 'channel_breakdown', label: 'Channel Performance', target: 'All channels', unit: '' }, +]; + +export const GA4Panel: Component = () => { + return ( +
+
+
+

Acquisition KPIs

+

GA4-powered web analytics

+
+ +
+ +
+ {ACQUISITION_KPIS.map((kpi) => ( +
+
+ {kpi.label} + Pending +
+
+
Target: {kpi.target}
+
+ ))} +
+ +
+

About GA4 Integration

+

Acquisition KPIs are powered by Google Analytics 4. Once GA4 is configured with enhanced e-commerce tracking, these metrics will populate automatically.

+
    +
  • Track traffic sources and channel attribution
  • +
  • Monitor CAC across marketing channels
  • +
  • Analyze landing page conversion funnels
  • +
+
+
+ ); +}; diff --git a/src/components/dashboard/KPIDashboard.tsx b/src/components/dashboard/KPIDashboard.tsx new file mode 100644 index 000000000..bd2a74d1e --- /dev/null +++ b/src/components/dashboard/KPIDashboard.tsx @@ -0,0 +1,57 @@ +import { Component, createSignal, Show } from 'solid-js'; +import { MixpanelPanel } from './MixpanelPanel'; +import { GA4Panel } from './GA4Panel'; +import { StripePanel } from './StripePanel'; +import { UnifiedReport } from './UnifiedReport'; + +type KPIView = 'product' | 'acquisition' | 'revenue' | 'report'; + +const TABS: { id: KPIView; label: string; icon: string }[] = [ + { id: 'product', label: 'Product KPIs', icon: '📈' }, + { id: 'acquisition', label: 'Acquisition KPIs', icon: '🎯' }, + { id: 'revenue', label: 'Revenue KPIs', icon: '💰' }, + { id: 'report', label: 'Unified Report', icon: '📋' }, +]; + +export const KPIDashboard: Component = () => { + const [activeTab, setActiveTab] = createSignal('product'); + + return ( +
+
+
+

KPI Dashboard

+

Real-time metrics across product, acquisition, and revenue

+
+
+ +
+ {TABS.map((tab) => ( + + ))} +
+ +
+ + + + + + + + + + + + +
+
+ ); +}; diff --git a/src/components/dashboard/MixpanelPanel.tsx b/src/components/dashboard/MixpanelPanel.tsx new file mode 100644 index 000000000..46681f7b6 --- /dev/null +++ b/src/components/dashboard/MixpanelPanel.tsx @@ -0,0 +1,50 @@ +import { Component } from 'solid-js'; + +const PRODUCT_KPIS = [ + { key: 'mau', label: 'Monthly Active Users', target: 'Growth MoM', unit: 'users' }, + { key: 'paying_users', label: 'Paying Users', target: '50K by EOY', unit: 'users' }, + { key: 'conversion_rate', label: 'Conversion Rate', target: '>3%', unit: '%' }, + { key: 'nps', label: 'Net Promoter Score', target: '>60', unit: 'pts' }, + { key: 'viral_coefficient', label: 'Viral Coefficient', target: '>0.5', unit: '' }, +]; + +export const MixpanelPanel: Component = () => { + return ( +
+
+
+

Product KPIs

+

Mixpanel-powered product analytics

+
+ +
+ +
+ {PRODUCT_KPIS.map((kpi) => ( +
+
+ {kpi.label} + Pending +
+
+
Target: {kpi.target}
+
+ ))} +
+ +
+

About Mixpanel Integration

+

Product KPIs are powered by Mixpanel analytics. Once the Mixpanel SDK is integrated and events are flowing, these metrics will populate automatically.

+
    +
  • Track user signups, project creation, feature usage
  • +
  • Set up funnels for conversion analysis
  • +
  • Monitor retention and engagement cohorts
  • +
+
+
+ ); +}; diff --git a/src/components/dashboard/StripePanel.tsx b/src/components/dashboard/StripePanel.tsx new file mode 100644 index 000000000..972fdc22b --- /dev/null +++ b/src/components/dashboard/StripePanel.tsx @@ -0,0 +1,50 @@ +import { Component } from 'solid-js'; + +const REVENUE_KPIS = [ + { key: 'mrr', label: 'Monthly Recurring Revenue', target: '$550K by EOY', unit: 'USD' }, + { key: 'churn_rate', label: 'Monthly Churn Rate', target: '<3%', unit: '%' }, + { key: 'ltv', label: 'Customer Lifetime Value', target: '>$120', unit: 'USD' }, + { key: 'arpu', label: 'Avg Revenue Per User', target: 'Growing', unit: 'USD' }, + { key: 'upgrades', label: 'Plan Upgrades', target: '>10% MoM', unit: '%' }, +]; + +export const StripePanel: Component = () => { + return ( +
+
+
+

Revenue KPIs

+

Stripe-powered revenue analytics

+
+ +
+ +
+ {REVENUE_KPIS.map((kpi) => ( +
+
+ {kpi.label} + Pending +
+
+
Target: {kpi.target}
+
+ ))} +
+ +
+

About Stripe Integration

+

Revenue KPIs are powered by Stripe. Once Stripe webhooks are configured and subscription events are flowing, these metrics will populate automatically.

+
    +
  • Track MRR, ARPU, and subscription changes
  • +
  • Monitor churn with automated alerts
  • +
  • Analyze LTV with cohort analysis
  • +
+
+
+ ); +}; diff --git a/src/components/dashboard/UnifiedReport.tsx b/src/components/dashboard/UnifiedReport.tsx new file mode 100644 index 000000000..7ca1aae4c --- /dev/null +++ b/src/components/dashboard/UnifiedReport.tsx @@ -0,0 +1,96 @@ +import { Component } from 'solid-js'; +import { KPI_THRESHOLDS } from '../../lib/analytics/kpi-service'; + +export const UnifiedReport: Component = () => { + const kpiEntries = Object.entries(KPI_THRESHOLDS) as [string, typeof KPI_THRESHOLDS[keyof typeof KPI_THRESHOLDS]][]; + + return ( +
+
+
+

Unified KPI Report

+

Cross-tool KPI summary template

+
+
+ +
+
+

KPI Thresholds Reference

+

All tracked KPIs with their target thresholds and alert levels. This template is designed for weekly/monthly reporting across all analytics tools.

+
+ + + + + + + + + + + + + {kpiEntries.map(([key, thresholds]) => ( + + + + + + + + ))} + +
KPICategoryWarning ThresholdCritical ThresholdDirection
{key.replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase())} + {getCategory(key)} + {thresholds.warning}{getUnit(key)}{thresholds.critical}{getUnit(key)} + + {thresholds.direction === 'higher' ? '↑ Higher is better' : '↓ Lower is better'} + +
+ +
+

Reporting Schedule

+
    +
  • Weekly Report: Auto-generated every Monday at 9:00 AM
  • +
  • Monthly Report: Auto-generated on the 1st of each month
  • +
  • Alert Thresholds: Real-time notifications via Slack when KPIs breach warning/critical levels
  • +
+
+ +
+

External Dashboards

+
    +
  • Mixpanel — Product analytics (MAU, retention, funnels, viral coefficient)
  • +
  • Google Analytics 4 — Web analytics (traffic sources, CAC tracking)
  • +
  • Stripe — Revenue tracking (MRR, churn, LTV)
  • +
+
+
+
+ ); +}; + +function getCategory(key: string): string { + const productKeys = ['mau', 'paying_users', 'conversion_rate', 'nps', 'viral_coefficient']; + const acquisitionKeys = ['cac']; + const revenueKeys = ['mrr', 'churn_rate', 'ltv']; + if (productKeys.includes(key)) return 'Product'; + if (acquisitionKeys.includes(key)) return 'Acquisition'; + if (revenueKeys.includes(key)) return 'Revenue'; + return 'Other'; +} + +function getUnit(key: string): string { + const units: Record = { + cac: ' USD', + mrr: ' USD', + ltv: ' USD', + churn_rate: '%', + conversion_rate: '%', + mau: '', + paying_users: '', + nps: ' pts', + viral_coefficient: '', + }; + return units[key] || ''; +} diff --git a/src/components/layout/AppLayout.tsx b/src/components/layout/AppLayout.tsx index e8aee9e85..2913cd57b 100644 --- a/src/components/layout/AppLayout.tsx +++ b/src/components/layout/AppLayout.tsx @@ -25,6 +25,10 @@ export const AppLayout: Component = (props) => { 📊 Dashboard + + 📈 + KPIs + 📁 Projects @@ -79,6 +83,7 @@ function getPageTitle(): string { const path = window.location.pathname; const titles: Record = { '/dashboard': 'Dashboard', + '/kpi': 'KPI Dashboard', '/projects': 'Projects', '/projects/new': 'New Project', '/profile': 'Profile', diff --git a/src/routes.tsx b/src/routes.tsx index 3b4940d23..39b46c233 100644 --- a/src/routes.tsx +++ b/src/routes.tsx @@ -11,14 +11,15 @@ import { Pricing } from './routes/pricing/Pricing'; import { About } from './routes/about/About'; import { Faq } from './routes/faq/Faq'; import { NotFound } from './routes/NotFound'; -import '../styles/landing.css'; -import '../styles/blog.css'; -import '../styles/features.css'; -import '../styles/pricing.css'; -import '../styles/about-faq.css'; +import './styles/landing.css'; +import './styles/blog.css'; +import './styles/features.css'; +import './styles/pricing.css'; +import './styles/about-faq.css'; const AppLayout = lazy(() => import('./components/layout/AppLayout')); const Dashboard = lazy(() => import('./components/dashboard/Dashboard')); +const KPIDashboard = lazy(() => import('./components/dashboard/KPIDashboard')); const ProjectList = lazy(() => import('./components/projects/ProjectList')); const ProjectDetail = lazy(() => import('./components/projects/ProjectDetail')); const ProjectForm = lazy(() => import('./components/projects/ProjectForm')); @@ -43,6 +44,9 @@ export const routes = [ , + + + , diff --git a/src/routes/faq/Faq.tsx b/src/routes/faq/Faq.tsx index 9546e4719..3065cf8cb 100644 --- a/src/routes/faq/Faq.tsx +++ b/src/routes/faq/Faq.tsx @@ -32,15 +32,15 @@ const faqCategories = [ }, { question: 'How does real-time collaboration work?', - answer: 'Invite collaborators to your script via email or shareable link. Multiple writers can edit simultaneously — you'll see each other's cursors and changes in real-time.' + answer: "Invite collaborators to your script via email or shareable link. Multiple writers can edit simultaneously — you'll see each other's cursors and changes in real-time." }, { question: 'Can I work offline?', - answer: 'Yes, with our desktop apps (Pro plan and above). Your work syncs automatically when you're back online. Offline mode is not available in the browser version.' + answer: "Yes, with our desktop apps (Pro plan and above). Your work syncs automatically when you're back online. Offline mode is not available in the browser version." }, { question: 'What is the AI writing assistant?', - answer: 'Our AI can help with dialogue suggestions, scene descriptions, character analysis, and more. It's available on the Premium plan and learns from your writing style.' + answer: "Our AI can help with dialogue suggestions, scene descriptions, character analysis, and more. It's available on the Premium plan and learns from your writing style." } ] }, @@ -48,7 +48,7 @@ const faqCategories = [ name: 'Pricing', faqs: [ { - question: 'What's included in the free plan?', + question: "What's included in the free plan?", answer: 'Free includes unlimited projects, industry-standard formatting, cloud saving, mobile editing, comments & mentions, and basic export (PDF, Fountain).' }, { @@ -57,7 +57,7 @@ const faqCategories = [ }, { question: 'Do you offer refunds?', - answer: 'Yes, we offer a 30-day money-back guarantee. If you're not satisfied, contact us within 30 days for a full refund.' + answer: "Yes, we offer a 30-day money-back guarantee. If you're not satisfied, contact us within 30 days for a full refund." }, { question: 'Do you offer education discounts?', diff --git a/src/styles/components.css b/src/styles/components.css index 20cdf2751..98d6e4ee4 100644 --- a/src/styles/components.css +++ b/src/styles/components.css @@ -1115,6 +1115,243 @@ } /* Responsive */ +/* KPI Dashboard */ +.freno-kpi-dashboard { + max-width: 1200px; +} + +.freno-text-muted { + color: var(--color-text-secondary); + font-size: 14px; + margin-top: 4px; +} + +.freno-text-success { + color: var(--color-success); +} + +.freno-text-error { + color: var(--color-error); +} + +.freno-kpi-tabs { + display: flex; + gap: 4px; + background: var(--color-bg-secondary); + border: 1px solid var(--color-border); + border-radius: var(--radius-lg); + padding: 4px; + margin-bottom: 24px; + overflow-x: auto; +} + +.freno-kpi-tab { + display: flex; + align-items: center; + gap: 8px; + padding: 10px 16px; + border-radius: var(--radius-md); + font-size: 14px; + font-weight: 500; + color: var(--color-text-secondary); + white-space: nowrap; + transition: all var(--transition-fast); +} + +.freno-kpi-tab:hover { + color: var(--color-text-primary); + background: var(--color-bg-elevated); +} + +.freno-kpi-tab-active { + background: var(--color-bg-elevated); + color: var(--color-text-primary); +} + +.freno-kpi-tab-icon { + font-size: 16px; +} + +.freno-kpi-panel { + min-height: 400px; +} + +.freno-kpi-panel-content { + display: flex; + flex-direction: column; + gap: 24px; +} + +.freno-kpi-panel-header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 16px; +} + +.freno-kpi-panel-header h2 { + font-size: 20px; + font-weight: 600; +} + +.freno-kpi-external-links { + display: flex; + gap: 8px; + flex-shrink: 0; +} + +.freno-kpi-metrics-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); + gap: 16px; +} + +.freno-kpi-metric-card { + background: var(--color-bg-secondary); + border: 1px solid var(--color-border); + border-radius: var(--radius-lg); + padding: 20px; + display: flex; + flex-direction: column; + gap: 8px; + transition: all var(--transition-fast); +} + +.freno-kpi-metric-card:hover { + border-color: var(--color-border-hover); +} + +.freno-kpi-metric-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; +} + +.freno-kpi-metric-label { + font-size: 13px; + color: var(--color-text-secondary); + font-weight: 500; +} + +.freno-kpi-badge { + display: inline-block; + padding: 2px 8px; + border-radius: var(--radius-full); + font-size: 11px; + font-weight: 500; + background: rgba(115, 115, 115, 0.15); + color: var(--color-text-muted); +} + +.freno-kpi-metric-value { + font-size: 32px; + font-weight: 700; + font-variant-numeric: tabular-nums; +} + +.freno-kpi-metric-target { + font-size: 12px; + color: var(--color-text-muted); +} + +.freno-kpi-metric-pending .freno-kpi-metric-value { + color: var(--color-text-muted); +} + +.freno-kpi-healthy { + border-color: rgba(34, 197, 94, 0.3); +} + +.freno-kpi-warning { + border-color: rgba(234, 179, 8, 0.3); +} + +.freno-kpi-critical { + border-color: rgba(239, 68, 68, 0.3); +} + +.freno-kpi-info-card { + background: var(--color-bg-secondary); + border: 1px solid var(--color-border); + border-radius: var(--radius-lg); + padding: 20px; +} + +.freno-kpi-info-card h3 { + font-size: 14px; + font-weight: 600; + margin-bottom: 8px; + color: var(--color-text-primary); +} + +.freno-kpi-info-card p { + font-size: 14px; + color: var(--color-text-secondary); + line-height: 1.6; + margin-bottom: 12px; +} + +.freno-kpi-info-card ul { + list-style: disc; + padding-left: 20px; + display: flex; + flex-direction: column; + gap: 6px; +} + +.freno-kpi-info-card li { + font-size: 14px; + color: var(--color-text-secondary); + line-height: 1.5; +} + +.freno-kpi-report-template { + display: flex; + flex-direction: column; + gap: 24px; +} + +.freno-kpi-table { + width: 100%; + border-collapse: collapse; + background: var(--color-bg-secondary); + border: 1px solid var(--color-border); + border-radius: var(--radius-lg); + overflow: hidden; +} + +.freno-kpi-table th { + text-align: left; + padding: 12px 16px; + font-size: 12px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--color-text-muted); + background: var(--color-bg-tertiary); + border-bottom: 1px solid var(--color-border); +} + +.freno-kpi-table td { + padding: 12px 16px; + font-size: 14px; + border-bottom: 1px solid var(--color-border); +} + +.freno-kpi-table tr:last-child td { + border-bottom: none; +} + +.freno-kpi-table-key { + font-weight: 500; + text-transform: capitalize; +} + +.freno-kpi-table tbody tr:hover { + background: var(--color-bg-elevated); +} + @media (max-width: 768px) { .freno-sidebar { position: fixed;