@opexa/portal-components 0.0.271 → 0.0.274
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 +1 -1
- package/dist/client/hooks/useCamera.js +51 -43
- package/dist/client/hooks/usePxToRemConverter.d.ts +2 -0
- package/dist/client/hooks/usePxToRemConverter.js +18 -0
- package/dist/client/hooks/useRect.d.ts +5 -0
- package/dist/client/hooks/useRect.js +45 -0
- package/dist/components/Bonus/Bonus.client.js +1 -1
- package/dist/components/Cashback/Cashback.client.js +1 -1
- package/dist/components/CustomPromo/CustomPromo.js +1 -1
- package/dist/components/GameLaunch/GameLaunch.lazy.js +1 -1
- package/dist/components/GamesSearch/GamesSearch.js +1 -1
- package/dist/components/Jackpots/JackpotsCarousel/JackpotsCarouselItem.module.css +184 -184
- package/dist/components/Jackpots/JackpotsList/JackpotsListItem.module.css +184 -184
- package/dist/components/KYC/AutoOpen.js +3 -8
- package/dist/components/KYC/KYCReminder.lazy.js +2 -0
- package/dist/components/KYC/utils.js +18 -18
- package/dist/components/Messages/Message.js +3 -2
- package/dist/components/Messages/MessagePopup.js +3 -2
- package/dist/components/Messages/MessagesPopup.js +4 -2
- package/dist/components/News/News.js +1 -1
- package/dist/components/PortalProvider/FeatureFlag.js +1 -1
- package/dist/components/Promo/Promo.client.js +1 -1
- package/dist/components/Search/Search.lazy.js +1 -1
- package/dist/components/SignUp/SignUpForm.js +0 -4
- package/dist/components/SignUp/SignUpKYC/IdFrontImageField/IdFrontImageField.client.d.ts +15 -0
- package/dist/components/SignUp/SignUpKYC/IdFrontImageField/IdFrontImageField.client.js +100 -0
- package/dist/components/SignUp/SignUpKYC/IdFrontImageField/IdFrontImageField.d.ts +2 -0
- package/dist/components/SignUp/SignUpKYC/IdFrontImageField/IdFrontImageField.js +11 -0
- package/dist/components/SignUp/SignUpKYC/IdFrontImageField/IdFrontImageFieldContext.d.ts +6 -0
- package/dist/components/SignUp/SignUpKYC/IdFrontImageField/IdFrontImageFieldContext.js +2 -0
- package/dist/components/SignUp/SignUpKYC/IdFrontImageField/index.d.ts +1 -0
- package/dist/components/SignUp/SignUpKYC/IdFrontImageField/index.js +1 -0
- package/dist/components/SignUp/SignUpKYC/IdFrontImageField/useIdFrontImageField.d.ts +18 -0
- package/dist/components/SignUp/SignUpKYC/IdFrontImageField/useIdFrontImageField.js +88 -0
- package/dist/components/SignUp/SignUpKYC/SelfieImageField/SelfieImageField.client.d.ts +15 -0
- package/dist/components/SignUp/SignUpKYC/SelfieImageField/SelfieImageField.client.js +113 -0
- package/dist/components/SignUp/SignUpKYC/SelfieImageField/SelfieImageField.d.ts +2 -0
- package/dist/components/SignUp/SignUpKYC/SelfieImageField/SelfieImageField.js +11 -0
- package/dist/components/SignUp/SignUpKYC/SelfieImageField/SelfieImageFieldContext.d.ts +6 -0
- package/dist/components/SignUp/SignUpKYC/SelfieImageField/SelfieImageFieldContext.js +2 -0
- package/dist/components/SignUp/SignUpKYC/SelfieImageField/index.d.ts +1 -0
- package/dist/components/SignUp/SignUpKYC/SelfieImageField/index.js +1 -0
- package/dist/components/SignUp/SignUpKYC/SelfieImageField/useSelfieImageField.d.ts +19 -0
- package/dist/components/SignUp/SignUpKYC/SelfieImageField/useSelfieImageField.js +186 -0
- package/dist/components/SignUp/SignUpKYC/SignUpKYC.lazy.js +3 -12
- package/dist/components/SignUp/SignUpKYC/SignUpKYCContext.d.ts +2 -3
- package/dist/components/SignUp/SignUpKYC/SignUpKYCForm.d.ts +1 -0
- package/dist/components/SignUp/SignUpKYC/SignUpKYCForm.js +464 -0
- package/dist/components/SignUp/SignUpKYC/utils.js +18 -18
- package/dist/components/SignUp/SignUpPagcor/CaptureIdDocument.d.ts +1 -0
- package/dist/components/SignUp/SignUpPagcor/CaptureIdDocument.js +198 -0
- package/dist/components/SignUp/SignUpPagcor/CaptureSelfie.d.ts +1 -0
- package/dist/components/SignUp/SignUpPagcor/CaptureSelfie.js +251 -0
- package/dist/components/SignUp/SignUpPagcor/ImageUploader.d.ts +10 -0
- package/dist/components/SignUp/SignUpPagcor/ImageUploader.js +41 -0
- package/dist/components/SignUp/SignUpPagcor/SignUpFormPagcor.d.ts +1 -0
- package/dist/components/SignUp/SignUpPagcor/SignUpFormPagcor.js +429 -0
- package/dist/components/SignUp/SignUpPagcor/SignUpPagcor.lazy.d.ts +13 -0
- package/dist/components/SignUp/SignUpPagcor/SignUpPagcor.lazy.js +26 -0
- package/dist/components/SignUp/SignUpPagcor/SignUpPagcorContext.d.ts +7 -0
- package/dist/components/SignUp/SignUpPagcor/SignUpPagcorContext.js +2 -0
- package/dist/components/SignUp/SignUpPagcor/useImageUploader.d.ts +11 -0
- package/dist/components/SignUp/SignUpPagcor/useImageUploader.js +20 -0
- package/dist/components/Tournaments/TournamentsCarousel/TournamentsCarouselItem.module.css +184 -184
- package/dist/components/Tournaments/TournamentsList/TournamentItem.module.css +184 -184
- package/dist/components/shared/IdFrontImageField/IdFrontImageField.client.js +11 -46
- package/dist/components/shared/SelfieImageField/SelfieImageField.client.js +20 -72
- package/dist/constants/EnvVar.d.ts +1 -1
- package/dist/handlers/postTransformIdFrontImage.d.ts +3 -0
- package/dist/handlers/postTransformIdFrontImage.js +67 -0
- package/dist/handlers/postTransformSelfieImage.d.ts +3 -0
- package/dist/handlers/postTransformSelfieImage.js +71 -0
- package/dist/next.config.js +0 -8
- package/dist/services/queries.js +2570 -2570
- package/dist/styles/theme.css +695 -695
- package/dist/ui/AlertDialog/AlertDialog.d.ts +88 -88
- package/dist/ui/AlertDialog/alertDialog.recipe.d.ts +8 -8
- package/dist/ui/Badge/Badge.d.ts +12 -12
- package/dist/ui/Badge/badge.anatomy.d.ts +1 -1
- package/dist/ui/Badge/badge.recipe.d.ts +3 -3
- package/dist/ui/Carousel/Carousel.d.ts +72 -72
- package/dist/ui/Carousel/carousel.recipe.d.ts +8 -8
- 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/Clipboard/clipboard.recipe.d.ts +3 -3
- package/dist/ui/Collapsible/Collapsible.d.ts +32 -32
- package/dist/ui/Collapsible/collapsible.recipe.d.ts +8 -8
- 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 +8 -8
- package/dist/ui/Drawer/Drawer.d.ts +33 -33
- package/dist/ui/Drawer/drawer.recipe.d.ts +3 -3
- package/dist/ui/Field/Field.d.ts +21 -21
- package/dist/ui/Field/field.recipe.d.ts +3 -3
- package/dist/ui/Menu/Menu.d.ts +306 -306
- package/dist/ui/Menu/menu.recipe.d.ts +17 -17
- package/dist/ui/PasswordInput/PasswordInput.d.ts +18 -18
- package/dist/ui/PasswordInput/passwordInput.recipe.d.ts +3 -3
- package/dist/ui/Popover/Popover.d.ts +88 -88
- 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/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/dist/utils/resizeImageSize.d.ts +1 -0
- package/dist/utils/resizeImageSize.js +5 -0
- package/package.json +163 -165
- package/dist/client/hooks/index.d.ts +0 -65
- package/dist/client/hooks/index.js +0 -65
- package/dist/client/services/index.d.ts +0 -4
- package/dist/client/services/index.js +0 -4
- package/dist/client/utils/index.d.ts +0 -3
- package/dist/client/utils/index.js +0 -3
- package/dist/components/CashbackBonus/CashbackBonus.client.d.ts +0 -2
- package/dist/components/CashbackBonus/CashbackBonus.client.js +0 -9
- package/dist/components/CashbackBonus/CashbackBonus.d.ts +0 -10
- package/dist/components/CashbackBonus/CashbackBonus.js +0 -5
- package/dist/components/CashbackBonus/index.d.ts +0 -1
- package/dist/components/CashbackBonus/index.js +0 -1
- package/dist/components/Empty.d.ts +0 -9
- package/dist/components/Empty.js +0 -7
- package/dist/components/Games/GameContext.d.ts +0 -2
- package/dist/components/Games/GameContext.js +0 -2
- package/dist/components/InfiniteScroll.d.ts +0 -10
- package/dist/components/InfiniteScroll.js +0 -25
- package/dist/components/JackpotCounter/JackpotCounter.client.d.ts +0 -2
- package/dist/components/JackpotCounter/JackpotCounter.client.js +0 -66
- package/dist/components/JackpotCounter/JackpotCounter.d.ts +0 -17
- package/dist/components/JackpotCounter/JackpotCounter.js +0 -21
- package/dist/components/JackpotCounter/JackpotCounterItem.d.ts +0 -18
- package/dist/components/JackpotCounter/JackpotCounterItem.js +0 -109
- package/dist/components/JackpotCounter/JackpotCounterItem.module.css +0 -184
- package/dist/components/JackpotCounter/index.d.ts +0 -1
- package/dist/components/JackpotCounter/index.js +0 -1
- package/dist/components/Jackpots/JackpotDesktop.d.ts +0 -1
- package/dist/components/Jackpots/JackpotDesktop.js +0 -45
- package/dist/components/Jackpots/JackpotGameProviders.d.ts +0 -5
- package/dist/components/Jackpots/JackpotGameProviders.js +0 -41
- package/dist/components/Jackpots/JackpotItemDesktop.d.ts +0 -9
- package/dist/components/Jackpots/JackpotItemDesktop.js +0 -43
- package/dist/components/Jackpots/JackpotItemMobile.d.ts +0 -9
- package/dist/components/Jackpots/JackpotItemMobile.js +0 -44
- package/dist/components/Jackpots/JackpotMobile.d.ts +0 -1
- package/dist/components/Jackpots/JackpotMobile.js +0 -46
- package/dist/components/Jackpots/JackpotRules.d.ts +0 -5
- package/dist/components/Jackpots/JackpotRules.js +0 -11
- package/dist/components/Jackpots/Jackpots.client.d.ts +0 -2
- package/dist/components/Jackpots/Jackpots.client.js +0 -29
- package/dist/components/Jackpots/JackpotsContext.d.ts +0 -4
- package/dist/components/Jackpots/JackpotsContext.js +0 -3
- package/dist/components/Jackpots/useJackpotData.d.ts +0 -14
- package/dist/components/Jackpots/useJackpotData.js +0 -93
- package/dist/components/Jackpots/useJackpotItemData.d.ts +0 -20
- package/dist/components/Jackpots/useJackpotItemData.js +0 -87
- package/dist/components/KYC/Header.d.ts +0 -1
- package/dist/components/KYC/Header.js +0 -8
- package/dist/components/KYC/Step1.d.ts +0 -1
- package/dist/components/KYC/Step1.js +0 -13
- package/dist/components/KYC/Step2.d.ts +0 -1
- package/dist/components/KYC/Step2.js +0 -13
- package/dist/components/KYC/Step3.d.ts +0 -1
- package/dist/components/KYC/Step3.js +0 -13
- package/dist/components/KYC/Step4.d.ts +0 -1
- package/dist/components/KYC/Step4.js +0 -7
- package/dist/components/Messages/MessageItem.d.ts +0 -4
- package/dist/components/Messages/MessageItem.js +0 -34
- package/dist/components/Promo/PromoContent.d.ts +0 -2
- package/dist/components/Promo/PromoContent.js +0 -9
- package/dist/components/Promos/PromoCarousel.d.ts +0 -2
- package/dist/components/Promos/PromoCarousel.js +0 -41
- package/dist/components/Promos/PromoGrid.d.ts +0 -2
- package/dist/components/Promos/PromoGrid.js +0 -12
- package/dist/components/Promos/PromoItem.d.ts +0 -7
- package/dist/components/Promos/PromoItem.js +0 -10
- package/dist/components/TournamentCounter/TournamentCounter.client.d.ts +0 -2
- package/dist/components/TournamentCounter/TournamentCounter.client.js +0 -50
- package/dist/components/TournamentCounter/TournamentCounter.d.ts +0 -8
- package/dist/components/TournamentCounter/TournamentCounter.js +0 -21
- package/dist/components/TournamentCounter/TournamentCounterItem.d.ts +0 -8
- package/dist/components/TournamentCounter/TournamentCounterItem.js +0 -106
- package/dist/components/TournamentCounter/index.d.ts +0 -1
- package/dist/components/TournamentCounter/index.js +0 -1
- package/dist/components/Tournaments/TournamentDesktop.d.ts +0 -1
- package/dist/components/Tournaments/TournamentDesktop.js +0 -57
- package/dist/components/Tournaments/TournamentGameProviders.d.ts +0 -5
- package/dist/components/Tournaments/TournamentGameProviders.js +0 -38
- package/dist/components/Tournaments/TournamentItemDesktop.d.ts +0 -7
- package/dist/components/Tournaments/TournamentItemDesktop.js +0 -56
- package/dist/components/Tournaments/TournamentItemMobile.d.ts +0 -7
- package/dist/components/Tournaments/TournamentItemMobile.js +0 -54
- package/dist/components/Tournaments/TournamentMobile.d.ts +0 -1
- package/dist/components/Tournaments/TournamentMobile.js +0 -55
- package/dist/components/Tournaments/TournamentRules.d.ts +0 -5
- package/dist/components/Tournaments/TournamentRules.js +0 -11
- package/dist/components/Tournaments/Tournaments.client.d.ts +0 -2
- package/dist/components/Tournaments/Tournaments.client.js +0 -20
- package/dist/components/Tournaments/TournamentsContext.d.ts +0 -4
- package/dist/components/Tournaments/TournamentsContext.js +0 -3
- package/dist/components/Tournaments/useTournamentData.d.ts +0 -10
- package/dist/components/Tournaments/useTournamentData.js +0 -72
- package/dist/components/Tournaments/useTournamentItemData.d.ts +0 -16
- package/dist/components/Tournaments/useTournamentItemData.js +0 -64
- package/dist/components/index.d.ts +0 -33
- package/dist/components/index.js +0 -33
- package/dist/constants/endpoints.d.ts +0 -10
- package/dist/constants/endpoints.js +0 -41
- package/dist/constants/envVars.d.ts +0 -5
- package/dist/constants/envVars.js +0 -28
- package/dist/constants/gameProviders.d.ts +0 -76
- package/dist/constants/gameProviders.js +0 -361
- package/dist/constants/gameTypes.d.ts +0 -3
- package/dist/constants/gameTypes.js +0 -64
- package/dist/constants/headerKeys.d.ts +0 -2
- package/dist/constants/headerKeys.js +0 -2
- package/dist/constants/secretQuestions.d.ts +0 -3
- package/dist/constants/secretQuestions.js +0 -24
- package/dist/constants/storageKeys.d.ts +0 -2
- package/dist/constants/storageKeys.js +0 -2
- package/dist/icons/index.d.ts +0 -65
- package/dist/icons/index.js +0 -65
- package/dist/images/hacksaw-gaming.png +0 -0
- package/dist/server/services/index.d.ts +0 -4
- package/dist/server/services/index.js +0 -4
- package/dist/server/utils/index.d.ts +0 -29
- package/dist/server/utils/index.js +0 -29
- package/dist/ui/Presence.d.ts +0 -2
- package/dist/ui/Presence.js +0 -7
- package/dist/ui/Prose.d.ts +0 -2
- package/dist/ui/Prose.js +0 -5
- package/dist/ui/index.d.ts +0 -26
- package/dist/ui/index.js +0 -26
- package/dist/utils/createErrorFromCode.d.ts +0 -2
- package/dist/utils/createErrorFromCode.js +0 -11
- package/dist/utils/index.d.ts +0 -17
- package/dist/utils/index.js +0 -17
- package/dist/utils/isMobile.d.ts +0 -5
- package/dist/utils/isMobile.js +0 -9
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import Image from 'next/image';
|
|
3
|
+
import { useRef } from 'react';
|
|
4
|
+
import { twMerge } from 'tailwind-merge';
|
|
5
|
+
import { usePxToRemConverter } from '../../../../client/hooks/usePxToRemConverter.js';
|
|
6
|
+
import { useRect } from '../../../../client/hooks/useRect.js';
|
|
7
|
+
import { Camera01Icon } from '../../../../icons/Camera01Icon.js';
|
|
8
|
+
import { CameraOffIcon } from '../../../../icons/CameraOffIcon.js';
|
|
9
|
+
import { CheckCircleIcon } from '../../../../icons/CheckCircleIcon.js';
|
|
10
|
+
import { RefreshCcw01Icon } from '../../../../icons/RefreshCcw01Icon.js';
|
|
11
|
+
import { Spinner02Icon } from '../../../../icons/Spinner02Icon.js';
|
|
12
|
+
import { UploadCloud02Icon } from '../../../../icons/UploadCloud02Icon.js';
|
|
13
|
+
import { XIcon } from '../../../../icons/XIcon.js';
|
|
14
|
+
import { Button } from '../../../../ui/Button/index.js';
|
|
15
|
+
import { Dialog } from '../../../../ui/Dialog/index.js';
|
|
16
|
+
import { Portal } from '../../../../ui/Portal/index.js';
|
|
17
|
+
import { createSplitProps } from '../../../../utils/createSplitProps.js';
|
|
18
|
+
import { validateFile, } from '../../../../utils/validateFile.js';
|
|
19
|
+
import { IdFrontImageFieldProvider, useIdFrontImageFieldContext, } from './IdFrontImageFieldContext.js';
|
|
20
|
+
import { useIdFrontImageField, } from './useIdFrontImageField.js';
|
|
21
|
+
export function IdFrontImageField__client(props) {
|
|
22
|
+
const [useIdFrontImageFieldProps, localProps] = createSplitProps([
|
|
23
|
+
'value',
|
|
24
|
+
'onChange',
|
|
25
|
+
'defaultValue',
|
|
26
|
+
])(props);
|
|
27
|
+
const context = useIdFrontImageField(useIdFrontImageFieldProps);
|
|
28
|
+
return (_jsxs(IdFrontImageFieldProvider, { value: context, children: [_jsxs("div", { ref: localProps.ref, id: localProps.id, style: localProps.style, className: twMerge('border-border-primary bg-bg-primary group relative aspect-[352/180] w-full shrink-0 rounded-xl border p-xl', localProps.className), onDragOver: (e) => e.preventDefault(), onDragEnter: (e) => e.preventDefault(), onDragLeave: (e) => e.preventDefault(), onDrop: (e) => {
|
|
29
|
+
e.preventDefault();
|
|
30
|
+
const file = e.dataTransfer.files?.[0];
|
|
31
|
+
if (!file)
|
|
32
|
+
return;
|
|
33
|
+
const error = validateFile(file);
|
|
34
|
+
if (error)
|
|
35
|
+
return localProps.onError?.(error);
|
|
36
|
+
context.setValue(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) => {
|
|
38
|
+
const file = e.target.files?.[0];
|
|
39
|
+
if (!file)
|
|
40
|
+
return;
|
|
41
|
+
const error = validateFile(file);
|
|
42
|
+
if (error)
|
|
43
|
+
return localProps.onError?.(error);
|
|
44
|
+
context.setValue(file);
|
|
45
|
+
}, disabled: context.field?.disabled || localProps.disabled, readOnly: context.field?.readOnly || localProps.readOnly, required: context.field?.required || localProps.required, className: "sr-only" }), !context.value && (_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: () => {
|
|
46
|
+
context.inputRef.current?.click();
|
|
47
|
+
}, disabled: context.field?.disabled ||
|
|
48
|
+
context.field?.readOnly ||
|
|
49
|
+
localProps.disabled ||
|
|
50
|
+
localProps.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: () => {
|
|
51
|
+
context.disclosure.setOpen(true);
|
|
52
|
+
}, disabled: context.field?.disabled ||
|
|
53
|
+
context.field?.readOnly ||
|
|
54
|
+
localProps.disabled ||
|
|
55
|
+
localProps.readOnly, children: "Take a photo" })] })), context.valueAsUrl && (_jsxs("div", { className: "relative", children: [_jsx(Image, { src: context.valueAsUrl, 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: () => {
|
|
56
|
+
context.setValue(null);
|
|
57
|
+
}, children: _jsx(XIcon, { className: "size-5" }) })] }))] }), _jsx(Camera, {})] }));
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* -------------------------------------
|
|
61
|
+
* Camera
|
|
62
|
+
* -------------------------------------
|
|
63
|
+
*/
|
|
64
|
+
function Camera() {
|
|
65
|
+
const context = useIdFrontImageFieldContext();
|
|
66
|
+
return (_jsx(Dialog.Root, { open: context.disclosure.open, onOpenChange: (details) => {
|
|
67
|
+
context.disclosure.setOpen(details.open);
|
|
68
|
+
}, closeOnEscape: false, closeOnInteractOutside: false, 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: [_jsx(Video, {}), context.camera.error && (_jsxs("div", { className: "border-border-disabled flex aspect-[4/3] flex-col items-center justify-center rounded-md border bg-black px-4 lg:aspect-video", 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: () => {
|
|
69
|
+
context.camera.reopen();
|
|
70
|
+
}, children: "Try again" })] })), context.camera.loading && (_jsx("div", { className: "border-border-disabled flex aspect-[4/3] flex-col items-center justify-center rounded-md border bg-black px-4 lg:aspect-video", children: _jsx(Spinner02Icon, { className: "text-text-placeholder size-8" }) })), context.camera.data && (_jsx("div", { className: "border-border-disabled aspect-[4/3] w-full overflow-hidden rounded-md border bg-black lg:aspect-video", children: _jsx(Image, { src: context.camera.data.url, alt: "", width: 600, height: 378.08, unoptimized: true, className: "mx-auto mt-2 block h-[calc(100%-1rem)] w-auto" }) }))] }), !context.camera.data && (_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 py-lg lg:w-[10rem]", onClick: () => {
|
|
71
|
+
context.camera.snap();
|
|
72
|
+
}, disabled: context.camera.snapping ||
|
|
73
|
+
context.camera.loading ||
|
|
74
|
+
!!context.camera.error, children: [context.camera.snapping ? (_jsx(Spinner02Icon, { className: "size-5" })) : (_jsx(Camera01Icon, { className: "size-5" })), "Capture"] }) })), context.camera.data && (_jsxs("div", { className: "mt-5 flex gap-3 lg:mt-10 lg:justify-center", children: [_jsxs(Button, { onClick: () => {
|
|
75
|
+
const file = context.camera.data?.file;
|
|
76
|
+
if (!file)
|
|
77
|
+
return;
|
|
78
|
+
context.setValue(file);
|
|
79
|
+
context.disclosure.setOpen(false);
|
|
80
|
+
}, className: "w-full lg:w-[10rem]", children: [_jsx(CheckCircleIcon, { className: "size-5" }), "Use Photo"] }), _jsxs(Button, { variant: "outline", colorScheme: "gray", onClick: () => {
|
|
81
|
+
context.camera.reset();
|
|
82
|
+
}, className: "w-full lg:w-[10rem]", children: [_jsx(RefreshCcw01Icon, { className: "size-5" }), " Retake"] })] }))] }) })] }) }));
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* -------------------------------------
|
|
86
|
+
* Video
|
|
87
|
+
* -------------------------------------
|
|
88
|
+
*/
|
|
89
|
+
function Video() {
|
|
90
|
+
const context = useIdFrontImageFieldContext();
|
|
91
|
+
const pxToRem = usePxToRemConverter();
|
|
92
|
+
const rootRef = useRef(null);
|
|
93
|
+
const rootRect = useRect(rootRef);
|
|
94
|
+
const guideRect = useRect(context.guideRef);
|
|
95
|
+
const x = (rootRect.width - guideRect.width) / 2;
|
|
96
|
+
const y = (rootRect.height - guideRect.height) / 2;
|
|
97
|
+
return (_jsxs("div", { ref: rootRef, className: twMerge('relative h-auto w-full overflow-hidden', !context.camera.data && !context.camera.error && !context.camera.loading
|
|
98
|
+
? 'block'
|
|
99
|
+
: 'hidden'), children: [_jsx("video", { ...context.camera.videoProps }), _jsx("div", { className: "pointer-events-none absolute left-0 right-0 top-0 z-20 bg-black/60 backdrop-blur-sm", style: { height: `${pxToRem(y)}rem` } }), _jsx("div", { className: "pointer-events-none absolute bottom-0 left-0 right-0 z-20 bg-black/60 backdrop-blur-sm", style: { height: `${pxToRem(y)}rem` } }), _jsx("div", { className: "pointer-events-none absolute left-0 z-20 bg-black/60 backdrop-blur-sm", style: { top: y, bottom: y, width: `${pxToRem(x)}rem` } }), _jsx("div", { className: "pointer-events-none absolute right-0 z-20 bg-black/60 backdrop-blur-sm", style: { top: y, bottom: y, width: `${pxToRem(x)}rem` } }), _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 border-4 border-dashed transition-all duration-200" })] }));
|
|
100
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import dynamic from 'next/dynamic';
|
|
4
|
+
import { Spinner02Icon } from '../../../../icons/Spinner02Icon.js';
|
|
5
|
+
const Component = dynamic(() => import('./IdFrontImageField.client.js').then((mod) => mod.IdFrontImageField__client), {
|
|
6
|
+
ssr: false,
|
|
7
|
+
loading: () => (_jsx("div", { className: "border-border-primary bg-bg-primary flex aspect-[352/180] w-full shrink-0 items-center justify-center rounded-xl border", children: _jsx(Spinner02Icon, { className: "text-text-quinary size-8" }) })),
|
|
8
|
+
});
|
|
9
|
+
export function IdFrontImageField(props) {
|
|
10
|
+
return _jsx(Component, { ...props });
|
|
11
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { UseIdFrontImageFieldReturn } from './useIdFrontImageField';
|
|
2
|
+
export declare const IdFrontImageFieldProvider: (props: {
|
|
3
|
+
value: UseIdFrontImageFieldReturn;
|
|
4
|
+
} & {
|
|
5
|
+
children?: import("react").ReactNode | undefined;
|
|
6
|
+
}) => React.ReactNode, useIdFrontImageFieldContext: () => UseIdFrontImageFieldReturn;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './IdFrontImageField';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './IdFrontImageField.js';
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface UseIdFrontImageFieldProps {
|
|
2
|
+
value?: File | null;
|
|
3
|
+
onChange?: (value: File | null) => void;
|
|
4
|
+
defaultValue?: File | null;
|
|
5
|
+
}
|
|
6
|
+
export interface UseIdFrontImageFieldReturn extends ReturnType<typeof useIdFrontImageField> {
|
|
7
|
+
}
|
|
8
|
+
export declare function useIdFrontImageField(props: UseIdFrontImageFieldProps): {
|
|
9
|
+
field: import("@ark-ui/react").UseFieldContext;
|
|
10
|
+
value: File | null;
|
|
11
|
+
valueAsUrl: string | null;
|
|
12
|
+
setValue: import("react").Dispatch<import("react").SetStateAction<File | null>>;
|
|
13
|
+
camera: import("../../../../client/hooks/useCamera").UseCameraReturn<never>;
|
|
14
|
+
disclosure: import("../../../../client/hooks/useDisclosure").UseDisclosureReturn;
|
|
15
|
+
inputRef: import("react").RefObject<HTMLInputElement | null>;
|
|
16
|
+
guideRef: import("react").RefObject<HTMLDivElement | null>;
|
|
17
|
+
maskRef: import("react").RefObject<HTMLDivElement | null>;
|
|
18
|
+
};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { useFieldContext } from '@ark-ui/react';
|
|
2
|
+
import { useRef } from 'react';
|
|
3
|
+
import invariant from 'tiny-invariant';
|
|
4
|
+
import { useMediaQuery } from 'usehooks-ts';
|
|
5
|
+
import { useCamera } from '../../../../client/hooks/useCamera.js';
|
|
6
|
+
import { useControllableState } from '../../../../client/hooks/useControllableState.js';
|
|
7
|
+
import { useDisclosure } from '../../../../client/hooks/useDisclosure.js';
|
|
8
|
+
export function useIdFrontImageField(props) {
|
|
9
|
+
const field = useFieldContext();
|
|
10
|
+
const [value, setValue] = useControllableState({
|
|
11
|
+
value: props.value,
|
|
12
|
+
defaultValue: props.defaultValue ?? null,
|
|
13
|
+
onChange: props.onChange,
|
|
14
|
+
});
|
|
15
|
+
const valueAsUrl = value ? URL.createObjectURL(value) : null;
|
|
16
|
+
const inputRef = useRef(null);
|
|
17
|
+
const maskRef = useRef(null);
|
|
18
|
+
const guideRef = useRef(null);
|
|
19
|
+
const desktop = useMediaQuery('(min-width: 1024px)');
|
|
20
|
+
const camera = useCamera({
|
|
21
|
+
video: {
|
|
22
|
+
facingMode: desktop ? 'user' : 'environment',
|
|
23
|
+
},
|
|
24
|
+
async transform(data) {
|
|
25
|
+
const guideRect = guideRef.current?.getBoundingClientRect();
|
|
26
|
+
const videoRect = data.video.getBoundingClientRect();
|
|
27
|
+
invariant(guideRect, "'guideRef.getBoundingClientRect()' fails");
|
|
28
|
+
const scaleX = data.image.width / videoRect.width;
|
|
29
|
+
const scaleY = data.image.height / videoRect.height;
|
|
30
|
+
const cropTop = (guideRect.top - videoRect.top) * scaleY;
|
|
31
|
+
const cropLeft = (guideRect.left - videoRect.left) * scaleX;
|
|
32
|
+
const cropWidth = guideRect.width * scaleX;
|
|
33
|
+
const cropHeight = guideRect.height * scaleY;
|
|
34
|
+
const targetWidth = 1200;
|
|
35
|
+
const targetHeight = 757;
|
|
36
|
+
const canvas = document.createElement('canvas');
|
|
37
|
+
const context = canvas.getContext('2d');
|
|
38
|
+
invariant(context, "'canvas.getContext' failed");
|
|
39
|
+
canvas.width = targetWidth;
|
|
40
|
+
canvas.height = targetHeight;
|
|
41
|
+
context.imageSmoothingEnabled = true;
|
|
42
|
+
context.imageSmoothingQuality = 'high';
|
|
43
|
+
context.filter = 'brightness(1.05) contrast(1.1) saturate(1.1)';
|
|
44
|
+
context.drawImage(data.image, cropLeft, cropTop, cropWidth, cropHeight, 0, 0, targetWidth, targetHeight);
|
|
45
|
+
const file = await new Promise((resolve) => canvas.toBlob((blob) => {
|
|
46
|
+
invariant(blob);
|
|
47
|
+
resolve(new File([blob], `front-image-${Date.now()}.jpeg`, {
|
|
48
|
+
type: 'image/jpeg',
|
|
49
|
+
endings: 'native',
|
|
50
|
+
lastModified: Date.now(),
|
|
51
|
+
}));
|
|
52
|
+
}, 'image/jpeg', 1.0));
|
|
53
|
+
const url = URL.createObjectURL(file);
|
|
54
|
+
const image = new Image();
|
|
55
|
+
image.src = url;
|
|
56
|
+
image.alt = '';
|
|
57
|
+
image.width = targetWidth;
|
|
58
|
+
image.height = targetHeight;
|
|
59
|
+
context.filter = 'brightness(1.05)';
|
|
60
|
+
return {
|
|
61
|
+
ok: true,
|
|
62
|
+
data: {
|
|
63
|
+
url,
|
|
64
|
+
file,
|
|
65
|
+
image,
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
const disclosure = useDisclosure({
|
|
71
|
+
onOpenChange(open) {
|
|
72
|
+
if (open) {
|
|
73
|
+
camera.open();
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
return {
|
|
78
|
+
field,
|
|
79
|
+
value,
|
|
80
|
+
valueAsUrl,
|
|
81
|
+
setValue,
|
|
82
|
+
camera,
|
|
83
|
+
disclosure,
|
|
84
|
+
inputRef,
|
|
85
|
+
guideRef,
|
|
86
|
+
maskRef,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type CSSProperties, type RefObject } from 'react';
|
|
2
|
+
import { type ValidateFileError } from '../../../../utils/validateFile';
|
|
3
|
+
import { type UseSelfieImageFieldProps } from './useSelfieImageField';
|
|
4
|
+
export interface SelfieImageFieldProps extends UseSelfieImageFieldProps {
|
|
5
|
+
ref?: RefObject<HTMLDivElement | null>;
|
|
6
|
+
id?: string;
|
|
7
|
+
style?: CSSProperties;
|
|
8
|
+
className?: string;
|
|
9
|
+
invalid?: boolean;
|
|
10
|
+
readOnly?: boolean;
|
|
11
|
+
disabled?: boolean;
|
|
12
|
+
required?: boolean;
|
|
13
|
+
onError?: (error: ValidateFileError<'FaceNotFoundError'>) => void;
|
|
14
|
+
}
|
|
15
|
+
export declare function SelfieImageField__client(props: SelfieImageFieldProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import Image from 'next/image';
|
|
3
|
+
import { useRef } from 'react';
|
|
4
|
+
import { twMerge } from 'tailwind-merge';
|
|
5
|
+
import { usePxToRemConverter } from '../../../../client/hooks/usePxToRemConverter.js';
|
|
6
|
+
import { useRect } from '../../../../client/hooks/useRect.js';
|
|
7
|
+
import { Camera01Icon } from '../../../../icons/Camera01Icon.js';
|
|
8
|
+
import { CameraOffIcon } from '../../../../icons/CameraOffIcon.js';
|
|
9
|
+
import { CheckCircleIcon } from '../../../../icons/CheckCircleIcon.js';
|
|
10
|
+
import { RefreshCcw01Icon } from '../../../../icons/RefreshCcw01Icon.js';
|
|
11
|
+
import { Spinner02Icon } from '../../../../icons/Spinner02Icon.js';
|
|
12
|
+
import { UploadCloud02Icon } from '../../../../icons/UploadCloud02Icon.js';
|
|
13
|
+
import { XIcon } from '../../../../icons/XIcon.js';
|
|
14
|
+
import { Button } from '../../../../ui/Button/index.js';
|
|
15
|
+
import { Dialog } from '../../../../ui/Dialog/index.js';
|
|
16
|
+
import { Portal } from '../../../../ui/Portal/index.js';
|
|
17
|
+
import { createSplitProps } from '../../../../utils/createSplitProps.js';
|
|
18
|
+
import { validateFile, } from '../../../../utils/validateFile.js';
|
|
19
|
+
import { SelfieImageFieldProvider, useSelfieImageFieldContext, } from './SelfieImageFieldContext.js';
|
|
20
|
+
import { useSelfieImageField, } from './useSelfieImageField.js';
|
|
21
|
+
export function SelfieImageField__client(props) {
|
|
22
|
+
const [useSelfieImageFieldProps, localProps] = createSplitProps([
|
|
23
|
+
'value',
|
|
24
|
+
'onChange',
|
|
25
|
+
'defaultValue',
|
|
26
|
+
])(props);
|
|
27
|
+
const context = useSelfieImageField(useSelfieImageFieldProps);
|
|
28
|
+
return (_jsxs(SelfieImageFieldProvider, { value: context, children: [_jsxs("div", { ref: localProps.ref, id: localProps.id, style: localProps.style, className: twMerge('group', 'border', 'border-border-primary', 'bg-bg-primary', 'relative', 'w-full', 'aspect-[352/180]', 'shrink-0', 'rounded-xl', 'p-xl', localProps.className), onDragOver: (e) => e.preventDefault(), onDragEnter: (e) => e.preventDefault(), onDragLeave: (e) => e.preventDefault(), onDrop: async (e) => {
|
|
29
|
+
e.preventDefault();
|
|
30
|
+
const file = e.dataTransfer.files?.[0];
|
|
31
|
+
if (!file)
|
|
32
|
+
return;
|
|
33
|
+
const error = validateFile(file);
|
|
34
|
+
if (error)
|
|
35
|
+
return localProps.onError?.(error);
|
|
36
|
+
context.setValue(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: async (e) => {
|
|
38
|
+
const file = e.target.files?.[0];
|
|
39
|
+
if (!file)
|
|
40
|
+
return;
|
|
41
|
+
const error = validateFile(file);
|
|
42
|
+
if (error)
|
|
43
|
+
return localProps.onError?.(error);
|
|
44
|
+
context.setValue(file);
|
|
45
|
+
}, disabled: context.field?.disabled || localProps.disabled, readOnly: context.field?.readOnly || localProps.readOnly, required: context.field?.required || localProps.required, className: "sr-only" }), !context.value && (_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: () => {
|
|
46
|
+
context.inputRef.current?.click();
|
|
47
|
+
}, disabled: context.field?.disabled ||
|
|
48
|
+
context.field?.readOnly ||
|
|
49
|
+
localProps.disabled ||
|
|
50
|
+
localProps.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: () => {
|
|
51
|
+
context.disclosure.setOpen(true);
|
|
52
|
+
}, disabled: context.field?.disabled ||
|
|
53
|
+
context.field?.readOnly ||
|
|
54
|
+
localProps.disabled ||
|
|
55
|
+
localProps.readOnly, children: "Take a photo" })] })), context.valueAsUrl && (_jsxs("div", { className: "relative", children: [_jsx(Image, { src: context.valueAsUrl, 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: () => {
|
|
56
|
+
context.setValue(null);
|
|
57
|
+
}, children: _jsx(XIcon, { className: "size-5" }) })] }))] }), _jsx(Camera, {})] }));
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* -------------------------------------
|
|
61
|
+
* Camera
|
|
62
|
+
* -------------------------------------
|
|
63
|
+
*/
|
|
64
|
+
function Camera() {
|
|
65
|
+
const context = useSelfieImageFieldContext();
|
|
66
|
+
return (_jsx(Dialog.Root, { open: context.disclosure.open, onOpenChange: (details) => {
|
|
67
|
+
context.disclosure.setOpen(details.open);
|
|
68
|
+
}, closeOnEscape: false, closeOnInteractOutside: false, 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: [_jsx(Video, {}), context.camera.error && (_jsxs("div", { className: "border-border-disabled flex aspect-[4/3] flex-col items-center justify-center rounded-md border bg-black px-4 lg:aspect-video", 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: () => {
|
|
69
|
+
context.camera.reopen();
|
|
70
|
+
}, children: "Try again" })] })), context.camera.loading && (_jsx("div", { className: "border-border-disabled flex aspect-[4/3] flex-col items-center justify-center rounded-md border bg-black px-4 lg:aspect-video", children: _jsx(Spinner02Icon, { className: "text-text-placeholder size-8" }) })), context.camera.data && (_jsx("div", { className: "border-border-disabled aspect-[4/3] w-full overflow-hidden rounded-md border bg-black lg:aspect-video", children: _jsx(Image, { src: context.camera.data.url, alt: "", width: 600, height: 600, unoptimized: true, className: "mx-auto mt-2 block h-[calc(100%-1rem)] w-auto" }) }))] }), !context.camera.data && (_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 py-lg lg:w-[10rem]", onClick: () => {
|
|
71
|
+
context.camera.snap();
|
|
72
|
+
}, disabled: context.camera.loading ||
|
|
73
|
+
context.camera.snapping ||
|
|
74
|
+
!!context.camera.error ||
|
|
75
|
+
!context.faceFound, children: [context.camera.snapping ? (_jsx(Spinner02Icon, { className: "size-5" })) : (_jsx(Camera01Icon, { className: "size-5" })), "Capture"] }) })), context.camera.data && (_jsxs("div", { className: "mt-5 flex gap-3 lg:mt-10 lg:justify-center", children: [_jsxs(Button, { onClick: () => {
|
|
76
|
+
const file = context.camera.data?.file;
|
|
77
|
+
if (!file)
|
|
78
|
+
return;
|
|
79
|
+
context.setValue(file);
|
|
80
|
+
context.disclosure.setOpen(false);
|
|
81
|
+
}, className: "w-full lg:w-[10rem]", children: [_jsx(CheckCircleIcon, { className: "size-5" }), "Use Photo"] }), _jsxs(Button, { variant: "outline", colorScheme: "gray", onClick: () => {
|
|
82
|
+
context.camera.reset();
|
|
83
|
+
}, className: "w-full lg:w-[10rem]", children: [_jsx(RefreshCcw01Icon, { className: "size-5" }), " Retake"] })] }))] }) })] }) }));
|
|
84
|
+
}
|
|
85
|
+
/*
|
|
86
|
+
* -------------------------------------
|
|
87
|
+
* Video
|
|
88
|
+
* -------------------------------------
|
|
89
|
+
*/
|
|
90
|
+
function Video() {
|
|
91
|
+
const context = useSelfieImageFieldContext();
|
|
92
|
+
const pxToRem = usePxToRemConverter();
|
|
93
|
+
const videoRect = useRect(context.camera.videoRef);
|
|
94
|
+
const guideWidth = videoRect.width <= 0 || videoRect.height <= 0
|
|
95
|
+
? 0
|
|
96
|
+
: videoRect.width > videoRect.height
|
|
97
|
+
? videoRect.height
|
|
98
|
+
: videoRect.width;
|
|
99
|
+
const rootRef = useRef(null);
|
|
100
|
+
const rootRect = useRect(rootRef, {
|
|
101
|
+
enabled: guideWidth > 0,
|
|
102
|
+
});
|
|
103
|
+
const guideRect = useRect(context.guideRef, {
|
|
104
|
+
enabled: guideWidth > 0,
|
|
105
|
+
});
|
|
106
|
+
const x = (rootRect.width - guideRect.width) / 2;
|
|
107
|
+
const y = (rootRect.height - guideRect.height) / 2;
|
|
108
|
+
return (_jsxs("div", { ref: rootRef, className: twMerge('relative h-auto w-full overflow-hidden', !context.camera.data && !context.camera.error && !context.camera.loading
|
|
109
|
+
? 'block'
|
|
110
|
+
: 'hidden'), children: [_jsx("div", { className: "pointer-events-none absolute left-0 right-0 top-0 z-20 bg-black/60 backdrop-blur-sm", style: { height: `${pxToRem(y)}rem` } }), _jsx("div", { className: "pointer-events-none absolute bottom-0 left-0 right-0 z-20 bg-black/60 backdrop-blur-sm", style: { height: `${pxToRem(y)}rem` } }), _jsx("div", { className: "pointer-events-none absolute left-0 z-20 bg-black/60 backdrop-blur-sm", style: { top: y, bottom: y, width: `${pxToRem(x)}rem` } }), _jsx("div", { className: "pointer-events-none absolute right-0 z-20 bg-black/60 backdrop-blur-sm", style: { top: y, bottom: y, width: `${pxToRem(x)}rem` } }), _jsx("video", { ...context.camera.videoProps }), _jsx("div", { ref: context.guideRef, className: twMerge('border-utility-warning-700 absolute left-1/2 top-1/2 z-20 aspect-square -translate-x-1/2 -translate-y-1/2 border-4 border-dashed', context.faceFound && 'border-utility-success-700', guideWidth <= 0 ? 'hidden' : 'block'), style: {
|
|
111
|
+
width: `calc(${pxToRem(guideWidth)}rem - 2rem)`,
|
|
112
|
+
} })] }));
|
|
113
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import dynamic from 'next/dynamic';
|
|
4
|
+
import { Spinner02Icon } from '../../../../icons/Spinner02Icon.js';
|
|
5
|
+
const Component = dynamic(() => import('./SelfieImageField.client.js').then((mod) => mod.SelfieImageField__client), {
|
|
6
|
+
ssr: false,
|
|
7
|
+
loading: () => (_jsx("div", { className: "border-border-primary bg-bg-primary flex aspect-[352/180] w-full shrink-0 items-center justify-center rounded-xl border", children: _jsx(Spinner02Icon, { className: "text-text-quinary size-8" }) })),
|
|
8
|
+
});
|
|
9
|
+
export function SelfieImageField(props) {
|
|
10
|
+
return _jsx(Component, { ...props });
|
|
11
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { UseSelfieImageFieldReturn } from './useSelfieImageField';
|
|
2
|
+
export declare const SelfieImageFieldProvider: (props: {
|
|
3
|
+
value: UseSelfieImageFieldReturn;
|
|
4
|
+
} & {
|
|
5
|
+
children?: import("react").ReactNode | undefined;
|
|
6
|
+
}) => React.ReactNode, useSelfieImageFieldContext: () => UseSelfieImageFieldReturn;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './SelfieImageField';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './SelfieImageField.js';
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface UseSelfieImageFieldProps {
|
|
2
|
+
value?: File | null;
|
|
3
|
+
onChange?: (value: File | null) => void;
|
|
4
|
+
defaultValue?: File | null;
|
|
5
|
+
}
|
|
6
|
+
export interface UseSelfieImageFieldReturn extends ReturnType<typeof useSelfieImageField> {
|
|
7
|
+
}
|
|
8
|
+
export declare function useSelfieImageField(props: UseSelfieImageFieldProps): {
|
|
9
|
+
field: import("@ark-ui/react").UseFieldContext;
|
|
10
|
+
value: File | null;
|
|
11
|
+
valueAsUrl: string | null;
|
|
12
|
+
setValue: import("react").Dispatch<import("react").SetStateAction<File | null>>;
|
|
13
|
+
camera: import("../../../../client/hooks/useCamera").UseCameraReturn<"FaceNotFoundError">;
|
|
14
|
+
disclosure: import("../../../../client/hooks/useDisclosure").UseDisclosureReturn;
|
|
15
|
+
inputRef: import("react").RefObject<HTMLInputElement | null>;
|
|
16
|
+
guideRef: import("react").RefObject<HTMLDivElement | null>;
|
|
17
|
+
maskRef: import("react").RefObject<HTMLDivElement | null>;
|
|
18
|
+
faceFound: boolean;
|
|
19
|
+
};
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { useFieldContext } from '@ark-ui/react';
|
|
2
|
+
import { FaceDetector, FilesetResolver } from '@mediapipe/tasks-vision';
|
|
3
|
+
import { useRef, useState } from 'react';
|
|
4
|
+
import invariant from 'tiny-invariant';
|
|
5
|
+
import { useInterval } from 'usehooks-ts';
|
|
6
|
+
import { useCamera } from '../../../../client/hooks/useCamera.js';
|
|
7
|
+
import { useControllableState } from '../../../../client/hooks/useControllableState.js';
|
|
8
|
+
import { useDisclosure } from '../../../../client/hooks/useDisclosure.js';
|
|
9
|
+
export function useSelfieImageField(props) {
|
|
10
|
+
const field = useFieldContext();
|
|
11
|
+
const [value, setValue] = useControllableState({
|
|
12
|
+
value: props.value,
|
|
13
|
+
defaultValue: props.defaultValue ?? null,
|
|
14
|
+
onChange: props.onChange,
|
|
15
|
+
});
|
|
16
|
+
const valueAsUrl = value ? URL.createObjectURL(value) : null;
|
|
17
|
+
const inputRef = useRef(null);
|
|
18
|
+
const maskRef = useRef(null);
|
|
19
|
+
const guideRef = useRef(null);
|
|
20
|
+
const [validatingFace, setValidatingFace] = useState(false);
|
|
21
|
+
const camera = useCamera({
|
|
22
|
+
async transform(data) {
|
|
23
|
+
if (validatingFace)
|
|
24
|
+
return { ok: true, data };
|
|
25
|
+
setValidatingFace(true);
|
|
26
|
+
const hasFace = await validateFaceFromImage(data.image);
|
|
27
|
+
setValidatingFace(false);
|
|
28
|
+
if (!hasFace) {
|
|
29
|
+
return {
|
|
30
|
+
ok: false,
|
|
31
|
+
error: {
|
|
32
|
+
name: 'FaceNotFoundError',
|
|
33
|
+
message: 'No face detected in the image. Please try again.',
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
const guideRect = guideRef.current?.getBoundingClientRect();
|
|
38
|
+
const videoRect = data.video.getBoundingClientRect();
|
|
39
|
+
invariant(guideRect, "'guideRef.getBoundingClientRect()' fails");
|
|
40
|
+
const scaleX = data.image.width / videoRect.width;
|
|
41
|
+
const scaleY = data.image.height / videoRect.height;
|
|
42
|
+
const cropTop = (guideRect.top - videoRect.top) * scaleY;
|
|
43
|
+
const cropLeft = (guideRect.left - videoRect.left) * scaleX;
|
|
44
|
+
const cropWidth = guideRect.width * scaleX;
|
|
45
|
+
const cropHeight = guideRect.height * scaleY;
|
|
46
|
+
const targetWidth = 1200;
|
|
47
|
+
const targetHeight = 1200;
|
|
48
|
+
const canvas = document.createElement('canvas');
|
|
49
|
+
const context = canvas.getContext('2d');
|
|
50
|
+
invariant(context, "'canvas.getContext' failed");
|
|
51
|
+
canvas.width = targetWidth;
|
|
52
|
+
canvas.height = targetHeight;
|
|
53
|
+
context.imageSmoothingEnabled = true;
|
|
54
|
+
context.imageSmoothingQuality = 'high';
|
|
55
|
+
context.filter = 'brightness(1.05)';
|
|
56
|
+
context.drawImage(data.image, cropLeft, cropTop, cropWidth, cropHeight, 0, 0, targetWidth, targetHeight);
|
|
57
|
+
const file = await new Promise((resolve) => canvas.toBlob((blob) => {
|
|
58
|
+
invariant(blob);
|
|
59
|
+
resolve(new File([blob], `front-image-${Date.now()}.jpeg`, {
|
|
60
|
+
type: 'image/jpeg',
|
|
61
|
+
endings: 'native',
|
|
62
|
+
lastModified: Date.now(),
|
|
63
|
+
}));
|
|
64
|
+
}, 'image/jpeg', 1.0));
|
|
65
|
+
const url = URL.createObjectURL(file);
|
|
66
|
+
const image = new Image();
|
|
67
|
+
image.src = url;
|
|
68
|
+
image.alt = '';
|
|
69
|
+
image.width = targetWidth;
|
|
70
|
+
image.height = targetHeight;
|
|
71
|
+
return {
|
|
72
|
+
ok: true,
|
|
73
|
+
data: {
|
|
74
|
+
url,
|
|
75
|
+
file,
|
|
76
|
+
image,
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
const disclosure = useDisclosure({
|
|
82
|
+
onOpenChange(open) {
|
|
83
|
+
if (open) {
|
|
84
|
+
camera.open();
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
const [faceFound, setFaceFound] = useState(false);
|
|
89
|
+
useInterval(async () => {
|
|
90
|
+
if (!camera.videoRef.current || !guideRef.current) {
|
|
91
|
+
return setFaceFound(false);
|
|
92
|
+
}
|
|
93
|
+
setFaceFound(await validateFaceFromVideo(camera.videoRef.current, guideRef.current));
|
|
94
|
+
}, disclosure.open && !camera.data && !camera.error && !camera.loading
|
|
95
|
+
? 500
|
|
96
|
+
: null);
|
|
97
|
+
return {
|
|
98
|
+
field,
|
|
99
|
+
value,
|
|
100
|
+
valueAsUrl,
|
|
101
|
+
setValue,
|
|
102
|
+
camera,
|
|
103
|
+
disclosure,
|
|
104
|
+
inputRef,
|
|
105
|
+
guideRef,
|
|
106
|
+
maskRef,
|
|
107
|
+
faceFound,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
let __image_face_detector__;
|
|
111
|
+
let __video_face_detector__;
|
|
112
|
+
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
|
113
|
+
let __vision__;
|
|
114
|
+
async function getImageFaceDetector() {
|
|
115
|
+
if (!__image_face_detector__) {
|
|
116
|
+
if (!__vision__) {
|
|
117
|
+
__vision__ = await FilesetResolver.forVisionTasks('https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@0.10.0/wasm');
|
|
118
|
+
}
|
|
119
|
+
const detector = await FaceDetector.createFromOptions(__vision__, {
|
|
120
|
+
runningMode: 'IMAGE',
|
|
121
|
+
minDetectionConfidence: 0.8,
|
|
122
|
+
baseOptions: {
|
|
123
|
+
delegate: 'GPU',
|
|
124
|
+
modelAssetPath: 'https://storage.googleapis.com/mediapipe-models/face_detector/blaze_face_short_range/float16/1/blaze_face_short_range.tflite',
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
__image_face_detector__ = detector;
|
|
128
|
+
return detector;
|
|
129
|
+
}
|
|
130
|
+
return __image_face_detector__;
|
|
131
|
+
}
|
|
132
|
+
async function getVideoFaceDetector() {
|
|
133
|
+
if (!__video_face_detector__) {
|
|
134
|
+
if (!__vision__) {
|
|
135
|
+
__vision__ = await FilesetResolver.forVisionTasks('https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@0.10.0/wasm');
|
|
136
|
+
}
|
|
137
|
+
const detector = await FaceDetector.createFromOptions(__vision__, {
|
|
138
|
+
runningMode: 'VIDEO',
|
|
139
|
+
minDetectionConfidence: 0.8,
|
|
140
|
+
baseOptions: {
|
|
141
|
+
delegate: 'GPU',
|
|
142
|
+
modelAssetPath: 'https://storage.googleapis.com/mediapipe-models/face_detector/blaze_face_short_range/float16/1/blaze_face_short_range.tflite',
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
__video_face_detector__ = detector;
|
|
146
|
+
return detector;
|
|
147
|
+
}
|
|
148
|
+
return __video_face_detector__;
|
|
149
|
+
}
|
|
150
|
+
async function validateFaceFromImage(image) {
|
|
151
|
+
const detector = await getImageFaceDetector();
|
|
152
|
+
try {
|
|
153
|
+
const result = detector.detect(image);
|
|
154
|
+
return result.detections.length > 0;
|
|
155
|
+
}
|
|
156
|
+
catch (e) {
|
|
157
|
+
console.error(e);
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
async function validateFaceFromVideo(video, guide) {
|
|
162
|
+
if (video.readyState < 2)
|
|
163
|
+
return false;
|
|
164
|
+
const detector = await getVideoFaceDetector();
|
|
165
|
+
const result = detector.detectForVideo(video, performance.now());
|
|
166
|
+
const detection = result.detections.at(0);
|
|
167
|
+
if (!detection)
|
|
168
|
+
return false;
|
|
169
|
+
const videoRect = video.getBoundingClientRect();
|
|
170
|
+
const guideRect = guide.getBoundingClientRect();
|
|
171
|
+
if (!detection.boundingBox)
|
|
172
|
+
return false;
|
|
173
|
+
const faceLeft = videoRect.left +
|
|
174
|
+
(detection.boundingBox.originX / video.videoWidth) * videoRect.width;
|
|
175
|
+
const faceTop = videoRect.top +
|
|
176
|
+
(detection.boundingBox.originY / video.videoHeight) * videoRect.height;
|
|
177
|
+
const faceRight = faceLeft +
|
|
178
|
+
(detection.boundingBox.width / video.videoWidth) * videoRect.width;
|
|
179
|
+
const faceBottom = faceTop +
|
|
180
|
+
(detection.boundingBox.height / video.videoHeight) * videoRect.height;
|
|
181
|
+
const isInside = faceLeft >= guideRect.left &&
|
|
182
|
+
faceTop >= guideRect.top &&
|
|
183
|
+
faceRight <= guideRect.right &&
|
|
184
|
+
faceBottom <= guideRect.bottom;
|
|
185
|
+
return isInside;
|
|
186
|
+
}
|