@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,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
+ }