11// src/components/dashboard/LeaderBoard/BadgeModal.tsx
2- import React , { useEffect } from "react" ;
2+ import React , { useEffect , useState } from "react" ;
33import { motion , AnimatePresence } from "framer-motion" ;
4- import { FaTimes } from "react-icons/fa" ;
4+ import { FaTimes , FaShareAlt , FaDownload } from "react-icons/fa" ;
55import { useSafeColorMode } from "@site/src/utils/useSafeColorMode" ;
6+ import { Contributor } from "./leaderboard" ;
7+ import { generateShareCard } from "../../../utils/cardGenerator" ;
68
7- interface BadgeConfig {
9+ export interface BadgeConfig {
810 image : string ;
911 name : string ;
1012 criteria : ( prs : number , points : number ) => boolean ;
@@ -15,17 +17,94 @@ interface BadgeModalProps {
1517 onClose : ( ) => void ;
1618 earnedBadges : string [ ] ;
1719 allBadges : BadgeConfig [ ] ;
18- contributorName ?: string ;
20+ contributor : Contributor ;
21+ rank : number ;
1922}
2023
2124export default function BadgeModal ( {
2225 isOpen,
2326 onClose,
2427 earnedBadges,
2528 allBadges,
26- contributorName,
29+ contributor,
30+ rank,
2731} : BadgeModalProps ) : JSX . Element | null {
2832 const { isDark } = useSafeColorMode ( ) ;
33+ const [ isSharing , setIsSharing ] = useState ( false ) ;
34+ const [ isDownloading , setIsDownloading ] = useState ( false ) ;
35+ const [ shareError , setShareError ] = useState < string | null > ( null ) ;
36+
37+ const getCardBlob = async ( ) => {
38+ return await generateShareCard ( {
39+ username : contributor . username ,
40+ avatarUrl : contributor . avatar ,
41+ prs : contributor . prs ,
42+ points : contributor . points ,
43+ rank,
44+ earnedBadges,
45+ allBadges,
46+ } ) ;
47+ } ;
48+
49+ const handleDownloadCard = async ( ) => {
50+ setIsDownloading ( true ) ;
51+ setShareError ( null ) ;
52+ try {
53+ const cardBlob = await getCardBlob ( ) ;
54+ const downloadUrl = URL . createObjectURL ( cardBlob ) ;
55+ const link = document . createElement ( "a" ) ;
56+ link . href = downloadUrl ;
57+ link . download = `recodehive-${ contributor . username } -achievements.png` ;
58+ document . body . appendChild ( link ) ;
59+ link . click ( ) ;
60+ document . body . removeChild ( link ) ;
61+ URL . revokeObjectURL ( downloadUrl ) ;
62+ } catch ( err ) {
63+ console . error ( "Error generating or downloading card: " , err ) ;
64+ setShareError ( "Could not download achievements card. Please try again." ) ;
65+ } finally {
66+ setIsDownloading ( false ) ;
67+ }
68+ } ;
69+
70+ const handleShareCard = async ( ) => {
71+ setIsSharing ( true ) ;
72+ setShareError ( null ) ;
73+ try {
74+ const cardBlob = await getCardBlob ( ) ;
75+ const file = new File (
76+ [ cardBlob ] ,
77+ `recodehive-${ contributor . username } -achievements.png` ,
78+ { type : "image/png" }
79+ ) ;
80+
81+ if (
82+ navigator . share &&
83+ navigator . canShare &&
84+ navigator . canShare ( { files : [ file ] } )
85+ ) {
86+ await navigator . share ( {
87+ files : [ file ] ,
88+ title : "My Recode Hive Open Source Achievements" ,
89+ text : `Check out my open-source contribution achievements on Recode Hive! I am ranked #${ rank } with ${ contributor . prs } merged PRs and ${ contributor . points } points. 🚀` ,
90+ } ) ;
91+ } else {
92+ const downloadUrl = URL . createObjectURL ( cardBlob ) ;
93+ const link = document . createElement ( "a" ) ;
94+ link . href = downloadUrl ;
95+ link . download = `recodehive-${ contributor . username } -achievements.png` ;
96+ document . body . appendChild ( link ) ;
97+ link . click ( ) ;
98+ document . body . removeChild ( link ) ;
99+ URL . revokeObjectURL ( downloadUrl ) ;
100+ }
101+ } catch ( err ) {
102+ console . error ( "Error generating or sharing card: " , err ) ;
103+ setShareError ( "Could not share achievements card. Please try again." ) ;
104+ } finally {
105+ setIsSharing ( false ) ;
106+ }
107+ } ;
29108
30109 // Close modal on Escape key press
31110 useEffect ( ( ) => {
@@ -79,27 +158,58 @@ export default function BadgeModal({
79158 id = "badge-modal-title"
80159 className = { `badge-modal-title ${ isDark ? "dark" : "light" } ` }
81160 >
82- { contributorName
83- ? `${ contributorName } 's Badges`
84- : "Achievement Badges" }
161+ { contributor . username ? `${ contributor . username } 's Badges` : "Achievement Badges" }
85162 </ h2 >
86163 < p
87164 className = { `badge-modal-subtitle ${ isDark ? "dark" : "light" } ` }
88165 >
89166 { earnedBadges . length } of { allBadges . length } badges earned
90167 </ p >
91168 </ div >
92- < button
93- className = { `badge-modal-close ${ isDark ? "dark" : "light" } ` }
94- onClick = { onClose }
95- aria-label = "Close modal"
96- >
97- < FaTimes />
98- </ button >
169+ < div className = "badge-modal-header-actions" >
170+ < button
171+ className = { `badge-modal-download-btn ${ isDark ? "dark" : "light" } ` }
172+ onClick = { handleDownloadCard }
173+ disabled = { isDownloading || isSharing }
174+ aria-label = "Download achievements card"
175+ title = "Download Card"
176+ >
177+ { isDownloading ? (
178+ < span className = "badge-modal-spinner" />
179+ ) : (
180+ < FaDownload />
181+ ) }
182+ </ button >
183+ < button
184+ className = { `badge-modal-share-btn ${ isDark ? "dark" : "light" } ` }
185+ onClick = { handleShareCard }
186+ disabled = { isDownloading || isSharing }
187+ aria-label = "Share achievements card"
188+ >
189+ { isSharing ? (
190+ < span className = "badge-modal-spinner" />
191+ ) : (
192+ < FaShareAlt style = { { marginRight : 6 } } />
193+ ) }
194+ { isSharing ? "Generating..." : "Share Card" }
195+ </ button >
196+ < button
197+ className = { `badge-modal-close ${ isDark ? "dark" : "light" } ` }
198+ onClick = { onClose }
199+ aria-label = "Close modal"
200+ >
201+ < FaTimes />
202+ </ button >
203+ </ div >
99204 </ div >
100205
101206 { /* Modal Body */ }
102207 < div className = { `badge-modal-body ${ isDark ? "dark" : "light" } ` } >
208+ { shareError && (
209+ < div className = { `badge-modal-error-banner ${ isDark ? "dark" : "light" } ` } >
210+ { shareError }
211+ </ div >
212+ ) }
103213 < div className = "badge-grid" >
104214 { allBadges . map ( ( badge , index ) => {
105215 const isEarned = earnedBadges . includes ( badge . image ) ;
0 commit comments