From 14dbd311366bf0a61526a064730f57cde52ad8e3 Mon Sep 17 00:00:00 2001 From: Pinapelz Date: Tue, 8 Apr 2025 21:16:39 -0700 Subject: add new better styled demo page and components --- site/src/App.tsx | 237 ++++++++++++++++++++++++++++++++++++ site/src/components/CaptchaGrid.tsx | 52 ++++++++ site/src/index.css | 1 + site/src/main.tsx | 10 ++ site/src/vite-env.d.ts | 1 + 5 files changed, 301 insertions(+) create mode 100644 site/src/App.tsx create mode 100644 site/src/components/CaptchaGrid.tsx create mode 100644 site/src/index.css create mode 100644 site/src/main.tsx create mode 100644 site/src/vite-env.d.ts (limited to 'site/src') diff --git a/site/src/App.tsx b/site/src/App.tsx new file mode 100644 index 0000000..1a72ed0 --- /dev/null +++ b/site/src/App.tsx @@ -0,0 +1,237 @@ +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 diff --git a/site/src/components/CaptchaGrid.tsx b/site/src/components/CaptchaGrid.tsx new file mode 100644 index 0000000..40b1c09 --- /dev/null +++ b/site/src/components/CaptchaGrid.tsx @@ -0,0 +1,52 @@ +import React, { useState } from "react"; + +interface CaptchaGridProps { + images: string[]; + onSelectionChange: (selectedIndicies: number[]) => void; +} + +const CaptchaGrid: React.FC = ({ images, onSelectionChange }) => { + const [selectedIndicies, setSelectedIndicies] = useState([]); + const handleSelect = (index: number) => { + let newSelectedState: number[]; + if (selectedIndicies.includes(index)) { + newSelectedState = selectedIndicies.filter((i) => i !== index) + } + else { + newSelectedState = [...selectedIndicies, index]; + } + setSelectedIndicies(newSelectedState); // update local view + onSelectionChange(newSelectedState); // pass to parent + + } + + return ( +
+ {images.map((imageUrl, idx) => ( +
handleSelect(idx)} + > + {`Image + {selectedIndicies.includes(idx) && ( +
+
+ + + +
+
+ )} +
+ ))} +
+ ); +}; + +export default CaptchaGrid diff --git a/site/src/index.css b/site/src/index.css new file mode 100644 index 0000000..f1d8c73 --- /dev/null +++ b/site/src/index.css @@ -0,0 +1 @@ +@import "tailwindcss"; diff --git a/site/src/main.tsx b/site/src/main.tsx new file mode 100644 index 0000000..bef5202 --- /dev/null +++ b/site/src/main.tsx @@ -0,0 +1,10 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import './index.css' +import App from './App.tsx' + +createRoot(document.getElementById('root')!).render( + + + , +) diff --git a/site/src/vite-env.d.ts b/site/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/site/src/vite-env.d.ts @@ -0,0 +1 @@ +/// -- cgit v1.2.3