@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,242 @@
|
|
|
1
|
+
import { Alert, Button, Card, CardBody, CardHeader, Input } from "@heroui/react";
|
|
2
|
+
import { useSession } from "@m5kdev/frontend/modules/auth/hooks/useSession";
|
|
3
|
+
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
4
|
+
import { useEffect, useMemo, useState } from "react";
|
|
5
|
+
import { useLocation, useNavigate } from "react-router";
|
|
6
|
+
import { toast } from "sonner";
|
|
7
|
+
import { GoogleIcon } from "#icons/GoogleIcon";
|
|
8
|
+
import { LinkedInIcon } from "#icons/LinkedInIcon";
|
|
9
|
+
import { MicrosoftIcon } from "#icons/MicrosoftIcon";
|
|
10
|
+
import type { UseBackendTRPC } from "#types";
|
|
11
|
+
|
|
12
|
+
export function ClaimAccountRoute({ useTRPC }: { useTRPC?: UseBackendTRPC }) {
|
|
13
|
+
const { data: session, registerSession } = useSession();
|
|
14
|
+
const navigate = useNavigate();
|
|
15
|
+
const location = useLocation();
|
|
16
|
+
const queryClient = useQueryClient();
|
|
17
|
+
const [email, setEmail] = useState("");
|
|
18
|
+
const [newPassword, setNewPassword] = useState("");
|
|
19
|
+
const [busy, setBusy] = useState<"none" | "email" | "password" | "link">("none");
|
|
20
|
+
|
|
21
|
+
const trpc = useTRPC?.();
|
|
22
|
+
|
|
23
|
+
const linkedProvider = useMemo(() => {
|
|
24
|
+
const params = new URLSearchParams(location.search);
|
|
25
|
+
return params.get("linked");
|
|
26
|
+
}, [location.search]);
|
|
27
|
+
|
|
28
|
+
const claimStatusQuery = useQuery({
|
|
29
|
+
queryKey: ["auth", "claim-status", session?.user?.id ?? null],
|
|
30
|
+
enabled: !!trpc && !!session?.user,
|
|
31
|
+
queryFn: async () => {
|
|
32
|
+
if (!trpc) return null;
|
|
33
|
+
return queryClient.fetchQuery(trpc.auth.getMyAccountClaimStatus.queryOptions());
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const setEmailMutation = useMutation(
|
|
38
|
+
trpc
|
|
39
|
+
? trpc.auth.setMyAccountClaimEmail.mutationOptions()
|
|
40
|
+
: {
|
|
41
|
+
mutationFn: async () => ({ status: false }),
|
|
42
|
+
}
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
const acceptClaimMutation = useMutation(
|
|
46
|
+
trpc
|
|
47
|
+
? trpc.auth.acceptMyAccountClaim.mutationOptions()
|
|
48
|
+
: {
|
|
49
|
+
mutationFn: async () => ({ status: false }),
|
|
50
|
+
}
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
if (session?.user?.email) {
|
|
55
|
+
setEmail(session.user.email);
|
|
56
|
+
}
|
|
57
|
+
}, [session?.user?.email]);
|
|
58
|
+
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
if (!linkedProvider || !trpc) return;
|
|
61
|
+
acceptClaimMutation
|
|
62
|
+
.mutateAsync(undefined)
|
|
63
|
+
.then(() => {
|
|
64
|
+
toast.success("Account provider linked");
|
|
65
|
+
queryClient.invalidateQueries({ queryKey: trpc.auth.getMyAccountClaimStatus.queryKey() });
|
|
66
|
+
})
|
|
67
|
+
.catch((error) => {
|
|
68
|
+
toast.error(error.message);
|
|
69
|
+
})
|
|
70
|
+
.finally(() => {
|
|
71
|
+
navigate("/claim-account", { replace: true });
|
|
72
|
+
});
|
|
73
|
+
}, [acceptClaimMutation, linkedProvider, navigate, queryClient, trpc]);
|
|
74
|
+
|
|
75
|
+
if (!trpc) {
|
|
76
|
+
return (
|
|
77
|
+
<Alert
|
|
78
|
+
color="warning"
|
|
79
|
+
variant="faded"
|
|
80
|
+
title="Claim flow is unavailable because backend TRPC is not configured."
|
|
81
|
+
/>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (!session?.user) {
|
|
86
|
+
return (
|
|
87
|
+
<Alert
|
|
88
|
+
color="warning"
|
|
89
|
+
variant="faded"
|
|
90
|
+
title="You need to sign in with your magic link before claiming this account."
|
|
91
|
+
/>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const claim = claimStatusQuery.data;
|
|
96
|
+
const hasClaimEmail = Boolean(claim?.claimedEmail);
|
|
97
|
+
|
|
98
|
+
const onSetEmail = async () => {
|
|
99
|
+
setBusy("email");
|
|
100
|
+
try {
|
|
101
|
+
await setEmailMutation.mutateAsync({ email });
|
|
102
|
+
registerSession(() => undefined);
|
|
103
|
+
await queryClient.invalidateQueries({
|
|
104
|
+
queryKey: trpc.auth.getMyAccountClaimStatus.queryKey(),
|
|
105
|
+
});
|
|
106
|
+
toast.success("Email updated");
|
|
107
|
+
} catch (error) {
|
|
108
|
+
toast.error(error instanceof Error ? error.message : "Unable to set email");
|
|
109
|
+
} finally {
|
|
110
|
+
setBusy("none");
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const onSetPassword = async () => {
|
|
115
|
+
setBusy("password");
|
|
116
|
+
try {
|
|
117
|
+
const response = await fetch(`${import.meta.env.VITE_SERVER_URL}/api/auth/set-password`, {
|
|
118
|
+
method: "POST",
|
|
119
|
+
credentials: "include",
|
|
120
|
+
headers: {
|
|
121
|
+
"Content-Type": "application/json",
|
|
122
|
+
},
|
|
123
|
+
body: JSON.stringify({
|
|
124
|
+
newPassword,
|
|
125
|
+
}),
|
|
126
|
+
});
|
|
127
|
+
const payload = await response.json().catch(() => ({}));
|
|
128
|
+
if (!response.ok) {
|
|
129
|
+
throw new Error(payload?.message ?? "Unable to set password");
|
|
130
|
+
}
|
|
131
|
+
await acceptClaimMutation.mutateAsync(undefined);
|
|
132
|
+
await queryClient.invalidateQueries({
|
|
133
|
+
queryKey: trpc.auth.getMyAccountClaimStatus.queryKey(),
|
|
134
|
+
});
|
|
135
|
+
toast.success("Password set. Account claimed.");
|
|
136
|
+
} catch (error) {
|
|
137
|
+
toast.error(error instanceof Error ? error.message : "Unable to set password");
|
|
138
|
+
} finally {
|
|
139
|
+
setBusy("none");
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const onLinkProvider = async (provider: "google" | "linkedin" | "microsoft") => {
|
|
144
|
+
setBusy("link");
|
|
145
|
+
try {
|
|
146
|
+
const response = await fetch(`${import.meta.env.VITE_SERVER_URL}/api/auth/link-social`, {
|
|
147
|
+
method: "POST",
|
|
148
|
+
credentials: "include",
|
|
149
|
+
headers: { "Content-Type": "application/json" },
|
|
150
|
+
body: JSON.stringify({
|
|
151
|
+
provider,
|
|
152
|
+
callbackURL: `${window.location.origin}/claim-account?linked=${provider}`,
|
|
153
|
+
}),
|
|
154
|
+
});
|
|
155
|
+
const payload = await response.json().catch(() => ({}));
|
|
156
|
+
if (!response.ok || !payload?.url) {
|
|
157
|
+
throw new Error(payload?.message ?? "Unable to start provider linking");
|
|
158
|
+
}
|
|
159
|
+
window.location.href = payload.url;
|
|
160
|
+
} catch (error) {
|
|
161
|
+
toast.error(error instanceof Error ? error.message : "Unable to link provider");
|
|
162
|
+
setBusy("none");
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
return (
|
|
167
|
+
<Card>
|
|
168
|
+
<CardHeader className="flex flex-col gap-1">
|
|
169
|
+
<p className="text-xl font-semibold">Claim your account</p>
|
|
170
|
+
<p className="text-sm text-default-600">
|
|
171
|
+
You are signed in and can now set your permanent login methods.
|
|
172
|
+
</p>
|
|
173
|
+
</CardHeader>
|
|
174
|
+
<CardBody className="grid gap-6">
|
|
175
|
+
{claimStatusQuery.isLoading ? (
|
|
176
|
+
<Alert color="default" variant="faded" title="Loading claim status..." />
|
|
177
|
+
) : !claim ? (
|
|
178
|
+
<Alert
|
|
179
|
+
color="warning"
|
|
180
|
+
variant="faded"
|
|
181
|
+
title="No pending account claim was found for your user."
|
|
182
|
+
/>
|
|
183
|
+
) : null}
|
|
184
|
+
|
|
185
|
+
<div className="grid gap-2">
|
|
186
|
+
<Input
|
|
187
|
+
type="email"
|
|
188
|
+
label="Email"
|
|
189
|
+
value={email}
|
|
190
|
+
onValueChange={setEmail}
|
|
191
|
+
description="Set your real email before linking providers and password."
|
|
192
|
+
/>
|
|
193
|
+
<Button onPress={onSetEmail} isDisabled={!email || busy !== "none" || !claim}>
|
|
194
|
+
{busy === "email" ? "Saving..." : "Save Email"}
|
|
195
|
+
</Button>
|
|
196
|
+
</div>
|
|
197
|
+
|
|
198
|
+
<div className="grid gap-2">
|
|
199
|
+
<p className="text-sm font-medium">Link a provider</p>
|
|
200
|
+
<div className="grid gap-3 sm:grid-cols-3">
|
|
201
|
+
<Button
|
|
202
|
+
variant="bordered"
|
|
203
|
+
onPress={() => onLinkProvider("google")}
|
|
204
|
+
isDisabled={busy !== "none" || !hasClaimEmail || !claim}
|
|
205
|
+
>
|
|
206
|
+
<GoogleIcon className="h-4 w-4" /> Google
|
|
207
|
+
</Button>
|
|
208
|
+
<Button
|
|
209
|
+
variant="bordered"
|
|
210
|
+
onPress={() => onLinkProvider("linkedin")}
|
|
211
|
+
isDisabled={busy !== "none" || !hasClaimEmail || !claim}
|
|
212
|
+
>
|
|
213
|
+
<LinkedInIcon className="h-4 w-4" /> LinkedIn
|
|
214
|
+
</Button>
|
|
215
|
+
<Button
|
|
216
|
+
variant="bordered"
|
|
217
|
+
onPress={() => onLinkProvider("microsoft")}
|
|
218
|
+
isDisabled={busy !== "none" || !hasClaimEmail || !claim}
|
|
219
|
+
>
|
|
220
|
+
<MicrosoftIcon className="h-4 w-4" /> Microsoft
|
|
221
|
+
</Button>
|
|
222
|
+
</div>
|
|
223
|
+
</div>
|
|
224
|
+
|
|
225
|
+
<div className="grid gap-2">
|
|
226
|
+
<Input
|
|
227
|
+
type="password"
|
|
228
|
+
label="Set password"
|
|
229
|
+
value={newPassword}
|
|
230
|
+
onValueChange={setNewPassword}
|
|
231
|
+
/>
|
|
232
|
+
<Button
|
|
233
|
+
onPress={onSetPassword}
|
|
234
|
+
isDisabled={!newPassword || busy !== "none" || !hasClaimEmail || !claim}
|
|
235
|
+
>
|
|
236
|
+
{busy === "password" ? "Saving..." : "Set Password and Claim"}
|
|
237
|
+
</Button>
|
|
238
|
+
</div>
|
|
239
|
+
</CardBody>
|
|
240
|
+
</Card>
|
|
241
|
+
);
|
|
242
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { Button, Card, CardBody, CardHeader } from "@heroui/react";
|
|
2
|
+
import { AlertCircle } from "lucide-react";
|
|
3
|
+
import { useQueryState } from "nuqs";
|
|
4
|
+
import { useTranslation } from "react-i18next";
|
|
5
|
+
import { Link } from "react-router";
|
|
6
|
+
|
|
7
|
+
export function ErrorAuthRoute() {
|
|
8
|
+
const { t } = useTranslation();
|
|
9
|
+
const [error] = useQueryState("error");
|
|
10
|
+
|
|
11
|
+
const ErrorEnum = {
|
|
12
|
+
invalid_callback_request: {
|
|
13
|
+
title: "Invalid callback request",
|
|
14
|
+
description: "The callback request is invalid. Please try again.",
|
|
15
|
+
buttons: ["login", "signup"],
|
|
16
|
+
signupLabel: null,
|
|
17
|
+
},
|
|
18
|
+
state_not_found: {
|
|
19
|
+
title: "State not found",
|
|
20
|
+
description: "The state was not found. Please try again.",
|
|
21
|
+
buttons: ["login", "signup"],
|
|
22
|
+
signupLabel: null,
|
|
23
|
+
},
|
|
24
|
+
account_already_linked_to_different_user: {
|
|
25
|
+
title: "Account already linked to different user",
|
|
26
|
+
description: "The account is already linked to a different user. Please try again.",
|
|
27
|
+
buttons: ["signup"],
|
|
28
|
+
signupLabel: null,
|
|
29
|
+
},
|
|
30
|
+
"email_doesn't_match": {
|
|
31
|
+
title: "Email doesn't match",
|
|
32
|
+
description: "The email doesn't match. Please try again.",
|
|
33
|
+
buttons: ["login", "signup"],
|
|
34
|
+
signupLabel: null,
|
|
35
|
+
},
|
|
36
|
+
email_not_found: {
|
|
37
|
+
title: "Email not found",
|
|
38
|
+
description: "The email was not found. Please try again.",
|
|
39
|
+
buttons: ["login", "signup"],
|
|
40
|
+
signupLabel: null,
|
|
41
|
+
},
|
|
42
|
+
no_callback_url: {
|
|
43
|
+
title: "No callback URL",
|
|
44
|
+
description: "The callback URL is not set. Please try again.",
|
|
45
|
+
buttons: ["login", "signup"],
|
|
46
|
+
signupLabel: null,
|
|
47
|
+
},
|
|
48
|
+
no_code: {
|
|
49
|
+
title: "No code",
|
|
50
|
+
description: "The code is not set. Please try again.",
|
|
51
|
+
buttons: ["login", "signup"],
|
|
52
|
+
signupLabel: null,
|
|
53
|
+
},
|
|
54
|
+
oauth_provider_not_found: {
|
|
55
|
+
title: "OAuth provider not found",
|
|
56
|
+
description: "The OAuth provider was not found. Please try again.",
|
|
57
|
+
buttons: ["login", "signup"],
|
|
58
|
+
signupLabel: null,
|
|
59
|
+
},
|
|
60
|
+
unable_to_link_account: {
|
|
61
|
+
title: "Unable to link account",
|
|
62
|
+
description: "The account could not be linked. Please try again.",
|
|
63
|
+
buttons: ["login", "signup"],
|
|
64
|
+
signupLabel: null,
|
|
65
|
+
},
|
|
66
|
+
unable_to_get_user_info: {
|
|
67
|
+
title: "Unable to get user info",
|
|
68
|
+
description: "The user info could not be retrieved. Please try again.",
|
|
69
|
+
buttons: ["login", "signup"],
|
|
70
|
+
signupLabel: null,
|
|
71
|
+
},
|
|
72
|
+
state_mismatch: {
|
|
73
|
+
title: "State mismatch",
|
|
74
|
+
description: "The state mismatch. Please try again.",
|
|
75
|
+
buttons: ["login", "signup"],
|
|
76
|
+
signupLabel: null,
|
|
77
|
+
},
|
|
78
|
+
signup_disabled: {
|
|
79
|
+
title: "Signup disabled",
|
|
80
|
+
description:
|
|
81
|
+
"The signup is disabled while we are in beta. Please join the waitlist to be notified when we launch.",
|
|
82
|
+
buttons: ["signup"],
|
|
83
|
+
signupLabel: "Join the waitlist",
|
|
84
|
+
},
|
|
85
|
+
default: {
|
|
86
|
+
title: "Authentication Failed",
|
|
87
|
+
description: "We encountered an issue with your authentication request",
|
|
88
|
+
buttons: ["login", "signup"],
|
|
89
|
+
signupLabel: null,
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const errorData = ErrorEnum[error as keyof typeof ErrorEnum] || ErrorEnum.default;
|
|
94
|
+
const { title, description, buttons, signupLabel } = errorData;
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
<div className="flex flex-col gap-6">
|
|
98
|
+
<Card>
|
|
99
|
+
<CardHeader className="text-center flex flex-col gap-2 items-center">
|
|
100
|
+
<AlertCircle className="w-10 h-10 text-red-500" />
|
|
101
|
+
<p className="text-xl font-semibold">{title}</p>
|
|
102
|
+
<p className="text-sm text-default-600">{description}</p>
|
|
103
|
+
</CardHeader>
|
|
104
|
+
<CardBody className="flex flex-col gap-4">
|
|
105
|
+
<div className="flex flex-col gap-2">
|
|
106
|
+
{buttons.includes("login") && (
|
|
107
|
+
<Button as={Link} to="/login" variant="bordered">
|
|
108
|
+
{t("web-ui:auth.error.backToLogin")}
|
|
109
|
+
</Button>
|
|
110
|
+
)}
|
|
111
|
+
{buttons.includes("signup") && (
|
|
112
|
+
<Button as={Link} to="/signup" variant="bordered">
|
|
113
|
+
{signupLabel || t("web-ui:auth.error.backToSignup")}
|
|
114
|
+
</Button>
|
|
115
|
+
)}
|
|
116
|
+
</div>
|
|
117
|
+
</CardBody>
|
|
118
|
+
</Card>
|
|
119
|
+
</div>
|
|
120
|
+
);
|
|
121
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { Button, Input } from "@heroui/react";
|
|
2
|
+
import { authClient } from "@m5kdev/frontend/modules/auth/auth.lib";
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { type SubmitHandler, useForm } from "react-hook-form";
|
|
5
|
+
import { useTranslation } from "react-i18next";
|
|
6
|
+
import { toast } from "sonner";
|
|
7
|
+
|
|
8
|
+
type Inputs = {
|
|
9
|
+
email: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export function ForgotPasswordForm() {
|
|
13
|
+
const { t } = useTranslation();
|
|
14
|
+
const {
|
|
15
|
+
register,
|
|
16
|
+
handleSubmit,
|
|
17
|
+
formState: { errors },
|
|
18
|
+
} = useForm<Inputs>();
|
|
19
|
+
|
|
20
|
+
const [isBusy, setIsBusy] = useState(false);
|
|
21
|
+
|
|
22
|
+
const onSubmit: SubmitHandler<Inputs> = (data) => {
|
|
23
|
+
setIsBusy(true);
|
|
24
|
+
authClient
|
|
25
|
+
.requestPasswordReset({
|
|
26
|
+
email: data.email,
|
|
27
|
+
redirectTo: "/reset-password",
|
|
28
|
+
})
|
|
29
|
+
.then(() => {
|
|
30
|
+
toast.success(t("web-ui:auth.forgotPassword.success"));
|
|
31
|
+
})
|
|
32
|
+
.catch(() => {
|
|
33
|
+
toast.error(t("web-ui:auth.forgotPassword.error"));
|
|
34
|
+
});
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<form onSubmit={handleSubmit(onSubmit)} className="grid gap-6">
|
|
39
|
+
<div className="grid gap-2">
|
|
40
|
+
<Input
|
|
41
|
+
labelPlacement="outside"
|
|
42
|
+
label={t("web-ui:auth.login.email")}
|
|
43
|
+
type="email"
|
|
44
|
+
placeholder={t("web-ui:auth.login.placeholder.email")}
|
|
45
|
+
variant="bordered"
|
|
46
|
+
isRequired
|
|
47
|
+
{...register("email", { required: true })}
|
|
48
|
+
/>
|
|
49
|
+
{errors.email && (
|
|
50
|
+
<span className="text-red-500 text-xs">{t("web-ui:auth.signup.emailRequired")}</span>
|
|
51
|
+
)}
|
|
52
|
+
</div>
|
|
53
|
+
<Button type="submit" className="w-full" color="primary" isDisabled={isBusy}>
|
|
54
|
+
{t("web-ui:auth.forgotPassword.button")}
|
|
55
|
+
</Button>
|
|
56
|
+
</form>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
@@ -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 { ForgotPasswordForm } from "#modules/auth/components/ForgotPasswordForm";
|
|
5
|
+
|
|
6
|
+
export function ForgotPasswordRoute() {
|
|
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
|
+
<p className="text-xl font-semibold">{t("web-ui:auth.forgotPassword.title")}</p>
|
|
13
|
+
<p className="text-sm text-default-600">{t("web-ui:auth.forgotPassword.description")}</p>
|
|
14
|
+
</CardHeader>
|
|
15
|
+
<CardBody>
|
|
16
|
+
<ForgotPasswordForm />
|
|
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
|
+
}
|