@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.
Files changed (127) hide show
  1. package/LICENSE +621 -0
  2. package/README.md +17 -0
  3. package/package.json +169 -0
  4. package/src/animations/card.motion.ts +9 -0
  5. package/src/components/AvatarUpload.tsx +133 -0
  6. package/src/components/Button.tsx +14 -0
  7. package/src/components/Calendar.css +684 -0
  8. package/src/components/Calendar.tsx +32 -0
  9. package/src/components/CardsSelect.tsx +155 -0
  10. package/src/components/CollapsibleSidebarMenuItem.tsx +57 -0
  11. package/src/components/ColorPicker.tsx +56 -0
  12. package/src/components/CopyButton.tsx +45 -0
  13. package/src/components/CropDialog.tsx +154 -0
  14. package/src/components/DialogProvider.tsx +105 -0
  15. package/src/components/ErrorFallback.tsx +17 -0
  16. package/src/components/FileDropzone.tsx +120 -0
  17. package/src/components/MultiSelectDropdown.tsx +233 -0
  18. package/src/components/Orb.tsx +288 -0
  19. package/src/components/PageAlert.tsx +121 -0
  20. package/src/components/SelectChips.tsx +40 -0
  21. package/src/components/SidebarItem.tsx +26 -0
  22. package/src/components/Steps.tsx +340 -0
  23. package/src/components/TablerIconPicker.tsx +4260 -0
  24. package/src/components/app-header.tsx +40 -0
  25. package/src/components/blur-card.tsx +132 -0
  26. package/src/components/features-section-demo-1.tsx +127 -0
  27. package/src/components/features-section-demo-2.tsx +102 -0
  28. package/src/components/features-section-demo-3.tsx +272 -0
  29. package/src/components/mode-toggle.tsx +31 -0
  30. package/src/components/nav-main.tsx +69 -0
  31. package/src/components/pricing-cards.tsx +133 -0
  32. package/src/components/shared/ButtonCopy.tsx +50 -0
  33. package/src/components/team-switcher.tsx +83 -0
  34. package/src/components/theme-provider.tsx +74 -0
  35. package/src/components/typewriter.tsx +90 -0
  36. package/src/components/ui/alert-dialog.tsx +133 -0
  37. package/src/components/ui/alert.tsx +60 -0
  38. package/src/components/ui/avatar.tsx +47 -0
  39. package/src/components/ui/badge.tsx +33 -0
  40. package/src/components/ui/bento-grid.tsx +54 -0
  41. package/src/components/ui/bento-grid2.tsx +66 -0
  42. package/src/components/ui/breadcrumb.tsx +101 -0
  43. package/src/components/ui/button.tsx +50 -0
  44. package/src/components/ui/card.tsx +55 -0
  45. package/src/components/ui/checkbox.tsx +26 -0
  46. package/src/components/ui/collapsible.tsx +9 -0
  47. package/src/components/ui/dialog.tsx +119 -0
  48. package/src/components/ui/dropdown-menu.tsx +186 -0
  49. package/src/components/ui/floating-navbar.tsx +78 -0
  50. package/src/components/ui/form.tsx +167 -0
  51. package/src/components/ui/image.tsx +55 -0
  52. package/src/components/ui/input.tsx +22 -0
  53. package/src/components/ui/label.tsx +19 -0
  54. package/src/components/ui/pagination.tsx +105 -0
  55. package/src/components/ui/progress.tsx +23 -0
  56. package/src/components/ui/resizable-navbar.tsx +260 -0
  57. package/src/components/ui/segment-control.tsx +143 -0
  58. package/src/components/ui/select.tsx +153 -0
  59. package/src/components/ui/separator.tsx +24 -0
  60. package/src/components/ui/sheet.tsx +121 -0
  61. package/src/components/ui/sidebar.tsx +736 -0
  62. package/src/components/ui/skeleton.tsx +7 -0
  63. package/src/components/ui/slider.tsx +23 -0
  64. package/src/components/ui/sonner.tsx +27 -0
  65. package/src/components/ui/spinner.tsx +45 -0
  66. package/src/components/ui/switch.tsx +27 -0
  67. package/src/components/ui/table.tsx +90 -0
  68. package/src/components/ui/tabs.tsx +52 -0
  69. package/src/components/ui/textarea.tsx +18 -0
  70. package/src/components/ui/timeline.tsx +95 -0
  71. package/src/components/ui/toast.tsx +126 -0
  72. package/src/components/ui/tooltip.tsx +55 -0
  73. package/src/components/ui/typewriter-effect.tsx +181 -0
  74. package/src/hooks/use-mobile.ts +19 -0
  75. package/src/hooks/useDialog.ts +25 -0
  76. package/src/icons/GoogleIcon.tsx +32 -0
  77. package/src/icons/LinkedInIcon.tsx +30 -0
  78. package/src/icons/MicrosoftIcon.tsx +21 -0
  79. package/src/lib/chatwoot.ts +51 -0
  80. package/src/lib/utils.ts +6 -0
  81. package/src/modules/app/components/AppLoader.tsx +9 -0
  82. package/src/modules/app/components/AppShell.tsx +21 -0
  83. package/src/modules/app/components/AppSidebar.tsx +26 -0
  84. package/src/modules/app/components/AppSidebarContent.tsx +73 -0
  85. package/src/modules/app/components/AppSidebarHeader.tsx +57 -0
  86. package/src/modules/app/components/AppSidebarInvites.tsx +32 -0
  87. package/src/modules/app/components/AppSidebarUser.tsx +128 -0
  88. package/src/modules/auth/components/AdminUserManagement.tsx +1136 -0
  89. package/src/modules/auth/components/AdminWaitlist.tsx +358 -0
  90. package/src/modules/auth/components/AuthLayout.tsx +13 -0
  91. package/src/modules/auth/components/AuthProviders.tsx +105 -0
  92. package/src/modules/auth/components/AuthRouter.tsx +29 -0
  93. package/src/modules/auth/components/ClaimAccountRoute.tsx +242 -0
  94. package/src/modules/auth/components/ErrorAuthRoute.tsx +121 -0
  95. package/src/modules/auth/components/ForgotPasswordForm.tsx +58 -0
  96. package/src/modules/auth/components/ForgotPasswordRoute.tsx +27 -0
  97. package/src/modules/auth/components/InviteFriends.tsx +273 -0
  98. package/src/modules/auth/components/LastUsedBadge.tsx +22 -0
  99. package/src/modules/auth/components/LoginForm.tsx +104 -0
  100. package/src/modules/auth/components/LoginRoute.tsx +31 -0
  101. package/src/modules/auth/components/LogoutRoute.tsx +21 -0
  102. package/src/modules/auth/components/OrganizationAcceptInvitationRoute.tsx +161 -0
  103. package/src/modules/auth/components/OrganizationMembersRoute.tsx +730 -0
  104. package/src/modules/auth/components/OrganizationSettingsRoute.tsx +280 -0
  105. package/src/modules/auth/components/OrganizationSwitcher.tsx +148 -0
  106. package/src/modules/auth/components/ProfileRoute.tsx +104 -0
  107. package/src/modules/auth/components/RangeNuqsDatePicker.tsx +365 -0
  108. package/src/modules/auth/components/ResetPasswordForm.tsx +103 -0
  109. package/src/modules/auth/components/ResetPasswordRoute.tsx +27 -0
  110. package/src/modules/auth/components/SignupFormRoute.tsx +189 -0
  111. package/src/modules/auth/components/SignupRoute.tsx +53 -0
  112. package/src/modules/auth/components/UserPreferences.tsx +144 -0
  113. package/src/modules/auth/components/WaitlistCard.tsx +78 -0
  114. package/src/modules/auth/components/WaitlistCodeValidation.tsx +79 -0
  115. package/src/modules/billing/components/BillingBetaPage.tsx +124 -0
  116. package/src/modules/billing/components/BillingInvoicePage.tsx +180 -0
  117. package/src/modules/billing/components/BillingPlanSelect.tsx +14 -0
  118. package/src/modules/billing/components/BillingRouter.tsx +20 -0
  119. package/src/modules/billing/components/BillingSinglePlanSelect.tsx +172 -0
  120. package/src/modules/table/components/ColumnOrderAndVisibility.tsx +127 -0
  121. package/src/modules/table/components/NuqsTable.tsx +396 -0
  122. package/src/modules/table/components/TableFiltering.tsx +520 -0
  123. package/src/modules/table/components/TablePagination.tsx +59 -0
  124. package/src/modules/table/components/table.types.ts +11 -0
  125. package/src/modules/table/filterTransformers.ts +323 -0
  126. package/src/types.ts +4 -0
  127. 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
+ }