Files
site-builder/craft/src/panels/right/styles/PricingStylePanel.tsx
T
shadowdaoandClaude Opus 4.6 91a6b6f34b Add Craft.js site builder (v2) - complete rebuild from GrapesJS
Rebuilt the visual site builder from scratch using Craft.js, React 18,
and TypeScript. The new editor renders directly in the DOM (no iframe),
supports 40+ components, multi-page with shared header/footer, 16
templates, full-spectrum color/gradient controls, custom head code
injection, save/publish workflow, and auto-save.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 18:31:16 -07:00

211 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
</>
);
};