UNPKG

@baanihali/captcha

Version:

A customizable sliding puzzle captcha component for React applications with server-side validation

263 lines (258 loc) 12.9 kB
// 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 */