B2B-88: add starter kit structure and elements

This commit is contained in:
devmc-ee
2025-06-08 16:18:30 +03:00
parent 657a36a298
commit e7b25600cb
1280 changed files with 77893 additions and 5688 deletions

View File

@@ -0,0 +1,39 @@
'use client';
import { createContext, useCallback, useRef, useState } from 'react';
import { TurnstileInstance } from '@marsidev/react-turnstile';
export const Captcha = createContext<{
token: string;
setToken: (token: string) => void;
instance: TurnstileInstance | null;
setInstance: (ref: TurnstileInstance) => void;
}>({
token: '',
instance: null,
setToken: (_: string) => {
// do nothing
return '';
},
setInstance: () => {
// do nothing
},
});
export function CaptchaProvider(props: { children: React.ReactNode }) {
const [token, setToken] = useState<string>('');
const instanceRef = useRef<TurnstileInstance | null>(null);
const setInstance = useCallback((ref: TurnstileInstance) => {
instanceRef.current = ref;
}, []);
return (
<Captcha.Provider
value={{ token, setToken, instance: instanceRef.current, setInstance }}
>
{props.children}
</Captcha.Provider>
);
}

View File

@@ -0,0 +1,37 @@
'use client';
import { useContext } from 'react';
import { Turnstile, TurnstileProps } from '@marsidev/react-turnstile';
import { Captcha } from './captcha-provider';
export function CaptchaTokenSetter(props: {
siteKey: string | undefined;
options?: TurnstileProps;
}) {
const { setToken, setInstance } = useContext(Captcha);
if (!props.siteKey) {
return null;
}
const options = props.options ?? {
options: {
size: 'invisible',
},
};
return (
<Turnstile
ref={(instance) => {
if (instance) {
setInstance(instance);
}
}}
siteKey={props.siteKey}
onSuccess={setToken}
{...options}
/>
);
}

View File

@@ -0,0 +1,3 @@
export * from './captcha-token-setter';
export * from './use-captcha-token';
export * from './captcha-provider';

View File

@@ -0,0 +1,23 @@
import { useContext, useMemo } from 'react';
import { Captcha } from './captcha-provider';
/**
* @name useCaptchaToken
* @description A hook to get the captcha token and reset function
* @returns The captcha token and reset function
*/
export function useCaptchaToken() {
const context = useContext(Captcha);
if (!context) {
throw new Error(`useCaptchaToken must be used within a CaptchaProvider`);
}
return useMemo(() => {
return {
captchaToken: context.token,
resetCaptchaToken: () => context.instance?.reset(),
};
}, [context]);
}

View File

@@ -0,0 +1 @@
export * from './verify-captcha';

View File

@@ -0,0 +1,39 @@
import 'server-only';
const verifyEndpoint =
'https://challenges.cloudflare.com/turnstile/v0/siteverify';
const CAPTCHA_SECRET_TOKEN = process.env.CAPTCHA_SECRET_TOKEN;
/**
* @name verifyCaptchaToken
* @description Verify the CAPTCHA token with the CAPTCHA service
* @param token - The CAPTCHA token to verify
*/
export async function verifyCaptchaToken(token: string) {
if (!CAPTCHA_SECRET_TOKEN) {
throw new Error('CAPTCHA_SECRET_TOKEN is not set');
}
const formData = new FormData();
formData.append('secret', CAPTCHA_SECRET_TOKEN);
formData.append('response', token);
const res = await fetch(verifyEndpoint, {
method: 'POST',
body: formData,
});
if (!res.ok) {
console.error(`Captcha failed:`, res.statusText);
throw new Error('Failed to verify CAPTCHA token');
}
const data = await res.json();
if (!data.success) {
throw new Error('Invalid CAPTCHA token');
}
}