Merge branch 'main' into rte-posts-upgrade
This commit is contained in:
179
client/src/components/HeroElement.jsx
Normal file
179
client/src/components/HeroElement.jsx
Normal file
@@ -0,0 +1,179 @@
|
||||
import { Link } from 'react-router-dom'
|
||||
|
||||
const MOON_IMAGE = '/assets/img/hero-moon.png'
|
||||
|
||||
// Font family tokens a line may opt into; default is the page serif.
|
||||
const FONT = { display: 'var(--display)', sans: 'var(--sans)' }
|
||||
|
||||
// fontSize may be a number (px, from the editor) or a CSS string (e.g. a clamp()
|
||||
// used by the pre-populated default so the hero stays responsive until edited).
|
||||
function sizeToCss(v) {
|
||||
return typeof v === 'number' ? `${v}px` : v
|
||||
}
|
||||
|
||||
function lineStyle(line) {
|
||||
return {
|
||||
display: 'block', // each line stacks (so a span line behaves like the others)
|
||||
margin: line.marginTop != null ? `${line.marginTop}px 0 0` : '0',
|
||||
fontFamily: FONT[line.font] || undefined,
|
||||
fontSize: sizeToCss(line.fontSize),
|
||||
color: line.color || 'inherit',
|
||||
fontWeight: line.weight || undefined,
|
||||
fontStyle: line.italic ? 'italic' : undefined,
|
||||
letterSpacing: line.letterSpacing || undefined,
|
||||
textTransform: line.transform || undefined,
|
||||
lineHeight: line.lineHeight || undefined,
|
||||
maxWidth: line.maxWidth ? `${line.maxWidth}px` : undefined,
|
||||
marginLeft: line.maxWidth ? 'auto' : undefined,
|
||||
marginRight: line.maxWidth ? 'auto' : undefined,
|
||||
}
|
||||
}
|
||||
|
||||
function TextBlock({ props }) {
|
||||
const align = props.align || 'center'
|
||||
return (
|
||||
<div style={{ textAlign: align, textShadow: '0 2px 22px rgba(0,0,0,0.82)' }}>
|
||||
{(props.lines || []).map((line, i) => {
|
||||
const Tag = /^(h1|h2|h3|p|span)$/.test(line.tag) ? line.tag : 'p'
|
||||
return (
|
||||
<Tag key={i} style={lineStyle(line)}>
|
||||
{line.text}
|
||||
</Tag>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function Buttons({ props }) {
|
||||
const justify = props.align === 'left' ? 'flex-start' : props.align === 'right' ? 'flex-end' : 'center'
|
||||
return (
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: props.gap ?? 12, justifyContent: justify }}>
|
||||
{(props.items || []).map((b, i) => (
|
||||
<Link key={i} to={b.to || '#'} className={`btn ${b.variant === 'ghost' ? 'btn-ghost' : 'btn-primary'}`}>
|
||||
{b.label}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function Badge({ props }) {
|
||||
return (
|
||||
<span
|
||||
className="sans"
|
||||
style={{
|
||||
display: 'inline-block',
|
||||
padding: '6px 14px',
|
||||
background: props.bgColor || 'rgba(11,22,48,0.6)',
|
||||
color: props.textColor || '#c2d2e6',
|
||||
borderRadius: props.borderRadius ?? 999,
|
||||
fontSize: '0.74rem',
|
||||
fontWeight: 700,
|
||||
letterSpacing: '0.18em',
|
||||
textTransform: 'uppercase',
|
||||
}}
|
||||
>
|
||||
{props.text}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
function HeroImage({ props }) {
|
||||
if (!props.src) {
|
||||
// Editor placeholder until an image is chosen (a srcless image never ships live).
|
||||
return (
|
||||
<div
|
||||
className="sans"
|
||||
style={{ width: 160, height: 100, display: 'grid', placeItems: 'center', border: '1px dashed var(--accent)', borderRadius: 8, color: 'var(--muted)', fontSize: '0.8rem', background: 'rgba(11,22,48,0.4)' }}
|
||||
>
|
||||
Upload an image
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<img
|
||||
src={props.src}
|
||||
alt={props.alt || ''}
|
||||
style={{ width: `${props.width || 40}%`, height: 'auto', display: 'block', borderRadius: 8 }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function content(element) {
|
||||
switch (element.type) {
|
||||
case 'text_block':
|
||||
return <TextBlock props={element.props || {}} />
|
||||
case 'buttons':
|
||||
return <Buttons props={element.props || {}} />
|
||||
case 'moon': {
|
||||
const size = element.props?.size || 96
|
||||
const glow = element.props?.glow ?? 0.45
|
||||
return (
|
||||
<img
|
||||
src={MOON_IMAGE}
|
||||
alt=""
|
||||
draggable={false}
|
||||
style={{
|
||||
width: size,
|
||||
height: 'auto',
|
||||
display: 'block',
|
||||
filter: glow ? `drop-shadow(0 0 ${size * 0.45}px rgba(216,226,239,${glow}))` : undefined,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
case 'badge':
|
||||
return <Badge props={element.props || {}} />
|
||||
case 'image':
|
||||
return <HeroImage props={element.props || {}} />
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// Absolute-positioned wrapper + type-specific content. In `editor` mode the inner
|
||||
// content is made non-interactive (so clicks select/drag the wrapper) and the
|
||||
// wrapper takes selection styling + an onPointerDown handler.
|
||||
export default function HeroElement({
|
||||
element,
|
||||
wrapperStyle,
|
||||
editor = false,
|
||||
selected = false,
|
||||
onPointerDown,
|
||||
children,
|
||||
}) {
|
||||
const anchor = element.anchor || 'center'
|
||||
const transform =
|
||||
anchor === 'center'
|
||||
? 'translate(-50%, -50%)'
|
||||
: anchor === 'top-right'
|
||||
? 'translateX(-100%)'
|
||||
: undefined
|
||||
// text_block/buttons may set a box width (px); kept within the containing block
|
||||
// (the hero section live, or the editor canvas) with small side gutters.
|
||||
const boxWidth =
|
||||
(element.type === 'text_block' || element.type === 'buttons') && element.props?.width
|
||||
? `min(${element.props.width}px, calc(100% - 36px))`
|
||||
: undefined
|
||||
const cls = [editor ? 'hero-el-editable' : '', selected ? 'is-selected' : ''].filter(Boolean).join(' ')
|
||||
return (
|
||||
<div
|
||||
className={cls || undefined}
|
||||
onPointerDown={onPointerDown}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: `${element.x}%`,
|
||||
top: `${element.y}%`,
|
||||
zIndex: element.z || 0,
|
||||
transform,
|
||||
width: boxWidth,
|
||||
cursor: editor ? 'move' : undefined,
|
||||
...wrapperStyle,
|
||||
}}
|
||||
>
|
||||
<div style={editor ? { pointerEvents: 'none' } : undefined}>{content(element)}</div>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,9 +1,8 @@
|
||||
// The little glowing moon used in the logo, login, and maintenance screens.
|
||||
export default function MoonDot({ size = 13, glow = 0.45 }) {
|
||||
return (
|
||||
<span
|
||||
className="moon"
|
||||
style={{ width: size, height: size, boxShadow: `0 0 ${size * 0.8}px rgba(216,226,239,${glow})` }}
|
||||
/>
|
||||
)
|
||||
// The little glowing moon used in the logo, login, maintenance screens, and the
|
||||
// hero canvas. `color` overrides the radial-gradient start point (else the CSS
|
||||
// .moon default is used).
|
||||
export default function MoonDot({ size = 13, glow = 0.45, color }) {
|
||||
const style = { width: size, height: size, boxShadow: `0 0 ${size * 0.8}px rgba(216,226,239,${glow})` }
|
||||
if (color) style.background = `radial-gradient(circle at 35% 30%, ${color}, #9fb0c6 55%, #5d6e88)`
|
||||
return <span className="moon" style={style} />
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user