@umituz/web-dashboard 3.1.6 → 3.1.8
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 +1 -1
- package/src/domains/analytics/components/AnalyticsChart.tsx +1 -1
- package/src/domains/analytics/hooks/useAnalytics.ts +5 -101
- package/src/domains/analytics/index.ts +0 -20
- package/src/domains/analytics/utils/analytics.ts +3 -4
- package/src/domains/auth/components/LoginForm.tsx +18 -11
- package/src/domains/auth/components/RegisterForm.tsx +18 -11
- package/src/domains/auth/types/auth.ts +9 -7
- package/src/domains/auth/utils/auth.ts +1 -1
- package/src/domains/billing/components/BillingPortal.tsx +1 -1
- package/src/domains/billing/hooks/useBilling.ts +2 -10
- package/src/domains/billing/utils/billing.ts +0 -25
- package/src/domains/layouts/components/BrandLogo.tsx +1 -0
- package/src/domains/layouts/components/DashboardLayout.tsx +3 -15
- package/src/domains/layouts/components/DashboardSidebar.tsx +19 -19
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/web-dashboard",
|
|
3
|
-
"version": "3.1.
|
|
3
|
+
"version": "3.1.8",
|
|
4
4
|
"description": "Dashboard Layout System - Comprehensive analytics, calendar, customizable layouts, and config-based architecture",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"types": "./src/index.ts",
|
|
@@ -145,7 +145,7 @@ export const AnalyticsChart = ({ config, className, height }: AnalyticsChartProp
|
|
|
145
145
|
innerRadius={config.type === "donut" ? 40 : 0}
|
|
146
146
|
dataKey="value"
|
|
147
147
|
>
|
|
148
|
-
{config.data.map((entry, index: number) => (
|
|
148
|
+
{config.data.map((entry: any, index: number) => (
|
|
149
149
|
<Cell key={`cell-${index}`} fill={colors[index % colors.length]} />
|
|
150
150
|
))}
|
|
151
151
|
</Pie>
|
|
@@ -2,10 +2,9 @@
|
|
|
2
2
|
* useAnalytics Hook
|
|
3
3
|
*
|
|
4
4
|
* Core analytics hook for fetching and managing analytics data
|
|
5
|
-
* Enhanced with config support and advanced analytics services
|
|
6
5
|
*/
|
|
7
6
|
|
|
8
|
-
import { useState, useCallback, useEffect
|
|
7
|
+
import { useState, useCallback, useEffect } from "react";
|
|
9
8
|
import type {
|
|
10
9
|
KPIs,
|
|
11
10
|
TimeSeriesData,
|
|
@@ -14,8 +13,6 @@ import type {
|
|
|
14
13
|
AnalyticsExportOptions,
|
|
15
14
|
} from "../types/analytics";
|
|
16
15
|
import { createKPI } from "../utils/analytics";
|
|
17
|
-
import { analyticsEngineService, performanceService } from "../services";
|
|
18
|
-
import type { DashboardConfig } from "../../../domain/config";
|
|
19
16
|
|
|
20
17
|
interface UseAnalyticsOptions {
|
|
21
18
|
/** Analytics API base URL */
|
|
@@ -24,10 +21,6 @@ interface UseAnalyticsOptions {
|
|
|
24
21
|
initialDateRange?: DateRangeValue;
|
|
25
22
|
/** Auto-refresh interval in ms (0 to disable) */
|
|
26
23
|
refreshInterval?: number;
|
|
27
|
-
/** Dashboard configuration */
|
|
28
|
-
config?: DashboardConfig;
|
|
29
|
-
/** Error callback */
|
|
30
|
-
onError?: (error: Error) => void;
|
|
31
24
|
}
|
|
32
25
|
|
|
33
26
|
interface AnalyticsData {
|
|
@@ -50,19 +43,7 @@ interface AnalyticsData {
|
|
|
50
43
|
* @returns Analytics data and actions
|
|
51
44
|
*/
|
|
52
45
|
export function useAnalytics(options: UseAnalyticsOptions = {}) {
|
|
53
|
-
const {
|
|
54
|
-
apiUrl = "/api/analytics",
|
|
55
|
-
initialDateRange,
|
|
56
|
-
refreshInterval = 0,
|
|
57
|
-
config,
|
|
58
|
-
onError,
|
|
59
|
-
} = options;
|
|
60
|
-
|
|
61
|
-
// Apply config defaults
|
|
62
|
-
const effectiveRefreshInterval = useMemo(
|
|
63
|
-
() => refreshInterval || config?.data?.refreshInterval || 0,
|
|
64
|
-
[refreshInterval, config]
|
|
65
|
-
);
|
|
46
|
+
const { apiUrl = "/api/analytics", initialDateRange, refreshInterval = 0 } = options;
|
|
66
47
|
|
|
67
48
|
// State
|
|
68
49
|
const [dateRange, setDateRange] = useState<DateRangeValue>(
|
|
@@ -181,81 +162,11 @@ export function useAnalytics(options: UseAnalyticsOptions = {}) {
|
|
|
181
162
|
|
|
182
163
|
// Auto-refresh
|
|
183
164
|
useEffect(() => {
|
|
184
|
-
if (
|
|
185
|
-
const interval = setInterval(fetchAnalytics,
|
|
165
|
+
if (refreshInterval > 0) {
|
|
166
|
+
const interval = setInterval(fetchAnalytics, refreshInterval);
|
|
186
167
|
return () => clearInterval(interval);
|
|
187
168
|
}
|
|
188
|
-
}, [fetchAnalytics,
|
|
189
|
-
|
|
190
|
-
// Advanced analytics methods
|
|
191
|
-
const calculateRetention = useCallback(
|
|
192
|
-
(userData: import("../services/AnalyticsEngineService").UserData[]) => {
|
|
193
|
-
try {
|
|
194
|
-
return analyticsEngineService.calculateRetention(userData);
|
|
195
|
-
} catch (err) {
|
|
196
|
-
onError?.(err as Error);
|
|
197
|
-
return [];
|
|
198
|
-
}
|
|
199
|
-
},
|
|
200
|
-
[onError]
|
|
201
|
-
);
|
|
202
|
-
|
|
203
|
-
const calculateFunnel = useCallback(
|
|
204
|
-
(data: import("../services/AnalyticsEngineService").FunnelItem[], steps: string[]) => {
|
|
205
|
-
try {
|
|
206
|
-
return analyticsEngineService.calculateFunnel(data, steps);
|
|
207
|
-
} catch (err) {
|
|
208
|
-
onError?.(err as Error);
|
|
209
|
-
return null;
|
|
210
|
-
}
|
|
211
|
-
},
|
|
212
|
-
[onError]
|
|
213
|
-
);
|
|
214
|
-
|
|
215
|
-
const segmentUsers = useCallback(
|
|
216
|
-
(userData: import("../services/AnalyticsEngineService").UserData[]) => {
|
|
217
|
-
try {
|
|
218
|
-
return analyticsEngineService.segmentUsers(userData);
|
|
219
|
-
} catch (err) {
|
|
220
|
-
onError?.(err as Error);
|
|
221
|
-
return [];
|
|
222
|
-
}
|
|
223
|
-
},
|
|
224
|
-
[onError]
|
|
225
|
-
);
|
|
226
|
-
|
|
227
|
-
const predictUserBehavior = useCallback(
|
|
228
|
-
(user: Record<string, unknown>, historicalData: import("../services/AnalyticsEngineService").UserData[]) => {
|
|
229
|
-
try {
|
|
230
|
-
return analyticsEngineService.predictUserBehavior(user, historicalData);
|
|
231
|
-
} catch (err) {
|
|
232
|
-
onError?.(err as Error);
|
|
233
|
-
return null;
|
|
234
|
-
}
|
|
235
|
-
},
|
|
236
|
-
[onError]
|
|
237
|
-
);
|
|
238
|
-
|
|
239
|
-
const getPerformanceMetrics = useCallback(() => {
|
|
240
|
-
try {
|
|
241
|
-
return performanceService.getDashboardMetrics();
|
|
242
|
-
} catch (err) {
|
|
243
|
-
onError?.(err as Error);
|
|
244
|
-
return null;
|
|
245
|
-
}
|
|
246
|
-
}, [onError]);
|
|
247
|
-
|
|
248
|
-
const getRealtimeMetrics = useCallback(
|
|
249
|
-
(previous?: import("../services/PerformanceService").RealtimeMetrics) => {
|
|
250
|
-
try {
|
|
251
|
-
return performanceService.simulateRealtimeMetrics(previous);
|
|
252
|
-
} catch (err) {
|
|
253
|
-
onError?.(err as Error);
|
|
254
|
-
return null;
|
|
255
|
-
}
|
|
256
|
-
},
|
|
257
|
-
[onError]
|
|
258
|
-
);
|
|
169
|
+
}, [fetchAnalytics, refreshInterval]);
|
|
259
170
|
|
|
260
171
|
return {
|
|
261
172
|
...data,
|
|
@@ -263,12 +174,5 @@ export function useAnalytics(options: UseAnalyticsOptions = {}) {
|
|
|
263
174
|
updateDateRange,
|
|
264
175
|
refresh,
|
|
265
176
|
exportData,
|
|
266
|
-
// Advanced analytics
|
|
267
|
-
calculateRetention,
|
|
268
|
-
calculateFunnel,
|
|
269
|
-
segmentUsers,
|
|
270
|
-
predictUserBehavior,
|
|
271
|
-
getPerformanceMetrics,
|
|
272
|
-
getRealtimeMetrics,
|
|
273
177
|
};
|
|
274
178
|
}
|
|
@@ -17,26 +17,6 @@ export {
|
|
|
17
17
|
useAnalytics,
|
|
18
18
|
} from "./hooks";
|
|
19
19
|
|
|
20
|
-
// Services
|
|
21
|
-
export {
|
|
22
|
-
AnalyticsEngineService,
|
|
23
|
-
analyticsEngineService,
|
|
24
|
-
PerformanceService,
|
|
25
|
-
performanceService,
|
|
26
|
-
} from "./services";
|
|
27
|
-
export type {
|
|
28
|
-
ConversionPath,
|
|
29
|
-
HeatmapData,
|
|
30
|
-
UserBehaviorPrediction,
|
|
31
|
-
UserData,
|
|
32
|
-
ConversionData,
|
|
33
|
-
FunnelItem,
|
|
34
|
-
ActivityItem,
|
|
35
|
-
PerformanceMetric,
|
|
36
|
-
DashboardMetrics,
|
|
37
|
-
RealtimeMetrics,
|
|
38
|
-
} from "./services";
|
|
39
|
-
|
|
40
20
|
// Utils
|
|
41
21
|
export {
|
|
42
22
|
formatNumber,
|
|
@@ -219,7 +219,7 @@ export function getDateRangePresets(): DateRangePreset[] {
|
|
|
219
219
|
export function aggregateByPeriod(
|
|
220
220
|
data: Array<{ date: string; [key: string]: number | string }>,
|
|
221
221
|
period: "day" | "week" | "month" = "day"
|
|
222
|
-
): Array<{ date: string; [key: string]:
|
|
222
|
+
): Array<{ date: string; [key: string]: number }> {
|
|
223
223
|
const grouped = new Map<string, Array<typeof data[0]>>();
|
|
224
224
|
|
|
225
225
|
data.forEach((item) => {
|
|
@@ -244,14 +244,13 @@ export function aggregateByPeriod(
|
|
|
244
244
|
});
|
|
245
245
|
|
|
246
246
|
return Array.from(grouped.entries()).map(([date, items]) => {
|
|
247
|
-
const aggregated:
|
|
247
|
+
const aggregated: any = { date };
|
|
248
248
|
|
|
249
249
|
// Sum all numeric fields
|
|
250
250
|
items.forEach((item) => {
|
|
251
251
|
Object.entries(item).forEach(([key, value]) => {
|
|
252
252
|
if (key !== "date" && typeof value === "number") {
|
|
253
|
-
|
|
254
|
-
aggregated[key] = typeof currentValue === "number" ? currentValue + value : value;
|
|
253
|
+
aggregated[key] = (aggregated[key] || 0) + value;
|
|
255
254
|
}
|
|
256
255
|
});
|
|
257
256
|
});
|
|
@@ -19,6 +19,7 @@ export const LoginForm = ({
|
|
|
19
19
|
showForgotPassword = true,
|
|
20
20
|
showRegisterLink = true,
|
|
21
21
|
showSocialLogin,
|
|
22
|
+
onLoginAttempt,
|
|
22
23
|
onLoginSuccess,
|
|
23
24
|
onLoginError,
|
|
24
25
|
onGoogleLogin,
|
|
@@ -47,20 +48,26 @@ export const LoginForm = ({
|
|
|
47
48
|
setIsLoading(true);
|
|
48
49
|
|
|
49
50
|
try {
|
|
50
|
-
|
|
51
|
-
// const user = await login({ email, password });
|
|
51
|
+
let user: { id: string; email: string; name?: string };
|
|
52
52
|
|
|
53
|
-
//
|
|
54
|
-
|
|
53
|
+
// Use custom login handler if provided (real auth), otherwise use mock auth
|
|
54
|
+
if (onLoginAttempt) {
|
|
55
|
+
// Real authentication flow
|
|
56
|
+
user = await onLoginAttempt({ email, password });
|
|
57
|
+
} else {
|
|
58
|
+
// Mock authentication flow (for development/demo)
|
|
59
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
60
|
+
user = {
|
|
61
|
+
id: "1",
|
|
62
|
+
email,
|
|
63
|
+
name: email.split("@")[0],
|
|
64
|
+
};
|
|
65
|
+
}
|
|
55
66
|
|
|
56
|
-
//
|
|
57
|
-
|
|
58
|
-
id: "1",
|
|
59
|
-
email,
|
|
60
|
-
name: email.split("@")[0],
|
|
61
|
-
};
|
|
67
|
+
// Call success callback
|
|
68
|
+
await onLoginSuccess?.(user);
|
|
62
69
|
|
|
63
|
-
|
|
70
|
+
// Navigate to after-login route
|
|
64
71
|
navigate(config.afterLoginRoute);
|
|
65
72
|
} catch (err) {
|
|
66
73
|
const errorMessage = err instanceof Error ? err.message : "Login failed";
|
|
@@ -19,6 +19,7 @@ export const RegisterForm = ({
|
|
|
19
19
|
showLoginLink = true,
|
|
20
20
|
requirePasswordConfirm = true,
|
|
21
21
|
showSocialLogin,
|
|
22
|
+
onRegisterAttempt,
|
|
22
23
|
onRegisterSuccess,
|
|
23
24
|
onRegisterError,
|
|
24
25
|
onGoogleLogin,
|
|
@@ -61,20 +62,26 @@ export const RegisterForm = ({
|
|
|
61
62
|
setIsLoading(true);
|
|
62
63
|
|
|
63
64
|
try {
|
|
64
|
-
|
|
65
|
-
// const user = await register({ email, password, name, metadata });
|
|
65
|
+
let user: { id: string; email: string; name?: string };
|
|
66
66
|
|
|
67
|
-
//
|
|
68
|
-
|
|
67
|
+
// Use custom register handler if provided (real auth), otherwise use mock auth
|
|
68
|
+
if (onRegisterAttempt) {
|
|
69
|
+
// Real authentication flow
|
|
70
|
+
user = await onRegisterAttempt({ email, password, name });
|
|
71
|
+
} else {
|
|
72
|
+
// Mock authentication flow (for development/demo)
|
|
73
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
74
|
+
user = {
|
|
75
|
+
id: "1",
|
|
76
|
+
email,
|
|
77
|
+
name: name || email.split("@")[0],
|
|
78
|
+
};
|
|
79
|
+
}
|
|
69
80
|
|
|
70
|
-
//
|
|
71
|
-
|
|
72
|
-
id: "1",
|
|
73
|
-
email,
|
|
74
|
-
name: name || email.split("@")[0],
|
|
75
|
-
};
|
|
81
|
+
// Call success callback
|
|
82
|
+
await onRegisterSuccess?.(user);
|
|
76
83
|
|
|
77
|
-
|
|
84
|
+
// Navigate to after-login route
|
|
78
85
|
navigate(config.afterLoginRoute);
|
|
79
86
|
} catch (err) {
|
|
80
87
|
const errorMessage = err instanceof Error ? err.message : "Registration failed";
|
|
@@ -24,8 +24,6 @@ export interface RegisterData {
|
|
|
24
24
|
email: string;
|
|
25
25
|
/** Password */
|
|
26
26
|
password: string;
|
|
27
|
-
/** Password confirmation */
|
|
28
|
-
confirmPassword?: string;
|
|
29
27
|
/** Full name */
|
|
30
28
|
name?: string;
|
|
31
29
|
/** Additional user data */
|
|
@@ -93,9 +91,9 @@ export interface User {
|
|
|
93
91
|
*/
|
|
94
92
|
export interface AuthActions {
|
|
95
93
|
/** Login with email/password */
|
|
96
|
-
login: (credentials: LoginCredentials) => Promise<
|
|
94
|
+
login: (credentials: LoginCredentials) => Promise<void>;
|
|
97
95
|
/** Register new account */
|
|
98
|
-
register: (data: RegisterData) => Promise<
|
|
96
|
+
register: (data: RegisterData) => Promise<void>;
|
|
99
97
|
/** Logout current user */
|
|
100
98
|
logout: () => Promise<void>;
|
|
101
99
|
/** Send password reset email */
|
|
@@ -105,7 +103,7 @@ export interface AuthActions {
|
|
|
105
103
|
/** Update user profile */
|
|
106
104
|
updateProfile: (data: Partial<User>) => Promise<void>;
|
|
107
105
|
/** Refresh authentication session */
|
|
108
|
-
refresh: () => Promise<
|
|
106
|
+
refresh: () => Promise<void>;
|
|
109
107
|
}
|
|
110
108
|
|
|
111
109
|
/**
|
|
@@ -214,7 +212,9 @@ export interface LoginFormProps {
|
|
|
214
212
|
showRegisterLink?: boolean;
|
|
215
213
|
/** Show social login buttons */
|
|
216
214
|
showSocialLogin?: boolean;
|
|
217
|
-
/**
|
|
215
|
+
/** Custom login handler (receives credentials, returns user) - overrides mock auth */
|
|
216
|
+
onLoginAttempt?: (credentials: LoginCredentials) => Promise<User>;
|
|
217
|
+
/** On successful login (called after onLoginAttempt or mock auth succeeds) */
|
|
218
218
|
onLoginSuccess?: (user: User) => void | Promise<void>;
|
|
219
219
|
/** On login error */
|
|
220
220
|
onLoginError?: (error: string) => void;
|
|
@@ -240,7 +240,9 @@ export interface RegisterFormProps {
|
|
|
240
240
|
requirePasswordConfirm?: boolean;
|
|
241
241
|
/** Show social login buttons */
|
|
242
242
|
showSocialLogin?: boolean;
|
|
243
|
-
/**
|
|
243
|
+
/** Custom register handler (receives register data, returns user) - overrides mock auth */
|
|
244
|
+
onRegisterAttempt?: (data: RegisterData) => Promise<User>;
|
|
245
|
+
/** On successful registration (called after onRegisterAttempt or mock auth succeeds) */
|
|
244
246
|
onRegisterSuccess?: (user: User) => void | Promise<void>;
|
|
245
247
|
/** On registration error */
|
|
246
248
|
onRegisterError?: (error: string) => void;
|
|
@@ -195,7 +195,7 @@ export function isEmailVerified(user: User | null): boolean {
|
|
|
195
195
|
* @returns Formatted date string or empty string
|
|
196
196
|
*/
|
|
197
197
|
export function formatUserCreatedAt(user: User | null, locale: string = "en-US"): string {
|
|
198
|
-
if (!user
|
|
198
|
+
if (!user.createdAt) return "";
|
|
199
199
|
return new Date(user.createdAt).toLocaleDateString(locale, {
|
|
200
200
|
year: "numeric",
|
|
201
201
|
month: "long",
|
|
@@ -11,7 +11,7 @@ import type { BillingPortalProps } from "../types/billing";
|
|
|
11
11
|
import { UsageCard } from "./UsageCard";
|
|
12
12
|
import { PaymentMethodsList } from "./PaymentMethodsList";
|
|
13
13
|
import { InvoiceCard } from "./InvoiceCard";
|
|
14
|
-
import { getDaysRemaining, getStatusColor, getStatusLabel, formatPrice
|
|
14
|
+
import { getDaysRemaining, getStatusColor, getStatusLabel, formatPrice } from "../utils/billing";
|
|
15
15
|
|
|
16
16
|
export const BillingPortal = ({
|
|
17
17
|
billing,
|
|
@@ -22,14 +22,6 @@ interface UseBillingOptions {
|
|
|
22
22
|
initialData?: BillingSummary;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
interface CardDetails {
|
|
26
|
-
last4: string;
|
|
27
|
-
brand: string;
|
|
28
|
-
expiryMonth: number;
|
|
29
|
-
expiryYear: number;
|
|
30
|
-
name?: string;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
25
|
interface BillingActions {
|
|
34
26
|
/** Load billing summary */
|
|
35
27
|
loadBilling: () => Promise<void>;
|
|
@@ -40,7 +32,7 @@ interface BillingActions {
|
|
|
40
32
|
/** Update billing cycle */
|
|
41
33
|
updateCycle: (cycle: BillingCycle) => Promise<void>;
|
|
42
34
|
/** Add payment method */
|
|
43
|
-
addPaymentMethod: (paymentMethodDetails:
|
|
35
|
+
addPaymentMethod: (paymentMethodDetails: any) => Promise<PaymentMethod>;
|
|
44
36
|
/** Remove payment method */
|
|
45
37
|
removePaymentMethod: (methodId: string) => Promise<void>;
|
|
46
38
|
/** Set default payment method */
|
|
@@ -264,7 +256,7 @@ export function useBilling(options: UseBillingOptions = {}) {
|
|
|
264
256
|
}, [apiUrl]);
|
|
265
257
|
|
|
266
258
|
// Add payment method
|
|
267
|
-
const addPaymentMethod = useCallback(async (paymentMethodDetails:
|
|
259
|
+
const addPaymentMethod = useCallback(async (paymentMethodDetails: any) => {
|
|
268
260
|
setIsLoading(true);
|
|
269
261
|
setError(null);
|
|
270
262
|
|
|
@@ -9,33 +9,12 @@ import type {
|
|
|
9
9
|
Currency,
|
|
10
10
|
PlanTier,
|
|
11
11
|
Subscription,
|
|
12
|
-
SubscriptionStatus,
|
|
13
12
|
PaymentMethod,
|
|
14
13
|
Invoice,
|
|
15
14
|
InvoiceStatus,
|
|
16
15
|
UsageMetric,
|
|
17
16
|
} from "../types/billing";
|
|
18
17
|
|
|
19
|
-
/**
|
|
20
|
-
* Format number with K/M/B suffixes
|
|
21
|
-
*
|
|
22
|
-
* @param num - Number to format
|
|
23
|
-
* @param decimals - Number of decimal places (default: 1)
|
|
24
|
-
* @returns Formatted string
|
|
25
|
-
*/
|
|
26
|
-
export function formatNumber(num: number, decimals: number = 1): string {
|
|
27
|
-
if (num >= 1_000_000_000) {
|
|
28
|
-
return (num / 1_000_000_000).toFixed(decimals) + "B";
|
|
29
|
-
}
|
|
30
|
-
if (num >= 1_000_000) {
|
|
31
|
-
return (num / 1_000_000).toFixed(decimals) + "M";
|
|
32
|
-
}
|
|
33
|
-
if (num >= 1_000) {
|
|
34
|
-
return (num / 1_000).toFixed(decimals) + "K";
|
|
35
|
-
}
|
|
36
|
-
return num.toFixed(decimals);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
18
|
/**
|
|
40
19
|
* Format price with currency
|
|
41
20
|
*
|
|
@@ -135,7 +114,6 @@ export function getStatusColor(status: SubscriptionStatus): string {
|
|
|
135
114
|
canceled: "text-gray-600 dark:text-gray-500",
|
|
136
115
|
unpaid: "text-red-600 dark:text-red-500",
|
|
137
116
|
incomplete: "text-yellow-600 dark:text-yellow-500",
|
|
138
|
-
revoked: "text-purple-600 dark:text-purple-500",
|
|
139
117
|
};
|
|
140
118
|
|
|
141
119
|
return colorMap[status] || "text-gray-600";
|
|
@@ -155,7 +133,6 @@ export function getStatusLabel(status: SubscriptionStatus): string {
|
|
|
155
133
|
canceled: "Canceled",
|
|
156
134
|
unpaid: "Unpaid",
|
|
157
135
|
incomplete: "Incomplete",
|
|
158
|
-
revoked: "Revoked",
|
|
159
136
|
};
|
|
160
137
|
|
|
161
138
|
return labelMap[status] || status;
|
|
@@ -174,7 +151,6 @@ export function getInvoiceStatusColor(status: InvoiceStatus): string {
|
|
|
174
151
|
paid: "text-green-600 dark:text-green-500",
|
|
175
152
|
void: "text-gray-600 dark:text-gray-500",
|
|
176
153
|
uncollectible: "text-red-600 dark:text-red-500",
|
|
177
|
-
refunded: "text-blue-600 dark:text-blue-500",
|
|
178
154
|
};
|
|
179
155
|
|
|
180
156
|
return colorMap[status] || "text-gray-600";
|
|
@@ -193,7 +169,6 @@ export function getInvoiceStatusLabel(status: InvoiceStatus): string {
|
|
|
193
169
|
paid: "Paid",
|
|
194
170
|
void: "Void",
|
|
195
171
|
uncollectible: "Uncollectible",
|
|
196
|
-
refunded: "Refunded",
|
|
197
172
|
};
|
|
198
173
|
|
|
199
174
|
return labelMap[status] || status;
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { useState, useEffect } from "react";
|
|
2
2
|
import { useLocation, Outlet, Navigate } from "react-router-dom";
|
|
3
|
-
import { useTranslation } from "react-i18next";
|
|
4
3
|
import { Skeleton } from "@umituz/web-design-system/atoms";
|
|
5
4
|
import { DashboardSidebar } from "./DashboardSidebar";
|
|
6
5
|
import { DashboardHeader } from "./DashboardHeader";
|
|
@@ -27,8 +26,6 @@ interface DashboardLayoutProps {
|
|
|
27
26
|
onDismissNotification?: (id: string) => void;
|
|
28
27
|
/** Login route for redirect */
|
|
29
28
|
loginRoute?: string;
|
|
30
|
-
/** Children for routing */
|
|
31
|
-
children?: React.ReactNode;
|
|
32
29
|
}
|
|
33
30
|
|
|
34
31
|
/**
|
|
@@ -56,10 +53,8 @@ export const DashboardLayout = ({
|
|
|
56
53
|
onMarkAllRead,
|
|
57
54
|
onDismissNotification,
|
|
58
55
|
loginRoute = "/login",
|
|
59
|
-
children,
|
|
60
56
|
}: DashboardLayoutProps) => {
|
|
61
57
|
const location = useLocation();
|
|
62
|
-
const { t } = useTranslation();
|
|
63
58
|
const [collapsed, setCollapsed] = useState(false);
|
|
64
59
|
const [mobileOpen, setMobileOpen] = useState(false);
|
|
65
60
|
const [loading, setLoading] = useState(true);
|
|
@@ -82,15 +77,8 @@ export const DashboardLayout = ({
|
|
|
82
77
|
.find((i) => i.path === location.pathname);
|
|
83
78
|
|
|
84
79
|
const getTitle = () => {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
return config.extraTitleMap[location.pathname];
|
|
88
|
-
}
|
|
89
|
-
// Use translated label from active item
|
|
90
|
-
if (activeItem) {
|
|
91
|
-
return t(activeItem.label);
|
|
92
|
-
}
|
|
93
|
-
return "Dashboard";
|
|
80
|
+
if (!activeItem) return config.extraTitleMap?.[location.pathname] || "Dashboard";
|
|
81
|
+
return activeItem.label; // Note: In real app, this would be translated
|
|
94
82
|
};
|
|
95
83
|
|
|
96
84
|
const currentTitle = getTitle();
|
|
@@ -156,7 +144,7 @@ export const DashboardLayout = ({
|
|
|
156
144
|
<Skeleton className="h-64 rounded-[32px]" />
|
|
157
145
|
</div>
|
|
158
146
|
) : (
|
|
159
|
-
|
|
147
|
+
<Outlet />
|
|
160
148
|
)}
|
|
161
149
|
</main>
|
|
162
150
|
</div>
|
|
@@ -54,12 +54,12 @@ export const DashboardSidebar = ({
|
|
|
54
54
|
return (
|
|
55
55
|
<div className="flex h-full flex-col">
|
|
56
56
|
{/* Brand Section */}
|
|
57
|
-
<div className="flex h-16 items-center gap-3 border-b border-sidebar-border
|
|
57
|
+
<div className="flex h-16 items-center gap-3 border-b border-sidebar-border px-4 transition-all duration-300">
|
|
58
58
|
<BrandLogo size={32} />
|
|
59
59
|
{!collapsed && (
|
|
60
|
-
<div className="flex flex-col">
|
|
61
|
-
<span className="text-
|
|
62
|
-
<span className="text-[
|
|
60
|
+
<div className="flex flex-col -gap-1">
|
|
61
|
+
<span className="text-2xl font-black text-sidebar-foreground tracking-tighter leading-none">{brandName}</span>
|
|
62
|
+
<span className="text-[11px] font-bold text-primary/70 lowercase tracking-tight mt-2 ml-1 select-none underline decoration-primary/40 underline-offset-[6px] decoration-2">
|
|
63
63
|
{brandTagline}
|
|
64
64
|
</span>
|
|
65
65
|
</div>
|
|
@@ -67,24 +67,24 @@ export const DashboardSidebar = ({
|
|
|
67
67
|
</div>
|
|
68
68
|
|
|
69
69
|
{/* Create Button */}
|
|
70
|
-
<div className="px-3 py-4 border-b border-sidebar-border/
|
|
70
|
+
<div className="px-3 py-4 border-b border-sidebar-border/50">
|
|
71
71
|
<Link to={createPostRoute}>
|
|
72
72
|
<Button
|
|
73
73
|
variant="default"
|
|
74
|
-
className={`w-full gap-3 shadow-glow transition-all active:scale-95 group overflow-hidden rounded-
|
|
75
|
-
collapsed ? "px-0 justify-center h-10 w-10 mx-auto" : "justify-start px-4 h-
|
|
74
|
+
className={`w-full gap-3 shadow-glow transition-all active:scale-95 group overflow-hidden rounded-xl ${
|
|
75
|
+
collapsed ? "px-0 justify-center h-10 w-10 mx-auto" : "justify-start px-4 h-11"
|
|
76
76
|
}`}
|
|
77
77
|
title={collapsed ? t('sidebar.createPost') : undefined}
|
|
78
78
|
>
|
|
79
79
|
<PenTool className={`shrink-0 transition-transform duration-300 ${collapsed ? "h-5 w-5" : "h-4 w-4 group-hover:scale-110"}`} />
|
|
80
|
-
{!collapsed && <span className="font-
|
|
80
|
+
{!collapsed && <span className="font-bold tracking-tight">{t('sidebar.createPost')}</span>}
|
|
81
81
|
</Button>
|
|
82
82
|
</Link>
|
|
83
83
|
</div>
|
|
84
84
|
|
|
85
85
|
{/* Navigation */}
|
|
86
|
-
<nav className="flex-1 overflow-y-auto px-2 py-
|
|
87
|
-
<div className="space-y-
|
|
86
|
+
<nav className="flex-1 overflow-y-auto px-2 py-3 scrollbar-hide">
|
|
87
|
+
<div className="space-y-6">
|
|
88
88
|
{sidebarGroups.map((group) => {
|
|
89
89
|
const filteredItems = filterSidebarItems(group.items, user);
|
|
90
90
|
|
|
@@ -97,16 +97,16 @@ export const DashboardSidebar = ({
|
|
|
97
97
|
{!collapsed && (
|
|
98
98
|
<button
|
|
99
99
|
onClick={() => toggleGroup(group.title)}
|
|
100
|
-
className="w-full flex items-center justify-between px-3 py-
|
|
100
|
+
className="w-full flex items-center justify-between px-3 py-2 mb-1 rounded-lg hover:bg-sidebar-accent/30 transition-all duration-200 group/header"
|
|
101
101
|
>
|
|
102
|
-
<span className="text-[10px] font-
|
|
102
|
+
<span className="text-[10px] font-bold uppercase tracking-widest text-sidebar-foreground/40 group-hover/header:text-sidebar-foreground/70 transition-colors">
|
|
103
103
|
{group.title === "sidebar.ai" ? `${brandName} AI` : t(group.title)}
|
|
104
104
|
</span>
|
|
105
105
|
<div className="flex-shrink-0">
|
|
106
106
|
{isGroupCollapsed ? (
|
|
107
|
-
<ChevronRight className="h-3.5 w-3.5 text-sidebar-foreground/
|
|
107
|
+
<ChevronRight className="h-3.5 w-3.5 text-sidebar-foreground/30 transition-transform duration-200 group-hover/header:text-sidebar-foreground/50" />
|
|
108
108
|
) : (
|
|
109
|
-
<ChevronDown className="h-3.5 w-3.5 text-sidebar-foreground/
|
|
109
|
+
<ChevronDown className="h-3.5 w-3.5 text-sidebar-foreground/30 transition-transform duration-200 group-hover/header:text-sidebar-foreground/50" />
|
|
110
110
|
)}
|
|
111
111
|
</div>
|
|
112
112
|
</button>
|
|
@@ -118,10 +118,10 @@ export const DashboardSidebar = ({
|
|
|
118
118
|
<Link
|
|
119
119
|
key={item.path}
|
|
120
120
|
to={item.path}
|
|
121
|
-
className={`flex items-center gap-3 rounded-
|
|
121
|
+
className={`flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium transition-all duration-200 ${
|
|
122
122
|
active
|
|
123
123
|
? "bg-sidebar-accent text-sidebar-accent-foreground shadow-sm"
|
|
124
|
-
: "text-sidebar-foreground/
|
|
124
|
+
: "text-sidebar-foreground/70 hover:bg-sidebar-accent/40 hover:text-sidebar-foreground"
|
|
125
125
|
} ${collapsed ? "justify-center" : ""}`}
|
|
126
126
|
title={collapsed ? t(item.label) : undefined}
|
|
127
127
|
>
|
|
@@ -137,14 +137,14 @@ export const DashboardSidebar = ({
|
|
|
137
137
|
</nav>
|
|
138
138
|
|
|
139
139
|
{/* Collapse Toggle */}
|
|
140
|
-
<div className="border-t border-sidebar-border
|
|
140
|
+
<div className="border-t border-sidebar-border p-3">
|
|
141
141
|
<div className={`flex items-center ${collapsed ? "justify-center" : "justify-between"}`}>
|
|
142
142
|
{!collapsed && (
|
|
143
|
-
<p className="text-[10px] uppercase tracking-wider text-sidebar-foreground/
|
|
143
|
+
<p className="text-[10px] uppercase tracking-wider text-sidebar-foreground/40 font-bold px-2">
|
|
144
144
|
{t('sidebar.system')}
|
|
145
145
|
</p>
|
|
146
146
|
)}
|
|
147
|
-
<Button variant="ghost" size="icon" onClick={() => setCollapsed(!collapsed)} className="text-sidebar-foreground/
|
|
147
|
+
<Button variant="ghost" size="icon" onClick={() => setCollapsed(!collapsed)} className="text-sidebar-foreground/70">
|
|
148
148
|
{collapsed ? <Menu className="h-4 w-4" /> : <ChevronLeft className="h-4 w-4" />}
|
|
149
149
|
</Button>
|
|
150
150
|
</div>
|