Fix dashboard share card by replacing htmlToImage with dedicated OG API route

This commit is contained in:
2026-09-22 17:46:57 +05:30
parent 6e8df1841e
commit caebbaee92
2 changed files with 121 additions and 11 deletions

View File

@@ -0,0 +1,96 @@
import { ImageResponse } from '@vercel/og';
import { NextRequest } from 'next/server';
export const runtime = 'edge';
export async function GET(req: NextRequest) {
try {
const { searchParams } = new URL(req.url);
// Read metrics
const score = searchParams.get('score') || '0 / 0';
const accuracy = searchParams.get('accuracy') || '0%';
const qs = searchParams.get('qs') || '0';
const rank = searchParams.get('rank') || '#--';
// Read user info
const name = searchParams.get('name') || 'Student';
const target = searchParams.get('target') || 'JEE';
const plan = searchParams.get('plan') || 'Free';
return new ImageResponse(
(
<div
style={{
height: '100%',
width: '100%',
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
backgroundColor: '#0a0a0a',
color: '#ffffff',
fontFamily: 'sans-serif',
padding: '60px',
border: '8px solid #222',
}}
>
{/* Header */}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', width: '100%' }}>
<div style={{ display: 'flex', alignItems: 'center' }}>
<div style={{ width: 40, height: 40, backgroundColor: '#06b6d4', borderRadius: 8, marginRight: 16 }} />
<span style={{ fontSize: 32, fontWeight: 900, letterSpacing: '0.05em' }}>PRAGATIVEER</span>
</div>
<div style={{ fontSize: 20, fontWeight: 700, color: '#06b6d4', backgroundColor: 'rgba(6,182,212,0.1)', padding: '8px 16px', borderRadius: 20, border: '1px solid rgba(6,182,212,0.2)' }}>
Performance Overview
</div>
</div>
{/* Main Content */}
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', width: '100%', marginTop: 20 }}>
<h3 style={{ fontSize: 48, fontWeight: 900, marginBottom: 40 }}>Dashboard Stats</h3>
{/* 2x2 Grid */}
<div style={{ display: 'flex', flexWrap: 'wrap', width: '600px', justifyContent: 'center', gap: '24px' }}>
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', width: '280px', height: '160px', backgroundColor: 'rgba(255,255,255,0.05)', borderRadius: 24, border: '1px solid rgba(255,255,255,0.1)' }}>
<span style={{ fontSize: 48, fontWeight: 900, color: '#f59e0b', marginBottom: 8 }}>{score}</span>
<span style={{ fontSize: 16, color: 'rgba(255,255,255,0.7)', textTransform: 'uppercase', fontWeight: 800, letterSpacing: '0.05em' }}>Today's Score</span>
</div>
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', width: '280px', height: '160px', backgroundColor: 'rgba(255,255,255,0.05)', borderRadius: 24, border: '1px solid rgba(255,255,255,0.1)' }}>
<span style={{ fontSize: 48, fontWeight: 900, color: '#4ade80', marginBottom: 8 }}>{accuracy}</span>
<span style={{ fontSize: 16, color: 'rgba(255,255,255,0.7)', textTransform: 'uppercase', fontWeight: 800, letterSpacing: '0.05em' }}>Accuracy</span>
</div>
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', width: '280px', height: '160px', backgroundColor: 'rgba(255,255,255,0.05)', borderRadius: 24, border: '1px solid rgba(255,255,255,0.1)' }}>
<span style={{ fontSize: 48, fontWeight: 900, color: '#22d3ee', marginBottom: 8 }}>{qs}</span>
<span style={{ fontSize: 16, color: 'rgba(255,255,255,0.7)', textTransform: 'uppercase', fontWeight: 800, letterSpacing: '0.05em' }}>Qs Attempted</span>
</div>
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', width: '280px', height: '160px', backgroundColor: 'rgba(255,255,255,0.05)', borderRadius: 24, border: '1px solid rgba(255,255,255,0.1)' }}>
<span style={{ fontSize: 48, fontWeight: 900, color: '#c084fc', marginBottom: 8 }}>{rank}</span>
<span style={{ fontSize: 16, color: 'rgba(255,255,255,0.7)', textTransform: 'uppercase', fontWeight: 800, letterSpacing: '0.05em' }}>City Rank</span>
</div>
</div>
</div>
{/* Footer */}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-end', width: '100%', marginTop: 20 }}>
<div style={{ display: 'flex', flexDirection: 'column' }}>
<div style={{ fontSize: 24, fontWeight: 800 }}>{name}</div>
<div style={{ fontSize: 18, color: 'rgba(255,255,255,0.7)', marginTop: 8 }}>{plan} Plan</div>
<div style={{ fontSize: 18, color: '#06b6d4', marginTop: 4 }}>{target} Aspirant</div>
</div>
<div style={{ fontSize: 18, color: 'rgba(255,255,255,0.5)', letterSpacing: '0.05em' }}>
contact@pragativeer.com
</div>
</div>
</div>
),
{
width: 1200,
height: 630,
}
);
} catch (e: any) {
return new Response(`Failed to generate image`, {
status: 500,
});
}
}

View File

@@ -5,7 +5,6 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/com
import { Badge } from "@/components/ui/badge" import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { BrainCircuit, Play, ChevronRight, Activity, Target, CheckCircle2, AlertTriangle, TrendingUp, Calendar, Zap, LayoutDashboard, Share2, Trophy, Lock, ArrowRight, Clock } from "lucide-react" import { BrainCircuit, Play, ChevronRight, Activity, Target, CheckCircle2, AlertTriangle, TrendingUp, Calendar, Zap, LayoutDashboard, Share2, Trophy, Lock, ArrowRight, Clock } from "lucide-react"
import * as htmlToImage from 'html-to-image'
import { MistakeHeatmap } from "@/components/AI/MistakeHeatmap" import { MistakeHeatmap } from "@/components/AI/MistakeHeatmap"
import { AdaptiveStudyPlan } from "@/components/AI/AdaptiveStudyPlan" import { AdaptiveStudyPlan } from "@/components/AI/AdaptiveStudyPlan"
import { useActiveUser } from "@/hooks/useActiveUser" import { useActiveUser } from "@/hooks/useActiveUser"
@@ -33,18 +32,33 @@ export default function DashboardPage() {
}, [user?.id]) }, [user?.id])
const handleExport = async () => { const handleExport = async () => {
if (progressRef.current) {
try { try {
const dataUrl = await htmlToImage.toPng(progressRef.current, { backgroundColor: '#09090b' }) const score = isNew ? "0 / 0" : `${userStats?.today_score || 0} / ${userStats?.today_max_score || 60}`
const accuracy = isNew ? "0%" : `${userStats?.overall_accuracy || 0}%`
const qs = isNew ? "0" : (userStats?.total_questions_attempted || 0).toLocaleString()
const rank = isNew ? "#--" : `#${userStats?.city_rank || 1} / ${userStats?.city_total || 50}`
const target = user?.target_exam === "NEET" ? "NEET" : "JEE"
const plan = user?.tier === "Al_Coach" ? "AI Coach" : user?.tier || "Free"
const name = user?.name || "Student"
const url = `/api/og/dashboard?score=${encodeURIComponent(score)}&accuracy=${encodeURIComponent(accuracy)}&qs=${encodeURIComponent(qs)}&rank=${encodeURIComponent(rank)}&name=${encodeURIComponent(name)}&target=${target}&plan=${encodeURIComponent(plan)}`
const res = await fetch(url)
const blob = await res.blob()
const blobUrl = window.URL.createObjectURL(blob)
const link = document.createElement('a') const link = document.createElement('a')
link.download = 'pragativeer-progress.png' link.download = 'pragativeer-dashboard.png'
link.href = dataUrl link.href = blobUrl
document.body.appendChild(link)
link.click() link.click()
document.body.removeChild(link)
window.URL.revokeObjectURL(blobUrl)
} catch (err) { } catch (err) {
console.error("Failed to export image", err) console.error("Failed to export image", err)
} }
} }
}
if (loading) { if (loading) {
return ( return (