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 <noreply@paperclip.ing>
This commit is contained in:
49
src/components/dashboard/GA4Panel.tsx
Normal file
49
src/components/dashboard/GA4Panel.tsx
Normal file
@@ -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 (
|
||||
<div class="freno-kpi-panel-content">
|
||||
<div class="freno-kpi-panel-header">
|
||||
<div>
|
||||
<h2>Acquisition KPIs</h2>
|
||||
<p class="freno-text-muted">GA4-powered web analytics</p>
|
||||
</div>
|
||||
<div class="freno-kpi-external-links">
|
||||
<a href="https://analytics.google.com" target="_blank" rel="noopener noreferrer" class="freno-btn freno-btn-secondary freno-btn-small">
|
||||
Open GA4 ↗
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="freno-kpi-metrics-grid">
|
||||
{ACQUISITION_KPIS.map((kpi) => (
|
||||
<div class="freno-kpi-metric-card freno-kpi-metric-pending">
|
||||
<div class="freno-kpi-metric-header">
|
||||
<span class="freno-kpi-metric-label">{kpi.label}</span>
|
||||
<span class="freno-kpi-badge freno-badge-draft">Pending</span>
|
||||
</div>
|
||||
<div class="freno-kpi-metric-value">—</div>
|
||||
<div class="freno-kpi-metric-target">Target: {kpi.target}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div class="freno-kpi-info-card">
|
||||
<h3>About GA4 Integration</h3>
|
||||
<p>Acquisition KPIs are powered by Google Analytics 4. Once GA4 is configured with enhanced e-commerce tracking, these metrics will populate automatically.</p>
|
||||
<ul>
|
||||
<li>Track traffic sources and channel attribution</li>
|
||||
<li>Monitor CAC across marketing channels</li>
|
||||
<li>Analyze landing page conversion funnels</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
57
src/components/dashboard/KPIDashboard.tsx
Normal file
57
src/components/dashboard/KPIDashboard.tsx
Normal file
@@ -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<KPIView>('product');
|
||||
|
||||
return (
|
||||
<div class="freno-kpi-dashboard">
|
||||
<div class="freno-page-header">
|
||||
<div>
|
||||
<h1>KPI Dashboard</h1>
|
||||
<p class="freno-text-muted">Real-time metrics across product, acquisition, and revenue</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="freno-kpi-tabs">
|
||||
{TABS.map((tab) => (
|
||||
<button
|
||||
class="freno-kpi-tab"
|
||||
classList={{ 'freno-kpi-tab-active': activeTab() === tab.id }}
|
||||
onClick={() => setActiveTab(tab.id)}
|
||||
>
|
||||
<span class="freno-kpi-tab-icon">{tab.icon}</span>
|
||||
<span>{tab.label}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div class="freno-kpi-panel">
|
||||
<Show when={activeTab() === 'product'}>
|
||||
<MixpanelPanel />
|
||||
</Show>
|
||||
<Show when={activeTab() === 'acquisition'}>
|
||||
<GA4Panel />
|
||||
</Show>
|
||||
<Show when={activeTab() === 'revenue'}>
|
||||
<StripePanel />
|
||||
</Show>
|
||||
<Show when={activeTab() === 'report'}>
|
||||
<UnifiedReport />
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
50
src/components/dashboard/MixpanelPanel.tsx
Normal file
50
src/components/dashboard/MixpanelPanel.tsx
Normal file
@@ -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 (
|
||||
<div class="freno-kpi-panel-content">
|
||||
<div class="freno-kpi-panel-header">
|
||||
<div>
|
||||
<h2>Product KPIs</h2>
|
||||
<p class="freno-text-muted">Mixpanel-powered product analytics</p>
|
||||
</div>
|
||||
<div class="freno-kpi-external-links">
|
||||
<a href="https://mixpanel.com" target="_blank" rel="noopener noreferrer" class="freno-btn freno-btn-secondary freno-btn-small">
|
||||
Open Mixpanel ↗
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="freno-kpi-metrics-grid">
|
||||
{PRODUCT_KPIS.map((kpi) => (
|
||||
<div class="freno-kpi-metric-card freno-kpi-metric-pending">
|
||||
<div class="freno-kpi-metric-header">
|
||||
<span class="freno-kpi-metric-label">{kpi.label}</span>
|
||||
<span class="freno-kpi-badge freno-badge-draft">Pending</span>
|
||||
</div>
|
||||
<div class="freno-kpi-metric-value">—</div>
|
||||
<div class="freno-kpi-metric-target">Target: {kpi.target}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div class="freno-kpi-info-card">
|
||||
<h3>About Mixpanel Integration</h3>
|
||||
<p>Product KPIs are powered by Mixpanel analytics. Once the Mixpanel SDK is integrated and events are flowing, these metrics will populate automatically.</p>
|
||||
<ul>
|
||||
<li>Track user signups, project creation, feature usage</li>
|
||||
<li>Set up funnels for conversion analysis</li>
|
||||
<li>Monitor retention and engagement cohorts</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
50
src/components/dashboard/StripePanel.tsx
Normal file
50
src/components/dashboard/StripePanel.tsx
Normal file
@@ -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 (
|
||||
<div class="freno-kpi-panel-content">
|
||||
<div class="freno-kpi-panel-header">
|
||||
<div>
|
||||
<h2>Revenue KPIs</h2>
|
||||
<p class="freno-text-muted">Stripe-powered revenue analytics</p>
|
||||
</div>
|
||||
<div class="freno-kpi-external-links">
|
||||
<a href="https://dashboard.stripe.com" target="_blank" rel="noopener noreferrer" class="freno-btn freno-btn-secondary freno-btn-small">
|
||||
Open Stripe ↗
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="freno-kpi-metrics-grid">
|
||||
{REVENUE_KPIS.map((kpi) => (
|
||||
<div class="freno-kpi-metric-card freno-kpi-metric-pending">
|
||||
<div class="freno-kpi-metric-header">
|
||||
<span class="freno-kpi-metric-label">{kpi.label}</span>
|
||||
<span class="freno-kpi-badge freno-badge-draft">Pending</span>
|
||||
</div>
|
||||
<div class="freno-kpi-metric-value">—</div>
|
||||
<div class="freno-kpi-metric-target">Target: {kpi.target}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div class="freno-kpi-info-card">
|
||||
<h3>About Stripe Integration</h3>
|
||||
<p>Revenue KPIs are powered by Stripe. Once Stripe webhooks are configured and subscription events are flowing, these metrics will populate automatically.</p>
|
||||
<ul>
|
||||
<li>Track MRR, ARPU, and subscription changes</li>
|
||||
<li>Monitor churn with automated alerts</li>
|
||||
<li>Analyze LTV with cohort analysis</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
96
src/components/dashboard/UnifiedReport.tsx
Normal file
96
src/components/dashboard/UnifiedReport.tsx
Normal file
@@ -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 (
|
||||
<div class="freno-kpi-panel-content">
|
||||
<div class="freno-kpi-panel-header">
|
||||
<div>
|
||||
<h2>Unified KPI Report</h2>
|
||||
<p class="freno-text-muted">Cross-tool KPI summary template</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="freno-kpi-report-template">
|
||||
<div class="freno-kpi-info-card">
|
||||
<h3>KPI Thresholds Reference</h3>
|
||||
<p>All tracked KPIs with their target thresholds and alert levels. This template is designed for weekly/monthly reporting across all analytics tools.</p>
|
||||
</div>
|
||||
|
||||
<table class="freno-kpi-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>KPI</th>
|
||||
<th>Category</th>
|
||||
<th>Warning Threshold</th>
|
||||
<th>Critical Threshold</th>
|
||||
<th>Direction</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{kpiEntries.map(([key, thresholds]) => (
|
||||
<tr>
|
||||
<td class="freno-kpi-table-key">{key.replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase())}</td>
|
||||
<td>
|
||||
<span class="freno-kpi-badge">{getCategory(key)}</span>
|
||||
</td>
|
||||
<td>{thresholds.warning}{getUnit(key)}</td>
|
||||
<td>{thresholds.critical}{getUnit(key)}</td>
|
||||
<td>
|
||||
<span classList={{ 'freno-text-success': thresholds.direction === 'higher', 'freno-text-error': thresholds.direction === 'lower' }}>
|
||||
{thresholds.direction === 'higher' ? '↑ Higher is better' : '↓ Lower is better'}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="freno-kpi-info-card">
|
||||
<h3>Reporting Schedule</h3>
|
||||
<ul>
|
||||
<li><strong>Weekly Report:</strong> Auto-generated every Monday at 9:00 AM</li>
|
||||
<li><strong>Monthly Report:</strong> Auto-generated on the 1st of each month</li>
|
||||
<li><strong>Alert Thresholds:</strong> Real-time notifications via Slack when KPIs breach warning/critical levels</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="freno-kpi-info-card">
|
||||
<h3>External Dashboards</h3>
|
||||
<ul>
|
||||
<li><a href="https://mixpanel.com" target="_blank" rel="noopener noreferrer">Mixpanel</a> — Product analytics (MAU, retention, funnels, viral coefficient)</li>
|
||||
<li><a href="https://analytics.google.com" target="_blank" rel="noopener noreferrer">Google Analytics 4</a> — Web analytics (traffic sources, CAC tracking)</li>
|
||||
<li><a href="https://dashboard.stripe.com" target="_blank" rel="noopener noreferrer">Stripe</a> — Revenue tracking (MRR, churn, LTV)</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
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<string, string> = {
|
||||
cac: ' USD',
|
||||
mrr: ' USD',
|
||||
ltv: ' USD',
|
||||
churn_rate: '%',
|
||||
conversion_rate: '%',
|
||||
mau: '',
|
||||
paying_users: '',
|
||||
nps: ' pts',
|
||||
viral_coefficient: '',
|
||||
};
|
||||
return units[key] || '';
|
||||
}
|
||||
Reference in New Issue
Block a user