@kirill.konshin/react 0.0.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/.ctirc +11 -0
- package/.turbo/turbo-build.log +44 -0
- package/dist/apiCall.d.ts +6 -0
- package/dist/apiCall.d.ts.map +1 -0
- package/dist/apiCall.js +23 -0
- package/dist/apiCall.js.map +1 -0
- package/dist/form/client.d.ts +7 -0
- package/dist/form/client.d.ts.map +1 -0
- package/dist/form/client.js +46 -0
- package/dist/form/client.js.map +1 -0
- package/dist/form/form.d.ts +59 -0
- package/dist/form/form.d.ts.map +1 -0
- package/dist/form/form.js +87 -0
- package/dist/form/form.js.map +1 -0
- package/dist/form/index.d.ts +3 -0
- package/dist/form/index.d.ts.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +26 -0
- package/dist/index.js.map +1 -0
- package/dist/keyboard.d.ts +10 -0
- package/dist/keyboard.d.ts.map +1 -0
- package/dist/keyboard.js +42 -0
- package/dist/keyboard.js.map +1 -0
- package/dist/useFetch.d.ts +2 -0
- package/dist/useFetch.d.ts.map +1 -0
- package/dist/useFetch.js +20 -0
- package/dist/useFetch.js.map +1 -0
- package/dist/useFetcher.d.ts +10 -0
- package/dist/useFetcher.d.ts.map +1 -0
- package/dist/useFetcher.js +43 -0
- package/dist/useFetcher.js.map +1 -0
- package/package.json +59 -0
- package/src/apiCall.ts +25 -0
- package/src/form/client.tsx +73 -0
- package/src/form/form.tsx +178 -0
- package/src/form/index.ts +2 -0
- package/src/index.ts +5 -0
- package/src/keyboard.tsx +58 -0
- package/src/useFetch.ts +29 -0
- package/src/useFetcher.ts +54 -0
- package/tsconfig.json +10 -0
- package/turbo.json +10 -0
- package/vite.config.ts +2 -0
package/.ctirc
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
[36mvite v7.0.6 [32mbuilding SSR bundle for production...[36m[39m
|
|
2
|
+
create succeeded: /home/runner/work/utils/utils/packages/react/src
|
|
3
|
+
transforming...
|
|
4
|
+
[32m✓[39m 8 modules transformed.
|
|
5
|
+
rendering chunks...
|
|
6
|
+
|
|
7
|
+
[vite:dts] Start generate declaration files...
|
|
8
|
+
[96msrc/form/form.tsx[0m:[93m1[0m:[93m10[0m - [91merror[0m[90m TS2724: [0m'"zod"' has no exported member named 'typeToFlattenedError'. Did you mean 'ZodFlattenedError'?
|
|
9
|
+
|
|
10
|
+
[7m1[0m import { typeToFlattenedError, z, SafeParseReturnType, TypeOf } from 'zod';
|
|
11
|
+
[7m [0m [91m ~~~~~~~~~~~~~~~~~~~~[0m
|
|
12
|
+
[96msrc/form/form.tsx[0m:[93m1[0m:[93m35[0m - [91merror[0m[90m TS2614: [0mModule '"zod"' has no exported member 'SafeParseReturnType'. Did you mean to use 'import SafeParseReturnType from "zod"' instead?
|
|
13
|
+
|
|
14
|
+
[7m1[0m import { typeToFlattenedError, z, SafeParseReturnType, TypeOf } from 'zod';
|
|
15
|
+
[7m [0m [91m ~~~~~~~~~~~~~~~~~~~[0m
|
|
16
|
+
[96msrc/form/form.tsx[0m:[93m7[0m:[93m61[0m - [91merror[0m[90m TS2769: [0mNo overload matches this call.
|
|
17
|
+
Overload 1 of 2, '(params?: string | { error?: string | $ZodErrorMap<$ZodIssueInvalidType<unknown>> | undefined; message?: string | undefined; } | undefined): ZodString', gave the following error.
|
|
18
|
+
Object literal may only specify known properties, and 'required_error' does not exist in type '{ error?: string | $ZodErrorMap<$ZodIssueInvalidType<unknown>> | undefined; message?: string | undefined; }'.
|
|
19
|
+
Overload 2 of 2, '(params?: string | { error?: string | $ZodErrorMap<$ZodIssueInvalidType<unknown>> | undefined; message?: string | undefined; } | undefined): $ZodType<...>', gave the following error.
|
|
20
|
+
Object literal may only specify known properties, and 'required_error' does not exist in type '{ error?: string | $ZodErrorMap<$ZodIssueInvalidType<unknown>> | undefined; message?: string | undefined; }'.
|
|
21
|
+
|
|
22
|
+
[7m7[0m export const stringRequired = (): z.ZodString => z.string({ required_error: nonEmpty }).min(1, nonEmpty);
|
|
23
|
+
[7m [0m [91m ~~~~~~~~~~~~~~[0m
|
|
24
|
+
|
|
25
|
+
[96msrc/form/form.tsx[0m:[93m12[0m:[93m46[0m - [91merror[0m[90m TS2694: [0mNamespace '"/home/runner/work/utils/utils/node_modules/zod/v4/classic/external"' has no exported member 'ZodEffects'.
|
|
26
|
+
|
|
27
|
+
[7m12[0m export type ZodObject = z.ZodObject<any> | z.ZodEffects<z.ZodObject<any>>; // z.ZodType<any, any, any>
|
|
28
|
+
[7m [0m [91m ~~~~~~~~~~[0m
|
|
29
|
+
[96msrc/form/form.tsx[0m:[93m43[0m:[93m56[0m - [91merror[0m[90m TS2694: [0mNamespace '"/home/runner/work/utils/utils/node_modules/zod/v4/classic/external"' has no exported member 'ZodEffects'.
|
|
30
|
+
|
|
31
|
+
[7m43[0m (schema as z.ZodObject<any>).shape || (schema as z.ZodEffects<z.ZodObject<any>>).sourceType().shape;
|
|
32
|
+
[7m [0m [91m ~~~~~~~~~~[0m
|
|
33
|
+
|
|
34
|
+
[2mdist/[22m[36museFetch.js [39m[1m[2m0.57 kB[22m[1m[22m[2m │ map: 1.69 kB[22m
|
|
35
|
+
[2mdist/[22m[36mindex.js [39m[1m[2m0.67 kB[22m[1m[22m[2m │ map: 0.10 kB[22m
|
|
36
|
+
[2mdist/[22m[36mapiCall.js [39m[1m[2m0.69 kB[22m[1m[22m[2m │ map: 1.45 kB[22m
|
|
37
|
+
[2mdist/[22m[36museFetcher.js [39m[1m[2m1.18 kB[22m[1m[22m[2m │ map: 2.62 kB[22m
|
|
38
|
+
[2mdist/[22m[36mkeyboard.js [39m[1m[2m1.24 kB[22m[1m[22m[2m │ map: 2.61 kB[22m
|
|
39
|
+
[2mdist/[22m[36mform/client.js [39m[1m[2m1.58 kB[22m[1m[22m[2m │ map: 3.88 kB[22m
|
|
40
|
+
[2mdist/[22m[36mform/form.js [39m[1m[2m2.87 kB[22m[1m[22m[2m │ map: 7.95 kB[22m
|
|
41
|
+
[vite:dts] Declaration files built in 1680ms.
|
|
42
|
+
|
|
43
|
+
[32m✓ built in 2.40s[39m
|
|
44
|
+
Updated package.json with exports
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"apiCall.d.ts","sourceRoot":"","sources":["../src/apiCall.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,eAAe,qBAAqB,CAAC;AAElD,MAAM,MAAM,YAAY,CAAC,CAAC,IAAI,QAAQ,GAAG;IAAE,IAAI,CAAC,EAAE,CAAC,CAAA;CAAE,CAAC;AAEtD,wBAAsB,OAAO,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CAoB1F"}
|
package/dist/apiCall.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
const jsonContentType = "application/json";
|
|
2
|
+
async function apiCall(url, init) {
|
|
3
|
+
const useBodyAsIs = !init?.body || init?.body instanceof FormData || typeof init?.body === "string";
|
|
4
|
+
const res = await fetch(url, {
|
|
5
|
+
method: "POST",
|
|
6
|
+
...init,
|
|
7
|
+
body: useBodyAsIs ? init?.body : JSON.stringify(init.body),
|
|
8
|
+
headers: {
|
|
9
|
+
...init?.headers,
|
|
10
|
+
...useBodyAsIs ? {} : { "Content-Type": jsonContentType }
|
|
11
|
+
}
|
|
12
|
+
});
|
|
13
|
+
if (res.headers.get("Content-Type")?.includes(jsonContentType)) {
|
|
14
|
+
res.data = await res.json();
|
|
15
|
+
}
|
|
16
|
+
if (!res.ok) throw new Error(res.statusText, { cause: res });
|
|
17
|
+
return res;
|
|
18
|
+
}
|
|
19
|
+
export {
|
|
20
|
+
apiCall,
|
|
21
|
+
jsonContentType
|
|
22
|
+
};
|
|
23
|
+
//# sourceMappingURL=apiCall.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"apiCall.js","sources":["../src/apiCall.ts"],"sourcesContent":["export const jsonContentType = 'application/json';\n\nexport type DataResponse<R> = Response & { data?: R };\n\nexport async function apiCall<R>(url: string, init?: RequestInit): Promise<DataResponse<R>> {\n const useBodyAsIs = !init?.body || init?.body instanceof FormData || typeof init?.body === 'string';\n\n const res: DataResponse<R> = await fetch(url, {\n method: 'POST',\n ...init,\n body: useBodyAsIs ? init?.body : JSON.stringify(init.body),\n headers: {\n ...init?.headers,\n ...(useBodyAsIs ? {} : { 'Content-Type': jsonContentType }),\n },\n });\n\n if (res.headers.get('Content-Type')?.includes(jsonContentType)) {\n res.data = await res.json();\n }\n\n if (!res.ok) throw new Error(res.statusText, { cause: res });\n\n return res;\n}\n"],"names":[],"mappings":"AAAO,MAAM,kBAAkB;AAI/B,eAAsB,QAAW,KAAa,MAA8C;AACxF,QAAM,cAAc,CAAC,MAAM,QAAQ,MAAM,gBAAgB,YAAY,OAAO,MAAM,SAAS;AAE3F,QAAM,MAAuB,MAAM,MAAM,KAAK;AAAA,IAC1C,QAAQ;AAAA,IACR,GAAG;AAAA,IACH,MAAM,cAAc,MAAM,OAAO,KAAK,UAAU,KAAK,IAAI;AAAA,IACzD,SAAS;AAAA,MACL,GAAG,MAAM;AAAA,MACT,GAAI,cAAc,CAAA,IAAK,EAAE,gBAAgB,gBAAA;AAAA,IAAgB;AAAA,EAC7D,CACH;AAED,MAAI,IAAI,QAAQ,IAAI,cAAc,GAAG,SAAS,eAAe,GAAG;AAC5D,QAAI,OAAO,MAAM,IAAI,KAAA;AAAA,EACzB;AAEA,MAAI,CAAC,IAAI,GAAI,OAAM,IAAI,MAAM,IAAI,YAAY,EAAE,OAAO,KAAK;AAE3D,SAAO;AACX;"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { MaybeTypeOf, Validation } from './form';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
export declare function createClient<S extends z.ZodObject<any>>(schema: S): {
|
|
4
|
+
useValidation: (actionFn: (data: FormData) => Promise<Validation<S>>, initialData?: MaybeTypeOf<S>) => [state: Validation<S>, dispatch: (payload: FormData) => void, isPending: boolean];
|
|
5
|
+
useValidationTransition: (actionFn: (data: FormData) => Promise<Validation<S>>, initialData?: MaybeTypeOf<S>) => [Validation<S>, (formData: FormData) => Promise<Validation<S>>, boolean];
|
|
6
|
+
};
|
|
7
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/form/client.tsx"],"names":[],"mappings":"AAGA,OAAO,EAAU,WAAW,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC;AACzD,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB,wBAAgB,YAAY,CAAC,CAAC,SAAS,CAAC,CAAC,SAAS,CAAC,GAAG,CAAC,EACnD,MAAM,EAAE,CAAC,GACV;IACC,aAAa,EAAE,CACX,QAAQ,EAAE,CAAC,IAAI,EAAE,QAAQ,KAAK,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,EACpD,WAAW,CAAC,EAAE,WAAW,CAAC,CAAC,CAAC,KAC3B,CAAC,KAAK,EAAE,UAAU,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,OAAO,EAAE,QAAQ,KAAK,IAAI,EAAE,SAAS,EAAE,OAAO,CAAC,CAAC;IACvF,uBAAuB,EAAE,CACrB,QAAQ,EAAE,CAAC,IAAI,EAAE,QAAQ,KAAK,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,EACpD,WAAW,CAAC,EAAE,WAAW,CAAC,CAAC,CAAC,KAC3B,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,CAAC,QAAQ,EAAE,QAAQ,KAAK,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC;CACjF,CAqDA"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useTransition, useState, useCallback, useActionState } from "react";
|
|
3
|
+
import { create } from "./form.js";
|
|
4
|
+
const FORM_DEBUG = process.env.NEXT_PUBLIC_FORM_DEBUG === "true";
|
|
5
|
+
function createClient(schema) {
|
|
6
|
+
const { validate } = create(schema);
|
|
7
|
+
function useValidationCallback(actionFn) {
|
|
8
|
+
return useCallback(
|
|
9
|
+
async (formData) => {
|
|
10
|
+
const clientRes = validate(formData);
|
|
11
|
+
console.log("Client validation", FORM_DEBUG ? "ignored" : "active", clientRes);
|
|
12
|
+
if (!clientRes.success && !FORM_DEBUG) return clientRes;
|
|
13
|
+
const serverRes = await actionFn(formData);
|
|
14
|
+
console.log("Server validation", serverRes);
|
|
15
|
+
return serverRes;
|
|
16
|
+
},
|
|
17
|
+
[actionFn]
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
function useValidation(actionFn, initialData = {}) {
|
|
21
|
+
const cb = useValidationCallback(actionFn);
|
|
22
|
+
return useActionState(async (_, data) => cb(data), {
|
|
23
|
+
success: false,
|
|
24
|
+
data: initialData
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
function useValidationTransition(actionFn, initialData = {}) {
|
|
28
|
+
const [isPending, startTransition] = useTransition();
|
|
29
|
+
const [state, setState] = useState({ success: false, data: initialData });
|
|
30
|
+
const cb = useValidationCallback(actionFn);
|
|
31
|
+
const wrappedCb = useCallback(
|
|
32
|
+
(formData) => {
|
|
33
|
+
const promise = cb(formData);
|
|
34
|
+
startTransition(() => promise.then(setState));
|
|
35
|
+
return promise;
|
|
36
|
+
},
|
|
37
|
+
[cb, startTransition]
|
|
38
|
+
);
|
|
39
|
+
return [state, wrappedCb, isPending];
|
|
40
|
+
}
|
|
41
|
+
return { useValidation, useValidationTransition };
|
|
42
|
+
}
|
|
43
|
+
export {
|
|
44
|
+
createClient
|
|
45
|
+
};
|
|
46
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","sources":["../../src/form/client.tsx"],"sourcesContent":["'use client';\n\nimport { useActionState, useCallback, useState, useTransition } from 'react';\nimport { create, MaybeTypeOf, Validation } from './form';\nimport { z } from 'zod';\n\nconst FORM_DEBUG = process.env.NEXT_PUBLIC_FORM_DEBUG === 'true';\n\nexport function createClient<S extends z.ZodObject<any>>(\n schema: S,\n): {\n useValidation: (\n actionFn: (data: FormData) => Promise<Validation<S>>,\n initialData?: MaybeTypeOf<S>,\n ) => [state: Validation<S>, dispatch: (payload: FormData) => void, isPending: boolean];\n useValidationTransition: (\n actionFn: (data: FormData) => Promise<Validation<S>>,\n initialData?: MaybeTypeOf<S>,\n ) => [Validation<S>, (formData: FormData) => Promise<Validation<S>>, boolean];\n} {\n const { validate } = create(schema);\n\n function useValidationCallback(\n actionFn: (data: FormData) => Promise<Validation<S>>,\n ): (formData: FormData) => Promise<Validation<S>> {\n return useCallback(\n async (formData: FormData) => {\n const clientRes = validate(formData);\n console.log('Client validation', FORM_DEBUG ? 'ignored' : 'active', clientRes);\n if (!clientRes.success && !FORM_DEBUG) return clientRes;\n\n const serverRes = await actionFn(formData);\n console.log('Server validation', serverRes);\n return serverRes;\n },\n [actionFn],\n );\n }\n\n function useValidation(\n actionFn: (data: FormData) => Promise<Validation<S>>,\n initialData: MaybeTypeOf<S> = {} as MaybeTypeOf<S>,\n ): [state: Validation<S>, dispatch: (payload: FormData) => void, isPending: boolean] {\n const cb = useValidationCallback(actionFn);\n\n return useActionState<Validation<S>, FormData>(async (_, data) => cb(data), {\n success: false,\n data: initialData,\n });\n }\n\n function useValidationTransition(\n actionFn: (data: FormData) => Promise<Validation<S>>,\n initialData: MaybeTypeOf<S> = {} as MaybeTypeOf<S>,\n ): [Validation<S>, (formData: FormData) => Promise<Validation<S>>, boolean] {\n const [isPending, startTransition] = useTransition();\n const [state, setState] = useState<Validation<S>>({ success: false, data: initialData });\n const cb = useValidationCallback(actionFn);\n\n const wrappedCb = useCallback(\n (formData: FormData) => {\n const promise = cb(formData);\n startTransition(() => promise.then(setState));\n return promise;\n },\n [cb, startTransition],\n );\n\n return [state, wrappedCb, isPending];\n }\n\n return { useValidation, useValidationTransition };\n}\n"],"names":[],"mappings":";;;AAMA,MAAM,aAAa,QAAQ,IAAI,2BAA2B;AAEnD,SAAS,aACZ,QAUF;AACE,QAAM,EAAE,SAAA,IAAa,OAAO,MAAM;AAElC,WAAS,sBACL,UAC8C;AAC9C,WAAO;AAAA,MACH,OAAO,aAAuB;AAC1B,cAAM,YAAY,SAAS,QAAQ;AACnC,gBAAQ,IAAI,qBAAqB,aAAa,YAAY,UAAU,SAAS;AAC7E,YAAI,CAAC,UAAU,WAAW,CAAC,WAAY,QAAO;AAE9C,cAAM,YAAY,MAAM,SAAS,QAAQ;AACzC,gBAAQ,IAAI,qBAAqB,SAAS;AAC1C,eAAO;AAAA,MACX;AAAA,MACA,CAAC,QAAQ;AAAA,IAAA;AAAA,EAEjB;AAEA,WAAS,cACL,UACA,cAA8B,IACmD;AACjF,UAAM,KAAK,sBAAsB,QAAQ;AAEzC,WAAO,eAAwC,OAAO,GAAG,SAAS,GAAG,IAAI,GAAG;AAAA,MACxE,SAAS;AAAA,MACT,MAAM;AAAA,IAAA,CACT;AAAA,EACL;AAEA,WAAS,wBACL,UACA,cAA8B,IAC0C;AACxE,UAAM,CAAC,WAAW,eAAe,IAAI,cAAA;AACrC,UAAM,CAAC,OAAO,QAAQ,IAAI,SAAwB,EAAE,SAAS,OAAO,MAAM,aAAa;AACvF,UAAM,KAAK,sBAAsB,QAAQ;AAEzC,UAAM,YAAY;AAAA,MACd,CAAC,aAAuB;AACpB,cAAM,UAAU,GAAG,QAAQ;AAC3B,wBAAgB,MAAM,QAAQ,KAAK,QAAQ,CAAC;AAC5C,eAAO;AAAA,MACX;AAAA,MACA,CAAC,IAAI,eAAe;AAAA,IAAA;AAGxB,WAAO,CAAC,OAAO,WAAW,SAAS;AAAA,EACvC;AAEA,SAAO,EAAE,eAAe,wBAAA;AAC5B;"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { typeToFlattenedError, z, SafeParseReturnType, TypeOf } from 'zod';
|
|
2
|
+
import { Context, FC } from 'react';
|
|
3
|
+
export declare const stringRequired: () => z.ZodString;
|
|
4
|
+
export declare const maxLength: (schema: z.ZodString) => number;
|
|
5
|
+
export declare const minLength: (schema: z.ZodString) => number;
|
|
6
|
+
export declare const isRequired: (schema: z.ZodString) => boolean;
|
|
7
|
+
export type ZodObject = z.ZodObject<any> | z.ZodEffects<z.ZodObject<any>>;
|
|
8
|
+
export type MaybeTypeOf<S extends ZodObject> = Partial<TypeOf<S>>;
|
|
9
|
+
export type SafeTypeOf<S extends ZodObject> = SafeParseReturnType<TypeOf<S>, TypeOf<S>>['data'];
|
|
10
|
+
export type Errors<S extends ZodObject> = typeToFlattenedError<TypeOf<S>>['fieldErrors'];
|
|
11
|
+
export type Validation<S extends ZodObject> = {
|
|
12
|
+
success: true;
|
|
13
|
+
data: SafeTypeOf<S>;
|
|
14
|
+
errors?: never;
|
|
15
|
+
} | {
|
|
16
|
+
success: false;
|
|
17
|
+
data?: MaybeTypeOf<S>;
|
|
18
|
+
errors?: Errors<S>;
|
|
19
|
+
};
|
|
20
|
+
export declare const FormContext: Context<{
|
|
21
|
+
schema: ZodObject;
|
|
22
|
+
}>;
|
|
23
|
+
export interface FormProps<S extends ZodObject> {
|
|
24
|
+
schema: S;
|
|
25
|
+
children: any;
|
|
26
|
+
}
|
|
27
|
+
export declare const Form: FC<FormProps<any>>;
|
|
28
|
+
export declare function create<S extends ZodObject>(schema: S): {
|
|
29
|
+
register: (name: keyof TypeOf<S>, data?: MaybeTypeOf<S>, errors?: Errors<S>, mui?: boolean) => {
|
|
30
|
+
label?: any;
|
|
31
|
+
helperText?: string;
|
|
32
|
+
error?: boolean;
|
|
33
|
+
name: keyof z.TypeOf<S>;
|
|
34
|
+
id: keyof z.TypeOf<S>;
|
|
35
|
+
required: boolean;
|
|
36
|
+
maxLength: number;
|
|
37
|
+
type: string;
|
|
38
|
+
defaultValue?: Partial<z.TypeOf<S>>[keyof z.TypeOf<S>];
|
|
39
|
+
};
|
|
40
|
+
validate: (formData: FormData) => Validation<S>;
|
|
41
|
+
validationError: (data: MaybeTypeOf<S>, errors: Errors<S>) => Validation<S>;
|
|
42
|
+
};
|
|
43
|
+
interface FieldProps<S extends ZodObject> {
|
|
44
|
+
children?: any;
|
|
45
|
+
name: keyof TypeOf<S>;
|
|
46
|
+
errors?: Validation<S>['errors'];
|
|
47
|
+
hint?: string;
|
|
48
|
+
className?: string;
|
|
49
|
+
labelProps?: any;
|
|
50
|
+
[key: string]: any;
|
|
51
|
+
}
|
|
52
|
+
export declare const Field: FC<FieldProps<any>>;
|
|
53
|
+
export interface HintProps {
|
|
54
|
+
children: any;
|
|
55
|
+
error?: boolean;
|
|
56
|
+
}
|
|
57
|
+
export declare const Hint: FC<HintProps>;
|
|
58
|
+
export {};
|
|
59
|
+
//# sourceMappingURL=form.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"form.d.ts","sourceRoot":"","sources":["../../src/form/form.tsx"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { createContext, memo, useMemo, useContext } from "react";
|
|
4
|
+
import clsx from "clsx";
|
|
5
|
+
const nonEmpty = "This field cannot be empty";
|
|
6
|
+
const stringRequired = () => z.string({ required_error: nonEmpty }).min(1, nonEmpty);
|
|
7
|
+
const maxLength = (schema) => schema.maxLength || 0;
|
|
8
|
+
const minLength = (schema) => schema.minLength || 0;
|
|
9
|
+
const isRequired = (schema) => minLength(schema) > 0;
|
|
10
|
+
const FormContext = createContext(null);
|
|
11
|
+
const Form = memo(function Form2({ schema, children }) {
|
|
12
|
+
const value = useMemo(() => ({ schema }), [schema]);
|
|
13
|
+
return /* @__PURE__ */ jsx(FormContext.Provider, { value, children });
|
|
14
|
+
});
|
|
15
|
+
const getShape = (schema) => schema.shape || schema.sourceType().shape;
|
|
16
|
+
function create(schema) {
|
|
17
|
+
if (!getShape(schema)) {
|
|
18
|
+
throw new Error("Invalid schema: only z.object() or z.object().refine() are supported");
|
|
19
|
+
}
|
|
20
|
+
function register(name, data, errors, mui = false) {
|
|
21
|
+
const field = getShape(schema)[name];
|
|
22
|
+
return {
|
|
23
|
+
name,
|
|
24
|
+
id: name,
|
|
25
|
+
required: isRequired(field),
|
|
26
|
+
maxLength: maxLength(field),
|
|
27
|
+
type: field.isEmail ? "email" : name.includes("password") ? "password" : "text",
|
|
28
|
+
defaultValue: data?.[name],
|
|
29
|
+
...mui ? {
|
|
30
|
+
label: field.description,
|
|
31
|
+
helperText: errors?.[name]?.join(", "),
|
|
32
|
+
error: !!errors?.[name]?.length
|
|
33
|
+
} : {}
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function validationError(data, errors) {
|
|
37
|
+
return {
|
|
38
|
+
success: false,
|
|
39
|
+
data,
|
|
40
|
+
// data is undefined if there are errors
|
|
41
|
+
errors
|
|
42
|
+
// Next.js will butcher error object, so we provide something more primitive
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function validate(formData) {
|
|
46
|
+
const rawData = Object.fromEntries(formData);
|
|
47
|
+
const { error, data } = schema.safeParse(rawData);
|
|
48
|
+
if (error) {
|
|
49
|
+
return validationError(rawData, error.flatten().fieldErrors);
|
|
50
|
+
}
|
|
51
|
+
return { success: true, data };
|
|
52
|
+
}
|
|
53
|
+
return { register, validate, validationError };
|
|
54
|
+
}
|
|
55
|
+
const Field = memo(function Field2({
|
|
56
|
+
children,
|
|
57
|
+
name,
|
|
58
|
+
errors,
|
|
59
|
+
hint,
|
|
60
|
+
className,
|
|
61
|
+
labelProps,
|
|
62
|
+
...props
|
|
63
|
+
}) {
|
|
64
|
+
const { schema } = useContext(FormContext);
|
|
65
|
+
const { description } = getShape(schema)[name];
|
|
66
|
+
return /* @__PURE__ */ jsxs("div", { ...props, className: clsx("form-row", className), children: [
|
|
67
|
+
description && /* @__PURE__ */ jsx("label", { ...labelProps, htmlFor: name, children: description }),
|
|
68
|
+
children,
|
|
69
|
+
hint && /* @__PURE__ */ jsx(Hint, { children: hint }),
|
|
70
|
+
errors?.[name]?.map((e) => /* @__PURE__ */ jsx(Hint, { error: true, children: e }, e))
|
|
71
|
+
] });
|
|
72
|
+
});
|
|
73
|
+
const Hint = memo(function Hint2({ children, error }) {
|
|
74
|
+
return /* @__PURE__ */ jsx("div", { className: `hint ${error ? "hint-error" : ""}`, children });
|
|
75
|
+
});
|
|
76
|
+
export {
|
|
77
|
+
Field,
|
|
78
|
+
Form,
|
|
79
|
+
FormContext,
|
|
80
|
+
Hint,
|
|
81
|
+
create,
|
|
82
|
+
isRequired,
|
|
83
|
+
maxLength,
|
|
84
|
+
minLength,
|
|
85
|
+
stringRequired
|
|
86
|
+
};
|
|
87
|
+
//# sourceMappingURL=form.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"form.js","sources":["../../src/form/form.tsx"],"sourcesContent":["import { typeToFlattenedError, z, SafeParseReturnType, TypeOf } from 'zod';\nimport { Context, createContext, useContext, useMemo, JSX, FC, memo } from 'react';\nimport clsx from 'clsx';\n\nconst nonEmpty = 'This field cannot be empty';\n\nexport const stringRequired = (): z.ZodString => z.string({ required_error: nonEmpty }).min(1, nonEmpty);\nexport const maxLength = (schema: z.ZodString): number => schema.maxLength || 0;\nexport const minLength = (schema: z.ZodString): number => schema.minLength || 0;\nexport const isRequired = (schema: z.ZodString): boolean => minLength(schema) > 0;\n\nexport type ZodObject = z.ZodObject<any> | z.ZodEffects<z.ZodObject<any>>; // z.ZodType<any, any, any>\nexport type MaybeTypeOf<S extends ZodObject> = Partial<TypeOf<S>>;\nexport type SafeTypeOf<S extends ZodObject> = SafeParseReturnType<TypeOf<S>, TypeOf<S>>['data'];\nexport type Errors<S extends ZodObject> = typeToFlattenedError<TypeOf<S>>['fieldErrors'];\nexport type Validation<S extends ZodObject> =\n | {\n success: true; // this is true only if form was validated successfully\n data: SafeTypeOf<S>;\n errors?: never;\n }\n | {\n success: false;\n data?: MaybeTypeOf<S>;\n errors?: Errors<S>;\n };\n\nexport const FormContext: Context<{\n schema: ZodObject;\n}> = createContext(null as never);\n\nexport interface FormProps<S extends ZodObject> {\n schema: S;\n children: any;\n}\n\nexport const Form: FC<FormProps<any>> = memo(function Form({ schema, children }) {\n const value = useMemo(() => ({ schema }), [schema]);\n return <FormContext.Provider value={value}>{children}</FormContext.Provider>;\n});\n\nconst getShape = <S extends ZodObject>(schema: S) =>\n (schema as z.ZodObject<any>).shape || (schema as z.ZodEffects<z.ZodObject<any>>).sourceType().shape;\n\nexport function create<S extends ZodObject>(\n schema: S,\n): {\n register: (\n name: keyof TypeOf<S>,\n data?: MaybeTypeOf<S>,\n errors?: Errors<S>,\n mui?: boolean,\n ) => {\n label?: any;\n helperText?: string;\n error?: boolean;\n name: keyof z.TypeOf<S>;\n id: keyof z.TypeOf<S>;\n required: boolean;\n maxLength: number;\n type: string;\n defaultValue?: Partial<z.TypeOf<S>>[keyof z.TypeOf<S>];\n };\n validate: (formData: FormData) => Validation<S>;\n validationError: (data: MaybeTypeOf<S>, errors: Errors<S>) => Validation<S>;\n} {\n if (!getShape(schema)) {\n throw new Error('Invalid schema: only z.object() or z.object().refine() are supported');\n }\n\n function register(\n name: keyof TypeOf<S>,\n data?: MaybeTypeOf<S>,\n errors?: Errors<S>,\n mui: boolean = false,\n ): {\n label?: any;\n helperText?: string;\n error?: boolean;\n name: keyof z.TypeOf<S>;\n id: keyof z.TypeOf<S>;\n required: boolean;\n maxLength: number;\n type: string;\n defaultValue?: Partial<z.TypeOf<S>>[keyof z.TypeOf<S>];\n } {\n const field = getShape(schema)[name];\n return {\n name,\n id: name,\n required: isRequired(field),\n maxLength: maxLength(field),\n type: field.isEmail ? 'email' : (name as string).includes('password') ? 'password' : 'text',\n defaultValue: data?.[name],\n ...(mui\n ? {\n label: field.description,\n helperText: errors?.[name]?.join(', '),\n error: !!errors?.[name]?.length,\n }\n : {}),\n };\n }\n\n function validationError(data: MaybeTypeOf<S>, errors: Errors<S>): Validation<S> {\n return {\n success: false,\n data, // data is undefined if there are errors\n errors, // Next.js will butcher error object, so we provide something more primitive\n };\n }\n\n function validate(formData: FormData): Validation<S> {\n const rawData = Object.fromEntries(formData) as TypeOf<S>;\n const { error, data } = schema.safeParse(rawData);\n\n // console.log('Validate result', { error, data, rawData });\n\n if (error) {\n // data is undefined if there are errors\n // Next.js will butcher error object, so we provide something more primitive\n return validationError(rawData, error.flatten().fieldErrors as any);\n }\n\n return { success: true, data };\n }\n\n return { register, validate, validationError };\n}\n\ninterface FieldProps<S extends ZodObject> {\n children?: any;\n name: keyof TypeOf<S>;\n errors?: Validation<S>['errors'];\n hint?: string;\n className?: string;\n labelProps?: any;\n [key: string]: any;\n}\n\nexport const Field: FC<FieldProps<any>> = memo(function Field({\n children,\n name,\n errors,\n hint,\n className,\n labelProps,\n ...props\n}) {\n const { schema } = useContext(FormContext);\n const { description } = getShape(schema)[name];\n\n return (\n <div {...props} className={clsx('form-row', className)}>\n {description && (\n <label {...labelProps} htmlFor={name}>\n {description}\n </label>\n )}\n {children}\n {hint && <Hint>{hint}</Hint>}\n {errors?.[name]?.map((e: string) => (\n <Hint error key={e}>\n {e}\n </Hint>\n ))}\n </div>\n );\n});\n\nexport interface HintProps {\n children: any;\n error?: boolean;\n}\n\nexport const Hint: FC<HintProps> = memo(function Hint({ children, error }) {\n return <div className={`hint ${error ? 'hint-error' : ''}`}>{children}</div>;\n});\n"],"names":["Form","Field","Hint"],"mappings":";;;;AAIA,MAAM,WAAW;AAEV,MAAM,iBAAiB,MAAmB,EAAE,OAAO,EAAE,gBAAgB,UAAU,EAAE,IAAI,GAAG,QAAQ;AAChG,MAAM,YAAY,CAAC,WAAgC,OAAO,aAAa;AACvE,MAAM,YAAY,CAAC,WAAgC,OAAO,aAAa;AACvE,MAAM,aAAa,CAAC,WAAiC,UAAU,MAAM,IAAI;AAkBzE,MAAM,cAER,cAAc,IAAa;AAOzB,MAAM,OAA2B,KAAK,SAASA,MAAK,EAAE,QAAQ,YAAY;AAC7E,QAAM,QAAQ,QAAQ,OAAO,EAAE,WAAW,CAAC,MAAM,CAAC;AAClD,SAAO,oBAAC,YAAY,UAAZ,EAAqB,OAAe,SAAA,CAAS;AACzD,CAAC;AAED,MAAM,WAAW,CAAsB,WAClC,OAA4B,SAAU,OAA0C,aAAa;AAE3F,SAAS,OACZ,QAoBF;AACE,MAAI,CAAC,SAAS,MAAM,GAAG;AACnB,UAAM,IAAI,MAAM,sEAAsE;AAAA,EAC1F;AAEA,WAAS,SACL,MACA,MACA,QACA,MAAe,OAWjB;AACE,UAAM,QAAQ,SAAS,MAAM,EAAE,IAAI;AACnC,WAAO;AAAA,MACH;AAAA,MACA,IAAI;AAAA,MACJ,UAAU,WAAW,KAAK;AAAA,MAC1B,WAAW,UAAU,KAAK;AAAA,MAC1B,MAAM,MAAM,UAAU,UAAW,KAAgB,SAAS,UAAU,IAAI,aAAa;AAAA,MACrF,cAAc,OAAO,IAAI;AAAA,MACzB,GAAI,MACE;AAAA,QACI,OAAO,MAAM;AAAA,QACb,YAAY,SAAS,IAAI,GAAG,KAAK,IAAI;AAAA,QACrC,OAAO,CAAC,CAAC,SAAS,IAAI,GAAG;AAAA,MAAA,IAE7B,CAAA;AAAA,IAAC;AAAA,EAEf;AAEA,WAAS,gBAAgB,MAAsB,QAAkC;AAC7E,WAAO;AAAA,MACH,SAAS;AAAA,MACT;AAAA;AAAA,MACA;AAAA;AAAA,IAAA;AAAA,EAER;AAEA,WAAS,SAAS,UAAmC;AACjD,UAAM,UAAU,OAAO,YAAY,QAAQ;AAC3C,UAAM,EAAE,OAAO,KAAA,IAAS,OAAO,UAAU,OAAO;AAIhD,QAAI,OAAO;AAGP,aAAO,gBAAgB,SAAS,MAAM,QAAA,EAAU,WAAkB;AAAA,IACtE;AAEA,WAAO,EAAE,SAAS,MAAM,KAAA;AAAA,EAC5B;AAEA,SAAO,EAAE,UAAU,UAAU,gBAAA;AACjC;AAYO,MAAM,QAA6B,KAAK,SAASC,OAAM;AAAA,EAC1D;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,GAAG;AACP,GAAG;AACC,QAAM,EAAE,OAAA,IAAW,WAAW,WAAW;AACzC,QAAM,EAAE,YAAA,IAAgB,SAAS,MAAM,EAAE,IAAI;AAE7C,SACI,qBAAC,SAAK,GAAG,OAAO,WAAW,KAAK,YAAY,SAAS,GAChD,UAAA;AAAA,IAAA,mCACI,SAAA,EAAO,GAAG,YAAY,SAAS,MAC3B,UAAA,aACL;AAAA,IAEH;AAAA,IACA,QAAQ,oBAAC,MAAA,EAAM,UAAA,KAAA,CAAK;AAAA,IACpB,SAAS,IAAI,GAAG,IAAI,CAAC,MAClB,oBAAC,MAAA,EAAK,OAAK,MACN,UAAA,EAAA,GADY,CAEjB,CACH;AAAA,EAAA,GACL;AAER,CAAC;AAOM,MAAM,OAAsB,KAAK,SAASC,MAAK,EAAE,UAAU,SAAS;AACvE,SAAO,oBAAC,SAAI,WAAW,QAAQ,QAAQ,eAAe,EAAE,IAAK,SAAA,CAAS;AAC1E,CAAC;"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/form/index.ts"],"names":[],"mappings":"AAAA,cAAc,UAAU,CAAC;AACzB,cAAc,QAAQ,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,QAAQ,CAAC;AACvB,cAAc,WAAW,CAAC;AAC1B,cAAc,YAAY,CAAC;AAC3B,cAAc,YAAY,CAAC;AAC3B,cAAc,cAAc,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { createClient } from "./form/client.js";
|
|
2
|
+
import { Field, Form, FormContext, Hint, create, isRequired, maxLength, minLength, stringRequired } from "./form/form.js";
|
|
3
|
+
import { apiCall, jsonContentType } from "./apiCall.js";
|
|
4
|
+
import { HotkeysContext, HotkeysProvider, useHotkeys } from "./keyboard.js";
|
|
5
|
+
import { useFetch } from "./useFetch.js";
|
|
6
|
+
import { useFetcher } from "./useFetcher.js";
|
|
7
|
+
export {
|
|
8
|
+
Field,
|
|
9
|
+
Form,
|
|
10
|
+
FormContext,
|
|
11
|
+
Hint,
|
|
12
|
+
HotkeysContext,
|
|
13
|
+
HotkeysProvider,
|
|
14
|
+
apiCall,
|
|
15
|
+
create,
|
|
16
|
+
createClient,
|
|
17
|
+
isRequired,
|
|
18
|
+
jsonContentType,
|
|
19
|
+
maxLength,
|
|
20
|
+
minLength,
|
|
21
|
+
stringRequired,
|
|
22
|
+
useFetch,
|
|
23
|
+
useFetcher,
|
|
24
|
+
useHotkeys
|
|
25
|
+
};
|
|
26
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Dispatch, SetStateAction, FC, Context } from 'react';
|
|
2
|
+
export type Hotkeys = Record<KeyboardEvent['code'], (e: KeyboardEvent) => void>;
|
|
3
|
+
export type HotkeyContextType = {
|
|
4
|
+
enabled: boolean;
|
|
5
|
+
setEnabled: Dispatch<SetStateAction<boolean>>;
|
|
6
|
+
};
|
|
7
|
+
export declare const HotkeysContext: Context<HotkeyContextType>;
|
|
8
|
+
export declare const HotkeysProvider: FC<any>;
|
|
9
|
+
export declare const useHotkeys: (hotkeys: Hotkeys) => void;
|
|
10
|
+
//# sourceMappingURL=keyboard.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"keyboard.d.ts","sourceRoot":"","sources":["../src/keyboard.tsx"],"names":[],"mappings":"AAEA,OAAO,EAA+C,QAAQ,EAAE,cAAc,EAAc,EAAE,EAAE,OAAO,EAAE,MAAM,OAAO,CAAC;AAMvH,MAAM,MAAM,OAAO,GAAG,MAAM,CAAC,aAAa,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,EAAE,aAAa,KAAK,IAAI,CAAC,CAAC;AAEhF,MAAM,MAAM,iBAAiB,GAAG;IAC5B,OAAO,EAAE,OAAO,CAAC;IACjB,UAAU,EAAE,QAAQ,CAAC,cAAc,CAAC,OAAO,CAAC,CAAC,CAAC;CACjD,CAAC;AAEF,eAAO,MAAM,cAAc,EAAE,OAAO,CAAC,iBAAiB,CAGrC,CAAC;AAElB,eAAO,MAAM,eAAe,EAAE,EAAE,CAAC,GAAG,CAMnC,CAAC;AAEF,eAAO,MAAM,UAAU,GAAI,SAAS,OAAO,KAAG,IA6B7C,CAAC"}
|
package/dist/keyboard.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx } from "react/jsx-runtime";
|
|
3
|
+
import { createContext, useState, useMemo, useContext, useEffect } from "react";
|
|
4
|
+
const isCtrlOrMeta = (e) => e.metaKey || e.ctrlKey;
|
|
5
|
+
const EVENT = "keydown";
|
|
6
|
+
const HotkeysContext = createContext(null);
|
|
7
|
+
const HotkeysProvider = ({ children }) => {
|
|
8
|
+
const [enabled, setEnabled] = useState(true);
|
|
9
|
+
const control = useMemo(() => ({ enabled, setEnabled }), [enabled, setEnabled]);
|
|
10
|
+
return /* @__PURE__ */ jsx(HotkeysContext.Provider, { value: control, children });
|
|
11
|
+
};
|
|
12
|
+
const useHotkeys = (hotkeys) => {
|
|
13
|
+
const { enabled } = useContext(HotkeysContext);
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
if (typeof document === "undefined" || !enabled) {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
const ctrl = new AbortController();
|
|
19
|
+
window.addEventListener(
|
|
20
|
+
EVENT,
|
|
21
|
+
(e) => {
|
|
22
|
+
if (!isCtrlOrMeta(e)) return;
|
|
23
|
+
for (const [code, callback] of Object.entries(hotkeys)) {
|
|
24
|
+
if (e.code === code) {
|
|
25
|
+
callback(e);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
{ signal: ctrl.signal, capture: true }
|
|
31
|
+
);
|
|
32
|
+
return () => {
|
|
33
|
+
ctrl.abort();
|
|
34
|
+
};
|
|
35
|
+
}, [hotkeys, enabled]);
|
|
36
|
+
};
|
|
37
|
+
export {
|
|
38
|
+
HotkeysContext,
|
|
39
|
+
HotkeysProvider,
|
|
40
|
+
useHotkeys
|
|
41
|
+
};
|
|
42
|
+
//# sourceMappingURL=keyboard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"keyboard.js","sources":["../src/keyboard.tsx"],"sourcesContent":["'use client';\n\nimport { useEffect, createContext, useMemo, useState, Dispatch, SetStateAction, useContext, FC, Context } from 'react';\n\nconst isCtrlOrMeta = (e: KeyboardEvent) => e.metaKey || e.ctrlKey;\n\nconst EVENT = 'keydown';\n\nexport type Hotkeys = Record<KeyboardEvent['code'], (e: KeyboardEvent) => void>;\n\nexport type HotkeyContextType = {\n enabled: boolean;\n setEnabled: Dispatch<SetStateAction<boolean>>;\n};\n\nexport const HotkeysContext: Context<HotkeyContextType> = createContext<{\n enabled: boolean;\n setEnabled: Dispatch<SetStateAction<boolean>>;\n}>(null as never);\n\nexport const HotkeysProvider: FC<any> = ({ children }) => {\n const [enabled, setEnabled] = useState(true);\n\n const control = useMemo(() => ({ enabled, setEnabled }), [enabled, setEnabled]);\n\n return <HotkeysContext.Provider value={control}>{children}</HotkeysContext.Provider>;\n};\n\nexport const useHotkeys = (hotkeys: Hotkeys): void => {\n const { enabled } = useContext(HotkeysContext);\n\n useEffect(() => {\n if (typeof document === 'undefined' || !enabled) {\n return;\n }\n\n const ctrl = new AbortController();\n\n window.addEventListener(\n EVENT,\n (e: KeyboardEvent) => {\n if (!isCtrlOrMeta(e)) return;\n\n for (const [code, callback] of Object.entries(hotkeys)) {\n if (e.code === code) {\n callback(e);\n return;\n }\n }\n },\n { signal: ctrl.signal, capture: true },\n );\n\n return () => {\n ctrl.abort();\n };\n }, [hotkeys, enabled]);\n};\n"],"names":[],"mappings":";;;AAIA,MAAM,eAAe,CAAC,MAAqB,EAAE,WAAW,EAAE;AAE1D,MAAM,QAAQ;AASP,MAAM,iBAA6C,cAGvD,IAAa;AAET,MAAM,kBAA2B,CAAC,EAAE,eAAe;AACtD,QAAM,CAAC,SAAS,UAAU,IAAI,SAAS,IAAI;AAE3C,QAAM,UAAU,QAAQ,OAAO,EAAE,SAAS,eAAe,CAAC,SAAS,UAAU,CAAC;AAE9E,6BAAQ,eAAe,UAAf,EAAwB,OAAO,SAAU,UAAS;AAC9D;AAEO,MAAM,aAAa,CAAC,YAA2B;AAClD,QAAM,EAAE,QAAA,IAAY,WAAW,cAAc;AAE7C,YAAU,MAAM;AACZ,QAAI,OAAO,aAAa,eAAe,CAAC,SAAS;AAC7C;AAAA,IACJ;AAEA,UAAM,OAAO,IAAI,gBAAA;AAEjB,WAAO;AAAA,MACH;AAAA,MACA,CAAC,MAAqB;AAClB,YAAI,CAAC,aAAa,CAAC,EAAG;AAEtB,mBAAW,CAAC,MAAM,QAAQ,KAAK,OAAO,QAAQ,OAAO,GAAG;AACpD,cAAI,EAAE,SAAS,MAAM;AACjB,qBAAS,CAAC;AACV;AAAA,UACJ;AAAA,QACJ;AAAA,MACJ;AAAA,MACA,EAAE,QAAQ,KAAK,QAAQ,SAAS,KAAA;AAAA,IAAK;AAGzC,WAAO,MAAM;AACT,WAAK,MAAA;AAAA,IACT;AAAA,EACJ,GAAG,CAAC,SAAS,OAAO,CAAC;AACzB;"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useFetch.d.ts","sourceRoot":"","sources":["../src/useFetch.ts"],"names":[],"mappings":"AAOA,wBAAgB,QAAQ,CAAC,CAAC,EACtB,EAAE,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,OAAO,CAAC,CAAC,CAAC,EAClC,YAAY,GAAE,CAAC,GAAG,IAAW,GAC9B,CAAC,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,EAAE,OAAO,EAAE,KAAK,GAAG,SAAS,CAAC,CAkBnD"}
|
package/dist/useFetch.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useTransition, useState, useCallback } from "react";
|
|
3
|
+
function useFetch(fn, defaultValue = null) {
|
|
4
|
+
const [isPending, startTransition] = useTransition();
|
|
5
|
+
const [data, setData] = useState(defaultValue);
|
|
6
|
+
const [error, setError] = useState();
|
|
7
|
+
const actionFn = useCallback(
|
|
8
|
+
(...args) => {
|
|
9
|
+
const promise = fn(...args);
|
|
10
|
+
startTransition(() => promise.then(setData).catch(setError));
|
|
11
|
+
return promise;
|
|
12
|
+
},
|
|
13
|
+
[fn]
|
|
14
|
+
);
|
|
15
|
+
return [data, actionFn, isPending, error];
|
|
16
|
+
}
|
|
17
|
+
export {
|
|
18
|
+
useFetch
|
|
19
|
+
};
|
|
20
|
+
//# sourceMappingURL=useFetch.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useFetch.js","sources":["../src/useFetch.ts"],"sourcesContent":["'use client';\n\nimport { useCallback, useState, useTransition } from 'react';\n\n//TODO useFetch https://use-http.com\n//TODO SWR?\n//TODO Tanstack Query?\nexport function useFetch<R>(\n fn: (...args: any[]) => Promise<R>,\n defaultValue: R | null = null,\n): [R | null, typeof fn, boolean, Error | undefined] {\n // An async function was passed to useActionState, but it was dispatched outside of an action context.\n // This is likely not what you intended. Either pass the dispatch function to an `action` prop, or dispatch manually inside `startTransition`\n const [isPending, startTransition] = useTransition();\n const [data, setData] = useState<R | null>(defaultValue);\n const [error, setError] = useState<Error>();\n\n const actionFn = useCallback(\n (...args: Parameters<typeof fn>) => {\n const promise = fn(...args);\n // https://react.dev/reference/react/useTransition#react-doesnt-treat-my-state-update-after-await-as-a-transition\n startTransition(() => promise.then(setData).catch(setError)); //FIXME sub-chain...\n return promise;\n },\n [fn],\n );\n\n return [data, actionFn, isPending, error];\n}\n"],"names":[],"mappings":";;AAOO,SAAS,SACZ,IACA,eAAyB,MACwB;AAGjD,QAAM,CAAC,WAAW,eAAe,IAAI,cAAA;AACrC,QAAM,CAAC,MAAM,OAAO,IAAI,SAAmB,YAAY;AACvD,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAA;AAE1B,QAAM,WAAW;AAAA,IACb,IAAI,SAAgC;AAChC,YAAM,UAAU,GAAG,GAAG,IAAI;AAE1B,sBAAgB,MAAM,QAAQ,KAAK,OAAO,EAAE,MAAM,QAAQ,CAAC;AAC3D,aAAO;AAAA,IACX;AAAA,IACA,CAAC,EAAE;AAAA,EAAA;AAGP,SAAO,CAAC,MAAM,UAAU,WAAW,KAAK;AAC5C;"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare function useFetcher<T = any>(cb: any, { fetchOnMount, onError }?: {
|
|
2
|
+
fetchOnMount?: boolean;
|
|
3
|
+
onError?: (e: Error) => void;
|
|
4
|
+
}): {
|
|
5
|
+
loading: boolean;
|
|
6
|
+
error: Error | null;
|
|
7
|
+
data: T | null;
|
|
8
|
+
trigger: (...args: any[]) => Promise<any>;
|
|
9
|
+
};
|
|
10
|
+
//# sourceMappingURL=useFetcher.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useFetcher.d.ts","sourceRoot":"","sources":["../src/useFetcher.ts"],"names":[],"mappings":"AAEA,wBAAgB,UAAU,CAAC,CAAC,GAAG,GAAG,EAC9B,EAAE,EAAE,GAAG,EAEP,EAAE,YAAoB,EAAE,OAAO,EAAE,GAAE;IAAE,YAAY,CAAC,EAAE,OAAO,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC,CAAC,EAAE,KAAK,KAAK,IAAI,CAAA;CAAO,GACjG;IACC,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IACpB,IAAI,EAAE,CAAC,GAAG,IAAI,CAAC;IACf,OAAO,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,OAAO,CAAC,GAAG,CAAC,CAAC;CAC7C,CA0CA"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { useState, useRef, useCallback, useEffect } from "react";
|
|
2
|
+
function useFetcher(cb, { fetchOnMount = false, onError } = {}) {
|
|
3
|
+
const [loading, setLoading] = useState(fetchOnMount);
|
|
4
|
+
const [error, setError] = useState(null);
|
|
5
|
+
const [data, setData] = useState(null);
|
|
6
|
+
const isMounted = useRef(false);
|
|
7
|
+
const trigger = useCallback(
|
|
8
|
+
async (...args) => {
|
|
9
|
+
try {
|
|
10
|
+
setLoading(true);
|
|
11
|
+
setError(null);
|
|
12
|
+
const res = await cb(args);
|
|
13
|
+
if (!isMounted.current) return;
|
|
14
|
+
setData(res);
|
|
15
|
+
return res;
|
|
16
|
+
} catch (e) {
|
|
17
|
+
console.error("Fetch failed", e);
|
|
18
|
+
if (!isMounted.current) return;
|
|
19
|
+
setError(e);
|
|
20
|
+
onError?.(e);
|
|
21
|
+
} finally {
|
|
22
|
+
if (isMounted.current) setLoading(false);
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
[cb, onError]
|
|
26
|
+
);
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
if (fetchOnMount) {
|
|
29
|
+
trigger().catch((e) => console.error("Fetch on mount failed", e));
|
|
30
|
+
}
|
|
31
|
+
}, [fetchOnMount, cb, trigger]);
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
isMounted.current = true;
|
|
34
|
+
return () => {
|
|
35
|
+
isMounted.current = false;
|
|
36
|
+
};
|
|
37
|
+
});
|
|
38
|
+
return { loading, error, data, trigger };
|
|
39
|
+
}
|
|
40
|
+
export {
|
|
41
|
+
useFetcher
|
|
42
|
+
};
|
|
43
|
+
//# sourceMappingURL=useFetcher.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useFetcher.js","sources":["../src/useFetcher.ts"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from 'react';\n\nexport function useFetcher<T = any>(\n cb: any,\n\n { fetchOnMount = false, onError }: { fetchOnMount?: boolean; onError?: (e: Error) => void } = {},\n): {\n loading: boolean;\n error: Error | null;\n data: T | null;\n trigger: (...args: any[]) => Promise<any>;\n} {\n const [loading, setLoading] = useState(fetchOnMount);\n const [error, setError] = useState<Error | null>(null);\n const [data, setData] = useState<T | null>(null);\n\n const isMounted = useRef(false);\n\n const trigger = useCallback(\n async (...args: any[]): Promise<any> => {\n try {\n setLoading(true);\n setError(null);\n const res = await cb(args);\n if (!isMounted.current) return;\n setData(res);\n return res;\n } catch (e) {\n console.error('Fetch failed', e);\n if (!isMounted.current) return;\n setError(e);\n (onError as any)?.(e);\n } finally {\n if (isMounted.current) setLoading(false);\n }\n },\n [cb, onError],\n );\n\n useEffect(() => {\n if (fetchOnMount) {\n trigger().catch((e) => console.error('Fetch on mount failed', e)); // catch actually will never happen\n }\n }, [fetchOnMount, cb, trigger]);\n\n useEffect(() => {\n isMounted.current = true;\n return () => {\n isMounted.current = false;\n };\n });\n\n return { loading, error, data, trigger };\n}\n"],"names":[],"mappings":";AAEO,SAAS,WACZ,IAEA,EAAE,eAAe,OAAO,QAAA,IAAsE,IAMhG;AACE,QAAM,CAAC,SAAS,UAAU,IAAI,SAAS,YAAY;AACnD,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAuB,IAAI;AACrD,QAAM,CAAC,MAAM,OAAO,IAAI,SAAmB,IAAI;AAE/C,QAAM,YAAY,OAAO,KAAK;AAE9B,QAAM,UAAU;AAAA,IACZ,UAAU,SAA8B;AACpC,UAAI;AACA,mBAAW,IAAI;AACf,iBAAS,IAAI;AACb,cAAM,MAAM,MAAM,GAAG,IAAI;AACzB,YAAI,CAAC,UAAU,QAAS;AACxB,gBAAQ,GAAG;AACX,eAAO;AAAA,MACX,SAAS,GAAG;AACR,gBAAQ,MAAM,gBAAgB,CAAC;AAC/B,YAAI,CAAC,UAAU,QAAS;AACxB,iBAAS,CAAC;AACT,kBAAkB,CAAC;AAAA,MACxB,UAAA;AACI,YAAI,UAAU,QAAS,YAAW,KAAK;AAAA,MAC3C;AAAA,IACJ;AAAA,IACA,CAAC,IAAI,OAAO;AAAA,EAAA;AAGhB,YAAU,MAAM;AACZ,QAAI,cAAc;AACd,cAAA,EAAU,MAAM,CAAC,MAAM,QAAQ,MAAM,yBAAyB,CAAC,CAAC;AAAA,IACpE;AAAA,EACJ,GAAG,CAAC,cAAc,IAAI,OAAO,CAAC;AAE9B,YAAU,MAAM;AACZ,cAAU,UAAU;AACpB,WAAO,MAAM;AACT,gBAAU,UAAU;AAAA,IACxB;AAAA,EACJ,CAAC;AAED,SAAO,EAAE,SAAS,OAAO,MAAM,QAAA;AACnC;"}
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kirill.konshin/react",
|
|
3
|
+
"description": "Utilities",
|
|
4
|
+
"version": "0.0.1",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"----- BUILD -----": "",
|
|
8
|
+
"clean": "rm -rf dist .tscache tsconfig.tsbuildinfo",
|
|
9
|
+
"build": "vite build",
|
|
10
|
+
"build:index": "cti create ./src",
|
|
11
|
+
"build:check-types": "attw --pack .",
|
|
12
|
+
"start": "yarn build --watch",
|
|
13
|
+
"wait": "wait-on ./dist/index.js",
|
|
14
|
+
"----- TEST -----": "",
|
|
15
|
+
"test:disabled": "vitest run --coverage",
|
|
16
|
+
"test:watch": "vitest watch --coverage",
|
|
17
|
+
"----- STORYBOOK -----": "",
|
|
18
|
+
"storybook:start": "storybook dev -p 6006",
|
|
19
|
+
"storybook:build": "storybook build"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"clsx": "^2.1.1",
|
|
23
|
+
"zod": "^4.0.14"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@kirill.konshin/utils-private": "*",
|
|
27
|
+
"react": "^19.1.1"
|
|
28
|
+
},
|
|
29
|
+
"peerDependencies": {
|
|
30
|
+
"react": "^19"
|
|
31
|
+
},
|
|
32
|
+
"peerDependenciesMeta": {
|
|
33
|
+
"react": {
|
|
34
|
+
"optional": true
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"publishConfig": {
|
|
38
|
+
"access": "public"
|
|
39
|
+
},
|
|
40
|
+
"author": "Kirill Konshin <kirill@konshin.org> (https://konshin.org)",
|
|
41
|
+
"license": "MIT",
|
|
42
|
+
"exports": {
|
|
43
|
+
"./form": {
|
|
44
|
+
"import": {
|
|
45
|
+
"import": "./dist/form/index.js",
|
|
46
|
+
"types": "./dist/form/index.d.ts"
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
".": {
|
|
50
|
+
"import": {
|
|
51
|
+
"import": "./dist/index.js",
|
|
52
|
+
"types": "./dist/index.d.ts"
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
"main": "./dist/index.js",
|
|
57
|
+
"module": "./dist/index.js",
|
|
58
|
+
"types": "./dist/index.d.ts"
|
|
59
|
+
}
|
package/src/apiCall.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export const jsonContentType = 'application/json';
|
|
2
|
+
|
|
3
|
+
export type DataResponse<R> = Response & { data?: R };
|
|
4
|
+
|
|
5
|
+
export async function apiCall<R>(url: string, init?: RequestInit): Promise<DataResponse<R>> {
|
|
6
|
+
const useBodyAsIs = !init?.body || init?.body instanceof FormData || typeof init?.body === 'string';
|
|
7
|
+
|
|
8
|
+
const res: DataResponse<R> = await fetch(url, {
|
|
9
|
+
method: 'POST',
|
|
10
|
+
...init,
|
|
11
|
+
body: useBodyAsIs ? init?.body : JSON.stringify(init.body),
|
|
12
|
+
headers: {
|
|
13
|
+
...init?.headers,
|
|
14
|
+
...(useBodyAsIs ? {} : { 'Content-Type': jsonContentType }),
|
|
15
|
+
},
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
if (res.headers.get('Content-Type')?.includes(jsonContentType)) {
|
|
19
|
+
res.data = await res.json();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (!res.ok) throw new Error(res.statusText, { cause: res });
|
|
23
|
+
|
|
24
|
+
return res;
|
|
25
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useActionState, useCallback, useState, useTransition } from 'react';
|
|
4
|
+
import { create, MaybeTypeOf, Validation } from './form';
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
|
|
7
|
+
const FORM_DEBUG = process.env.NEXT_PUBLIC_FORM_DEBUG === 'true';
|
|
8
|
+
|
|
9
|
+
export function createClient<S extends z.ZodObject<any>>(
|
|
10
|
+
schema: S,
|
|
11
|
+
): {
|
|
12
|
+
useValidation: (
|
|
13
|
+
actionFn: (data: FormData) => Promise<Validation<S>>,
|
|
14
|
+
initialData?: MaybeTypeOf<S>,
|
|
15
|
+
) => [state: Validation<S>, dispatch: (payload: FormData) => void, isPending: boolean];
|
|
16
|
+
useValidationTransition: (
|
|
17
|
+
actionFn: (data: FormData) => Promise<Validation<S>>,
|
|
18
|
+
initialData?: MaybeTypeOf<S>,
|
|
19
|
+
) => [Validation<S>, (formData: FormData) => Promise<Validation<S>>, boolean];
|
|
20
|
+
} {
|
|
21
|
+
const { validate } = create(schema);
|
|
22
|
+
|
|
23
|
+
function useValidationCallback(
|
|
24
|
+
actionFn: (data: FormData) => Promise<Validation<S>>,
|
|
25
|
+
): (formData: FormData) => Promise<Validation<S>> {
|
|
26
|
+
return useCallback(
|
|
27
|
+
async (formData: FormData) => {
|
|
28
|
+
const clientRes = validate(formData);
|
|
29
|
+
console.log('Client validation', FORM_DEBUG ? 'ignored' : 'active', clientRes);
|
|
30
|
+
if (!clientRes.success && !FORM_DEBUG) return clientRes;
|
|
31
|
+
|
|
32
|
+
const serverRes = await actionFn(formData);
|
|
33
|
+
console.log('Server validation', serverRes);
|
|
34
|
+
return serverRes;
|
|
35
|
+
},
|
|
36
|
+
[actionFn],
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function useValidation(
|
|
41
|
+
actionFn: (data: FormData) => Promise<Validation<S>>,
|
|
42
|
+
initialData: MaybeTypeOf<S> = {} as MaybeTypeOf<S>,
|
|
43
|
+
): [state: Validation<S>, dispatch: (payload: FormData) => void, isPending: boolean] {
|
|
44
|
+
const cb = useValidationCallback(actionFn);
|
|
45
|
+
|
|
46
|
+
return useActionState<Validation<S>, FormData>(async (_, data) => cb(data), {
|
|
47
|
+
success: false,
|
|
48
|
+
data: initialData,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function useValidationTransition(
|
|
53
|
+
actionFn: (data: FormData) => Promise<Validation<S>>,
|
|
54
|
+
initialData: MaybeTypeOf<S> = {} as MaybeTypeOf<S>,
|
|
55
|
+
): [Validation<S>, (formData: FormData) => Promise<Validation<S>>, boolean] {
|
|
56
|
+
const [isPending, startTransition] = useTransition();
|
|
57
|
+
const [state, setState] = useState<Validation<S>>({ success: false, data: initialData });
|
|
58
|
+
const cb = useValidationCallback(actionFn);
|
|
59
|
+
|
|
60
|
+
const wrappedCb = useCallback(
|
|
61
|
+
(formData: FormData) => {
|
|
62
|
+
const promise = cb(formData);
|
|
63
|
+
startTransition(() => promise.then(setState));
|
|
64
|
+
return promise;
|
|
65
|
+
},
|
|
66
|
+
[cb, startTransition],
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
return [state, wrappedCb, isPending];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return { useValidation, useValidationTransition };
|
|
73
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { typeToFlattenedError, z, SafeParseReturnType, TypeOf } from 'zod';
|
|
2
|
+
import { Context, createContext, useContext, useMemo, JSX, FC, memo } from 'react';
|
|
3
|
+
import clsx from 'clsx';
|
|
4
|
+
|
|
5
|
+
const nonEmpty = 'This field cannot be empty';
|
|
6
|
+
|
|
7
|
+
export const stringRequired = (): z.ZodString => z.string({ required_error: nonEmpty }).min(1, nonEmpty);
|
|
8
|
+
export const maxLength = (schema: z.ZodString): number => schema.maxLength || 0;
|
|
9
|
+
export const minLength = (schema: z.ZodString): number => schema.minLength || 0;
|
|
10
|
+
export const isRequired = (schema: z.ZodString): boolean => minLength(schema) > 0;
|
|
11
|
+
|
|
12
|
+
export type ZodObject = z.ZodObject<any> | z.ZodEffects<z.ZodObject<any>>; // z.ZodType<any, any, any>
|
|
13
|
+
export type MaybeTypeOf<S extends ZodObject> = Partial<TypeOf<S>>;
|
|
14
|
+
export type SafeTypeOf<S extends ZodObject> = SafeParseReturnType<TypeOf<S>, TypeOf<S>>['data'];
|
|
15
|
+
export type Errors<S extends ZodObject> = typeToFlattenedError<TypeOf<S>>['fieldErrors'];
|
|
16
|
+
export type Validation<S extends ZodObject> =
|
|
17
|
+
| {
|
|
18
|
+
success: true; // this is true only if form was validated successfully
|
|
19
|
+
data: SafeTypeOf<S>;
|
|
20
|
+
errors?: never;
|
|
21
|
+
}
|
|
22
|
+
| {
|
|
23
|
+
success: false;
|
|
24
|
+
data?: MaybeTypeOf<S>;
|
|
25
|
+
errors?: Errors<S>;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export const FormContext: Context<{
|
|
29
|
+
schema: ZodObject;
|
|
30
|
+
}> = createContext(null as never);
|
|
31
|
+
|
|
32
|
+
export interface FormProps<S extends ZodObject> {
|
|
33
|
+
schema: S;
|
|
34
|
+
children: any;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const Form: FC<FormProps<any>> = memo(function Form({ schema, children }) {
|
|
38
|
+
const value = useMemo(() => ({ schema }), [schema]);
|
|
39
|
+
return <FormContext.Provider value={value}>{children}</FormContext.Provider>;
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const getShape = <S extends ZodObject>(schema: S) =>
|
|
43
|
+
(schema as z.ZodObject<any>).shape || (schema as z.ZodEffects<z.ZodObject<any>>).sourceType().shape;
|
|
44
|
+
|
|
45
|
+
export function create<S extends ZodObject>(
|
|
46
|
+
schema: S,
|
|
47
|
+
): {
|
|
48
|
+
register: (
|
|
49
|
+
name: keyof TypeOf<S>,
|
|
50
|
+
data?: MaybeTypeOf<S>,
|
|
51
|
+
errors?: Errors<S>,
|
|
52
|
+
mui?: boolean,
|
|
53
|
+
) => {
|
|
54
|
+
label?: any;
|
|
55
|
+
helperText?: string;
|
|
56
|
+
error?: boolean;
|
|
57
|
+
name: keyof z.TypeOf<S>;
|
|
58
|
+
id: keyof z.TypeOf<S>;
|
|
59
|
+
required: boolean;
|
|
60
|
+
maxLength: number;
|
|
61
|
+
type: string;
|
|
62
|
+
defaultValue?: Partial<z.TypeOf<S>>[keyof z.TypeOf<S>];
|
|
63
|
+
};
|
|
64
|
+
validate: (formData: FormData) => Validation<S>;
|
|
65
|
+
validationError: (data: MaybeTypeOf<S>, errors: Errors<S>) => Validation<S>;
|
|
66
|
+
} {
|
|
67
|
+
if (!getShape(schema)) {
|
|
68
|
+
throw new Error('Invalid schema: only z.object() or z.object().refine() are supported');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function register(
|
|
72
|
+
name: keyof TypeOf<S>,
|
|
73
|
+
data?: MaybeTypeOf<S>,
|
|
74
|
+
errors?: Errors<S>,
|
|
75
|
+
mui: boolean = false,
|
|
76
|
+
): {
|
|
77
|
+
label?: any;
|
|
78
|
+
helperText?: string;
|
|
79
|
+
error?: boolean;
|
|
80
|
+
name: keyof z.TypeOf<S>;
|
|
81
|
+
id: keyof z.TypeOf<S>;
|
|
82
|
+
required: boolean;
|
|
83
|
+
maxLength: number;
|
|
84
|
+
type: string;
|
|
85
|
+
defaultValue?: Partial<z.TypeOf<S>>[keyof z.TypeOf<S>];
|
|
86
|
+
} {
|
|
87
|
+
const field = getShape(schema)[name];
|
|
88
|
+
return {
|
|
89
|
+
name,
|
|
90
|
+
id: name,
|
|
91
|
+
required: isRequired(field),
|
|
92
|
+
maxLength: maxLength(field),
|
|
93
|
+
type: field.isEmail ? 'email' : (name as string).includes('password') ? 'password' : 'text',
|
|
94
|
+
defaultValue: data?.[name],
|
|
95
|
+
...(mui
|
|
96
|
+
? {
|
|
97
|
+
label: field.description,
|
|
98
|
+
helperText: errors?.[name]?.join(', '),
|
|
99
|
+
error: !!errors?.[name]?.length,
|
|
100
|
+
}
|
|
101
|
+
: {}),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function validationError(data: MaybeTypeOf<S>, errors: Errors<S>): Validation<S> {
|
|
106
|
+
return {
|
|
107
|
+
success: false,
|
|
108
|
+
data, // data is undefined if there are errors
|
|
109
|
+
errors, // Next.js will butcher error object, so we provide something more primitive
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function validate(formData: FormData): Validation<S> {
|
|
114
|
+
const rawData = Object.fromEntries(formData) as TypeOf<S>;
|
|
115
|
+
const { error, data } = schema.safeParse(rawData);
|
|
116
|
+
|
|
117
|
+
// console.log('Validate result', { error, data, rawData });
|
|
118
|
+
|
|
119
|
+
if (error) {
|
|
120
|
+
// data is undefined if there are errors
|
|
121
|
+
// Next.js will butcher error object, so we provide something more primitive
|
|
122
|
+
return validationError(rawData, error.flatten().fieldErrors as any);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return { success: true, data };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return { register, validate, validationError };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
interface FieldProps<S extends ZodObject> {
|
|
132
|
+
children?: any;
|
|
133
|
+
name: keyof TypeOf<S>;
|
|
134
|
+
errors?: Validation<S>['errors'];
|
|
135
|
+
hint?: string;
|
|
136
|
+
className?: string;
|
|
137
|
+
labelProps?: any;
|
|
138
|
+
[key: string]: any;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export const Field: FC<FieldProps<any>> = memo(function Field({
|
|
142
|
+
children,
|
|
143
|
+
name,
|
|
144
|
+
errors,
|
|
145
|
+
hint,
|
|
146
|
+
className,
|
|
147
|
+
labelProps,
|
|
148
|
+
...props
|
|
149
|
+
}) {
|
|
150
|
+
const { schema } = useContext(FormContext);
|
|
151
|
+
const { description } = getShape(schema)[name];
|
|
152
|
+
|
|
153
|
+
return (
|
|
154
|
+
<div {...props} className={clsx('form-row', className)}>
|
|
155
|
+
{description && (
|
|
156
|
+
<label {...labelProps} htmlFor={name}>
|
|
157
|
+
{description}
|
|
158
|
+
</label>
|
|
159
|
+
)}
|
|
160
|
+
{children}
|
|
161
|
+
{hint && <Hint>{hint}</Hint>}
|
|
162
|
+
{errors?.[name]?.map((e: string) => (
|
|
163
|
+
<Hint error key={e}>
|
|
164
|
+
{e}
|
|
165
|
+
</Hint>
|
|
166
|
+
))}
|
|
167
|
+
</div>
|
|
168
|
+
);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
export interface HintProps {
|
|
172
|
+
children: any;
|
|
173
|
+
error?: boolean;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export const Hint: FC<HintProps> = memo(function Hint({ children, error }) {
|
|
177
|
+
return <div className={`hint ${error ? 'hint-error' : ''}`}>{children}</div>;
|
|
178
|
+
});
|
package/src/index.ts
ADDED
package/src/keyboard.tsx
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect, createContext, useMemo, useState, Dispatch, SetStateAction, useContext, FC, Context } from 'react';
|
|
4
|
+
|
|
5
|
+
const isCtrlOrMeta = (e: KeyboardEvent) => e.metaKey || e.ctrlKey;
|
|
6
|
+
|
|
7
|
+
const EVENT = 'keydown';
|
|
8
|
+
|
|
9
|
+
export type Hotkeys = Record<KeyboardEvent['code'], (e: KeyboardEvent) => void>;
|
|
10
|
+
|
|
11
|
+
export type HotkeyContextType = {
|
|
12
|
+
enabled: boolean;
|
|
13
|
+
setEnabled: Dispatch<SetStateAction<boolean>>;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const HotkeysContext: Context<HotkeyContextType> = createContext<{
|
|
17
|
+
enabled: boolean;
|
|
18
|
+
setEnabled: Dispatch<SetStateAction<boolean>>;
|
|
19
|
+
}>(null as never);
|
|
20
|
+
|
|
21
|
+
export const HotkeysProvider: FC<any> = ({ children }) => {
|
|
22
|
+
const [enabled, setEnabled] = useState(true);
|
|
23
|
+
|
|
24
|
+
const control = useMemo(() => ({ enabled, setEnabled }), [enabled, setEnabled]);
|
|
25
|
+
|
|
26
|
+
return <HotkeysContext.Provider value={control}>{children}</HotkeysContext.Provider>;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export const useHotkeys = (hotkeys: Hotkeys): void => {
|
|
30
|
+
const { enabled } = useContext(HotkeysContext);
|
|
31
|
+
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
if (typeof document === 'undefined' || !enabled) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const ctrl = new AbortController();
|
|
38
|
+
|
|
39
|
+
window.addEventListener(
|
|
40
|
+
EVENT,
|
|
41
|
+
(e: KeyboardEvent) => {
|
|
42
|
+
if (!isCtrlOrMeta(e)) return;
|
|
43
|
+
|
|
44
|
+
for (const [code, callback] of Object.entries(hotkeys)) {
|
|
45
|
+
if (e.code === code) {
|
|
46
|
+
callback(e);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
{ signal: ctrl.signal, capture: true },
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
return () => {
|
|
55
|
+
ctrl.abort();
|
|
56
|
+
};
|
|
57
|
+
}, [hotkeys, enabled]);
|
|
58
|
+
};
|
package/src/useFetch.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useCallback, useState, useTransition } from 'react';
|
|
4
|
+
|
|
5
|
+
//TODO useFetch https://use-http.com
|
|
6
|
+
//TODO SWR?
|
|
7
|
+
//TODO Tanstack Query?
|
|
8
|
+
export function useFetch<R>(
|
|
9
|
+
fn: (...args: any[]) => Promise<R>,
|
|
10
|
+
defaultValue: R | null = null,
|
|
11
|
+
): [R | null, typeof fn, boolean, Error | undefined] {
|
|
12
|
+
// An async function was passed to useActionState, but it was dispatched outside of an action context.
|
|
13
|
+
// This is likely not what you intended. Either pass the dispatch function to an `action` prop, or dispatch manually inside `startTransition`
|
|
14
|
+
const [isPending, startTransition] = useTransition();
|
|
15
|
+
const [data, setData] = useState<R | null>(defaultValue);
|
|
16
|
+
const [error, setError] = useState<Error>();
|
|
17
|
+
|
|
18
|
+
const actionFn = useCallback(
|
|
19
|
+
(...args: Parameters<typeof fn>) => {
|
|
20
|
+
const promise = fn(...args);
|
|
21
|
+
// https://react.dev/reference/react/useTransition#react-doesnt-treat-my-state-update-after-await-as-a-transition
|
|
22
|
+
startTransition(() => promise.then(setData).catch(setError)); //FIXME sub-chain...
|
|
23
|
+
return promise;
|
|
24
|
+
},
|
|
25
|
+
[fn],
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
return [data, actionFn, isPending, error];
|
|
29
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
export function useFetcher<T = any>(
|
|
4
|
+
cb: any,
|
|
5
|
+
|
|
6
|
+
{ fetchOnMount = false, onError }: { fetchOnMount?: boolean; onError?: (e: Error) => void } = {},
|
|
7
|
+
): {
|
|
8
|
+
loading: boolean;
|
|
9
|
+
error: Error | null;
|
|
10
|
+
data: T | null;
|
|
11
|
+
trigger: (...args: any[]) => Promise<any>;
|
|
12
|
+
} {
|
|
13
|
+
const [loading, setLoading] = useState(fetchOnMount);
|
|
14
|
+
const [error, setError] = useState<Error | null>(null);
|
|
15
|
+
const [data, setData] = useState<T | null>(null);
|
|
16
|
+
|
|
17
|
+
const isMounted = useRef(false);
|
|
18
|
+
|
|
19
|
+
const trigger = useCallback(
|
|
20
|
+
async (...args: any[]): Promise<any> => {
|
|
21
|
+
try {
|
|
22
|
+
setLoading(true);
|
|
23
|
+
setError(null);
|
|
24
|
+
const res = await cb(args);
|
|
25
|
+
if (!isMounted.current) return;
|
|
26
|
+
setData(res);
|
|
27
|
+
return res;
|
|
28
|
+
} catch (e) {
|
|
29
|
+
console.error('Fetch failed', e);
|
|
30
|
+
if (!isMounted.current) return;
|
|
31
|
+
setError(e);
|
|
32
|
+
(onError as any)?.(e);
|
|
33
|
+
} finally {
|
|
34
|
+
if (isMounted.current) setLoading(false);
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
[cb, onError],
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
if (fetchOnMount) {
|
|
42
|
+
trigger().catch((e) => console.error('Fetch on mount failed', e)); // catch actually will never happen
|
|
43
|
+
}
|
|
44
|
+
}, [fetchOnMount, cb, trigger]);
|
|
45
|
+
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
isMounted.current = true;
|
|
48
|
+
return () => {
|
|
49
|
+
isMounted.current = false;
|
|
50
|
+
};
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
return { loading, error, data, trigger };
|
|
54
|
+
}
|
package/tsconfig.json
ADDED
package/turbo.json
ADDED
package/vite.config.ts
ADDED