diff --git a/app/compare/CompareClient.tsx b/app/compare/CompareClient.tsx index a45dc5c02..c18928eb7 100644 --- a/app/compare/CompareClient.tsx +++ b/app/compare/CompareClient.tsx @@ -1,7 +1,8 @@ 'use client'; -import { useState, useEffect, useCallback } from 'react'; +import { useState, useEffect, useCallback, useRef } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; +import TopRivalriesTicker from '@/components/TopRivalriesTicker'; import { Radar, RadarChart, @@ -33,7 +34,16 @@ import { Code2, GitPullRequest, CircleDot, + Cpu, + RefreshCw, + Component, + Users as UsersIcon, + CalendarDays, + Tent, + Camera, + Share2, } from 'lucide-react'; +import html2canvas from 'html2canvas'; /* ── types ────────────────────────────────────────────────────────────── */ @@ -202,17 +212,86 @@ function StatBattle({ ); } +/* ── helper: developer persona ────────────────────────────────────────── */ + +function getDeveloperPersona(user: CompareUserData) { + const { stats, profile, activity } = user; + + let additions = 0; + let deletions = 0; + activity.forEach((a) => { + additions += a.locAdditions || 0; + deletions += a.locDeletions || 0; + }); + + // Determine Persona based on stats + if (stats.currentStreak > 30 || stats.totalContributions > 2000) { + return { + name: 'The Machine', + icon: Cpu, + color: 'from-purple-500 to-indigo-500', + text: 'text-purple-400', + border: 'border-purple-500/30', + shadow: 'shadow-[0_0_15px_rgba(168,85,247,0.4)]', + }; + } + if (deletions > 0 && deletions > additions * 1.5) { + return { + name: 'The Refactorer', + icon: RefreshCw, + color: 'from-rose-500 to-orange-500', + text: 'text-rose-400', + border: 'border-rose-500/30', + shadow: 'shadow-[0_0_15px_rgba(244,63,94,0.4)]', + }; + } + if (profile.stats.stars > 200) { + return { + name: 'The Architect', + icon: Component, + color: 'from-amber-400 to-yellow-600', + text: 'text-amber-400', + border: 'border-amber-500/30', + shadow: 'shadow-[0_0_15px_rgba(251,191,36,0.4)]', + }; + } + if ((stats.totalPRs || 0) > 50) { + return { + name: 'Team Player', + icon: UsersIcon, + color: 'from-blue-400 to-cyan-500', + text: 'text-blue-400', + border: 'border-blue-500/30', + shadow: 'shadow-[0_0_15px_rgba(59,130,246,0.4)]', + }; + } + if (stats.peakStreak > 14) { + return { + name: 'Consistent Coder', + icon: CalendarDays, + color: 'from-emerald-400 to-teal-500', + text: 'text-emerald-400', + border: 'border-emerald-500/30', + shadow: 'shadow-[0_0_15px_rgba(16,185,129,0.4)]', + }; + } + return { + name: 'Weekend Warrior', + icon: Tent, + color: 'from-zinc-400 to-gray-600', + text: 'text-zinc-400', + border: 'border-zinc-500/30', + shadow: 'shadow-[0_0_15px_rgba(161,161,170,0.4)]', + }; +} + /* ── helper: profile card ─────────────────────────────────────────────── */ -function CompareProfileCard({ - profile, - stats, - side, -}: { - profile: UserProfile; - stats: UserStats; - side: 'left' | 'right'; -}) { +function CompareProfileCard({ user, side }: { user: CompareUserData; side: 'left' | 'right' }) { + const { profile, stats } = user; + const persona = getDeveloperPersona(user); + const PersonaIcon = persona.icon; + return ( {profile.isPro && ( - + PRO )} -

- {profile.name} -

+

{profile.name}

+ + {/* Animated Developer Persona Badge */} + + {/* Subtle background glow that pulses */} + + + + {persona.name} + + +

@{profile.username}

{profile.bio} @@ -791,9 +896,41 @@ export default function CompareClient() { const [user1, setUser1] = useState(searchParams.get('user1') || ''); const [user2, setUser2] = useState(searchParams.get('user2') || ''); - const [loading, setLoading] = useState(false); const [error, setError] = useState(''); + const [loading, setLoading] = useState(false); const [data, setData] = useState(null); + const [isExporting, setIsExporting] = useState(false); + + const captureRef = useRef(null); + + const handleDownloadCard = async () => { + if (!captureRef.current || !data) return; + setIsExporting(true); + + try { + // Small delay to allow export overlay/scanning UI to render first + await new Promise((res) => setTimeout(res, 300)); + + const canvas = await html2canvas(captureRef.current, { + scale: 2, // higher resolution + backgroundColor: document.documentElement.classList.contains('dark') + ? '#000000' + : '#ffffff', + useCORS: true, + logging: false, + }); + + const image = canvas.toDataURL('image/png'); + const link = document.createElement('a'); + link.href = image; + link.download = `commitpulse-battle-${data.user1.profile.username}-vs-${data.user2.profile.username}.png`; + link.click(); + } catch (err) { + console.error('Failed to export image', err); + } finally { + setIsExporting(false); + } + }; const BASE_URL = typeof window !== 'undefined' ? window.location.origin : 'https://commitpulse.vercel.app'; @@ -871,275 +1008,338 @@ export default function CompareClient() { } return ( -

-
- {/* Page Header */} - -
- - - Developer Showdown - -
-

- Compare Developers -

-

- Put two GitHub profiles head-to-head. Streaks, contributions, languages — who comes out - on top? -

-
- - {/* Input Form */} - -
-
- - setUser1(e.target.value)} - onKeyDown={(e) => e.key === 'Enter' && handleCompare(user1, user2)} - className="w-full pl-10 pr-4 py-3 rounded-xl border border-black/10 dark:border-[rgba(255,255,255,0.1)] bg-white dark:bg-[#0a0a0a] text-gray-900 dark:text-white text-sm placeholder:text-[#A1A1AA] focus:outline-none focus:border-emerald-500/50 transition-colors" - /> + <> +
+ +
+
+
+ {/* Page Header */} + +
+ + + Developer Showdown +
+

+ Compare Developers +

+

+ Put two GitHub profiles head-to-head. Streaks, contributions, languages — who comes + out on top? +

+
-
- VS -
+ {/* Input Form */} + +
+
+ + setUser1(e.target.value)} + onKeyDown={(e) => e.key === 'Enter' && handleCompare(user1, user2)} + className="w-full pl-10 pr-4 py-3 rounded-xl border border-black/10 dark:border-[rgba(255,255,255,0.1)] bg-white dark:bg-[#0a0a0a] text-gray-900 dark:text-white text-sm placeholder:text-[#A1A1AA] focus:outline-none focus:border-emerald-500/50 transition-colors" + /> +
+ +
+ VS +
-
- - setUser2(e.target.value)} - onKeyDown={(e) => e.key === 'Enter' && handleCompare(user1, user2)} - className="w-full pl-10 pr-4 py-3 rounded-xl border border-black/10 dark:border-[rgba(255,255,255,0.1)] bg-white dark:bg-[#0a0a0a] text-gray-900 dark:text-white text-sm placeholder:text-[#A1A1AA] focus:outline-none focus:border-emerald-500/50 transition-colors" - /> +
+ + setUser2(e.target.value)} + onKeyDown={(e) => e.key === 'Enter' && handleCompare(user1, user2)} + className="w-full pl-10 pr-4 py-3 rounded-xl border border-black/10 dark:border-[rgba(255,255,255,0.1)] bg-white dark:bg-[#0a0a0a] text-gray-900 dark:text-white text-sm placeholder:text-[#A1A1AA] focus:outline-none focus:border-emerald-500/50 transition-colors" + /> +
+ + handleCompare(user1, user2)} + disabled={loading} + className="flex items-center justify-center gap-2 px-6 py-3 rounded-xl bg-black dark:bg-white text-white dark:text-black text-sm font-semibold hover:bg-zinc-800 dark:hover:bg-zinc-100 transition-colors disabled:opacity-50 disabled:cursor-not-allowed" + > + {loading ? : } + {loading ? 'Comparing...' : 'Compare'} +
+ - handleCompare(user1, user2)} - disabled={loading} - className="flex items-center justify-center gap-2 px-6 py-3 rounded-xl bg-black dark:bg-white text-white dark:text-black text-sm font-semibold hover:bg-zinc-800 dark:hover:bg-zinc-100 transition-colors disabled:opacity-50 disabled:cursor-not-allowed" - > - {loading ? : } - {loading ? 'Comparing...' : 'Compare'} - -
-
+ {/* Error */} + + {error && ( + +

{error}

+
+ )} +
+ + {/* Loading */} + {loading && } + + {/* Results */} + + {d1 && d2 && !loading && ( + + {/* Optional: Spotify Wrapped style header only visible in the canvas or just part of the card */} +
+ +
- {/* Error */} - - {error && ( - -

{error}

-
- )} -
+
+ {/* Winner Banner */} + {winner && ( + +
+ + + {winner === 'tie' + ? "It's a Tie! Both developers are evenly matched." + : `@${winner} wins the showdown!`} + +
+
+ )} - {/* Loading */} - {loading && } + {/* Profile Cards */} +
+ - {/* Results */} - - {d1 && d2 && !loading && ( - - {/* Winner Banner */} - {winner && ( - -
- - - {winner === 'tie' - ? "It's a Tie! Both developers are evenly matched." - : `@${winner} wins the showdown!`} - -
-
- )} + {/* VS Divider */} +
+
+ + VS + +
+
- {/* Profile Cards */} -
- + +
- {/* VS Divider */} -
-
- VS + {/* Stats Battle Grid */} +
+

+ Stats Showdown +

+
+ + + + + + + + +
-
- -
+ {/* Coding Habits Showdown */} + - {/* Stats Battle Grid */} -
-

- Stats Showdown -

-
- - - - - - - - + + {/* Developer Skills Radar */} + + + {/* Language Comparison */} + + + {/* Activity Heatmaps */} +
+ {[ + { user: d1, side: 'left' as const }, + { user: d2, side: 'right' as const }, + ].map(({ user, side }) => ( + +

+ {user.profile.username}'s Activity (Last 13 Weeks) +

+ +
+ ))} +
+ + {/* 3D Monolith Embeds */} +
+

+ 3D Monolith Comparison +

+
+ {[d1, d2].map((user) => ( + +
+ + @{user.profile.username} + +
+ {/* eslint-disable-next-line @next/next/no-img-element */} + {`${user.profile.username}'s +
+ ))} +
+
-
- {/* Coding Habits Showdown */} - - - {/* Code Volume Showdown */} - - - {/* Developer Skills Radar */} - - - {/* Language Comparison */} - - - {/* Activity Heatmaps */} -
- {[ - { user: d1, side: 'left' as const }, - { user: d2, side: 'right' as const }, - ].map(({ user, side }) => ( - + -

- {user.profile.username}'s Activity (Last 13 Weeks) -

- -
- ))} -
+ + {isExporting ? : } + + {isExporting ? 'Generating Epic Card...' : 'Export Wrapped Card'} + + {/* Subtle glare effect on hover */} +
+ + - {/* 3D Monolith Embeds */} -
-

- 3D Monolith Comparison -

-
- {[d1, d2].map((user) => ( + {/* Fullscreen Scanner Overlay during export */} + + {isExporting && ( -
- - @{user.profile.username} - -
- {/* eslint-disable-next-line @next/next/no-img-element */} - {`${user.profile.username}'s +

+ Capturing the Showdown... +

- ))} -
-
- - )} - -
-
+ )} + + + )} + +
+
+ ); } diff --git a/app/layout.tsx b/app/layout.tsx index 7250073c5..b72fe23b5 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -7,6 +7,7 @@ import ReturnToTop from '@/components/ReturnToTop'; import type { Metadata } from 'next'; import ScrollRestoration from './components/ScrollRestoration'; import AnimatedCursor from '@/components/AnimatedCursor'; +import KonamiEasterEgg from '@/components/KonamiEasterEgg'; const inter = Inter({ subsets: ['latin'] }); @@ -93,6 +94,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
{children}
+ diff --git a/components/KonamiEasterEgg.tsx b/components/KonamiEasterEgg.tsx new file mode 100644 index 000000000..8a503a8bf --- /dev/null +++ b/components/KonamiEasterEgg.tsx @@ -0,0 +1,353 @@ +'use client'; + +import { useState, useEffect, useCallback, useMemo } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; + +/* ── config ───────────────────────────────────────────────────────────── */ + +const SECRET_CODE = 'commit'; +const DISPLAY_DURATION = 6000; // ms the whole show lasts +const MATRIX_CHAR_COUNT = 80; +const CONFETTI_COUNT = 60; + +/* ── helper: random ───────────────────────────────────────────────────── */ + +function rand(min: number, max: number) { + return Math.random() * (max - min) + min; +} + +const MATRIX_CHARS = + '01アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワヲン'; +const CONFETTI_COLORS = [ + '#10b981', // emerald + '#6366f1', // indigo + '#f59e0b', // amber + '#ec4899', // pink + '#3b82f6', // blue + '#8b5cf6', // violet + '#14b8a6', // teal + '#f43f5e', // rose +]; + +/* ── types for pre-computed data ──────────────────────────────────────── */ + +interface MatrixDropData { + id: number; + left: number; + delay: number; + chars: { char: string; fontSize: number }[]; + duration: number; +} + +interface ConfettiPieceData { + id: number; + delay: number; + color: string; + size: number; + startX: number; + startY: number; + endX: number; + endY: number; + rotation: number; + isCircle: boolean; + duration: number; +} + +/* ── matrix rain column ───────────────────────────────────────────────── */ + +function MatrixDrop({ data }: { data: MatrixDropData }) { + return ( + + {data.chars.map((c, i) => ( + + {c.char} + + ))} + + ); +} + +/* ── confetti particle ────────────────────────────────────────────────── */ + +function ConfettiPiece({ data }: { data: ConfettiPieceData }) { + return ( + + ); +} + +/* ── pre-compute all random data ──────────────────────────────────────── */ + +function generateMatrixDrops(): MatrixDropData[] { + return Array.from({ length: MATRIX_CHAR_COUNT }, (_, i) => { + const len = Math.floor(rand(8, 22)); + return { + id: i, + left: rand(0, 100), + delay: rand(0, 2.5), + duration: rand(2.5, 5), + chars: Array.from({ length: len }, () => ({ + char: MATRIX_CHARS[Math.floor(Math.random() * MATRIX_CHARS.length)], + fontSize: rand(10, 16), + })), + }; + }); +} + +function generateConfettiPieces(): ConfettiPieceData[] { + return Array.from({ length: CONFETTI_COUNT }, (_, i) => { + const startX = rand(20, 80); + const startY = rand(30, 50); + return { + id: i, + delay: rand(0.5, 1.5) + rand(0, 0.3), + color: CONFETTI_COLORS[Math.floor(Math.random() * CONFETTI_COLORS.length)], + size: rand(6, 14), + startX, + startY, + endX: startX + rand(-30, 30), + endY: startY + rand(30, 70), + rotation: rand(0, 720), + isCircle: Math.random() > 0.5, + duration: rand(1.5, 3), + }; + }); +} + +/* ── main component ───────────────────────────────────────────────────── */ + +export default function KonamiEasterEgg() { + const [triggered, setTriggered] = useState(false); + const [buffer, setBuffer] = useState(''); + + // Pre-compute all random data ONCE inside useMemo (called at mount, not during render of children) + const matrixDrops = useMemo(() => generateMatrixDrops(), []); + const confettiPieces = useMemo(() => generateConfettiPieces(), []); + + const handleKeyDown = useCallback( + (e: KeyboardEvent) => { + if (triggered) return; + // Ignore if user is typing in an input/textarea + const tag = (e.target as HTMLElement)?.tagName; + if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return; + + const next = (buffer + e.key.toLowerCase()).slice(-SECRET_CODE.length); + setBuffer(next); + + if (next === SECRET_CODE) { + setTriggered(true); + setBuffer(''); + setTimeout(() => setTriggered(false), DISPLAY_DURATION); + } + }, + [buffer, triggered] + ); + + useEffect(() => { + window.addEventListener('keydown', handleKeyDown); + return () => window.removeEventListener('keydown', handleKeyDown); + }, [handleKeyDown]); + + return ( + + {triggered && ( + + {/* Dark backdrop with subtle green tint */} + + + {/* Matrix rain */} + {matrixDrops.map((drop) => ( + + ))} + + {/* Confetti explosion */} + {confettiPieces.map((piece) => ( + + ))} + + {/* Center message */} +
+ + {/* Glow ring behind text */} + + +
+ {/* Glitch scanlines */} + + + + 🚀 + + + + You Found It! + + + + $ git commit -m + "unlocked_easter_egg" + + + + + + ✦ CommitPulse v2 ✦ Built with 💚 by open-source devs + +
+
+
+ + {/* Top-left binary rain accent */} + + {'010110\n110011\n001101\n101010\n011001\n110100'.split('\n').map((line, i) => ( + + {line} + + ))} + + + {/* Bottom-right hex accent */} + + {'0xDEAD\n0xBEEF\n0xC0DE\n0xCAFE\n0xF00D'.split('\n').map((line, i) => ( + + {line} + + ))} + +
+ )} +
+ ); +} diff --git a/components/TopRivalriesTicker.tsx b/components/TopRivalriesTicker.tsx new file mode 100644 index 000000000..288f3c872 --- /dev/null +++ b/components/TopRivalriesTicker.tsx @@ -0,0 +1,85 @@ +'use client'; + +import { motion } from 'framer-motion'; +import { Flame, Zap, Trophy, Target, Star, Swords } from 'lucide-react'; +import { useRouter } from 'next/navigation'; + +const MOCK_RIVALRIES = [ + { + u1: 'torvalds', + u2: 'gaearon', + label: 'Kernel vs React', + icon: Flame, + color: 'text-orange-500', + }, + { u1: 'rich-harris', u2: 'antfu', label: 'Svelte vs Nuxt', icon: Zap, color: 'text-yellow-400' }, + { u1: 'shadcn', u2: 'pacocoursey', label: 'UI Masters', icon: Target, color: 'text-indigo-400' }, + { u1: 'vercel', u2: 'netlify', label: 'Platform Wars', icon: Trophy, color: 'text-emerald-500' }, + { u1: 'dhh', u2: 'taylorotwell', label: 'Ruby vs PHP', icon: Star, color: 'text-rose-500' }, + { + u1: 'jhasourav07', + u2: 'leerob', + label: 'Rising vs Vet', + icon: Swords, + color: 'text-purple-500', + }, +]; + +export default function TopRivalriesTicker() { + const router = useRouter(); + + const handleRivalryClick = (u1: string, u2: string) => { + // Navigate to comparison and reload data + router.push(`/compare?user1=${encodeURIComponent(u1)}&user2=${encodeURIComponent(u2)}`); + }; + + return ( +
+ {/* Edge Gradients for smooth fade in/out */} +
+
+ + {/* Marquee Content */} + + {/* We map twice to create the infinite seamless loop effect */} + {[...MOCK_RIVALRIES, ...MOCK_RIVALRIES].map((rivalry, idx) => { + const Icon = rivalry.icon; + return ( +
handleRivalryClick(rivalry.u1, rivalry.u2)} + className="group flex items-center gap-3 px-6 cursor-pointer hover:bg-black/5 dark:hover:bg-white/5 rounded-full transition-colors mx-2 py-1.5" + > + +
+ + {rivalry.u1} + + + VS + + + {rivalry.u2} + +
+ + {rivalry.label} + +
+ ); + })} +
+
+ ); +} diff --git a/package-lock.json b/package-lock.json index 299a03b11..22ffd7d61 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "framer-motion": "^12.38.0", "gsap": "^3.15.0", "html-to-image": "^1.11.13", + "html2canvas": "^1.4.1", "jspdf": "^4.2.1", "lightningcss": "^1.32.0", "lucide-react": "^1.17.0", @@ -958,6 +959,111 @@ "fast-glob": "3.3.1" } }, + "node_modules/@next/swc-darwin-arm64": { + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.6.tgz", + "integrity": "sha512-ZJGkkcNfYgrrMkqOdZ7zoLa1TOy0qpcMfk/z4Mh/FKUz40gVO+HNQWqmLxf67Z5WB64DRp0dhEbyHfel+6sJUg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.6.tgz", + "integrity": "sha512-v/YLBHIY132Ced3puBJ7YJKw1lqsCrgcNo2aRJlCEyQrrCeRJlvGlnmxhPxNQI3KE3N1DN5r9TPNPvka3nq5RQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.6.tgz", + "integrity": "sha512-RPOvqlYBbcQjkz9VQQDZ2T2bARIjXZV1KFlt+V2Mr6SW/e4I9fcKsaA0hdyf2FHoTlsV2xnBd5Y912rP/1Ce6w==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.6.tgz", + "integrity": "sha512-URUTu1+dMkxJsPFgm+OeEvq9wf5sujw0EvgYy80TDGHTSLTnIHeqb0Eu8A3sC95IRgjejQL+kC4mw+4yPxiAXA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.6.tgz", + "integrity": "sha512-DOj182mPV8G3UkrayLoREM5YEYI+Dk5wv7Ox9xl1fFibAELEsFD0lDPfHIeILlutMMfdyhlzYPELG3peuKaurw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.6.tgz", + "integrity": "sha512-HKQ5SP/V/ub73UvF7n/zeJlxk2kLmtL7Wzrg4WfmkjmNos5onJ2tKu7yZOPdL18A6Svfn3max29ym+ry7NkK4g==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.6.tgz", + "integrity": "sha512-LZXpTlPyS5v7HhSmnvsLGP3iIYgYOBnc8r8ArlT55sGHV89bR2HlDdBjWQ+PY6SJMmk8TuVGFuxalnP3k/0Dwg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/@next/swc-win32-x64-msvc": { "version": "16.2.6", "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.6.tgz", @@ -1763,7 +1869,7 @@ "version": "19.2.15", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.15.tgz", "integrity": "sha512-eRwcGNHve+E8qtEQSSRl6urh+rFop4v8gm6O8rGv25CodbvFdLjA1vVQ1KkiFE0w0UPOnb8tDiFKL5lp0rtY5Q==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "csstype": "^3.2.2" @@ -2772,7 +2878,6 @@ "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==", "license": "MIT", - "optional": true, "engines": { "node": ">= 0.6.0" } @@ -3159,7 +3264,6 @@ "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz", "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==", "license": "MIT", - "optional": true, "dependencies": { "utrie": "^1.0.2" } @@ -5069,7 +5173,6 @@ "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz", "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==", "license": "MIT", - "optional": true, "dependencies": { "css-line-break": "^2.1.0", "text-segmentation": "^1.0.3" @@ -7434,7 +7537,6 @@ "version": "19.2.6", "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.6.tgz", "integrity": "sha512-XjBR15BhXuylgWGuslhDKqlSayuqvqBX91BP8pauG8kd1zY8kotkNWbXksTCNRarse4kuGbe2kIY05ARtwNIvw==", - "dev": true, "license": "MIT" }, "node_modules/react-kapsule": { @@ -8571,7 +8673,6 @@ "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz", "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==", "license": "MIT", - "optional": true, "dependencies": { "utrie": "^1.0.2" } @@ -9068,7 +9169,6 @@ "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz", "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==", "license": "MIT", - "optional": true, "dependencies": { "base64-arraybuffer": "^1.0.2" } diff --git a/package.json b/package.json index 2269c18a5..04b6605b8 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "framer-motion": "^12.38.0", "gsap": "^3.15.0", "html-to-image": "^1.11.13", + "html2canvas": "^1.4.1", "jspdf": "^4.2.1", "lightningcss": "^1.32.0", "lucide-react": "^1.17.0",