211 lines
10 KiB
TypeScript
211 lines
10 KiB
TypeScript
import React, { useCallback, useState } from 'react';
|
||||
|
|
import { useEditor } from '@craftjs/core';
|
|||
|
|
import {
|
|||
|
|
StylePanelProps,
|
|||
|
|
CollapsibleSection,
|
|||
|
|
ColorPickerField,
|
|||
|
|
labelStyle,
|
|||
|
|
inputStyle,
|
|||
|
|
smallInputStyle,
|
|||
|
|
btnActiveStyle,
|
|||
|
|
sectionGap,
|
|||
|
|
} from './shared';
|
|||
|
|
|
|||
|
|
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 [expandedPlan, setExpandedPlan] = useState<number>(0);
|
|||
|
|
|
|||
|
|
const plans: any[] = Array.isArray(nodeProps.plans) ? nodeProps.plans : [];
|
|||
|
|
const currentBullet = nodeProps.bulletType || 'check';
|
|||
|
|
|
|||
|
|
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; })} />
|
|||
|
|
</CollapsibleSection>
|
|||
|
|
</>
|
|||
|
|
);
|
|||
|
|
};
|