refactor(web): use client-side React rendering for live calendar preview

- Render calendar using AbacusStatic components directly in browser
- Remove API call and React Query dependency for preview
- Show live preview that updates instantly when month/year changes
- Display generated PDF SVG after user clicks generate button
- Eliminates unnecessary server round-trip for interactive preview

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Thomas Hallock
2025-11-04 09:32:25 -06:00
parent 14a5de0dfa
commit f880cbe4bf

View File

@@ -1,27 +1,18 @@
'use client'
import { AbacusStatic } from '@soroban/abacus-react/static'
import { css } from '../../../../../styled-system/css'
import { AbacusReact, useAbacusConfig } from '@soroban/abacus-react'
interface CalendarPreviewProps {
month: number
year: number
format: 'monthly' | 'daily'
previewSvg: string | null
}
const MONTHS = [
'January',
'February',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December',
const MONTH_NAMES = [
'January', 'February', 'March', 'April', 'May', 'June',
'July', 'August', 'September', 'October', 'November', 'December',
]
const WEEKDAYS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
@@ -34,11 +25,47 @@ function getFirstDayOfWeek(year: number, month: number): number {
return new Date(year, month - 1, 1).getDay()
}
export function CalendarPreview({ month, year, format }: CalendarPreviewProps) {
const abacusConfig = useAbacusConfig()
const daysInMonth = getDaysInMonth(year, month)
const firstDayOfWeek = getFirstDayOfWeek(year, month)
export function CalendarPreview({ month, year, format, previewSvg }: CalendarPreviewProps) {
// If we have the generated PDF SVG, show that instead
if (previewSvg) {
return (
<div
data-component="calendar-preview"
className={css({
bg: 'gray.800',
borderRadius: '12px',
padding: '2rem',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
})}
>
<p
className={css({
fontSize: '1.125rem',
color: 'yellow.400',
marginBottom: '1rem',
textAlign: 'center',
fontWeight: 'bold',
})}
>
Generated PDF Preview
</p>
<div
className={css({
bg: 'white',
borderRadius: '8px',
padding: '1rem',
maxWidth: '100%',
overflow: 'auto',
})}
dangerouslySetInnerHTML={{ __html: previewSvg }}
/>
</div>
)
}
// For daily format, no live preview
if (format === 'daily') {
return (
<div
@@ -57,110 +84,29 @@ export function CalendarPreview({ month, year, format }: CalendarPreviewProps) {
<p
className={css({
fontSize: '1.25rem',
color: 'gray.300',
marginBottom: '1.5rem',
textAlign: 'center',
})}
>
Daily format preview
</p>
<div
className={css({
bg: 'white',
padding: '3rem 2rem',
borderRadius: '8px',
boxShadow: '0 4px 6px rgba(0, 0, 0, 0.1)',
maxWidth: '400px',
width: '100%',
})}
>
{/* Year at top */}
<div
className={css({
display: 'flex',
justifyContent: 'center',
marginBottom: '2rem',
})}
>
<AbacusReact
value={year}
columns={4}
customStyles={abacusConfig.customStyles}
scaleFactor={0.4}
showNumbers={false}
/>
</div>
{/* Large day number */}
<div
className={css({
display: 'flex',
justifyContent: 'center',
marginBottom: '2rem',
})}
>
<AbacusReact
value={1}
columns={2}
customStyles={abacusConfig.customStyles}
scaleFactor={0.8}
showNumbers={false}
/>
</div>
{/* Date text */}
<div
className={css({
textAlign: 'center',
color: 'gray.800',
})}
>
<div
className={css({
fontSize: '1.25rem',
fontWeight: '600',
marginBottom: '0.25rem',
})}
>
{new Date(year, month - 1, 1).toLocaleDateString('en-US', {
weekday: 'long',
})}
</div>
<div
className={css({
fontSize: '1rem',
color: 'gray.600',
})}
>
{MONTHS[month - 1]} 1, {year}
</div>
</div>
</div>
<p
className={css({
fontSize: '0.875rem',
color: 'gray.400',
marginTop: '1rem',
textAlign: 'center',
})}
>
Example of first day (1 page per day for all {daysInMonth} days)
Daily format - preview after generation
</p>
</div>
)
}
// Monthly format
const calendarDays: (number | null)[] = []
// Live preview: render calendar with React components
const daysInMonth = getDaysInMonth(year, month)
const firstDayOfWeek = getFirstDayOfWeek(year, month)
const monthName = MONTH_NAMES[month - 1]
const yearColumns = Math.max(1, Math.ceil(Math.log10(year + 1)))
// Add empty cells for days before the first day of month
// Generate calendar cells
const calendarCells: (number | null)[] = []
for (let i = 0; i < firstDayOfWeek; i++) {
calendarDays.push(null)
calendarCells.push(null)
}
// Add actual days
for (let day = 1; day <= daysInMonth; day++) {
calendarDays.push(day)
calendarCells.push(day)
}
return (
@@ -170,101 +116,126 @@ export function CalendarPreview({ month, year, format }: CalendarPreviewProps) {
bg: 'gray.800',
borderRadius: '12px',
padding: '2rem',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
})}
>
<div
<p
className={css({
fontSize: '1.125rem',
color: 'yellow.400',
marginBottom: '1rem',
textAlign: 'center',
marginBottom: '2rem',
fontWeight: 'bold',
})}
>
<h2
className={css({
fontSize: '2rem',
fontWeight: 'bold',
marginBottom: '1rem',
color: 'yellow.400',
})}
>
{MONTHS[month - 1]}
</h2>
Live Preview
</p>
<div
className={css({
bg: 'white',
borderRadius: '8px',
padding: '1rem',
maxWidth: '100%',
overflow: 'auto',
})}
>
{/* Calendar Header */}
<div
className={css({
display: 'flex',
justifyContent: 'center',
flexDirection: 'column',
alignItems: 'center',
marginBottom: '1rem',
})}
>
<AbacusReact
value={year}
columns={4}
customStyles={abacusConfig.customStyles}
scaleFactor={0.6}
showNumbers={false}
/>
<h2
className={css({
fontSize: '2rem',
fontWeight: 'bold',
marginBottom: '0.5rem',
color: '#1a1a1a',
})}
>
{monthName}
</h2>
<div className={css({ transform: 'scale(0.6)' })}>
<AbacusStatic
value={year}
columns={yearColumns}
scaleFactor={1}
showNumbers={false}
frameVisible={true}
compact={false}
/>
</div>
</div>
{/* Weekday Headers */}
<div
className={css({
display: 'grid',
gridTemplateColumns: 'repeat(7, 1fr)',
gap: '2px',
marginBottom: '2px',
})}
>
{WEEKDAYS.map((day) => (
<div
key={day}
className={css({
textAlign: 'center',
fontWeight: 'bold',
fontSize: '0.875rem',
padding: '0.5rem',
color: '#555',
borderBottom: '2px solid #333',
})}
>
{day}
</div>
))}
</div>
{/* Calendar Grid */}
<div
className={css({
display: 'grid',
gridTemplateColumns: 'repeat(7, 1fr)',
gap: '2px',
bg: '#333',
border: '2px solid #333',
})}
>
{calendarCells.map((day, index) => (
<div
key={index}
className={css({
bg: 'white',
minHeight: '120px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: '0.25rem',
})}
>
{day !== null && (
<div className={css({ transform: 'scale(0.4)' })}>
<AbacusStatic
value={day}
columns={2}
scaleFactor={1}
showNumbers={false}
frameVisible={true}
compact={false}
/>
</div>
)}
</div>
))}
</div>
</div>
{/* Calendar Grid */}
<div
className={css({
display: 'grid',
gridTemplateColumns: 'repeat(7, 1fr)',
gap: '0.5rem',
})}
>
{/* Weekday headers */}
{WEEKDAYS.map((day) => (
<div
key={day}
className={css({
textAlign: 'center',
fontWeight: '600',
padding: '0.5rem',
color: 'yellow.400',
fontSize: '0.875rem',
})}
>
{day}
</div>
))}
{/* Calendar days */}
{calendarDays.map((day, index) => (
<div
key={index}
className={css({
aspectRatio: '1',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
bg: day ? 'gray.700' : 'transparent',
borderRadius: '6px',
padding: '0.25rem',
})}
>
{day && (
<AbacusReact
value={day}
columns={2}
customStyles={abacusConfig.customStyles}
scaleFactor={1.0}
showNumbers={false}
/>
)}
</div>
))}
</div>
<p
className={css({
fontSize: '0.875rem',
color: 'gray.400',
marginTop: '1.5rem',
textAlign: 'center',
})}
>
Preview of monthly calendar layout (actual PDF will be optimized for printing)
</p>
</div>
)
}