@@ -9,6 +9,9 @@ const USERNAME = 'Codestz';
99const PURPLE = '#7c3aed' ;
1010const TOKEN = process . env . GITHUB_TOKEN ;
1111
12+ // Featured repos rendered as standalone cards (stars/forks auto-refreshed)
13+ const FEATURED_REPOS = [ 'claude-hindsight' ] ;
14+
1215if ( ! TOKEN ) {
1316 console . error ( 'GITHUB_TOKEN is required' ) ;
1417 process . exit ( 1 ) ;
@@ -69,6 +72,19 @@ async function fetchStats() {
6972 return data . user ;
7073}
7174
75+ async function fetchRepo ( name ) {
76+ const { data } = await githubAPI ( `{
77+ repository(owner: "${ USERNAME } ", name: "${ name } ") {
78+ name
79+ description
80+ stargazerCount
81+ forkCount
82+ primaryLanguage { name color }
83+ }
84+ }` ) ;
85+ return data . repository ;
86+ }
87+
7288function calculateStreak ( weeks ) {
7389 const days = weeks
7490 . flatMap ( ( w ) => w . contributionDays )
@@ -340,6 +356,73 @@ function generateContributionGraphSVG(weeks, theme) {
340356</svg>` ;
341357}
342358
359+ function escapeXml ( str ) {
360+ return String ( str || '' )
361+ . replace ( / & / g, '&' )
362+ . replace ( / < / g, '<' )
363+ . replace ( / > / g, '>' ) ;
364+ }
365+
366+ function wrapText ( text , maxChars , maxLines ) {
367+ const words = String ( text || '' )
368+ . split ( / \s + / )
369+ . filter ( Boolean ) ;
370+ const all = [ ] ;
371+ let line = '' ;
372+ for ( const word of words ) {
373+ const candidate = line ? `${ line } ${ word } ` : word ;
374+ if ( candidate . length > maxChars && line ) {
375+ all . push ( line ) ;
376+ line = word ;
377+ } else {
378+ line = candidate ;
379+ }
380+ }
381+ if ( line ) all . push ( line ) ;
382+ if ( all . length <= maxLines ) return all ;
383+
384+ const kept = all . slice ( 0 , maxLines ) ;
385+ const last = kept [ maxLines - 1 ] ;
386+ kept [ maxLines - 1 ] = ( last . length > maxChars - 1 ? last . slice ( 0 , maxChars - 1 ) : last ) + '…' ;
387+ return kept ;
388+ }
389+
390+ function generateRepoCardSVG ( repo , theme ) {
391+ const isDark = theme === 'dark' ;
392+ const textColor = isDark ? '#c9d1d9' : '#333333' ;
393+ const subColor = isDark ? '#888888' : '#666666' ;
394+ const lang = repo . primaryLanguage ;
395+ const descLines = wrapText ( repo . description , 52 , 2 ) ;
396+
397+ const descSvg = descLines
398+ . map (
399+ ( l , i ) =>
400+ `<text x="25" y="${ 66 + i * 20 } " fill="${ subColor } " font-size="13" font-family="'Segoe UI', Ubuntu, 'Helvetica Neue', sans-serif">${ escapeXml ( l ) } </text>`
401+ )
402+ . join ( '\n ' ) ;
403+
404+ const footerY = 66 + descLines . length * 20 + 18 ;
405+
406+ const langSvg = lang
407+ ? `<circle cx="29" cy="${ footerY - 4 } " r="6" fill="${ lang . color || PURPLE } "/>
408+ <text x="42" y="${ footerY } " fill="${ textColor } " font-size="13" font-family="'Segoe UI', Ubuntu, 'Helvetica Neue', sans-serif">${ escapeXml ( lang . name ) } </text>`
409+ : '' ;
410+
411+ const height = footerY + 20 ;
412+
413+ return `<svg xmlns="http://www.w3.org/2000/svg" width="400" height="${ height } " viewBox="0 0 400 ${ height } ">
414+ <rect x="2" y="2" width="396" height="${ height - 4 } " fill="none" stroke="${ PURPLE } " stroke-width="3"/>
415+ <svg x="21" y="20" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="${ PURPLE } " stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
416+ <path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"/>
417+ </svg>
418+ <text x="48" y="34" fill="${ PURPLE } " font-size="17" font-weight="bold" font-family="'Segoe UI', Ubuntu, 'Helvetica Neue', sans-serif">${ escapeXml ( repo . name ) } </text>
419+ ${ descSvg }
420+ ${ langSvg }
421+ <text x="300" y="${ footerY } " fill="${ textColor } " font-size="13" font-family="'Segoe UI', Ubuntu, 'Helvetica Neue', sans-serif">★ ${ repo . stargazerCount . toLocaleString ( ) } </text>
422+ <text x="350" y="${ footerY } " fill="${ textColor } " font-size="13" font-family="'Segoe UI', Ubuntu, 'Helvetica Neue', sans-serif">⑂ ${ repo . forkCount . toLocaleString ( ) } </text>
423+ </svg>` ;
424+ }
425+
343426async function main ( ) {
344427 console . log ( 'Fetching GitHub stats...' ) ;
345428 const stats = await fetchStats ( ) ;
@@ -374,6 +457,25 @@ async function main() {
374457 console . log ( `Generated ${ theme } theme SVGs` ) ;
375458 }
376459
460+ for ( const name of FEATURED_REPOS ) {
461+ try {
462+ const repo = await fetchRepo ( name ) ;
463+ if ( ! repo ) {
464+ console . log ( `Repo ${ name } not found, skipping card` ) ;
465+ continue ;
466+ }
467+ for ( const theme of [ 'dark' , 'light' ] ) {
468+ fs . writeFileSync (
469+ path . join ( outDir , `repo-${ name } -${ theme } .svg` ) ,
470+ generateRepoCardSVG ( repo , theme )
471+ ) ;
472+ }
473+ console . log ( `Generated repo card for ${ name } ` ) ;
474+ } catch ( err ) {
475+ console . log ( `Failed to generate card for ${ name } : ${ err . message } ` ) ;
476+ }
477+ }
478+
377479 console . log ( 'Done! SVGs written to public/stats/' ) ;
378480}
379481
0 commit comments