All files / web/scripts generateAllDayIcons.tsx

0% Statements 0/138
0% Branches 0/1
0% Functions 0/1
0% Lines 0/138

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139                                                                                                                                                                                                                                                                                     
#!/usr/bin/env tsx

/**
 * Pre-generate all 31 day-of-month favicons for production
 *
 * This script generates icon-day-01.svg through icon-day-31.svg
 * Production route handler will read these pre-generated files
 */

// biome-ignore lint/correctness/noUnusedImports: React is required for JSX transform
import React from 'react'
import { renderToStaticMarkup } from 'react-dom/server'
import { writeFileSync, mkdirSync } from 'fs'
import { join } from 'path'
import { AbacusStatic } from '@soroban/abacus-react'

// Extract just the SVG element from rendered output
function extractSvgElement(markup: string): string {
  const svgMatch = markup.match(/<svg[^>]*>[\s\S]*?<\/svg>/)
  if (!svgMatch) {
    throw new Error('No SVG element found in rendered output')
  }
  return svgMatch[0]
}

// Generate a single day icon
function generateDayIcon(day: number): string {
  // Render 2-column abacus showing day of month
  const abacusMarkup = renderToStaticMarkup(
    <AbacusStatic
      value={day}
      columns={2}
      scaleFactor={1.8}
      showNumbers={false}
      hideInactiveBeads={true}
      frameVisible={true}
      cropToActiveBeads={{
        padding: {
          top: 8,
          bottom: 2,
          left: 5,
          right: 5,
        },
      }}
      customStyles={{
        columnPosts: {
          fill: '#1c1917',
          stroke: '#0c0a09',
          strokeWidth: 2,
        },
        reckoningBar: {
          fill: '#1c1917',
          stroke: '#0c0a09',
          strokeWidth: 3,
        },
        columns: {
          0: {
            // Ones place - Gold (royal theme)
            heavenBeads: { fill: '#fbbf24', stroke: '#f59e0b', strokeWidth: 2 },
            earthBeads: { fill: '#fbbf24', stroke: '#f59e0b', strokeWidth: 2 },
          },
          1: {
            // Tens place - Purple (royal theme)
            heavenBeads: { fill: '#a855f7', stroke: '#7e22ce', strokeWidth: 2 },
            earthBeads: { fill: '#a855f7', stroke: '#7e22ce', strokeWidth: 2 },
          },
        },
      }}
    />
  )

  // Extract the cropped SVG
  let croppedSvg = extractSvgElement(abacusMarkup)

  // Remove !important from CSS (production code policy)
  croppedSvg = croppedSvg.replace(/\s*!important/g, '')

  // Parse width and height from the cropped SVG
  const widthMatch = croppedSvg.match(/width="([^"]+)"/)
  const heightMatch = croppedSvg.match(/height="([^"]+)"/)

  if (!widthMatch || !heightMatch) {
    throw new Error('Could not parse dimensions from cropped SVG')
  }

  const croppedWidth = parseFloat(widthMatch[1])
  const croppedHeight = parseFloat(heightMatch[1])

  // Calculate scale to fit cropped region into 96x96 (leaving room for border)
  const targetSize = 96
  const scale = Math.min(targetSize / croppedWidth, targetSize / croppedHeight)

  // Center in 100x100 canvas
  const scaledWidth = croppedWidth * scale
  const scaledHeight = croppedHeight * scale
  const offsetX = (100 - scaledWidth) / 2
  const offsetY = (100 - scaledHeight) / 2

  // Wrap in 100x100 SVG canvas for favicon
  // Extract viewBox from cropped SVG to preserve it
  const viewBoxMatch = croppedSvg.match(/viewBox="([^"]+)"/)
  const viewBox = viewBoxMatch ? viewBoxMatch[1] : `0 0 ${croppedWidth} ${croppedHeight}`

  return `<svg width="100" height="100" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
  <!-- Abacus showing day ${day.toString().padStart(2, '0')} (US Central Time) - cropped to active beads -->
  <svg x="${offsetX}" y="${offsetY}" width="${scaledWidth}" height="${scaledHeight}"
       viewBox="${viewBox}">
    ${croppedSvg.match(/<svg[^>]*>([\s\S]*?)<\/svg>/)?.[1] || ''}
  </svg>
</svg>
`
}

// Main execution
const publicDir = join(__dirname, '..', 'public')
const iconsDir = join(publicDir, 'icons')

try {
  // Ensure icons directory exists
  mkdirSync(iconsDir, { recursive: true })

  console.log('Generating all 31 day-of-month favicons...\n')

  // Generate all 31 days
  for (let day = 1; day <= 31; day++) {
    const svg = generateDayIcon(day)
    const filename = `icon-day-${day.toString().padStart(2, '0')}.svg`
    const filepath = join(iconsDir, filename)
    writeFileSync(filepath, svg)
    console.log(`āœ“ Generated ${filename}`)
  }

  console.log('\nāœ… All day icons generated successfully!')
  console.log(`   Location: public/icons/icon-day-*.svg`)
} catch (error) {
  console.error('āŒ Error generating day icons:', error)
  process.exit(1)
}