@umituz/web-dashboard 2.2.0 → 2.4.0

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/web-dashboard",
3
- "version": "2.2.0",
3
+ "version": "2.4.0",
4
4
  "description": "Dashboard Layout System - Customizable, themeable dashboard layouts and settings",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -33,6 +33,11 @@
33
33
  "./analytics/hooks": "./src/domains/analytics/hooks/index.ts",
34
34
  "./analytics/utils": "./src/domains/analytics/utils/index.ts",
35
35
  "./analytics/types": "./src/domains/analytics/types/index.ts",
36
+ "./billing": "./src/domains/billing/index.ts",
37
+ "./billing/components": "./src/domains/billing/components/index.ts",
38
+ "./billing/hooks": "./src/domains/billing/hooks/index.ts",
39
+ "./billing/utils": "./src/domains/billing/utils/index.ts",
40
+ "./billing/types": "./src/domains/billing/types/index.ts",
36
41
  "./package.json": "./package.json"
37
42
  },
38
43
  "files": [
@@ -85,6 +90,12 @@
85
90
  "kpi",
86
91
  "visualization",
87
92
  "recharts",
93
+ "billing",
94
+ "subscription",
95
+ "payment",
96
+ "invoices",
97
+ "pricing",
98
+ "plans",
88
99
  "react",
89
100
  "typescript",
90
101
  "components",
@@ -4,7 +4,7 @@
4
4
  * Configurable analytics page layout with KPIs and charts
5
5
  */
6
6
 
7
- import { RefreshCw, Download, Calendar } from "lucide-react";
7
+ import { RefreshCw, Download, Calendar, Loader2 } from "lucide-react";
8
8
  import { cn } from "@umituz/web-design-system/utils";
9
9
  import { Button } from "@umituz/web-design-system/atoms";
10
10
  import type { AnalyticsLayoutProps } from "../types/analytics";
@@ -13,11 +13,16 @@ import { MetricCard } from "./MetricCard";
13
13
  import { getDateRangePresets } from "../utils/analytics";
14
14
 
15
15
  export const AnalyticsLayout = ({
16
+ config,
16
17
  title,
17
18
  description,
19
+ metrics,
20
+ loading = false,
21
+ period,
22
+ onPeriodChange,
18
23
  showDateRange = true,
19
- showRefresh = true,
20
- showExport = true,
24
+ showRefresh,
25
+ showExport,
21
26
  kpis,
22
27
  charts,
23
28
  headerContent,
@@ -35,38 +40,50 @@ export const AnalyticsLayout = ({
35
40
  console.log("Refreshing analytics data...");
36
41
  };
37
42
 
43
+ // Use config settings if props not provided
44
+ const _showRefresh = showRefresh ?? config?.showRefresh ?? true;
45
+ const _showExport = showExport ?? config?.showExport ?? true;
46
+
38
47
  return (
39
48
  <div className="w-full space-y-6">
40
49
  {/* Header */}
41
50
  <div className="flex items-center justify-between">
42
51
  <div>
43
- {title && <h1 className="text-3xl font-bold text-foreground">{title}</h1>}
52
+ {(title || config?.brandName) && (
53
+ <h1 className="text-3xl font-bold text-foreground">
54
+ {title || `${config?.brandName || ''} Analytics`}
55
+ </h1>
56
+ )}
44
57
  {description && <p className="text-muted-foreground mt-1">{description}</p>}
45
58
  </div>
46
59
 
47
60
  {/* Actions */}
48
61
  <div className="flex items-center gap-3">
49
- {showDateRange && (
62
+ {showDateRange && onPeriodChange && (
50
63
  <div className="flex items-center gap-2 bg-background border border-border rounded-lg px-3 py-2">
51
64
  <Calendar className="h-4 w-4 text-muted-foreground" />
52
- <select className="bg-transparent text-sm text-foreground outline-none">
53
- {dateRangePresets.map((preset) => (
54
- <option key={preset.value} value={preset.value}>
55
- {preset.label}
65
+ <select
66
+ className="bg-transparent text-sm text-foreground outline-none"
67
+ value={period}
68
+ onChange={(e) => onPeriodChange(e.target.value)}
69
+ >
70
+ {config?.availablePeriods?.map((p) => (
71
+ <option key={p} value={p}>
72
+ {p === "7d" ? "7 Days" : p === "30d" ? "30 Days" : p === "90d" ? "90 Days" : p}
56
73
  </option>
57
74
  ))}
58
75
  </select>
59
76
  </div>
60
77
  )}
61
78
 
62
- {showRefresh && (
63
- <Button variant="ghost" size="sm" onClick={handleRefresh}>
64
- <RefreshCw className="h-4 w-4" />
79
+ {_showRefresh && (
80
+ <Button variant="ghost" size="sm" onClick={handleRefresh} disabled={loading}>
81
+ <RefreshCw className={cn("h-4 w-4", loading && "animate-spin")} />
65
82
  </Button>
66
83
  )}
67
84
 
68
- {showExport && (
69
- <Button variant="ghost" size="sm" onClick={handleExport}>
85
+ {_showExport && (
86
+ <Button variant="ghost" size="sm" onClick={handleExport} disabled={loading}>
70
87
  <Download className="h-4 w-4" />
71
88
  Export
72
89
  </Button>
@@ -76,59 +93,78 @@ export const AnalyticsLayout = ({
76
93
  </div>
77
94
  </div>
78
95
 
79
- {/* KPI Cards */}
80
- {kpis && (
81
- <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-5 gap-4">
82
- <MetricCard
83
- metric={{
84
- id: "downloads",
85
- name: "Downloads",
86
- value: kpis.downloads.current,
87
- previousValue: kpis.downloads.previous,
88
- unit: "K",
89
- }}
90
- />
91
- <MetricCard
92
- metric={{
93
- id: "engagement",
94
- name: "Engagement",
95
- value: kpis.engagement.current,
96
- previousValue: kpis.engagement.previous,
97
- unit: "%",
98
- }}
99
- />
100
- <MetricCard
101
- metric={{
102
- id: "users",
103
- name: "Users",
104
- value: kpis.users.current,
105
- previousValue: kpis.users.previous,
106
- unit: "K",
107
- }}
108
- />
109
- <MetricCard
110
- metric={{
111
- id: "revenue",
112
- name: "Revenue",
113
- value: kpis.revenue.current,
114
- previousValue: kpis.revenue.previous,
115
- unit: "$",
116
- }}
117
- />
118
- <MetricCard
119
- metric={{
120
- id: "retention",
121
- name: "Retention",
122
- value: kpis.retention.current,
123
- previousValue: kpis.retention.previous,
124
- unit: "%",
125
- }}
126
- />
96
+ {/* Loading State */}
97
+ {loading && (
98
+ <div className="flex items-center justify-center py-24">
99
+ <Loader2 className="h-12 w-12 animate-spin text-muted-foreground" />
100
+ </div>
101
+ )}
102
+
103
+ {/* Metrics / KPI Cards */}
104
+ {!loading && (metrics || kpis) && (
105
+ <div className={cn(
106
+ "grid gap-4",
107
+ (metrics || kpis) && "grid-cols-1 sm:grid-cols-2 lg:grid-cols-5"
108
+ )}>
109
+ {metrics?.map((metric) => (
110
+ <MetricCard key={metric.id} metric={metric} />
111
+ ))}
112
+
113
+ {/* Legacy kpis support */}
114
+ {kpis && !metrics && (
115
+ <>
116
+ <MetricCard
117
+ metric={{
118
+ id: "downloads",
119
+ name: "Downloads",
120
+ value: kpis.downloads.current,
121
+ previousValue: kpis.downloads.previous,
122
+ unit: "K",
123
+ }}
124
+ />
125
+ <MetricCard
126
+ metric={{
127
+ id: "engagement",
128
+ name: "Engagement",
129
+ value: kpis.engagement.current,
130
+ previousValue: kpis.engagement.previous,
131
+ unit: "%",
132
+ }}
133
+ />
134
+ <MetricCard
135
+ metric={{
136
+ id: "users",
137
+ name: "Users",
138
+ value: kpis.users.current,
139
+ previousValue: kpis.users.previous,
140
+ unit: "K",
141
+ }}
142
+ />
143
+ <MetricCard
144
+ metric={{
145
+ id: "revenue",
146
+ name: "Revenue",
147
+ value: kpis.revenue.current,
148
+ previousValue: kpis.revenue.previous,
149
+ unit: "$",
150
+ }}
151
+ />
152
+ <MetricCard
153
+ metric={{
154
+ id: "retention",
155
+ name: "Retention",
156
+ value: kpis.retention.current,
157
+ previousValue: kpis.retention.previous,
158
+ unit: "%",
159
+ }}
160
+ />
161
+ </>
162
+ )}
127
163
  </div>
128
164
  )}
129
165
 
130
166
  {/* Charts */}
131
- {charts && charts.length > 0 && (
167
+ {!loading && charts && charts.length > 0 && (
132
168
  <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
133
169
  {charts.map((chartConfig, index) => (
134
170
  <AnalyticsCard
@@ -142,7 +178,7 @@ export const AnalyticsLayout = ({
142
178
  )}
143
179
 
144
180
  {/* Custom Content */}
145
- {children}
181
+ {!loading && children}
146
182
  </div>
147
183
  );
148
184
  };
@@ -225,10 +225,20 @@ export interface AnalyticsCardProps {
225
225
  * Analytics layout props
226
226
  */
227
227
  export interface AnalyticsLayoutProps {
228
+ /** Analytics configuration */
229
+ config?: AnalyticsConfig;
228
230
  /** Page title */
229
231
  title?: string;
230
232
  /** Page description */
231
233
  description?: string;
234
+ /** Metrics data */
235
+ metrics?: Metric[];
236
+ /** Loading state */
237
+ loading?: boolean;
238
+ /** Current period */
239
+ period?: string;
240
+ /** Period change handler */
241
+ onPeriodChange?: (period: string) => void;
232
242
  /** Date range selector */
233
243
  showDateRange?: boolean;
234
244
  /** Refresh button */
@@ -289,3 +299,38 @@ export interface AnalyticsExportOptions {
289
299
  /** Filename */
290
300
  filename?: string;
291
301
  }
302
+
303
+ /**
304
+ * Analytics configuration
305
+ */
306
+ export interface AnalyticsConfig {
307
+ /** Brand/application name */
308
+ brandName: string;
309
+ /** Default period selection */
310
+ defaultPeriod?: string;
311
+ /** Available period options */
312
+ availablePeriods?: string[];
313
+ /** Show export button */
314
+ showExport?: boolean;
315
+ /** Show refresh button */
316
+ showRefresh?: boolean;
317
+ /** Enable real-time updates */
318
+ enableRealTime?: boolean;
319
+ /** Metrics configuration */
320
+ metrics?: MetricConfig[];
321
+ }
322
+
323
+ /**
324
+ * Metric configuration
325
+ */
326
+ export interface MetricConfig {
327
+ /** Metric identifier */
328
+ id: string;
329
+ /** Metric name */
330
+ name: string;
331
+ /** Unit type */
332
+ unit?: string;
333
+ /** Icon identifier */
334
+ icon?: string;
335
+ }
336
+
@@ -5,6 +5,7 @@
5
5
  */
6
6
 
7
7
  import { BrandLogo } from "../../layouts/components";
8
+ import { cn } from "@umituz/web-design-system/utils";
8
9
  import type { AuthLayoutProps } from "../types/auth";
9
10
 
10
11
  export const AuthLayout = ({
@@ -18,8 +18,11 @@ export const LoginForm = ({
18
18
  showRememberMe = true,
19
19
  showForgotPassword = true,
20
20
  showRegisterLink = true,
21
+ showSocialLogin,
21
22
  onLoginSuccess,
22
23
  onLoginError,
24
+ onGoogleLogin,
25
+ onAppleLogin,
23
26
  }: LoginFormProps) => {
24
27
  const navigate = useNavigate();
25
28
  const [email, setEmail] = useState(defaultCredentials.email || "");
@@ -221,6 +224,62 @@ export const LoginForm = ({
221
224
  </button>
222
225
  </p>
223
226
  )}
227
+
228
+ {/* Social Login */}
229
+ {showSocialLogin && config.showSocialLogin && config.socialProviders && config.socialProviders.length > 0 && (
230
+ <>
231
+ {/* Divider */}
232
+ <div className="relative">
233
+ <div className="absolute inset-0 flex items-center">
234
+ <div className="w-full border-t border-border" />
235
+ </div>
236
+ <div className="relative flex justify-center text-sm">
237
+ <span className="px-2 bg-background text-muted-foreground">
238
+ Or continue with
239
+ </span>
240
+ </div>
241
+ </div>
242
+
243
+ {/* Social Buttons */}
244
+ <div className="grid grid-cols-2 gap-3">
245
+ {config.socialProviders.map((provider) => (
246
+ <button
247
+ key={provider.id}
248
+ type="button"
249
+ className={cn(
250
+ "flex items-center justify-center gap-2 px-4 py-3 rounded-lg border",
251
+ "bg-background hover:bg-muted transition-colors",
252
+ "text-sm font-medium text-foreground",
253
+ isLoading && "opacity-50 cursor-not-allowed"
254
+ )}
255
+ onClick={async () => {
256
+ if (provider.id === "google" && onGoogleLogin) {
257
+ await onGoogleLogin();
258
+ } else if (provider.id === "apple" && onAppleLogin) {
259
+ await onAppleLogin();
260
+ }
261
+ }}
262
+ disabled={isLoading}
263
+ >
264
+ {provider.icon === "google" && (
265
+ <svg width="18" height="18" viewBox="0 0 24 24">
266
+ <path d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92a5.06 5.06 0 01-2.2 3.32v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.1z" fill="#4285F4"/>
267
+ <path d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" fill="#34A853"/>
268
+ <path d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" fill="#FBBC05"/>
269
+ <path d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" fill="#EA4335"/>
270
+ </svg>
271
+ )}
272
+ {provider.icon === "apple" && (
273
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" className="text-foreground">
274
+ <path d="M17.05 20.28c-.98.95-2.05.8-3.08.35-1.09-.46-2.09-.48-3.24 0-1.44.62-2.2.44-3.06-.35C2.79 15.25 3.51 7.59 9.05 7.31c1.35.07 2.29.74 3.08.8 1.18-.24 2.31-.93 3.57-.84 1.51.12 2.65.72 3.4 1.8.3-3.12 1.38-5.98 0-8.1-3.12-1.87-3.12-3.28-8.1.3-8.1.8-1.06 0-2.29.44-3.06.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"/>
275
+ </svg>
276
+ )}
277
+ <span>{provider.name}</span>
278
+ </button>
279
+ ))}
280
+ </div>
281
+ </>
282
+ )}
224
283
  </form>
225
284
  );
226
285
  };
@@ -18,8 +18,11 @@ export const RegisterForm = ({
18
18
  showTerms = true,
19
19
  showLoginLink = true,
20
20
  requirePasswordConfirm = true,
21
+ showSocialLogin,
21
22
  onRegisterSuccess,
22
23
  onRegisterError,
24
+ onGoogleLogin,
25
+ onAppleLogin,
23
26
  }: RegisterFormProps) => {
24
27
  const navigate = useNavigate();
25
28
  const [name, setName] = useState(defaultData.name || "");
@@ -289,6 +292,62 @@ export const RegisterForm = ({
289
292
  </button>
290
293
  </p>
291
294
  )}
295
+
296
+ {/* Social Login */}
297
+ {showSocialLogin && config.showSocialLogin && config.socialProviders && config.socialProviders.length > 0 && (
298
+ <>
299
+ {/* Divider */}
300
+ <div className="relative">
301
+ <div className="absolute inset-0 flex items-center">
302
+ <div className="w-full border-t border-border" />
303
+ </div>
304
+ <div className="relative flex justify-center text-sm">
305
+ <span className="px-2 bg-background text-muted-foreground">
306
+ Or continue with
307
+ </span>
308
+ </div>
309
+ </div>
310
+
311
+ {/* Social Buttons */}
312
+ <div className="grid grid-cols-2 gap-3">
313
+ {config.socialProviders.map((provider) => (
314
+ <button
315
+ key={provider.id}
316
+ type="button"
317
+ className={cn(
318
+ "flex items-center justify-center gap-2 px-4 py-3 rounded-lg border",
319
+ "bg-background hover:bg-muted transition-colors",
320
+ "text-sm font-medium text-foreground",
321
+ isLoading && "opacity-50 cursor-not-allowed"
322
+ )}
323
+ onClick={async () => {
324
+ if (provider.id === "google" && onGoogleLogin) {
325
+ await onGoogleLogin();
326
+ } else if (provider.id === "apple" && onAppleLogin) {
327
+ await onAppleLogin();
328
+ }
329
+ }}
330
+ disabled={isLoading}
331
+ >
332
+ {provider.icon === "google" && (
333
+ <svg width="18" height="18" viewBox="0 0 24 24">
334
+ <path d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92a5.06 5.06 0 01-2.2 3.32v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.1z" fill="#4285F4"/>
335
+ <path d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" fill="#34A853"/>
336
+ <path d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" fill="#FBBC05"/>
337
+ <path d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" fill="#EA4335"/>
338
+ </svg>
339
+ )}
340
+ {provider.icon === "apple" && (
341
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" className="text-foreground">
342
+ <path d="M17.05 20.28c-.98.95-2.05.8-3.08.35-1.09-.46-2.09-.48-3.24 0-1.44.62-2.2.44-3.06-.35C2.79 15.25 3.51 7.59 9.05 7.31c1.35.07 2.29.74 3.08.8 1.18-.24 2.31-.93 3.57-.84 1.51.12 2.65.72 3.4 1.8.3-3.12 1.38-5.98 0-8.1-3.12-1.87-3.12-3.28-8.1.3-8.1.8-1.06 0-2.29.44-3.06.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"/>
343
+ </svg>
344
+ )}
345
+ <span>{provider.name}</span>
346
+ </button>
347
+ ))}
348
+ </div>
349
+ </>
350
+ )}
292
351
  </form>
293
352
  );
294
353
  };
@@ -210,10 +210,16 @@ export interface LoginFormProps {
210
210
  showForgotPassword?: boolean;
211
211
  /** Show register link */
212
212
  showRegisterLink?: boolean;
213
+ /** Show social login buttons */
214
+ showSocialLogin?: boolean;
213
215
  /** On successful login */
214
216
  onLoginSuccess?: (user: User) => void | Promise<void>;
215
217
  /** On login error */
216
218
  onLoginError?: (error: string) => void;
219
+ /** Google login handler */
220
+ onGoogleLogin?: () => void | Promise<void>;
221
+ /** Apple login handler */
222
+ onAppleLogin?: () => void | Promise<void>;
217
223
  }
218
224
 
219
225
  /**
@@ -230,10 +236,16 @@ export interface RegisterFormProps {
230
236
  showLoginLink?: boolean;
231
237
  /** Require password confirmation */
232
238
  requirePasswordConfirm?: boolean;
239
+ /** Show social login buttons */
240
+ showSocialLogin?: boolean;
233
241
  /** On successful registration */
234
242
  onRegisterSuccess?: (user: User) => void | Promise<void>;
235
243
  /** On registration error */
236
244
  onRegisterError?: (error: string) => void;
245
+ /** Google login handler */
246
+ onGoogleLogin?: () => void | Promise<void>;
247
+ /** Apple login handler */
248
+ onAppleLogin?: () => void | Promise<void>;
237
249
  }
238
250
 
239
251
  /**
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Billing Layout Component
3
+ *
4
+ * Layout wrapper for billing pages
5
+ */
6
+
7
+ import { BrandLogo } from "../../layouts/components";
8
+ import type { BillingLayoutProps } from "../types/billing";
9
+
10
+ export const BillingLayout = ({ config, children }: BillingLayoutProps) => {
11
+ return (
12
+ <div className="min-h-screen bg-background flex flex-col">
13
+ {/* Header */}
14
+ <header className="border-b border-border px-6 py-4 flex items-center justify-between">
15
+ <div className="flex items-center gap-2">
16
+ <BrandLogo size={28} />
17
+ <span className="font-bold text-xl text-foreground">{config.brandName}</span>
18
+ </div>
19
+ <a
20
+ href="/dashboard"
21
+ className="text-sm text-muted-foreground hover:text-foreground transition-colors"
22
+ >
23
+ Back to Dashboard
24
+ </a>
25
+ </header>
26
+
27
+ {/* Main Content */}
28
+ <main className="flex-1 container max-w-6xl mx-auto px-4 py-12">
29
+ {/* Page Title */}
30
+ <div className="mb-8">
31
+ <h1 className="text-3xl font-bold text-foreground mb-2">
32
+ Billing & Subscription
33
+ </h1>
34
+ <p className="text-muted-foreground">
35
+ Manage your subscription, payment methods, and invoices
36
+ </p>
37
+ </div>
38
+
39
+ {/* Children Content */}
40
+ {children}
41
+ </main>
42
+
43
+ {/* Support Link */}
44
+ {config.supportEmail && (
45
+ <footer className="border-t border-border px-6 py-4">
46
+ <div className="container max-w-6xl mx-auto text-center">
47
+ <p className="text-sm text-muted-foreground">
48
+ Need help? Contact{" "}
49
+ <a
50
+ href={`mailto:${config.supportEmail}`}
51
+ className="text-primary hover:underline"
52
+ >
53
+ {config.supportEmail}
54
+ </a>
55
+ </p>
56
+ </div>
57
+ </footer>
58
+ )}
59
+ </div>
60
+ );
61
+ };
62
+
63
+ export default BillingLayout;