Files
site-builder/craft/src/panels/right/styles/PricingStylePanel.tsx
T

308 lines
15 KiB
TypeScript
Raw Normal View History

import React, { useCallback, useState } from 'react';
import { useEditor } from '@craftjs/core';
import { SHADOW_PRESETS } from '../../../constants/presets';
import {
StylePanelProps,
CollapsibleSection,
ColorPickerField,
SectionLabel,
PresetButtonGrid,
labelStyle,
inputStyle,
smallInputStyle,
btnActiveStyle,
sectionGap,
useNodeProp,
SpacingControl,
BorderControl,
BorderValue,
buildBorderShorthand,
AnimationControl,
VisibilityControl,
} from './shared';
/** Parses a border shorthand string ("2px solid #hex") produced by
* buildBorderShorthand() back into its parts for round-tripping through
* BorderControl. See SectionTypePanel.tsx for the identical helper. */
function parseBorderShorthand(v: string | undefined): BorderValue {
if (!v || v === 'none') return { width: '', style: 'none', color: '#000000' };
const m = String(v).match(/^([\d.]+[a-z%]*)\s+(\w+)\s+(.+)$/);
if (!m) return { width: '', style: 'none', color: '#000000' };
return { width: m[1], style: m[2], color: m[3] };
}
const capSide = (s: string): string => s.charAt(0).toUpperCase() + s.slice(1);
const bulletOptions = [
{ label: '✓', value: 'check' },
{ label: '●', value: 'dot' },
{ label: '→', value: 'arrow' },
{ label: '★', value: 'star' },
{ label: '—', value: 'dash' },
{ label: 'None', value: 'none' },
];
const bulletChar: Record<string, string> = {
check: '✓', dot: '●', arrow: '→', star: '★', dash: '—', none: '',
};
export const PricingStylePanel: React.FC<StylePanelProps> = ({ selectedId, nodeProps }) => {
const { actions } = useEditor();
const { setProp, setPropStyle } = useNodeProp(selectedId);
const [expandedPlan, setExpandedPlan] = useState<number>(0);
const plans: any[] = Array.isArray(nodeProps.plans) ? nodeProps.plans : [];
const currentBullet = nodeProps.bulletType || 'check';
const style = nodeProps.style || {};
const updatePlan = useCallback((planIndex: number, field: string, value: any) => {
actions.setProp(selectedId, (props: any) => {
const updated = [...(Array.isArray(props.plans) ? props.plans : [])];
updated[planIndex] = { ...updated[planIndex], [field]: value };
props.plans = updated;
});
}, [actions, selectedId]);
const addPlan = useCallback(() => {
actions.setProp(selectedId, (props: any) => {
const updated = [...(Array.isArray(props.plans) ? props.plans : [])];
updated.push({
name: 'New Plan',
price: '$0',
period: '/month',
features: ['Feature 1'],
buttonText: 'Choose Plan',
buttonHref: '#',
isFeatured: false,
});
props.plans = updated;
});
}, [actions, selectedId]);
const removePlan = useCallback((index: number) => {
actions.setProp(selectedId, (props: any) => {
const updated = [...(Array.isArray(props.plans) ? props.plans : [])];
updated.splice(index, 1);
props.plans = updated;
});
}, [actions, selectedId]);
const addFeature = useCallback((planIndex: number) => {
actions.setProp(selectedId, (props: any) => {
const updated = [...(Array.isArray(props.plans) ? props.plans : [])];
const features = [...(Array.isArray(updated[planIndex]?.features) ? updated[planIndex].features : [])];
features.push('New feature');
updated[planIndex] = { ...updated[planIndex], features };
props.plans = updated;
});
}, [actions, selectedId]);
const updateFeature = useCallback((planIndex: number, featureIndex: number, value: string) => {
actions.setProp(selectedId, (props: any) => {
const updated = [...(Array.isArray(props.plans) ? props.plans : [])];
const features = [...(Array.isArray(updated[planIndex]?.features) ? updated[planIndex].features : [])];
features[featureIndex] = value;
updated[planIndex] = { ...updated[planIndex], features };
props.plans = updated;
});
}, [actions, selectedId]);
const removeFeature = useCallback((planIndex: number, featureIndex: number) => {
actions.setProp(selectedId, (props: any) => {
const updated = [...(Array.isArray(props.plans) ? props.plans : [])];
const features = [...(Array.isArray(updated[planIndex]?.features) ? updated[planIndex].features : [])];
features.splice(featureIndex, 1);
updated[planIndex] = { ...updated[planIndex], features };
props.plans = updated;
});
}, [actions, selectedId]);
return (
<>
{/* Bullet type */}
<CollapsibleSection title="Bullet Style">
<div style={{ display: 'flex', gap: 4 }}>
{bulletOptions.map((b) => (
<button key={b.value} onClick={() => actions.setProp(selectedId, (p: any) => { p.bulletType = b.value; })}
style={{ ...btnActiveStyle(currentBullet === b.value), flex: 1, fontSize: 14 }}>
{b.label}
</button>
))}
</div>
</CollapsibleSection>
{/* Plans */}
<CollapsibleSection title={`Plans (${plans.length})`}>
{plans.map((plan, i) => {
const isExpanded = expandedPlan === i;
const features: string[] = Array.isArray(plan.features) ? plan.features : [];
return (
<div key={i} style={{
marginBottom: 8, background: '#18181b', borderRadius: 6,
border: plan.isFeatured ? '1px solid #3b82f6' : '1px solid #27272a',
}}>
{/* Plan header - click to expand */}
<div onClick={() => setExpandedPlan(isExpanded ? -1 : i)} style={{
padding: '8px 10px', cursor: 'pointer', display: 'flex', justifyContent: 'space-between', alignItems: 'center',
}}>
<span style={{ fontSize: 12, fontWeight: 600, color: '#e4e4e7' }}>
{plan.name || 'Plan'} {plan.isFeatured && <span style={{ fontSize: 9, background: '#3b82f6', color: '#fff', padding: '1px 5px', borderRadius: 3, marginLeft: 4 }}>Featured</span>}
</span>
<div style={{ display: 'flex', gap: 4, alignItems: 'center' }}>
<span style={{ fontSize: 11, color: '#71717a' }}>{plan.price}</span>
<i className={`fa fa-chevron-${isExpanded ? 'up' : 'down'}`} style={{ fontSize: 10, color: '#71717a' }} />
</div>
</div>
{/* Expanded plan settings */}
{isExpanded && (
<div style={{ padding: '0 10px 10px', display: 'flex', flexDirection: 'column', gap: 6 }}>
<div style={{ display: 'flex', gap: 4 }}>
<div style={{ flex: 1 }}>
<label style={{ fontSize: 9, color: '#52525b' }}>Name</label>
<input type="text" value={plan.name || ''} onChange={(e) => updatePlan(i, 'name', e.target.value)} style={smallInputStyle} />
</div>
<div style={{ flex: 1 }}>
<label style={{ fontSize: 9, color: '#52525b' }}>Price</label>
<input type="text" value={plan.price || ''} onChange={(e) => updatePlan(i, 'price', e.target.value)} style={smallInputStyle} />
</div>
</div>
<div>
<label style={{ fontSize: 9, color: '#52525b' }}>Period</label>
<input type="text" value={plan.period || ''} onChange={(e) => updatePlan(i, 'period', e.target.value)} placeholder="/month" style={smallInputStyle} />
</div>
<div style={{ display: 'flex', gap: 4 }}>
<div style={{ flex: 1 }}>
<label style={{ fontSize: 9, color: '#52525b' }}>Button Text</label>
<input type="text" value={plan.buttonText || ''} onChange={(e) => updatePlan(i, 'buttonText', e.target.value)} style={smallInputStyle} />
</div>
<div style={{ flex: 1 }}>
<label style={{ fontSize: 9, color: '#52525b' }}>Button URL</label>
<input type="text" value={plan.buttonHref || ''} onChange={(e) => updatePlan(i, 'buttonHref', e.target.value)} style={smallInputStyle} />
</div>
</div>
<label style={{ fontSize: 10, color: '#71717a', display: 'flex', alignItems: 'center', gap: 4, cursor: 'pointer' }}>
<input type="checkbox" checked={!!plan.isFeatured} onChange={(e) => updatePlan(i, 'isFeatured', e.target.checked)} />
Featured (highlighted)
</label>
{/* Features list */}
<div>
<label style={{ fontSize: 9, color: '#52525b', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span>Features ({features.length})</span>
<button onClick={() => addFeature(i)} style={{ fontSize: 9, background: '#3b82f6', color: '#fff', border: 'none', borderRadius: 3, padding: '2px 6px', cursor: 'pointer' }}>
+ Add
</button>
</label>
<div style={{ display: 'flex', flexDirection: 'column', gap: 2, marginTop: 4 }}>
{features.map((feat, fi) => (
<div key={fi} style={{ display: 'flex', gap: 2, alignItems: 'center' }}>
<span style={{ fontSize: 11, color: '#10b981', width: 14, textAlign: 'center' }}>{bulletChar[currentBullet] || '✓'}</span>
<input type="text" value={feat} onChange={(e) => updateFeature(i, fi, e.target.value)} style={{ ...smallInputStyle, flex: 1 }} />
<button onClick={() => removeFeature(i, fi)} style={{ fontSize: 9, background: '#ef4444', color: '#fff', border: 'none', borderRadius: 3, padding: '1px 4px', cursor: 'pointer', lineHeight: 1 }}>
×
</button>
</div>
))}
</div>
</div>
{/* Remove plan */}
{plans.length > 1 && (
<button onClick={() => removePlan(i)} style={{ fontSize: 10, background: 'none', color: '#ef4444', border: '1px solid #ef4444', borderRadius: 4, padding: '3px 8px', cursor: 'pointer', marginTop: 4 }}>
Remove Plan
</button>
)}
</div>
)}
</div>
);
})}
<button onClick={addPlan} style={{ width: '100%', padding: '6px', fontSize: 11, background: '#27272a', color: '#e4e4e7', border: '1px solid #3f3f46', borderRadius: 4, cursor: 'pointer', marginTop: 4 }}>
+ Add Plan
</button>
</CollapsibleSection>
{/* Colors */}
<CollapsibleSection title="Colors" defaultOpen={false}>
<ColorPickerField label="Featured Plan Color" value={nodeProps.featuredBg || '#3b82f6'} onChange={(v) => actions.setProp(selectedId, (p: any) => { p.featuredBg = v; })} />
{/* Regular (non-featured) card colors -- built into PricingTable's
render/toHtml but previously hard-coded literals with no control
surfacing them. Each falls back to the prior literal when blank. */}
<ColorPickerField label="Card Background" value={nodeProps.cardBg || '#ffffff'} onChange={(v) => setProp('cardBg', v)} />
<ColorPickerField label="Heading / Price Color" value={nodeProps.textColor || '#18181b'} onChange={(v) => setProp('textColor', v)} />
<ColorPickerField label="Period Text Color" value={nodeProps.subColor || '#64748b'} onChange={(v) => setProp('subColor', v)} />
<ColorPickerField label="Feature Text Color" value={nodeProps.featColor || '#4b5563'} onChange={(v) => setProp('featColor', v)} />
<ColorPickerField label="Checkmark Color" value={nodeProps.checkColor || '#10b981'} onChange={(v) => setProp('checkColor', v)} />
<ColorPickerField label="Button Background" value={nodeProps.btnBg || nodeProps.featuredBg || '#3b82f6'} onChange={(v) => setProp('btnBg', v)} />
<ColorPickerField label="Button Text Color" value={nodeProps.btnColor || '#ffffff'} onChange={(v) => setProp('btnColor', v)} />
</CollapsibleSection>
{/* Box model: margin, padding, border, shadow, opacity */}
<CollapsibleSection title="Spacing & Border" defaultOpen={false}>
<SpacingControl
label="Margin"
value={{
top: style.marginTop as string, right: style.marginRight as string,
bottom: style.marginBottom as string, left: style.marginLeft as string,
}}
onChange={(side, v) => setPropStyle(`margin${capSide(side)}`, v)}
/>
<SpacingControl
label="Padding"
value={{
top: style.paddingTop as string, right: style.paddingRight as string,
bottom: style.paddingBottom as string, left: style.paddingLeft as string,
}}
onChange={(side, v) => setPropStyle(`padding${capSide(side)}`, v)}
/>
<BorderControl
value={parseBorderShorthand(style.border as string)}
onChange={(v) => setPropStyle('border', buildBorderShorthand(v))}
/>
<div className="guided-section">
<SectionLabel>Shadow</SectionLabel>
<PresetButtonGrid presets={SHADOW_PRESETS} activeValue={style.boxShadow as string} onSelect={(v) => setPropStyle('boxShadow', v)} />
</div>
<div className="guided-section">
<SectionLabel>Opacity</SectionLabel>
<input
type="range"
min={0}
max={100}
value={style.opacity !== undefined ? Math.round(Number(style.opacity) * 100) : 100}
onChange={(e) => setPropStyle('opacity', String(Number(e.target.value) / 100))}
style={{ width: '100%' }}
/>
</div>
</CollapsibleSection>
{/* Entrance animation */}
<CollapsibleSection title="Animation" defaultOpen={false}>
<AnimationControl
value={{ animation: nodeProps.animation || 'none', animationDelay: nodeProps.animationDelay || '0' }}
onChange={(v) => { setProp('animation', v.animation); setProp('animationDelay', v.animationDelay); }}
/>
</CollapsibleSection>
{/* Responsive visibility */}
<CollapsibleSection title="Visibility" defaultOpen={false}>
<VisibilityControl
value={{
hideOnDesktop: !!nodeProps.hideOnDesktop,
hideOnTablet: !!nodeProps.hideOnTablet,
hideOnMobile: !!nodeProps.hideOnMobile,
}}
onChange={(v) => {
setProp('hideOnDesktop', !!v.hideOnDesktop);
setProp('hideOnTablet', !!v.hideOnTablet);
setProp('hideOnMobile', !!v.hideOnMobile);
}}
/>
</CollapsibleSection>
</>
);
};