@opexa/portal-components 0.0.236 → 0.0.238
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/dist/client/hooks/useCamera.d.ts +8 -7
- package/dist/client/hooks/useCamera.js +6 -6
- package/dist/components/Bonuses/ClaimSpotBonus.lazy.js +1 -1
- package/dist/components/DepositWithdrawal/Deposit/OnlineBankDeposit/OnlineBankDepositContext.d.ts +2 -2
- package/dist/components/DepositWithdrawal/Deposit/OnlineBankDeposit/OnlineBankDepositDetails.js +1 -1
- package/dist/components/DepositWithdrawal/Deposit/OnlineBankDeposit/useOnlineBankDeposit.d.ts +1 -1
- package/dist/components/DepositWithdrawal/Deposit/QRPHDeposit/QRPHDepositContext.d.ts +2 -2
- package/dist/components/DepositWithdrawal/Deposit/QRPHDeposit/QRPHDepositDetails.js +1 -1
- package/dist/components/DepositWithdrawal/Deposit/QRPHDeposit/useQRPHDeposit.d.ts +1 -1
- package/dist/components/DepositWithdrawal/DepositWithdrawal.lazy.js +1 -1
- package/dist/components/Disclaimer/DisclaimerV1.js +1 -1
- package/dist/components/Disclaimer/DisclaimerV2.js +1 -1
- package/dist/components/Disclaimer/ResponsibleGaming.d.ts +2 -1
- package/dist/components/Disclaimer/ResponsibleGaming.js +4 -4
- package/dist/components/Disclaimer/TermsOfUse.d.ts +2 -1
- package/dist/components/Disclaimer/TermsOfUse.js +5 -5
- package/dist/components/GameLaunch/GameLaunch.lazy.js +1 -1
- package/dist/components/Jackpots/JackpotsList/JackpotsListItemRules.js +1 -1
- package/dist/components/KYC/KYC.lazy.js +1 -1
- package/dist/components/KYC/KYCReminder.lazy.js +1 -1
- package/dist/components/Messages/Messages.lazy.js +1 -1
- package/dist/components/Messages/MessagesPopup.js +1 -1
- package/dist/components/Quests/DailyCheckInQuest.js +1 -1
- package/dist/components/Quests/OnboardingQuest.js +1 -1
- package/dist/components/Quests/WageringQuest.js +1 -1
- package/dist/components/Search/Search.lazy.js +1 -1
- package/dist/components/SignIn/SignIn.lazy.js +1 -1
- package/dist/components/SignUp/SignUpDefault/SignUp.lazy.js +1 -1
- package/dist/components/SignUp/SignUpKYC/CaptureIdDocument.js +1 -1
- package/dist/components/SignUp/SignUpKYC/CaptureSelfie.js +1 -1
- package/dist/components/SignUp/SignUpKYC/SignUpKYC.lazy.js +1 -1
- package/dist/components/Tournaments/TournamentsList/TournamentsListItemRules.js +1 -1
- package/dist/components/shared/IdDocumentField.d.ts +14 -6
- package/dist/components/shared/IdDocumentField.js +69 -58
- package/dist/components/shared/SelfieField.d.ts +12 -6
- package/dist/components/shared/SelfieField.js +188 -72
- package/dist/next.config.js +8 -0
- package/dist/ui/AlertDialog/AlertDialog.d.ts +88 -88
- package/dist/ui/AlertDialog/alertDialog.recipe.d.ts +11 -11
- package/dist/ui/Avatar/avatar.recipe.d.ts +3 -3
- package/dist/ui/Badge/Badge.d.ts +12 -12
- package/dist/ui/Carousel/Carousel.d.ts +45 -45
- package/dist/ui/Carousel/carousel.recipe.d.ts +5 -5
- package/dist/ui/Checkbox/Checkbox.d.ts +23 -23
- package/dist/ui/Checkbox/checkbox.recipe.d.ts +3 -3
- package/dist/ui/Clipboard/Clipboard.d.ts +18 -18
- package/dist/ui/Collapsible/Collapsible.d.ts +32 -32
- package/dist/ui/Collapsible/collapsible.recipe.d.ts +5 -5
- package/dist/ui/Combobox/Combobox.d.ts +42 -42
- package/dist/ui/Combobox/combobox.recipe.d.ts +3 -3
- package/dist/ui/DatePicker/DatePicker.d.ts +72 -72
- package/dist/ui/DatePicker/datePicker.recipe.d.ts +3 -3
- package/dist/ui/Dialog/Dialog.d.ts +88 -88
- package/dist/ui/Dialog/dialog.recipe.d.ts +11 -11
- package/dist/ui/Drawer/Drawer.d.ts +33 -33
- package/dist/ui/Drawer/drawer.recipe.d.ts +3 -3
- package/dist/ui/Menu/Menu.d.ts +144 -144
- package/dist/ui/Menu/menu.recipe.d.ts +20 -20
- package/dist/ui/PasswordInput/PasswordInput.d.ts +18 -18
- package/dist/ui/Popover/Popover.d.ts +55 -55
- package/dist/ui/Popover/popover.recipe.d.ts +8 -8
- package/dist/ui/Progress/Progress.d.ts +27 -27
- package/dist/ui/Progress/progress.recipe.d.ts +3 -3
- package/dist/ui/QrCode/qrCode.recipe.d.ts +5 -5
- package/dist/ui/SegmentGroup/SegmentGroup.d.ts +18 -18
- package/dist/ui/SegmentGroup/segmentGroup.recipe.d.ts +3 -3
- package/dist/ui/Select/Select.d.ts +45 -45
- package/dist/ui/Select/select.recipe.d.ts +3 -3
- package/dist/ui/Table/Table.d.ts +21 -21
- package/dist/ui/Table/table.anatomy.d.ts +1 -1
- package/dist/ui/Table/table.recipe.d.ts +3 -3
- package/dist/ui/Tabs/Tabs.d.ts +15 -15
- package/dist/ui/Tabs/tabs.recipe.d.ts +3 -3
- package/package.json +5 -3
|
@@ -22,11 +22,9 @@ import { XIcon } from '../../icons/XIcon.js';
|
|
|
22
22
|
import { Button } from '../../ui/Button/index.js';
|
|
23
23
|
import { Dialog } from '../../ui/Dialog/index.js';
|
|
24
24
|
import { Portal } from '../../ui/Portal/index.js';
|
|
25
|
-
import { dataAttr } from '../../utils/dataAttr.js';
|
|
26
25
|
import { validateFile } from '../../utils/validateFile.js';
|
|
27
26
|
export function IdDocumentField(props) {
|
|
28
27
|
const context = useIdDocumentField(props);
|
|
29
|
-
const { field, setValue, query, mutation, inputRef, guideRef, camera, disclosure, } = context;
|
|
30
28
|
return (_jsxs(IdDocumentFieldProvider, { value: context, children: [_jsxs("div", { ref: props.ref, id: props.id, style: props.style, className: twMerge('border-border-primary bg-bg-primary group relative aspect-[352/180] w-full shrink-0 rounded-xl border p-xl', props.className), onDragOver: (e) => e.preventDefault(), onDragEnter: (e) => e.preventDefault(), onDragLeave: (e) => e.preventDefault(), onDrop: (e) => {
|
|
31
29
|
e.preventDefault();
|
|
32
30
|
const file = e.dataTransfer.files?.[0];
|
|
@@ -35,60 +33,73 @@ export function IdDocumentField(props) {
|
|
|
35
33
|
const error = validateFile(file);
|
|
36
34
|
if (error)
|
|
37
35
|
return props.onError?.(error);
|
|
38
|
-
mutation.mutate({ file });
|
|
39
|
-
}, "
|
|
36
|
+
context.mutation.mutate({ file });
|
|
37
|
+
}, "aria-describedby": context.field?.ariaDescribedby, children: [_jsx("input", { id: context.field?.ids?.control, ref: context.inputRef, type: "file", accept: "image/png, image/jpeg", onChange: (e) => {
|
|
40
38
|
const file = e.target.files?.[0];
|
|
41
39
|
if (!file)
|
|
42
40
|
return;
|
|
43
41
|
const error = validateFile(file);
|
|
44
42
|
if (error)
|
|
45
43
|
return props.onError?.(error);
|
|
46
|
-
mutation.mutate({ file });
|
|
47
|
-
}, disabled: field?.disabled ||
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
inputRef.current?.click();
|
|
52
|
-
}, disabled: field?.disabled ||
|
|
53
|
-
field?.readOnly ||
|
|
44
|
+
context.mutation.mutate({ file });
|
|
45
|
+
}, disabled: context.field?.disabled ||
|
|
46
|
+
context.query.isLoading ||
|
|
47
|
+
context.mutation.isPending ||
|
|
48
|
+
props.disabled, readOnly: context.field?.readOnly || props.readOnly, required: context.field?.required || props.required, className: "sr-only" }), !context.query.data?.url && (_jsxs("div", { className: "flex size-full flex-col justify-center", children: [_jsx("div", { className: "border-border-primary shadow-xs mx-auto flex size-8 items-center justify-center rounded-sm border", children: _jsx(UploadCloud02Icon, { className: "text-text-placeholder-subtle size-4" }) }), _jsxs("h2", { className: "text-text-tertiary-600 mt-lg", children: [_jsxs("span", { className: "block text-center text-sm", children: [_jsx("button", { type: "button", onClick: () => {
|
|
49
|
+
context.inputRef.current?.click();
|
|
50
|
+
}, disabled: context.field?.disabled ||
|
|
51
|
+
context.field?.readOnly ||
|
|
52
|
+
context.query.isLoading ||
|
|
53
|
+
context.mutation.isPending ||
|
|
54
54
|
props.disabled ||
|
|
55
|
-
props.readOnly
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
55
|
+
props.readOnly, className: "text-button-secondary-fg font-semibold disabled:opacity-60", children: "Click to upload" }), ' ', "or drag and drop"] }), _jsx("span", { className: "mt-xs block text-center text-xs", children: "PNG, JPG or JPEG (max. 10mb)" }), _jsx("span", { className: "m-txs block text-center text-xs", children: "or" })] }), _jsx(Button, { size: "sm", variant: "outline", className: "mx-auto mt-md w-auto", onClick: () => {
|
|
56
|
+
context.disclosure.setOpen(true);
|
|
57
|
+
}, disabled: context.field?.disabled ||
|
|
58
|
+
context.field?.readOnly ||
|
|
59
|
+
context.query.isLoading ||
|
|
60
|
+
context.mutation.isPending ||
|
|
61
61
|
props.disabled ||
|
|
62
|
-
props.readOnly
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
62
|
+
props.readOnly, children: "Take a photo" })] })), context.query.data?.url && (_jsxs("div", { className: "relative", children: [_jsx(Image, { src: context.query.data.url, alt: "", width: 600, height: 378.08, unoptimized: true, className: "h-auto w-full rounded-md" }), _jsx("button", { type: "button", className: "backdrop-blur-xs absolute right-2 top-2 rounded-full bg-white/10 p-1", onClick: () => {
|
|
63
|
+
context.setValue('');
|
|
64
|
+
}, children: _jsx(XIcon, { className: "size-5" }) })] }))] }), _jsx(Camera, {})] }));
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* -------------------------------------
|
|
68
|
+
* Camera
|
|
69
|
+
* -------------------------------------
|
|
70
|
+
*/
|
|
71
|
+
function Camera() {
|
|
72
|
+
const context = useIdDocumentFieldContext();
|
|
73
|
+
return (_jsx(Dialog.Root, { open: context.disclosure.open, onOpenChange: (details) => {
|
|
74
|
+
context.disclosure.setOpen(details.open);
|
|
75
|
+
}, onExitComplete: context.camera.close, children: _jsxs(Portal, { children: [_jsx(Dialog.Backdrop, { className: "!z-[calc(var(--z-dialog)+1)]" }), _jsx(Dialog.Positioner, { className: "!z-[calc(var(--z-dialog)+2)] flex items-center justify-center overflow-y-auto py-4", children: _jsxs(Dialog.Content, { className: "bg-bg-primary-alt mx-auto w-[calc(100dvw-1rem)] max-w-[calc(100dvw-1rem)] overflow-y-auto rounded-lg px-4 py-5 lg:w-[747px] lg:max-w-[747px] lg:px-3xl lg:py-4xl", children: [_jsx(Dialog.CloseTrigger, { children: _jsx(XIcon, {}) }), _jsx(Dialog.Title, { className: "text-center font-semibold lg:text-xl", children: "Take a Picture of Your Front ID" }), _jsxs(Dialog.Description, { className: "text-text-tertiary-600 mt-md text-center text-xs lg:text-base", children: ["Make sure your ID is clearly visible, well-lit, and not blurry.", ' ', _jsx("br", { className: "hidden lg:block" }), "Avoid glare or reflections, and ensure all corners are within\u00A0the\u00A0frame."] }), _jsxs("div", { className: "relative mt-5 lg:mt-10 lg:px-3xl", children: [_jsxs("div", { className: twMerge('border-border-disabled relative w-full overflow-hidden rounded-md border bg-black lg:rounded-lg', !context.camera.data &&
|
|
76
|
+
!context.camera.error &&
|
|
77
|
+
!context.camera.loading
|
|
78
|
+
? 'block'
|
|
79
|
+
: 'hidden'), children: [_jsx("video", { ...context.camera.videoProps }), _jsx(Mask, {})] }), context.camera.error && (_jsxs("div", { className: "border-border-disabled flex aspect-[85.60/53.98] flex-col items-center justify-center rounded-md border bg-black px-4 lg:rounded-lg", children: [_jsx(CameraOffIcon, { className: "text-text-placeholder-subtle size-10 text-center lg:size-12" }), _jsx("h2", { className: "mt-3 text-sm font-semibold lg:mt-4 lg:text-base", children: context.camera.error.name }), _jsx("p", { className: "text-text-tertiary-600 mt-0.5 text-center text-xs lg:mt-1 lg:text-sm", children: context.camera.error.message }), _jsx(Button, { size: "xs", variant: "outline", colorScheme: "gray", fullWidth: false, className: "mt-4 lg:mt-5", onClick: () => {
|
|
80
|
+
context.camera.reopen();
|
|
81
|
+
}, children: "Try again" })] })), context.camera.loading && (_jsx("div", { className: "border-border-disabled flex aspect-[85.60/53.98] flex-col items-center justify-center rounded-md border bg-black px-4 lg:rounded-lg", children: _jsx(Spinner02Icon, { className: "text-text-placeholder size-8" }) })), context.camera.data && (_jsx("div", { className: "border-border-disabled h-auto w-full overflow-hidden rounded-md border bg-black lg:rounded-lg", children: _jsx(Image, { src: context.camera.data.url, alt: "", width: 600, height: 378.08, unoptimized: true, className: "block h-auto w-full" }) }))] }), !context.camera.data &&
|
|
82
|
+
!context.camera.error &&
|
|
83
|
+
!context.camera.loading && (_jsx("div", { className: "mt-5 lg:mt-10", children: _jsxs(Button, { className: "disabled:bg-bg-disabled disabled:text-text-disabled mx-auto w-full px-xl lg:w-auto", onClick: () => {
|
|
84
|
+
const rect = context.guideRef.current?.getBoundingClientRect();
|
|
85
|
+
const crop = !rect
|
|
86
|
+
? undefined
|
|
87
|
+
: {
|
|
88
|
+
y: rect.y,
|
|
89
|
+
x: rect.x,
|
|
90
|
+
width: rect.width,
|
|
91
|
+
height: rect.height,
|
|
92
|
+
};
|
|
93
|
+
context.camera.snap({ crop });
|
|
94
|
+
}, children: [_jsx(Camera01Icon, { className: "size-5" }), "Capture"] }) })), context.camera.data && (_jsxs("div", { className: "mt-5 flex gap-3 lg:mt-10", children: [_jsxs(Button, { onClick: () => {
|
|
95
|
+
const file = context.camera.data?.file;
|
|
96
|
+
if (!file)
|
|
97
|
+
return;
|
|
98
|
+
context.mutation.mutate({ file });
|
|
99
|
+
context.disclosure.setOpen(false);
|
|
100
|
+
}, children: [_jsx(CheckCircleIcon, { className: "size-5" }), "Use Photo"] }), _jsxs(Button, { variant: "outline", colorScheme: "gray", onClick: () => {
|
|
101
|
+
context.camera.reset();
|
|
102
|
+
}, children: [_jsx(RefreshCcw01Icon, { className: "size-5" }), " Retake"] })] }))] }) })] }) }));
|
|
92
103
|
}
|
|
93
104
|
/**
|
|
94
105
|
* -------------------------------------
|
|
@@ -97,7 +108,7 @@ export function IdDocumentField(props) {
|
|
|
97
108
|
*/
|
|
98
109
|
function Mask() {
|
|
99
110
|
const id = useId();
|
|
100
|
-
const
|
|
111
|
+
const context = useIdDocumentFieldContext();
|
|
101
112
|
const [maskBox, setMaskBox] = useState({
|
|
102
113
|
x: 0,
|
|
103
114
|
y: 0,
|
|
@@ -106,8 +117,8 @@ function Mask() {
|
|
|
106
117
|
});
|
|
107
118
|
useLayoutEffect(() => {
|
|
108
119
|
const update = () => {
|
|
109
|
-
const guide = guideRef.current;
|
|
110
|
-
const mask = maskRef.current;
|
|
120
|
+
const guide = context.guideRef.current;
|
|
121
|
+
const mask = context.maskRef.current;
|
|
111
122
|
if (!guide || !mask)
|
|
112
123
|
return;
|
|
113
124
|
const guideRect = guide.getBoundingClientRect();
|
|
@@ -126,19 +137,19 @@ function Mask() {
|
|
|
126
137
|
}
|
|
127
138
|
};
|
|
128
139
|
const observer = new ResizeObserver(update);
|
|
129
|
-
if (guideRef.current)
|
|
130
|
-
observer.observe(guideRef.current);
|
|
131
|
-
if (maskRef.current)
|
|
132
|
-
observer.observe(maskRef.current);
|
|
140
|
+
if (context.guideRef.current)
|
|
141
|
+
observer.observe(context.guideRef.current);
|
|
142
|
+
if (context.maskRef.current)
|
|
143
|
+
observer.observe(context.maskRef.current);
|
|
133
144
|
requestAnimationFrame(update);
|
|
134
145
|
return () => observer.disconnect();
|
|
135
|
-
}, [guideRef, maskRef]);
|
|
136
|
-
return (_jsxs(_Fragment, { children: [_jsx("svg", { className: "pointer-events-none absolute h-full w-full", children: _jsx("defs", { children: _jsxs("mask", { id: id, maskUnits: "userSpaceOnUse", children: [_jsx("rect", { x: "0", y: "0", width: "100%", height: "100%", fill: "white" }), _jsx("rect", { x: maskBox.x, y: maskBox.y, width: maskBox.width, height: maskBox.height, fill: "black", rx: "8", ry: "8" })] }) }) }), _jsx("div", { ref: maskRef, className: "absolute inset-0 bg-black/60 backdrop-blur-sm", style: {
|
|
146
|
+
}, [context.guideRef, context.maskRef]);
|
|
147
|
+
return (_jsxs(_Fragment, { children: [_jsx("svg", { className: "pointer-events-none absolute h-full w-full", children: _jsx("defs", { children: _jsxs("mask", { id: id, maskUnits: "userSpaceOnUse", children: [_jsx("rect", { x: "0", y: "0", width: "100%", height: "100%", fill: "white" }), _jsx("rect", { x: maskBox.x, y: maskBox.y, width: maskBox.width, height: maskBox.height, fill: "black", rx: "8", ry: "8" })] }) }) }), _jsx("div", { ref: context.maskRef, className: "absolute inset-0 bg-black/60 backdrop-blur-sm", style: {
|
|
137
148
|
mask: `url(#${id})`,
|
|
138
149
|
WebkitMask: `url(#${id})`,
|
|
139
150
|
maskRepeat: 'no-repeat',
|
|
140
151
|
WebkitMaskRepeat: 'no-repeat',
|
|
141
|
-
} }), _jsx("div", { ref: guideRef, className: "border-utility-warning-700 pointer-events-none absolute left-1/2 top-1/2 z-10 aspect-[85.6/53.98] w-[calc(100%-1rem)] max-w-[24.688rem] -translate-x-1/2 -translate-y-1/2 rounded-md border-2 border-dashed" })] }));
|
|
152
|
+
} }), _jsx("div", { ref: context.guideRef, className: "border-utility-warning-700 pointer-events-none absolute left-1/2 top-1/2 z-10 aspect-[85.6/53.98] w-[calc(100%-1rem)] max-w-[24.688rem] -translate-x-1/2 -translate-y-1/2 rounded-md border-2 border-dashed transition-all duration-200" })] }));
|
|
142
153
|
}
|
|
143
154
|
function useIdDocumentField(props) {
|
|
144
155
|
const field = useFieldContext();
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import
|
|
1
|
+
import '@mediapipe/face_detection';
|
|
2
|
+
import '@tensorflow/tfjs-backend-webgl';
|
|
3
|
+
import '@tensorflow/tfjs-core';
|
|
4
|
+
import { type CSSProperties, type RefObject } from 'react';
|
|
2
5
|
import { type ValidateFileError } from '../../utils/validateFile';
|
|
3
|
-
export interface SelfieFieldProps {
|
|
4
|
-
ref?:
|
|
6
|
+
export interface SelfieFieldProps extends UseSelfieFieldProps {
|
|
7
|
+
ref?: RefObject<HTMLDivElement | null>;
|
|
5
8
|
id?: string;
|
|
6
9
|
style?: CSSProperties;
|
|
7
10
|
className?: string;
|
|
8
|
-
value?: string;
|
|
9
|
-
onChange?: (value: string) => void;
|
|
10
|
-
defaultValue?: string;
|
|
11
11
|
invalid?: boolean;
|
|
12
12
|
readOnly?: boolean;
|
|
13
13
|
disabled?: boolean;
|
|
@@ -15,3 +15,9 @@ export interface SelfieFieldProps {
|
|
|
15
15
|
onError?: (error: ValidateFileError) => void;
|
|
16
16
|
}
|
|
17
17
|
export declare function SelfieField(props: SelfieFieldProps): import("react/jsx-runtime").JSX.Element;
|
|
18
|
+
interface UseSelfieFieldProps {
|
|
19
|
+
value?: string;
|
|
20
|
+
onChange?: (value: string) => void;
|
|
21
|
+
defaultValue?: string;
|
|
22
|
+
}
|
|
23
|
+
export {};
|
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import '@mediapipe/face_detection';
|
|
4
|
+
import * as faceDetection from '@tensorflow-models/face-detection';
|
|
5
|
+
import '@tensorflow/tfjs-backend-webgl';
|
|
6
|
+
import '@tensorflow/tfjs-core';
|
|
3
7
|
import { useFieldContext } from '@ark-ui/react';
|
|
4
8
|
import Image from 'next/image';
|
|
5
9
|
import { useId, useLayoutEffect, useRef, useState, } from 'react';
|
|
6
10
|
import { twMerge } from 'tailwind-merge';
|
|
11
|
+
import { useInterval } from 'usehooks-ts';
|
|
7
12
|
import { useCamera } from '../../client/hooks/useCamera.js';
|
|
8
13
|
import { useControllableState } from '../../client/hooks/useControllableState.js';
|
|
9
14
|
import { useDisclosure } from '../../client/hooks/useDisclosure.js';
|
|
@@ -21,12 +26,10 @@ import { XIcon } from '../../icons/XIcon.js';
|
|
|
21
26
|
import { Button } from '../../ui/Button/index.js';
|
|
22
27
|
import { Dialog } from '../../ui/Dialog/index.js';
|
|
23
28
|
import { Portal } from '../../ui/Portal/index.js';
|
|
24
|
-
import { dataAttr } from '../../utils/dataAttr.js';
|
|
25
29
|
import { validateFile } from '../../utils/validateFile.js';
|
|
26
30
|
export function SelfieField(props) {
|
|
27
31
|
const context = useSelfieField(props);
|
|
28
|
-
|
|
29
|
-
return (_jsxs(SelfieFieldProvider, { value: context, children: [_jsxs("div", { ref: props.ref, id: props.id, style: props.style, className: twMerge('border-border-primary bg-bg-primary group relative aspect-[352/180] w-full shrink-0 rounded-xl border p-xl', props.className), onDragOver: (e) => e.preventDefault(), onDragEnter: (e) => e.preventDefault(), onDragLeave: (e) => e.preventDefault(), onDrop: (e) => {
|
|
32
|
+
return (_jsxs(SelfieFieldProvider, { value: context, children: [_jsxs("div", { ref: props.ref, id: props.id, style: props.style, className: twMerge('group', 'border', 'border-border-primary', 'bg-bg-primary', 'relative', 'w-full', 'aspect-[352/180]', 'shrink-0', 'rounded-xl', 'p-xl', props.className), onDragOver: (e) => e.preventDefault(), onDragEnter: (e) => e.preventDefault(), onDragLeave: (e) => e.preventDefault(), onDrop: (e) => {
|
|
30
33
|
e.preventDefault();
|
|
31
34
|
const file = e.dataTransfer.files?.[0];
|
|
32
35
|
if (!file)
|
|
@@ -34,103 +37,123 @@ export function SelfieField(props) {
|
|
|
34
37
|
const error = validateFile(file);
|
|
35
38
|
if (error)
|
|
36
39
|
return props.onError?.(error);
|
|
37
|
-
mutation.mutate({ file });
|
|
38
|
-
}, "
|
|
40
|
+
context.mutation.mutate({ file });
|
|
41
|
+
}, "aria-describedby": context.field?.ariaDescribedby, children: [_jsx("input", { id: context.field?.ids?.control, ref: context.inputRef, type: "file", accept: "image/png, image/jpeg", onChange: (e) => {
|
|
39
42
|
const file = e.target.files?.[0];
|
|
40
43
|
if (!file)
|
|
41
44
|
return;
|
|
42
45
|
const error = validateFile(file);
|
|
43
46
|
if (error)
|
|
44
47
|
return props.onError?.(error);
|
|
45
|
-
mutation.mutate({ file });
|
|
46
|
-
}, disabled: field?.disabled ||
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
inputRef.current?.click();
|
|
51
|
-
}, disabled: field?.disabled ||
|
|
52
|
-
field?.readOnly ||
|
|
48
|
+
context.mutation.mutate({ file });
|
|
49
|
+
}, disabled: context.field?.disabled ||
|
|
50
|
+
context.query.isLoading ||
|
|
51
|
+
context.mutation.isPending ||
|
|
52
|
+
props.disabled, readOnly: context.field?.readOnly || props.readOnly, required: context.field?.required || props.required, className: "sr-only" }), !context.query.data?.url && (_jsxs("div", { className: "flex size-full flex-col justify-center", children: [_jsx("div", { className: "border-border-primary shadow-xs mx-auto flex size-8 items-center justify-center rounded-sm border", children: _jsx(UploadCloud02Icon, { className: "text-text-placeholder-subtle size-4" }) }), _jsxs("h2", { className: "text-text-tertiary-600 mt-lg", children: [_jsxs("span", { className: "block text-center text-sm", children: [_jsx("button", { type: "button", onClick: () => {
|
|
53
|
+
context.inputRef.current?.click();
|
|
54
|
+
}, disabled: context.field?.disabled ||
|
|
55
|
+
context.field?.readOnly ||
|
|
56
|
+
context.query.isLoading ||
|
|
57
|
+
context.mutation.isPending ||
|
|
53
58
|
props.disabled ||
|
|
54
|
-
props.readOnly
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
59
|
+
props.readOnly, className: "text-button-secondary-fg font-semibold disabled:opacity-60", children: "Click to upload" }), ' ', "or drag and drop"] }), _jsx("span", { className: "mt-xs block text-center text-xs", children: "PNG, JPG or JPEG (max. 10mb)" }), _jsx("span", { className: "m-txs block text-center text-xs", children: "or" })] }), _jsx(Button, { size: "sm", variant: "outline", className: "mx-auto mt-md w-auto", onClick: () => {
|
|
60
|
+
context.disclosure.setOpen(true);
|
|
61
|
+
}, disabled: context.field?.disabled ||
|
|
62
|
+
context.field?.readOnly ||
|
|
63
|
+
context.query.isLoading ||
|
|
64
|
+
context.mutation.isPending ||
|
|
60
65
|
props.disabled ||
|
|
61
|
-
props.readOnly
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
setValue('');
|
|
65
|
-
}, children: _jsx(XIcon, { className: "size-5" }) })] }))] }), _jsx(Dialog.Root, { open: disclosure.open, onOpenChange: (details) => {
|
|
66
|
-
disclosure.setOpen(details.open);
|
|
67
|
-
}, onExitComplete: camera.close, children: _jsxs(Portal, { children: [_jsx(Dialog.Backdrop, { className: "!z-[calc(var(--z-dialog)+1)]" }), _jsx(Dialog.Positioner, { className: "!z-[calc(var(--z-dialog)+2)] flex items-center justify-center overflow-y-auto py-4", children: _jsxs(Dialog.Content, { className: "bg-bg-primary-alt mx-auto w-[calc(100dvw-1rem)] max-w-[calc(100dvw-1rem)] rounded-lg px-4 py-5 lg:w-[747px] lg:max-w-[747px] lg:px-3xl lg:py-4xl", children: [_jsx(Dialog.CloseTrigger, { children: _jsx(XIcon, {}) }), _jsx(Dialog.Title, { className: "text-center font-semibold lg:text-xl", children: "Take a Selfie Holding Your ID" }), _jsx(Dialog.Description, { className: "text-text-tertiary-600 mt-md text-center text-xs lg:text-base", children: "Make sure your face and ID are clearly visible, with no glare\u00A0or\u00A0blur." }), _jsxs("div", { className: "relative mt-5 lg:mt-10 lg:px-3xl", children: [_jsxs("div", { className: twMerge('border-border-disabled relative w-full overflow-hidden rounded-md border bg-black lg:rounded-lg', !camera.data && !camera.error && !camera.loading
|
|
68
|
-
? 'block'
|
|
69
|
-
: 'hidden'), children: [_jsx("video", { ...camera.videoProps }), _jsx(Mask, {})] }), camera.error && (_jsxs("div", { className: "border-border-disabled flex aspect-[85.60/53.98] flex-col items-center justify-center rounded-md border bg-black px-4 lg:rounded-lg", children: [_jsx(CameraOffIcon, { className: "text-text-placeholder-subtle size-10 text-center lg:size-12" }), _jsx("h2", { className: "mt-3 text-sm font-semibold lg:mt-4 lg:text-base", children: camera.error.name }), _jsx("p", { className: "text-text-tertiary-600 mt-0.5 text-center text-xs lg:mt-1 lg:text-sm", children: camera.error.message }), _jsx(Button, { size: "xs", variant: "outline", colorScheme: "gray", fullWidth: false, className: "mt-4 lg:mt-5", onClick: () => {
|
|
70
|
-
camera.reopen();
|
|
71
|
-
}, children: "Try again" })] })), camera.loading && (_jsx("div", { className: "border-border-disabled flex aspect-[85.60/53.98] flex-col items-center justify-center rounded-md border bg-black px-4 lg:rounded-lg", children: _jsx(Spinner02Icon, { className: "text-text-placeholder size-8" }) })), camera.data && (_jsx("div", { className: "border-border-disabled h-auto w-full overflow-hidden rounded-md border bg-black lg:rounded-lg", children: _jsx(Image, { src: camera.data.url, alt: "", width: 600, height: 600, unoptimized: true, className: "block h-auto w-full" }) }))] }), !camera.data && !camera.error && !camera.loading && (_jsx("div", { className: "mt-5 lg:mt-10", children: _jsxs(Button, { className: "disabled:bg-bg-disabled disabled:text-text-disabled mx-auto w-full px-xl lg:w-auto", onClick: () => {
|
|
72
|
-
const rect = guideRef.current?.getBoundingClientRect();
|
|
73
|
-
const crop = !rect
|
|
74
|
-
? undefined
|
|
75
|
-
: {
|
|
76
|
-
y: rect.y,
|
|
77
|
-
x: rect.x,
|
|
78
|
-
width: rect.width,
|
|
79
|
-
height: rect.height,
|
|
80
|
-
};
|
|
81
|
-
camera.snap({ crop });
|
|
82
|
-
}, children: [_jsx(Camera01Icon, { className: "size-5" }), "Capture"] }) })), camera.data && (_jsxs("div", { className: "mt-5 flex gap-3 lg:mt-10", children: [_jsxs(Button, { onClick: () => {
|
|
83
|
-
const file = camera.data?.file;
|
|
84
|
-
if (!file)
|
|
85
|
-
return;
|
|
86
|
-
mutation.mutate({ file });
|
|
87
|
-
disclosure.setOpen(false);
|
|
88
|
-
}, children: [_jsx(CheckCircleIcon, { className: "size-5" }), "Use Photo"] }), _jsxs(Button, { variant: "outline", colorScheme: "gray", onClick: () => {
|
|
89
|
-
camera.reset();
|
|
90
|
-
}, children: [_jsx(RefreshCcw01Icon, { className: "size-5" }), " Retake"] })] }))] }) })] }) })] }));
|
|
66
|
+
props.readOnly, children: "Take a photo" })] })), context.query.data?.url && (_jsxs("div", { className: "relative", children: [_jsx(Image, { src: context.query.data.url, alt: "", width: 600, height: 600, unoptimized: true, className: "h-auto w-full rounded-md" }), _jsx("button", { type: "button", className: "backdrop-blur-xs absolute right-2 top-2 rounded-full bg-white/10 p-1", onClick: () => {
|
|
67
|
+
context.setValue('');
|
|
68
|
+
}, children: _jsx(XIcon, { className: "size-5" }) })] }))] }), _jsx(Camera, {})] }));
|
|
91
69
|
}
|
|
92
70
|
/**
|
|
71
|
+
* -------------------------------------
|
|
72
|
+
* Camera
|
|
73
|
+
* -------------------------------------
|
|
74
|
+
*/
|
|
75
|
+
function Camera() {
|
|
76
|
+
const context = useSelfieFieldContext();
|
|
77
|
+
return (_jsx(Dialog.Root, { open: context.disclosure.open, onOpenChange: (details) => {
|
|
78
|
+
context.disclosure.setOpen(details.open);
|
|
79
|
+
}, onExitComplete: context.camera.close, children: _jsxs(Portal, { children: [_jsx(Dialog.Backdrop, { className: "!z-[calc(var(--z-dialog)+1)]" }), _jsx(Dialog.Positioner, { className: "!z-[calc(var(--z-dialog)+2)] flex items-center justify-center overflow-y-auto py-4", children: _jsxs(Dialog.Content, { className: "bg-bg-primary-alt mx-auto w-[calc(100dvw-1rem)] max-w-[calc(100dvw-1rem)] overflow-y-auto rounded-lg px-4 py-5 lg:w-[747px] lg:max-w-[747px] lg:px-3xl lg:py-4xl", children: [_jsx(Dialog.CloseTrigger, { children: _jsx(XIcon, {}) }), _jsx(Dialog.Title, { className: "text-center font-semibold lg:text-xl", children: "Take a Selfie Holding Your ID" }), _jsx(Dialog.Description, { className: "text-text-tertiary-600 mt-md text-center text-xs lg:text-base", children: "Make sure your face and ID are clearly visible, with no glare\u00A0or\u00A0blur." }), _jsxs("div", { className: "relative mt-5 lg:mt-10 lg:px-3xl", children: [_jsxs("div", { className: twMerge('border-border-disabled relative w-full overflow-hidden rounded-md border bg-black lg:rounded-lg', !context.camera.data &&
|
|
80
|
+
!context.camera.error &&
|
|
81
|
+
!context.camera.loading
|
|
82
|
+
? 'block'
|
|
83
|
+
: 'hidden'), children: [_jsx("video", { ...context.camera.videoProps }), _jsx(Mask, {})] }), context.camera.error && (_jsxs("div", { className: "border-border-disabled flex aspect-[85.60/53.98] flex-col items-center justify-center rounded-md border bg-black px-4 lg:rounded-lg", children: [_jsx(CameraOffIcon, { className: "text-text-placeholder-subtle size-10 text-center lg:size-12" }), _jsx("h2", { className: "mt-3 text-sm font-semibold lg:mt-4 lg:text-base", children: context.camera.error.name }), _jsx("p", { className: "text-text-tertiary-600 mt-0.5 text-center text-xs lg:mt-1 lg:text-sm", children: context.camera.error.message }), _jsx(Button, { size: "xs", variant: "outline", colorScheme: "gray", fullWidth: false, className: "mt-4 lg:mt-5", onClick: () => {
|
|
84
|
+
context.camera.reopen();
|
|
85
|
+
}, children: "Try again" })] })), context.camera.loading && (_jsx("div", { className: "border-border-disabled flex aspect-[85.60/53.98] flex-col items-center justify-center rounded-md border bg-black px-4 lg:rounded-lg", children: _jsx(Spinner02Icon, { className: "text-text-placeholder size-8" }) })), context.camera.data && (_jsx("div", { className: "border-border-disabled h-auto w-full overflow-hidden rounded-md border bg-black lg:rounded-lg", children: _jsx(Image, { src: context.camera.data.url, alt: "", width: 600, height: 600, unoptimized: true, className: "block h-auto w-full" }) }))] }), !context.camera.data &&
|
|
86
|
+
!context.camera.error &&
|
|
87
|
+
!context.camera.loading && (_jsx("div", { className: "mt-5 lg:mt-10", children: _jsxs(Button, { className: "disabled:bg-bg-disabled disabled:text-text-disabled mx-auto w-full px-xl lg:w-auto", onClick: () => {
|
|
88
|
+
const rect = context.guideRef.current?.getBoundingClientRect();
|
|
89
|
+
const crop = !rect
|
|
90
|
+
? undefined
|
|
91
|
+
: {
|
|
92
|
+
y: rect.y,
|
|
93
|
+
x: rect.x,
|
|
94
|
+
width: rect.width,
|
|
95
|
+
height: rect.height,
|
|
96
|
+
};
|
|
97
|
+
context.camera.snap({ crop });
|
|
98
|
+
}, disabled: !context.faceFound, children: [_jsx(Camera01Icon, { className: "size-5" }), "Capture"] }) })), context.camera.data && (_jsxs("div", { className: "mt-5 flex gap-3 lg:mt-10", children: [_jsxs(Button, { onClick: () => {
|
|
99
|
+
const file = context.camera.data?.file;
|
|
100
|
+
if (!file)
|
|
101
|
+
return;
|
|
102
|
+
context.mutation.mutate({ file });
|
|
103
|
+
context.disclosure.setOpen(false);
|
|
104
|
+
}, children: [_jsx(CheckCircleIcon, { className: "size-5" }), "Use Photo"] }), _jsxs(Button, { variant: "outline", colorScheme: "gray", onClick: () => {
|
|
105
|
+
context.camera.reset();
|
|
106
|
+
}, children: [_jsx(RefreshCcw01Icon, { className: "size-5" }), " Retake"] })] }))] }) })] }) }));
|
|
107
|
+
}
|
|
108
|
+
/*
|
|
93
109
|
* -------------------------------------
|
|
94
110
|
* Mask
|
|
95
111
|
* -------------------------------------
|
|
96
112
|
*/
|
|
97
113
|
function Mask() {
|
|
98
114
|
const id = useId();
|
|
99
|
-
const
|
|
115
|
+
const context = useSelfieFieldContext();
|
|
100
116
|
const [maskBox, setMaskBox] = useState({
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
117
|
+
x: 0,
|
|
118
|
+
y: 0,
|
|
119
|
+
width: 0,
|
|
120
|
+
height: 0,
|
|
104
121
|
});
|
|
105
122
|
useLayoutEffect(() => {
|
|
106
123
|
const update = () => {
|
|
107
|
-
const guide = guideRef.current;
|
|
108
|
-
const mask = maskRef.current;
|
|
124
|
+
const guide = context.guideRef.current;
|
|
125
|
+
const mask = context.maskRef.current;
|
|
109
126
|
if (!guide || !mask)
|
|
110
127
|
return;
|
|
111
128
|
const guideRect = guide.getBoundingClientRect();
|
|
112
129
|
const maskRect = mask.getBoundingClientRect();
|
|
113
|
-
const
|
|
114
|
-
const
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
130
|
+
const x = guideRect.left - maskRect.left;
|
|
131
|
+
const y = guideRect.top - maskRect.top;
|
|
132
|
+
const width = guideRect.width;
|
|
133
|
+
const height = guideRect.height;
|
|
134
|
+
if (width && height) {
|
|
135
|
+
setMaskBox({
|
|
136
|
+
x,
|
|
137
|
+
y,
|
|
138
|
+
width,
|
|
139
|
+
height,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
121
142
|
};
|
|
122
|
-
update();
|
|
123
143
|
const observer = new ResizeObserver(update);
|
|
124
|
-
if (guideRef.current)
|
|
125
|
-
observer.observe(guideRef.current);
|
|
126
|
-
if (maskRef.current)
|
|
127
|
-
observer.observe(maskRef.current);
|
|
144
|
+
if (context.guideRef.current)
|
|
145
|
+
observer.observe(context.guideRef.current);
|
|
146
|
+
if (context.maskRef.current)
|
|
147
|
+
observer.observe(context.maskRef.current);
|
|
148
|
+
requestAnimationFrame(update);
|
|
128
149
|
return () => observer.disconnect();
|
|
129
|
-
}, [guideRef, maskRef]);
|
|
130
|
-
return (_jsxs(_Fragment, { children: [_jsx("svg", { className: "absolute h-full w-full", children: _jsxs("mask", { id: id, maskUnits: "userSpaceOnUse", children: [_jsx("rect", { width: "100%", height: "100%", fill: "white" }), _jsx("
|
|
150
|
+
}, [context.guideRef, context.maskRef]);
|
|
151
|
+
return (_jsxs(_Fragment, { children: [_jsx("svg", { className: "pointer-events-none absolute h-full w-full", children: _jsx("defs", { children: _jsxs("mask", { id: id, maskUnits: "userSpaceOnUse", children: [_jsx("rect", { x: "0", y: "0", width: "100%", height: "100%", fill: "white" }), _jsx("rect", { x: maskBox.x, y: maskBox.y, width: maskBox.width, height: maskBox.height, fill: "black", rx: "8" /* change to 100% for circle */, ry: "8" /* change to 100% for circle */ })] }) }) }), _jsx("div", { ref: context.maskRef, className: "absolute inset-0 bg-black/60 backdrop-blur-sm", style: {
|
|
131
152
|
mask: `url(#${id})`,
|
|
132
153
|
WebkitMask: `url(#${id})`,
|
|
133
|
-
|
|
154
|
+
maskRepeat: 'no-repeat',
|
|
155
|
+
WebkitMaskRepeat: 'no-repeat',
|
|
156
|
+
} }), _jsx("div", { ref: context.guideRef, className: twMerge('border-utility-warning-700 absolute left-1/2 top-1/2 z-20 aspect-square w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 rounded-md border-2 border-dashed transition-all duration-200 lg:w-[24.688rem]', context.faceFound && 'border-utility-success-700 border-solid') })] }));
|
|
134
157
|
}
|
|
135
158
|
function useSelfieField(props) {
|
|
136
159
|
const field = useFieldContext();
|
|
@@ -166,7 +189,22 @@ function useSelfieField(props) {
|
|
|
166
189
|
const inputRef = useRef(null);
|
|
167
190
|
const maskRef = useRef(null);
|
|
168
191
|
const guideRef = useRef(null);
|
|
169
|
-
const camera = useCamera(
|
|
192
|
+
const camera = useCamera({
|
|
193
|
+
async validate(data) {
|
|
194
|
+
const face = await getFaceFromFile(data.file);
|
|
195
|
+
console.log(face);
|
|
196
|
+
/*
|
|
197
|
+
⛏️ FIXME
|
|
198
|
+
if (!face) {
|
|
199
|
+
return {
|
|
200
|
+
name: 'FaceNotFoundError',
|
|
201
|
+
message: 'Please ensure your face is clearly visible in the photo.',
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
*/
|
|
205
|
+
return null;
|
|
206
|
+
},
|
|
207
|
+
});
|
|
170
208
|
const disclosure = useDisclosure({
|
|
171
209
|
onOpenChange(open) {
|
|
172
210
|
if (open) {
|
|
@@ -174,6 +212,37 @@ function useSelfieField(props) {
|
|
|
174
212
|
}
|
|
175
213
|
},
|
|
176
214
|
});
|
|
215
|
+
const [faceFound, setFaceFound] = useState(false);
|
|
216
|
+
useInterval(async () => {
|
|
217
|
+
if (!camera.videoRef.current)
|
|
218
|
+
return setFaceFound(false);
|
|
219
|
+
if (!guideRef.current)
|
|
220
|
+
return setFaceFound(false);
|
|
221
|
+
const guide = guideRef.current;
|
|
222
|
+
const video = camera.videoRef.current;
|
|
223
|
+
const guideRect = guide.getBoundingClientRect();
|
|
224
|
+
const videoRect = video.getBoundingClientRect();
|
|
225
|
+
const face = await getFace(video);
|
|
226
|
+
if (!face)
|
|
227
|
+
return setFaceFound(false);
|
|
228
|
+
/* ⛏️ FIXME */
|
|
229
|
+
const scaleX = videoRect.width / video.videoWidth;
|
|
230
|
+
const scaleY = videoRect.height / video.videoHeight;
|
|
231
|
+
const offsetX = videoRect.left;
|
|
232
|
+
const offsetY = videoRect.top;
|
|
233
|
+
const faceBoxX = offsetX + face.box.xMin * video.videoWidth * scaleX;
|
|
234
|
+
const faceBoxY = offsetY + face.box.yMin * video.videoHeight * scaleY;
|
|
235
|
+
const faceBoxWidth = (face.box.xMax - face.box.xMin) * video.videoWidth * scaleX;
|
|
236
|
+
const faceBoxHeight = (face.box.yMax - face.box.yMin) * video.videoHeight * scaleY;
|
|
237
|
+
const faceIsInsideGuide = faceBoxX >= guideRect.left &&
|
|
238
|
+
faceBoxY >= guideRect.top &&
|
|
239
|
+
faceBoxX + faceBoxWidth <= guideRect.right &&
|
|
240
|
+
faceBoxY + faceBoxHeight <= guideRect.bottom;
|
|
241
|
+
console.log({ faceIsInsideGuide });
|
|
242
|
+
setFaceFound(true);
|
|
243
|
+
}, disclosure.open && !camera.data && !camera.error && !camera.loading
|
|
244
|
+
? 1000
|
|
245
|
+
: null);
|
|
177
246
|
return {
|
|
178
247
|
field,
|
|
179
248
|
value,
|
|
@@ -185,6 +254,53 @@ function useSelfieField(props) {
|
|
|
185
254
|
inputRef,
|
|
186
255
|
guideRef,
|
|
187
256
|
maskRef,
|
|
257
|
+
faceFound,
|
|
188
258
|
};
|
|
189
259
|
}
|
|
190
260
|
const [SelfieFieldProvider, useSelfieFieldContext] = createContext();
|
|
261
|
+
/*
|
|
262
|
+
* -------------------------------------
|
|
263
|
+
* Utils
|
|
264
|
+
* -------------------------------------
|
|
265
|
+
*/
|
|
266
|
+
let __face_detector__;
|
|
267
|
+
async function getFaceDetector() {
|
|
268
|
+
if (__face_detector__)
|
|
269
|
+
return __face_detector__;
|
|
270
|
+
__face_detector__ = await faceDetection.createDetector(faceDetection.SupportedModels.MediaPipeFaceDetector, {
|
|
271
|
+
runtime: 'tfjs',
|
|
272
|
+
maxFaces: 1,
|
|
273
|
+
});
|
|
274
|
+
return __face_detector__;
|
|
275
|
+
}
|
|
276
|
+
async function getFace(video) {
|
|
277
|
+
if (!video || video.readyState < 2)
|
|
278
|
+
return null;
|
|
279
|
+
try {
|
|
280
|
+
const detector = await getFaceDetector();
|
|
281
|
+
const faces = await detector.estimateFaces(video);
|
|
282
|
+
return faces.at(0) ?? null;
|
|
283
|
+
}
|
|
284
|
+
catch {
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
function fileToImage(file) {
|
|
289
|
+
return new Promise((resolve, reject) => {
|
|
290
|
+
const img = new globalThis.Image();
|
|
291
|
+
img.src = URL.createObjectURL(file);
|
|
292
|
+
img.onload = () => resolve(img);
|
|
293
|
+
img.onerror = () => reject('Failed to load image');
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
async function getFaceFromFile(file) {
|
|
297
|
+
try {
|
|
298
|
+
const detector = await getFaceDetector();
|
|
299
|
+
const image = await fileToImage(file);
|
|
300
|
+
const faces = await detector.estimateFaces(image);
|
|
301
|
+
return faces.at(0) ?? null;
|
|
302
|
+
}
|
|
303
|
+
catch {
|
|
304
|
+
return null;
|
|
305
|
+
}
|
|
306
|
+
}
|
package/dist/next.config.js
CHANGED
|
@@ -48,6 +48,14 @@ export const config = {
|
|
|
48
48
|
compiler: {
|
|
49
49
|
removeConsole: process.env.NODE_ENV === 'production',
|
|
50
50
|
},
|
|
51
|
+
webpack(config) {
|
|
52
|
+
config.resolve.fallback = {
|
|
53
|
+
fs: false,
|
|
54
|
+
path: false,
|
|
55
|
+
crypto: false,
|
|
56
|
+
};
|
|
57
|
+
return config;
|
|
58
|
+
},
|
|
51
59
|
};
|
|
52
60
|
export function extendConfig(userConfig) {
|
|
53
61
|
return {
|