diff --git a/admin-spa/src/components/ProductCard.tsx b/admin-spa/src/components/ProductCard.tsx index 8ae569e..31ec7f8 100644 --- a/admin-spa/src/components/ProductCard.tsx +++ b/admin-spa/src/components/ProductCard.tsx @@ -1 +1,38 @@ -export function ProductCard({ product }: any) { return
{product?.title || 'Product'}
; } +import React from 'react'; +import { ShoppingCart } from 'lucide-react'; + +export function ProductCard({ product }: any) { + const name = product?.name || product?.title || 'Sample Product'; + const price = product?.price || '$49.99'; + const image = product?.image || product?.image_url || ''; + + return ( +
+
+ {image ? ( + {name} + ) : ( +
+ No Image +
+ )} +
+
+

+ {name} +

+
+ + {price} + +
+ +
+
+ ); +} diff --git a/admin-spa/src/components/SharedContentLayout.tsx b/admin-spa/src/components/SharedContentLayout.tsx index b678a0f..fb22fbd 100644 --- a/admin-spa/src/components/SharedContentLayout.tsx +++ b/admin-spa/src/components/SharedContentLayout.tsx @@ -78,161 +78,79 @@ export const SharedContentLayout: React.FC = ({ return (
- {containerWidth === 'boxed' ? ( -
-
- {/* Image Side */} - {hasImage && ( -
- {title -
- )} - - {/* Content Side */} -
- {title && ( -

- {title} -

- )} - - - - {text && ( -
- )} - - - - {/* Buttons */} - {buttons && buttons.length > 0 && ( -
- {buttons.map((btn, idx) => ( - btn.text && btn.url && ( - - {btn.text} - - ) - ))} -
- )} -
+
+ {/* Image Side */} + {hasImage && ( +
+ {title
-
- ) : ( -
- {/* Image Side */} - {hasImage && ( -
- {title -
+ )} + + {/* Content Side */} +
+ {title && ( +

+ {title} +

)} - {/* Content Side */} -
- {title && ( -

- {title} -

- )} + {text && ( +
+ )} - {text && ( -
- )} - - {/* Buttons */} - {buttons && buttons.length > 0 && ( -
- {buttons.map((btn, idx) => ( - btn.text && btn.url && ( - - {btn.text} - - ) - ))} -
- )} -
+ {/* Buttons */} + {buttons && buttons.length > 0 && ( +
+ {buttons.map((btn, idx) => ( + btn.text && btn.url && ( + + {btn.text} + + ) + ))} +
+ )}
- )} +
); }; diff --git a/admin-spa/src/components/ui/rich-text-editor.tsx b/admin-spa/src/components/ui/rich-text-editor.tsx index d9c9e7a..a82f4a9 100644 --- a/admin-spa/src/components/ui/rich-text-editor.tsx +++ b/admin-spa/src/components/ui/rich-text-editor.tsx @@ -5,7 +5,6 @@ import Placeholder from '@tiptap/extension-placeholder'; import Link from '@tiptap/extension-link'; import TextAlign from '@tiptap/extension-text-align'; import Image from '@tiptap/extension-image'; -import { ButtonExtension } from './tiptap-button-extension'; import { openWPMediaImage } from '@/lib/wp-media'; import { Bold, @@ -17,7 +16,6 @@ import { AlignCenter, AlignRight, ImageIcon, - MousePointer, Undo, Redo, } from 'lucide-react'; @@ -50,8 +48,6 @@ export function RichTextEditor({ Placeholder.configure({ placeholder, }), - // ButtonExtension MUST come before Link to ensure buttons are parsed first - ButtonExtension, Link.configure({ openOnClick: false, HTMLAttributes: { @@ -109,13 +105,6 @@ export function RichTextEditor({ } }; - const [buttonDialogOpen, setButtonDialogOpen] = useState(false); - const [buttonText, setButtonText] = useState('Click Here'); - const [buttonHref, setButtonHref] = useState('{order_url}'); - const [buttonStyle, setButtonStyle] = useState<'solid' | 'outline' | 'link'>('solid'); - const [isEditingButton, setIsEditingButton] = useState(false); - const [editingButtonPos, setEditingButtonPos] = useState(null); - const addImage = () => { openWPMediaImage((file) => { editor.chain().focus().setImage({ @@ -126,87 +115,6 @@ export function RichTextEditor({ }); }; - const openButtonDialog = () => { - setButtonText('Click Here'); - setButtonHref('{order_url}'); - setButtonStyle('solid'); - setIsEditingButton(false); - setEditingButtonPos(null); - setButtonDialogOpen(true); - }; - - // Handle clicking on buttons in the editor to edit them - const handleEditorClick = (e: React.MouseEvent) => { - const target = e.target as HTMLElement; - const buttonEl = target.closest('a[data-button]') as HTMLElement | null; - - if (buttonEl && editor) { - e.preventDefault(); - e.stopPropagation(); - - // Get button attributes - const text = buttonEl.getAttribute('data-text') || buttonEl.textContent?.replace('🔘 ', '') || 'Click Here'; - const href = buttonEl.getAttribute('data-href') || '#'; - const style = (buttonEl.getAttribute('data-style') as 'solid' | 'outline') || 'solid'; - - // Find the position of this button node - const { state } = editor.view; - let foundPos: number | null = null; - - state.doc.descendants((node, pos) => { - if (node.type.name === 'button' && - node.attrs.text === text && - node.attrs.href === href) { - foundPos = pos; - return false; // Stop iteration - } - return true; - }); - - // Open dialog in edit mode - setButtonText(text); - setButtonHref(href); - setButtonStyle(style); - setIsEditingButton(true); - setEditingButtonPos(foundPos); - setButtonDialogOpen(true); - } - }; - - const insertButton = () => { - if (isEditingButton && editingButtonPos !== null && editor) { - // Delete old button and insert new one at same position - editor - .chain() - .focus() - .deleteRange({ from: editingButtonPos, to: editingButtonPos + 1 }) - .insertContentAt(editingButtonPos, { - type: 'button', - attrs: { text: buttonText, href: buttonHref, style: buttonStyle }, - }) - .run(); - } else { - // Insert new button - editor.chain().focus().setButton({ text: buttonText, href: buttonHref, style: buttonStyle }).run(); - } - setButtonDialogOpen(false); - setIsEditingButton(false); - setEditingButtonPos(null); - }; - - const deleteButton = () => { - if (editingButtonPos !== null && editor) { - editor - .chain() - .focus() - .deleteRange({ from: editingButtonPos, to: editingButtonPos + 1 }) - .run(); - setButtonDialogOpen(false); - setIsEditingButton(false); - setEditingButtonPos(null); - } - }; - const getActiveHeading = () => { if (editor.isActive('heading', { level: 1 })) return 'h1'; if (editor.isActive('heading', { level: 2 })) return 'h2'; @@ -326,14 +234,6 @@ export function RichTextEditor({ > -
- )} - - - - -
); } diff --git a/admin-spa/src/lib/api/client.ts b/admin-spa/src/lib/api/client.ts index 5025cac..eca5c7e 100644 --- a/admin-spa/src/lib/api/client.ts +++ b/admin-spa/src/lib/api/client.ts @@ -1,2 +1,3 @@ import { api } from '../api'; export const apiClient = api; +export { api }; diff --git a/admin-spa/src/routes/Appearance/Pages/components/CanvasRenderer.tsx b/admin-spa/src/routes/Appearance/Pages/components/CanvasRenderer.tsx index f3911c1..5ebe87f 100644 --- a/admin-spa/src/routes/Appearance/Pages/components/CanvasRenderer.tsx +++ b/admin-spa/src/routes/Appearance/Pages/components/CanvasRenderer.tsx @@ -107,6 +107,8 @@ function withSectionWrapper(Component: any) { colorScheme={section.colorScheme} elementStyles={section.elementStyles} styles={section.styles} + isEditor={true} + section={section} {...flatProps} /> ); @@ -210,7 +212,7 @@ export function CanvasRenderer({ 'bg-white transition-all duration-300 min-h-[500px] wn-page', deviceMode === 'mobile' ? 'max-w-sm mx-auto shadow-2xl rounded-[2.5rem] border-[12px] border-gray-800 my-8 overflow-hidden' - : cn('h-full', containerWidth === 'boxed' ? 'container mx-auto max-w-6xl shadow-sm border-x border-b' : 'w-full') + : cn('min-h-full', containerWidth === 'boxed' ? 'container mx-auto max-w-6xl shadow-sm border-x' : 'w-full') )} > {sections.length === 0 ? ( diff --git a/admin-spa/src/routes/Appearance/Pages/components/CanvasSection.tsx b/admin-spa/src/routes/Appearance/Pages/components/CanvasSection.tsx index e85f77a..a22b17e 100644 --- a/admin-spa/src/routes/Appearance/Pages/components/CanvasSection.tsx +++ b/admin-spa/src/routes/Appearance/Pages/components/CanvasSection.tsx @@ -82,8 +82,15 @@ export function CanvasSection({ {/* Section content with Styles */}
-
+
{children}
diff --git a/admin-spa/src/routes/Appearance/Pages/components/InspectorField.tsx b/admin-spa/src/routes/Appearance/Pages/components/InspectorField.tsx index 15cb68c..36b3358 100644 --- a/admin-spa/src/routes/Appearance/Pages/components/InspectorField.tsx +++ b/admin-spa/src/routes/Appearance/Pages/components/InspectorField.tsx @@ -127,7 +127,7 @@ export function InspectorField({ placeholder={fieldType === 'url' ? 'https://' : `Enter ${fieldLabel.toLowerCase()}`} className="flex-1" /> - {(fieldType === 'url' || fieldType === 'image') && ( + {(fieldType === 'image') && ( handleValueChange(url)} type="image" diff --git a/admin-spa/src/routes/Appearance/Pages/components/InspectorPanel.tsx b/admin-spa/src/routes/Appearance/Pages/components/InspectorPanel.tsx index 4d7178a..7dc0292 100644 --- a/admin-spa/src/routes/Appearance/Pages/components/InspectorPanel.tsx +++ b/admin-spa/src/routes/Appearance/Pages/components/InspectorPanel.tsx @@ -3,6 +3,7 @@ import { cn } from '@/lib/utils'; import { __ } from '@/lib/i18n'; import { Button } from '@/components/ui/button'; import { Label } from '@/components/ui/label'; +import { Input } from '@/components/ui/input'; import { Select, SelectContent, @@ -413,6 +414,7 @@ export function InspectorPanel({ { name: 'label', label: 'Label', type: 'text' }, { name: 'image', label: 'Image', type: 'image' }, { name: 'url', label: 'Link URL', type: 'text' }, + { name: 'backgroundColor', label: 'Background Color', type: 'color' }, { name: 'size', label: 'Size (small/medium/large/tall)', type: 'text' }, ]} itemLabelKey="label" @@ -436,7 +438,7 @@ export function InspectorPanel({ // Allow advanced override/editing of asset/data if needed { name: 'product_name', label: 'Product Name', type: 'text' }, { name: 'product_price', label: 'Price', type: 'text' }, - { name: 'product_image', label: 'Product Image URL', type: 'text' }, + { name: 'product_image', label: 'Product Image URL', type: 'image' }, { name: 'x', label: 'X Position (%)', type: 'text' }, { name: 'y', label: 'Y Position (%)', type: 'text' }, ]} @@ -448,6 +450,36 @@ export function InspectorPanel({
); })()} + + {/* Contact Form Fields Repeater */} + {selectedSection.type === 'contact-form' && (() => { + const fieldsProp = selectedSection.props.fields; + const fields = Array.isArray(fieldsProp?.value) ? fieldsProp.value : []; + return ( +
+ onSectionPropChange('fields', { type: 'static', value: newItems })} + fields={[ + { name: 'name', label: 'Field Name (Key)', type: 'text' }, + { name: 'label', label: 'Label / Placeholder', type: 'text' }, + { name: 'type', label: 'Input Type', type: 'select', options: [ + { label: 'Text', value: 'text' }, + { label: 'Email', value: 'email' }, + { label: 'Telephone', value: 'tel' }, + { label: 'Textarea (Multiline)', value: 'textarea' }, + ]}, + { name: 'required', label: 'Is Required?', type: 'checkbox' } + ]} + itemLabelKey="label" + /> +

+ The Field Name (Key) will be the key used when sending data to your webhook. +

+
+ ); + })()} {/* Design Tab */} @@ -491,10 +523,10 @@ export function InspectorPanel({ onChange={(e) => onSectionStylesChange({ backgroundColor: e.target.value })} />
- onSectionStylesChange({ backgroundColor: e.target.value })} /> @@ -525,9 +557,9 @@ export function InspectorPanel({ onChange={(e) => onSectionStylesChange({ gradientFrom: e.target.value })} />
- onSectionStylesChange({ gradientFrom: e.target.value })} /> @@ -545,9 +577,9 @@ export function InspectorPanel({ onChange={(e) => onSectionStylesChange({ gradientTo: e.target.value })} />
- onSectionStylesChange({ gradientTo: e.target.value })} /> @@ -660,20 +692,20 @@ export function InspectorPanel({
- onSectionStylesChange({ paddingTop: e.target.value })} />
- onSectionStylesChange({ paddingBottom: e.target.value })} /> @@ -702,6 +734,49 @@ export function InspectorPanel({
+ {selectedSection.styles?.contentWidth === 'boxed' && ( + <> +
+ +
+
+
+ onSectionStylesChange({ cardBackgroundColor: e.target.value })} + /> +
+ onSectionStylesChange({ cardBackgroundColor: e.target.value })} + /> +
+
+
+ +
+
+ onSectionStylesChange({ cardPaddingTop: e.target.value })} /> +
+
+ onSectionStylesChange({ cardPaddingRight: e.target.value })} /> +
+
+ onSectionStylesChange({ cardPaddingBottom: e.target.value })} /> +
+
+ onSectionStylesChange({ cardPaddingLeft: e.target.value })} /> +
+
+
+ + )} +
+ +
+
+
+ onElementStylesChange(field.name, { backgroundColor: e.target.value })} + /> +
+ onElementStylesChange(field.name, { backgroundColor: e.target.value })} />
- onElementStylesChange(field.name, { backgroundColor: e.target.value })} - />
-
+ )} - {!isImage ? ( + {(!isImage && field.type !== 'container') && ( <> {/* Text Color */}
@@ -776,10 +853,10 @@ export function InspectorPanel({ onChange={(e) => onElementStylesChange(field.name, { color: e.target.value })} />
- onElementStylesChange(field.name, { color: e.target.value })} /> @@ -830,15 +907,17 @@ export function InspectorPanel({
- + {!field.disableAlignment && ( + + )}
{/* Link Specific Styles */} @@ -865,10 +944,10 @@ export function InspectorPanel({ onChange={(e) => onElementStylesChange(field.name, { hoverColor: e.target.value })} />
- onElementStylesChange(field.name, { hoverColor: e.target.value })} /> @@ -876,45 +955,12 @@ export function InspectorPanel({
)} - - {/* Button/Box Specific Styles */} - {field.name === 'button' && ( -
- -
-
- -
-
-
- onElementStylesChange(field.name, { borderColor: e.target.value })} - /> -
-
-
-
- - onElementStylesChange(field.name, { borderWidth: e.target.value })} /> -
-
- - onElementStylesChange(field.name, { borderRadius: e.target.value })} /> -
-
- - onElementStylesChange(field.name, { padding: e.target.value })} /> -
-
-
- )} - ) : ( + )} + + {/* Image Settings */} + {isImage && ( <> - {/* Image Settings */}
+
+ + +
+
+ + +
- onElementStylesChange(field.name, { width: e.target.value })} /> + onElementStylesChange(field.name, { width: e.target.value })} />
- onElementStylesChange(field.name, { height: e.target.value })} /> + onElementStylesChange(field.name, { height: e.target.value })} />
)} + + {/* Button/Box Specific Styles */} + {(field.name === 'button' || field.type === 'container') && ( +
+ +
+
+ +
+
+
+ onElementStylesChange(field.name, { borderColor: e.target.value })} + /> +
+
+
+
+ + onElementStylesChange(field.name, { borderWidth: e.target.value })} /> +
+
+ + onElementStylesChange(field.name, { borderRadius: e.target.value })} /> +
+
+ + onElementStylesChange(field.name, { padding: e.target.value })} /> +
+
+
+ )} ); diff --git a/admin-spa/src/routes/Appearance/Pages/components/InspectorRepeater.tsx b/admin-spa/src/routes/Appearance/Pages/components/InspectorRepeater.tsx index 9e5d7d4..f64972d 100644 --- a/admin-spa/src/routes/Appearance/Pages/components/InspectorRepeater.tsx +++ b/admin-spa/src/routes/Appearance/Pages/components/InspectorRepeater.tsx @@ -16,7 +16,8 @@ import { SelectTrigger, SelectValue, } from '@/components/ui/select'; -import { Plus, Trash2, GripVertical } from 'lucide-react'; +import { Plus, Trash2, GripVertical, Image as ImageIcon } from 'lucide-react'; +import { Switch } from '@/components/ui/switch'; import { cn } from '@/lib/utils'; import { useSortable } from '@dnd-kit/sortable'; import { CSS } from '@dnd-kit/utilities'; @@ -41,8 +42,9 @@ import RepeaterProductField from './RepeaterProductField'; interface RepeaterFieldDef { name: string; label: string; - type: 'text' | 'textarea' | 'url' | 'image' | 'icon' | 'product'; + type: 'text' | 'textarea' | 'url' | 'image' | 'icon' | 'product' | 'select' | 'checkbox' | 'color'; placeholder?: string; + options?: { label: string; value: string }[]; } interface InspectorRepeaterProps { @@ -91,8 +93,8 @@ function SortableItem({ 'Wifi', 'Wrench', ].sort(); - const handleFieldChange = (fieldName: string, value: any) => { - onChange(index, fieldName, value); + const handleFieldChange = (fieldNameOrUpdates: string | Record, value?: any) => { + onChange(index, fieldNameOrUpdates, value); }; return ( @@ -151,7 +153,7 @@ function RepeaterFieldRenderer({ field: RepeaterFieldDef; item: any; index: number; - onChange: (fieldName: string, value: any) => void; + onChange: (fieldNameOrUpdates: string | Record, value?: any) => void; ICON_OPTIONS: string[]; }) { const value = item[field.name] || ''; @@ -195,44 +197,53 @@ function RepeaterFieldRenderer({ ); } + if (field.type === 'color') { + return ( +
+ +
+
+
+ onChange(field.name, e.target.value)} + /> +
+ onChange(field.name, e.target.value)} + /> +
+
+ ); + } + if (field.type === 'image') { return (
-
- {value ? ( - onChange(field.name, url)} - type="image" - > -
- {field.label} -
- Change -
- -
-
- ) : ( - onChange(field.name, url)} - type="image" - > - - - )} +
+ onChange(field.name, e.target.value)} + placeholder="https://..." + className="flex-1 text-xs h-8" + /> + onChange(field.name, url)} + type="image" + className="shrink-0" + > + +
); @@ -251,6 +262,41 @@ function RepeaterFieldRenderer({ ); } + if (field.type === 'select') { + return ( +
+ + +
+ ); + } + + if (field.type === 'checkbox') { + return ( +
+ + onChange(field.name, checked)} + /> +
+ ); + } + // default: text/url inputs const inputType = field.type === 'url' ? 'url' : 'text'; @@ -296,9 +342,13 @@ export function InspectorRepeater({ } }; - const handleItemChange = (index: number, fieldName: string, value: string) => { + const handleItemChange = (index: number, fieldNameOrUpdates: string | Record, value?: any) => { const newItems = [...items]; - newItems[index] = { ...newItems[index], [fieldName]: value }; + if (typeof fieldNameOrUpdates === 'string') { + newItems[index] = { ...newItems[index], [fieldNameOrUpdates]: value }; + } else { + newItems[index] = { ...newItems[index], ...fieldNameOrUpdates }; + } onChange(newItems); }; @@ -341,7 +391,7 @@ export function InspectorRepeater({ item={item} fields={fields} itemLabelKey={itemLabelKey} - onChange={(idx: number, fieldName: string, value: string) => handleItemChange(idx, fieldName, value)} + onChange={(idx: number, fieldNameOrUpdates: string | Record, value?: any) => handleItemChange(idx, fieldNameOrUpdates, value)} onDelete={handleDeleteItem} /> ))} diff --git a/admin-spa/src/routes/Appearance/Pages/components/RepeaterProductField.tsx b/admin-spa/src/routes/Appearance/Pages/components/RepeaterProductField.tsx index e73240e..ea0e5b5 100644 --- a/admin-spa/src/routes/Appearance/Pages/components/RepeaterProductField.tsx +++ b/admin-spa/src/routes/Appearance/Pages/components/RepeaterProductField.tsx @@ -10,7 +10,7 @@ export default function RepeaterProductField({ }: { label: string; value: string; - onChange: (fieldName: string, nextValue: any) => void; + onChange: (fieldNameOrUpdates: string | Record, nextValue?: any) => void; }) { const [search, setSearch] = React.useState(''); const [options, setOptions] = React.useState([]); @@ -77,11 +77,13 @@ export default function RepeaterProductField({ const selected = options.find((o) => o.value === v)?.product; if (!selected) return; - onChange('product_slug', selected.product_slug || ''); - onChange('product_name', selected.name || ''); - onChange('product_price', selected.sale_price ?? selected.price ?? ''); - onChange('product_image', selected.image_url ?? ''); - onChange('product_id', selected.id ? Number(selected.id) : 0); + onChange({ + product_slug: selected.product_slug || '', + product_name: selected.name || '', + product_price: selected.sale_price ?? selected.price ?? '', + product_image: selected.image_url ?? '', + product_id: selected.id ? Number(selected.id) : 0, + }); }} options={options.map((o) => ({ value: String(o.value ?? ''), diff --git a/admin-spa/src/routes/Appearance/Pages/components/section-renderers/CTABannerRenderer.tsx b/admin-spa/src/routes/Appearance/Pages/components/section-renderers/CTABannerRenderer.tsx index 2e6807d..a187d55 100644 --- a/admin-spa/src/routes/Appearance/Pages/components/section-renderers/CTABannerRenderer.tsx +++ b/admin-spa/src/routes/Appearance/Pages/components/section-renderers/CTABannerRenderer.tsx @@ -32,16 +32,6 @@ export function CTABannerRenderer({ section, className }: CTABannerRendererProps const buttonText = section.props?.button_text?.value || 'Get Started'; const buttonUrl = section.props?.button_url?.value || '#'; - const heightMap: Record = { - 'default': 'py-12 md:py-20', - 'small': 'py-8 md:py-12', - 'medium': 'py-16 md:py-24', - 'large': 'py-24 md:py-36', - 'fullscreen': 'min-h-[50vh] flex flex-col justify-center', - }; - const customPadding = section.styles?.paddingTop || section.styles?.paddingBottom; - const heightClasses = customPadding ? '' : (heightMap[section.styles?.heightPreset || 'default'] || 'py-12 md:py-20'); - // Helper to get text styles (including font family) const getTextStyles = (elementName: string) => { const styles = section.elementStyles?.[elementName] || {}; @@ -69,7 +59,7 @@ export function CTABannerRenderer({ section, className }: CTABannerRendererProps const hasCustomBackground = !!section.styles?.backgroundColor || !!section.styles?.backgroundImage || section.styles?.backgroundType === 'gradient'; return ( -
+

- {text} + {text || "Description text missing"}

e.preventDefault()}> - {/* Name field */} -
- - -
+ {/* Render fields from config, fallback to default if missing */} + {(() => { + const defaultFields = [ + { name: 'name', label: 'Your Name', type: 'text', required: true }, + { name: 'email', label: 'Your Email', type: 'email', required: true }, + { name: 'message', label: 'Your Message', type: 'textarea', required: true } + ]; + const fieldsProp = section.props?.fields?.value; + const fields = Array.isArray(fieldsProp) && fieldsProp.length > 0 ? fieldsProp : defaultFields; - {/* Email field */} -
- - -
- - {/* Message field */} -
- -