Fix dashboard share card by replacing htmlToImage with dedicated OG API route
This commit is contained in:
96
src/app/api/og/dashboard/route.tsx
Normal file
96
src/app/api/og/dashboard/route.tsx
Normal 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,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,6 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/com
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
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 * as htmlToImage from 'html-to-image'
|
||||
import { MistakeHeatmap } from "@/components/AI/MistakeHeatmap"
|
||||
import { AdaptiveStudyPlan } from "@/components/AI/AdaptiveStudyPlan"
|
||||
import { useActiveUser } from "@/hooks/useActiveUser"
|
||||
@@ -33,16 +32,31 @@ export default function DashboardPage() {
|
||||
}, [user?.id])
|
||||
|
||||
const handleExport = async () => {
|
||||
if (progressRef.current) {
|
||||
try {
|
||||
const dataUrl = await htmlToImage.toPng(progressRef.current, { backgroundColor: '#09090b' })
|
||||
const link = document.createElement('a')
|
||||
link.download = 'pragativeer-progress.png'
|
||||
link.href = dataUrl
|
||||
link.click()
|
||||
} catch (err) {
|
||||
console.error("Failed to export image", err)
|
||||
}
|
||||
try {
|
||||
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')
|
||||
link.download = 'pragativeer-dashboard.png'
|
||||
link.href = blobUrl
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
window.URL.revokeObjectURL(blobUrl)
|
||||
} catch (err) {
|
||||
console.error("Failed to export image", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user