From 85e623ce493a432fe427db5cc60ac9583f86247f Mon Sep 17 00:00:00 2001 From: Lazarus Date: Tue, 4 Feb 2025 21:17:37 -0600 Subject: [PATCH 1/5] feat: added shareable results --- .../public/MindVaultLogoTransparentHD.svg | 1 + frontend/public/authority-icon.svg | 1 + frontend/public/equality-icon.svg | 1 + frontend/public/globe-icon.svg | 1 + frontend/public/liberty-icon.svg | 1 + frontend/public/market-icon.svg | 1 + frontend/public/nation-icon.svg | 1 + frontend/public/progress-icon.svg | 1 + frontend/public/tradition-icon.svg | 1 + frontend/src/app/api/public-figures/route.ts | 284 ++++++++++++++++ frontend/src/app/insights/page.tsx | 135 +++++++- frontend/src/components/Canvas.tsx | 309 ++++++++++++++++++ 12 files changed, 736 insertions(+), 1 deletion(-) create mode 100644 frontend/public/MindVaultLogoTransparentHD.svg create mode 100644 frontend/public/authority-icon.svg create mode 100644 frontend/public/equality-icon.svg create mode 100644 frontend/public/globe-icon.svg create mode 100644 frontend/public/liberty-icon.svg create mode 100644 frontend/public/market-icon.svg create mode 100644 frontend/public/nation-icon.svg create mode 100644 frontend/public/progress-icon.svg create mode 100644 frontend/public/tradition-icon.svg create mode 100644 frontend/src/app/api/public-figures/route.ts create mode 100644 frontend/src/components/Canvas.tsx diff --git a/frontend/public/MindVaultLogoTransparentHD.svg b/frontend/public/MindVaultLogoTransparentHD.svg new file mode 100644 index 0000000..db24567 --- /dev/null +++ b/frontend/public/MindVaultLogoTransparentHD.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/authority-icon.svg b/frontend/public/authority-icon.svg new file mode 100644 index 0000000..bd0f814 --- /dev/null +++ b/frontend/public/authority-icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/equality-icon.svg b/frontend/public/equality-icon.svg new file mode 100644 index 0000000..9c64d0f --- /dev/null +++ b/frontend/public/equality-icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/globe-icon.svg b/frontend/public/globe-icon.svg new file mode 100644 index 0000000..3ed854d --- /dev/null +++ b/frontend/public/globe-icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/liberty-icon.svg b/frontend/public/liberty-icon.svg new file mode 100644 index 0000000..3f997ac --- /dev/null +++ b/frontend/public/liberty-icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/market-icon.svg b/frontend/public/market-icon.svg new file mode 100644 index 0000000..cb0f20f --- /dev/null +++ b/frontend/public/market-icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/nation-icon.svg b/frontend/public/nation-icon.svg new file mode 100644 index 0000000..2514e2f --- /dev/null +++ b/frontend/public/nation-icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/progress-icon.svg b/frontend/public/progress-icon.svg new file mode 100644 index 0000000..5be61e3 --- /dev/null +++ b/frontend/public/progress-icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/tradition-icon.svg b/frontend/public/tradition-icon.svg new file mode 100644 index 0000000..625f244 --- /dev/null +++ b/frontend/public/tradition-icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/app/api/public-figures/route.ts b/frontend/src/app/api/public-figures/route.ts new file mode 100644 index 0000000..348f8d0 --- /dev/null +++ b/frontend/src/app/api/public-figures/route.ts @@ -0,0 +1,284 @@ +import { getXataClient } from "@/lib/utils"; +import { NextResponse } from "next/server"; +import { cookies } from 'next/headers'; +import { jwtVerify } from 'jose'; + +interface UserScores { + dipl: number; + econ: number; + govt: number; + scty: number; +} + +const JWT_SECRET = process.env.JWT_SECRET; +if (!JWT_SECRET) { + throw new Error('JWT_SECRET environment variable is required'); +} + +const secret = new TextEncoder().encode(JWT_SECRET); + +/** + * Calculate the similarity between user scores and celebrity scores + * Lower score means more similar + */ +function calculateSimilarity(userScores: UserScores, celebrityScores: UserScores): number { + const diff = { + dipl: Math.abs(userScores.dipl - celebrityScores.dipl), + econ: Math.abs(userScores.econ - celebrityScores.econ), + govt: Math.abs(userScores.govt - celebrityScores.govt), + scty: Math.abs(userScores.scty - celebrityScores.scty) + }; + + // Return average difference (lower is better) + return (diff.dipl + diff.econ + diff.govt + diff.scty) / 4; +} + +/** + * @swagger + * /api/public-figures: + * post: + * summary: Calculate user's celebrity match based on scores + * description: Matches user's scores with closest celebrity and updates PublicFiguresPerUser table + * tags: + * - Celebrity + * security: + * - SessionCookie: [] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - dipl + * - econ + * - govt + * - scty + * properties: + * dipl: + * type: number + * minimum: 0 + * maximum: 100 + * econ: + * type: number + * minimum: 0 + * maximum: 100 + * govt: + * type: number + * minimum: 0 + * maximum: 100 + * scty: + * type: number + * minimum: 0 + * maximum: 100 + * responses: + * 200: + * description: Successfully matched and saved celebrity + * content: + * application/json: + * schema: + * type: object + * properties: + * celebrity: + * type: string + * example: "Elon Musk" + * similarity: + * type: number + * example: 85.5 + * 400: + * description: Invalid scores provided + * 401: + * description: Unauthorized + * 404: + * description: User not found + * 500: + * description: Internal server error + */ +export async function POST(request: Request) { + try { + const xata = getXataClient(); + let user; + + // Get token from cookies + const token = cookies().get('session')?.value; + + if (!token) { + return NextResponse.json( + { error: "Unauthorized" }, + { status: 401 } + ); + } + + try { + const { payload } = await jwtVerify(token, secret); + if (payload.address) { + user = await xata.db.Users.filter({ + wallet_address: payload.address + }).getFirst(); + } + } catch (error) { + return NextResponse.json( + { error: "Invalid session" }, + { status: 401 } + ); + } + + if (!user) { + return NextResponse.json( + { error: "User not found" }, + { status: 404 } + ); + } + + // Get user scores from request body + const userScores = await request.json() as UserScores; + + // Validate scores + const scores = [userScores.dipl, userScores.econ, userScores.govt, userScores.scty]; + if (scores.some(score => score < 0 || score > 100 || !Number.isFinite(score))) { + return NextResponse.json( + { error: "Invalid scores. All scores must be between 0 and 100" }, + { status: 400 } + ); + } + + // Get all ideologies + const celebrities = await xata.db.PublicFigures.getAll(); + + if (!celebrities.length) { + return NextResponse.json( + { error: "No public figures found in database" }, + { status: 404 } + ); + } + + // Find best matching ideology + let bestMatch = celebrities[0]; + let bestSimilarity = calculateSimilarity(userScores, celebrities[0].scores as UserScores); + + for (const celebrity of celebrities) { + const similarity = calculateSimilarity(userScores, celebrity.scores as UserScores); + if (similarity < bestSimilarity) { + bestSimilarity = similarity; + bestMatch = celebrity; + } + } + + // Get latest celebrity_user_id + const latestCelebrity = await xata.db.PublicFigurePerUser + .sort("celebrity_user_id", "desc") + .getFirst(); + const nextCelebrityId = (latestCelebrity?.celebrity_user_id || 0) + 1; + + // Update or create PublicFigurePerUser record + await xata.db.PublicFigurePerUser.create({ + user: user.xata_id, + public_figure: bestMatch.xata_id, + celebrity_user_id: nextCelebrityId + }); + + return NextResponse.json({ + public_figure: bestMatch.name + }); + + } catch (error) { + console.error("Error calculating celebrity match:", error); + return NextResponse.json( + { error: "Failed to calculate celebrity match" }, + { status: 500 } + ); + } +} + +/** + * @swagger + * /api/public-figures: + * get: + * summary: Get user's celebrity match + * description: Retrieves the user's current celebrity match from their latest test results + * tags: + * - Celebrity + * security: + * - SessionCookie: [] + * responses: + * 200: + * description: Successfully retrieved user's celebrity match + * content: + * application/json: + * schema: + * type: object + * properties: + * public_figure: + * type: string + * example: "Elon Musk" + * 401: + * description: Unauthorized + * 404: + * description: User or celebrity match not found + * 500: + * description: Internal server error + */ +export async function GET() { + try { + const xata = getXataClient(); + let user; + + // Get token from cookies + const token = cookies().get('session')?.value; + + if (!token) { + return NextResponse.json( + { error: "Unauthorized" }, + { status: 401 } + ); + } + + try { + const { payload } = await jwtVerify(token, secret); + if (payload.address) { + user = await xata.db.Users.filter({ + wallet_address: payload.address + }).getFirst(); + } + } catch (error) { + return NextResponse.json( + { error: "Invalid session" }, + { status: 401 } + ); + } + + if (!user) { + return NextResponse.json( + { error: "User not found" }, + { status: 404 } + ); + } + + // Get user's latest celebrity match from PublicFigurePerUser + const userCelebrity = await xata.db.PublicFigurePerUser + .filter({ + "user.xata_id": user.xata_id + }) + .sort("celebrity_user_id", "desc") + .select(["celebrity.name"]) + .getFirst(); + + if (!userCelebrity || !userCelebrity.celebrity?.name) { + return NextResponse.json( + { error: "No celebrity found for user" }, + { status: 404 } + ); + } + + return NextResponse.json({ + celebrity: userCelebrity.celebrity.name + }); + + } catch (error) { + console.error("Error fetching celebrity:", error); + return NextResponse.json( + { error: "Failed to fetch celebrity" }, + { status: 500 } + ); + } +} diff --git a/frontend/src/app/insights/page.tsx b/frontend/src/app/insights/page.tsx index 73e42c0..729a26c 100644 --- a/frontend/src/app/insights/page.tsx +++ b/frontend/src/app/insights/page.tsx @@ -1,6 +1,6 @@ "use client"; -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useState, useRef } from 'react'; import { InsightResultCard } from '@/components/ui/InsightResultCard'; import { FilledButton } from '@/components/ui/FilledButton'; import { useSearchParams, useRouter } from 'next/navigation'; @@ -8,6 +8,7 @@ import { motion } from 'framer-motion'; import { LoadingSpinner } from "@/components/ui/LoadingSpinner"; import { BookOpen } from 'lucide-react'; import { cn } from '@/lib/utils'; +import ResultsCanvas from '@/components/Canvas'; interface Insight { category: string; @@ -28,10 +29,14 @@ export default function InsightsPage() { const [insights, setInsights] = useState([]); const [loading, setLoading] = useState(true); const [isModalOpen, setIsModalOpen] = useState(false); + const [isShareModalOpen, setIsShareModalOpen] = useState(false); const [isProUser, setIsProUser] = useState(false); const [fullAnalysis, setFullAnalysis] = useState(''); const [ideology, setIdeology] = useState(''); const searchParams = useSearchParams(); + const [scores, setScores] = useState({ econ: 0, dipl: 0, govt: 0, scty: 0 }); + const [publicFigure, setPublicFigure] = useState(''); + const canvasRef = useRef(null); const testId = searchParams.get('testId') @@ -66,6 +71,14 @@ export default function InsightsPage() { const scoresResponse = await fetch(`/api/tests/${testId}/progress`); const scoresData = await scoresResponse.json(); const { scores } = scoresData; + setScores(scoresData.scores); + + // Get public figure match + const figureResponse = await fetch('/api/public-figures'); + if (figureResponse.ok) { + const figureData = await figureResponse.json(); + setPublicFigure(figureData.celebrity || 'Unknown Match'); + } // Call DeepSeek API for full analysis if (isProUser) { @@ -107,6 +120,23 @@ export default function InsightsPage() { setIsModalOpen(true); }; + const handleShareClick = () => { + setIsShareModalOpen(true); + }; + + const downloadImage = () => { + if (!canvasRef.current) return; + + 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); + }; + if (loading) { return } @@ -198,8 +228,111 @@ export default function InsightsPage() { No insights available. Please try again later. )} + + {/* Added Share Button */} +
+ + Share Results + +
+ + + {isShareModalOpen && ( + setIsShareModalOpen(false)} + > + e.stopPropagation()} + > +
+ +
+ +

+ Share Your Results +

+
+ +
+
+ +
+
+ +
+ + + + + Download Image + + console.log('Share functionality')} + className="flex-1 py-3 text-sm bg-[#E36C59] + flex items-center justify-center gap-2" + > + + + + Share Link + +
+ + )} + {/* Existing Advanced Insights Modal */} {isModalOpen && ( (({ econ, dipl, govt, scty, closestMatch }, ref) => { + const canvasRef = useRef(null); + + useEffect(() => { + const canvas = canvasRef.current; + if (!canvas) return; + + const ctx = canvas.getContext('2d'); + if (!ctx) return; + + // Helper to load images + const loadImage = (src: string): Promise => { + return new Promise((resolve, reject) => { + const img = new Image(); + img.onload = () => resolve(img); + img.onerror = reject; + img.src = src; + }); + }; + + const drawIcon = (img: HTMLImageElement, x: number, y: number, size: number = 50) => { + ctx.drawImage(img, x - size / 2, y - size / 2, size, size); + }; + + const roundRect = (x: number, y: number, w: number, h: number, radius: number) => { + ctx.beginPath(); + ctx.moveTo(x + radius, y); + ctx.arcTo(x + w, y, x + w, y + h, radius); + ctx.arcTo(x + w, y + h, x, y + h, radius); + ctx.arcTo(x, y + h, x, y, radius); + ctx.arcTo(x, y, x + w, y, radius); + ctx.closePath(); + }; + + // Enhanced hexagonal pattern + const drawHexPattern = (x: number, y: number) => { + const size = 30; + const h = size * Math.sqrt(3); + ctx.strokeStyle = 'rgba(76, 170, 158, 0.03)'; + ctx.lineWidth = 1; + ctx.beginPath(); + ctx.moveTo(x, y); + for (let i = 0; i < 6; i++) { + const angle = (i * Math.PI) / 3; + const nextX = x + size * Math.cos(angle); + const nextY = y + size * Math.sin(angle); + ctx.lineTo(nextX, nextY); + } + ctx.closePath(); + ctx.stroke(); + }; + + const drawCanvas = async () => { + // Load all images + const logo = await loadImage('/MindVaultLogoTransparentHD.svg'); + const icons = { + equality: await loadImage('/equality-icon.svg'), + market: await loadImage('/market-icon.svg'), + nation: await loadImage('/nation-icon.svg'), + globe: await loadImage('/globe-icon.svg'), + authority: await loadImage('/authority-icon.svg'), + liberty: await loadImage('/liberty-icon.svg'), + tradition: await loadImage('/tradition-icon.svg'), + progress: await loadImage('/progress-icon.svg'), + }; + + // Background gradient + const bgGradient = ctx.createRadialGradient( + canvas.width / 2, canvas.height / 2, 0, + canvas.width / 2, canvas.height / 2, canvas.height + ); + bgGradient.addColorStop(0, '#0C1E1E'); + bgGradient.addColorStop(1, '#081616'); + ctx.fillStyle = bgGradient; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + // Draw hex pattern + for (let i = 0; i < canvas.width; i += 60) { + for (let j = 0; j < canvas.height; j += 52) { + drawHexPattern(i, j); + } + } + + // Main panel + const gradient = ctx.createLinearGradient(25, 100, canvas.width - 80, 100); + gradient.addColorStop(0, '#1E3232'); + gradient.addColorStop(1, '#243C3C'); + ctx.fillStyle = gradient; + roundRect((canvas.width - (canvas.width - 80)) / 2, 100, canvas.width - 80, 360, 30); + ctx.fill(); + + // Border glow + ctx.shadowColor = 'rgba(64, 224, 208, 0.15)'; + ctx.shadowBlur = 15; + ctx.strokeStyle = 'rgba(64, 224, 208, 0.2)'; + ctx.lineWidth = 2; + roundRect((canvas.width - (canvas.width - 80)) / 2, 100, canvas.width - 80, 360, 30); + ctx.stroke(); + ctx.shadowBlur = 0; + + // Logo + drawIcon(logo, canvas.width / 2, 180, 120); + + // Title text + ctx.shadowColor = 'rgba(0, 0, 0, 0.4)'; + ctx.shadowBlur = 15; + ctx.fillStyle = '#E5E7EB'; + ctx.textAlign = 'center'; + ctx.font = 'bold 76px Inter'; + ctx.fillText('MindVault', canvas.width / 2, 320); + ctx.fillText('Political Compass', canvas.width / 2, 400); + + // Subtitle + ctx.font = '32px Inter'; + ctx.fillText('Discover Your Political Identity', canvas.width / 2, 510); + + // Axis drawer + const drawAxis = ( + label: string, + leftLabel: string, + rightLabel: string, + rightValue: number, + y: number, + leftColor: string, + rightColor: string, + leftIcon: HTMLImageElement, + rightIcon: HTMLImageElement + ) => { + const barWidth = canvas.width - 160; + const barHeight = 80; + const x = (canvas.width - barWidth) / 2; + const PADDING = 20; + const ICON_SIZE = 50; + + // Label + ctx.shadowColor = 'rgba(0, 0, 0, 0.3)'; + ctx.shadowBlur = 10; + ctx.fillStyle = '#E5E7EB'; + ctx.font = 'bold 36px Inter'; + ctx.textAlign = 'center'; + ctx.fillText(label, canvas.width / 2, y - 35); + ctx.shadowBlur = 0; + + // Background bar + ctx.fillStyle = '#1A2C2C'; + roundRect(x, y, barWidth, barHeight, barHeight / 2); + ctx.fill(); + + const leftValue = 100 - rightValue; + const meetingPoint = x + (barWidth * leftValue / 100); + + // Left gradient + if (leftValue > 0) { + const leftGradient = ctx.createLinearGradient(x, y, meetingPoint, y); + leftGradient.addColorStop(0, '#FF9B45'); + leftGradient.addColorStop(1, '#E69A45'); + ctx.fillStyle = leftGradient; + roundRect(x, y, meetingPoint - x, barHeight, barHeight / 2); + ctx.fill(); + + if (leftValue >= 10) { + ctx.shadowColor = 'rgba(255, 255, 255, 0.3)'; + ctx.shadowBlur = 10; + ctx.fillStyle = '#0C1E1E'; + ctx.font = 'bold 28px Inter'; + ctx.textAlign = 'left'; + ctx.fillText(`${leftValue}%`, x + PADDING, y + barHeight / 2 + 10); + ctx.shadowBlur = 0; + } + } + + // Right gradient + if (rightValue > 0) { + const rightEnd = x + barWidth; + const rightGradient = ctx.createLinearGradient(meetingPoint, y, rightEnd, y); + rightGradient.addColorStop(0, '#4BAA9E'); + rightGradient.addColorStop(1, '#40E0D0'); + ctx.fillStyle = rightGradient; + roundRect(meetingPoint, y, rightEnd - meetingPoint, barHeight, barHeight / 2); + ctx.fill(); + + if (rightValue >= 10) { + ctx.shadowColor = 'rgba(255, 255, 255, 0.3)'; + ctx.shadowBlur = 10; + ctx.fillStyle = '#243C3C'; + ctx.font = 'bold 28px Inter'; + ctx.textAlign = 'right'; + ctx.fillText(`${rightValue}%`, rightEnd - PADDING, y + barHeight / 2 + 10); + ctx.shadowBlur = 0; + } + } + + // Indicator + ctx.shadowColor = 'rgba(255, 255, 255, 0.6)'; + ctx.shadowBlur = 15; + ctx.fillStyle = '#FFFFFF'; + ctx.beginPath(); + ctx.arc(meetingPoint, y + barHeight / 2, 8, 0, Math.PI * 2); + ctx.fill(); + ctx.shadowBlur = 0; + + // Icons + ctx.shadowColor = 'rgba(255, 255, 255, 0.2)'; + ctx.shadowBlur = 10; + drawIcon(leftIcon, x + 50, y - 35, ICON_SIZE); + drawIcon(rightIcon, x + barWidth - 50, y - 35, ICON_SIZE); + ctx.shadowBlur = 0; + + // Labels + ctx.fillStyle = '#E5E7EB'; + ctx.font = 'bold 28px Inter'; + ctx.textAlign = 'left'; + ctx.fillText(leftLabel, x, y + barHeight + 35); + ctx.textAlign = 'right'; + ctx.fillText(rightLabel, x + barWidth, y + barHeight + 35); + }; + + // Draw the four axes + drawAxis('Economic Axis', 'Equality', 'Markets', econ, 640, '#FF9B45', '#40E0D0', icons.equality, icons.market); + drawAxis('Diplomatic Axis', 'Nation', 'Globe', dipl, 840, '#FFB347', '#45D1C5', icons.nation, icons.globe); + drawAxis('Government Axis', 'Authority', 'Liberty', govt, 1040, '#FFA500', '#48D1C0', icons.authority, icons.liberty); + drawAxis('Societal Axis', 'Tradition', 'Progress', scty, 1240, '#FF8C00', '#43E0D0', icons.tradition, icons.progress); + + // Closest match box + ctx.shadowColor = 'rgba(64, 224, 208, 0.2)'; + ctx.shadowBlur = 20; + const matchGradient = ctx.createLinearGradient(40, 1400, canvas.width - 40, 1400); + matchGradient.addColorStop(0, '#1E3232'); + matchGradient.addColorStop(1, '#243C3C'); + ctx.fillStyle = matchGradient; + roundRect(40, 1410, canvas.width - 80, 290, 30); + ctx.fill(); + + ctx.strokeStyle = 'rgba(64, 224, 208, 0.2)'; + ctx.lineWidth = 2; + roundRect(40, 1410, canvas.width - 80, 290, 30); + ctx.stroke(); + ctx.shadowBlur = 0; + + // Match text + const textGradient = ctx.createLinearGradient( + canvas.width / 2 - 200, 1580, + canvas.width / 2 + 200, 1580 + ); + textGradient.addColorStop(0, '#E5E7EB'); + textGradient.addColorStop(0.5, '#FFFFFF'); + textGradient.addColorStop(1, '#E5E7EB'); + + ctx.fillStyle = '#D1D5DB'; + ctx.font = '36px Inter'; + ctx.textAlign = 'center'; + ctx.fillText('Your Closest Match', canvas.width / 2, 1480); + + ctx.shadowColor = 'rgba(255, 255, 255, 0.2)'; + ctx.shadowBlur = 15; + ctx.fillStyle = textGradient; + ctx.font = 'bold 64px Inter'; + ctx.fillText(closestMatch, canvas.width / 2, 1590); + ctx.shadowBlur = 0; + + // Footer + ctx.fillStyle = '#D1D5DB'; + ctx.font = '32px Inter'; + ctx.fillText('Tag @MindVault & share your results!', canvas.width / 2, 1760); + + // CTA button + ctx.shadowColor = 'rgba(0, 0, 0, 0.3)'; + ctx.shadowBlur = 15; + ctx.fillStyle = "#4BAA9E"; + roundRect(canvas.width / 2 - 350, 1810, 700, 60, 30); + ctx.fill(); + + ctx.shadowColor = 'rgba(255, 255, 255, 0.2)'; + ctx.shadowBlur = 10; + ctx.fillStyle = '#FFFFFF'; + ctx.font = 'bold 36px Inter'; + ctx.fillText('Get the full experience on WorldApp', canvas.width / 2, 1850); + ctx.shadowBlur = 0; + }; + + drawCanvas().catch(console.error); + }, [econ, dipl, govt, scty, closestMatch]); + + return ( + { + canvasRef.current = node; + if (typeof ref === 'function') { + ref(node); + } else if (ref) { + ref.current = node; + } + }} + width={1080} + height={1920} + className="mx-auto w-full h-auto" + /> + ); +}); + +export default ResultsCanvas; From 0eea3565016b98c74c01cc9ec4ab96eb63f0f74b Mon Sep 17 00:00:00 2001 From: Lazarus Date: Wed, 5 Feb 2025 04:09:39 -0600 Subject: [PATCH 2/5] fix: made both insights modals follow the same theme --- frontend/src/app/insights/page.tsx | 98 +++++++++++++++++------------- 1 file changed, 57 insertions(+), 41 deletions(-) diff --git a/frontend/src/app/insights/page.tsx b/frontend/src/app/insights/page.tsx index 729a26c..71120d2 100644 --- a/frontend/src/app/insights/page.tsx +++ b/frontend/src/app/insights/page.tsx @@ -124,6 +124,11 @@ export default function InsightsPage() { setIsShareModalOpen(true); }; + const handleShareAnalysis = () => { + setIsModalOpen(false); + setIsShareModalOpen(true); + }; + const downloadImage = () => { if (!canvasRef.current) return; @@ -342,69 +347,80 @@ export default function InsightsPage() { onClick={() => setIsModalOpen(false)} > e.stopPropagation()} > - - - {isProUser ? ( - + +
+ +

+ {isProUser ? 'Advanced Ideological Analysis' : 'Unlock Advanced Insights'} +

+
-
-

+

+ {isProUser ? ( +
+

{fullAnalysis}

- - ) : ( - -

- Unlock Advanced Insights -

+ ) : ( +

Dive deeper into your ideological profile with Awaken Pro. Get comprehensive analysis and personalized insights.

{ - router.push('/awaken-pro'); - }} + onClick={() => router.push('/awaken-pro')} className="transform transition-all duration-300 hover:scale-105" > Upgrade to Pro
- +
+ )} +
+ + {isProUser && ( +
+ + + + + Share Analysis + +
)} From d9b21c17f52e2307ead999597744523c08172daf Mon Sep 17 00:00:00 2001 From: Lazarus Date: Thu, 6 Feb 2025 19:13:05 -0600 Subject: [PATCH 3/5] feat: added Instagram sharing --- frontend/src/app/insights/page.tsx | 53 +++++++++++++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) diff --git a/frontend/src/app/insights/page.tsx b/frontend/src/app/insights/page.tsx index 71120d2..1728f7b 100644 --- a/frontend/src/app/insights/page.tsx +++ b/frontend/src/app/insights/page.tsx @@ -142,6 +142,57 @@ export default function InsightsPage() { document.body.removeChild(link); }; + const handleInstagramShare = async () => { + if (!canvasRef.current) return; + + try { + // Convert canvas to Blob + const blob = await new Promise((resolve, reject) => { + canvasRef.current?.toBlob((blob) => { + blob ? resolve(blob) : reject(new Error("Canvas conversion failed")); + }, 'image/png'); + }); + const file = new File([blob], 'results.png', { type: 'image/png' }); + + // Use native share if available + if (navigator.share && navigator.canShare && navigator.canShare({ files: [file] })) { + try { + await navigator.share({ + files: [file], + title: 'My Political Compass Results', + text: 'Check out my political compass results!' + }); + return; + } catch (error) { + console.error('Error with native sharing:', error); + } + } + + // Fallback: share via Instagram Stories URL scheme + const dataUrl = canvasRef.current.toDataURL('image/png'); + const encodedImage = encodeURIComponent(dataUrl); + const instagramUrl = `instagram-stories://share?backgroundImage=${encodedImage}&backgroundTopColor=%23000000&backgroundBottomColor=%23000000`; + window.location.href = instagramUrl; + + // Alert if Instagram doesn't open automatically + setTimeout(() => { + alert('If Instagram did not open automatically, please open Instagram and use the image from your camera roll to share to your story.'); + }, 2500); + + // Final fallback: download the image + 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 sharing to Instagram:', error); + alert('Unable to share directly to Instagram. The image has been downloaded to your device – you can manually share it to your Instagram story.'); + } + }; + if (loading) { return } @@ -317,7 +368,7 @@ export default function InsightsPage() { console.log('Share functionality')} + onClick={handleInstagramShare} className="flex-1 py-3 text-sm bg-[#E36C59] flex items-center justify-center gap-2" > From 0206f1d603e6752b630d3c69ed3b07fdbef94ce5 Mon Sep 17 00:00:00 2001 From: Lazarus Date: Fri, 7 Feb 2025 16:01:54 -0600 Subject: [PATCH 4/5] fix: fixes to format and lint --- frontend/src/app/insights/page.tsx | 299 ++++++++++++++++------------- 1 file changed, 161 insertions(+), 138 deletions(-) diff --git a/frontend/src/app/insights/page.tsx b/frontend/src/app/insights/page.tsx index 1728f7b..8dc7f4b 100644 --- a/frontend/src/app/insights/page.tsx +++ b/frontend/src/app/insights/page.tsx @@ -1,11 +1,11 @@ -"use client"; +'use client'; import React, { useEffect, useState, useRef } from 'react'; import { InsightResultCard } from '@/components/ui/InsightResultCard'; import { FilledButton } from '@/components/ui/FilledButton'; import { useSearchParams, useRouter } from 'next/navigation'; import { motion } from 'framer-motion'; -import { LoadingSpinner } from "@/components/ui/LoadingSpinner"; +import { LoadingSpinner } from '@/components/ui/LoadingSpinner'; import { BookOpen } from 'lucide-react'; import { cn } from '@/lib/utils'; import ResultsCanvas from '@/components/Canvas'; @@ -38,7 +38,7 @@ export default function InsightsPage() { const [publicFigure, setPublicFigure] = useState(''); const canvasRef = useRef(null); - const testId = searchParams.get('testId') + const testId = searchParams.get('testId'); const fetchInsights = async () => { setLoading(true); @@ -99,11 +99,13 @@ export default function InsightsPage() { const deepSeekData = await deepSeekResponse.json(); setFullAnalysis(deepSeekData.analysis); } else { - console.error('Error fetching DeepSeek analysis:', deepSeekResponse.statusText); + console.error( + 'Error fetching DeepSeek analysis:', + deepSeekResponse.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.'); @@ -135,7 +137,7 @@ export default function InsightsPage() { const canvas = canvasRef.current; const dataUrl = canvas.toDataURL('image/png'); const link = document.createElement('a'); - link.download = `results.png`; + link.download = 'results.png'; link.href = dataUrl; document.body.appendChild(link); link.click(); @@ -144,41 +146,43 @@ export default function InsightsPage() { const handleInstagramShare = async () => { if (!canvasRef.current) return; - + try { // Convert canvas to Blob const blob = await new Promise((resolve, reject) => { - canvasRef.current?.toBlob((blob) => { - blob ? resolve(blob) : reject(new Error("Canvas conversion failed")); + canvasRef.current?.toBlob((b) => { + b ? resolve(b) : reject(new Error('Canvas conversion failed')); }, 'image/png'); }); const file = new File([blob], 'results.png', { type: 'image/png' }); - + // Use native share if available if (navigator.share && navigator.canShare && navigator.canShare({ files: [file] })) { try { await navigator.share({ files: [file], title: 'My Political Compass Results', - text: 'Check out my political compass results!' + text: 'Check out my political compass results!', }); return; } catch (error) { console.error('Error with native sharing:', error); } } - + // Fallback: share via Instagram Stories URL scheme const dataUrl = canvasRef.current.toDataURL('image/png'); const encodedImage = encodeURIComponent(dataUrl); const instagramUrl = `instagram-stories://share?backgroundImage=${encodedImage}&backgroundTopColor=%23000000&backgroundBottomColor=%23000000`; window.location.href = instagramUrl; - + // Alert if Instagram doesn't open automatically setTimeout(() => { - alert('If Instagram did not open automatically, please open Instagram and use the image from your camera roll to share to your story.'); + alert( + 'If Instagram did not open automatically, please open Instagram and use the image from your camera roll to share to your story.' + ); }, 2500); - + // Final fallback: download the image const link = document.createElement('a'); link.download = 'results.png'; @@ -186,22 +190,23 @@ export default function InsightsPage() { document.body.appendChild(link); link.click(); document.body.removeChild(link); - } catch (error) { console.error('Error sharing to Instagram:', error); - alert('Unable to share directly to Instagram. The image has been downloaded to your device – you can manually share it to your Instagram story.'); + alert( + 'Unable to share directly to Instagram. The image has been downloaded to your device – you can manually share it to your Instagram story.' + ); } }; - + if (loading) { - return + return ; } return (
- Explore how your values align across key ideological dimensions.

- - {isProUser ? 'Advanced Insights' : 'Unlock Advanced Insights'} @@ -249,7 +254,7 @@ export default function InsightsPage() {
- )) ) : ( - )} - {/* Added Share Button */} + {/* Share Button */}
- {isShareModalOpen && ( - setIsShareModalOpen(false)} - > - e.stopPropagation()} - > -
- -
- -

- Share Your Results -

-
+
-
-
- -
-
+
+ +

+ Share Your Results +

+
-
- +
+ +
+
+ +
+ - - - - Download Image - - + + + Download Image + + - - - - Share Link - -
- - - )} + > + + + + Share Link + +
+ + + )} - {/* Existing Advanced Insights Modal */} {isModalOpen && ( - setIsModalOpen(false)} > - e.stopPropagation()} >
- +
) : ( -
-

- Dive deeper into your ideological profile with Awaken Pro. Get comprehensive analysis and personalized insights. -

-
- router.push('/awaken-pro')} - className="transform transition-all duration-300 hover:scale-105" - > - Upgrade to Pro - +
+

+ Dive deeper into your ideological profile with Awaken Pro. Get + comprehensive analysis and personalized insights. +

+
+ router.push('/awaken-pro')} + className="transform transition-all duration-300 hover:scale-105" + > + Upgrade to Pro + +
-
)}
@@ -460,14 +478,19 @@ export default function InsightsPage() { className="flex-1 py-3 text-sm bg-[#E36C59] flex items-center justify-center gap-2" > - - + Share Analysis @@ -478,4 +501,4 @@ export default function InsightsPage() { )}
); -} \ No newline at end of file +} From c2dc4d6254a0d6a21958c9b8fd787c4b0d8ce454 Mon Sep 17 00:00:00 2001 From: Lazarus Date: Sun, 9 Feb 2025 16:59:44 -0600 Subject: [PATCH 5/5] fix: better error handling and small tweaks --- frontend/src/app/api/public-figures/route.ts | 9 ++--- frontend/src/app/insights/page.tsx | 39 +++++++++++++------- 2 files changed, 29 insertions(+), 19 deletions(-) diff --git a/frontend/src/app/api/public-figures/route.ts b/frontend/src/app/api/public-figures/route.ts index 348f8d0..d4645d2 100644 --- a/frontend/src/app/api/public-figures/route.ts +++ b/frontend/src/app/api/public-figures/route.ts @@ -82,9 +82,6 @@ function calculateSimilarity(userScores: UserScores, celebrityScores: UserScores * celebrity: * type: string * example: "Elon Musk" - * similarity: - * type: number - * example: 85.5 * 400: * description: Invalid scores provided * 401: @@ -173,12 +170,12 @@ export async function POST(request: Request) { // Update or create PublicFigurePerUser record await xata.db.PublicFigurePerUser.create({ user: user.xata_id, - public_figure: bestMatch.xata_id, + celebrity: bestMatch.xata_id, celebrity_user_id: nextCelebrityId }); return NextResponse.json({ - public_figure: bestMatch.name + celebrity: bestMatch.name }); } catch (error) { @@ -208,7 +205,7 @@ export async function POST(request: Request) { * schema: * type: object * properties: - * public_figure: + * celebrity: * type: string * example: "Elon Musk" * 401: diff --git a/frontend/src/app/insights/page.tsx b/frontend/src/app/insights/page.tsx index 88b49a0..8ea39a9 100644 --- a/frontend/src/app/insights/page.tsx +++ b/frontend/src/app/insights/page.tsx @@ -68,16 +68,20 @@ export default function InsightsPage() { // Get scores from database const scoresResponse = await fetch(`/api/tests/${testId}/progress`); + if (!scoresResponse.ok) { + throw new Error("Failed to fetch scores"); + } const scoresData = await scoresResponse.json(); const { scores } = scoresData; setScores(scoresData.scores); // Get public figure match const figureResponse = await fetch('/api/public-figures'); - if (figureResponse.ok) { - const figureData = await figureResponse.json(); - setPublicFigure(figureData.celebrity || 'Unknown Match'); + if (!figureResponse.ok) { + throw new Error("Failed to fetch public figure match"); } + const figureData = await figureResponse.json(); + setPublicFigure(figureData.celebrity || 'Unknown Match'); // Call DeepSeek API for full analysis if (isProUser) { @@ -133,15 +137,20 @@ export default function InsightsPage() { 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); + 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 () => { @@ -360,8 +369,10 @@ export default function InsightsPage() {