From bdca3154f8336e17a7031be8d2917f9cf05f274a Mon Sep 17 00:00:00 2001
From: Thomas Hallock
Date: Wed, 5 Nov 2025 09:31:31 -0600
Subject: [PATCH] feat(calendar): add beautiful daily calendar with
locale-based paper size detection
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Implement fully-featured daily calendar generation with enhanced visual design:
Daily Calendar Features:
- Beautiful single-page-per-day design with decorative borders
- Blue double-border frame for visual polish
- Light blue header section with uppercase month name (Georgia serif)
- Large prominent day-of-week text (42pt)
- 2.5x larger day abacus as main focal point
- Styled notes section with warm yellow background
- Full support for all paper sizes (US Letter, A4, A3, Tabloid)
Preview System:
- Add live preview support for daily calendars
- Generate composite SVG for preview (avoid Typst multi-page export issues)
- Show "Live Preview (First Day)" label for daily format
- Use same composite SVG approach as monthly calendars
Locale Detection:
- Auto-detect paper size from browser locale on page load
- US Letter for US, CA, MX, GT, PA, DO, PR, PH
- A4 for all other countries
- Hardcoded mapping (stable, no external deps needed)
Code Quality:
- Remove all debug console.log statements
- Fix unused imports
- Format and lint all files
Technical Details:
- Daily preview creates single composite SVG with embedded abacuses
- Uses Typst's responsive layout (percentages) for multi-size support
- Matches monthly calendar's single-image-export pattern
- Full PDF generation uses Typst #page() for multi-page output
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude
---
.../app/api/create/calendar/preview/route.ts | 144 +++++++++++++++---
.../create/calendar/utils/typstGenerator.ts | 126 ++++++++++-----
.../calendar/components/CalendarPreview.tsx | 43 ++----
apps/web/src/app/create/calendar/page.tsx | 15 +-
4 files changed, 235 insertions(+), 93 deletions(-)
diff --git a/apps/web/src/app/api/create/calendar/preview/route.ts b/apps/web/src/app/api/create/calendar/preview/route.ts
index 33a1740f..997cb7fd 100644
--- a/apps/web/src/app/api/create/calendar/preview/route.ts
+++ b/apps/web/src/app/api/create/calendar/preview/route.ts
@@ -5,6 +5,7 @@ import { join } from 'path'
import { execSync } from 'child_process'
import { generateMonthlyTypst, getDaysInMonth } from '../utils/typstGenerator'
import { generateCalendarComposite } from '@/utils/calendar/generateCalendarComposite'
+import { generateAbacusElement } from '@/utils/calendar/generateCalendarAbacus'
interface PreviewRequest {
month: number
@@ -26,34 +27,137 @@ export async function POST(request: NextRequest) {
return NextResponse.json({ error: 'Invalid month or year' }, { status: 400 })
}
- // Only generate preview for monthly format
- if (format !== 'monthly') {
- return NextResponse.json({ svg: null })
- }
-
// Dynamic import to avoid Next.js bundler issues
const { renderToStaticMarkup } = await import('react-dom/server')
- // Create temp directory for SVG file
+ // Create temp directory for SVG file(s)
tempDir = join(tmpdir(), `calendar-preview-${Date.now()}-${Math.random()}`)
mkdirSync(tempDir, { recursive: true })
- // Generate and write composite SVG
- const calendarSvg = generateCalendarComposite({
- month,
- year,
- renderToString: renderToStaticMarkup,
- })
- writeFileSync(join(tempDir, 'calendar.svg'), calendarSvg)
-
// Generate Typst document content
const daysInMonth = getDaysInMonth(year, month)
- const typstContent = generateMonthlyTypst({
- month,
- year,
- paperSize: 'us-letter',
- daysInMonth,
- })
+ let typstContent: string
+
+ if (format === 'monthly') {
+ // Generate and write composite SVG
+ const calendarSvg = generateCalendarComposite({
+ month,
+ year,
+ renderToString: renderToStaticMarkup,
+ })
+ writeFileSync(join(tempDir, 'calendar.svg'), calendarSvg)
+
+ typstContent = generateMonthlyTypst({
+ month,
+ year,
+ paperSize: 'us-letter',
+ daysInMonth,
+ })
+ } else {
+ // Daily format: Create a SINGLE composite SVG (like monthly) to avoid multi-image export issue
+
+ // Generate individual abacus SVGs
+ const daySvg = renderToStaticMarkup(generateAbacusElement(1, 2))
+ if (!daySvg || daySvg.trim().length === 0) {
+ throw new Error('Generated empty SVG for day 1')
+ }
+
+ const yearColumns = Math.max(1, Math.ceil(Math.log10(year + 1)))
+ const yearSvg = renderToStaticMarkup(generateAbacusElement(year, yearColumns))
+ if (!yearSvg || yearSvg.trim().length === 0) {
+ throw new Error(`Generated empty SVG for year ${year}`)
+ }
+
+ // Create composite SVG with both year and day abacus
+ const monthName = [
+ 'January',
+ 'February',
+ 'March',
+ 'April',
+ 'May',
+ 'June',
+ 'July',
+ 'August',
+ 'September',
+ 'October',
+ 'November',
+ 'December',
+ ][month - 1]
+ const dayOfWeek = new Date(year, month - 1, 1).toLocaleDateString('en-US', {
+ weekday: 'long',
+ })
+
+ // Extract SVG content (remove outer
(null)
+ // Detect default paper size based on user's locale (client-side only)
+ useEffect(() => {
+ // Get user's locale
+ const locale = navigator.language || navigator.languages?.[0] || 'en-US'
+ const country = locale.split('-')[1]?.toUpperCase()
+
+ // Countries that use US Letter (8.5" × 11")
+ const letterCountries = ['US', 'CA', 'MX', 'GT', 'PA', 'DO', 'PR', 'PH']
+
+ const detectedSize = letterCountries.includes(country || '') ? 'us-letter' : 'a4'
+ setPaperSize(detectedSize)
+ }, [])
+
const handleGenerate = async () => {
setIsGenerating(true)
try {