@payez/next-mvp 4.1.8 → 4.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (155) hide show
  1. package/README.md +792 -792
  2. package/dist/auth/better-auth.js +15 -3
  3. package/dist/components/account/MobileNavDrawer.js +18 -18
  4. package/dist/components/account/UserAvatarMenu.js +1 -1
  5. package/dist/components/admin/AlertSettingsTab.js +40 -40
  6. package/dist/components/admin/LoggingSettingsTab.js +28 -28
  7. package/dist/lib/idp-fetch.js +9 -25
  8. package/dist/pages/login/page.js +28 -28
  9. package/dist/pages/settings/EnhancedSettingsPage.js +17 -17
  10. package/package.json +1 -1
  11. package/scripts/dev-broker.ps1 +35 -35
  12. package/scripts/dev-local.ps1 +45 -45
  13. package/src/api/auth-handler.ts +548 -548
  14. package/src/api-handlers/account/change-password.ts +142 -142
  15. package/src/api-handlers/account/update-phone.ts +79 -79
  16. package/src/api-handlers/account/verify-email.ts +125 -125
  17. package/src/api-handlers/account/verify-sms.ts +125 -125
  18. package/src/api-handlers/admin/analytics.ts +443 -443
  19. package/src/api-handlers/admin/audit.ts +223 -223
  20. package/src/api-handlers/admin/index.ts +63 -63
  21. package/src/api-handlers/admin/redis-sessions.ts +251 -251
  22. package/src/api-handlers/admin/sessions.ts +318 -318
  23. package/src/api-handlers/admin/site-logs.ts +365 -365
  24. package/src/api-handlers/admin/users.ts +242 -242
  25. package/src/api-handlers/admin/vibe-data.ts +324 -324
  26. package/src/api-handlers/auth/refresh.ts +682 -682
  27. package/src/api-handlers/auth/signout.ts +199 -199
  28. package/src/api-handlers/auth/status.ts +20 -20
  29. package/src/api-handlers/auth/update-session.ts +110 -110
  30. package/src/api-handlers/auth/verify-code.ts +112 -112
  31. package/src/api-handlers/session/refresh-viability.ts +36 -36
  32. package/src/api-handlers/test/force-expire.ts +60 -60
  33. package/src/auth/auth-decision.ts +229 -229
  34. package/src/auth/better-auth.ts +15 -3
  35. package/src/auth/route-config.ts +219 -219
  36. package/src/auth/types/auth-types.ts +555 -555
  37. package/src/auth/types/index.ts +7 -7
  38. package/src/auth/utils/idp-client.ts +445 -445
  39. package/src/auth/utils/index.ts +6 -6
  40. package/src/auth/utils/token-utils.ts +243 -243
  41. package/src/client/AuthContext.tsx +143 -143
  42. package/src/client/fetch-with-auth.ts +47 -47
  43. package/src/components/SessionSync.tsx +138 -138
  44. package/src/components/SignalRHealthCheck.tsx +131 -131
  45. package/src/components/account/MobileNavDrawer.tsx +305 -305
  46. package/src/components/account/UserAvatarMenu.tsx +266 -266
  47. package/src/components/account/index.ts +13 -13
  48. package/src/components/admin/AlertSettingsTab.tsx +728 -728
  49. package/src/components/admin/AnalyticsTab.tsx +703 -703
  50. package/src/components/admin/DataBrowserTab.tsx +505 -505
  51. package/src/components/admin/LoggingSettingsTab.tsx +665 -665
  52. package/src/components/admin/SessionsTab.tsx +414 -414
  53. package/src/components/admin/StatsTab.tsx +379 -379
  54. package/src/components/admin/VibeAdminLayout.tsx +187 -187
  55. package/src/components/admin/index.ts +59 -59
  56. package/src/components/auth/FederatedAuthSection.tsx +95 -95
  57. package/src/components/auth/ModeAwareLoginPage.tsx +135 -135
  58. package/src/components/auth/ModeAwareSignupPage.tsx +267 -267
  59. package/src/components/auth/TraditionalAuthSection.tsx +99 -99
  60. package/src/components/recovery/CompleteStep.tsx +36 -36
  61. package/src/components/recovery/InitiateRecoveryStep.tsx +68 -68
  62. package/src/components/recovery/SelectMethodStep.tsx +73 -73
  63. package/src/components/recovery/SetPasswordStep.tsx +97 -97
  64. package/src/components/recovery/VerifyCodeStep.tsx +90 -90
  65. package/src/components/reserved/ReservedRecoveryWarning.tsx +160 -160
  66. package/src/components/reserved/ReservedStatusBox.tsx +118 -118
  67. package/src/components/ui/BetaBadge.tsx +58 -58
  68. package/src/components/ui/Footer.tsx +93 -93
  69. package/src/config/env.ts +57 -57
  70. package/src/config/logger.ts +62 -62
  71. package/src/config/vibe-log-transport.ts +264 -264
  72. package/src/hooks/useAuth.ts +117 -117
  73. package/src/hooks/useAuthSettings.ts +97 -97
  74. package/src/hooks/useAvailableProviders.ts +120 -120
  75. package/src/hooks/usePasswordValidation.ts +127 -127
  76. package/src/hooks/usePublicAuthSettings.ts +149 -149
  77. package/src/hooks/useSessionExpiration.ts +101 -101
  78. package/src/hooks/useViabilitySession.ts +336 -336
  79. package/src/index.ts +60 -60
  80. package/src/lib/api-handler.ts +643 -643
  81. package/src/lib/app-slug.ts +178 -178
  82. package/src/lib/geolocation.ts +265 -265
  83. package/src/lib/idp-fetch.ts +10 -26
  84. package/src/lib/jwt-decode.ts +82 -82
  85. package/src/lib/roles.ts +177 -177
  86. package/src/lib/site-logger.ts +245 -245
  87. package/src/lib/standardized-client-api.ts +901 -901
  88. package/src/lib/test-aware-get-token.ts +41 -41
  89. package/src/lib/user-agent-parser.ts +252 -252
  90. package/src/logging/api/admin-analytics.ts +51 -51
  91. package/src/logging/api/audit-log.ts +53 -53
  92. package/src/logging/components/AdminAnalyticsLayout.tsx +49 -49
  93. package/src/logging/components/AuditLogViewer.tsx +125 -125
  94. package/src/logging/components/ErrorMetricsCard.tsx +98 -98
  95. package/src/logging/components/HealthMetricsCard.tsx +70 -70
  96. package/src/logging/hooks/useAdminAnalytics.ts +22 -22
  97. package/src/logging/hooks/useAuditLog.ts +24 -24
  98. package/src/logging/hooks/useErrorMetrics.ts +40 -40
  99. package/src/logging/hooks/useHealthMetrics.ts +44 -44
  100. package/src/logging/index.ts +18 -18
  101. package/src/logging/types/analytics.ts +81 -81
  102. package/src/logging/types/audit.ts +31 -31
  103. package/src/logging/types/index.ts +3 -3
  104. package/src/middleware/create-middleware.ts +663 -663
  105. package/src/middleware/rbac-check.ts +279 -279
  106. package/src/middleware/twofa-presets.ts +224 -224
  107. package/src/pages/admin-login/page.tsx +221 -221
  108. package/src/pages/admin-page-permissions/PagePermissionsAdminPage.tsx +527 -527
  109. package/src/pages/admin-page-permissions/index.ts +7 -7
  110. package/src/pages/admin-roles/RolesAdminPage.tsx +533 -533
  111. package/src/pages/admin-roles/modals.tsx +469 -469
  112. package/src/pages/client-admin/ClientSiteAdminPage.tsx +382 -382
  113. package/src/pages/client-admin/index.ts +33 -33
  114. package/src/pages/coming-soon/page.tsx +83 -83
  115. package/src/pages/login/page.tsx +456 -456
  116. package/src/pages/profile/EnhancedProfilePage.tsx +485 -485
  117. package/src/pages/profile/index.ts +9 -9
  118. package/src/pages/profile/page.tsx +166 -166
  119. package/src/pages/recovery/page.tsx +234 -234
  120. package/src/pages/roles/MyRolesPage.tsx +211 -211
  121. package/src/pages/roles/components.tsx +294 -294
  122. package/src/pages/roles/index.ts +17 -17
  123. package/src/pages/security/EnhancedSecurityPage.tsx +565 -565
  124. package/src/pages/security/index.ts +9 -9
  125. package/src/pages/security/page.tsx +507 -507
  126. package/src/pages/settings/EnhancedSettingsPage.tsx +642 -642
  127. package/src/pages/settings/index.ts +9 -9
  128. package/src/pages/settings/page.tsx +47 -47
  129. package/src/pages/showcase/ShowcasePage.tsx +532 -532
  130. package/src/pages/showcase/index.ts +13 -13
  131. package/src/pages/test-env/EmergencyLogoutPage.tsx +180 -180
  132. package/src/pages/test-env/JwtInspectPage.tsx +456 -456
  133. package/src/pages/test-env/RefreshTokenPage.tsx +157 -157
  134. package/src/pages/test-env/TestEnvPage.tsx +118 -118
  135. package/src/pages/test-env/index.ts +25 -25
  136. package/src/pages/verify-code/page.tsx +652 -652
  137. package/src/routes/account/update-phone.ts +13 -13
  138. package/src/routes/auth/logout.ts +108 -108
  139. package/src/routes/auth/nextauth.ts +45 -45
  140. package/src/routes/auth/settings.ts +75 -75
  141. package/src/routes/auth/viability.ts +224 -224
  142. package/src/server/auth-guard.ts +170 -170
  143. package/src/server/slim-middleware.ts +117 -117
  144. package/src/server/with-auth.ts +93 -93
  145. package/src/stores/authStore.ts +1899 -1899
  146. package/src/templates/instrumentation.ts +41 -41
  147. package/src/utils/logout.ts +30 -30
  148. package/src/vibe/client.ts +590 -590
  149. package/src/vibe/enterprise-auth.ts +208 -208
  150. package/src/vibe/errors.ts +185 -185
  151. package/src/vibe/generic.ts +431 -431
  152. package/src/vibe/hooks/index.ts +367 -367
  153. package/src/vibe/index.ts +132 -132
  154. package/src/vibe/sessions.ts +551 -551
  155. package/src/vibe/types.ts +577 -577
@@ -1,703 +1,703 @@
1
- /**
2
- * =============================================================================
3
- * VIBE ADMIN ANALYTICS TAB
4
- * =============================================================================
5
- *
6
- * Generic dashboard tab showing login statistics and analytics.
7
- * Can be extended by consumer apps with custom components.
8
- *
9
- * =============================================================================
10
- */
11
-
12
- 'use client';
13
-
14
- import React, { useState, useEffect, useCallback, ReactNode } from 'react';
15
-
16
- // -----------------------------------------------------------------------------
17
- // TYPES
18
- // -----------------------------------------------------------------------------
19
-
20
- type Period = 'today' | 'week' | 'month' | 'year';
21
-
22
- interface TimeSeries {
23
- date: string;
24
- count: number;
25
- label: string;
26
- }
27
-
28
- interface CountryStats {
29
- country: string;
30
- code: string;
31
- flag: string;
32
- count: number;
33
- percentage: number;
34
- }
35
-
36
- interface UserStats {
37
- email: string;
38
- name: string;
39
- count: number;
40
- lastLogin: string;
41
- }
42
-
43
- interface TierDistribution {
44
- tier: string;
45
- count: number;
46
- percentage: number;
47
- }
48
-
49
- interface FeatureUsage {
50
- name: string;
51
- uses: number;
52
- uniqueUsers: number;
53
- }
54
-
55
- interface RevenueByTier {
56
- tier: string;
57
- subscribers: number;
58
- pricePerMonth: number;
59
- mrr: number;
60
- }
61
-
62
- interface RevenueTrend {
63
- month: string;
64
- mrr: number;
65
- }
66
-
67
- interface TierData {
68
- distribution: TierDistribution[];
69
- total: number;
70
- }
71
-
72
- interface FeatureData {
73
- features: FeatureUsage[];
74
- }
75
-
76
- interface RevenueData {
77
- mrr: number;
78
- arr: number;
79
- paidSubscribers: number;
80
- totalUsers: number;
81
- conversionRate: number;
82
- revenueByTier: RevenueByTier[];
83
- trend: RevenueTrend[];
84
- currency: string;
85
- }
86
-
87
- interface AnalyticsData {
88
- period: Period;
89
- dateRange: {
90
- start: string;
91
- end: string;
92
- };
93
- summary: {
94
- totalLogins: number;
95
- uniqueUsers: number;
96
- newUsers: number;
97
- peakHour: string;
98
- avgLoginsPerDay: number;
99
- };
100
- timeSeries: TimeSeries[];
101
- byCountry: CountryStats[];
102
- byDevice: Record<string, number>;
103
- byBrowser: Record<string, number>;
104
- byOS: Record<string, number>;
105
- topUsers: UserStats[];
106
- }
107
-
108
- export interface AnalyticsTabProps {
109
- isDark?: boolean;
110
- /** Base API path for analytics (default: /api/admin/analytics) */
111
- apiBasePath?: string;
112
- /** Optional custom component to render in the geo section (e.g., WorldMap) */
113
- geoMapComponent?: ReactNode;
114
- /** Show business metrics section (default: true) */
115
- showBusinessMetrics?: boolean;
116
- }
117
-
118
- // -----------------------------------------------------------------------------
119
- // COMPONENT
120
- // -----------------------------------------------------------------------------
121
-
122
- export function AnalyticsTab({
123
- isDark = true,
124
- apiBasePath = '/api/admin/analytics',
125
- geoMapComponent,
126
- showBusinessMetrics = true,
127
- }: AnalyticsTabProps) {
128
- const [period, setPeriod] = useState<Period>('week');
129
- const [data, setData] = useState<AnalyticsData | null>(null);
130
- const [loading, setLoading] = useState(true);
131
- const [error, setError] = useState<string | null>(null);
132
-
133
- // Business metrics data
134
- const [tierData, setTierData] = useState<TierData | null>(null);
135
- const [featureData, setFeatureData] = useState<FeatureData | null>(null);
136
- const [revenueData, setRevenueData] = useState<RevenueData | null>(null);
137
-
138
- const themeClasses = {
139
- cardBg: isDark ? 'bg-slate-800 border-slate-700' : 'bg-white border-gray-200 shadow-sm',
140
- textPrimary: isDark ? 'text-white' : 'text-gray-900',
141
- textSecondary: isDark ? 'text-gray-400' : 'text-gray-600',
142
- textMuted: isDark ? 'text-gray-500' : 'text-gray-500',
143
- barBg: isDark ? 'bg-slate-700' : 'bg-gray-200',
144
- };
145
-
146
- const fetchAnalytics = useCallback(async () => {
147
- setLoading(true);
148
- setError(null);
149
- try {
150
- const res = await fetch(`${apiBasePath}/logins?period=${period}`);
151
- if (!res.ok) throw new Error('Failed to fetch analytics');
152
- const json = await res.json();
153
- setData(json);
154
- } catch (err: any) {
155
- setError(err.message);
156
- } finally {
157
- setLoading(false);
158
- }
159
- }, [period, apiBasePath]);
160
-
161
- // Fetch tier, feature, and revenue data
162
- const fetchBusinessMetrics = useCallback(async () => {
163
- if (!showBusinessMetrics) return;
164
-
165
- try {
166
- const [tierRes, featureRes, revenueRes] = await Promise.all([
167
- fetch(`${apiBasePath}/tiers`),
168
- fetch(`${apiBasePath}/feature-usage`),
169
- fetch(`${apiBasePath}/revenue`),
170
- ]);
171
-
172
- if (tierRes.ok) {
173
- const data = await tierRes.json();
174
- setTierData(data);
175
- }
176
- if (featureRes.ok) {
177
- const data = await featureRes.json();
178
- setFeatureData(data);
179
- }
180
- if (revenueRes.ok) {
181
- const data = await revenueRes.json();
182
- setRevenueData(data);
183
- }
184
- } catch (err) {
185
- console.error('Failed to fetch business metrics:', err);
186
- }
187
- }, [apiBasePath, showBusinessMetrics]);
188
-
189
- useEffect(() => {
190
- fetchAnalytics();
191
- fetchBusinessMetrics();
192
- }, [fetchAnalytics, fetchBusinessMetrics]);
193
-
194
- const formatDate = (dateString: string) => {
195
- return new Date(dateString).toLocaleDateString('en-US', {
196
- month: 'short',
197
- day: 'numeric',
198
- hour: '2-digit',
199
- minute: '2-digit',
200
- });
201
- };
202
-
203
- // Simple bar chart component
204
- const BarChart = ({
205
- data,
206
- maxValue,
207
- colorClass = 'bg-blue-500'
208
- }: {
209
- data: { label: string; value: number }[];
210
- maxValue: number;
211
- colorClass?: string;
212
- }) => (
213
- <div className="space-y-2">
214
- {data.map((item, i) => (
215
- <div key={i} className="flex items-center gap-2">
216
- <span className={`text-xs ${themeClasses.textSecondary} w-20 truncate`}>{item.label}</span>
217
- <div className={`flex-1 h-6 ${themeClasses.barBg} rounded overflow-hidden`}>
218
- <div
219
- className={`h-full ${colorClass} transition-all duration-300`}
220
- style={{ width: `${maxValue > 0 ? (item.value / maxValue) * 100 : 0}%` }}
221
- />
222
- </div>
223
- <span className={`text-xs ${themeClasses.textPrimary} w-12 text-right`}>{item.value}</span>
224
- </div>
225
- ))}
226
- </div>
227
- );
228
-
229
- // Time series line chart (simplified as bar chart)
230
- const TimeSeriesChart = ({ series }: { series: TimeSeries[] }) => {
231
- const maxCount = Math.max(...series.map(s => s.count), 1);
232
- return (
233
- <div className="h-48 flex items-end gap-1">
234
- {series.map((item, i) => (
235
- <div
236
- key={i}
237
- className="flex-1 flex flex-col items-center group relative"
238
- >
239
- <div
240
- className="w-full bg-blue-500 rounded-t transition-all duration-300 hover:bg-blue-400"
241
- style={{ height: `${(item.count / maxCount) * 100}%`, minHeight: item.count > 0 ? '4px' : '0' }}
242
- />
243
- {/* Tooltip */}
244
- <div className="absolute bottom-full mb-2 hidden group-hover:block z-10">
245
- <div className={`${isDark ? 'bg-slate-900' : 'bg-gray-800'} text-white text-xs px-2 py-1 rounded shadow-lg whitespace-nowrap`}>
246
- {item.label}: {item.count}
247
- </div>
248
- </div>
249
- </div>
250
- ))}
251
- </div>
252
- );
253
- };
254
-
255
- if (loading && !data) {
256
- return (
257
- <div className="flex items-center justify-center py-12">
258
- <svg className="animate-spin h-8 w-8 text-blue-400" viewBox="0 0 24 24" fill="none">
259
- <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
260
- <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z" />
261
- </svg>
262
- </div>
263
- );
264
- }
265
-
266
- if (error) {
267
- return (
268
- <div className={`${isDark ? 'bg-red-900/30 border-red-700' : 'bg-red-50 border-red-300'} border rounded-lg p-4 ${isDark ? 'text-red-300' : 'text-red-700'}`}>
269
- {error}
270
- <button onClick={fetchAnalytics} className="ml-4 underline">Retry</button>
271
- </div>
272
- );
273
- }
274
-
275
- if (!data) return null;
276
-
277
- const deviceData = Object.entries(data.byDevice)
278
- .map(([label, value]) => ({ label: label.charAt(0).toUpperCase() + label.slice(1), value }))
279
- .sort((a, b) => b.value - a.value);
280
-
281
- const browserData = Object.entries(data.byBrowser)
282
- .map(([label, value]) => ({ label, value }))
283
- .sort((a, b) => b.value - a.value)
284
- .slice(0, 5);
285
-
286
- const osData = Object.entries(data.byOS)
287
- .map(([label, value]) => ({ label, value }))
288
- .sort((a, b) => b.value - a.value)
289
- .slice(0, 5);
290
-
291
- return (
292
- <div className="space-y-6">
293
- {/* Period Selector */}
294
- <div className="flex items-center justify-between">
295
- <div className="flex gap-2">
296
- {(['today', 'week', 'month', 'year'] as Period[]).map((p) => (
297
- <button
298
- key={p}
299
- onClick={() => setPeriod(p)}
300
- className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
301
- period === p
302
- ? 'bg-blue-600 text-white'
303
- : isDark ? 'bg-slate-700 text-gray-300 hover:bg-slate-600' : 'bg-gray-200 text-gray-700 hover:bg-gray-300'
304
- }`}
305
- >
306
- {p === 'today' ? 'Today' : p === 'week' ? 'This Week' : p === 'month' ? 'This Month' : 'This Year'}
307
- </button>
308
- ))}
309
- </div>
310
- <button
311
- onClick={fetchAnalytics}
312
- disabled={loading}
313
- className={`p-2 ${themeClasses.textSecondary} hover:${themeClasses.textPrimary} transition-colors disabled:opacity-50`}
314
- title="Refresh"
315
- >
316
- <svg className={`w-5 h-5 ${loading ? 'animate-spin' : ''}`} fill="none" viewBox="0 0 24 24" stroke="currentColor">
317
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
318
- </svg>
319
- </button>
320
- </div>
321
-
322
- {/* Summary Cards */}
323
- <div className="grid grid-cols-2 md:grid-cols-5 gap-4">
324
- <div className={`${themeClasses.cardBg} border rounded-lg p-4`}>
325
- <p className={themeClasses.textSecondary + ' text-sm'}>Total Logins</p>
326
- <p className={`text-3xl font-bold ${themeClasses.textPrimary}`}>{data.summary.totalLogins.toLocaleString()}</p>
327
- </div>
328
- <div className={`${themeClasses.cardBg} border rounded-lg p-4`}>
329
- <p className={themeClasses.textSecondary + ' text-sm'}>Unique Users</p>
330
- <p className="text-3xl font-bold text-blue-400">{data.summary.uniqueUsers.toLocaleString()}</p>
331
- </div>
332
- <div className={`${themeClasses.cardBg} border rounded-lg p-4`}>
333
- <p className={themeClasses.textSecondary + ' text-sm'}>New Users</p>
334
- <p className="text-3xl font-bold text-green-400">{data.summary.newUsers.toLocaleString()}</p>
335
- </div>
336
- <div className={`${themeClasses.cardBg} border rounded-lg p-4`}>
337
- <p className={themeClasses.textSecondary + ' text-sm'}>Peak Hour</p>
338
- <p className="text-3xl font-bold text-purple-400">{data.summary.peakHour}</p>
339
- </div>
340
- <div className={`${themeClasses.cardBg} border rounded-lg p-4`}>
341
- <p className={themeClasses.textSecondary + ' text-sm'}>Avg/Day</p>
342
- <p className="text-3xl font-bold text-orange-400">{data.summary.avgLoginsPerDay.toLocaleString()}</p>
343
- </div>
344
- </div>
345
-
346
- {/* Time Series Chart */}
347
- <div className={`${themeClasses.cardBg} border rounded-lg p-6`}>
348
- <h3 className={`text-lg font-semibold ${themeClasses.textPrimary} mb-4`}>Logins Over Time</h3>
349
- <TimeSeriesChart series={data.timeSeries} />
350
- <div className={`flex justify-between mt-2 text-xs ${themeClasses.textMuted}`}>
351
- <span>{data.timeSeries[0]?.label}</span>
352
- <span>{data.timeSeries[data.timeSeries.length - 1]?.label}</span>
353
- </div>
354
- </div>
355
-
356
- {/* Geographic Map (optional slot) */}
357
- {geoMapComponent && (
358
- <div className={`${themeClasses.cardBg} border rounded-lg p-6`}>
359
- <h3 className={`text-lg font-semibold ${themeClasses.textPrimary} mb-4`}>Geographic Distribution</h3>
360
- {geoMapComponent}
361
- </div>
362
- )}
363
-
364
- {/* Charts Grid */}
365
- <div className="grid md:grid-cols-2 gap-6">
366
- {/* By Country */}
367
- <div className={`${themeClasses.cardBg} border rounded-lg p-6`}>
368
- <h3 className={`text-lg font-semibold ${themeClasses.textPrimary} mb-4`}>Top Countries</h3>
369
- {data.byCountry.length > 0 ? (
370
- <div className="space-y-3">
371
- {data.byCountry.slice(0, 8).map((country, i) => (
372
- <div key={i} className="flex items-center gap-3">
373
- <span className="text-xl">{country.flag}</span>
374
- <span className={`${themeClasses.textPrimary} flex-1`}>{country.country}</span>
375
- <div className={`w-24 h-4 ${themeClasses.barBg} rounded overflow-hidden`}>
376
- <div
377
- className="h-full bg-green-500"
378
- style={{ width: `${country.percentage}%` }}
379
- />
380
- </div>
381
- <span className={`${themeClasses.textSecondary} text-sm w-16 text-right`}>
382
- {country.count} ({country.percentage}%)
383
- </span>
384
- </div>
385
- ))}
386
- </div>
387
- ) : (
388
- <p className={`${themeClasses.textMuted} text-center py-4`}>No data</p>
389
- )}
390
- </div>
391
-
392
- {/* By Device */}
393
- <div className={`${themeClasses.cardBg} border rounded-lg p-6`}>
394
- <h3 className={`text-lg font-semibold ${themeClasses.textPrimary} mb-4`}>Device Types</h3>
395
- {deviceData.length > 0 ? (
396
- <div className="space-y-4">
397
- {deviceData.map((item, i) => {
398
- const total = deviceData.reduce((sum, d) => sum + d.value, 0);
399
- const percentage = total > 0 ? Math.round((item.value / total) * 100) : 0;
400
- const icon = item.label.toLowerCase() === 'mobile' ? '📱' :
401
- item.label.toLowerCase() === 'tablet' ? '📱' : '💻';
402
- return (
403
- <div key={i} className="flex items-center gap-3">
404
- <span className="text-xl">{icon}</span>
405
- <span className={`${themeClasses.textPrimary} w-20`}>{item.label}</span>
406
- <div className={`flex-1 h-6 ${themeClasses.barBg} rounded overflow-hidden`}>
407
- <div
408
- className="h-full bg-purple-500"
409
- style={{ width: `${percentage}%` }}
410
- />
411
- </div>
412
- <span className={`${themeClasses.textSecondary} text-sm w-20 text-right`}>
413
- {item.value} ({percentage}%)
414
- </span>
415
- </div>
416
- );
417
- })}
418
- </div>
419
- ) : (
420
- <p className={`${themeClasses.textMuted} text-center py-4`}>No data</p>
421
- )}
422
- </div>
423
-
424
- {/* By Browser */}
425
- <div className={`${themeClasses.cardBg} border rounded-lg p-6`}>
426
- <h3 className={`text-lg font-semibold ${themeClasses.textPrimary} mb-4`}>Browsers</h3>
427
- <BarChart
428
- data={browserData}
429
- maxValue={Math.max(...browserData.map(d => d.value), 1)}
430
- colorClass="bg-orange-500"
431
- />
432
- </div>
433
-
434
- {/* By OS */}
435
- <div className={`${themeClasses.cardBg} border rounded-lg p-6`}>
436
- <h3 className={`text-lg font-semibold ${themeClasses.textPrimary} mb-4`}>Operating Systems</h3>
437
- <BarChart
438
- data={osData}
439
- maxValue={Math.max(...osData.map(d => d.value), 1)}
440
- colorClass="bg-cyan-500"
441
- />
442
- </div>
443
- </div>
444
-
445
- {/* Top Users Table */}
446
- <div className={`${themeClasses.cardBg} border rounded-lg overflow-hidden`}>
447
- <div className={`p-4 border-b ${isDark ? 'border-slate-700' : 'border-gray-200'}`}>
448
- <h3 className={`text-lg font-semibold ${themeClasses.textPrimary}`}>Top Users by Login Count</h3>
449
- </div>
450
- <div className="overflow-x-auto">
451
- <table className="w-full text-sm">
452
- <thead className={isDark ? 'bg-slate-700/50' : 'bg-gray-50'}>
453
- <tr>
454
- <th className={`px-4 py-3 text-left ${themeClasses.textSecondary} font-medium`}>#</th>
455
- <th className={`px-4 py-3 text-left ${themeClasses.textSecondary} font-medium`}>User</th>
456
- <th className={`px-4 py-3 text-left ${themeClasses.textSecondary} font-medium`}>Logins</th>
457
- <th className={`px-4 py-3 text-left ${themeClasses.textSecondary} font-medium`}>Last Login</th>
458
- </tr>
459
- </thead>
460
- <tbody className={`divide-y ${isDark ? 'divide-slate-700' : 'divide-gray-200'}`}>
461
- {data.topUsers.length > 0 ? (
462
- data.topUsers.map((user, i) => (
463
- <tr key={i} className={isDark ? 'hover:bg-slate-700/50' : 'hover:bg-gray-50'}>
464
- <td className={`px-4 py-3 ${themeClasses.textMuted}`}>{i + 1}</td>
465
- <td className="px-4 py-3">
466
- <div>
467
- <p className={`${themeClasses.textPrimary} font-medium`}>{user.name}</p>
468
- <p className={`${themeClasses.textSecondary} text-xs`}>{user.email}</p>
469
- </div>
470
- </td>
471
- <td className="px-4 py-3">
472
- <span className="px-2 py-1 bg-blue-600/20 text-blue-400 rounded text-xs font-medium">
473
- {user.count}
474
- </span>
475
- </td>
476
- <td className={`px-4 py-3 ${themeClasses.textSecondary}`}>{formatDate(user.lastLogin)}</td>
477
- </tr>
478
- ))
479
- ) : (
480
- <tr>
481
- <td colSpan={4} className={`px-4 py-8 text-center ${themeClasses.textMuted}`}>
482
- No login data for this period
483
- </td>
484
- </tr>
485
- )}
486
- </tbody>
487
- </table>
488
- </div>
489
- </div>
490
-
491
- {/* Business Metrics Section */}
492
- {showBusinessMetrics && (
493
- <div className={`border-t ${isDark ? 'border-slate-700' : 'border-gray-200'} pt-6 mt-6`}>
494
- <h2 className={`text-xl font-bold ${themeClasses.textPrimary} mb-6`}>Business Metrics</h2>
495
-
496
- {/* Revenue Summary Cards */}
497
- {revenueData && (
498
- <div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
499
- <div className={`${themeClasses.cardBg} border rounded-lg p-4`}>
500
- <p className={themeClasses.textSecondary + ' text-sm'}>Monthly Revenue (MRR)</p>
501
- <p className="text-3xl font-bold text-green-400">${revenueData.mrr.toLocaleString()}</p>
502
- </div>
503
- <div className={`${themeClasses.cardBg} border rounded-lg p-4`}>
504
- <p className={themeClasses.textSecondary + ' text-sm'}>Annual Revenue (ARR)</p>
505
- <p className="text-3xl font-bold text-green-400">${revenueData.arr.toLocaleString()}</p>
506
- </div>
507
- <div className={`${themeClasses.cardBg} border rounded-lg p-4`}>
508
- <p className={themeClasses.textSecondary + ' text-sm'}>Paid Subscribers</p>
509
- <p className="text-3xl font-bold text-blue-400">{revenueData.paidSubscribers.toLocaleString()}</p>
510
- </div>
511
- <div className={`${themeClasses.cardBg} border rounded-lg p-4`}>
512
- <p className={themeClasses.textSecondary + ' text-sm'}>Conversion Rate</p>
513
- <p className="text-3xl font-bold text-purple-400">{revenueData.conversionRate}%</p>
514
- </div>
515
- </div>
516
- )}
517
-
518
- <div className="grid md:grid-cols-2 gap-6">
519
- {/* Tier Distribution - Donut Chart */}
520
- {tierData && (
521
- <div className={`${themeClasses.cardBg} border rounded-lg p-6`}>
522
- <h3 className={`text-lg font-semibold ${themeClasses.textPrimary} mb-4`}>Tier Distribution</h3>
523
- <div className="flex items-center gap-8">
524
- {/* Donut Chart */}
525
- <div className="relative w-40 h-40">
526
- <svg viewBox="0 0 100 100" className="transform -rotate-90">
527
- {(() => {
528
- const colors: Record<string, string> = {
529
- free: '#64748b',
530
- premium: '#3b82f6',
531
- ultimate: '#f59e0b',
532
- enterprise: '#8b5cf6',
533
- };
534
- let offset = 0;
535
- return tierData.distribution.map((tier) => {
536
- const strokeDasharray = `${tier.percentage} ${100 - tier.percentage}`;
537
- const strokeDashoffset = -offset;
538
- offset += tier.percentage;
539
- return (
540
- <circle
541
- key={tier.tier}
542
- cx="50"
543
- cy="50"
544
- r="40"
545
- fill="transparent"
546
- stroke={colors[tier.tier] || '#64748b'}
547
- strokeWidth="20"
548
- strokeDasharray={strokeDasharray}
549
- strokeDashoffset={strokeDashoffset}
550
- />
551
- );
552
- });
553
- })()}
554
- </svg>
555
- <div className="absolute inset-0 flex items-center justify-center">
556
- <div className="text-center">
557
- <p className={`text-2xl font-bold ${themeClasses.textPrimary}`}>{tierData.total}</p>
558
- <p className={`text-xs ${themeClasses.textSecondary}`}>Total</p>
559
- </div>
560
- </div>
561
- </div>
562
- {/* Legend */}
563
- <div className="flex-1 space-y-2">
564
- {tierData.distribution.map((tier) => {
565
- const colors: Record<string, string> = {
566
- free: 'bg-slate-500',
567
- premium: 'bg-blue-500',
568
- ultimate: 'bg-amber-500',
569
- enterprise: 'bg-purple-500',
570
- };
571
- return (
572
- <div key={tier.tier} className="flex items-center justify-between">
573
- <div className="flex items-center gap-2">
574
- <div className={`w-3 h-3 rounded-full ${colors[tier.tier] || 'bg-slate-500'}`} />
575
- <span className={`${themeClasses.textPrimary} capitalize`}>{tier.tier}</span>
576
- </div>
577
- <div className="text-right">
578
- <span className={`${themeClasses.textPrimary} font-medium`}>{tier.count}</span>
579
- <span className={`${themeClasses.textMuted} text-sm ml-2`}>({tier.percentage}%)</span>
580
- </div>
581
- </div>
582
- );
583
- })}
584
- </div>
585
- </div>
586
- </div>
587
- )}
588
-
589
- {/* Feature Usage Chart */}
590
- {featureData && (
591
- <div className={`${themeClasses.cardBg} border rounded-lg p-6`}>
592
- <h3 className={`text-lg font-semibold ${themeClasses.textPrimary} mb-4`}>Feature Usage</h3>
593
- <div className="space-y-3">
594
- {featureData.features.map((feature) => {
595
- const maxUses = Math.max(...featureData.features.map(f => f.uses), 1);
596
- const percentage = (feature.uses / maxUses) * 100;
597
- return (
598
- <div key={feature.name}>
599
- <div className="flex justify-between items-center mb-1">
600
- <span className={`${themeClasses.textPrimary} text-sm`}>{feature.name}</span>
601
- <span className={`${themeClasses.textSecondary} text-xs`}>{feature.uses.toLocaleString()} uses</span>
602
- </div>
603
- <div className={`h-4 ${themeClasses.barBg} rounded overflow-hidden`}>
604
- <div
605
- className="h-full bg-gradient-to-r from-indigo-500 to-purple-500 transition-all duration-300"
606
- style={{ width: `${percentage}%` }}
607
- />
608
- </div>
609
- </div>
610
- );
611
- })}
612
- </div>
613
- </div>
614
- )}
615
- </div>
616
-
617
- {/* Revenue by Tier */}
618
- {revenueData && revenueData.revenueByTier.length > 0 && (
619
- <div className={`mt-6 ${themeClasses.cardBg} border rounded-lg overflow-hidden`}>
620
- <div className={`p-4 border-b ${isDark ? 'border-slate-700' : 'border-gray-200'}`}>
621
- <h3 className={`text-lg font-semibold ${themeClasses.textPrimary}`}>Revenue by Tier</h3>
622
- </div>
623
- <div className="overflow-x-auto">
624
- <table className="w-full text-sm">
625
- <thead className={isDark ? 'bg-slate-700/50' : 'bg-gray-50'}>
626
- <tr>
627
- <th className={`px-4 py-3 text-left ${themeClasses.textSecondary} font-medium`}>Tier</th>
628
- <th className={`px-4 py-3 text-left ${themeClasses.textSecondary} font-medium`}>Subscribers</th>
629
- <th className={`px-4 py-3 text-left ${themeClasses.textSecondary} font-medium`}>Price/Mo</th>
630
- <th className={`px-4 py-3 text-left ${themeClasses.textSecondary} font-medium`}>MRR</th>
631
- <th className={`px-4 py-3 text-left ${themeClasses.textSecondary} font-medium`}>% of Revenue</th>
632
- </tr>
633
- </thead>
634
- <tbody className={`divide-y ${isDark ? 'divide-slate-700' : 'divide-gray-200'}`}>
635
- {revenueData.revenueByTier.map((tier) => {
636
- const totalMrr = revenueData.revenueByTier.reduce((sum, t) => sum + t.mrr, 0);
637
- const percentage = totalMrr > 0 ? Math.round((tier.mrr / totalMrr) * 100) : 0;
638
- const tierColors: Record<string, string> = {
639
- premium: 'text-blue-400',
640
- ultimate: 'text-amber-400',
641
- enterprise: 'text-purple-400',
642
- };
643
- return (
644
- <tr key={tier.tier} className={isDark ? 'hover:bg-slate-700/50' : 'hover:bg-gray-50'}>
645
- <td className={`px-4 py-3 font-medium capitalize ${tierColors[tier.tier] || themeClasses.textPrimary}`}>
646
- {tier.tier}
647
- </td>
648
- <td className={`px-4 py-3 ${themeClasses.textPrimary}`}>{tier.subscribers.toLocaleString()}</td>
649
- <td className={`px-4 py-3 ${themeClasses.textSecondary}`}>${tier.pricePerMonth}</td>
650
- <td className="px-4 py-3 text-green-400 font-medium">${tier.mrr.toLocaleString()}</td>
651
- <td className="px-4 py-3">
652
- <div className="flex items-center gap-2">
653
- <div className={`w-16 h-2 ${themeClasses.barBg} rounded overflow-hidden`}>
654
- <div className="h-full bg-green-500" style={{ width: `${percentage}%` }} />
655
- </div>
656
- <span className={`${themeClasses.textSecondary} text-xs`}>{percentage}%</span>
657
- </div>
658
- </td>
659
- </tr>
660
- );
661
- })}
662
- </tbody>
663
- </table>
664
- </div>
665
- </div>
666
- )}
667
-
668
- {/* MRR Trend Chart */}
669
- {revenueData && revenueData.trend.length > 0 && (
670
- <div className={`mt-6 ${themeClasses.cardBg} border rounded-lg p-6`}>
671
- <h3 className={`text-lg font-semibold ${themeClasses.textPrimary} mb-4`}>MRR Trend (6 Months)</h3>
672
- <div className="h-48 flex items-end gap-2">
673
- {revenueData.trend.map((point, i) => {
674
- const maxMrr = Math.max(...revenueData.trend.map(p => p.mrr), 1);
675
- const height = (point.mrr / maxMrr) * 100;
676
- return (
677
- <div key={i} className="flex-1 flex flex-col items-center group">
678
- <div className="relative w-full">
679
- <div
680
- className="w-full bg-gradient-to-t from-green-600 to-green-400 rounded-t transition-all duration-300 hover:from-green-500 hover:to-green-300"
681
- style={{ height: `${height * 1.8}px`, minHeight: '4px' }}
682
- />
683
- {/* Tooltip */}
684
- <div className="absolute bottom-full mb-2 left-1/2 -translate-x-1/2 hidden group-hover:block z-10">
685
- <div className={`${isDark ? 'bg-slate-900' : 'bg-gray-800'} text-white text-xs px-2 py-1 rounded shadow-lg whitespace-nowrap`}>
686
- ${point.mrr.toLocaleString()}
687
- </div>
688
- </div>
689
- </div>
690
- <span className={`text-xs ${themeClasses.textMuted} mt-2`}>{point.month}</span>
691
- </div>
692
- );
693
- })}
694
- </div>
695
- </div>
696
- )}
697
- </div>
698
- )}
699
- </div>
700
- );
701
- }
702
-
703
- export default AnalyticsTab;
1
+ /**
2
+ * =============================================================================
3
+ * VIBE ADMIN ANALYTICS TAB
4
+ * =============================================================================
5
+ *
6
+ * Generic dashboard tab showing login statistics and analytics.
7
+ * Can be extended by consumer apps with custom components.
8
+ *
9
+ * =============================================================================
10
+ */
11
+
12
+ 'use client';
13
+
14
+ import React, { useState, useEffect, useCallback, ReactNode } from 'react';
15
+
16
+ // -----------------------------------------------------------------------------
17
+ // TYPES
18
+ // -----------------------------------------------------------------------------
19
+
20
+ type Period = 'today' | 'week' | 'month' | 'year';
21
+
22
+ interface TimeSeries {
23
+ date: string;
24
+ count: number;
25
+ label: string;
26
+ }
27
+
28
+ interface CountryStats {
29
+ country: string;
30
+ code: string;
31
+ flag: string;
32
+ count: number;
33
+ percentage: number;
34
+ }
35
+
36
+ interface UserStats {
37
+ email: string;
38
+ name: string;
39
+ count: number;
40
+ lastLogin: string;
41
+ }
42
+
43
+ interface TierDistribution {
44
+ tier: string;
45
+ count: number;
46
+ percentage: number;
47
+ }
48
+
49
+ interface FeatureUsage {
50
+ name: string;
51
+ uses: number;
52
+ uniqueUsers: number;
53
+ }
54
+
55
+ interface RevenueByTier {
56
+ tier: string;
57
+ subscribers: number;
58
+ pricePerMonth: number;
59
+ mrr: number;
60
+ }
61
+
62
+ interface RevenueTrend {
63
+ month: string;
64
+ mrr: number;
65
+ }
66
+
67
+ interface TierData {
68
+ distribution: TierDistribution[];
69
+ total: number;
70
+ }
71
+
72
+ interface FeatureData {
73
+ features: FeatureUsage[];
74
+ }
75
+
76
+ interface RevenueData {
77
+ mrr: number;
78
+ arr: number;
79
+ paidSubscribers: number;
80
+ totalUsers: number;
81
+ conversionRate: number;
82
+ revenueByTier: RevenueByTier[];
83
+ trend: RevenueTrend[];
84
+ currency: string;
85
+ }
86
+
87
+ interface AnalyticsData {
88
+ period: Period;
89
+ dateRange: {
90
+ start: string;
91
+ end: string;
92
+ };
93
+ summary: {
94
+ totalLogins: number;
95
+ uniqueUsers: number;
96
+ newUsers: number;
97
+ peakHour: string;
98
+ avgLoginsPerDay: number;
99
+ };
100
+ timeSeries: TimeSeries[];
101
+ byCountry: CountryStats[];
102
+ byDevice: Record<string, number>;
103
+ byBrowser: Record<string, number>;
104
+ byOS: Record<string, number>;
105
+ topUsers: UserStats[];
106
+ }
107
+
108
+ export interface AnalyticsTabProps {
109
+ isDark?: boolean;
110
+ /** Base API path for analytics (default: /api/admin/analytics) */
111
+ apiBasePath?: string;
112
+ /** Optional custom component to render in the geo section (e.g., WorldMap) */
113
+ geoMapComponent?: ReactNode;
114
+ /** Show business metrics section (default: true) */
115
+ showBusinessMetrics?: boolean;
116
+ }
117
+
118
+ // -----------------------------------------------------------------------------
119
+ // COMPONENT
120
+ // -----------------------------------------------------------------------------
121
+
122
+ export function AnalyticsTab({
123
+ isDark = true,
124
+ apiBasePath = '/api/admin/analytics',
125
+ geoMapComponent,
126
+ showBusinessMetrics = true,
127
+ }: AnalyticsTabProps) {
128
+ const [period, setPeriod] = useState<Period>('week');
129
+ const [data, setData] = useState<AnalyticsData | null>(null);
130
+ const [loading, setLoading] = useState(true);
131
+ const [error, setError] = useState<string | null>(null);
132
+
133
+ // Business metrics data
134
+ const [tierData, setTierData] = useState<TierData | null>(null);
135
+ const [featureData, setFeatureData] = useState<FeatureData | null>(null);
136
+ const [revenueData, setRevenueData] = useState<RevenueData | null>(null);
137
+
138
+ const themeClasses = {
139
+ cardBg: isDark ? 'bg-slate-800 border-slate-700' : 'bg-white border-gray-200 shadow-sm',
140
+ textPrimary: isDark ? 'text-white' : 'text-gray-900',
141
+ textSecondary: isDark ? 'text-gray-400' : 'text-gray-600',
142
+ textMuted: isDark ? 'text-gray-500' : 'text-gray-500',
143
+ barBg: isDark ? 'bg-slate-700' : 'bg-gray-200',
144
+ };
145
+
146
+ const fetchAnalytics = useCallback(async () => {
147
+ setLoading(true);
148
+ setError(null);
149
+ try {
150
+ const res = await fetch(`${apiBasePath}/logins?period=${period}`);
151
+ if (!res.ok) throw new Error('Failed to fetch analytics');
152
+ const json = await res.json();
153
+ setData(json);
154
+ } catch (err: any) {
155
+ setError(err.message);
156
+ } finally {
157
+ setLoading(false);
158
+ }
159
+ }, [period, apiBasePath]);
160
+
161
+ // Fetch tier, feature, and revenue data
162
+ const fetchBusinessMetrics = useCallback(async () => {
163
+ if (!showBusinessMetrics) return;
164
+
165
+ try {
166
+ const [tierRes, featureRes, revenueRes] = await Promise.all([
167
+ fetch(`${apiBasePath}/tiers`),
168
+ fetch(`${apiBasePath}/feature-usage`),
169
+ fetch(`${apiBasePath}/revenue`),
170
+ ]);
171
+
172
+ if (tierRes.ok) {
173
+ const data = await tierRes.json();
174
+ setTierData(data);
175
+ }
176
+ if (featureRes.ok) {
177
+ const data = await featureRes.json();
178
+ setFeatureData(data);
179
+ }
180
+ if (revenueRes.ok) {
181
+ const data = await revenueRes.json();
182
+ setRevenueData(data);
183
+ }
184
+ } catch (err) {
185
+ console.error('Failed to fetch business metrics:', err);
186
+ }
187
+ }, [apiBasePath, showBusinessMetrics]);
188
+
189
+ useEffect(() => {
190
+ fetchAnalytics();
191
+ fetchBusinessMetrics();
192
+ }, [fetchAnalytics, fetchBusinessMetrics]);
193
+
194
+ const formatDate = (dateString: string) => {
195
+ return new Date(dateString).toLocaleDateString('en-US', {
196
+ month: 'short',
197
+ day: 'numeric',
198
+ hour: '2-digit',
199
+ minute: '2-digit',
200
+ });
201
+ };
202
+
203
+ // Simple bar chart component
204
+ const BarChart = ({
205
+ data,
206
+ maxValue,
207
+ colorClass = 'bg-blue-500'
208
+ }: {
209
+ data: { label: string; value: number }[];
210
+ maxValue: number;
211
+ colorClass?: string;
212
+ }) => (
213
+ <div className="space-y-2">
214
+ {data.map((item, i) => (
215
+ <div key={i} className="flex items-center gap-2">
216
+ <span className={`text-xs ${themeClasses.textSecondary} w-20 truncate`}>{item.label}</span>
217
+ <div className={`flex-1 h-6 ${themeClasses.barBg} rounded overflow-hidden`}>
218
+ <div
219
+ className={`h-full ${colorClass} transition-all duration-300`}
220
+ style={{ width: `${maxValue > 0 ? (item.value / maxValue) * 100 : 0}%` }}
221
+ />
222
+ </div>
223
+ <span className={`text-xs ${themeClasses.textPrimary} w-12 text-right`}>{item.value}</span>
224
+ </div>
225
+ ))}
226
+ </div>
227
+ );
228
+
229
+ // Time series line chart (simplified as bar chart)
230
+ const TimeSeriesChart = ({ series }: { series: TimeSeries[] }) => {
231
+ const maxCount = Math.max(...series.map(s => s.count), 1);
232
+ return (
233
+ <div className="h-48 flex items-end gap-1">
234
+ {series.map((item, i) => (
235
+ <div
236
+ key={i}
237
+ className="flex-1 flex flex-col items-center group relative"
238
+ >
239
+ <div
240
+ className="w-full bg-blue-500 rounded-t transition-all duration-300 hover:bg-blue-400"
241
+ style={{ height: `${(item.count / maxCount) * 100}%`, minHeight: item.count > 0 ? '4px' : '0' }}
242
+ />
243
+ {/* Tooltip */}
244
+ <div className="absolute bottom-full mb-2 hidden group-hover:block z-10">
245
+ <div className={`${isDark ? 'bg-slate-900' : 'bg-gray-800'} text-white text-xs px-2 py-1 rounded shadow-lg whitespace-nowrap`}>
246
+ {item.label}: {item.count}
247
+ </div>
248
+ </div>
249
+ </div>
250
+ ))}
251
+ </div>
252
+ );
253
+ };
254
+
255
+ if (loading && !data) {
256
+ return (
257
+ <div className="flex items-center justify-center py-12">
258
+ <svg className="animate-spin h-8 w-8 text-blue-400" viewBox="0 0 24 24" fill="none">
259
+ <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
260
+ <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z" />
261
+ </svg>
262
+ </div>
263
+ );
264
+ }
265
+
266
+ if (error) {
267
+ return (
268
+ <div className={`${isDark ? 'bg-red-900/30 border-red-700' : 'bg-red-50 border-red-300'} border rounded-lg p-4 ${isDark ? 'text-red-300' : 'text-red-700'}`}>
269
+ {error}
270
+ <button onClick={fetchAnalytics} className="ml-4 underline">Retry</button>
271
+ </div>
272
+ );
273
+ }
274
+
275
+ if (!data) return null;
276
+
277
+ const deviceData = Object.entries(data.byDevice)
278
+ .map(([label, value]) => ({ label: label.charAt(0).toUpperCase() + label.slice(1), value }))
279
+ .sort((a, b) => b.value - a.value);
280
+
281
+ const browserData = Object.entries(data.byBrowser)
282
+ .map(([label, value]) => ({ label, value }))
283
+ .sort((a, b) => b.value - a.value)
284
+ .slice(0, 5);
285
+
286
+ const osData = Object.entries(data.byOS)
287
+ .map(([label, value]) => ({ label, value }))
288
+ .sort((a, b) => b.value - a.value)
289
+ .slice(0, 5);
290
+
291
+ return (
292
+ <div className="space-y-6">
293
+ {/* Period Selector */}
294
+ <div className="flex items-center justify-between">
295
+ <div className="flex gap-2">
296
+ {(['today', 'week', 'month', 'year'] as Period[]).map((p) => (
297
+ <button
298
+ key={p}
299
+ onClick={() => setPeriod(p)}
300
+ className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
301
+ period === p
302
+ ? 'bg-blue-600 text-white'
303
+ : isDark ? 'bg-slate-700 text-gray-300 hover:bg-slate-600' : 'bg-gray-200 text-gray-700 hover:bg-gray-300'
304
+ }`}
305
+ >
306
+ {p === 'today' ? 'Today' : p === 'week' ? 'This Week' : p === 'month' ? 'This Month' : 'This Year'}
307
+ </button>
308
+ ))}
309
+ </div>
310
+ <button
311
+ onClick={fetchAnalytics}
312
+ disabled={loading}
313
+ className={`p-2 ${themeClasses.textSecondary} hover:${themeClasses.textPrimary} transition-colors disabled:opacity-50`}
314
+ title="Refresh"
315
+ >
316
+ <svg className={`w-5 h-5 ${loading ? 'animate-spin' : ''}`} fill="none" viewBox="0 0 24 24" stroke="currentColor">
317
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
318
+ </svg>
319
+ </button>
320
+ </div>
321
+
322
+ {/* Summary Cards */}
323
+ <div className="grid grid-cols-2 md:grid-cols-5 gap-4">
324
+ <div className={`${themeClasses.cardBg} border rounded-lg p-4`}>
325
+ <p className={themeClasses.textSecondary + ' text-sm'}>Total Logins</p>
326
+ <p className={`text-3xl font-bold ${themeClasses.textPrimary}`}>{data.summary.totalLogins.toLocaleString()}</p>
327
+ </div>
328
+ <div className={`${themeClasses.cardBg} border rounded-lg p-4`}>
329
+ <p className={themeClasses.textSecondary + ' text-sm'}>Unique Users</p>
330
+ <p className="text-3xl font-bold text-blue-400">{data.summary.uniqueUsers.toLocaleString()}</p>
331
+ </div>
332
+ <div className={`${themeClasses.cardBg} border rounded-lg p-4`}>
333
+ <p className={themeClasses.textSecondary + ' text-sm'}>New Users</p>
334
+ <p className="text-3xl font-bold text-green-400">{data.summary.newUsers.toLocaleString()}</p>
335
+ </div>
336
+ <div className={`${themeClasses.cardBg} border rounded-lg p-4`}>
337
+ <p className={themeClasses.textSecondary + ' text-sm'}>Peak Hour</p>
338
+ <p className="text-3xl font-bold text-purple-400">{data.summary.peakHour}</p>
339
+ </div>
340
+ <div className={`${themeClasses.cardBg} border rounded-lg p-4`}>
341
+ <p className={themeClasses.textSecondary + ' text-sm'}>Avg/Day</p>
342
+ <p className="text-3xl font-bold text-orange-400">{data.summary.avgLoginsPerDay.toLocaleString()}</p>
343
+ </div>
344
+ </div>
345
+
346
+ {/* Time Series Chart */}
347
+ <div className={`${themeClasses.cardBg} border rounded-lg p-6`}>
348
+ <h3 className={`text-lg font-semibold ${themeClasses.textPrimary} mb-4`}>Logins Over Time</h3>
349
+ <TimeSeriesChart series={data.timeSeries} />
350
+ <div className={`flex justify-between mt-2 text-xs ${themeClasses.textMuted}`}>
351
+ <span>{data.timeSeries[0]?.label}</span>
352
+ <span>{data.timeSeries[data.timeSeries.length - 1]?.label}</span>
353
+ </div>
354
+ </div>
355
+
356
+ {/* Geographic Map (optional slot) */}
357
+ {geoMapComponent && (
358
+ <div className={`${themeClasses.cardBg} border rounded-lg p-6`}>
359
+ <h3 className={`text-lg font-semibold ${themeClasses.textPrimary} mb-4`}>Geographic Distribution</h3>
360
+ {geoMapComponent}
361
+ </div>
362
+ )}
363
+
364
+ {/* Charts Grid */}
365
+ <div className="grid md:grid-cols-2 gap-6">
366
+ {/* By Country */}
367
+ <div className={`${themeClasses.cardBg} border rounded-lg p-6`}>
368
+ <h3 className={`text-lg font-semibold ${themeClasses.textPrimary} mb-4`}>Top Countries</h3>
369
+ {data.byCountry.length > 0 ? (
370
+ <div className="space-y-3">
371
+ {data.byCountry.slice(0, 8).map((country, i) => (
372
+ <div key={i} className="flex items-center gap-3">
373
+ <span className="text-xl">{country.flag}</span>
374
+ <span className={`${themeClasses.textPrimary} flex-1`}>{country.country}</span>
375
+ <div className={`w-24 h-4 ${themeClasses.barBg} rounded overflow-hidden`}>
376
+ <div
377
+ className="h-full bg-green-500"
378
+ style={{ width: `${country.percentage}%` }}
379
+ />
380
+ </div>
381
+ <span className={`${themeClasses.textSecondary} text-sm w-16 text-right`}>
382
+ {country.count} ({country.percentage}%)
383
+ </span>
384
+ </div>
385
+ ))}
386
+ </div>
387
+ ) : (
388
+ <p className={`${themeClasses.textMuted} text-center py-4`}>No data</p>
389
+ )}
390
+ </div>
391
+
392
+ {/* By Device */}
393
+ <div className={`${themeClasses.cardBg} border rounded-lg p-6`}>
394
+ <h3 className={`text-lg font-semibold ${themeClasses.textPrimary} mb-4`}>Device Types</h3>
395
+ {deviceData.length > 0 ? (
396
+ <div className="space-y-4">
397
+ {deviceData.map((item, i) => {
398
+ const total = deviceData.reduce((sum, d) => sum + d.value, 0);
399
+ const percentage = total > 0 ? Math.round((item.value / total) * 100) : 0;
400
+ const icon = item.label.toLowerCase() === 'mobile' ? '📱' :
401
+ item.label.toLowerCase() === 'tablet' ? '📱' : '💻';
402
+ return (
403
+ <div key={i} className="flex items-center gap-3">
404
+ <span className="text-xl">{icon}</span>
405
+ <span className={`${themeClasses.textPrimary} w-20`}>{item.label}</span>
406
+ <div className={`flex-1 h-6 ${themeClasses.barBg} rounded overflow-hidden`}>
407
+ <div
408
+ className="h-full bg-purple-500"
409
+ style={{ width: `${percentage}%` }}
410
+ />
411
+ </div>
412
+ <span className={`${themeClasses.textSecondary} text-sm w-20 text-right`}>
413
+ {item.value} ({percentage}%)
414
+ </span>
415
+ </div>
416
+ );
417
+ })}
418
+ </div>
419
+ ) : (
420
+ <p className={`${themeClasses.textMuted} text-center py-4`}>No data</p>
421
+ )}
422
+ </div>
423
+
424
+ {/* By Browser */}
425
+ <div className={`${themeClasses.cardBg} border rounded-lg p-6`}>
426
+ <h3 className={`text-lg font-semibold ${themeClasses.textPrimary} mb-4`}>Browsers</h3>
427
+ <BarChart
428
+ data={browserData}
429
+ maxValue={Math.max(...browserData.map(d => d.value), 1)}
430
+ colorClass="bg-orange-500"
431
+ />
432
+ </div>
433
+
434
+ {/* By OS */}
435
+ <div className={`${themeClasses.cardBg} border rounded-lg p-6`}>
436
+ <h3 className={`text-lg font-semibold ${themeClasses.textPrimary} mb-4`}>Operating Systems</h3>
437
+ <BarChart
438
+ data={osData}
439
+ maxValue={Math.max(...osData.map(d => d.value), 1)}
440
+ colorClass="bg-cyan-500"
441
+ />
442
+ </div>
443
+ </div>
444
+
445
+ {/* Top Users Table */}
446
+ <div className={`${themeClasses.cardBg} border rounded-lg overflow-hidden`}>
447
+ <div className={`p-4 border-b ${isDark ? 'border-slate-700' : 'border-gray-200'}`}>
448
+ <h3 className={`text-lg font-semibold ${themeClasses.textPrimary}`}>Top Users by Login Count</h3>
449
+ </div>
450
+ <div className="overflow-x-auto">
451
+ <table className="w-full text-sm">
452
+ <thead className={isDark ? 'bg-slate-700/50' : 'bg-gray-50'}>
453
+ <tr>
454
+ <th className={`px-4 py-3 text-left ${themeClasses.textSecondary} font-medium`}>#</th>
455
+ <th className={`px-4 py-3 text-left ${themeClasses.textSecondary} font-medium`}>User</th>
456
+ <th className={`px-4 py-3 text-left ${themeClasses.textSecondary} font-medium`}>Logins</th>
457
+ <th className={`px-4 py-3 text-left ${themeClasses.textSecondary} font-medium`}>Last Login</th>
458
+ </tr>
459
+ </thead>
460
+ <tbody className={`divide-y ${isDark ? 'divide-slate-700' : 'divide-gray-200'}`}>
461
+ {data.topUsers.length > 0 ? (
462
+ data.topUsers.map((user, i) => (
463
+ <tr key={i} className={isDark ? 'hover:bg-slate-700/50' : 'hover:bg-gray-50'}>
464
+ <td className={`px-4 py-3 ${themeClasses.textMuted}`}>{i + 1}</td>
465
+ <td className="px-4 py-3">
466
+ <div>
467
+ <p className={`${themeClasses.textPrimary} font-medium`}>{user.name}</p>
468
+ <p className={`${themeClasses.textSecondary} text-xs`}>{user.email}</p>
469
+ </div>
470
+ </td>
471
+ <td className="px-4 py-3">
472
+ <span className="px-2 py-1 bg-blue-600/20 text-blue-400 rounded text-xs font-medium">
473
+ {user.count}
474
+ </span>
475
+ </td>
476
+ <td className={`px-4 py-3 ${themeClasses.textSecondary}`}>{formatDate(user.lastLogin)}</td>
477
+ </tr>
478
+ ))
479
+ ) : (
480
+ <tr>
481
+ <td colSpan={4} className={`px-4 py-8 text-center ${themeClasses.textMuted}`}>
482
+ No login data for this period
483
+ </td>
484
+ </tr>
485
+ )}
486
+ </tbody>
487
+ </table>
488
+ </div>
489
+ </div>
490
+
491
+ {/* Business Metrics Section */}
492
+ {showBusinessMetrics && (
493
+ <div className={`border-t ${isDark ? 'border-slate-700' : 'border-gray-200'} pt-6 mt-6`}>
494
+ <h2 className={`text-xl font-bold ${themeClasses.textPrimary} mb-6`}>Business Metrics</h2>
495
+
496
+ {/* Revenue Summary Cards */}
497
+ {revenueData && (
498
+ <div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
499
+ <div className={`${themeClasses.cardBg} border rounded-lg p-4`}>
500
+ <p className={themeClasses.textSecondary + ' text-sm'}>Monthly Revenue (MRR)</p>
501
+ <p className="text-3xl font-bold text-green-400">${revenueData.mrr.toLocaleString()}</p>
502
+ </div>
503
+ <div className={`${themeClasses.cardBg} border rounded-lg p-4`}>
504
+ <p className={themeClasses.textSecondary + ' text-sm'}>Annual Revenue (ARR)</p>
505
+ <p className="text-3xl font-bold text-green-400">${revenueData.arr.toLocaleString()}</p>
506
+ </div>
507
+ <div className={`${themeClasses.cardBg} border rounded-lg p-4`}>
508
+ <p className={themeClasses.textSecondary + ' text-sm'}>Paid Subscribers</p>
509
+ <p className="text-3xl font-bold text-blue-400">{revenueData.paidSubscribers.toLocaleString()}</p>
510
+ </div>
511
+ <div className={`${themeClasses.cardBg} border rounded-lg p-4`}>
512
+ <p className={themeClasses.textSecondary + ' text-sm'}>Conversion Rate</p>
513
+ <p className="text-3xl font-bold text-purple-400">{revenueData.conversionRate}%</p>
514
+ </div>
515
+ </div>
516
+ )}
517
+
518
+ <div className="grid md:grid-cols-2 gap-6">
519
+ {/* Tier Distribution - Donut Chart */}
520
+ {tierData && (
521
+ <div className={`${themeClasses.cardBg} border rounded-lg p-6`}>
522
+ <h3 className={`text-lg font-semibold ${themeClasses.textPrimary} mb-4`}>Tier Distribution</h3>
523
+ <div className="flex items-center gap-8">
524
+ {/* Donut Chart */}
525
+ <div className="relative w-40 h-40">
526
+ <svg viewBox="0 0 100 100" className="transform -rotate-90">
527
+ {(() => {
528
+ const colors: Record<string, string> = {
529
+ free: '#64748b',
530
+ premium: '#3b82f6',
531
+ ultimate: '#f59e0b',
532
+ enterprise: '#8b5cf6',
533
+ };
534
+ let offset = 0;
535
+ return tierData.distribution.map((tier) => {
536
+ const strokeDasharray = `${tier.percentage} ${100 - tier.percentage}`;
537
+ const strokeDashoffset = -offset;
538
+ offset += tier.percentage;
539
+ return (
540
+ <circle
541
+ key={tier.tier}
542
+ cx="50"
543
+ cy="50"
544
+ r="40"
545
+ fill="transparent"
546
+ stroke={colors[tier.tier] || '#64748b'}
547
+ strokeWidth="20"
548
+ strokeDasharray={strokeDasharray}
549
+ strokeDashoffset={strokeDashoffset}
550
+ />
551
+ );
552
+ });
553
+ })()}
554
+ </svg>
555
+ <div className="absolute inset-0 flex items-center justify-center">
556
+ <div className="text-center">
557
+ <p className={`text-2xl font-bold ${themeClasses.textPrimary}`}>{tierData.total}</p>
558
+ <p className={`text-xs ${themeClasses.textSecondary}`}>Total</p>
559
+ </div>
560
+ </div>
561
+ </div>
562
+ {/* Legend */}
563
+ <div className="flex-1 space-y-2">
564
+ {tierData.distribution.map((tier) => {
565
+ const colors: Record<string, string> = {
566
+ free: 'bg-slate-500',
567
+ premium: 'bg-blue-500',
568
+ ultimate: 'bg-amber-500',
569
+ enterprise: 'bg-purple-500',
570
+ };
571
+ return (
572
+ <div key={tier.tier} className="flex items-center justify-between">
573
+ <div className="flex items-center gap-2">
574
+ <div className={`w-3 h-3 rounded-full ${colors[tier.tier] || 'bg-slate-500'}`} />
575
+ <span className={`${themeClasses.textPrimary} capitalize`}>{tier.tier}</span>
576
+ </div>
577
+ <div className="text-right">
578
+ <span className={`${themeClasses.textPrimary} font-medium`}>{tier.count}</span>
579
+ <span className={`${themeClasses.textMuted} text-sm ml-2`}>({tier.percentage}%)</span>
580
+ </div>
581
+ </div>
582
+ );
583
+ })}
584
+ </div>
585
+ </div>
586
+ </div>
587
+ )}
588
+
589
+ {/* Feature Usage Chart */}
590
+ {featureData && (
591
+ <div className={`${themeClasses.cardBg} border rounded-lg p-6`}>
592
+ <h3 className={`text-lg font-semibold ${themeClasses.textPrimary} mb-4`}>Feature Usage</h3>
593
+ <div className="space-y-3">
594
+ {featureData.features.map((feature) => {
595
+ const maxUses = Math.max(...featureData.features.map(f => f.uses), 1);
596
+ const percentage = (feature.uses / maxUses) * 100;
597
+ return (
598
+ <div key={feature.name}>
599
+ <div className="flex justify-between items-center mb-1">
600
+ <span className={`${themeClasses.textPrimary} text-sm`}>{feature.name}</span>
601
+ <span className={`${themeClasses.textSecondary} text-xs`}>{feature.uses.toLocaleString()} uses</span>
602
+ </div>
603
+ <div className={`h-4 ${themeClasses.barBg} rounded overflow-hidden`}>
604
+ <div
605
+ className="h-full bg-gradient-to-r from-indigo-500 to-purple-500 transition-all duration-300"
606
+ style={{ width: `${percentage}%` }}
607
+ />
608
+ </div>
609
+ </div>
610
+ );
611
+ })}
612
+ </div>
613
+ </div>
614
+ )}
615
+ </div>
616
+
617
+ {/* Revenue by Tier */}
618
+ {revenueData && revenueData.revenueByTier.length > 0 && (
619
+ <div className={`mt-6 ${themeClasses.cardBg} border rounded-lg overflow-hidden`}>
620
+ <div className={`p-4 border-b ${isDark ? 'border-slate-700' : 'border-gray-200'}`}>
621
+ <h3 className={`text-lg font-semibold ${themeClasses.textPrimary}`}>Revenue by Tier</h3>
622
+ </div>
623
+ <div className="overflow-x-auto">
624
+ <table className="w-full text-sm">
625
+ <thead className={isDark ? 'bg-slate-700/50' : 'bg-gray-50'}>
626
+ <tr>
627
+ <th className={`px-4 py-3 text-left ${themeClasses.textSecondary} font-medium`}>Tier</th>
628
+ <th className={`px-4 py-3 text-left ${themeClasses.textSecondary} font-medium`}>Subscribers</th>
629
+ <th className={`px-4 py-3 text-left ${themeClasses.textSecondary} font-medium`}>Price/Mo</th>
630
+ <th className={`px-4 py-3 text-left ${themeClasses.textSecondary} font-medium`}>MRR</th>
631
+ <th className={`px-4 py-3 text-left ${themeClasses.textSecondary} font-medium`}>% of Revenue</th>
632
+ </tr>
633
+ </thead>
634
+ <tbody className={`divide-y ${isDark ? 'divide-slate-700' : 'divide-gray-200'}`}>
635
+ {revenueData.revenueByTier.map((tier) => {
636
+ const totalMrr = revenueData.revenueByTier.reduce((sum, t) => sum + t.mrr, 0);
637
+ const percentage = totalMrr > 0 ? Math.round((tier.mrr / totalMrr) * 100) : 0;
638
+ const tierColors: Record<string, string> = {
639
+ premium: 'text-blue-400',
640
+ ultimate: 'text-amber-400',
641
+ enterprise: 'text-purple-400',
642
+ };
643
+ return (
644
+ <tr key={tier.tier} className={isDark ? 'hover:bg-slate-700/50' : 'hover:bg-gray-50'}>
645
+ <td className={`px-4 py-3 font-medium capitalize ${tierColors[tier.tier] || themeClasses.textPrimary}`}>
646
+ {tier.tier}
647
+ </td>
648
+ <td className={`px-4 py-3 ${themeClasses.textPrimary}`}>{tier.subscribers.toLocaleString()}</td>
649
+ <td className={`px-4 py-3 ${themeClasses.textSecondary}`}>${tier.pricePerMonth}</td>
650
+ <td className="px-4 py-3 text-green-400 font-medium">${tier.mrr.toLocaleString()}</td>
651
+ <td className="px-4 py-3">
652
+ <div className="flex items-center gap-2">
653
+ <div className={`w-16 h-2 ${themeClasses.barBg} rounded overflow-hidden`}>
654
+ <div className="h-full bg-green-500" style={{ width: `${percentage}%` }} />
655
+ </div>
656
+ <span className={`${themeClasses.textSecondary} text-xs`}>{percentage}%</span>
657
+ </div>
658
+ </td>
659
+ </tr>
660
+ );
661
+ })}
662
+ </tbody>
663
+ </table>
664
+ </div>
665
+ </div>
666
+ )}
667
+
668
+ {/* MRR Trend Chart */}
669
+ {revenueData && revenueData.trend.length > 0 && (
670
+ <div className={`mt-6 ${themeClasses.cardBg} border rounded-lg p-6`}>
671
+ <h3 className={`text-lg font-semibold ${themeClasses.textPrimary} mb-4`}>MRR Trend (6 Months)</h3>
672
+ <div className="h-48 flex items-end gap-2">
673
+ {revenueData.trend.map((point, i) => {
674
+ const maxMrr = Math.max(...revenueData.trend.map(p => p.mrr), 1);
675
+ const height = (point.mrr / maxMrr) * 100;
676
+ return (
677
+ <div key={i} className="flex-1 flex flex-col items-center group">
678
+ <div className="relative w-full">
679
+ <div
680
+ className="w-full bg-gradient-to-t from-green-600 to-green-400 rounded-t transition-all duration-300 hover:from-green-500 hover:to-green-300"
681
+ style={{ height: `${height * 1.8}px`, minHeight: '4px' }}
682
+ />
683
+ {/* Tooltip */}
684
+ <div className="absolute bottom-full mb-2 left-1/2 -translate-x-1/2 hidden group-hover:block z-10">
685
+ <div className={`${isDark ? 'bg-slate-900' : 'bg-gray-800'} text-white text-xs px-2 py-1 rounded shadow-lg whitespace-nowrap`}>
686
+ ${point.mrr.toLocaleString()}
687
+ </div>
688
+ </div>
689
+ </div>
690
+ <span className={`text-xs ${themeClasses.textMuted} mt-2`}>{point.month}</span>
691
+ </div>
692
+ );
693
+ })}
694
+ </div>
695
+ </div>
696
+ )}
697
+ </div>
698
+ )}
699
+ </div>
700
+ );
701
+ }
702
+
703
+ export default AnalyticsTab;