diff --git a/app/compare/CompareClient.tsx b/app/compare/CompareClient.tsx index a45dc5c0..c18928eb 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 7250073c..b72fe23b 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 00000000..8a503a8b --- /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 00000000..288f3c87 --- /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/components/dashboard/DashboardClient.empty-fallback.test.tsx b/components/dashboard/DashboardClient.empty-fallback.test.tsx index e00aaad7..12c67f3f 100644 --- a/components/dashboard/DashboardClient.empty-fallback.test.tsx +++ b/components/dashboard/DashboardClient.empty-fallback.test.tsx @@ -111,6 +111,10 @@ vi.mock('./GrowthTrendChart', () => ({ default: () =>
Growth Trend Chart
, })); +vi.mock('./RoastWidget', () => ({ + default: () =>
Roast Widget
, +})); + const mockPeriod = { kind: 'year' as const, label: '2026', diff --git a/components/dashboard/DashboardClient.test.tsx b/components/dashboard/DashboardClient.test.tsx index 3ba58802..bc95481d 100644 --- a/components/dashboard/DashboardClient.test.tsx +++ b/components/dashboard/DashboardClient.test.tsx @@ -105,6 +105,10 @@ vi.mock('./AIInsights', () => ({ default: () =>
, })); +vi.mock('./RoastWidget', () => ({ + default: () =>
, +})); + vi.mock('./StatsCard', () => ({ default: ({ title }: any) =>
, })); @@ -556,39 +560,38 @@ describe('DashboardClient', () => { expect(screen.getByText('Exit Compare Mode')).toBeDefined(); }); - // Both profiles have stats that generate the "Consistency Beast 🔥" tag - // Using Regex /.../i to match the text even if there is an emoji next to it! const tags = screen.getAllByText(/Consistency Beast/i); expect(tags).toHaveLength(2); }); -}); -it('shows Most Consistent badge for profile with higher peak streak in compare mode', async () => { - const mockFetch = vi.fn().mockResolvedValue({ - ok: true, - json: async () => secondDataWithLowerStreak, - }); - vi.stubGlobal('fetch', mockFetch); + it('shows Most Consistent badge for profile with higher peak streak in compare mode', async () => { + const mockFetch = vi.fn().mockResolvedValue({ + ok: true, + json: async () => secondDataWithLowerStreak, + }); - render( - - ); + vi.stubGlobal('fetch', mockFetch); - fireEvent.click(screen.getByText('Compare Profile')); + render( + + ); - fireEvent.change(screen.getByPlaceholderText('Enter GitHub Username'), { - target: { value: 'JhaSourav07' }, - }); + fireEvent.click(screen.getByText('Compare Profile')); - fireEvent.click(screen.getByText('Compare')); + fireEvent.change(screen.getByPlaceholderText('Enter GitHub Username'), { + target: { value: 'JhaSourav07' }, + }); - await waitFor(() => { - expect(screen.getByText('Exit Compare Mode')).toBeDefined(); - }); + fireEvent.click(screen.getByText('Compare')); - expect(screen.getByText(/Most Consistent/i)).toBeDefined(); + await waitFor(() => { + expect(screen.getByText('Exit Compare Mode')).toBeDefined(); + }); + + expect(screen.getByText(/Most Consistent/i)).toBeDefined(); + }); }); diff --git a/components/dashboard/DashboardClient.tsx b/components/dashboard/DashboardClient.tsx index 5ba3417e..c446f60a 100644 --- a/components/dashboard/DashboardClient.tsx +++ b/components/dashboard/DashboardClient.tsx @@ -17,6 +17,7 @@ import CommitClock from './CommitClock'; import Heatmap from './Heatmap'; import HistoricalTrendView from './HistoricalTrendView'; import AIInsights from './AIInsights'; +import RoastWidget from './RoastWidget'; import StatsCard from './StatsCard'; import RepositoryGraph from './RepositoryGraph'; import ComparisonStatsCard from './ComparisonStatsCard'; @@ -686,6 +687,7 @@ export default function DashboardClient({
+ `500+ contributions but 0 PRs? Do you even know how to work in a team? 🤡`, + (s: CombinedStats) => + `${s.repositories} repos? And how many of those are abandoned side projects? 👻`, + (s: CombinedStats) => + `Peak streak of ${s.peakStreak} days. Did your router break after that or what? 🔌`, + () => `Your contribution graph looks like a barcode that scans as "Skill Issue". 💀`, + (s: CombinedStats) => + `Only ${s.totalContributions} commits? The "Commit" button isn't going to bite you. 🐛`, + (s: CombinedStats) => + `${s.followers} followers. Impressive. Do any of them actually read your code? 🧐`, +]; + +const HYPES = [ + (s: CombinedStats) => + `A ${s.currentStreak}-day streak! Your keyboard must be melting right now. 🔥`, + (s: CombinedStats) => + `${s.totalContributions} commits! The open-source world rests on your shoulders. 🌍`, + (s: CombinedStats) => + `${s.stars} stars earned! You're basically GitHub royalty at this point. 👑`, + (s: CombinedStats) => + `${s.repositories} repositories? The devil works hard, but you work harder. ⚡`, + (s: CombinedStats) => `With a peak streak of ${s.peakStreak}, you're literally unstoppable. 🚀`, + (s: CombinedStats) => `${s.followers} followers! They're witnessing greatness in real-time. 📈`, +]; + +type Mode = 'roast' | 'hype'; + +export default function RoastWidget({ stats, profile }: RoastWidgetProps) { + const [mode, setMode] = useState('roast'); + const [messageIndex, setMessageIndex] = useState(0); + const [isAnimating, setIsAnimating] = useState(false); + + const combinedStats: CombinedStats = { + ...stats, + ...profile.stats, + }; + + const messages = mode === 'roast' ? ROASTS : HYPES; + const currentMessage = messages[messageIndex](combinedStats); + + const handleGenerate = (newMode: Mode) => { + if (isAnimating) return; + setIsAnimating(true); + setMode(newMode); + + // Pick a random index different from current if staying in same mode + let nextIdx = Math.floor(Math.random() * messages.length); + if (newMode === mode && nextIdx === messageIndex) { + nextIdx = (nextIdx + 1) % messages.length; + } + + setMessageIndex(nextIdx); + + setTimeout(() => { + setIsAnimating(false); + }, 600); + }; + + return ( + + {/* Animated gradient border */} +
+ +
+
+

+ + AI Vibe Check +

+
+ + +
+
+ +
+ + + "{currentMessage}" + + +
+ + +
+ + ); +} diff --git a/package-lock.json b/package-lock.json index 48616a83..1a303d28 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", @@ -2772,7 +2773,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 +3159,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 +5068,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" @@ -8570,7 +8568,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" } @@ -9067,7 +9064,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 2269c18a..04b6605b 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",