Files
site-builder/craft/src/components/sections/Countdown.tsx

312 lines
9.8 KiB
TypeScript
Raw Normal View History

import React, { CSSProperties, useEffect, useState, useCallback } from 'react';
import { useNode, UserComponent } from '@craftjs/core';
import { cssPropsToString } from '../../utils/style-helpers';
interface CountdownProps {
targetDate?: string;
heading?: string;
style?: CSSProperties;
digitColor?: string;
labelColor?: string;
bgColor?: string;
}
interface TimeLeft {
days: number;
hours: number;
minutes: number;
seconds: number;
}
function getDefaultTargetDate(): string {
const d = new Date();
d.setDate(d.getDate() + 30);
return d.toISOString().split('T')[0];
}
function calcTimeLeft(target: string): TimeLeft {
const diff = new Date(target).getTime() - Date.now();
if (diff <= 0) return { days: 0, hours: 0, minutes: 0, seconds: 0 };
return {
days: Math.floor(diff / (1000 * 60 * 60 * 24)),
hours: Math.floor((diff / (1000 * 60 * 60)) % 24),
minutes: Math.floor((diff / (1000 * 60)) % 60),
seconds: Math.floor((diff / 1000) % 60),
};
}
const DEFAULT_TARGET = getDefaultTargetDate();
export const Countdown: UserComponent<CountdownProps> = ({
targetDate = DEFAULT_TARGET,
heading = 'Coming Soon',
style = {},
digitColor = '#ffffff',
labelColor = 'rgba(255,255,255,0.7)',
bgColor = '#18181b',
}) => {
const {
connectors: { connect, drag },
selected,
} = useNode((node) => ({
selected: node.events.selected,
}));
const [timeLeft, setTimeLeft] = useState<TimeLeft>(() => calcTimeLeft(targetDate));
useEffect(() => {
setTimeLeft(calcTimeLeft(targetDate));
const interval = setInterval(() => {
setTimeLeft(calcTimeLeft(targetDate));
}, 1000);
return () => clearInterval(interval);
}, [targetDate]);
const pad = (n: number) => String(n).padStart(2, '0');
const boxStyle: CSSProperties = {
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: '4px',
minWidth: '80px',
};
const digitStyle: CSSProperties = {
fontSize: '48px',
fontWeight: '700',
color: digitColor,
lineHeight: '1',
fontFamily: 'Inter, sans-serif',
};
const unitLabelStyle: CSSProperties = {
fontSize: '12px',
color: labelColor,
textTransform: 'uppercase',
letterSpacing: '0.1em',
fontFamily: 'Inter, sans-serif',
};
const units: Array<{ label: string; value: number }> = [
{ label: 'Days', value: timeLeft.days },
{ label: 'Hours', value: timeLeft.hours },
{ label: 'Minutes', value: timeLeft.minutes },
{ label: 'Seconds', value: timeLeft.seconds },
];
return (
<section
ref={(ref: HTMLElement | null): void => { if (ref) connect(drag(ref)); }}
style={{
padding: '60px 20px',
textAlign: 'center',
backgroundColor: bgColor,
outline: selected ? '2px solid #3b82f6' : 'none',
...style,
}}
>
{heading && (
<h2 style={{ fontSize: '32px', fontWeight: '700', color: digitColor, marginBottom: '32px', fontFamily: 'Inter, sans-serif' }}>
{heading}
</h2>
)}
<div style={{ display: 'flex', justifyContent: 'center', gap: '24px', flexWrap: 'wrap' }}>
{units.map((u) => (
<div key={u.label} style={boxStyle}>
<span style={digitStyle}>{pad(u.value)}</span>
<span style={unitLabelStyle}>{u.label}</span>
</div>
))}
</div>
</section>
);
};
/* ---------- Settings panel ---------- */
const CountdownSettings: React.FC = () => {
const { actions: { setProp }, props } = useNode((node) => ({
props: node.data.props as CountdownProps,
}));
const labelStyle: CSSProperties = { fontSize: 11, color: '#a1a1aa', display: 'block', marginBottom: 6 };
const inputStyle: CSSProperties = {
width: '100%', padding: '4px 8px', background: '#27272a', color: '#e4e4e7',
border: '1px solid #3f3f46', borderRadius: 4, fontSize: 12,
};
const colorPresets = ['#ffffff', '#f8fafc', '#3b82f6', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6', '#ec4899'];
const bgPresets = ['#18181b', '#0f172a', '#1e293b', '#1e1b4b', '#042f2e', '#27272a', '#ffffff', '#f8fafc'];
return (
<div style={{ padding: '12px', display: 'flex', flexDirection: 'column', gap: '14px' }}>
{/* Target date */}
<div>
<label style={labelStyle}>Target Date</label>
<input
type="date"
value={props.targetDate || DEFAULT_TARGET}
onChange={(e) => setProp((p: CountdownProps) => { p.targetDate = e.target.value; })}
style={inputStyle}
/>
</div>
{/* Heading */}
<div>
<label style={labelStyle}>Heading</label>
<input
type="text"
value={props.heading || ''}
onChange={(e) => setProp((p: CountdownProps) => { p.heading = e.target.value; })}
placeholder="Coming Soon"
style={inputStyle}
/>
</div>
{/* Digit color */}
<div>
<label style={labelStyle}>Digit Color</label>
<div style={{ display: 'flex', gap: 4, flexWrap: 'wrap' }}>
{colorPresets.map((c) => (
<button
key={c}
onClick={() => setProp((p: CountdownProps) => { p.digitColor = c; })}
style={{
width: 24, height: 24, borderRadius: 4, border: '1px solid #3f3f46',
backgroundColor: c, cursor: 'pointer',
outline: props.digitColor === c ? '2px solid #3b82f6' : 'none',
outlineOffset: 1,
}}
/>
))}
</div>
</div>
{/* Label color */}
<div>
<label style={labelStyle}>Label Color</label>
<div style={{ display: 'flex', gap: 4, flexWrap: 'wrap' }}>
{colorPresets.map((c) => (
<button
key={c}
onClick={() => setProp((p: CountdownProps) => { p.labelColor = c; })}
style={{
width: 24, height: 24, borderRadius: 4, border: '1px solid #3f3f46',
backgroundColor: c, cursor: 'pointer',
outline: props.labelColor === c ? '2px solid #3b82f6' : 'none',
outlineOffset: 1,
}}
/>
))}
</div>
</div>
{/* Background color */}
<div>
<label style={labelStyle}>Background</label>
<div style={{ display: 'flex', gap: 4, flexWrap: 'wrap' }}>
{bgPresets.map((c) => (
<button
key={c}
onClick={() => setProp((p: CountdownProps) => { p.bgColor = c; })}
style={{
width: 24, height: 24, borderRadius: 4, border: '1px solid #3f3f46',
backgroundColor: c, cursor: 'pointer',
outline: props.bgColor === c ? '2px solid #3b82f6' : 'none',
outlineOffset: 1,
}}
/>
))}
</div>
</div>
</div>
);
};
/* ---------- Craft config ---------- */
Countdown.craft = {
displayName: 'Countdown',
props: {
targetDate: DEFAULT_TARGET,
heading: 'Coming Soon',
style: {},
digitColor: '#ffffff',
labelColor: 'rgba(255,255,255,0.7)',
bgColor: '#18181b',
},
rules: {
canDrag: () => true,
canMoveIn: () => false,
canMoveOut: () => true,
},
related: {
settings: CountdownSettings,
},
};
/* ---------- HTML export ---------- */
(Countdown as any).toHtml = (props: CountdownProps, _childrenHtml: string) => {
const esc = (s: any) => String(s ?? "").replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
const {
targetDate = DEFAULT_TARGET,
heading = 'Coming Soon',
style = {},
digitColor = '#ffffff',
labelColor = 'rgba(255,255,255,0.7)',
bgColor = '#18181b',
} = props;
const sectionStyle = cssPropsToString({
padding: '60px 20px',
textAlign: 'center',
backgroundColor: bgColor,
...style,
});
const headingHtml = heading
? `<h2 style="font-size:32px;font-weight:700;color:${digitColor};margin-bottom:32px;font-family:Inter,sans-serif">${esc(heading)}</h2>`
: '';
const boxStyle = 'display:flex;flex-direction:column;align-items:center;gap:4px;min-width:80px';
const dStyle = `font-size:48px;font-weight:700;color:${digitColor};line-height:1;font-family:Inter,sans-serif`;
const lStyle = `font-size:12px;color:${labelColor};text-transform:uppercase;letter-spacing:0.1em;font-family:Inter,sans-serif`;
// Generate a unique ID for this countdown instance
const uid = 'cd_' + Math.random().toString(36).slice(2, 8);
return {
html: `<section${sectionStyle ? ` style="${sectionStyle}"` : ''}>
${headingHtml}
<div style="display:flex;justify-content:center;gap:24px;flex-wrap:wrap">
<div style="${boxStyle}"><span id="${uid}_d" style="${dStyle}">00</span><span style="${lStyle}">Days</span></div>
<div style="${boxStyle}"><span id="${uid}_h" style="${dStyle}">00</span><span style="${lStyle}">Hours</span></div>
<div style="${boxStyle}"><span id="${uid}_m" style="${dStyle}">00</span><span style="${lStyle}">Minutes</span></div>
<div style="${boxStyle}"><span id="${uid}_s" style="${dStyle}">00</span><span style="${lStyle}">Seconds</span></div>
</div>
<script>
(function(){
var target = new Date("${targetDate}").getTime();
function pad(n){ return String(n).padStart(2,'0'); }
function update(){
var diff = target - Date.now();
if(diff<=0){ diff=0; }
var d = Math.floor(diff/(1000*60*60*24));
var h = Math.floor((diff/(1000*60*60))%24);
var m = Math.floor((diff/(1000*60))%60);
var s = Math.floor((diff/1000)%60);
document.getElementById("${uid}_d").textContent = pad(d);
document.getElementById("${uid}_h").textContent = pad(h);
document.getElementById("${uid}_m").textContent = pad(m);
document.getElementById("${uid}_s").textContent = pad(s);
}
update();
setInterval(update,1000);
})();
</script>
</section>`,
};
};