import React, { useState, useEffect, useCallback } from 'react' import CaptchaGrid from './components/CaptchaGrid' interface Vtuber { affiliation: string; answer: boolean; id: number; image: string; name: string; } interface VtuberData { category: string; onFail: { extra: any; text: string }; questions: Vtuber[]; title: string; session: string | null } const App: React.FC = () => { const [captchaData, setCaptchaData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [selectedIndices, setSelectedIndices] = useState([]); const [serverAuth, setServerAuth] = useState(false); const [availableOrganizations, setAvailableOrganizations] = useState([]); const [organization, setOrganization] = useState("Hololive"); const [sessionId, setSessionId] = useState(null); const loadCaptchaData = useCallback(() => { setLoading(true); fetch(import.meta.env.VITE_API_URL + '/api/affiliation/' + organization + (serverAuth ? '?auth=server' : '')) .then((response) => { if (!response.ok) { throw new Error("Network response for affiliation endpoint was not ok"); } return response.json(); }) .then((jsonData: VtuberData) => { setCaptchaData(jsonData); setSelectedIndices([]); // Reset selections if(serverAuth){ setSessionId(jsonData.session) } setLoading(false); }) .catch((err) => { setError(err.message); setLoading(false); }); }, [organization, serverAuth]); useEffect(() => { loadCaptchaData(); }, [loadCaptchaData]); useEffect(() => { fetch(import.meta.env.VITE_API_URL + '/api/list_orgs') .then((response) => { if (!response.ok) { throw new Error("Network response for list of available orgs was not ok"); } return response.json(); }) .then((data) => { setAvailableOrganizations(data); }) .catch((err) => { console.error("Failed to load organizations:", err); }); }, []); const verifyResults = () => { if (!captchaData) return; if (serverAuth) { const answerString = selectedIndices .map((index) => captchaData.questions[index].id) .join(","); const formData = new FormData(); if (sessionId) { formData.append('session', sessionId); } formData.append('answer', answerString); fetch(import.meta.env.VITE_API_URL + '/api/verify', { method: 'POST', body: formData, }) .then((response) => response.json()) .then((data) => { if (data.success) { alert("CORRECT! You did it!"); } else { alert("FAILED: You did not select all correct choices or selected an incorrect option"); } }) .catch((error) => { alert("Error verifying answers: " + error.message); }); } else { const correctIndices = captchaData.questions.reduce((acc, question, index) => { if (question.answer === true) { acc.push(index); } return acc; }, []); const sortedSelected = [...selectedIndices].sort((a, b) => a - b); const sortedCorrect = [...correctIndices].sort((a, b) => a - b); const isEqual = sortedSelected.length === sortedCorrect.length && sortedSelected.every((val, index) => val === sortedCorrect[index]); if (isEqual) { alert("CORRECT! You did it!"); } else { alert("FAILED: You did not select all correct choices or selected an incorrect option"); } } loadCaptchaData(); } if (loading) return
Loading data...
; if (error) return
Error loading data: {error}
; if (!captchaData) return
No data available
; const images = captchaData.questions.map((q) => q.image); const handleSelectionChange = (indices: number[]) => { setSelectedIndices(indices); }; return ( <>
i

{captchaData.title}

Select all images that match the description

{selectedIndices.length > 0 ? (
{selectedIndices.map((index) => ( {captchaData.questions[index].name} ))}
) : ( None selected )}
Organization
Server Side Authentication:

VTuber Captcha

Above is a demo component of what the captcha could look like in your application.
It's strongly recommended that you use a middleware service and store answers on your own server rather than relying on the API's server-auth functionality, as session persistence cannot be guaranteed.

); } export default App