188 lines
8.8 KiB
JavaScript
188 lines
8.8 KiB
JavaScript
import React from 'react'
|
|
|
|
const aspectRatios = {
|
|
square: '1 / 1',
|
|
portrait: '4 / 5',
|
|
story: '9 / 16',
|
|
landscape: '16 / 9',
|
|
}
|
|
|
|
const placementStyles = {
|
|
'top-left': { top: '12%', left: '12%' },
|
|
'top-right': { top: '12%', right: '12%' },
|
|
'bottom-left': { bottom: '12%', left: '12%' },
|
|
'bottom-right': { bottom: '12%', right: '12%' },
|
|
}
|
|
|
|
function overlayStyle(style) {
|
|
if (style === 'dark-strong') return 'linear-gradient(180deg, rgba(2,6,23,0.38), rgba(2,6,23,0.68))'
|
|
if (style === 'light-soft') return 'linear-gradient(180deg, rgba(255,255,255,0.08), rgba(255,255,255,0.22))'
|
|
if (style === 'dark-soft') return 'linear-gradient(180deg, rgba(2,6,23,0.18), rgba(2,6,23,0.48))'
|
|
return 'none'
|
|
}
|
|
|
|
function positionStyle(position) {
|
|
if (position === 'top') return { alignItems: 'flex-start', paddingTop: '10%' }
|
|
if (position === 'upper-middle') return { alignItems: 'flex-start', paddingTop: '22%' }
|
|
if (position === 'lower-middle') return { alignItems: 'flex-end', paddingBottom: '18%' }
|
|
if (position === 'bottom') return { alignItems: 'flex-end', paddingBottom: '10%' }
|
|
return { alignItems: 'center' }
|
|
}
|
|
|
|
function alignmentClass(alignment) {
|
|
if (alignment === 'left') return 'items-start text-left'
|
|
if (alignment === 'right') return 'items-end text-right'
|
|
return 'items-center text-center'
|
|
}
|
|
|
|
function focalPositionStyle(position) {
|
|
if (position === 'top') return 'center top'
|
|
if (position === 'bottom') return 'center bottom'
|
|
if (position === 'left') return 'left center'
|
|
if (position === 'right') return 'right center'
|
|
if (position === 'top-left') return 'left top'
|
|
if (position === 'top-right') return 'right top'
|
|
if (position === 'bottom-left') return 'left bottom'
|
|
if (position === 'bottom-right') return 'right bottom'
|
|
return 'center center'
|
|
}
|
|
|
|
function shadowValue(preset) {
|
|
if (preset === 'none') return 'none'
|
|
if (preset === 'strong') return '0 12px 36px rgba(2, 6, 23, 0.72)'
|
|
return '0 8px 24px rgba(2, 6, 23, 0.5)'
|
|
}
|
|
|
|
function resolveTextBlocks(card, project) {
|
|
const blocks = Array.isArray(project.text_blocks) ? project.text_blocks.filter((block) => block?.enabled !== false && String(block?.text || '').trim() !== '') : []
|
|
if (blocks.length) return blocks
|
|
|
|
const content = project.content || {}
|
|
|
|
return [
|
|
{ key: 'title', type: 'title', text: card?.title || content.title || 'Untitled card', enabled: true },
|
|
{ key: 'quote', type: 'quote', text: card?.quote_text || content.quote_text || 'Your next quote starts here.', enabled: true },
|
|
{ key: 'author', type: 'author', text: card?.quote_author || content.quote_author || '', enabled: Boolean(card?.quote_author || content.quote_author) },
|
|
{ key: 'source', type: 'source', text: card?.quote_source || content.quote_source || '', enabled: Boolean(card?.quote_source || content.quote_source) },
|
|
].filter((block) => String(block.text || '').trim() !== '')
|
|
}
|
|
|
|
function blockClass(type) {
|
|
if (type === 'title') return 'text-[11px] font-semibold uppercase tracking-[0.28em] text-white/55'
|
|
if (type === 'author') return 'font-medium uppercase tracking-[0.18em] text-white/80 sm:text-sm lg:text-base'
|
|
if (type === 'source') return 'text-[11px] uppercase tracking-[0.18em] text-white/50 sm:text-xs'
|
|
if (type === 'caption') return 'text-[10px] uppercase tracking-[0.2em] text-white/45'
|
|
if (type === 'body') return 'text-sm leading-6 text-white/90 sm:text-base'
|
|
return 'font-semibold tracking-[-0.03em] sm:text-[1.65rem] lg:text-[2.1rem]'
|
|
}
|
|
|
|
function blockStyle(type, typography, textColor, accentColor) {
|
|
const quoteSize = Math.max(26, Math.min(typography.quote_size || 72, 120))
|
|
const authorSize = Math.max(14, Math.min(typography.author_size || 28, 42))
|
|
const letterSpacing = Math.max(-1, Math.min(typography.letter_spacing || 0, 10))
|
|
const lineHeight = Math.max(0.9, Math.min(typography.line_height || 1.2, 1.8))
|
|
const shadowPreset = typography.shadow_preset || 'soft'
|
|
|
|
if (type === 'title') {
|
|
return { color: accentColor, letterSpacing: `${Math.max(letterSpacing, 0) / 10}em`, textShadow: shadowValue(shadowPreset) }
|
|
}
|
|
|
|
if (type === 'author' || type === 'source') {
|
|
return { color: accentColor, fontSize: `${authorSize / 4}px`, textShadow: shadowValue(shadowPreset) }
|
|
}
|
|
|
|
if (type === 'body' || type === 'caption') {
|
|
return { color: textColor, lineHeight, textShadow: shadowValue(shadowPreset) }
|
|
}
|
|
|
|
return { color: textColor, fontSize: `${quoteSize / 4}px`, lineHeight, letterSpacing: `${letterSpacing / 12}px`, textShadow: shadowValue(shadowPreset) }
|
|
}
|
|
|
|
export default function NovaCardCanvasPreview({ card, className = '' }) {
|
|
const project = card?.project_json || {}
|
|
const layout = project.layout || {}
|
|
const typography = project.typography || {}
|
|
const background = project.background || {}
|
|
const backgroundImage = card?.background_image?.processed_url
|
|
const colors = Array.isArray(background.gradient_colors) && background.gradient_colors.length >= 2
|
|
? background.gradient_colors
|
|
: ['#0f172a', '#1d4ed8']
|
|
const backgroundStyle = background.type === 'upload' && backgroundImage
|
|
? `url(${backgroundImage}) ${focalPositionStyle(background.focal_position)}/cover no-repeat`
|
|
: background.type === 'solid'
|
|
? background.solid_color || '#111827'
|
|
: `linear-gradient(180deg, ${colors[0]}, ${colors[1]})`
|
|
|
|
const textBlocks = resolveTextBlocks(card, project)
|
|
const decorations = Array.isArray(project.decorations) ? project.decorations : []
|
|
const assetItems = Array.isArray(project.assets?.items) ? project.assets.items : []
|
|
const textColor = typography.text_color || '#ffffff'
|
|
const accentColor = typography.accent_color || textColor
|
|
const maxWidth = layout.max_width === 'compact' ? '62%' : layout.max_width === 'wide' ? '88%' : '76%'
|
|
const padding = layout.padding === 'tight' ? '8%' : layout.padding === 'airy' ? '14%' : '11%'
|
|
|
|
return (
|
|
<div className={`relative overflow-hidden rounded-[28px] border border-white/10 bg-slate-950 shadow-[0_30px_80px_rgba(2,6,23,0.35)] ${className}`} style={{ aspectRatio: aspectRatios[card?.format || 'square'] || aspectRatios.square }}>
|
|
<div className="absolute inset-0" style={{ background: backgroundStyle, filter: background.type === 'upload' && Number(background.blur_level || 0) > 0 ? `blur(${Math.max(Number(background.blur_level) / 8, 0)}px)` : undefined }} />
|
|
<div className="absolute inset-0" style={{ background: overlayStyle(background.overlay_style), opacity: Math.max(0, Math.min(Number(background.opacity || 50), 100)) / 100 }} />
|
|
<div className="absolute left-4 top-4 rounded-full border border-white/10 bg-black/25 px-3 py-1 text-[10px] font-semibold uppercase tracking-[0.18em] text-white/80 backdrop-blur">
|
|
{(card?.format || 'square').replace('-', ' ')}
|
|
</div>
|
|
|
|
{decorations.slice(0, 6).map((decoration, index) => {
|
|
const placement = placementStyles[decoration.placement] || placementStyles['top-right']
|
|
return (
|
|
<div
|
|
key={`${decoration.key || decoration.glyph || 'dec'}-${index}`}
|
|
className="absolute text-white/85 drop-shadow-[0_6px_14px_rgba(2,6,23,0.3)]"
|
|
style={{
|
|
...placement,
|
|
color: accentColor,
|
|
fontSize: `${Math.max(18, Math.min(decoration.size || 28, 64))}px`,
|
|
}}
|
|
>
|
|
{decoration.glyph || '✦'}
|
|
</div>
|
|
)
|
|
})}
|
|
|
|
{assetItems.slice(0, 6).map((item, index) => {
|
|
if (item?.type === 'frame') {
|
|
const top = index % 2 === 0 ? '10%' : '88%'
|
|
return <div key={`${item.asset_key || item.label || 'frame'}-${index}`} className="absolute left-[12%] right-[12%] h-px bg-white/35" style={{ top }} />
|
|
}
|
|
|
|
return (
|
|
<div
|
|
key={`${item.asset_key || item.label || 'asset'}-${index}`}
|
|
className="absolute text-white/85 drop-shadow-[0_6px_14px_rgba(2,6,23,0.3)]"
|
|
style={{
|
|
top: `${12 + ((index % 3) * 18)}%`,
|
|
left: `${10 + (Math.floor(index / 3) * 72)}%`,
|
|
color: accentColor,
|
|
fontSize: `${Math.max(18, Math.min(item.size || 26, 56))}px`,
|
|
}}
|
|
>
|
|
{item.glyph || item.label || '✦'}
|
|
</div>
|
|
)
|
|
})}
|
|
|
|
<div className={`relative flex h-full w-full ${alignmentClass(layout.alignment)}`} style={{ padding, ...positionStyle(layout.position) }}>
|
|
<div className="flex w-full flex-col gap-4" style={{ maxWidth }}>
|
|
{textBlocks.map((block, index) => {
|
|
const type = block?.type || 'body'
|
|
const text = type === 'author' ? `— ${block.text}` : block.text
|
|
|
|
return (
|
|
<div key={`${block.key || type}-${index}`} style={blockStyle(type, typography, textColor, accentColor)} className={blockClass(type)}>
|
|
{text}
|
|
</div>
|
|
)
|
|
})}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|