@umituz/web-dashboard 3.2.4 → 4.1.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/README.md +228 -14
- package/package.json +23 -30
- package/src/domains/analytics/components/AnalyticsCard.tsx +43 -42
- package/src/domains/analytics/components/AnalyticsChart.tsx +345 -149
- package/src/domains/analytics/components/AnalyticsLayout.tsx +93 -112
- package/src/domains/analytics/components/MetricCard.tsx +29 -87
- package/src/domains/analytics/hooks/index.ts +7 -1
- package/src/domains/analytics/hooks/useAnalytics.ts +144 -139
- package/src/domains/analytics/index.ts +11 -0
- package/src/domains/analytics/services/AnalyticsEngineService.ts +103 -55
- package/src/domains/analytics/services/PerformanceService.ts +117 -69
- package/src/domains/analytics/types/analytics.ts +20 -6
- package/src/domains/analytics/types/index.ts +3 -0
- package/src/domains/analytics/utils/analytics.test.ts +183 -0
- package/src/domains/analytics/utils/analytics.ts +26 -9
- package/src/domains/analytics/utils/i18nKeys.ts +40 -0
- package/src/domains/analytics/utils/index.ts +2 -0
- package/src/domains/auth/components/AuthLayout.tsx +63 -54
- package/src/domains/auth/components/ForgotPasswordForm.tsx +96 -98
- package/src/domains/auth/components/LoginForm.tsx +167 -226
- package/src/domains/auth/components/RegisterForm.tsx +202 -272
- package/src/domains/auth/components/ResetPasswordForm.tsx +105 -151
- package/src/domains/auth/components/shared/AuthErrorBanner.tsx +35 -0
- package/src/domains/auth/components/shared/EmailInput.tsx +91 -0
- package/src/domains/auth/components/shared/PasswordInput.tsx +119 -0
- package/src/domains/auth/components/shared/PasswordStrengthIndicator.tsx +75 -0
- package/src/domains/auth/components/shared/SocialLoginButtons.tsx +58 -0
- package/src/domains/auth/components/shared/SocialProviderIcons.tsx +48 -0
- package/src/domains/auth/components/shared/index.ts +20 -0
- package/src/domains/auth/hooks/index.ts +1 -0
- package/src/domains/auth/hooks/useAuth.ts +151 -232
- package/src/domains/auth/index.ts +8 -4
- package/src/domains/auth/types/auth.ts +26 -5
- package/src/domains/auth/types/index.ts +1 -0
- package/src/domains/auth/utils/auth.test.ts +182 -0
- package/src/domains/auth/utils/auth.ts +71 -130
- package/src/domains/auth/utils/i18nKeys.ts +127 -0
- package/src/domains/auth/utils/index.ts +7 -1
- package/src/domains/auth/utils/secureToken.ts +33 -0
- package/src/domains/auth/utils/validationKeys.ts +23 -0
- package/src/domains/calendar/hooks/useCalendar.ts +145 -151
- package/src/domains/calendar/index.ts +0 -1
- package/src/domains/calendar/services/CalendarService.ts +122 -179
- package/src/domains/calendar/utils/contentItemMapper.test.ts +108 -0
- package/src/domains/calendar/utils/contentItemMapper.ts +99 -0
- package/src/domains/calendar/utils/index.ts +33 -30
- package/src/domains/layouts/components/BrandLogo.tsx +12 -9
- package/src/domains/layouts/components/DashboardHeader.tsx +78 -80
- package/src/domains/layouts/components/DashboardLayout.tsx +45 -21
- package/src/domains/layouts/components/DashboardSidebar.tsx +48 -51
- package/src/domains/layouts/types/layout.ts +7 -1
- package/src/domains/layouts/types/user.ts +3 -1
- package/src/domains/modules/build.test.tsx +97 -0
- package/src/domains/modules/build.tsx +71 -0
- package/src/domains/modules/index.ts +2 -0
- package/src/domains/modules/types.ts +29 -0
- package/src/domains/onboarding/components/AppFocusStep.tsx +99 -68
- package/src/domains/onboarding/components/OnboardingWizard.tsx +109 -183
- package/src/domains/onboarding/components/PlanStep.tsx +156 -123
- package/src/domains/onboarding/components/PlatformsStep.tsx +74 -54
- package/src/domains/onboarding/components/StepNavigation.tsx +108 -0
- package/src/domains/onboarding/components/StepProgress.tsx +62 -0
- package/src/domains/onboarding/components/UserTypeStep.tsx +59 -80
- package/src/domains/onboarding/components/index.ts +4 -0
- package/src/domains/onboarding/hooks/useOnboarding.ts +76 -66
- package/src/domains/onboarding/index.ts +1 -0
- package/src/domains/onboarding/types/onboarding.ts +57 -17
- package/src/domains/onboarding/utils/i18nKeys.ts +72 -0
- package/src/domains/onboarding/utils/index.ts +2 -0
- package/src/domains/permissions/PermissionsContext.tsx +103 -0
- package/src/domains/permissions/hasPermission.test.ts +50 -0
- package/src/domains/permissions/hasPermission.ts +38 -0
- package/src/domains/permissions/index.ts +19 -0
- package/src/domains/permissions/types.ts +35 -0
- package/src/domains/permissions/utils/i18nKeys.ts +11 -0
- package/src/domains/session/DemoLogin.tsx +50 -0
- package/src/domains/session/OrgSwitcher.tsx +110 -0
- package/src/domains/session/SessionContext.tsx +150 -0
- package/src/domains/session/inMemoryAuthAdapter.ts +53 -0
- package/src/domains/session/index.ts +18 -0
- package/src/domains/session/types.ts +47 -0
- package/src/domains/session/utils/i18nKeys.ts +12 -0
- package/src/domains/settings/components/AuditLogList.tsx +59 -0
- package/src/domains/settings/components/PermissionMatrixScreen.tsx +80 -0
- package/src/domains/settings/components/SettingsLayout.tsx +75 -60
- package/src/domains/settings/components/SettingsSection.tsx +35 -22
- package/src/domains/settings/components/UserManagementTable.tsx +69 -0
- package/src/domains/settings/components/index.ts +10 -0
- package/src/domains/settings/hooks/useSettings.ts +77 -52
- package/src/domains/settings/types/settings.ts +2 -0
- package/src/domains/settings/utils/i18nKeys.ts +18 -0
- package/src/domains/settings/utils/index.ts +1 -0
- package/src/domains/settings/utils/settings.ts +11 -4
- package/src/i18n/I18nContext.tsx +33 -0
- package/src/i18n/defaultT.test.ts +43 -0
- package/src/i18n/defaultT.ts +34 -0
- package/src/i18n/index.ts +7 -0
- package/src/i18n/types.ts +13 -0
- package/src/index.ts +1 -7
- package/src/primitives/Button.tsx +24 -0
- package/src/primitives/Checkbox.tsx +17 -0
- package/src/primitives/Input.tsx +23 -0
- package/src/primitives/Skeleton.tsx +6 -0
- package/src/primitives/cn.ts +10 -0
- package/src/primitives/index.ts +23 -0
- package/src/styles/dashboard.css +2731 -0
- package/src/styles/tokens.contract.test.ts +91 -0
- package/src/styles/tokens.css +116 -0
- package/src/domains/billing/components/BillingLayout.tsx +0 -63
- package/src/domains/billing/components/BillingPage.tsx +0 -49
- package/src/domains/billing/components/BillingPortal.tsx +0 -198
- package/src/domains/billing/components/InvoiceCard.tsx +0 -143
- package/src/domains/billing/components/PaymentMethodsList.tsx +0 -116
- package/src/domains/billing/components/PlanComparison.tsx +0 -199
- package/src/domains/billing/components/UsageCard.tsx +0 -103
- package/src/domains/billing/components/index.ts +0 -13
- package/src/domains/billing/constants/billing.ts +0 -69
- package/src/domains/billing/hooks/index.ts +0 -7
- package/src/domains/billing/hooks/useBilling.ts +0 -385
- package/src/domains/billing/index.ts +0 -73
- package/src/domains/billing/types/billing.ts +0 -342
- package/src/domains/billing/types/index.ts +0 -30
- package/src/domains/billing/utils/billing.ts +0 -360
- package/src/domains/billing/utils/index.ts +0 -31
- package/src/domains/billing/utils/polar.ts +0 -63
- package/src/domains/calendar/components/index.ts +0 -7
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { describe, expect, it } from "vitest";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Stylesheet-token contract:
|
|
7
|
+
* - every `--u-*` custom property referenced by dashboard.css must be
|
|
8
|
+
* defined in tokens.css (catches token typos and dead references),
|
|
9
|
+
* - the core token families (color/radius/space/font/shadow/layout)
|
|
10
|
+
* must all be present in tokens.css.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const read = (file: string): string =>
|
|
14
|
+
readFileSync(join(__dirname, file), "utf8");
|
|
15
|
+
|
|
16
|
+
const tokensCss = read("tokens.css");
|
|
17
|
+
const dashboardCss = read("dashboard.css");
|
|
18
|
+
|
|
19
|
+
const DEFINED = new Set([...tokensCss.matchAll(/(--u-[a-z0-9-]+)\s*:/g)].map((m) => m[1]));
|
|
20
|
+
const USED = new Set([...dashboardCss.matchAll(/var\((--u-[a-z0-9-]+)/g)].map((m) => m[1]));
|
|
21
|
+
|
|
22
|
+
describe("dashboard stylesheet token contract", () => {
|
|
23
|
+
it("defines every token the stylesheet references", () => {
|
|
24
|
+
const undefinedTokens = [...USED].filter((token) => !DEFINED.has(token));
|
|
25
|
+
expect(undefinedTokens).toEqual([]);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("references the token system at all", () => {
|
|
29
|
+
expect(USED.size).toBeGreaterThan(40);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("ships the core token families", () => {
|
|
33
|
+
const CORE = [
|
|
34
|
+
"--u-color-bg",
|
|
35
|
+
"--u-color-surface",
|
|
36
|
+
"--u-color-text",
|
|
37
|
+
"--u-color-muted",
|
|
38
|
+
"--u-color-border",
|
|
39
|
+
"--u-color-border-strong",
|
|
40
|
+
"--u-color-primary",
|
|
41
|
+
"--u-color-primary-hover",
|
|
42
|
+
"--u-color-primary-contrast",
|
|
43
|
+
"--u-color-primary-soft",
|
|
44
|
+
"--u-color-danger",
|
|
45
|
+
"--u-color-success",
|
|
46
|
+
"--u-color-warning",
|
|
47
|
+
"--u-color-ring",
|
|
48
|
+
"--u-radius-sm",
|
|
49
|
+
"--u-radius-lg",
|
|
50
|
+
"--u-radius-pill",
|
|
51
|
+
"--u-space-1",
|
|
52
|
+
"--u-space-8",
|
|
53
|
+
"--u-font-sans",
|
|
54
|
+
"--u-font-size-md",
|
|
55
|
+
"--u-font-weight-bold",
|
|
56
|
+
"--u-shadow-sm",
|
|
57
|
+
"--u-sidebar-width",
|
|
58
|
+
"--u-header-height",
|
|
59
|
+
];
|
|
60
|
+
expect(CORE.filter((token) => !DEFINED.has(token))).toEqual([]);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("provides dark-mode overrides", () => {
|
|
64
|
+
expect(tokensCss).toContain('[data-udb-theme="dark"]');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("bridges core tokens to design-system variables with fallbacks", () => {
|
|
68
|
+
const BRIDGED = [
|
|
69
|
+
["--u-color-primary", "var(--color-primary, #2563eb)"],
|
|
70
|
+
["--u-color-bg", "var(--color-background, #f8fafc)"],
|
|
71
|
+
["--u-color-danger", "var(--color-destructive, #dc2626)"],
|
|
72
|
+
["--u-radius-md", "var(--radius-md, 8px)"],
|
|
73
|
+
["--u-shadow-lg", "var(--shadow-lg,"],
|
|
74
|
+
["--u-font-sans", "var(--font-sans,"],
|
|
75
|
+
] as const;
|
|
76
|
+
for (const [token, bridge] of BRIDGED) {
|
|
77
|
+
expect(tokensCss).toContain(`${token}: ${bridge}`);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("derives hover/soft shades from the bridged base color", () => {
|
|
82
|
+
expect(tokensCss).toMatch(/--u-color-primary-hover: var\(--color-primary-hover, color-mix\(/);
|
|
83
|
+
expect(tokensCss).toMatch(/--u-color-primary-soft: var\(--color-primary-soft, color-mix\(/);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("pulls the design-system stylesheet transitively", () => {
|
|
87
|
+
const imports = [...dashboardCss.matchAll(/@import\s+"([^"]+)"/g)].map((m) => m[1]);
|
|
88
|
+
expect(imports[0]).toBe("@umituz/web-design-system/styles.css");
|
|
89
|
+
expect(imports).toContain("./tokens.css");
|
|
90
|
+
});
|
|
91
|
+
});
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @umituz/web-dashboard — design tokens (`--u-*`)
|
|
3
|
+
*
|
|
4
|
+
* Colors, radii, fonts and shadows are bridged to the design system's
|
|
5
|
+
* `--color-*` / `--radius-*` / `--font-*` / `--shadow-*` variables; each
|
|
6
|
+
* bridge keeps its previous value as fallback. Dark mode: add
|
|
7
|
+
* `data-udb-theme="dark"` on any ancestor — the dark block re-sets the
|
|
8
|
+
* design-system variables and every bridge follows automatically.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
:root {
|
|
12
|
+
/* Surfaces & text */
|
|
13
|
+
--u-color-bg: var(--color-background, #f8fafc);
|
|
14
|
+
--u-color-surface: var(--color-card, #ffffff);
|
|
15
|
+
--u-color-surface-muted: var(--color-muted, #f1f5f9);
|
|
16
|
+
--u-color-text: var(--color-foreground, #0f172a);
|
|
17
|
+
--u-color-muted: var(--color-muted-foreground, #64748b);
|
|
18
|
+
--u-color-border: var(--color-border, #e2e8f0);
|
|
19
|
+
--u-color-border-strong: #cbd5e1;
|
|
20
|
+
|
|
21
|
+
/* Brand (hover/soft derive from the base color) */
|
|
22
|
+
--u-color-primary: var(--color-primary, #2563eb);
|
|
23
|
+
--u-color-primary-hover: var(--color-primary-hover, color-mix(in oklab, var(--color-primary, #2563eb) 86%, #000));
|
|
24
|
+
--u-color-primary-contrast: var(--color-primary-foreground, #ffffff);
|
|
25
|
+
--u-color-primary-soft: var(--color-primary-soft, color-mix(in srgb, var(--color-primary, #2563eb) 10%, transparent));
|
|
26
|
+
--u-color-secondary: var(--color-secondary, #8b5cf6);
|
|
27
|
+
|
|
28
|
+
/* Semantic */
|
|
29
|
+
--u-color-danger: var(--color-destructive, #dc2626);
|
|
30
|
+
--u-color-danger-hover: var(--color-destructive-hover, color-mix(in oklab, var(--color-destructive, #dc2626) 85%, #000));
|
|
31
|
+
--u-color-danger-contrast: var(--color-destructive-foreground, #ffffff);
|
|
32
|
+
--u-color-danger-soft: var(--color-destructive-soft, color-mix(in srgb, var(--color-destructive, #dc2626) 10%, transparent));
|
|
33
|
+
--u-color-success: var(--color-success, #16a34a);
|
|
34
|
+
--u-color-success-soft: var(--color-success-soft, color-mix(in srgb, var(--color-success, #16a34a) 12%, transparent));
|
|
35
|
+
--u-color-warning: var(--color-warning, #d97706);
|
|
36
|
+
--u-color-warning-soft: var(--color-warning-soft, color-mix(in srgb, var(--color-warning, #d97706) 15%, transparent));
|
|
37
|
+
|
|
38
|
+
/* Focus */
|
|
39
|
+
--u-color-ring: var(--color-ring, rgba(37, 99, 235, 0.45));
|
|
40
|
+
|
|
41
|
+
/* Radii */
|
|
42
|
+
--u-radius-sm: var(--radius-sm, 6px);
|
|
43
|
+
--u-radius-md: var(--radius-md, 8px);
|
|
44
|
+
--u-radius-lg: var(--radius-lg, 12px);
|
|
45
|
+
--u-radius-xl: var(--radius-xl, 16px);
|
|
46
|
+
--u-radius-pill: 9999px;
|
|
47
|
+
|
|
48
|
+
/* Spacing (4px base) */
|
|
49
|
+
--u-space-1: 4px;
|
|
50
|
+
--u-space-2: 8px;
|
|
51
|
+
--u-space-3: 12px;
|
|
52
|
+
--u-space-4: 16px;
|
|
53
|
+
--u-space-5: 20px;
|
|
54
|
+
--u-space-6: 24px;
|
|
55
|
+
--u-space-7: 32px;
|
|
56
|
+
--u-space-8: 40px;
|
|
57
|
+
|
|
58
|
+
/* Typography */
|
|
59
|
+
--u-font-sans: var(--font-sans, ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto,
|
|
60
|
+
"Helvetica Neue", Arial, "Noto Sans", sans-serif);
|
|
61
|
+
--u-font-size-xs: 11px;
|
|
62
|
+
--u-font-size-sm: 12px;
|
|
63
|
+
--u-font-size-md: 14px;
|
|
64
|
+
--u-font-size-lg: 16px;
|
|
65
|
+
--u-font-size-xl: 20px;
|
|
66
|
+
--u-font-size-2xl: 24px;
|
|
67
|
+
--u-font-size-3xl: 30px;
|
|
68
|
+
--u-font-size-4xl: 36px;
|
|
69
|
+
--u-font-weight-normal: 400;
|
|
70
|
+
--u-font-weight-medium: 500;
|
|
71
|
+
--u-font-weight-bold: 700;
|
|
72
|
+
|
|
73
|
+
/* Layout */
|
|
74
|
+
--u-sidebar-width: 240px;
|
|
75
|
+
--u-sidebar-width-collapsed: 64px;
|
|
76
|
+
--u-header-height: 56px;
|
|
77
|
+
|
|
78
|
+
/* Elevation */
|
|
79
|
+
--u-shadow-sm: var(--shadow-sm, 0 1px 2px rgba(15, 23, 42, 0.06));
|
|
80
|
+
--u-shadow-md: var(--shadow-md, 0 4px 12px rgba(15, 23, 42, 0.08));
|
|
81
|
+
--u-shadow-lg: var(--shadow-lg, 0 12px 32px rgba(15, 23, 42, 0.14));
|
|
82
|
+
|
|
83
|
+
/* Sidebar palette */
|
|
84
|
+
--u-sidebar-bg: var(--u-color-surface);
|
|
85
|
+
--u-sidebar-text: var(--u-color-text);
|
|
86
|
+
--u-sidebar-muted: var(--u-color-muted);
|
|
87
|
+
--u-sidebar-border: var(--u-color-border);
|
|
88
|
+
--u-sidebar-accent: var(--u-color-surface-muted);
|
|
89
|
+
--u-sidebar-accent-text: var(--u-color-text);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
[data-udb-theme="dark"] {
|
|
93
|
+
--color-background: #0b1120;
|
|
94
|
+
--color-card: #111827;
|
|
95
|
+
--color-muted: #1e293b;
|
|
96
|
+
--color-foreground: #f1f5f9;
|
|
97
|
+
--color-muted-foreground: #94a3b8;
|
|
98
|
+
--color-border: #1f2a3c;
|
|
99
|
+
|
|
100
|
+
--color-primary: #3b82f6;
|
|
101
|
+
--color-primary-foreground: #ffffff;
|
|
102
|
+
--color-secondary: #a78bfa;
|
|
103
|
+
|
|
104
|
+
--color-destructive: #ef4444;
|
|
105
|
+
--color-destructive-foreground: #ffffff;
|
|
106
|
+
--color-success: #22c55e;
|
|
107
|
+
--color-warning: #f59e0b;
|
|
108
|
+
|
|
109
|
+
--color-ring: rgba(96, 165, 250, 0.5);
|
|
110
|
+
|
|
111
|
+
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.4);
|
|
112
|
+
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.45);
|
|
113
|
+
--shadow-lg: 0 12px 32px rgba(0, 0, 0, 0.55);
|
|
114
|
+
|
|
115
|
+
--u-color-border-strong: #334155;
|
|
116
|
+
}
|
|
@@ -1,63 +0,0 @@
|
|
|
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;
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* BillingPage Component
|
|
3
|
-
*
|
|
4
|
-
* Complete billing page for displaying subscription, invoices, and plan management
|
|
5
|
-
* Data fetching should be handled by passing billing summary
|
|
6
|
-
*
|
|
7
|
-
* @example
|
|
8
|
-
* ```tsx
|
|
9
|
-
* <BillingPage
|
|
10
|
-
* billing={billingData}
|
|
11
|
-
* loading={false}
|
|
12
|
-
* activeTab="overview"
|
|
13
|
-
* onTabChange={(tab) => setActiveTab(tab)}
|
|
14
|
-
* />
|
|
15
|
-
* ```
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
import { BillingLayout, BillingPortal } from ".";
|
|
19
|
-
import type { BillingPortalProps } from "../types/billing";
|
|
20
|
-
|
|
21
|
-
export interface BillingPageProps extends Omit<BillingPortalProps, "showTabs"> {
|
|
22
|
-
/** Billing configuration */
|
|
23
|
-
config?: {
|
|
24
|
-
brandName?: string;
|
|
25
|
-
supportEmail?: string;
|
|
26
|
-
};
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* BillingPage Component
|
|
31
|
-
*
|
|
32
|
-
* Ready-to-use billing page component
|
|
33
|
-
* Combine BillingLayout with BillingPortal
|
|
34
|
-
*/
|
|
35
|
-
export const BillingPage = ({ config, ...portalProps }: BillingPageProps) => {
|
|
36
|
-
const billingConfig = {
|
|
37
|
-
brandName: config?.brandName || "Growth Factory",
|
|
38
|
-
supportEmail: config?.supportEmail,
|
|
39
|
-
plans: [],
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
return (
|
|
43
|
-
<BillingLayout config={billingConfig}>
|
|
44
|
-
<BillingPortal showTabs={true} {...portalProps} />
|
|
45
|
-
</BillingLayout>
|
|
46
|
-
);
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
export default BillingPage;
|
|
@@ -1,198 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Billing Portal Component
|
|
3
|
-
*
|
|
4
|
-
* Main billing portal with tabs
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { CreditCard, FileText, BarChart3, Settings, Loader2, AlertCircle } from "lucide-react";
|
|
8
|
-
import { cn } from "@umituz/web-design-system/utils";
|
|
9
|
-
import { Button } from "@umituz/web-design-system/atoms";
|
|
10
|
-
import type { BillingPortalProps } from "../types/billing";
|
|
11
|
-
import { UsageCard } from "./UsageCard";
|
|
12
|
-
import { PaymentMethodsList } from "./PaymentMethodsList";
|
|
13
|
-
import { InvoiceCard } from "./InvoiceCard";
|
|
14
|
-
import { getDaysRemaining, getStatusColor, getStatusLabel, formatPrice, getPlanPrice } from "../utils/billing";
|
|
15
|
-
|
|
16
|
-
export const BillingPortal = ({
|
|
17
|
-
billing,
|
|
18
|
-
loading = false,
|
|
19
|
-
error,
|
|
20
|
-
showTabs = true,
|
|
21
|
-
activeTab = "overview",
|
|
22
|
-
onTabChange,
|
|
23
|
-
}: BillingPortalProps) => {
|
|
24
|
-
const tabs = [
|
|
25
|
-
{ id: "overview", label: "Overview", icon: BarChart3 },
|
|
26
|
-
{ id: "payment-methods", label: "Payment Methods", icon: CreditCard },
|
|
27
|
-
{ id: "invoices", label: "Invoices", icon: FileText },
|
|
28
|
-
{ id: "plan", label: "Plan", icon: Settings },
|
|
29
|
-
];
|
|
30
|
-
|
|
31
|
-
if (loading) {
|
|
32
|
-
return (
|
|
33
|
-
<div className="flex items-center justify-center py-24">
|
|
34
|
-
<Loader2 className="h-12 w-12 animate-spin text-muted-foreground" />
|
|
35
|
-
</div>
|
|
36
|
-
);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
if (error) {
|
|
40
|
-
return (
|
|
41
|
-
<div className="flex items-center justify-center py-24 gap-4 text-destructive">
|
|
42
|
-
<AlertCircle className="h-6 w-6" />
|
|
43
|
-
<p>{error}</p>
|
|
44
|
-
</div>
|
|
45
|
-
);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
if (!billing) return null;
|
|
49
|
-
|
|
50
|
-
const renderContent = () => {
|
|
51
|
-
switch (activeTab) {
|
|
52
|
-
case "overview":
|
|
53
|
-
return (
|
|
54
|
-
<div className="space-y-6">
|
|
55
|
-
{/* Current Subscription */}
|
|
56
|
-
<div className="p-6 rounded-xl border border-border bg-background">
|
|
57
|
-
<h3 className="text-lg font-semibold text-foreground mb-4">
|
|
58
|
-
Current Subscription
|
|
59
|
-
</h3>
|
|
60
|
-
<div className="flex items-start justify-between">
|
|
61
|
-
<div>
|
|
62
|
-
<p className="text-2xl font-bold text-foreground">
|
|
63
|
-
{billing.subscription.plan.name}
|
|
64
|
-
</p>
|
|
65
|
-
<p className={cn("text-sm font-medium", getStatusColor(billing.subscription.status))}>
|
|
66
|
-
{getStatusLabel(billing.subscription.status)}
|
|
67
|
-
</p>
|
|
68
|
-
<p className="text-sm text-muted-foreground mt-1">
|
|
69
|
-
{formatPrice(
|
|
70
|
-
getPlanPrice(billing.subscription.plan, billing.subscription.cycle),
|
|
71
|
-
billing.subscription.plan.currency
|
|
72
|
-
)}
|
|
73
|
-
/{billing.subscription.cycle}
|
|
74
|
-
</p>
|
|
75
|
-
</div>
|
|
76
|
-
<div className="text-right">
|
|
77
|
-
{billing.subscription.status === "trialing" && billing.subscription.trialEnd && (
|
|
78
|
-
<p className="text-sm text-muted-foreground">
|
|
79
|
-
{getDaysRemaining(billing.subscription.trialEnd)} days left in trial
|
|
80
|
-
</p>
|
|
81
|
-
)}
|
|
82
|
-
{billing.upcomingInvoice && (
|
|
83
|
-
<p className="text-sm text-muted-foreground">
|
|
84
|
-
Next billing:{" "}
|
|
85
|
-
{new Date(billing.upcomingInvoice.date).toLocaleDateString()}
|
|
86
|
-
</p>
|
|
87
|
-
)}
|
|
88
|
-
</div>
|
|
89
|
-
</div>
|
|
90
|
-
</div>
|
|
91
|
-
|
|
92
|
-
{/* Usage Metrics */}
|
|
93
|
-
{billing.usage.length > 0 && (
|
|
94
|
-
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
95
|
-
{billing.usage.map((metric) => (
|
|
96
|
-
<UsageCard key={metric.id} metric={metric} />
|
|
97
|
-
))}
|
|
98
|
-
</div>
|
|
99
|
-
)}
|
|
100
|
-
|
|
101
|
-
{/* Upcoming Invoice */}
|
|
102
|
-
{billing.upcomingInvoice && (
|
|
103
|
-
<div className="p-6 rounded-xl border border-border bg-background">
|
|
104
|
-
<h3 className="text-lg font-semibold text-foreground mb-2">
|
|
105
|
-
Upcoming Invoice
|
|
106
|
-
</h3>
|
|
107
|
-
<div className="flex items-center justify-between">
|
|
108
|
-
<div>
|
|
109
|
-
<p className="text-3xl font-bold text-foreground">
|
|
110
|
-
{formatPrice(billing.upcomingInvoice.amount, billing.upcomingInvoice.currency)}
|
|
111
|
-
</p>
|
|
112
|
-
<p className="text-sm text-muted-foreground">
|
|
113
|
-
Due {new Date(billing.upcomingInvoice.date).toLocaleDateString()}
|
|
114
|
-
</p>
|
|
115
|
-
</div>
|
|
116
|
-
</div>
|
|
117
|
-
</div>
|
|
118
|
-
)}
|
|
119
|
-
</div>
|
|
120
|
-
);
|
|
121
|
-
|
|
122
|
-
case "payment-methods":
|
|
123
|
-
return (
|
|
124
|
-
<PaymentMethodsList
|
|
125
|
-
paymentMethods={billing.paymentMethods}
|
|
126
|
-
onAddNew={() => console.log("Add payment method")}
|
|
127
|
-
onSetDefault={(id) => console.log("Set default:", id)}
|
|
128
|
-
onRemove={(id) => console.log("Remove:", id)}
|
|
129
|
-
/>
|
|
130
|
-
);
|
|
131
|
-
|
|
132
|
-
case "invoices":
|
|
133
|
-
return (
|
|
134
|
-
<div className="space-y-4">
|
|
135
|
-
{billing.recentInvoices.length > 0 ? (
|
|
136
|
-
billing.recentInvoices.map((invoice) => (
|
|
137
|
-
<InvoiceCard
|
|
138
|
-
key={invoice.id}
|
|
139
|
-
invoice={invoice}
|
|
140
|
-
onClick={(inv) => console.log("View invoice:", inv)}
|
|
141
|
-
/>
|
|
142
|
-
))
|
|
143
|
-
) : (
|
|
144
|
-
<div className="text-center py-12 text-muted-foreground">
|
|
145
|
-
<FileText className="h-12 w-12 mx-auto mb-4 opacity-50" />
|
|
146
|
-
<p>No invoices yet</p>
|
|
147
|
-
</div>
|
|
148
|
-
)}
|
|
149
|
-
</div>
|
|
150
|
-
);
|
|
151
|
-
|
|
152
|
-
case "plan":
|
|
153
|
-
return (
|
|
154
|
-
<div className="text-center py-12">
|
|
155
|
-
<p className="text-muted-foreground">Plan management coming soon...</p>
|
|
156
|
-
</div>
|
|
157
|
-
);
|
|
158
|
-
|
|
159
|
-
default:
|
|
160
|
-
return null;
|
|
161
|
-
}
|
|
162
|
-
};
|
|
163
|
-
|
|
164
|
-
return (
|
|
165
|
-
<div className="w-full">
|
|
166
|
-
{/* Tabs */}
|
|
167
|
-
{showTabs && (
|
|
168
|
-
<div className="border-b border-border mb-6">
|
|
169
|
-
<div className="flex gap-6">
|
|
170
|
-
{tabs.map((tab) => {
|
|
171
|
-
const Icon = tab.icon;
|
|
172
|
-
return (
|
|
173
|
-
<button
|
|
174
|
-
key={tab.id}
|
|
175
|
-
onClick={() => onTabChange?.(tab.id)}
|
|
176
|
-
className={cn(
|
|
177
|
-
"flex items-center gap-2 pb-4 border-b-2 transition-colors",
|
|
178
|
-
activeTab === tab.id
|
|
179
|
-
? "border-primary text-foreground"
|
|
180
|
-
: "border-transparent text-muted-foreground hover:text-foreground"
|
|
181
|
-
)}
|
|
182
|
-
>
|
|
183
|
-
<Icon className="h-4 w-4" />
|
|
184
|
-
<span className="font-medium">{tab.label}</span>
|
|
185
|
-
</button>
|
|
186
|
-
);
|
|
187
|
-
})}
|
|
188
|
-
</div>
|
|
189
|
-
</div>
|
|
190
|
-
)}
|
|
191
|
-
|
|
192
|
-
{/* Content */}
|
|
193
|
-
{renderContent()}
|
|
194
|
-
</div>
|
|
195
|
-
);
|
|
196
|
-
};
|
|
197
|
-
|
|
198
|
-
export default BillingPortal;
|
|
@@ -1,143 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Invoice Card Component
|
|
3
|
-
*
|
|
4
|
-
* Display invoice information
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { FileText, Download, ExternalLink } from "lucide-react";
|
|
8
|
-
import { cn } from "@umituz/web-design-system/utils";
|
|
9
|
-
import { Button } from "@umituz/web-design-system/atoms";
|
|
10
|
-
import type { InvoiceCardProps } from "../types/billing";
|
|
11
|
-
import { formatPrice, getInvoiceStatusColor, getInvoiceStatusLabel } from "../utils/billing";
|
|
12
|
-
|
|
13
|
-
export const InvoiceCard = ({
|
|
14
|
-
invoice,
|
|
15
|
-
compact = false,
|
|
16
|
-
onClick,
|
|
17
|
-
}: InvoiceCardProps) => {
|
|
18
|
-
const handleClick = () => {
|
|
19
|
-
onClick?.(invoice);
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
if (compact) {
|
|
23
|
-
return (
|
|
24
|
-
<div
|
|
25
|
-
onClick={handleClick}
|
|
26
|
-
className="flex items-center justify-between p-4 rounded-lg border border-border hover:border-primary/50 bg-background cursor-pointer transition-colors"
|
|
27
|
-
>
|
|
28
|
-
<div className="flex items-center gap-3">
|
|
29
|
-
<div className="w-10 h-10 rounded-lg bg-muted flex items-center justify-center">
|
|
30
|
-
<FileText className="h-5 w-5 text-muted-foreground" />
|
|
31
|
-
</div>
|
|
32
|
-
<div>
|
|
33
|
-
<p className="font-medium text-foreground text-sm">
|
|
34
|
-
{invoice.number}
|
|
35
|
-
</p>
|
|
36
|
-
<p className="text-xs text-muted-foreground">
|
|
37
|
-
{new Date(invoice.date).toLocaleDateString()}
|
|
38
|
-
</p>
|
|
39
|
-
</div>
|
|
40
|
-
</div>
|
|
41
|
-
|
|
42
|
-
<div className="text-right">
|
|
43
|
-
<p className="font-bold text-foreground">
|
|
44
|
-
{formatPrice(invoice.amount, invoice.currency)}
|
|
45
|
-
</p>
|
|
46
|
-
<p className={cn("text-xs font-medium", getInvoiceStatusColor(invoice.status))}>
|
|
47
|
-
{getInvoiceStatusLabel(invoice.status)}
|
|
48
|
-
</p>
|
|
49
|
-
</div>
|
|
50
|
-
</div>
|
|
51
|
-
);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
return (
|
|
55
|
-
<div
|
|
56
|
-
onClick={handleClick}
|
|
57
|
-
className="p-6 rounded-xl border border-border hover:border-primary/50 bg-background cursor-pointer transition-colors"
|
|
58
|
-
>
|
|
59
|
-
{/* Header */}
|
|
60
|
-
<div className="flex items-start justify-between mb-4">
|
|
61
|
-
<div className="flex items-center gap-3">
|
|
62
|
-
<div className="w-12 h-12 rounded-lg bg-muted flex items-center justify-center">
|
|
63
|
-
<FileText className="h-6 w-6 text-muted-foreground" />
|
|
64
|
-
</div>
|
|
65
|
-
<div>
|
|
66
|
-
<p className="font-bold text-foreground">{invoice.number}</p>
|
|
67
|
-
<p className="text-sm text-muted-foreground">
|
|
68
|
-
{new Date(invoice.date).toLocaleDateString(undefined, {
|
|
69
|
-
year: "numeric",
|
|
70
|
-
month: "long",
|
|
71
|
-
day: "numeric",
|
|
72
|
-
})}
|
|
73
|
-
</p>
|
|
74
|
-
</div>
|
|
75
|
-
</div>
|
|
76
|
-
|
|
77
|
-
<div className="text-right">
|
|
78
|
-
<p className="text-2xl font-bold text-foreground">
|
|
79
|
-
{formatPrice(invoice.amount, invoice.currency)}
|
|
80
|
-
</p>
|
|
81
|
-
<p className={cn("text-sm font-medium", getInvoiceStatusColor(invoice.status))}>
|
|
82
|
-
{getInvoiceStatusLabel(invoice.status)}
|
|
83
|
-
</p>
|
|
84
|
-
</div>
|
|
85
|
-
</div>
|
|
86
|
-
|
|
87
|
-
{/* Items Preview */}
|
|
88
|
-
{invoice.items && invoice.items.length > 0 && (
|
|
89
|
-
<div className="space-y-2 mb-4">
|
|
90
|
-
{invoice.items.slice(0, 2).map((item, index) => (
|
|
91
|
-
<div key={index} className="flex justify-between text-sm">
|
|
92
|
-
<span className="text-muted-foreground">{item.description}</span>
|
|
93
|
-
<span className="text-foreground">
|
|
94
|
-
{formatPrice(item.amount, invoice.currency)}
|
|
95
|
-
</span>
|
|
96
|
-
</div>
|
|
97
|
-
))}
|
|
98
|
-
{invoice.items.length > 2 && (
|
|
99
|
-
<p className="text-xs text-muted-foreground">
|
|
100
|
-
+{invoice.items.length - 2} more items
|
|
101
|
-
</p>
|
|
102
|
-
)}
|
|
103
|
-
</div>
|
|
104
|
-
)}
|
|
105
|
-
|
|
106
|
-
{/* Actions */}
|
|
107
|
-
<div className="flex items-center justify-between pt-4 border-t border-border">
|
|
108
|
-
<p className="text-xs text-muted-foreground">
|
|
109
|
-
Due {new Date(invoice.dueDate).toLocaleDateString()}
|
|
110
|
-
</p>
|
|
111
|
-
|
|
112
|
-
<div className="flex items-center gap-2">
|
|
113
|
-
{invoice.invoiceUrl && (
|
|
114
|
-
<Button
|
|
115
|
-
variant="ghost"
|
|
116
|
-
size="sm"
|
|
117
|
-
onClick={(e) => {
|
|
118
|
-
e.stopPropagation();
|
|
119
|
-
window.open(invoice.invoiceUrl, "_blank");
|
|
120
|
-
}}
|
|
121
|
-
>
|
|
122
|
-
<ExternalLink className="h-4 w-4" />
|
|
123
|
-
</Button>
|
|
124
|
-
)}
|
|
125
|
-
{invoice.pdfUrl && (
|
|
126
|
-
<Button
|
|
127
|
-
variant="ghost"
|
|
128
|
-
size="sm"
|
|
129
|
-
onClick={(e) => {
|
|
130
|
-
e.stopPropagation();
|
|
131
|
-
window.open(invoice.pdfUrl, "_blank");
|
|
132
|
-
}}
|
|
133
|
-
>
|
|
134
|
-
<Download className="h-4 w-4" />
|
|
135
|
-
</Button>
|
|
136
|
-
)}
|
|
137
|
-
</div>
|
|
138
|
-
</div>
|
|
139
|
-
</div>
|
|
140
|
-
);
|
|
141
|
-
};
|
|
142
|
-
|
|
143
|
-
export default InvoiceCard;
|