@umituz/web-dashboard 3.2.2 → 4.0.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 +202 -14
- package/package.json +21 -29
- package/src/domains/analytics/components/AnalyticsCard.tsx +23 -21
- 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 +42 -64
- 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 +87 -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/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 +30 -0
- package/src/primitives/Checkbox.tsx +40 -0
- package/src/primitives/Input.tsx +19 -0
- package/src/primitives/Skeleton.tsx +26 -0
- package/src/primitives/cn.ts +15 -0
- package/src/primitives/index.ts +23 -0
- package/src/styles/dashboard.css +3045 -0
- package/src/styles/tokens.contract.test.ts +66 -0
- package/src/styles/tokens.css +124 -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,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth Utilities - Secure Token
|
|
3
|
+
*
|
|
4
|
+
* Cryptographically secure token generation utilities
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Generate a cryptographically secure random token.
|
|
9
|
+
*
|
|
10
|
+
* Requires the Web Crypto API (`crypto.getRandomValues`), available in
|
|
11
|
+
* all modern browsers, Node.js >= 19 (and >= 15 via `globalThis.crypto`),
|
|
12
|
+
* and React Native with a crypto polyfill. Throws instead of silently
|
|
13
|
+
* degrading to an insecure PRNG — predictable tokens are worse than
|
|
14
|
+
* a loud failure.
|
|
15
|
+
*
|
|
16
|
+
* @param length - Token length in bytes (default: 32)
|
|
17
|
+
* @returns Hex-encoded random token
|
|
18
|
+
* @throws Error when no CSPRNG is available in the runtime
|
|
19
|
+
*/
|
|
20
|
+
export function generateResetToken(length: number = 32): string {
|
|
21
|
+
if (typeof crypto === 'undefined' || typeof crypto.getRandomValues !== 'function') {
|
|
22
|
+
throw new Error(
|
|
23
|
+
'generateResetToken: no cryptographic random source available in this runtime ' +
|
|
24
|
+
'(requires crypto.getRandomValues — Web Crypto, Node >= 19, or a polyfill)',
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const byteLength = Math.max(1, Math.floor(length));
|
|
29
|
+
const bytes = new Uint8Array(byteLength);
|
|
30
|
+
crypto.getRandomValues(bytes);
|
|
31
|
+
|
|
32
|
+
return Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('');
|
|
33
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth Validation Error Keys
|
|
3
|
+
*
|
|
4
|
+
* Stable i18n keys for validation errors. The auth UI maps these to
|
|
5
|
+
* localized messages via the consumer's translation system.
|
|
6
|
+
*
|
|
7
|
+
* Why keys instead of literal strings:
|
|
8
|
+
* - No hardcoded user-facing strings in source code
|
|
9
|
+
* - Centralized catalog: easier to audit and translate
|
|
10
|
+
* - Validators stay pure and testable
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export const AUTH_VALIDATION_KEYS = {
|
|
14
|
+
emailRequired: 'auth.validation.emailRequired',
|
|
15
|
+
emailInvalid: 'auth.validation.emailInvalid',
|
|
16
|
+
passwordRequired: 'auth.validation.passwordRequired',
|
|
17
|
+
passwordTooShort: 'auth.validation.passwordTooShort',
|
|
18
|
+
passwordsDoNotMatch: 'auth.validation.passwordsDoNotMatch',
|
|
19
|
+
nameRequired: 'auth.validation.nameRequired',
|
|
20
|
+
invalidResetToken: 'auth.validation.invalidResetToken',
|
|
21
|
+
} as const;
|
|
22
|
+
|
|
23
|
+
export type AuthValidationKey = (typeof AUTH_VALIDATION_KEYS)[keyof typeof AUTH_VALIDATION_KEYS];
|
|
@@ -1,25 +1,36 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* useCalendar
|
|
2
|
+
* useCalendar
|
|
3
3
|
*
|
|
4
|
-
* React hook for calendar
|
|
4
|
+
* React hook for the calendar domain. Delegates persistence
|
|
5
|
+
* to the injected `CalendarService`, guards refreshes with a
|
|
6
|
+
* request id so a stale response can never overwrite a newer one,
|
|
7
|
+
* and returns a clean discriminated CRUD contract.
|
|
5
8
|
*/
|
|
6
9
|
|
|
7
|
-
import { useState, useCallback, useEffect, useMemo } from 'react';
|
|
8
|
-
import type {
|
|
9
|
-
|
|
10
|
+
import { useState, useCallback, useEffect, useMemo, useRef } from 'react';
|
|
11
|
+
import type {
|
|
12
|
+
CalendarConfig,
|
|
13
|
+
ContentItem,
|
|
14
|
+
CalendarFilter,
|
|
15
|
+
} from '../types/calendar.types';
|
|
16
|
+
import { calendarService, applyCalendarFilter } from '../services';
|
|
10
17
|
import { DEFAULT_CALENDAR_CONFIG } from '../utils';
|
|
18
|
+
import type { ICalendarService } from '../types/calendar.types';
|
|
11
19
|
|
|
12
|
-
|
|
13
|
-
|
|
20
|
+
type CalendarView = 'month' | 'week' | 'day' | 'timeline';
|
|
21
|
+
|
|
22
|
+
export interface UseCalendarOptions {
|
|
23
|
+
/** Calendar configuration overrides */
|
|
14
24
|
config?: Partial<CalendarConfig>;
|
|
15
|
-
/** User ID */
|
|
25
|
+
/** User ID (required to fetch items) */
|
|
16
26
|
userId: string;
|
|
17
|
-
/** Error callback */
|
|
27
|
+
/** Error callback — receives the original Error */
|
|
18
28
|
onError?: (error: Error) => void;
|
|
29
|
+
/** Override the default service (useful for tests) */
|
|
30
|
+
service?: ICalendarService;
|
|
19
31
|
}
|
|
20
32
|
|
|
21
|
-
interface UseCalendarReturn {
|
|
22
|
-
// Data
|
|
33
|
+
export interface UseCalendarReturn {
|
|
23
34
|
items: ContentItem[];
|
|
24
35
|
loading: boolean;
|
|
25
36
|
error: string | null;
|
|
@@ -28,182 +39,171 @@ interface UseCalendarReturn {
|
|
|
28
39
|
selectedDate: Date;
|
|
29
40
|
filter: CalendarFilter;
|
|
30
41
|
|
|
31
|
-
// Actions
|
|
32
42
|
setCurrentView: (view: CalendarView) => void;
|
|
33
43
|
setCurrentDate: (date: Date) => void;
|
|
34
44
|
setSelectedDate: (date: Date) => void;
|
|
35
45
|
setFilter: (filter: Partial<CalendarFilter>) => void;
|
|
36
46
|
refresh: () => Promise<void>;
|
|
37
47
|
|
|
38
|
-
|
|
39
|
-
|
|
48
|
+
createItem: (
|
|
49
|
+
item: Omit<ContentItem, 'id' | 'created_at' | 'updated_at'>,
|
|
50
|
+
) => Promise<ContentItem>;
|
|
40
51
|
updateItem: (id: string, updates: Partial<ContentItem>) => Promise<void>;
|
|
41
52
|
deleteItem: (id: string) => Promise<void>;
|
|
42
53
|
moveItem: (id: string, newDate: Date) => Promise<void>;
|
|
43
54
|
|
|
44
|
-
// Computed
|
|
45
55
|
filteredItems: ContentItem[];
|
|
46
56
|
itemsForDate: (date: Date) => ContentItem[];
|
|
47
57
|
}
|
|
48
58
|
|
|
49
|
-
|
|
59
|
+
const toError = (err: unknown, fallback: string): Error =>
|
|
60
|
+
err instanceof Error ? err : new Error(fallback);
|
|
50
61
|
|
|
51
|
-
/**
|
|
52
|
-
* useCalendar hook
|
|
53
|
-
*
|
|
54
|
-
* Manages calendar state and operations with config support
|
|
55
|
-
*/
|
|
56
62
|
export function useCalendar(options: UseCalendarOptions): UseCalendarReturn {
|
|
57
|
-
const { config: userConfig, userId, onError } = options;
|
|
63
|
+
const { config: userConfig, userId, onError, service = calendarService } = options;
|
|
58
64
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
}), [userConfig]);
|
|
65
|
+
const config = useMemo(
|
|
66
|
+
() => ({ ...DEFAULT_CALENDAR_CONFIG, ...userConfig }),
|
|
67
|
+
[userConfig],
|
|
68
|
+
);
|
|
64
69
|
|
|
65
|
-
// State
|
|
66
70
|
const [items, setItems] = useState<ContentItem[]>([]);
|
|
67
71
|
const [loading, setLoading] = useState(false);
|
|
68
72
|
const [error, setError] = useState<string | null>(null);
|
|
69
|
-
const [currentView, setCurrentView] = useState<CalendarView>(
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
const [
|
|
73
|
+
const [currentView, setCurrentView] = useState<CalendarView>(
|
|
74
|
+
config.defaultView ?? 'month',
|
|
75
|
+
);
|
|
76
|
+
const [currentDate, setCurrentDate] = useState(() => new Date());
|
|
77
|
+
const [selectedDate, setSelectedDate] = useState(() => new Date());
|
|
78
|
+
const [filter, setFilterState] = useState<CalendarFilter>({});
|
|
79
|
+
|
|
80
|
+
// Keep the latest onError in a ref so refresh identity stays stable
|
|
81
|
+
// when consumers pass a fresh closure each render.
|
|
82
|
+
const onErrorRef = useRef(onError);
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
onErrorRef.current = onError;
|
|
85
|
+
}, [onError]);
|
|
86
|
+
|
|
87
|
+
// Guards against out-of-order responses: only the most recent
|
|
88
|
+
// refresh may commit state (e.g. user changes the filter while a
|
|
89
|
+
// previous fetch is still resolving).
|
|
90
|
+
const requestIdRef = useRef(0);
|
|
73
91
|
|
|
74
|
-
// Fetch items
|
|
75
92
|
const refresh = useCallback(async () => {
|
|
76
|
-
if (!userId)
|
|
93
|
+
if (!userId) {
|
|
94
|
+
setError('User ID is required to load calendar items');
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
77
97
|
|
|
98
|
+
const requestId = ++requestIdRef.current;
|
|
78
99
|
setLoading(true);
|
|
79
100
|
setError(null);
|
|
80
|
-
|
|
81
101
|
try {
|
|
82
|
-
const data = await
|
|
83
|
-
|
|
102
|
+
const data = await service.getContentItems(userId, filter);
|
|
103
|
+
if (requestIdRef.current === requestId) {
|
|
104
|
+
setItems(data);
|
|
105
|
+
}
|
|
84
106
|
} catch (err) {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
107
|
+
if (requestIdRef.current === requestId) {
|
|
108
|
+
const wrapped = toError(err, 'Failed to fetch calendar items');
|
|
109
|
+
setError(wrapped.message);
|
|
110
|
+
onErrorRef.current?.(wrapped);
|
|
111
|
+
}
|
|
88
112
|
} finally {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
// Create item
|
|
94
|
-
const createItem = useCallback(async (item: Omit<ContentItem, 'id' | 'created_at' | 'updated_at'>): Promise<ContentItem> => {
|
|
95
|
-
if (!userId) {
|
|
96
|
-
throw new Error('User ID is required');
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
try {
|
|
100
|
-
const created = await calendarService.createContentItem(userId, item);
|
|
101
|
-
await refresh();
|
|
102
|
-
return created;
|
|
103
|
-
} catch (err) {
|
|
104
|
-
onError?.(err as Error);
|
|
105
|
-
throw err;
|
|
106
|
-
}
|
|
107
|
-
}, [userId, refresh, onError]);
|
|
108
|
-
|
|
109
|
-
// Update item
|
|
110
|
-
const updateItem = useCallback(async (id: string, updates: Partial<ContentItem>): Promise<void> => {
|
|
111
|
-
try {
|
|
112
|
-
await calendarService.updateContentItem(id, updates);
|
|
113
|
-
await refresh();
|
|
114
|
-
} catch (err) {
|
|
115
|
-
onError?.(err as Error);
|
|
116
|
-
throw err;
|
|
117
|
-
}
|
|
118
|
-
}, [refresh, onError]);
|
|
119
|
-
|
|
120
|
-
// Delete item
|
|
121
|
-
const deleteItem = useCallback(async (id: string): Promise<void> => {
|
|
122
|
-
try {
|
|
123
|
-
await calendarService.deleteContentItem(id);
|
|
124
|
-
await refresh();
|
|
125
|
-
} catch (err) {
|
|
126
|
-
onError?.(err as Error);
|
|
127
|
-
throw err;
|
|
128
|
-
}
|
|
129
|
-
}, [refresh, onError]);
|
|
130
|
-
|
|
131
|
-
// Move item
|
|
132
|
-
const moveItem = useCallback(async (id: string, newDate: Date): Promise<void> => {
|
|
133
|
-
try {
|
|
134
|
-
await calendarService.moveContentItem(id, newDate);
|
|
135
|
-
await refresh();
|
|
136
|
-
} catch (err) {
|
|
137
|
-
onError?.(err as Error);
|
|
138
|
-
throw err;
|
|
113
|
+
if (requestIdRef.current === requestId) {
|
|
114
|
+
setLoading(false);
|
|
115
|
+
}
|
|
139
116
|
}
|
|
140
|
-
}, [
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
117
|
+
}, [userId, filter, service]);
|
|
118
|
+
|
|
119
|
+
const createItem = useCallback(
|
|
120
|
+
async (item: Omit<ContentItem, 'id' | 'created_at' | 'updated_at'>) => {
|
|
121
|
+
if (!userId) throw new Error('User ID is required');
|
|
122
|
+
try {
|
|
123
|
+
const created = await service.createContentItem(userId, item);
|
|
124
|
+
await refresh();
|
|
125
|
+
return created;
|
|
126
|
+
} catch (err) {
|
|
127
|
+
const wrapped = toError(err, 'Failed to create item');
|
|
128
|
+
onErrorRef.current?.(wrapped);
|
|
129
|
+
throw wrapped;
|
|
153
130
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
131
|
+
},
|
|
132
|
+
[userId, service, refresh],
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
const updateItem = useCallback(
|
|
136
|
+
async (id: string, updates: Partial<ContentItem>) => {
|
|
137
|
+
try {
|
|
138
|
+
await service.updateContentItem(id, updates);
|
|
139
|
+
await refresh();
|
|
140
|
+
} catch (err) {
|
|
141
|
+
const wrapped = toError(err, 'Failed to update item');
|
|
142
|
+
onErrorRef.current?.(wrapped);
|
|
143
|
+
throw wrapped;
|
|
160
144
|
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
145
|
+
},
|
|
146
|
+
[service, refresh],
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
const deleteItem = useCallback(
|
|
150
|
+
async (id: string) => {
|
|
151
|
+
try {
|
|
152
|
+
await service.deleteContentItem(id);
|
|
153
|
+
await refresh();
|
|
154
|
+
} catch (err) {
|
|
155
|
+
const wrapped = toError(err, 'Failed to delete item');
|
|
156
|
+
onErrorRef.current?.(wrapped);
|
|
157
|
+
throw wrapped;
|
|
167
158
|
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
159
|
+
},
|
|
160
|
+
[service, refresh],
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
const moveItem = useCallback(
|
|
164
|
+
async (id: string, newDate: Date) => {
|
|
165
|
+
try {
|
|
166
|
+
await service.moveContentItem(id, newDate);
|
|
167
|
+
await refresh();
|
|
168
|
+
} catch (err) {
|
|
169
|
+
const wrapped = toError(err, 'Failed to move item');
|
|
170
|
+
onErrorRef.current?.(wrapped);
|
|
171
|
+
throw wrapped;
|
|
180
172
|
}
|
|
173
|
+
},
|
|
174
|
+
[service, refresh],
|
|
175
|
+
);
|
|
181
176
|
|
|
182
|
-
|
|
183
|
-
});
|
|
184
|
-
}, [
|
|
185
|
-
|
|
186
|
-
// Get items for specific date
|
|
187
|
-
const itemsForDate = useCallback((date: Date): ContentItem[] => {
|
|
188
|
-
const dateStart = new Date(date);
|
|
189
|
-
dateStart.setHours(0, 0, 0, 0);
|
|
190
|
-
|
|
191
|
-
const dateEnd = new Date(date);
|
|
192
|
-
dateEnd.setHours(23, 59, 59, 999);
|
|
177
|
+
const updateFilter = useCallback((updates: Partial<CalendarFilter>) => {
|
|
178
|
+
setFilterState((prev) => ({ ...prev, ...updates }));
|
|
179
|
+
}, []);
|
|
193
180
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
181
|
+
// Single source of truth for filtering (shared with the service layer)
|
|
182
|
+
// so client-side refiltering can never drift from server-side semantics.
|
|
183
|
+
const filteredItems = useMemo(
|
|
184
|
+
() => applyCalendarFilter(items, filter),
|
|
185
|
+
[items, filter],
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
const itemsForDate = useCallback(
|
|
189
|
+
(date: Date): ContentItem[] => {
|
|
190
|
+
const dateStart = new Date(date);
|
|
191
|
+
dateStart.setHours(0, 0, 0, 0);
|
|
192
|
+
const dateEnd = new Date(date);
|
|
193
|
+
dateEnd.setHours(23, 59, 59, 999);
|
|
194
|
+
return filteredItems.filter((item) => {
|
|
195
|
+
const itemDate = new Date(item.scheduled_at);
|
|
196
|
+
return itemDate >= dateStart && itemDate <= dateEnd;
|
|
197
|
+
});
|
|
198
|
+
},
|
|
199
|
+
[filteredItems],
|
|
200
|
+
);
|
|
199
201
|
|
|
200
|
-
// Initial fetch
|
|
201
202
|
useEffect(() => {
|
|
202
203
|
refresh();
|
|
203
204
|
}, [refresh]);
|
|
204
205
|
|
|
205
206
|
return {
|
|
206
|
-
// Data
|
|
207
207
|
items,
|
|
208
208
|
loading,
|
|
209
209
|
error,
|
|
@@ -211,21 +211,15 @@ export function useCalendar(options: UseCalendarOptions): UseCalendarReturn {
|
|
|
211
211
|
currentDate,
|
|
212
212
|
selectedDate,
|
|
213
213
|
filter,
|
|
214
|
-
|
|
215
|
-
// Actions
|
|
216
214
|
setCurrentView,
|
|
217
215
|
setCurrentDate,
|
|
218
216
|
setSelectedDate,
|
|
219
217
|
setFilter: updateFilter,
|
|
220
218
|
refresh,
|
|
221
|
-
|
|
222
|
-
// CRUD
|
|
223
219
|
createItem,
|
|
224
220
|
updateItem,
|
|
225
221
|
deleteItem,
|
|
226
222
|
moveItem,
|
|
227
|
-
|
|
228
|
-
// Computed
|
|
229
223
|
filteredItems,
|
|
230
224
|
itemsForDate,
|
|
231
225
|
};
|