refine-mantine 1.0.0-dev.1
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 +21 -0
- package/README.md +21 -0
- package/dist/index.d.ts +276 -0
- package/dist/index.js +1576 -0
- package/package.json +89 -0
- package/src/App.tsx +33 -0
- package/src/Router.tsx +97 -0
- package/src/components/ColorSchemeToggle.tsx +13 -0
- package/src/components/breadcrumb/Breadcrumb.tsx +80 -0
- package/src/components/buttons/CloneButton.story.tsx +22 -0
- package/src/components/buttons/CloneButton.tsx +86 -0
- package/src/components/buttons/CreateButton.story.tsx +22 -0
- package/src/components/buttons/CreateButton.tsx +81 -0
- package/src/components/buttons/DeleteButton.story.tsx +22 -0
- package/src/components/buttons/DeleteButton.tsx +133 -0
- package/src/components/buttons/EditButton.story.tsx +22 -0
- package/src/components/buttons/EditButton.tsx +87 -0
- package/src/components/buttons/ExportButton.story.tsx +28 -0
- package/src/components/buttons/ExportButton.tsx +48 -0
- package/src/components/buttons/ImportButton.story.tsx +44 -0
- package/src/components/buttons/ImportButton.tsx +61 -0
- package/src/components/buttons/ListButton.story.tsx +25 -0
- package/src/components/buttons/ListButton.tsx +91 -0
- package/src/components/buttons/RefreshButton.story.tsx +22 -0
- package/src/components/buttons/RefreshButton.tsx +59 -0
- package/src/components/buttons/SaveButton.story.tsx +22 -0
- package/src/components/buttons/SaveButton.tsx +51 -0
- package/src/components/buttons/ShowButton.story.tsx +22 -0
- package/src/components/buttons/ShowButton.tsx +83 -0
- package/src/components/crud/Create.story.tsx +83 -0
- package/src/components/crud/Create.tsx +146 -0
- package/src/components/crud/Edit.story.tsx +173 -0
- package/src/components/crud/Edit.tsx +236 -0
- package/src/components/crud/List.story.tsx +98 -0
- package/src/components/crud/List.tsx +109 -0
- package/src/components/crud/Show.tsx +220 -0
- package/src/components/layout/Layout.story.tsx +28 -0
- package/src/components/layout/Layout.tsx +257 -0
- package/src/components/notification/AutoSaveIndicator.story.tsx +41 -0
- package/src/components/notification/AutoSaveIndicator.tsx +58 -0
- package/src/components/notification/Message.story.tsx +30 -0
- package/src/components/notification/Message.tsx +79 -0
- package/src/components/table/ColumnFilter.tsx +107 -0
- package/src/components/table/ColumnSorter.tsx +26 -0
- package/src/components/table/Table.story.tsx +146 -0
- package/src/components/table/Table.tsx +64 -0
- package/src/favicon.svg +1 -0
- package/src/hooks/useForm.ts +271 -0
- package/src/hooks/useOtp.ts +45 -0
- package/src/index.ts +36 -0
- package/src/main.tsx +4 -0
- package/src/pages/auth/DefaultTitle.tsx +13 -0
- package/src/pages/auth/ForgotPasswordPage.story.tsx +14 -0
- package/src/pages/auth/ForgotPasswordPage.tsx +128 -0
- package/src/pages/auth/LoginPage.story.tsx +98 -0
- package/src/pages/auth/LoginPage.tsx +256 -0
- package/src/pages/auth/RegisterPage.story.tsx +18 -0
- package/src/pages/auth/RegisterPage.tsx +151 -0
- package/src/pages/auth/UpdatePasswordPage.story.tsx +27 -0
- package/src/pages/auth/UpdatePasswordPage.tsx +124 -0
- package/src/pages/category/CategoryCreate.tsx +1 -0
- package/src/pages/category/CategoryEdit.tsx +1 -0
- package/src/pages/category/CategoryList.tsx +1 -0
- package/src/pages/category/CategoryShow.tsx +1 -0
- package/src/pages/product/ProductCreate.tsx +1 -0
- package/src/pages/product/ProductEdit.tsx +1 -0
- package/src/pages/product/ProductList.tsx +1 -0
- package/src/pages/product/ProductShow.tsx +1 -0
- package/src/providers/authProvider.ts +105 -0
- package/src/providers/i18nProvider.ts +7 -0
- package/src/providers/notificationProvider.tsx +122 -0
- package/src/resources.tsx +63 -0
- package/src/theme.ts +5 -0
- package/src/utils/paths.ts +52 -0
- package/src/utils/wait.ts +4 -0
- package/src/vite-env.d.ts +1 -0
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import { get, has, set } from "@/utils/paths";
|
|
2
|
+
import type { UseFormInput } from "@mantine/form";
|
|
3
|
+
import {
|
|
4
|
+
type UseFormReturnType as UseMantineFormReturnType,
|
|
5
|
+
useForm as useMantineForm,
|
|
6
|
+
} from "@mantine/form";
|
|
7
|
+
import { useDebouncedCallback } from "@mantine/hooks";
|
|
8
|
+
import {
|
|
9
|
+
type BaseRecord,
|
|
10
|
+
type HttpError,
|
|
11
|
+
type UseFormProps as UseFormCoreProps,
|
|
12
|
+
type UseFormReturnType as UseFormReturnTypeCore,
|
|
13
|
+
flattenObjectKeys,
|
|
14
|
+
useForm as useFormCore,
|
|
15
|
+
useRefineContext,
|
|
16
|
+
useTranslate,
|
|
17
|
+
useWarnAboutChange,
|
|
18
|
+
} from "@refinedev/core";
|
|
19
|
+
import type React from "react";
|
|
20
|
+
import { useEffect } from "react";
|
|
21
|
+
|
|
22
|
+
type FormVariableType<TVariables, TTransformed> = ReturnType<
|
|
23
|
+
NonNullable<
|
|
24
|
+
UseFormInput<
|
|
25
|
+
TVariables,
|
|
26
|
+
(values: TVariables) => TTransformed
|
|
27
|
+
>["transformValues"]
|
|
28
|
+
>
|
|
29
|
+
>;
|
|
30
|
+
|
|
31
|
+
export type UseFormReturnType<
|
|
32
|
+
TQueryFnData extends BaseRecord = BaseRecord,
|
|
33
|
+
TError extends HttpError = HttpError,
|
|
34
|
+
TVariables = Record<string, unknown>,
|
|
35
|
+
TTransformed = TVariables,
|
|
36
|
+
TData extends BaseRecord = TQueryFnData,
|
|
37
|
+
TResponse extends BaseRecord = TData,
|
|
38
|
+
TResponseError extends HttpError = TError,
|
|
39
|
+
> = UseMantineFormReturnType<
|
|
40
|
+
TVariables,
|
|
41
|
+
(values: TVariables) => TTransformed
|
|
42
|
+
> & {
|
|
43
|
+
refineCore: UseFormReturnTypeCore<
|
|
44
|
+
TQueryFnData,
|
|
45
|
+
TError,
|
|
46
|
+
FormVariableType<TVariables, TTransformed>,
|
|
47
|
+
TData,
|
|
48
|
+
TResponse,
|
|
49
|
+
TResponseError
|
|
50
|
+
>;
|
|
51
|
+
saveButtonProps: {
|
|
52
|
+
disabled: boolean;
|
|
53
|
+
onClick: (e: React.PointerEvent<HTMLButtonElement>) => void;
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export type UseFormProps<
|
|
58
|
+
TQueryFnData extends BaseRecord = BaseRecord,
|
|
59
|
+
TError extends HttpError = HttpError,
|
|
60
|
+
TVariables = Record<string, unknown>,
|
|
61
|
+
TTransformed = TVariables,
|
|
62
|
+
TData extends BaseRecord = TQueryFnData,
|
|
63
|
+
TResponse extends BaseRecord = TData,
|
|
64
|
+
TResponseError extends HttpError = TError,
|
|
65
|
+
> = {
|
|
66
|
+
refineCoreProps?: UseFormCoreProps<
|
|
67
|
+
TQueryFnData,
|
|
68
|
+
TError,
|
|
69
|
+
FormVariableType<TVariables, TTransformed>,
|
|
70
|
+
TData,
|
|
71
|
+
TResponse,
|
|
72
|
+
TResponseError
|
|
73
|
+
> & {
|
|
74
|
+
warnWhenUnsavedChanges?: boolean;
|
|
75
|
+
};
|
|
76
|
+
} & UseFormInput<TVariables, (values: TVariables) => TTransformed> & {
|
|
77
|
+
/**
|
|
78
|
+
* Disables server-side validation
|
|
79
|
+
* @default false
|
|
80
|
+
* @see {@link https://refine.dev/docs/advanced-tutorials/forms/server-side-form-validation/}
|
|
81
|
+
*/
|
|
82
|
+
disableServerSideValidation?: boolean;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export const useForm = <
|
|
86
|
+
TQueryFnData extends BaseRecord = BaseRecord,
|
|
87
|
+
TError extends HttpError = HttpError,
|
|
88
|
+
// biome-ignore lint/suspicious/noExplicitAny: that's fine
|
|
89
|
+
TVariables extends Record<string, any> = Record<string, any>,
|
|
90
|
+
TTransformed = TVariables,
|
|
91
|
+
TData extends BaseRecord = TQueryFnData,
|
|
92
|
+
TResponse extends BaseRecord = TData,
|
|
93
|
+
TResponseError extends HttpError = TError,
|
|
94
|
+
>({
|
|
95
|
+
refineCoreProps,
|
|
96
|
+
disableServerSideValidation: disableServerSideValidationProp = false,
|
|
97
|
+
...rest
|
|
98
|
+
}: UseFormProps<
|
|
99
|
+
TQueryFnData,
|
|
100
|
+
TError,
|
|
101
|
+
TVariables,
|
|
102
|
+
TTransformed,
|
|
103
|
+
TData,
|
|
104
|
+
TResponse,
|
|
105
|
+
TResponseError
|
|
106
|
+
> = {}): UseFormReturnType<
|
|
107
|
+
TQueryFnData,
|
|
108
|
+
TError,
|
|
109
|
+
TVariables,
|
|
110
|
+
TTransformed,
|
|
111
|
+
TData,
|
|
112
|
+
TResponse,
|
|
113
|
+
TResponseError
|
|
114
|
+
> => {
|
|
115
|
+
const { options } = useRefineContext();
|
|
116
|
+
const disableServerSideValidation =
|
|
117
|
+
options?.disableServerSideValidation || disableServerSideValidationProp;
|
|
118
|
+
|
|
119
|
+
const translate = useTranslate();
|
|
120
|
+
|
|
121
|
+
const warnWhenUnsavedChangesProp = refineCoreProps?.warnWhenUnsavedChanges;
|
|
122
|
+
|
|
123
|
+
const { warnWhenUnsavedChanges: warnWhenUnsavedChangesRefine, setWarnWhen } =
|
|
124
|
+
useWarnAboutChange();
|
|
125
|
+
const warnWhenUnsavedChanges =
|
|
126
|
+
warnWhenUnsavedChangesProp ?? warnWhenUnsavedChangesRefine;
|
|
127
|
+
|
|
128
|
+
const useMantineFormResult = useMantineForm<
|
|
129
|
+
TVariables,
|
|
130
|
+
(values: TVariables) => TTransformed
|
|
131
|
+
>({
|
|
132
|
+
...rest,
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
const {
|
|
136
|
+
setValues,
|
|
137
|
+
onSubmit: onMantineSubmit,
|
|
138
|
+
isDirty,
|
|
139
|
+
resetDirty,
|
|
140
|
+
setFieldError,
|
|
141
|
+
values,
|
|
142
|
+
} = useMantineFormResult;
|
|
143
|
+
|
|
144
|
+
const useFormCoreResult = useFormCore<
|
|
145
|
+
TQueryFnData,
|
|
146
|
+
TError,
|
|
147
|
+
FormVariableType<TVariables, TTransformed>,
|
|
148
|
+
TData,
|
|
149
|
+
TResponse,
|
|
150
|
+
TResponseError
|
|
151
|
+
>({
|
|
152
|
+
...refineCoreProps,
|
|
153
|
+
onMutationError: (error, _variables, _context) => {
|
|
154
|
+
if (disableServerSideValidation) {
|
|
155
|
+
refineCoreProps?.onMutationError?.(error, _variables, _context);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const errors = error?.errors;
|
|
160
|
+
|
|
161
|
+
for (const key in errors) {
|
|
162
|
+
const fieldError = errors[key];
|
|
163
|
+
|
|
164
|
+
let newError = "";
|
|
165
|
+
|
|
166
|
+
if (Array.isArray(fieldError)) {
|
|
167
|
+
newError = fieldError.join(" ");
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (typeof fieldError === "string") {
|
|
171
|
+
newError = fieldError;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (typeof fieldError === "boolean") {
|
|
175
|
+
newError = "Field is not valid.";
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (typeof fieldError === "object" && "key" in fieldError) {
|
|
179
|
+
const translatedMessage = translate(
|
|
180
|
+
fieldError.key,
|
|
181
|
+
fieldError.message,
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
newError = translatedMessage;
|
|
185
|
+
}
|
|
186
|
+
setFieldError(key, newError);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
refineCoreProps?.onMutationError?.(error, _variables, _context);
|
|
190
|
+
},
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
const { query, formLoading, onFinish, onFinishAutoSave } = useFormCoreResult;
|
|
194
|
+
|
|
195
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: code taken from refine, will check later
|
|
196
|
+
useEffect(() => {
|
|
197
|
+
if (typeof query?.data !== "undefined") {
|
|
198
|
+
// biome-ignore lint/suspicious/noExplicitAny: that's fine
|
|
199
|
+
const fields: any = {};
|
|
200
|
+
|
|
201
|
+
const registeredFields = flattenObjectKeys(rest.initialValues ?? {});
|
|
202
|
+
|
|
203
|
+
const data = query?.data?.data ?? {};
|
|
204
|
+
|
|
205
|
+
Object.keys(registeredFields).forEach((key) => {
|
|
206
|
+
const hasValue = has(data, key);
|
|
207
|
+
const dataValue = get(data, key);
|
|
208
|
+
|
|
209
|
+
if (hasValue) {
|
|
210
|
+
set(fields, key, dataValue);
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
setValues(fields);
|
|
215
|
+
resetDirty(fields);
|
|
216
|
+
}
|
|
217
|
+
}, [query?.data]);
|
|
218
|
+
|
|
219
|
+
const isValuesChanged = isDirty();
|
|
220
|
+
|
|
221
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: code taken from refine, will check later
|
|
222
|
+
useEffect(() => {
|
|
223
|
+
if (warnWhenUnsavedChanges) {
|
|
224
|
+
setWarnWhen(isValuesChanged);
|
|
225
|
+
}
|
|
226
|
+
}, [isValuesChanged]);
|
|
227
|
+
|
|
228
|
+
// workaround since refines auto save is buggy
|
|
229
|
+
// https://github.com/refinedev/refine/issues/7082
|
|
230
|
+
const handleAutoSave = useDebouncedCallback((transformedValues: TTransformed) => {
|
|
231
|
+
onFinishAutoSave(transformedValues)
|
|
232
|
+
.catch((error) => error);
|
|
233
|
+
}, refineCoreProps?.autoSave?.debounce ?? 1000);
|
|
234
|
+
|
|
235
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: code taken from refine, will check later
|
|
236
|
+
useEffect(() => {
|
|
237
|
+
if (isValuesChanged && refineCoreProps?.autoSave && values) {
|
|
238
|
+
setWarnWhen(false);
|
|
239
|
+
|
|
240
|
+
const transformedValues = rest.transformValues
|
|
241
|
+
? rest.transformValues(values)
|
|
242
|
+
: (values as unknown as TTransformed);
|
|
243
|
+
|
|
244
|
+
handleAutoSave(transformedValues);
|
|
245
|
+
}
|
|
246
|
+
}, [values]);
|
|
247
|
+
|
|
248
|
+
const onSubmit: (typeof useMantineFormResult)["onSubmit"] =
|
|
249
|
+
(handleSubmit, handleValidationFailure) => async (e) => {
|
|
250
|
+
setWarnWhen(false);
|
|
251
|
+
return await onMantineSubmit(handleSubmit, handleValidationFailure)(e);
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
const saveButtonProps = {
|
|
255
|
+
disabled: formLoading,
|
|
256
|
+
onClick: (e: React.PointerEvent<HTMLButtonElement>) => {
|
|
257
|
+
onSubmit(
|
|
258
|
+
(v) => onFinish(v).catch((error) => error),
|
|
259
|
+
() => false,
|
|
260
|
+
// @ts-expect-error event type is not compatible with pointer event
|
|
261
|
+
)(e);
|
|
262
|
+
},
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
return {
|
|
266
|
+
...useMantineFormResult,
|
|
267
|
+
onSubmit,
|
|
268
|
+
refineCore: useFormCoreResult,
|
|
269
|
+
saveButtonProps,
|
|
270
|
+
};
|
|
271
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
|
|
2
|
+
import { useCallback, useEffect, useState } from "react";
|
|
3
|
+
|
|
4
|
+
export type OtpHandler = ReturnType<typeof useOtp>;
|
|
5
|
+
|
|
6
|
+
export const useOtp = () => {
|
|
7
|
+
const [otpPromise, setOtpPromise] = useState<{
|
|
8
|
+
resolve: (otp: string) => void;
|
|
9
|
+
reject: () => void;
|
|
10
|
+
}>();
|
|
11
|
+
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
return () => {
|
|
14
|
+
otpPromise?.reject();
|
|
15
|
+
};
|
|
16
|
+
}, [otpPromise]);
|
|
17
|
+
|
|
18
|
+
const request = useCallback(() => {
|
|
19
|
+
return new Promise<string>((resolve, reject) => {
|
|
20
|
+
setOtpPromise({ resolve, reject });
|
|
21
|
+
});
|
|
22
|
+
}, []);
|
|
23
|
+
|
|
24
|
+
const reject = useCallback(() => {
|
|
25
|
+
otpPromise?.reject();
|
|
26
|
+
setOtpPromise(undefined);
|
|
27
|
+
}, [otpPromise]);
|
|
28
|
+
|
|
29
|
+
const resolve = useCallback(
|
|
30
|
+
(otp: string) => {
|
|
31
|
+
otpPromise?.resolve(otp);
|
|
32
|
+
setOtpPromise(undefined);
|
|
33
|
+
},
|
|
34
|
+
[otpPromise],
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
const isPending = !!otpPromise;
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
isPending,
|
|
41
|
+
request,
|
|
42
|
+
reject,
|
|
43
|
+
resolve,
|
|
44
|
+
};
|
|
45
|
+
};
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// breadcrumbs
|
|
2
|
+
export * from "@/components/breadcrumb/Breadcrumb";
|
|
3
|
+
// buttons
|
|
4
|
+
export * from "@/components/buttons/CreateButton";
|
|
5
|
+
export * from "@/components/buttons/DeleteButton";
|
|
6
|
+
export * from "@/components/buttons/EditButton";
|
|
7
|
+
export * from "@/components/buttons/ListButton";
|
|
8
|
+
export * from "@/components/buttons/RefreshButton";
|
|
9
|
+
export * from "@/components/buttons/SaveButton";
|
|
10
|
+
export * from "@/components/buttons/ShowButton";
|
|
11
|
+
// crud
|
|
12
|
+
export * from "@/components/crud/Create";
|
|
13
|
+
export * from "@/components/crud/Edit";
|
|
14
|
+
export * from "@/components/crud/List";
|
|
15
|
+
export * from "@/components/crud/Show";
|
|
16
|
+
// layout
|
|
17
|
+
export * from "@/components/layout/Layout";
|
|
18
|
+
// notification
|
|
19
|
+
export * from "@/components/notification/Message";
|
|
20
|
+
// table
|
|
21
|
+
export * from "@/components/table/ColumnFilter";
|
|
22
|
+
export * from "@/components/table/ColumnSorter";
|
|
23
|
+
export * from "@/components/table/Table";
|
|
24
|
+
// uncategorized components
|
|
25
|
+
export * from "@/components/notification/AutoSaveIndicator";
|
|
26
|
+
export * from "@/components/ColorSchemeToggle";
|
|
27
|
+
// hooks
|
|
28
|
+
export * from "@/hooks/useForm";
|
|
29
|
+
export * from "@/hooks/useOtp";
|
|
30
|
+
// pages
|
|
31
|
+
export * from "@/pages/auth/LoginPage";
|
|
32
|
+
export * from "@/pages/auth/RegisterPage";
|
|
33
|
+
// providers
|
|
34
|
+
export type * from "@/providers/authProvider";
|
|
35
|
+
export * from "@/providers/notificationProvider";
|
|
36
|
+
|
package/src/main.tsx
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Stack } from "@mantine/core";
|
|
2
|
+
import { useRefineOptions } from "@refinedev/core";
|
|
3
|
+
|
|
4
|
+
export const DefaultTitle = () => {
|
|
5
|
+
const { title } = useRefineOptions();
|
|
6
|
+
|
|
7
|
+
return (
|
|
8
|
+
<Stack align="center" mb="lg">
|
|
9
|
+
{title.icon}
|
|
10
|
+
{title.text}
|
|
11
|
+
</Stack>
|
|
12
|
+
);
|
|
13
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { ForgotPasswordPage } from './ForgotPasswordPage';
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
title: 'Auth/ForgotPasswordPage',
|
|
5
|
+
component: ForgotPasswordPage,
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export const Basic = () =>
|
|
9
|
+
<ForgotPasswordPage />;
|
|
10
|
+
|
|
11
|
+
export const WithLoginLink = () =>
|
|
12
|
+
<ForgotPasswordPage
|
|
13
|
+
loginLink="/login"
|
|
14
|
+
/>;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/** biome-ignore-all lint/correctness/useUniqueElementIds: test ids for playwright */
|
|
2
|
+
import {
|
|
3
|
+
Anchor,
|
|
4
|
+
Button,
|
|
5
|
+
type ButtonProps,
|
|
6
|
+
Card,
|
|
7
|
+
type CardProps,
|
|
8
|
+
LoadingOverlay,
|
|
9
|
+
type LoadingOverlayProps,
|
|
10
|
+
ScrollArea,
|
|
11
|
+
type ScrollAreaProps,
|
|
12
|
+
Stack,
|
|
13
|
+
type StackProps,
|
|
14
|
+
Text,
|
|
15
|
+
TextInput,
|
|
16
|
+
type TextInputProps,
|
|
17
|
+
Title
|
|
18
|
+
} from "@mantine/core";
|
|
19
|
+
import { type FormValidateInput, isEmail } from "@mantine/form";
|
|
20
|
+
import { Link, useForgotPassword, useTranslate } from "@refinedev/core";
|
|
21
|
+
import { IconAt } from "@tabler/icons-react";
|
|
22
|
+
import type { ReactNode } from "react";
|
|
23
|
+
import { useForm } from "@/hooks/useForm";
|
|
24
|
+
import { DefaultTitle } from "./DefaultTitle";
|
|
25
|
+
|
|
26
|
+
export interface ForgotPasswordPageProps {
|
|
27
|
+
loginLink?: string;
|
|
28
|
+
validate?: FormValidateInput<ForgotPasswordForm>;
|
|
29
|
+
// customization
|
|
30
|
+
wrapperProps?: StackProps;
|
|
31
|
+
scrollAreaProps?: ScrollAreaProps;
|
|
32
|
+
emailFieldProps?: TextInputProps;
|
|
33
|
+
cardProps?: CardProps;
|
|
34
|
+
loadingOverlayProps?: LoadingOverlayProps;
|
|
35
|
+
submitButtonProps?: ButtonProps;
|
|
36
|
+
// components
|
|
37
|
+
icon?: ReactNode;
|
|
38
|
+
title?: ReactNode;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface ForgotPasswordForm {
|
|
42
|
+
email: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export const ForgotPasswordPage: React.FC<ForgotPasswordPageProps> = (p: {
|
|
46
|
+
loginLink?: string;
|
|
47
|
+
validate?: FormValidateInput<ForgotPasswordForm>;
|
|
48
|
+
// customization
|
|
49
|
+
wrapperProps?: StackProps;
|
|
50
|
+
scrollAreaProps?: ScrollAreaProps;
|
|
51
|
+
emailFieldProps?: TextInputProps;
|
|
52
|
+
cardProps?: CardProps;
|
|
53
|
+
loadingOverlayProps?: LoadingOverlayProps;
|
|
54
|
+
submitButtonProps?: ButtonProps;
|
|
55
|
+
// components
|
|
56
|
+
icon?: ReactNode;
|
|
57
|
+
title?: ReactNode;
|
|
58
|
+
}) => {
|
|
59
|
+
const translate = useTranslate();
|
|
60
|
+
const { mutate: forgotPassword, isPending } = useForgotPassword();
|
|
61
|
+
|
|
62
|
+
const { getInputProps, onSubmit, key } = useForm({
|
|
63
|
+
initialValues: {
|
|
64
|
+
email: "",
|
|
65
|
+
},
|
|
66
|
+
validate: {
|
|
67
|
+
email: isEmail(
|
|
68
|
+
translate("pages.login.invalidEmail", "Email is not valid")
|
|
69
|
+
),
|
|
70
|
+
},
|
|
71
|
+
...p.validate,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const handleResetPassword = onSubmit(({ email }) => {
|
|
75
|
+
forgotPassword({ email, translate });
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<Stack h="100vh" align="center" justify="center" {...p.wrapperProps}>
|
|
80
|
+
<ScrollArea type="never" {...p.scrollAreaProps}>
|
|
81
|
+
{p.icon ?? <DefaultTitle />}
|
|
82
|
+
<Card shadow="sm" padding="lg" radius="md" withBorder {...p.cardProps}>
|
|
83
|
+
<LoadingOverlay
|
|
84
|
+
visible={isPending }
|
|
85
|
+
{...p.loadingOverlayProps}
|
|
86
|
+
/>
|
|
87
|
+
{p.title ?? (
|
|
88
|
+
<Title order={5} mb="lg" ta="center">
|
|
89
|
+
{translate("pages.forgotPassword.title", "Reset your password")}
|
|
90
|
+
</Title>
|
|
91
|
+
)}
|
|
92
|
+
<form onSubmit={handleResetPassword}>
|
|
93
|
+
<TextInput
|
|
94
|
+
mb="lg"
|
|
95
|
+
type="email"
|
|
96
|
+
label={translate("pages.forgotPassword.fields.email", "Email")}
|
|
97
|
+
leftSection={<IconAt size={18} />}
|
|
98
|
+
placeholder={translate("pages.forgotPassword.fields.emailPlaceholder", "name@example.com")}
|
|
99
|
+
key={key("email")}
|
|
100
|
+
{...getInputProps("email")}
|
|
101
|
+
{...p.emailFieldProps}
|
|
102
|
+
/>
|
|
103
|
+
<Button
|
|
104
|
+
fullWidth
|
|
105
|
+
variant="outline"
|
|
106
|
+
type="submit"
|
|
107
|
+
{...p.submitButtonProps}
|
|
108
|
+
>
|
|
109
|
+
{translate("pages.forgotPassword.buttons.submit", "Send reset instructions")}
|
|
110
|
+
</Button>
|
|
111
|
+
{p.loginLink &&
|
|
112
|
+
<Text mt="lg" size="xs" ta="center">
|
|
113
|
+
{translate(
|
|
114
|
+
"pages.register.buttons.haveAccount",
|
|
115
|
+
"Have an account? ",
|
|
116
|
+
)}{" "}
|
|
117
|
+
{/** biome-ignore lint/suspicious/noExplicitAny: that's fine */}
|
|
118
|
+
<Anchor component={Link as any} to={p.loginLink}>
|
|
119
|
+
{translate("pages.login.signin", "Sign in")}
|
|
120
|
+
</Anchor>
|
|
121
|
+
</Text>
|
|
122
|
+
}
|
|
123
|
+
</form>
|
|
124
|
+
</Card>
|
|
125
|
+
</ScrollArea>
|
|
126
|
+
</Stack>
|
|
127
|
+
);
|
|
128
|
+
};
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import type { Meta } from "@storybook/react";
|
|
2
|
+
import { IconBrandApple, IconBrandFacebook, IconBrandGoogle } from '@tabler/icons-react';
|
|
3
|
+
import { useOtp } from '@/hooks/useOtp';
|
|
4
|
+
import { LoginPage } from './LoginPage';
|
|
5
|
+
|
|
6
|
+
export default {
|
|
7
|
+
title: 'Auth/LoginPage',
|
|
8
|
+
component: LoginPage,
|
|
9
|
+
} satisfies Meta<typeof LoginPage>;
|
|
10
|
+
|
|
11
|
+
export const WithPassword = () =>
|
|
12
|
+
<LoginPage method='password' />;
|
|
13
|
+
|
|
14
|
+
export const WithProviders = () =>
|
|
15
|
+
<LoginPage
|
|
16
|
+
method='oauth'
|
|
17
|
+
providers={[{
|
|
18
|
+
name: "google",
|
|
19
|
+
label: "Continue with Google",
|
|
20
|
+
icon: <IconBrandGoogle />,
|
|
21
|
+
buttonProps: {
|
|
22
|
+
color: "red",
|
|
23
|
+
}
|
|
24
|
+
}, {
|
|
25
|
+
name: "facebook",
|
|
26
|
+
label: "Continue with the Facebook",
|
|
27
|
+
icon: <IconBrandFacebook />,
|
|
28
|
+
buttonProps: {
|
|
29
|
+
color: "blue",
|
|
30
|
+
}
|
|
31
|
+
}, {
|
|
32
|
+
name: "apple",
|
|
33
|
+
label: "Continue with Apple",
|
|
34
|
+
icon: <IconBrandApple />,
|
|
35
|
+
buttonProps: {
|
|
36
|
+
color: "gray",
|
|
37
|
+
}
|
|
38
|
+
}]}
|
|
39
|
+
/>;
|
|
40
|
+
|
|
41
|
+
export const WithProvidersAndPassword = () =>
|
|
42
|
+
<LoginPage
|
|
43
|
+
method='password'
|
|
44
|
+
providers={[{
|
|
45
|
+
name: "google",
|
|
46
|
+
label: "Continue with Google",
|
|
47
|
+
icon: <IconBrandGoogle />,
|
|
48
|
+
buttonProps: {
|
|
49
|
+
color: "red",
|
|
50
|
+
}
|
|
51
|
+
}]}
|
|
52
|
+
/>;
|
|
53
|
+
|
|
54
|
+
export const WithProvidersAndOtp = () => {
|
|
55
|
+
const otpHandler = useOtp();
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<LoginPage
|
|
59
|
+
method='otp'
|
|
60
|
+
otpHandler={otpHandler}
|
|
61
|
+
providers={[{
|
|
62
|
+
name: "google",
|
|
63
|
+
label: "Continue with Google",
|
|
64
|
+
icon: <IconBrandGoogle />,
|
|
65
|
+
buttonProps: {
|
|
66
|
+
color: "red",
|
|
67
|
+
}
|
|
68
|
+
}]}
|
|
69
|
+
/>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export const WithProvidersAndMfa = () => {
|
|
74
|
+
const otpHandler = useOtp();
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<LoginPage
|
|
78
|
+
method='mfa'
|
|
79
|
+
otpHandler={otpHandler}
|
|
80
|
+
otpInputProps={{ length: 6 }}
|
|
81
|
+
providers={[{
|
|
82
|
+
name: "google",
|
|
83
|
+
label: "Continue with Google",
|
|
84
|
+
icon: <IconBrandGoogle />,
|
|
85
|
+
buttonProps: {
|
|
86
|
+
color: "red",
|
|
87
|
+
}
|
|
88
|
+
}]}
|
|
89
|
+
/>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export const WithLinks = () =>
|
|
94
|
+
<LoginPage
|
|
95
|
+
method="password"
|
|
96
|
+
registerLink="/register"
|
|
97
|
+
forgotPasswordLink="/forgot-password"
|
|
98
|
+
/>;
|