@baanihali/captcha
Version:
A customizable sliding puzzle captcha component for React applications with server-side validation
263 lines (258 loc) • 12.9 kB
JavaScript
// src/client/index.tsx
import { useState } from "react";
// #style-inject:#style-inject
function styleInject(css, { insertAt } = {}) {
if (!css || typeof document === "undefined") return;
const head = document.head || document.getElementsByTagName("head")[0];
const style = document.createElement("style");
style.type = "text/css";
if (insertAt === "top") {
if (head.firstChild) {
head.insertBefore(style, head.firstChild);
} else {
head.appendChild(style);
}
} else {
head.appendChild(style);
}
if (style.styleSheet) {
style.styleSheet.cssText = css;
} else {
style.appendChild(document.createTextNode(css));
}
}
// src/client/index.css
styleInject(":root {\n --primary-blue: #3b82f6;\n --primary-blue-dark: #2563eb;\n --primary-blue-darker: #1d4ed8;\n --gray-50: #f8fafc;\n --gray-100: #f1f5f9;\n --gray-200: #e2e8f0;\n --gray-300: #cbd5e1;\n --gray-400: #94a3b8;\n --gray-500: #64748b;\n --gray-600: #303946;\n --red-500: #ef4444;\n --white: #ffffff;\n --shadow-sm: 0 2px 4px rgba(0, 0, 0, 0.1);\n --shadow-md: 0 4px 8px rgba(0, 0, 0, 0.1);\n --shadow-lg: 0 10px 25px rgba(0, 0, 0, 0.15);\n --shadow-xl: 0 4px 12px rgba(0, 0, 0, 0.15);\n --blue-shadow: 0 2px 8px rgba(59, 130, 246, 0.3);\n --blue-shadow-hover: 0 4px 12px rgba(59, 130, 246, 0.4);\n}\n.custom-captcha-container {\n position: absolute;\n z-index: 10;\n border: 1px solid var(--gray-200);\n border-radius: 12px;\n box-shadow: var(--shadow-lg);\n background:\n linear-gradient(\n 135deg,\n var(--gray-50) 0%,\n var(--gray-200) 100%);\n width: 320px;\n display: flex;\n flex-direction: column;\n justify-content: space-between;\n overflow: hidden;\n backdrop-filter: blur(10px);\n transition: all 0.3s ease;\n bottom: 40px;\n}\n.custom-captcha-hidden {\n display: none;\n}\n.spinner-container,\n.custom-captcha-container > .spinner-container {\n position: absolute;\n inset: 0;\n display: flex;\n justify-content: center;\n align-items: center;\n background-color: rgba(255, 255, 255, 0.8);\n backdrop-filter: blur(5px);\n}\n.custom-spinner {\n border-radius: 50%;\n width: 32px;\n height: 32px;\n border: 3px solid var(--primary-blue);\n border-top-color: var(--gray-300);\n animation: spin 1s linear infinite;\n}\n@keyframes spin {\n to {\n transform: rotate(360deg);\n }\n}\n.custom-button {\n border-radius: 8px;\n width: 36px;\n height: 36px;\n border: 1px solid var(--gray-300);\n background:\n linear-gradient(\n 135deg,\n var(--white) 0%,\n var(--gray-100) 100%);\n display: flex;\n justify-content: center;\n align-items: center;\n cursor: pointer;\n transition: all 0.2s ease;\n color: var(--gray-600);\n}\n.custom-button:hover {\n transform: translateY(-1px);\n box-shadow: var(--shadow-xl);\n border-color: var(--gray-400);\n color: var(--gray-600);\n}\n.custom-button:active {\n transform: translateY(0);\n box-shadow: var(--shadow-sm);\n}\n.custom-button:disabled {\n opacity: 0.5;\n cursor: not-allowed;\n transform: none;\n}\n.custom-button:disabled:hover {\n transform: none;\n box-shadow: none;\n}\n.custom-button.verify-button {\n background:\n linear-gradient(\n 135deg,\n var(--primary-blue) 0%,\n var(--primary-blue-dark) 100%);\n color: var(--white);\n border-color: var(--primary-blue-dark);\n}\n.custom-button.verify-button:hover {\n background:\n linear-gradient(\n 135deg,\n var(--primary-blue-dark) 0%,\n var(--primary-blue-darker) 100%);\n border-color: var(--primary-blue-darker);\n}\n.captcha-slider {\n width: 100%;\n height: 6px;\n border-radius: 3px;\n background:\n linear-gradient(\n 90deg,\n var(--gray-200) 0%,\n var(--gray-300) 100%);\n outline: none;\n appearance: none;\n cursor: pointer;\n transition: all 0.2s ease;\n}\n.captcha-slider::-webkit-slider-thumb,\n.captcha-slider::-moz-range-thumb {\n appearance: none;\n width: 20px;\n height: 20px;\n border-radius: 50%;\n background:\n linear-gradient(\n 135deg,\n var(--primary-blue) 0%,\n var(--primary-blue-dark) 100%);\n cursor: pointer;\n border: 2px solid var(--white);\n box-shadow: var(--blue-shadow);\n transition: all 0.2s ease;\n}\n.captcha-slider::-webkit-slider-thumb:hover,\n.captcha-slider::-moz-range-thumb:hover {\n transform: scale(1.1);\n box-shadow: var(--blue-shadow-hover);\n}\n.puzzle-container {\n position: relative;\n margin-bottom: 8px;\n border-radius: 8px;\n overflow: hidden;\n box-shadow: var(--shadow-md);\n}\n.puzzle-background {\n width: 100%;\n height: 200px;\n opacity: 0.9;\n display: block;\n transition: opacity 0.2s ease;\n}\n.puzzle-piece {\n width: 50px;\n height: 50px;\n border: 2px solid var(--primary-blue);\n position: absolute;\n top: 74px;\n left: 0;\n transition: transform 0.1s ease;\n box-shadow: var(--blue-shadow);\n}\n.captcha-controls {\n padding: 12px;\n}\n.captcha-hint {\n font-size: 14px;\n color: var(--gray-500);\n margin-left: 6px;\n margin-bottom: 6px;\n font-weight: 500;\n}\n.captcha-hint.error {\n color: var(--red-500);\n}\n.captcha-buttons {\n display: flex;\n align-items: center;\n justify-content: flex-end;\n gap: 8px;\n margin-top: 8px;\n}\n");
// src/client/spinner.tsx
import { jsx } from "react/jsx-runtime";
var Spinner = () => {
return /* @__PURE__ */ jsx("div", { className: "spinner-container", children: /* @__PURE__ */ jsx("div", { className: "custom-spinner" }) });
};
// src/client/index.tsx
import { jsx as jsx2, jsxs } from "react/jsx-runtime";
var CaptchaComponent = ({
loading,
refreshCaptcha,
verifyCaptcha
}) => {
const [correct, setCorrect] = useState(false);
const [captcha, setCaptcha] = useState(null);
const [sliderValue, setSliderValue] = useState(0);
const [captchaOpen, setCaptchaOpen] = useState(false);
const [error, setError] = useState(null);
const _refreshCaptcha = async () => {
setSliderValue(0);
setError(null);
setCorrect(false);
const refreshed = await refreshCaptcha();
if (!refreshed || "error" in refreshed) {
return setError(refreshed?.error || "Oops! Could not load captcha.");
}
;
setCaptcha(refreshed);
};
const verify = async () => {
if (!captcha?.id) return;
const res = await verifyCaptcha(captcha.id, sliderValue.toString());
if (res?.error) {
setCorrect(false);
setError(res.error);
return;
}
;
if (res?.success) {
setCorrect(true);
setCaptchaOpen(false);
return;
}
;
};
const handleCheckChange = (event) => {
setCaptchaOpen(!!event.target.checked);
if (!captcha?.id && !loading) {
_refreshCaptcha();
}
};
return /* @__PURE__ */ jsxs("div", { className: "relative", children: [
/* @__PURE__ */ jsxs("div", { className: "flex space-x-2 items-center", children: [
/* @__PURE__ */ jsx2(
"input",
{
type: "checkbox",
name: "custom-captcha-w3s435q",
id: "custom-captcha-w3s435q",
checked: correct,
required: true,
onChange: handleCheckChange
}
),
/* @__PURE__ */ jsx2("label", { htmlFor: "custom-captcha-w3s435q", className: "text-sm font-medium", children: correct ? "Verified!" : "Verify you're human" })
] }),
/* @__PURE__ */ jsxs(
"div",
{
className: captchaOpen ? "custom-captcha-container" : "custom-captcha-hidden",
children: [
captcha && /* @__PURE__ */ jsxs("div", { children: [
/* @__PURE__ */ jsxs("div", { className: "puzzle-container", children: [
/* @__PURE__ */ jsx2(
"img",
{
src: captcha.background,
width: 300,
height: 200,
alt: "Background",
className: "puzzle-background"
}
),
/* @__PURE__ */ jsx2(
"img",
{
src: captcha.puzzle,
height: 50,
width: 50,
alt: "Puzzle Piece",
className: "puzzle-piece",
style: {
transform: `translateX(${sliderValue}px)`
}
}
)
] }),
/* @__PURE__ */ jsx2("div", { className: "captcha-controls", children: /* @__PURE__ */ jsx2(
"input",
{
type: "range",
min: "0",
max: "250",
value: sliderValue,
onChange: (e) => setSliderValue(Number(e.target.value)),
className: "captcha-slider"
}
) })
] }),
loading && /* @__PURE__ */ jsx2(Spinner, {}),
/* @__PURE__ */ jsx2("div", { className: `captcha-hint ${error ? "error" : ""}`, children: error ? error : "Slide the puzzle piece to the correct position" }),
/* @__PURE__ */ jsxs("div", { className: "captcha-buttons", children: [
/* @__PURE__ */ jsx2(
"button",
{
onClick: _refreshCaptcha,
disabled: loading,
title: "Refresh",
className: "custom-button",
type: "button",
children: /* @__PURE__ */ jsxs(
"svg",
{
xmlns: "http://www.w3.org/2000/svg",
width: "18",
height: "18",
viewBox: "0 0 24 24",
fill: "none",
stroke: "currentColor",
strokeWidth: "2",
strokeLinecap: "round",
strokeLinejoin: "round",
children: [
/* @__PURE__ */ jsx2("path", { d: "M21 12a9 9 0 0 0-9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" }),
/* @__PURE__ */ jsx2("path", { d: "M3 3v5h5" }),
/* @__PURE__ */ jsx2("path", { d: "M3 12a9 9 0 0 0 9 9 9.75 9.75 0 0 0 6.74-2.74L21 16" }),
/* @__PURE__ */ jsx2("path", { d: "M16 16h5v5" })
]
}
)
}
),
/* @__PURE__ */ jsx2(
"button",
{
onClick: verify,
disabled: loading || !captcha,
title: "Verify",
className: "custom-button verify-button",
type: "button",
children: /* @__PURE__ */ jsx2(
"svg",
{
xmlns: "http://www.w3.org/2000/svg",
width: "18",
height: "18",
viewBox: "0 0 24 24",
fill: "none",
stroke: "currentColor",
strokeWidth: "2",
strokeLinecap: "round",
strokeLinejoin: "round",
children: /* @__PURE__ */ jsx2("path", { d: "M20 6 9 17l-5-5" })
}
)
}
)
] })
]
}
)
] });
};
var client_default = CaptchaComponent;
export {
client_default as CaptchaComponent
};
/**
* A customizable sliding puzzle captcha component for React applications.
*
* This component provides a user-friendly captcha verification system where users
* slide a puzzle piece to the correct position on a background image.
*
* @component
* @example
* ```tsx
* import CaptchaComponent from 'custom-captcha/client';
*
* function MyForm() {
* const [loading, setLoading] = useState(false);
*
* const handleRefresh = async () => {
* // Your implementation to fetch new captcha data
* return {
* background: 'data:image/jpeg;base64,...' or 'https://....',
* puzzle: 'data:image/jpeg;base64,...' or 'https://....',
* id: 'unique-captcha-id'
* };
* };
*
* const handleVerify = async (id: string, value: string) => {
* // Your implementation to verify the captcha
* return { success: true };
* };
*
* return (
* <CaptchaComponent
* loading={loading}
* refreshCaptcha={handleRefresh}
* verifyCaptcha={handleVerify}
* />
* );
* }
* ```
*
* @param props - The component props
* @returns JSX element representing the captcha component
*
* @version 1.0.0
* @author Murtaza Baanihali
* @license MIT
*/
/**
* Custom Captcha Component Package
*
* A React component that provides sliding puzzle captcha functionality.
* Perfect for form validation and bot prevention.
*
* You can import this component using:
* `import CaptchaComponent from 'custom-captcha/client';`
*
* @author Murtaza Baanihali
* @license MIT
*/