@m5kdev/web-ui 0.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/LICENSE +621 -0
- package/README.md +17 -0
- package/package.json +169 -0
- package/src/animations/card.motion.ts +9 -0
- package/src/components/AvatarUpload.tsx +133 -0
- package/src/components/Button.tsx +14 -0
- package/src/components/Calendar.css +684 -0
- package/src/components/Calendar.tsx +32 -0
- package/src/components/CardsSelect.tsx +155 -0
- package/src/components/CollapsibleSidebarMenuItem.tsx +57 -0
- package/src/components/ColorPicker.tsx +56 -0
- package/src/components/CopyButton.tsx +45 -0
- package/src/components/CropDialog.tsx +154 -0
- package/src/components/DialogProvider.tsx +105 -0
- package/src/components/ErrorFallback.tsx +17 -0
- package/src/components/FileDropzone.tsx +120 -0
- package/src/components/MultiSelectDropdown.tsx +233 -0
- package/src/components/Orb.tsx +288 -0
- package/src/components/PageAlert.tsx +121 -0
- package/src/components/SelectChips.tsx +40 -0
- package/src/components/SidebarItem.tsx +26 -0
- package/src/components/Steps.tsx +340 -0
- package/src/components/TablerIconPicker.tsx +4260 -0
- package/src/components/app-header.tsx +40 -0
- package/src/components/blur-card.tsx +132 -0
- package/src/components/features-section-demo-1.tsx +127 -0
- package/src/components/features-section-demo-2.tsx +102 -0
- package/src/components/features-section-demo-3.tsx +272 -0
- package/src/components/mode-toggle.tsx +31 -0
- package/src/components/nav-main.tsx +69 -0
- package/src/components/pricing-cards.tsx +133 -0
- package/src/components/shared/ButtonCopy.tsx +50 -0
- package/src/components/team-switcher.tsx +83 -0
- package/src/components/theme-provider.tsx +74 -0
- package/src/components/typewriter.tsx +90 -0
- package/src/components/ui/alert-dialog.tsx +133 -0
- package/src/components/ui/alert.tsx +60 -0
- package/src/components/ui/avatar.tsx +47 -0
- package/src/components/ui/badge.tsx +33 -0
- package/src/components/ui/bento-grid.tsx +54 -0
- package/src/components/ui/bento-grid2.tsx +66 -0
- package/src/components/ui/breadcrumb.tsx +101 -0
- package/src/components/ui/button.tsx +50 -0
- package/src/components/ui/card.tsx +55 -0
- package/src/components/ui/checkbox.tsx +26 -0
- package/src/components/ui/collapsible.tsx +9 -0
- package/src/components/ui/dialog.tsx +119 -0
- package/src/components/ui/dropdown-menu.tsx +186 -0
- package/src/components/ui/floating-navbar.tsx +78 -0
- package/src/components/ui/form.tsx +167 -0
- package/src/components/ui/image.tsx +55 -0
- package/src/components/ui/input.tsx +22 -0
- package/src/components/ui/label.tsx +19 -0
- package/src/components/ui/pagination.tsx +105 -0
- package/src/components/ui/progress.tsx +23 -0
- package/src/components/ui/resizable-navbar.tsx +260 -0
- package/src/components/ui/segment-control.tsx +143 -0
- package/src/components/ui/select.tsx +153 -0
- package/src/components/ui/separator.tsx +24 -0
- package/src/components/ui/sheet.tsx +121 -0
- package/src/components/ui/sidebar.tsx +736 -0
- package/src/components/ui/skeleton.tsx +7 -0
- package/src/components/ui/slider.tsx +23 -0
- package/src/components/ui/sonner.tsx +27 -0
- package/src/components/ui/spinner.tsx +45 -0
- package/src/components/ui/switch.tsx +27 -0
- package/src/components/ui/table.tsx +90 -0
- package/src/components/ui/tabs.tsx +52 -0
- package/src/components/ui/textarea.tsx +18 -0
- package/src/components/ui/timeline.tsx +95 -0
- package/src/components/ui/toast.tsx +126 -0
- package/src/components/ui/tooltip.tsx +55 -0
- package/src/components/ui/typewriter-effect.tsx +181 -0
- package/src/hooks/use-mobile.ts +19 -0
- package/src/hooks/useDialog.ts +25 -0
- package/src/icons/GoogleIcon.tsx +32 -0
- package/src/icons/LinkedInIcon.tsx +30 -0
- package/src/icons/MicrosoftIcon.tsx +21 -0
- package/src/lib/chatwoot.ts +51 -0
- package/src/lib/utils.ts +6 -0
- package/src/modules/app/components/AppLoader.tsx +9 -0
- package/src/modules/app/components/AppShell.tsx +21 -0
- package/src/modules/app/components/AppSidebar.tsx +26 -0
- package/src/modules/app/components/AppSidebarContent.tsx +73 -0
- package/src/modules/app/components/AppSidebarHeader.tsx +57 -0
- package/src/modules/app/components/AppSidebarInvites.tsx +32 -0
- package/src/modules/app/components/AppSidebarUser.tsx +128 -0
- package/src/modules/auth/components/AdminUserManagement.tsx +1136 -0
- package/src/modules/auth/components/AdminWaitlist.tsx +358 -0
- package/src/modules/auth/components/AuthLayout.tsx +13 -0
- package/src/modules/auth/components/AuthProviders.tsx +105 -0
- package/src/modules/auth/components/AuthRouter.tsx +29 -0
- package/src/modules/auth/components/ClaimAccountRoute.tsx +242 -0
- package/src/modules/auth/components/ErrorAuthRoute.tsx +121 -0
- package/src/modules/auth/components/ForgotPasswordForm.tsx +58 -0
- package/src/modules/auth/components/ForgotPasswordRoute.tsx +27 -0
- package/src/modules/auth/components/InviteFriends.tsx +273 -0
- package/src/modules/auth/components/LastUsedBadge.tsx +22 -0
- package/src/modules/auth/components/LoginForm.tsx +104 -0
- package/src/modules/auth/components/LoginRoute.tsx +31 -0
- package/src/modules/auth/components/LogoutRoute.tsx +21 -0
- package/src/modules/auth/components/OrganizationAcceptInvitationRoute.tsx +161 -0
- package/src/modules/auth/components/OrganizationMembersRoute.tsx +730 -0
- package/src/modules/auth/components/OrganizationSettingsRoute.tsx +280 -0
- package/src/modules/auth/components/OrganizationSwitcher.tsx +148 -0
- package/src/modules/auth/components/ProfileRoute.tsx +104 -0
- package/src/modules/auth/components/RangeNuqsDatePicker.tsx +365 -0
- package/src/modules/auth/components/ResetPasswordForm.tsx +103 -0
- package/src/modules/auth/components/ResetPasswordRoute.tsx +27 -0
- package/src/modules/auth/components/SignupFormRoute.tsx +189 -0
- package/src/modules/auth/components/SignupRoute.tsx +53 -0
- package/src/modules/auth/components/UserPreferences.tsx +144 -0
- package/src/modules/auth/components/WaitlistCard.tsx +78 -0
- package/src/modules/auth/components/WaitlistCodeValidation.tsx +79 -0
- package/src/modules/billing/components/BillingBetaPage.tsx +124 -0
- package/src/modules/billing/components/BillingInvoicePage.tsx +180 -0
- package/src/modules/billing/components/BillingPlanSelect.tsx +14 -0
- package/src/modules/billing/components/BillingRouter.tsx +20 -0
- package/src/modules/billing/components/BillingSinglePlanSelect.tsx +172 -0
- package/src/modules/table/components/ColumnOrderAndVisibility.tsx +127 -0
- package/src/modules/table/components/NuqsTable.tsx +396 -0
- package/src/modules/table/components/TableFiltering.tsx +520 -0
- package/src/modules/table/components/TablePagination.tsx +59 -0
- package/src/modules/table/components/table.types.ts +11 -0
- package/src/modules/table/filterTransformers.ts +323 -0
- package/src/types.ts +4 -0
- package/src/vite-env.d.ts +1 -0
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DateRangePicker,
|
|
3
|
+
type DateValue,
|
|
4
|
+
type RangeValue,
|
|
5
|
+
Select,
|
|
6
|
+
SelectItem,
|
|
7
|
+
} from "@heroui/react";
|
|
8
|
+
import { CalendarDate, getLocalTimeZone, today } from "@internationalized/date";
|
|
9
|
+
import type { QueryFilters } from "@m5kdev/commons/modules/schemas/query.schema";
|
|
10
|
+
import { useNuqsQueryParams } from "@m5kdev/frontend/modules/table/hooks/useNuqsQueryParams";
|
|
11
|
+
import {
|
|
12
|
+
calendarDateToEndOfDayUTC,
|
|
13
|
+
calendarDateToUTC,
|
|
14
|
+
dateFilterToRangeValue,
|
|
15
|
+
} from "@m5kdev/web-ui/modules/table/filterTransformers";
|
|
16
|
+
import { DateTime } from "luxon";
|
|
17
|
+
import { useEffect, useMemo, useState } from "react";
|
|
18
|
+
import { useTranslation } from "react-i18next";
|
|
19
|
+
|
|
20
|
+
type QuickRangeKey =
|
|
21
|
+
| "today"
|
|
22
|
+
| "yesterday"
|
|
23
|
+
| "last7"
|
|
24
|
+
| "last30"
|
|
25
|
+
| "thisWeek"
|
|
26
|
+
| "lastWeek"
|
|
27
|
+
| "thisMonth"
|
|
28
|
+
| "lastMonth"
|
|
29
|
+
| "thisYear"
|
|
30
|
+
| "lastYear";
|
|
31
|
+
|
|
32
|
+
const toCalendarDate = (dt: DateTime): CalendarDate => new CalendarDate(dt.year, dt.month, dt.day);
|
|
33
|
+
|
|
34
|
+
const isSameCalendarDate = (a: CalendarDate, b: CalendarDate) =>
|
|
35
|
+
a.year === b.year && a.month === b.month && a.day === b.day;
|
|
36
|
+
|
|
37
|
+
export interface RangeNuqsDatePickerProps {
|
|
38
|
+
/**
|
|
39
|
+
* The column ID to use for the date filter (e.g., "startedAt")
|
|
40
|
+
*/
|
|
41
|
+
columnId?: string;
|
|
42
|
+
/**
|
|
43
|
+
* The end column ID for range filters (e.g., "endedAt")
|
|
44
|
+
*/
|
|
45
|
+
endColumnId?: string;
|
|
46
|
+
/**
|
|
47
|
+
* Label for the date range picker
|
|
48
|
+
*/
|
|
49
|
+
dateRangeLabel?: string;
|
|
50
|
+
/**
|
|
51
|
+
* Label for the quick range selector
|
|
52
|
+
*/
|
|
53
|
+
quickRangeLabel?: string;
|
|
54
|
+
/**
|
|
55
|
+
* Translation namespace for i18n
|
|
56
|
+
*/
|
|
57
|
+
translationNamespace?: string;
|
|
58
|
+
/**
|
|
59
|
+
* Whether to show quick range selector
|
|
60
|
+
*/
|
|
61
|
+
showQuickRange?: boolean;
|
|
62
|
+
/**
|
|
63
|
+
* Custom className for the container
|
|
64
|
+
*/
|
|
65
|
+
className?: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export const RangeNuqsDatePicker = ({
|
|
69
|
+
columnId = "startedAt",
|
|
70
|
+
endColumnId = "endedAt",
|
|
71
|
+
dateRangeLabel,
|
|
72
|
+
quickRangeLabel,
|
|
73
|
+
translationNamespace = "horecaai-app",
|
|
74
|
+
showQuickRange = true,
|
|
75
|
+
className,
|
|
76
|
+
}: RangeNuqsDatePickerProps) => {
|
|
77
|
+
const { t } = useTranslation();
|
|
78
|
+
const { filters, setFilters } = useNuqsQueryParams();
|
|
79
|
+
|
|
80
|
+
const [quickRange, setQuickRange] = useState<QuickRangeKey | null>(null);
|
|
81
|
+
const [hasInitialized, setHasInitialized] = useState(false);
|
|
82
|
+
|
|
83
|
+
// Extract date range from filters using shared helper
|
|
84
|
+
const dateRange = useMemo(() => {
|
|
85
|
+
return dateFilterToRangeValue(filters, columnId);
|
|
86
|
+
}, [filters, columnId]);
|
|
87
|
+
|
|
88
|
+
const todayIso = DateTime.utc().toISODate();
|
|
89
|
+
|
|
90
|
+
const quickRangeOptions = useMemo(() => {
|
|
91
|
+
const now = DateTime.utc();
|
|
92
|
+
const todayStart = now.startOf("day");
|
|
93
|
+
const weekStart = todayStart.startOf("week");
|
|
94
|
+
const monthStart = todayStart.startOf("month");
|
|
95
|
+
const lastWeekStart = weekStart.minus({ weeks: 1 });
|
|
96
|
+
const lastMonthStart = monthStart.minus({ months: 1 });
|
|
97
|
+
const yearStart = todayStart.startOf("year");
|
|
98
|
+
const lastYearStart = yearStart.minus({ years: 1 });
|
|
99
|
+
|
|
100
|
+
const options: { key: QuickRangeKey; label: string; start: DateTime; end: DateTime }[] = [
|
|
101
|
+
{
|
|
102
|
+
key: "today",
|
|
103
|
+
label: t(`${translationNamespace}:reports.quickRange.today`, { defaultValue: "Today" }),
|
|
104
|
+
start: todayStart,
|
|
105
|
+
end: todayStart,
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
key: "yesterday",
|
|
109
|
+
label: t(`${translationNamespace}:reports.quickRange.yesterday`, {
|
|
110
|
+
defaultValue: "Yesterday",
|
|
111
|
+
}),
|
|
112
|
+
start: todayStart.minus({ days: 1 }),
|
|
113
|
+
end: todayStart.minus({ days: 1 }),
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
key: "last7",
|
|
117
|
+
label: t(`${translationNamespace}:reports.quickRange.last7`, {
|
|
118
|
+
defaultValue: "Last 7 days",
|
|
119
|
+
}),
|
|
120
|
+
start: todayStart.minus({ days: 6 }),
|
|
121
|
+
end: todayStart,
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
key: "last30",
|
|
125
|
+
label: t(`${translationNamespace}:reports.quickRange.last30`, {
|
|
126
|
+
defaultValue: "Last 30 days",
|
|
127
|
+
}),
|
|
128
|
+
start: todayStart.minus({ days: 29 }),
|
|
129
|
+
end: todayStart,
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
key: "thisWeek",
|
|
133
|
+
label: t(`${translationNamespace}:reports.quickRange.thisWeek`, {
|
|
134
|
+
defaultValue: "This week",
|
|
135
|
+
}),
|
|
136
|
+
start: weekStart,
|
|
137
|
+
end: todayStart,
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
key: "lastWeek",
|
|
141
|
+
label: t(`${translationNamespace}:reports.quickRange.lastWeek`, {
|
|
142
|
+
defaultValue: "Last week",
|
|
143
|
+
}),
|
|
144
|
+
start: lastWeekStart,
|
|
145
|
+
end: lastWeekStart.plus({ days: 6 }),
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
key: "thisMonth",
|
|
149
|
+
label: t(`${translationNamespace}:reports.quickRange.thisMonth`, {
|
|
150
|
+
defaultValue: "This month",
|
|
151
|
+
}),
|
|
152
|
+
start: monthStart,
|
|
153
|
+
end: todayStart,
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
key: "lastMonth",
|
|
157
|
+
label: t(`${translationNamespace}:reports.quickRange.lastMonth`, {
|
|
158
|
+
defaultValue: "Last month",
|
|
159
|
+
}),
|
|
160
|
+
start: lastMonthStart,
|
|
161
|
+
end: lastMonthStart.endOf("month"),
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
key: "thisYear",
|
|
165
|
+
label: t(`${translationNamespace}:reports.quickRange.thisYear`, {
|
|
166
|
+
defaultValue: "This year",
|
|
167
|
+
}),
|
|
168
|
+
start: yearStart,
|
|
169
|
+
end: todayStart,
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
key: "lastYear",
|
|
173
|
+
label: t(`${translationNamespace}:reports.quickRange.lastYear`, {
|
|
174
|
+
defaultValue: "Last year",
|
|
175
|
+
}),
|
|
176
|
+
start: lastYearStart,
|
|
177
|
+
end: lastYearStart.endOf("year"),
|
|
178
|
+
},
|
|
179
|
+
];
|
|
180
|
+
|
|
181
|
+
return options.map((opt) => ({
|
|
182
|
+
key: opt.key,
|
|
183
|
+
label: opt.label,
|
|
184
|
+
range: {
|
|
185
|
+
start: toCalendarDate(opt.start),
|
|
186
|
+
end: toCalendarDate(opt.end),
|
|
187
|
+
},
|
|
188
|
+
}));
|
|
189
|
+
}, [t, todayIso, translationNamespace]);
|
|
190
|
+
|
|
191
|
+
// Set default to "this month" on initial load if no date range is set
|
|
192
|
+
useEffect(() => {
|
|
193
|
+
if (hasInitialized || !setFilters) return;
|
|
194
|
+
|
|
195
|
+
// Check if there's already a valid date range from URL
|
|
196
|
+
if (dateRange) {
|
|
197
|
+
setHasInitialized(true);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// No date range set, initialize to "this month"
|
|
202
|
+
const thisMonthOption = quickRangeOptions.find((opt) => opt.key === "thisMonth");
|
|
203
|
+
if (thisMonthOption) {
|
|
204
|
+
setHasInitialized(true);
|
|
205
|
+
setQuickRange("thisMonth");
|
|
206
|
+
|
|
207
|
+
// Set filters directly for "this month" range
|
|
208
|
+
const startDate = thisMonthOption.range.start;
|
|
209
|
+
const endDate = thisMonthOption.range.end;
|
|
210
|
+
|
|
211
|
+
const newFilters: QueryFilters = [
|
|
212
|
+
...(filters?.filter((f) => f.columnId !== columnId && f.columnId !== endColumnId) ?? []),
|
|
213
|
+
{
|
|
214
|
+
columnId,
|
|
215
|
+
type: "date" as const,
|
|
216
|
+
method: "intersect" as const,
|
|
217
|
+
value: calendarDateToUTC(startDate),
|
|
218
|
+
valueTo: calendarDateToEndOfDayUTC(endDate),
|
|
219
|
+
endColumnId,
|
|
220
|
+
},
|
|
221
|
+
];
|
|
222
|
+
|
|
223
|
+
setFilters(newFilters);
|
|
224
|
+
}
|
|
225
|
+
}, [dateRange, columnId, endColumnId, quickRangeOptions, hasInitialized, setFilters]);
|
|
226
|
+
|
|
227
|
+
useEffect(() => {
|
|
228
|
+
// Don't sync quickRange if we're still initializing
|
|
229
|
+
if (!hasInitialized) return;
|
|
230
|
+
|
|
231
|
+
if (!dateRange?.start || !dateRange?.end) {
|
|
232
|
+
setQuickRange(null);
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const start = dateRange.start as unknown as CalendarDate;
|
|
237
|
+
const end = dateRange.end as unknown as CalendarDate;
|
|
238
|
+
|
|
239
|
+
const matches = quickRangeOptions.filter(
|
|
240
|
+
(option) =>
|
|
241
|
+
isSameCalendarDate(option.range.start, start) && isSameCalendarDate(option.range.end, end)
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
if (matches.length === 0) {
|
|
245
|
+
setQuickRange(null);
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// If the current quick range still matches, keep it
|
|
250
|
+
const currentStillMatches = quickRange ? matches.find((m) => m.key === quickRange) : null;
|
|
251
|
+
if (currentStillMatches) {
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Otherwise pick the highest-priority match to keep intent (week/month over last7 overlap)
|
|
256
|
+
const priority: QuickRangeKey[] = [
|
|
257
|
+
"today",
|
|
258
|
+
"yesterday",
|
|
259
|
+
"thisWeek",
|
|
260
|
+
"lastWeek",
|
|
261
|
+
"thisMonth",
|
|
262
|
+
"lastMonth",
|
|
263
|
+
"last7",
|
|
264
|
+
"last30",
|
|
265
|
+
];
|
|
266
|
+
const preferred =
|
|
267
|
+
priority.map((key) => matches.find((m) => m.key === key)).find((m) => Boolean(m)) ??
|
|
268
|
+
matches[0];
|
|
269
|
+
|
|
270
|
+
setQuickRange(preferred?.key ?? null);
|
|
271
|
+
}, [dateRange, quickRangeOptions, quickRange, hasInitialized]);
|
|
272
|
+
|
|
273
|
+
// Handle date range changes
|
|
274
|
+
const handleDateRangeChange = (range: RangeValue<DateValue> | null) => {
|
|
275
|
+
if (!setFilters) return;
|
|
276
|
+
|
|
277
|
+
if (!range?.start || !range?.end) {
|
|
278
|
+
// Remove date filters
|
|
279
|
+
const newFilters =
|
|
280
|
+
filters?.filter((f) => f.columnId !== columnId && f.columnId !== endColumnId) ?? [];
|
|
281
|
+
setFilters(newFilters);
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Use intersect to find records where [startedAt, endedAt] overlaps with the selected range
|
|
286
|
+
// This includes ongoing records (endedAt = NULL) and records that overlap the range
|
|
287
|
+
const startDate = range.start as unknown as CalendarDate;
|
|
288
|
+
const endDate = range.end as unknown as CalendarDate;
|
|
289
|
+
|
|
290
|
+
const newFilters: QueryFilters = [
|
|
291
|
+
...(filters?.filter((f) => f.columnId !== columnId && f.columnId !== endColumnId) ?? []),
|
|
292
|
+
{
|
|
293
|
+
columnId,
|
|
294
|
+
type: "date" as const,
|
|
295
|
+
method: "intersect" as const,
|
|
296
|
+
value: calendarDateToUTC(startDate),
|
|
297
|
+
valueTo: calendarDateToEndOfDayUTC(endDate),
|
|
298
|
+
endColumnId,
|
|
299
|
+
},
|
|
300
|
+
];
|
|
301
|
+
|
|
302
|
+
setFilters(newFilters);
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
const handleQuickRangeChange = (key: QuickRangeKey | null) => {
|
|
306
|
+
if (!key) {
|
|
307
|
+
setQuickRange(null);
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const option = quickRangeOptions.find((opt) => opt.key === key);
|
|
312
|
+
if (!option) return;
|
|
313
|
+
|
|
314
|
+
setQuickRange(key);
|
|
315
|
+
handleDateRangeChange({
|
|
316
|
+
start: option.range.start as unknown as DateValue,
|
|
317
|
+
end: option.range.end as unknown as DateValue,
|
|
318
|
+
});
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
return (
|
|
322
|
+
<div className={`flex gap-4 flex-row ${className ?? ""}`}>
|
|
323
|
+
<div className="flex flex-col gap-2">
|
|
324
|
+
<span className="text-sm text-muted-foreground">
|
|
325
|
+
{dateRangeLabel ??
|
|
326
|
+
t(`${translationNamespace}:reports.dateRange`, { defaultValue: "Date range" })}
|
|
327
|
+
</span>
|
|
328
|
+
<DateRangePicker
|
|
329
|
+
value={(dateRange as any) ?? undefined}
|
|
330
|
+
onChange={handleDateRangeChange}
|
|
331
|
+
className="w-[300px]"
|
|
332
|
+
granularity="day"
|
|
333
|
+
showMonthAndYearPickers
|
|
334
|
+
maxValue={today(getLocalTimeZone()) as unknown as DateValue}
|
|
335
|
+
popoverProps={{
|
|
336
|
+
portalContainer: document.body,
|
|
337
|
+
disableAnimation: true,
|
|
338
|
+
}}
|
|
339
|
+
/>
|
|
340
|
+
</div>
|
|
341
|
+
{showQuickRange && (
|
|
342
|
+
<div className="flex flex-col gap-2">
|
|
343
|
+
<span className="text-sm text-muted-foreground">
|
|
344
|
+
{quickRangeLabel ??
|
|
345
|
+
t(`${translationNamespace}:reports.quickRange.label`, {
|
|
346
|
+
defaultValue: "Quick range",
|
|
347
|
+
})}
|
|
348
|
+
</span>
|
|
349
|
+
<Select
|
|
350
|
+
selectedKeys={quickRange ? [quickRange] : []}
|
|
351
|
+
onChange={(e) => handleQuickRangeChange((e.target.value as QuickRangeKey) ?? null)}
|
|
352
|
+
className="w-[300px]"
|
|
353
|
+
popoverProps={{
|
|
354
|
+
portalContainer: document.body,
|
|
355
|
+
}}
|
|
356
|
+
>
|
|
357
|
+
{quickRangeOptions.map((option) => (
|
|
358
|
+
<SelectItem key={option.key}>{option.label}</SelectItem>
|
|
359
|
+
))}
|
|
360
|
+
</Select>
|
|
361
|
+
</div>
|
|
362
|
+
)}
|
|
363
|
+
</div>
|
|
364
|
+
);
|
|
365
|
+
};
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { Button, Card, CardBody, CardHeader, Input } from "@heroui/react";
|
|
2
|
+
import { authClient } from "@m5kdev/frontend/modules/auth/auth.lib";
|
|
3
|
+
import { type SubmitHandler, useForm } from "react-hook-form";
|
|
4
|
+
import { useTranslation } from "react-i18next";
|
|
5
|
+
import { Link, useSearchParams } from "react-router";
|
|
6
|
+
import { toast } from "sonner";
|
|
7
|
+
|
|
8
|
+
type Inputs = {
|
|
9
|
+
password: string;
|
|
10
|
+
confirmPassword: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export function ResetPasswordForm() {
|
|
14
|
+
const { t } = useTranslation();
|
|
15
|
+
const {
|
|
16
|
+
register,
|
|
17
|
+
handleSubmit,
|
|
18
|
+
setError,
|
|
19
|
+
formState: { errors },
|
|
20
|
+
} = useForm<Inputs>();
|
|
21
|
+
const [searchParams] = useSearchParams();
|
|
22
|
+
const token = searchParams.get("token");
|
|
23
|
+
|
|
24
|
+
const onSubmit: SubmitHandler<Inputs> = (data) => {
|
|
25
|
+
if (data.password !== data.confirmPassword) {
|
|
26
|
+
setError("confirmPassword", { message: t("web-ui:auth.resetPassword.passwordMismatch") });
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (!token) {
|
|
31
|
+
throw new Error(t("web-ui:auth.resetPassword.tokenRequired"));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
authClient
|
|
35
|
+
.resetPassword({
|
|
36
|
+
newPassword: data.password,
|
|
37
|
+
token,
|
|
38
|
+
})
|
|
39
|
+
.then(() => {
|
|
40
|
+
toast.success(t("web-ui:auth.resetPassword.success"));
|
|
41
|
+
// Optionally, redirect
|
|
42
|
+
})
|
|
43
|
+
.catch(() => {
|
|
44
|
+
toast.error(t("web-ui:auth.resetPassword.error"));
|
|
45
|
+
});
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<div className="flex flex-col gap-6">
|
|
50
|
+
<Card>
|
|
51
|
+
<CardHeader className="text-center flex flex-col gap-1">
|
|
52
|
+
<h2 className="text-xl font-semibold">{t("web-ui:auth.resetPassword.title")}</h2>
|
|
53
|
+
<p className="text-sm text-default-600">{t("web-ui:auth.resetPassword.description")}</p>
|
|
54
|
+
</CardHeader>
|
|
55
|
+
<CardBody className="gap-6">
|
|
56
|
+
<form onSubmit={handleSubmit(onSubmit)} className="grid gap-6">
|
|
57
|
+
<div className="grid gap-2">
|
|
58
|
+
<Input
|
|
59
|
+
labelPlacement="outside"
|
|
60
|
+
label={t("web-ui:auth.resetPassword.newPassword")}
|
|
61
|
+
placeholder={t("web-ui:auth.resetPassword.newPassword")}
|
|
62
|
+
type="password"
|
|
63
|
+
variant="bordered"
|
|
64
|
+
isRequired
|
|
65
|
+
{...register("password", {
|
|
66
|
+
required: t("web-ui:auth.resetPassword.passwordRequired"),
|
|
67
|
+
})}
|
|
68
|
+
/>
|
|
69
|
+
{errors.password && (
|
|
70
|
+
<span className="text-red-500 text-xs">{errors.password.message}</span>
|
|
71
|
+
)}
|
|
72
|
+
</div>
|
|
73
|
+
<div className="grid gap-2">
|
|
74
|
+
<Input
|
|
75
|
+
labelPlacement="outside"
|
|
76
|
+
label={t("web-ui:auth.resetPassword.confirmPassword")}
|
|
77
|
+
placeholder={t("web-ui:auth.resetPassword.confirmPassword")}
|
|
78
|
+
type="password"
|
|
79
|
+
variant="bordered"
|
|
80
|
+
isRequired
|
|
81
|
+
{...register("confirmPassword", {
|
|
82
|
+
required: t("web-ui:auth.resetPassword.confirmPasswordRequired"),
|
|
83
|
+
})}
|
|
84
|
+
/>
|
|
85
|
+
{errors.confirmPassword && (
|
|
86
|
+
<span className="text-red-500 text-xs">{errors.confirmPassword.message}</span>
|
|
87
|
+
)}
|
|
88
|
+
</div>
|
|
89
|
+
<Button type="submit" className="w-full" color="primary">
|
|
90
|
+
{t("web-ui:auth.resetPassword.button")}
|
|
91
|
+
</Button>
|
|
92
|
+
</form>
|
|
93
|
+
</CardBody>
|
|
94
|
+
</Card>
|
|
95
|
+
<div className="text-center text-xs text-muted-foreground">
|
|
96
|
+
{t("web-ui:auth.forgotPassword.rememberPassword")}{" "}
|
|
97
|
+
<Link to="/login" className="underline underline-offset-4 hover:text-primary">
|
|
98
|
+
{t("web-ui:auth.login.button")}
|
|
99
|
+
</Link>
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Card, CardBody, CardHeader } from "@heroui/react";
|
|
2
|
+
import { useTranslation } from "react-i18next";
|
|
3
|
+
import { Link } from "react-router";
|
|
4
|
+
import { ResetPasswordForm } from "#modules/auth/components/ResetPasswordForm";
|
|
5
|
+
|
|
6
|
+
export function ResetPasswordRoute() {
|
|
7
|
+
const { t } = useTranslation();
|
|
8
|
+
return (
|
|
9
|
+
<div className="flex flex-col gap-6">
|
|
10
|
+
<Card>
|
|
11
|
+
<CardHeader className="text-center flex flex-col gap-1">
|
|
12
|
+
<h2 className="text-xl font-semibold">{t("web-ui:auth.resetPassword.title")}</h2>
|
|
13
|
+
<p className="text-sm text-default-600">{t("web-ui:auth.resetPassword.description")}</p>
|
|
14
|
+
</CardHeader>
|
|
15
|
+
<CardBody>
|
|
16
|
+
<ResetPasswordForm />
|
|
17
|
+
</CardBody>
|
|
18
|
+
</Card>
|
|
19
|
+
<div className="text-center text-xs text-muted-foreground">
|
|
20
|
+
{t("web-ui:auth.forgotPassword.rememberPassword")}{" "}
|
|
21
|
+
<Link to="/login" className="underline underline-offset-4 hover:text-primary">
|
|
22
|
+
{t("web-ui:auth.login.button")}
|
|
23
|
+
</Link>
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { Alert, Button, Input } from "@heroui/react";
|
|
2
|
+
import { authClient } from "@m5kdev/frontend/modules/auth/auth.lib";
|
|
3
|
+
import { useSession } from "@m5kdev/frontend/modules/auth/hooks/useSession";
|
|
4
|
+
import { useState } from "react";
|
|
5
|
+
import { type SubmitHandler, useForm } from "react-hook-form";
|
|
6
|
+
import { useTranslation } from "react-i18next";
|
|
7
|
+
import { useNavigate } from "react-router";
|
|
8
|
+
import { toast } from "sonner";
|
|
9
|
+
|
|
10
|
+
type Inputs = {
|
|
11
|
+
email: string;
|
|
12
|
+
password: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
function getEmailProviderUrl(email: string): string | null {
|
|
16
|
+
const domain = email.split("@")[1]?.toLowerCase();
|
|
17
|
+
if (!domain) return null;
|
|
18
|
+
|
|
19
|
+
const EMAIL_URLS = {
|
|
20
|
+
GMAIL: "https://mail.google.com/mail/u/0/#inbox",
|
|
21
|
+
OUTLOOK: "https://outlook.live.com/mail/0/inbox",
|
|
22
|
+
YAHOO: "https://mail.yahoo.com",
|
|
23
|
+
ICLOUD: "https://www.icloud.com/mail",
|
|
24
|
+
PROTON: "https://mail.proton.me",
|
|
25
|
+
} as const;
|
|
26
|
+
|
|
27
|
+
const domainMap: Record<string, string> = {
|
|
28
|
+
"gmail.com": EMAIL_URLS.GMAIL,
|
|
29
|
+
"googlemail.com": EMAIL_URLS.GMAIL,
|
|
30
|
+
"outlook.com": EMAIL_URLS.OUTLOOK,
|
|
31
|
+
"hotmail.com": EMAIL_URLS.OUTLOOK,
|
|
32
|
+
"live.com": EMAIL_URLS.OUTLOOK,
|
|
33
|
+
"msn.com": EMAIL_URLS.OUTLOOK,
|
|
34
|
+
"yahoo.com": EMAIL_URLS.YAHOO,
|
|
35
|
+
"yahoo.co.uk": EMAIL_URLS.YAHOO,
|
|
36
|
+
"yahoo.fr": EMAIL_URLS.YAHOO,
|
|
37
|
+
"ymail.com": EMAIL_URLS.YAHOO,
|
|
38
|
+
"icloud.com": EMAIL_URLS.ICLOUD,
|
|
39
|
+
"me.com": EMAIL_URLS.ICLOUD,
|
|
40
|
+
"mac.com": EMAIL_URLS.ICLOUD,
|
|
41
|
+
"protonmail.com": EMAIL_URLS.PROTON,
|
|
42
|
+
"proton.me": EMAIL_URLS.PROTON,
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
return domainMap[domain] || null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function SignupForm({
|
|
49
|
+
code,
|
|
50
|
+
email,
|
|
51
|
+
waitlist,
|
|
52
|
+
}: {
|
|
53
|
+
code?: string | null;
|
|
54
|
+
email?: string | null;
|
|
55
|
+
waitlist?: boolean;
|
|
56
|
+
}) {
|
|
57
|
+
const { t } = useTranslation();
|
|
58
|
+
const [status, setStatus] = useState("start");
|
|
59
|
+
const [userEmail, setUserEmail] = useState<string>(email || "");
|
|
60
|
+
const { registerSession } = useSession();
|
|
61
|
+
const navigate = useNavigate();
|
|
62
|
+
const {
|
|
63
|
+
register,
|
|
64
|
+
handleSubmit,
|
|
65
|
+
formState: { errors },
|
|
66
|
+
} = useForm<Inputs>();
|
|
67
|
+
|
|
68
|
+
const onSubmit: SubmitHandler<Inputs> = (data) => {
|
|
69
|
+
console.log(data);
|
|
70
|
+
setStatus("busy");
|
|
71
|
+
setUserEmail(data.email);
|
|
72
|
+
|
|
73
|
+
authClient.signUp
|
|
74
|
+
.email(
|
|
75
|
+
{
|
|
76
|
+
name: data.email,
|
|
77
|
+
email: data.email,
|
|
78
|
+
password: data.password,
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
headers: {
|
|
82
|
+
"Waitlist-Invitation-Code": code || "",
|
|
83
|
+
},
|
|
84
|
+
}
|
|
85
|
+
)
|
|
86
|
+
.then((result) => {
|
|
87
|
+
console.log(result);
|
|
88
|
+
if (result.error) {
|
|
89
|
+
toast.error(t("web-ui:auth.errors.invitationCodeInvalid"));
|
|
90
|
+
setStatus("start");
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (waitlist) {
|
|
94
|
+
authClient.signIn
|
|
95
|
+
.email({
|
|
96
|
+
email: data.email,
|
|
97
|
+
password: data.password,
|
|
98
|
+
})
|
|
99
|
+
.then((res) => {
|
|
100
|
+
console.log(res);
|
|
101
|
+
if (res.data?.user) {
|
|
102
|
+
registerSession(() => {
|
|
103
|
+
navigate("/");
|
|
104
|
+
});
|
|
105
|
+
} else if (res.error) {
|
|
106
|
+
toast.error(t("web-ui:auth.errors.authentication"), {
|
|
107
|
+
description: res.error.message,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
})
|
|
111
|
+
.catch((error) => {
|
|
112
|
+
toast.error(t("web-ui:auth.errors.server"), {
|
|
113
|
+
description: error.message,
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
} else setStatus("done");
|
|
117
|
+
})
|
|
118
|
+
.catch((error) => {
|
|
119
|
+
toast.error(error.message);
|
|
120
|
+
setStatus("start");
|
|
121
|
+
});
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
if (status === "done") {
|
|
125
|
+
const emailProviderUrl = userEmail ? getEmailProviderUrl(userEmail) : null;
|
|
126
|
+
|
|
127
|
+
return (
|
|
128
|
+
<Alert
|
|
129
|
+
color="primary"
|
|
130
|
+
variant="faded"
|
|
131
|
+
title={t("web-ui:auth.signup.verificationEmailSent.title")}
|
|
132
|
+
>
|
|
133
|
+
<div className="mt-2">
|
|
134
|
+
{t("web-ui:auth.signup.verificationEmailSent.description")}
|
|
135
|
+
{emailProviderUrl && (
|
|
136
|
+
<div className="mt-3">
|
|
137
|
+
<a
|
|
138
|
+
href={emailProviderUrl}
|
|
139
|
+
target="_blank"
|
|
140
|
+
rel="noopener noreferrer"
|
|
141
|
+
className="font-medium underline underline-offset-4 hover:opacity-80 transition-opacity"
|
|
142
|
+
>
|
|
143
|
+
{t("web-ui:auth.signup.verificationEmailSent.openEmail")}
|
|
144
|
+
</a>
|
|
145
|
+
</div>
|
|
146
|
+
)}
|
|
147
|
+
</div>
|
|
148
|
+
</Alert>
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return (
|
|
153
|
+
<form onSubmit={handleSubmit(onSubmit)} className="grid gap-6">
|
|
154
|
+
<div className="grid gap-2">
|
|
155
|
+
<Input
|
|
156
|
+
labelPlacement="outside"
|
|
157
|
+
label={t("web-ui:auth.login.email")}
|
|
158
|
+
type="email"
|
|
159
|
+
placeholder={t("web-ui:auth.login.placeholder.email")}
|
|
160
|
+
variant="bordered"
|
|
161
|
+
isRequired
|
|
162
|
+
isDisabled={!!email}
|
|
163
|
+
defaultValue={email ?? ""}
|
|
164
|
+
{...register("email", { required: true })}
|
|
165
|
+
/>
|
|
166
|
+
{errors.email && (
|
|
167
|
+
<span className="text-red-500 text-xs">{t("web-ui:auth.signup.emailRequired")}</span>
|
|
168
|
+
)}
|
|
169
|
+
</div>
|
|
170
|
+
<div className="grid gap-2">
|
|
171
|
+
<Input
|
|
172
|
+
labelPlacement="outside"
|
|
173
|
+
label={t("web-ui:auth.login.password")}
|
|
174
|
+
placeholder={t("web-ui:auth.login.password")}
|
|
175
|
+
type="password"
|
|
176
|
+
variant="bordered"
|
|
177
|
+
isRequired
|
|
178
|
+
{...register("password", { required: true })}
|
|
179
|
+
/>
|
|
180
|
+
{errors.password && (
|
|
181
|
+
<span className="text-red-500 text-xs">{t("web-ui:auth.signup.passwordRequired")}</span>
|
|
182
|
+
)}
|
|
183
|
+
</div>
|
|
184
|
+
<Button type="submit" className="w-full" color="primary" isDisabled={status === "busy"}>
|
|
185
|
+
{status === "busy" ? t("web-ui:auth.signup.signingUp") : t("web-ui:auth.signup.button")}
|
|
186
|
+
</Button>
|
|
187
|
+
</form>
|
|
188
|
+
);
|
|
189
|
+
}
|