From f98fd771dc3de91827cb1162b64fbcf7359016c8 Mon Sep 17 00:00:00 2001 From: evgongora Date: Fri, 14 Feb 2025 09:27:17 -0600 Subject: [PATCH] fix: minor changes to UI and logic on canvas --- frontend/.env.example | 5 +- frontend/package.json | 2 +- frontend/pnpm-lock.yaml | 6 +- frontend/src/app/ideology-test/page.tsx | 42 +++- frontend/src/app/insights/page.tsx | 177 ++++++++-------- frontend/src/components/Canvas.tsx | 243 ++++++++++++++++------ frontend/src/providers/eruda-provider.tsx | 2 +- frontend/src/providers/index.tsx | 2 +- 8 files changed, 323 insertions(+), 156 deletions(-) diff --git a/frontend/.env.example b/frontend/.env.example index 2a09281..9d8eafd 100644 --- a/frontend/.env.example +++ b/frontend/.env.example @@ -20,4 +20,7 @@ XATA_DATABASE_URL= # Environment Configuration NEXT_PUBLIC_PAYMENT_ADDRESS= NEXT_PUBLIC_APP_ENV= -NODE_ENV= \ No newline at end of file +NODE_ENV= + +# Enable development tools (Eruda) +NEXT_PUBLIC_ENABLE_DEVTOOLS= \ No newline at end of file diff --git a/frontend/package.json b/frontend/package.json index e7af535..e9d63fd 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -20,7 +20,6 @@ "@worldcoin/minikit-js": "1.5.0", "@xata.io/client": "0.0.0-next.va121e4207b94bfe0a3c025fc00b247b923880930", "dotenv": "^16.4.5", - "eruda": "^3.2.3", "framer-motion": "^12.0.5", "jose": "^5.9.6", "jotai": "^2.11.1", @@ -46,6 +45,7 @@ "autoprefixer": "^10.4.20", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "eruda": "^3.2.3", "eslint": "^9.17.0", "eslint-config-next": "15.1.3", "postcss": "^8.4.49", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 5484c09..f32bcc5 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -35,9 +35,6 @@ importers: dotenv: specifier: ^16.4.5 version: 16.4.7 - eruda: - specifier: ^3.2.3 - version: 3.4.1 framer-motion: specifier: ^12.0.5 version: 12.4.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -105,6 +102,9 @@ importers: clsx: specifier: ^2.1.1 version: 2.1.1 + eruda: + specifier: ^3.2.3 + version: 3.4.1 eslint: specifier: ^9.17.0 version: 9.20.1(jiti@1.21.7) diff --git a/frontend/src/app/ideology-test/page.tsx b/frontend/src/app/ideology-test/page.tsx index 29eec06..557b28a 100644 --- a/frontend/src/app/ideology-test/page.tsx +++ b/frontend/src/app/ideology-test/page.tsx @@ -28,6 +28,7 @@ export default function IdeologyTest() { const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [isSubmitting, setIsSubmitting] = useState(false); + const [originalAnswers, setOriginalAnswers] = useState>({}); const totalQuestions = questions.length; const progress = ((currentQuestion + 1) / totalQuestions) * 100; @@ -49,6 +50,13 @@ export default function IdeologyTest() { const response = await fetch(`/api/tests/${testId}/progress`); if (response.ok) { const data = await response.json(); + + // Check if test is already completed + if (data.status === "completed") { + router.push(`/insights?testId=${testId}`); + return; + } + if (data.answers && Object.keys(data.answers).length > 0) { const lastAnsweredId = Object.keys(data.answers).pop(); const lastAnsweredIndex = loadedQuestions.findIndex( @@ -61,6 +69,7 @@ export default function IdeologyTest() { setCurrentQuestion(nextQuestionIndex); setScores(data.scores || { econ: 0, dipl: 0, govt: 0, scty: 0 }); setUserAnswers(data.answers); + setOriginalAnswers(data.answers); } } } catch (error) { @@ -89,9 +98,8 @@ export default function IdeologyTest() { }, [testId]); const handleEndTest = async () => { - if (isSubmitting) return; // Prevent multiple submissions + if (isSubmitting) return; - // Check if all questions have been answered const unansweredQuestions = Object.keys(userAnswers).length; if (unansweredQuestions < questions.length) { setError( @@ -105,6 +113,7 @@ export default function IdeologyTest() { setIsSubmitting(true); try { + // Calculate scores first as we'll need them in both cases const maxEcon = questions.reduce( (sum, q) => sum + Math.abs(q.effect.econ), 0, @@ -134,6 +143,17 @@ export default function IdeologyTest() { scty: Math.round(sctyScore), }; + // Check if insights exist + const insightsResponse = await fetch(`/api/insights/${testId}`); + const hasExistingInsights = insightsResponse.ok && + (await insightsResponse.json()).insights?.length > 0; + + // Check if answers have changed + const hasAnswersChanged = Object.keys(originalAnswers).some( + key => originalAnswers[key] !== userAnswers[key] + ); + + // Save progress and update scores const response = await fetch(`/api/tests/${testId}/progress`, { method: "POST", headers: { @@ -151,7 +171,23 @@ export default function IdeologyTest() { throw new Error("Failed to save final answers"); } - const resultsResponse = await fetch(`/api/tests/${testId}/results`); + // Handle the three scenarios: + // 1. If insights exist and no changes - just redirect + if (hasExistingInsights && !hasAnswersChanged) { + router.push(`/insights?testId=${testId}`); + return; + } + + // 2. If no insights exist - create new ones + // 3. If answers changed - rewrite existing insights + const resultsResponse = await fetch(`/api/tests/${testId}/results`, { + method: hasExistingInsights ? "PUT" : "POST", // Use PUT to update existing insights + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ forceUpdate: hasAnswersChanged }), + }); + if (!resultsResponse.ok) { throw new Error("Failed to save final results"); } diff --git a/frontend/src/app/insights/page.tsx b/frontend/src/app/insights/page.tsx index 7de9430..2df5ae3 100644 --- a/frontend/src/app/insights/page.tsx +++ b/frontend/src/app/insights/page.tsx @@ -37,6 +37,8 @@ export default function InsightsPage() { const [scores, setScores] = useState({ econ: 0, dipl: 0, govt: 0, scty: 0 }); const [publicFigure, setPublicFigure] = useState(""); const canvasRef = useRef(null); + const [isCanvasLoading, setIsCanvasLoading] = useState(true); + const [isGeminiLoading, setIsGeminiLoading] = useState(false); // Emit modal state changes useEffect(() => { @@ -46,6 +48,14 @@ export default function InsightsPage() { window.dispatchEvent(event); }, [isShareModalOpen]); + // Emit advanced insights modal state changes + useEffect(() => { + const event = new CustomEvent("shareModalState", { + detail: { isOpen: isModalOpen }, + }); + window.dispatchEvent(event); + }, [isModalOpen]); + const testId = searchParams.get("testId"); useEffect(() => { @@ -80,7 +90,6 @@ export default function InsightsPage() { throw new Error("Failed to fetch scores"); } const scoresData = await scoresResponse.json(); - const { scores } = scoresData; setScores(scoresData.scores); // Get public figure match @@ -91,44 +100,58 @@ export default function InsightsPage() { const figureData = await figureResponse.json(); setPublicFigure(figureData.celebrity || "Unknown Match"); - // Call Gemini API for full analysis (Pro users only) - if (isProUser) { - const geminiResponse = await fetch("/api/gemini-flash", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - econ: Number.parseFloat(scores.econ || "0"), - dipl: Number.parseFloat(scores.dipl || "0"), - govt: Number.parseFloat(scores.govt || "0"), - scty: Number.parseFloat(scores.scty || "0"), - }), - }); - - if (geminiResponse.status === 200) { - const geminiData = await geminiResponse.json(); - setFullAnalysis(geminiData.analysis); - } else { - console.error( - "Error fetching Gemini analysis:", - geminiResponse.statusText, - ); - setFullAnalysis( - "Failed to generate analysis. Please try again later.", - ); - } - } } catch (error) { console.error("Error fetching insights:", error); - setFullAnalysis("Failed to generate analysis. Please try again later."); } finally { setLoading(false); } } void fetchInsights(); - }, [testId, isProUser]); + }, [testId]); + + // Separate effect for Gemini API call + useEffect(() => { + async function fetchGeminiAnalysis() { + if (!isProUser || !isModalOpen) return; + + setIsGeminiLoading(true); + try { + const geminiResponse = await fetch("/api/gemini-flash", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + econ: scores.econ || 0, + dipl: scores.dipl || 0, + govt: scores.govt || 0, + scty: scores.scty || 0, + }), + }); + + if (geminiResponse.status === 200) { + const geminiData = await geminiResponse.json(); + setFullAnalysis(geminiData.analysis); + } else { + console.error( + "Error fetching Gemini analysis:", + geminiResponse.statusText, + ); + setFullAnalysis( + "Failed to generate analysis. Please try again later.", + ); + } + } catch (error) { + console.error("Error fetching Gemini analysis:", error); + setFullAnalysis("Failed to generate analysis. Please try again later."); + } finally { + setIsGeminiLoading(false); + } + } + + void fetchGeminiAnalysis(); + }, [isProUser, isModalOpen, scores]); const handleAdvancedInsightsClick = () => { setIsModalOpen(true); @@ -143,23 +166,6 @@ export default function InsightsPage() { setIsShareModalOpen(true); }; - const downloadImage = () => { - if (!canvasRef.current) return; - try { - const canvas = canvasRef.current; - const dataUrl = canvas.toDataURL("image/png"); - const link = document.createElement("a"); - link.download = "results.png"; - link.href = dataUrl; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - } catch (error) { - console.error("Error downloading image:", error); - alert("Failed to download image. Please try again."); - } - }; - const handleInstagramShare = async () => { if (!canvasRef.current) return; @@ -227,7 +233,7 @@ export default function InsightsPage() { } return ( -
+
+ {isCanvasLoading && ( +
+ +
+ )} setIsCanvasLoading(false)} />
-
- - - Download Image - +
- Share Link + Share Results
@@ -498,9 +486,15 @@ export default function InsightsPage() {
{isProUser ? (
-

- {fullAnalysis} -

+ {isGeminiLoading ? ( +
+ +
+ ) : ( +

+ {fullAnalysis} +

+ )}
) : (
@@ -552,6 +546,17 @@ export default function InsightsPage() { )} + + {/* Emit modal state for bottom nav visibility */} +