22
33import { useState , useEffect , useRef } from 'react' ;
44import { motion , AnimatePresence } from 'framer-motion' ;
5- import { X , Link2 , Download , Share2 , Check , Loader2 , Smartphone } from 'lucide-react' ;
5+ import { Check , Download , FileJson , Link2 , Loader2 , Share2 , Smartphone , X } from 'lucide-react' ;
66import { toPng } from 'html-to-image' ;
7+ import type { DashboardExportData } from '@/types/dashboard' ;
78
89// Inline branded icons (Twitter/X brand, LinkedIn brand)
910const XBrandIcon = ( { size = 18 } : { size ?: number } ) => (
@@ -22,6 +23,7 @@ interface ShareSheetProps {
2223 username : string ;
2324 isOpen : boolean ;
2425 onClose : ( ) => void ;
26+ exportData : DashboardExportData ;
2527}
2628
2729type OptionState = 'idle' | 'loading' | 'success' | 'error' ;
@@ -31,7 +33,7 @@ const PROFILE_URL = (username: string) =>
3133 ? `${ window . location . origin } /${ username } `
3234 : `https://commitpulse.vercel.app/${ username } ` ;
3335
34- export default function ShareSheet ( { username, isOpen, onClose } : ShareSheetProps ) {
36+ export default function ShareSheet ( { username, isOpen, onClose, exportData } : ShareSheetProps ) {
3537 const [ states , setStates ] = useState < Record < string , OptionState > > ( { } ) ;
3638 const overlayRef = useRef < HTMLDivElement > ( null ) ;
3739
@@ -116,6 +118,33 @@ export default function ShareSheet({ username, isOpen, onClose }: ShareSheetProp
116118 }
117119 } ;
118120
121+ const handleDownloadJSON = ( ) => {
122+ setOptionState ( 'json' , 'loading' ) ;
123+ try {
124+ const payload = {
125+ username,
126+ profileUrl : PROFILE_URL ( username ) ,
127+ exportedAt : new Date ( ) . toISOString ( ) ,
128+ currentStreak : exportData . stats . currentStreak ,
129+ longestStreak : exportData . stats . peakStreak ,
130+ totalContributions : exportData . stats . totalContributions ,
131+ topLanguages : exportData . languages ,
132+ } ;
133+ const blob = new Blob ( [ JSON . stringify ( payload , null , 2 ) ] , {
134+ type : 'application/json' ,
135+ } ) ;
136+ const url = URL . createObjectURL ( blob ) ;
137+ const link = document . createElement ( 'a' ) ;
138+ link . download = `commitpulse-${ username } .json` ;
139+ link . href = url ;
140+ link . click ( ) ;
141+ URL . revokeObjectURL ( url ) ;
142+ setOptionState ( 'json' , 'success' ) ;
143+ } catch {
144+ setOptionState ( 'json' , 'error' ) ;
145+ }
146+ } ;
147+
119148 const handleNativeShare = async ( ) => {
120149 if ( ! ( 'share' in navigator ) ) {
121150 // Graceful fallback: just copy the link
@@ -180,6 +209,15 @@ export default function ShareSheet({ username, isOpen, onClose }: ShareSheetProp
180209 glow : 'transparent' ,
181210 action : handleDownloadPNG ,
182211 } ,
212+ {
213+ key : 'json' ,
214+ icon : FileJson ,
215+ label : 'Download JSON' ,
216+ description : 'Export raw streak and language data' ,
217+ gradient : 'bg-zinc-800' ,
218+ glow : 'transparent' ,
219+ action : handleDownloadJSON ,
220+ } ,
183221 {
184222 key : 'native' ,
185223 icon : typeof window !== 'undefined' && 'share' in navigator ? Smartphone : Share2 ,
@@ -275,7 +313,9 @@ export default function ShareSheet({ username, isOpen, onClose }: ShareSheetProp
275313 ? 'Link Copied!'
276314 : opt . key === 'png'
277315 ? 'Downloaded!'
278- : opt . label
316+ : opt . key === 'json'
317+ ? 'JSON Downloaded!'
318+ : opt . label
279319 : state === 'error'
280320 ? 'Failed — try again'
281321 : opt . label }
0 commit comments