JSPM

strainers

1.0.0
  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 4
  • Score
    100M100P100Q49148F
  • License Apache-2.0

simple challenge page creator.

Package Exports

  • strainers
  • strainers/config

Readme

Strainers

Strainers Logo

Simple Challenge Page Creator

A powerful React library for creating beautiful, Cloudflare-style challenge pages with hCaptcha, reCAPTCHA, or Turnstile verification. Shows only once per session.

npm version npm downloads license


✨ Features

  • πŸ” Multiple Captcha Support - hCaptcha, reCAPTCHA, and Cloudflare Turnstile
  • 🎨 Beautiful UI - Cloudflare-style challenge page design
  • πŸ”„ Session Persistence - Shows only once per session (like Cloudflare Challenge Page)
  • βœ… Visual Feedback - βœ… on success, ❌ on failure
  • πŸŒ— Theme Support - Light, Dark, and Auto themes
  • βš›οΈ React Frameworks - Works with React, Next.js, Remix, Vite, and more
  • πŸ“¦ TypeScript - Full TypeScript support with types included
  • 🎯 Fully Customizable - Custom components, slots, layouts, animations, and styles

πŸ“¦ Installation

npm install strainers
yarn add strainers
pnpm add strainers

πŸš€ Quick Start

Basic Usage

'use client';

import { ChallengePage } from 'strainers';

export default function Challenge() {
  return (
    <ChallengePage
      config={{
        // ===== μœ„μ ― 선택 (택 1) =====
        
        // βœ… Cloudflare Turnstile μ‚¬μš©
        widget: 'turnstile',
        siteKey: 'YOUR_TURNSTILE_SITE_KEY',
        
        // βœ… hCaptcha μ‚¬μš©
        // widget: 'hcaptcha',
        // siteKey: 'YOUR_HCAPTCHA_SITE_KEY',
        
        // βœ… Google reCAPTCHA v2 μ‚¬μš©
        // widget: 'recaptcha',
        // siteKey: 'YOUR_RECAPTCHA_SITE_KEY',

        // ===== λ¦¬λ””λ ‰μ…˜ μ„€μ • =====
        successRedirect: '/dashboard',  // 인증 성곡 ν›„ 이동할 νŽ˜μ΄μ§€
        // successRedirect: '/',        // κΈ°λ³Έκ°’
        
        // ===== 선택적 μ„€μ • =====
        theme: 'auto',  // 'light' | 'dark' | 'auto'
        title: 'Security Check',
        subtitle: 'Please complete the verification to continue.',
      }}
      callbacks={{
        onSuccess: (token) => console.log('βœ… Verified:', token),
        onError: (error) => console.error('❌ Failed:', error),
      }}
    />
  );
}

πŸ”„ Session Persistence

Strainers automatically saves verification status in sessionStorage. This means:

  • βœ… Challenge page shows only once when user first visits
  • βœ… Page refresh β†’ Challenge page is skipped
  • βœ… Page navigation β†’ Challenge page is skipped
  • βœ… Close browser tab β†’ Verification resets (next visit shows challenge)
// To manually clear verification (for testing or logout)
import { clearVerification } from 'strainers';

function LogoutButton() {
  const handleLogout = () => {
    clearVerification();  // Clear session verification
    window.location.href = '/';
  };
  
  return <button onClick={handleLogout}>Logout</button>;
}

πŸ“± Examples

With Next.js App Router

// app/page.tsx - Entry point (Challenge Page)
'use client';

import { ChallengePage } from 'strainers';

export default function Home() {
  return (
    <ChallengePage
      config={{
        // Turnstile μ‚¬μš©
        widget: 'turnstile',
        siteKey: process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY!,
        
        // hCaptcha μ‚¬μš©μ‹œ:
        // widget: 'hcaptcha',
        // siteKey: process.env.NEXT_PUBLIC_HCAPTCHA_SITE_KEY!,
        
        // reCAPTCHA μ‚¬μš©μ‹œ:
        // widget: 'recaptcha',
        // siteKey: process.env.NEXT_PUBLIC_RECAPTCHA_SITE_KEY!,

        theme: 'auto',
        successRedirect: '/dashboard',
      }}
    />
  );
}
// app/dashboard/page.tsx - Main page after verification
'use client';

import { clearVerification } from 'strainers';

export default function Dashboard() {
  return (
    <div>
      <h1>πŸŽ‰ Welcome!</h1>
      <p>You have passed the security check.</p>
      
      {/* ν…ŒμŠ€νŠΈμš©: 인증 μ΄ˆκΈ°ν™” λ²„νŠΌ */}
      <button onClick={() => {
        clearVerification();
        window.location.href = '/';
      }}>
        πŸ”„ Reset Verification
      </button>
    </div>
  );
}

With Remix

// app/routes/_index.tsx
'use client';

import { ChallengePage } from 'strainers';

export default function Index() {
  return (
    <ChallengePage
      config={{
        // Turnstile
        widget: 'turnstile',
        siteKey: 'YOUR_TURNSTILE_KEY',
        
        // hCaptcha
        // widget: 'hcaptcha',
        // siteKey: 'YOUR_HCAPTCHA_KEY',
        
        // reCAPTCHA
        // widget: 'recaptcha',
        // siteKey: 'YOUR_RECAPTCHA_KEY',

        successRedirect: '/dashboard',
      }}
    />
  );
}

With Vite

// src/pages/Challenge.tsx
import { ChallengePage } from 'strainers';

export function Challenge() {
  return (
    <ChallengePage
      config={{
        // Turnstile
        widget: 'turnstile',
        siteKey: import.meta.env.VITE_TURNSTILE_SITE_KEY,
        
        // hCaptcha
        // widget: 'hcaptcha',
        // siteKey: import.meta.env.VITE_HCAPTCHA_SITE_KEY,
        
        // reCAPTCHA
        // widget: 'recaptcha',
        // siteKey: import.meta.env.VITE_RECAPTCHA_SITE_KEY,

        theme: 'dark',
        successRedirect: '/',
      }}
    />
  );
}

βš™οΈ Configuration

All Options

<ChallengePage
  config={{
    // ===== Required =====
    widget: 'turnstile',  // 'turnstile' | 'hcaptcha' | 'recaptcha'
    siteKey: 'YOUR_SITE_KEY',

    // ===== Theme & Appearance =====
    theme: 'auto',  // 'light' | 'dark' | 'auto'
    title: 'Security Check',  // string | ReactNode | false
    subtitle: 'Please verify...',  // string | ReactNode | false
    logoUrl: '/logo.png',  // string | false
    widgetSize: 'normal',  // 'normal' | 'compact' | 'invisible'

    // ===== Redirect =====
    successRedirect: '/dashboard',  // string | false (to disable)
    redirectDelay: 1500,  // ms before redirect

    // ===== Messages =====
    successMessage: 'βœ… Verified! Redirecting...',
    loadingMessage: 'Verifying...',
    errorMessages: {
      verificationFailed: '❌ Verification failed.',
      timeout: '⏰ Timed out.',
      maxAttemptsReached: '🚫 Max attempts reached.',
    },

    // ===== Retry =====
    retry: {
      enabled: true,
      maxAttempts: 3,
      delay: 1000,
    },

    // ===== Branding =====
    showBranding: true,
    brandingText: 'Protected by Strainers',
  }}
/>

🎨 Full Customization

Element Customization

<ChallengePage
  config={{
    widget: 'turnstile',  // turnstile, hcaptcha, recaptcha
    siteKey: 'KEY',
    successRedirect: '/dashboard',
    
    elements: {
      container: {
        style: { background: 'linear-gradient(135deg, #1a1a2e, #16213e)' },
      },
      card: {
        style: { maxWidth: '500px', borderRadius: '24px' },
      },
      logo: {
        width: 80,
        height: 80,
      },
      title: {
        style: { fontSize: '2rem' },
      },
      subtitle: {
        hidden: true,  // Hide subtitle
      },
      retryButton: {
        text: 'λ‹€μ‹œ μ‹œλ„',
      },
    },
  }}
/>

Slot Components

<ChallengePage
  config={{
    widget: 'turnstile',  // or 'hcaptcha' or 'recaptcha'
    siteKey: 'KEY',
    successRedirect: '/dashboard',
    
    slots: {
      // Static content
      beforeLogo: <div className="badge">Secure</div>,
      
      // Dynamic content
      footer: ({ status, attempts }) => (
        <p>Status: {status} | Attempts: {attempts}</p>
      ),
      
      // Custom header (replaces logo, title, subtitle)
      header: ({ theme }) => (
        <div>
          <h1>Custom Header</h1>
        </div>
      ),
    },
  }}
/>

Custom Components

<ChallengePage
  config={{
    widget: 'hcaptcha',  // or 'turnstile' or 'recaptcha'
    siteKey: 'KEY',
    
    components: {
      Container: ({ children, style }) => (
        <main style={style}>{children}</main>
      ),
      Logo: ({ src }) => (
        <img src={src} className="custom-logo" />
      ),
      RetryButton: ({ onClick, text }) => (
        <button onClick={onClick}>{text}</button>
      ),
      Spinner: () => <div className="custom-loader" />,
    },
  }}
/>

Unstyled Mode

<ChallengePage
  config={{
    widget: 'recaptcha',
    siteKey: 'KEY',
    unstyled: true,  // Remove ALL default styles
    elements: {
      container: { className: 'my-custom-container' },
      card: { className: 'my-custom-card' },
    },
  }}
/>

πŸͺ Hooks

useChallenge

import { useChallenge } from 'strainers';

function MyComponent() {
  const { state, reset, verify } = useChallenge({
    widget: 'turnstile',
    siteKey: 'KEY',
  });

  return (
    <div>
      <p>Status: {state.status}</p>
      <p>Attempts: {state.attempts}</p>
      <button onClick={reset}>Reset</button>
    </div>
  );
}

πŸ”§ Individual Widget Components

import { CaptchaWidget, TurnstileWidget, HCaptchaWidget, ReCaptchaWidget } from 'strainers';

// Universal widget
<CaptchaWidget
  type="turnstile"  // or "hcaptcha" or "recaptcha"
  siteKey="KEY"
  onVerify={(token) => console.log(token)}
/>

// Individual widgets
<TurnstileWidget siteKey="KEY" onVerify={handleVerify} />
<HCaptchaWidget siteKey="KEY" onVerify={handleVerify} />
<ReCaptchaWidget siteKey="KEY" onVerify={handleVerify} />

πŸ“¦ Publishing to NPM

To publish this package to NPM:

  1. Login to NPM:

    npm login
  2. Build the package:

    npm run build
  3. Publish:

    npm publish
    • Note: Ensure verification of the version in package.json before publishing.
    • Use npm publish --access public if this is your first time publishing a scoped package or if you want it correctly listed.

πŸ“ TypeScript

All types are exported:

import type {
  StrainersConfig,
  CaptchaWidgetType,  // 'turnstile' | 'hcaptcha' | 'recaptcha'
  ChallengeTheme,
  VerificationStatus,
  ChallengePageProps,
  VerificationCallbacks,
} from 'strainers';

πŸ“„ License

Apache-2.0 Β© Madmovies


Made with ❀️ by neramc