diff --git a/templates/saas/nextjs-monolith/app/analytics/page.tsx b/templates/saas/nextjs-monolith/app/analytics/page.tsx index 502bf63..c9650fa 100644 --- a/templates/saas/nextjs-monolith/app/analytics/page.tsx +++ b/templates/saas/nextjs-monolith/app/analytics/page.tsx @@ -1,27 +1,70 @@ +import SparkLine from '../../components/SparkLine'; +import LineChart from '../../components/LineChart'; +import DonutChart from '../../components/DonutChart'; +import BarChart from '../../components/BarChart'; + export default function AnalyticsPage() { + const summaryMetrics = [ + { label: 'Total Page Views', value: '284,920', change: '+18.3%', positive: true, sparkData: [180, 210, 195, 230, 255, 270, 285] }, + { label: 'Unique Visitors', value: '11,847', change: '+9.7%', positive: true, sparkData: [7.2, 8.1, 8.8, 9.5, 10.2, 11.0, 11.8] }, + { label: 'Conversion Rate', value: '7.1%', change: '-0.4%', positive: false, sparkData: [7.8, 7.6, 7.5, 7.3, 7.2, 7.0, 7.1], color: '#ef4444' }, + ]; + + const trafficData = [ + { label: '1', value: 8200 }, + { label: '2', value: 7800 }, + { label: '3', value: 9100 }, + { label: '4', value: 8600 }, + { label: '5', value: 9400 }, + { label: '6', value: 10200 }, + { label: '7', value: 9800 }, + { label: '8', value: 8900 }, + { label: '9', value: 9600 }, + { label: '10', value: 10800 }, + { label: '11', value: 11200 }, + { label: '12', value: 10500 }, + { label: '13', value: 9900 }, + { label: '14', value: 10100 }, + { label: '15', value: 11400 }, + { label: '16', value: 12100 }, + { label: '17', value: 11800 }, + { label: '18', value: 10900 }, + { label: '19', value: 11500 }, + { label: '20', value: 12400 }, + { label: '21', value: 11900 }, + { label: '22', value: 10600 }, + { label: '23', value: 11100 }, + { label: '24', value: 12800 }, + { label: '25', value: 13200 }, + { label: '26', value: 12600 }, + { label: '27', value: 11800 }, + { label: '28', value: 12300 }, + { label: '29', value: 13500 }, + { label: '30', value: 14100 }, + ]; + const trafficSources = [ - { source: 'Organic Search', visitors: 4521, percentage: 38, color: 'bg-blue-500' }, - { source: 'Direct', visitors: 2847, percentage: 24, color: 'bg-green-500' }, - { source: 'Social Media', visitors: 1923, percentage: 16, color: 'bg-purple-500' }, - { source: 'Referral', visitors: 1456, percentage: 12, color: 'bg-orange-500' }, - { source: 'Email', visitors: 1100, percentage: 10, color: 'bg-pink-500' }, + { label: 'Organic', value: 38, color: 'var(--primary)' }, + { label: 'Direct', value: 24, color: '#10b981' }, + { label: 'Social', value: 16, color: '#8b5cf6' }, + { label: 'Referral', value: 12, color: '#f59e0b' }, + { label: 'Email', value: 10, color: '#ec4899' }, ]; - const pageViews = [ - { page: '/dashboard', views: 12450, avgTime: '3m 24s', bounceRate: '18%' }, - { page: '/analytics', views: 8920, avgTime: '5m 12s', bounceRate: '12%' }, - { page: '/users', views: 6340, avgTime: '2m 48s', bounceRate: '24%' }, - { page: '/billing', views: 4210, avgTime: '4m 06s', bounceRate: '15%' }, - { page: '/settings', views: 3180, avgTime: '1m 52s', bounceRate: '32%' }, - { page: '/integrations', views: 2890, avgTime: '6m 30s', bounceRate: '8%' }, + const topPages = [ + { label: 'Dashboard', value: 12450 }, + { label: 'Analytics', value: 8920 }, + { label: 'Users', value: 6340 }, + { label: 'Billing', value: 4210 }, + { label: 'Settings', value: 3180 }, ]; const conversionFunnel = [ - { stage: 'Visitors', count: 45000, width: '100%' }, - { stage: 'Sign Ups', count: 12800, width: '72%' }, - { stage: 'Activated', count: 8400, width: '48%' }, - { stage: 'Subscribed', count: 3200, width: '28%' }, - { stage: 'Enterprise', count: 420, width: '12%' }, + { stage: 'Visitors', count: 45000, percentage: 100 }, + { stage: 'Sign Ups', count: 12800, percentage: 72 }, + { stage: 'Activated', count: 8400, percentage: 48 }, + { stage: 'Subscribed', count: 3200, percentage: 28 }, + { stage: 'Enterprise', count: 420, percentage: 12 }, ]; return ( @@ -38,94 +81,74 @@ export default function AnalyticsPage() { - {/* Summary Row */} + {/* Summary Metrics */}
-
-

Total Page Views

-

284,920

-

+18.3% vs last period

-
-
-

Unique Visitors

-

11,847

-

+9.7% vs last period

-
-
-

Conversion Rate

-

7.1%

-

-0.4% vs last period

+ {summaryMetrics.map((metric) => ( +
+
+
+

{metric.label}

+

{metric.value}

+
+ +
+

+ {metric.change} vs last period +

+
+ ))} +
+ + {/* Traffic Over Time */} +
+

Traffic Over Time

+ +
+
+

Total Views (30d)

+

284,920

+
+
+

Daily Average

+

9,497

+
+
+

Peak Day

+

14,100

+
+ {/* Two-column: Donut + Bar */}
- {/* Traffic Sources */}

Traffic Sources

-
- {trafficSources.map((source) => ( -
-
- {source.source} - {source.visitors.toLocaleString()} ({source.percentage}%) -
-
-
-
-
- ))} -
+
- - {/* Conversion Funnel */}
-

Conversion Funnel

-
- {conversionFunnel.map((stage) => ( -
-
{stage.stage}
-
-
- {stage.count.toLocaleString()} -
-
-
- ))} -
+

Top Pages

+
- {/* Top Pages */} -
-
-

Top Pages

-
-
- - - - - - - - - - - {pageViews.map((page) => ( - - - - - - - ))} - -
PageViewsAvg. TimeBounce Rate
{page.page}{page.views.toLocaleString()}{page.avgTime}{page.bounceRate}
+ {/* Conversion Funnel */} +
+

Conversion Funnel

+
+ {conversionFunnel.map((stage) => ( +
+
{stage.stage}
+
+
+ {stage.count.toLocaleString()} +
+
+
{stage.percentage}%
+
+ ))}
diff --git a/templates/saas/nextjs-monolith/app/page.tsx b/templates/saas/nextjs-monolith/app/page.tsx index 72c8189..66df1ff 100644 --- a/templates/saas/nextjs-monolith/app/page.tsx +++ b/templates/saas/nextjs-monolith/app/page.tsx @@ -1,18 +1,45 @@ +import SparkLine from '../components/SparkLine'; +import LineChart from '../components/LineChart'; +import DonutChart from '../components/DonutChart'; +import BarChart from '../components/BarChart'; + export default function Dashboard() { const metrics = [ - { label: 'Monthly Revenue', value: '$48,295', change: '+12.5%', positive: true }, - { label: 'Active Users', value: '12,847', change: '+8.2%', positive: true }, - { label: 'Churn Rate', value: '2.4%', change: '-0.3%', positive: true }, - { label: 'Avg. Session', value: '4m 32s', change: '-12s', positive: false }, + { label: 'Monthly Revenue', value: '$48,295', change: '+12.5%', positive: true, sparkData: [32, 35, 38, 41, 44, 48] }, + { label: 'Active Users', value: '12,847', change: '+8.2%', positive: true, sparkData: [8, 9, 9.8, 10.5, 11.3, 12.8] }, + { label: 'Churn Rate', value: '2.4%', change: '-0.3%', positive: true, sparkData: [3.2, 2.9, 2.8, 2.7, 2.5, 2.4], color: '#ef4444' }, + { label: 'Avg. Session', value: '4m 32s', change: '-12s', positive: false, sparkData: [4.1, 4.3, 4.5, 4.4, 4.6, 4.5] }, + ]; + + const revenueData = [ + { label: 'Jan', value: 28400 }, + { label: 'Feb', value: 31200 }, + { label: 'Mar', value: 29800 }, + { label: 'Apr', value: 35600 }, + { label: 'May', value: 33100 }, + { label: 'Jun', value: 38500 }, + { label: 'Jul', value: 36200 }, + { label: 'Aug', value: 41000 }, + { label: 'Sep', value: 39400 }, + { label: 'Oct', value: 44200 }, + { label: 'Nov', value: 46100 }, + { label: 'Dec', value: 48295 }, ]; - const chartData = [ - { month: 'Jan', revenue: 32000, users: 8400 }, - { month: 'Feb', revenue: 35000, users: 9200 }, - { month: 'Mar', revenue: 38500, users: 9800 }, - { month: 'Apr', revenue: 41000, users: 10500 }, - { month: 'May', revenue: 44200, users: 11300 }, - { month: 'Jun', revenue: 48295, users: 12847 }, + const revenueBySource = [ + { label: 'Direct', value: 38, color: 'var(--primary)' }, + { label: 'Organic', value: 28, color: '#10b981' }, + { label: 'Referral', value: 18, color: '#f59e0b' }, + { label: 'Social', value: 16, color: '#8b5cf6' }, + ]; + + const monthlyUsers = [ + { label: 'Jul', value: 8400 }, + { label: 'Aug', value: 9200 }, + { label: 'Sep', value: 9800 }, + { label: 'Oct', value: 10500 }, + { label: 'Nov', value: 11300 }, + { label: 'Dec', value: 12847 }, ]; const recentTransactions = [ @@ -23,8 +50,6 @@ export default function Dashboard() { { id: 'TXN-005', customer: 'BigCo Ltd', plan: 'Enterprise', amount: '$2,400', status: 'Failed', date: '2 days ago' }, ]; - const maxRevenue = Math.max(...chartData.map((d) => d.revenue)); - return (
{/* Header */} @@ -41,9 +66,14 @@ export default function Dashboard() {
{metrics.map((metric) => (
-

{metric.label}

-

{metric.value}

-

+

+
+

{metric.label}

+

{metric.value}

+
+ +
+

{metric.change} from last month

@@ -52,30 +82,40 @@ export default function Dashboard() { {/* Revenue Chart */}
-

Revenue Overview

-
- {chartData.map((point) => ( -
-
- {point.month} -
- ))} +
+

Revenue Overview

+
+ + + + +
+
-

Total Revenue (6mo)

-

$238,995

+

Total Revenue (12mo)

+

$451,795

Growth Rate

-

+50.9%

+

+70.1%

+ {/* Two-column: Donut + Bar */} +
+
+

Revenue by Source

+ +
+
+

Monthly Active Users

+ +
+
+ {/* Recent Transactions */}
diff --git a/templates/saas/nextjs-monolith/components/BarChart.tsx b/templates/saas/nextjs-monolith/components/BarChart.tsx new file mode 100644 index 0000000..07b7f4f --- /dev/null +++ b/templates/saas/nextjs-monolith/components/BarChart.tsx @@ -0,0 +1,36 @@ +interface BarChartProps { + data: { label: string; value: number }[]; + height?: number; +} + +export default function BarChart({ data, height = 200 }: BarChartProps) { + const max = Math.max(...data.map((d) => d.value)); + + return ( +
+
+ {data.map((item) => { + const barHeight = (item.value / max) * 100; + return ( +
+ + {item.value >= 1000 ? `${(item.value / 1000).toFixed(1)}k` : item.value} + +
+
+ ); + })} +
+
+ {data.map((item) => ( +
+ {item.label} +
+ ))} +
+
+ ); +} diff --git a/templates/saas/nextjs-monolith/components/DonutChart.tsx b/templates/saas/nextjs-monolith/components/DonutChart.tsx new file mode 100644 index 0000000..2fe0426 --- /dev/null +++ b/templates/saas/nextjs-monolith/components/DonutChart.tsx @@ -0,0 +1,87 @@ +interface DonutChartProps { + segments: { label: string; value: number; color: string }[]; + size?: number; +} + +export default function DonutChart({ segments, size = 180 }: DonutChartProps) { + const total = segments.reduce((sum, s) => sum + s.value, 0); + const strokeWidth = size * 0.18; + const radius = (size - strokeWidth) / 2; + const circumference = 2 * Math.PI * radius; + const center = size / 2; + + let cumulativeOffset = 0; + + const segmentArcs = segments.map((segment) => { + const percentage = segment.value / total; + const dashLength = circumference * percentage; + const dashGap = circumference - dashLength; + const offset = -cumulativeOffset; + cumulativeOffset += dashLength; + + return { + ...segment, + percentage, + dashArray: `${dashLength} ${dashGap}`, + dashOffset: offset, + }; + }); + + return ( +
+
+ + {/* Background circle */} + + {/* Segments */} + {segmentArcs.map((seg, i) => ( + + ))} + + {/* Center text */} +
+ {total} + Total +
+
+ + {/* Legend */} +
+ {segments.map((seg) => ( +
+ + {seg.label} + + {Math.round((seg.value / total) * 100)}% + +
+ ))} +
+
+ ); +} diff --git a/templates/saas/nextjs-monolith/components/LineChart.tsx b/templates/saas/nextjs-monolith/components/LineChart.tsx new file mode 100644 index 0000000..15a8610 --- /dev/null +++ b/templates/saas/nextjs-monolith/components/LineChart.tsx @@ -0,0 +1,122 @@ +interface LineChartProps { + data: { label: string; value: number }[]; + height?: number; +} + +export default function LineChart({ data, height = 200 }: LineChartProps) { + const max = Math.max(...data.map((d) => d.value)); + const min = Math.min(...data.map((d) => d.value)); + const range = max - min || 1; + + const padding = { top: 20, right: 20, bottom: 30, left: 10 }; + const chartWidth = 800; + const chartHeight = height; + const innerWidth = chartWidth - padding.left - padding.right; + const innerHeight = chartHeight - padding.top - padding.bottom; + + const points = data.map((d, i) => { + const x = padding.left + (i / (data.length - 1)) * innerWidth; + const y = padding.top + innerHeight - ((d.value - min) / range) * innerHeight; + return { x, y }; + }); + + const polylinePoints = points.map((p) => `${p.x},${p.y}`).join(' '); + + const areaPath = [ + `M ${points[0].x},${padding.top + innerHeight}`, + `L ${points[0].x},${points[0].y}`, + ...points.slice(1).map((p) => `L ${p.x},${p.y}`), + `L ${points[points.length - 1].x},${padding.top + innerHeight}`, + 'Z', + ].join(' '); + + const gridLines = 4; + const gridValues = Array.from({ length: gridLines }, (_, i) => { + const value = min + (range / (gridLines - 1)) * i; + const y = padding.top + innerHeight - ((value - min) / range) * innerHeight; + return { value, y }; + }); + + return ( +
+ + + + + + + + + {/* Grid lines */} + {gridValues.map((grid, i) => ( + + ))} + + {/* Area fill */} + + + {/* Line */} + + + {/* Data points */} + {points.map((p, i) => ( + + + + + ))} + + {/* X-axis labels */} + {data.map((d, i) => { + const x = padding.left + (i / (data.length - 1)) * innerWidth; + return ( + + {d.label} + + ); + })} + +
+ ); +} diff --git a/templates/saas/nextjs-monolith/components/SparkLine.tsx b/templates/saas/nextjs-monolith/components/SparkLine.tsx new file mode 100644 index 0000000..0f8fd2b --- /dev/null +++ b/templates/saas/nextjs-monolith/components/SparkLine.tsx @@ -0,0 +1,30 @@ +interface SparkLineProps { + data: number[]; + color?: string; + width?: number; + height?: number; +} + +export default function SparkLine({ data, color = 'var(--primary)', width = 80, height = 24 }: SparkLineProps) { + const max = Math.max(...data); + const min = Math.min(...data); + const range = max - min || 1; + const points = data.map((val, i) => { + const x = (i / (data.length - 1)) * width; + const y = height - ((val - min) / range) * (height - 4) - 2; + return `${x},${y}`; + }).join(' '); + + return ( + + + + ); +}