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 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 | /** * Generate a complete monthly calendar as a single SVG * This prevents multi-page overflow - one image scales to fit */ import type React from 'react' import { AbacusStatic, calculateAbacusDimensions } from '@soroban/abacus-react/static' interface CalendarCompositeOptions { month: number year: number renderToString: (element: React.ReactElement) => string } const MONTH_NAMES = [ 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December', ] const WEEKDAYS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'] function getDaysInMonth(year: number, month: number): number { return new Date(year, month, 0).getDate() } function getFirstDayOfWeek(year: number, month: number): number { return new Date(year, month - 1, 1).getDay() } export function generateCalendarComposite(options: CalendarCompositeOptions): string { const { month, year, renderToString } = options const daysInMonth = getDaysInMonth(year, month) const firstDayOfWeek = getFirstDayOfWeek(year, month) const monthName = MONTH_NAMES[month - 1] // Layout constants for US Letter aspect ratio (8.5 x 11) const WIDTH = 850 const HEIGHT = 1100 const MARGIN = 50 const CONTENT_WIDTH = WIDTH - MARGIN * 2 const CONTENT_HEIGHT = HEIGHT - MARGIN * 2 // Abacus natural size is 120x230 at scale=1 const ABACUS_NATURAL_WIDTH = 120 const ABACUS_NATURAL_HEIGHT = 230 // Calculate how many columns needed for year const yearColumns = Math.max(1, Math.ceil(Math.log10(year + 1))) // Year abacus dimensions (calculate first to determine header height) // Use the shared dimension calculator so we stay in sync with AbacusStatic const { width: yearAbacusActualWidth, height: yearAbacusActualHeight } = calculateAbacusDimensions({ columns: yearColumns, showNumbers: false, columnLabels: [], }) const yearAbacusDisplayWidth = WIDTH * 0.15 // Display size on page const yearAbacusDisplayHeight = (yearAbacusActualHeight / yearAbacusActualWidth) * yearAbacusDisplayWidth // Header - sized to fit month name + year abacus const MONTH_NAME_HEIGHT = 40 const HEADER_HEIGHT = MONTH_NAME_HEIGHT + yearAbacusDisplayHeight + 20 // 20px spacing const TITLE_Y = MARGIN + 35 const yearAbacusX = (WIDTH - yearAbacusDisplayWidth) / 2 const yearAbacusY = TITLE_Y + 10 // Calendar grid const GRID_START_Y = MARGIN + HEADER_HEIGHT const GRID_HEIGHT = CONTENT_HEIGHT - HEADER_HEIGHT const WEEKDAY_ROW_HEIGHT = 25 const DAY_GRID_HEIGHT = GRID_HEIGHT - WEEKDAY_ROW_HEIGHT // 7 columns, up to 6 rows (35 cells max = 5 empty + 30 days worst case) const CELL_WIDTH = CONTENT_WIDTH / 7 const DAY_CELL_HEIGHT = DAY_GRID_HEIGHT / 6 // Day abacus sizing - fit in cell with padding const CELL_PADDING = 5 // Calculate max scale to fit in cell const MAX_SCALE_X = (CELL_WIDTH - CELL_PADDING * 2) / ABACUS_NATURAL_WIDTH const MAX_SCALE_Y = (DAY_CELL_HEIGHT - CELL_PADDING * 2) / ABACUS_NATURAL_HEIGHT const ABACUS_SCALE = Math.min(MAX_SCALE_X, MAX_SCALE_Y) * 0.9 // 90% to leave breathing room const SCALED_ABACUS_WIDTH = ABACUS_NATURAL_WIDTH * ABACUS_SCALE const SCALED_ABACUS_HEIGHT = ABACUS_NATURAL_HEIGHT * ABACUS_SCALE // Generate calendar grid const calendarCells: (number | null)[] = [] for (let i = 0; i < firstDayOfWeek; i++) { calendarCells.push(null) } for (let day = 1; day <= daysInMonth; day++) { calendarCells.push(day) } // Render individual abacus SVGs as complete SVG elements function renderAbacusSVG(value: number, columns: number, scale: number): string { return renderToString( <AbacusStatic value={value} columns={columns} scaleFactor={scale} showNumbers={false} frameVisible={true} compact={false} hideInactiveBeads={true} cropToActiveBeads={{ padding: { top: 8, bottom: 2, left: 5, right: 5 }, }} /> ) } // Main composite SVG const compositeSVG = `<svg xmlns="http://www.w3.org/2000/svg" width="${WIDTH}" height="${HEIGHT}" viewBox="0 0 ${WIDTH} ${HEIGHT}"> <!-- Background --> <rect width="${WIDTH}" height="${HEIGHT}" fill="white"/> <!-- Title: Month Name --> <text x="${WIDTH / 2}" y="${TITLE_Y}" text-anchor="middle" font-family="Arial" font-size="32" font-weight="bold" fill="#1a1a1a"> ${monthName} </text> <!-- Year Abacus (centered below month name) --> ${(() => { const yearAbacusSVG = renderAbacusSVG(year, yearColumns, 1) const yearAbacusContent = yearAbacusSVG.replace(/<svg[^>]*>/, '').replace(/<\/svg>$/, '') return `<svg x="${yearAbacusX}" y="${yearAbacusY}" width="${yearAbacusDisplayWidth}" height="${yearAbacusDisplayHeight}" viewBox="0 0 ${yearAbacusActualWidth} ${yearAbacusActualHeight}"> ${yearAbacusContent} </svg>` })()} <!-- Weekday Headers --> ${WEEKDAYS.map( (day, i) => ` <text x="${MARGIN + i * CELL_WIDTH + CELL_WIDTH / 2}" y="${GRID_START_Y + 18}" text-anchor="middle" font-family="Arial" font-size="14" font-weight="bold" fill="#555"> ${day} </text>` ).join('')} <!-- Separator line under weekdays --> <line x1="${MARGIN}" y1="${GRID_START_Y + WEEKDAY_ROW_HEIGHT}" x2="${WIDTH - MARGIN}" y2="${GRID_START_Y + WEEKDAY_ROW_HEIGHT}" stroke="#333" stroke-width="2"/> <!-- Calendar Grid Cells --> ${calendarCells .map((day, index) => { const row = Math.floor(index / 7) const col = index % 7 const cellX = MARGIN + col * CELL_WIDTH const cellY = GRID_START_Y + WEEKDAY_ROW_HEIGHT + row * DAY_CELL_HEIGHT return ` <rect x="${cellX}" y="${cellY}" width="${CELL_WIDTH}" height="${DAY_CELL_HEIGHT}" fill="none" stroke="#333" stroke-width="2"/>` }) .join('')} <!-- Calendar Day Abaci --> ${calendarCells .map((day, index) => { if (day === null) return '' const row = Math.floor(index / 7) const col = index % 7 const cellX = MARGIN + col * CELL_WIDTH const cellY = GRID_START_Y + WEEKDAY_ROW_HEIGHT + row * DAY_CELL_HEIGHT // Render cropped abacus SVG const abacusSVG = renderAbacusSVG(day, 2, 1) // Extract viewBox and dimensions from the cropped SVG const viewBoxMatch = abacusSVG.match(/viewBox="([^"]*)"/) const widthMatch = abacusSVG.match(/width="?([0-9.]+)"?/) const heightMatch = abacusSVG.match(/height="?([0-9.]+)"?/) const croppedViewBox = viewBoxMatch ? viewBoxMatch[1] : '0 0 120 230' const croppedWidth = widthMatch ? parseFloat(widthMatch[1]) : ABACUS_NATURAL_WIDTH const croppedHeight = heightMatch ? parseFloat(heightMatch[1]) : ABACUS_NATURAL_HEIGHT // Calculate scale to fit cropped abacus in cell const MAX_SCALE_X = (CELL_WIDTH - CELL_PADDING * 2) / croppedWidth const MAX_SCALE_Y = (DAY_CELL_HEIGHT - CELL_PADDING * 2) / croppedHeight const fitScale = Math.min(MAX_SCALE_X, MAX_SCALE_Y) * 0.95 // 95% to leave breathing room const scaledWidth = croppedWidth * fitScale const scaledHeight = croppedHeight * fitScale // Center abacus in cell const abacusCenterX = cellX + CELL_WIDTH / 2 const abacusCenterY = cellY + DAY_CELL_HEIGHT / 2 // Offset to top-left corner of abacus const abacusX = abacusCenterX - scaledWidth / 2 const abacusY = abacusCenterY - scaledHeight / 2 // Extract SVG content (remove outer <svg> tags) const svgContent = abacusSVG.replace(/<svg[^>]*>/, '').replace(/<\/svg>$/, '') return ` <!-- Day ${day} (row ${row}, col ${col}) --> <svg x="${abacusX}" y="${abacusY}" width="${scaledWidth}" height="${scaledHeight}" viewBox="${croppedViewBox}"> ${svgContent} </svg>` }) .join('')} </svg>` return compositeSVG } |