commit f1e7005b26fdbf5d445d7ddc55d431d9f11efd59 Author: Jeremy Rangel Date: Sun Nov 30 00:50:56 2025 -0800 Initial diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c2658d7 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules/ diff --git a/BorderRadiusControl.js b/BorderRadiusControl.js new file mode 100644 index 0000000..6315e09 --- /dev/null +++ b/BorderRadiusControl.js @@ -0,0 +1,126 @@ +import { useState } from '@wordpress/element'; +import { + BaseControl, + Button, + __experimentalUnitControl as UnitControl, + RangeControl, + __experimentalHStack as HStack, + __experimentalVStack as VStack, +} from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; +import { link, linkOff } from '@wordpress/icons'; + +export default function BorderRadiusControl({ label, value = { radius: '0px' }, onChange }) { + // Determine if value is advanced initially + const initialAdvanced = value.topLeft || value.topRight || value.bottomLeft || value.bottomRight ? true : false; + const [advanced, setAdvanced] = useState(initialAdvanced); + + const parseValue = (val) => { + if (!val) return 0; + if (typeof val === 'string') return parseInt(val); + if (val.radius) return parseInt(val.radius); + return 0; + }; + + // Flat handlers + const handleFlatChange = (val) => { + const newValue = { radius: val }; + console.log('Flat change →', newValue); + onChange(newValue); + }; + const handleFlatRangeChange = (val) => { + const currentUnit = value.radius?.replace(/[0-9]/g, '') || 'px'; + const newValue = { radius: `${val}${currentUnit}` }; + console.log('Flat range change →', newValue); + onChange(newValue); + }; + + // Advanced handlers + const handleCornerChange = (corner, val) => { + // Always return a full object with all four corners + const newValue = { + topLeft: corner === 'topLeft' ? val : value.topLeft || '0px', + topRight: corner === 'topRight' ? val : value.topRight || '0px', + bottomRight: corner === 'bottomRight' ? val : value.bottomRight || '0px', + bottomLeft: corner === 'bottomLeft' ? val : value.bottomLeft || '0px', + }; + console.log('Corner change →', newValue); + onChange(newValue); + }; + + return ( + + + {/* Left column: controls */} + + {!advanced && ( + + +
+ +
+
+ )} + + {advanced && ( +
+ handleCornerChange('topLeft', val)} + min={0} + max={500} + /> + handleCornerChange('topRight', val)} + min={0} + max={500} + /> + + handleCornerChange('bottomLeft', val)} + min={0} + max={500} + /> + handleCornerChange('bottomRight', val)} + min={0} + max={500} + /> +
+ )} +
+ + {/* Right column: toggle button */} + + + + )} +/> + + + + ); +} diff --git a/CodeControl.js b/CodeControl.js new file mode 100644 index 0000000..5424c45 --- /dev/null +++ b/CodeControl.js @@ -0,0 +1,66 @@ +import { useState, useEffect } from '@wordpress/element'; +import { TextareaControl } from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; + +/** + * Simple validator functions for languages. + * You can expand these for more robust validation. + */ +const validators = { + css: (code) => { + // Very basic CSS validation: checks for opening/closing braces + const openBraces = (code.match(/{/g) || []).length; + const closeBraces = (code.match(/}/g) || []).length; + return openBraces === closeBraces; + }, + js: (code) => { + try { + // Attempt to parse as JavaScript + new Function(code); + return true; + } catch (e) { + return false; + } + }, + html: (code) => { + // Very basic HTML check: matching opening/closing tags + const openTags = (code.match(/<[^/!][^>]*>/g) || []).length; + const closeTags = (code.match(/<\/[^>]+>/g) || []).length; + return openTags === closeTags; + }, +}; + +export default function CodeControl({ + value, + onChange, + language = 'css', + label = __('Code', 'your-text-domain'), + rows = 10, +}) { + const [error, setError] = useState(false); + + useEffect(() => { + // Validate code whenever value or language changes + const isValid = validators[language] + ? validators[language](value) + : true; // fallback: no validation + setError(!isValid); + }, [value, language]); + + return ( +
+ +
+ ); +} diff --git a/ColorControl.js b/ColorControl.js new file mode 100644 index 0000000..5d03756 --- /dev/null +++ b/ColorControl.js @@ -0,0 +1,83 @@ +import { useState, useRef } from '@wordpress/element'; +import { Popover, ToolbarButton, ColorPalette, __experimentalHStack as HStack } from '@wordpress/components'; + +const ColorControl = ({ name, colors = [], value, onChange, className }) => { + const [isOpen, setIsOpen] = useState(false); + const buttonRef = useRef(); + + // Resolve what to show as the swatch color + const resolveColorValue = (val) => { + if (!val) return ''; + if (val.startsWith('var(')) { + // Convert var(--wp--preset--color--primary) → #hex from the palette + const match = colors.find(c => val.includes(c.slug)); + return match?.color || val; + } + return val; + }; + + const selectedColor = resolveColorValue(value); + + const handleChange = (colorValue) => { + // Find if it matches a theme color + const match = colors.find(c => c.color === colorValue); + + let finalValue = colorValue; + if (match?.slug) { + // Store CSS variable instead of raw color code + finalValue = `var(--wp--preset--color--${match.slug})`; + } + + if (onChange) onChange(finalValue); + setIsOpen(false); + }; + + return ( + <> + setIsOpen(!isOpen)} + > + + {name} + + + {isOpen && ( + setIsOpen(false)} + > +
+ +
+
+ )} + + ); +}; + +export default ColorControl; diff --git a/DirectoryListingItem.js b/DirectoryListingItem.js new file mode 100644 index 0000000..ef7813d --- /dev/null +++ b/DirectoryListingItem.js @@ -0,0 +1,139 @@ +import { useState, useEffect } from '@wordpress/element'; + +function isOpenNow(operatingHours = []) { + if (!operatingHours.length) return null; + + const now = new Date(); + const dayNames = ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday']; + const today = dayNames[now.getDay()]; + const todaysHours = operatingHours.filter(h => h.day === today); + if (!todaysHours.length) return null; + + for (let h of todaysHours) { + if (h.closed) continue; + if (h.open_24_hours) return true; + if (!h.opening_time || !h.closing_time) continue; + + const [openH, openM] = h.opening_time.split(':').map(Number); + const [closeH, closeM] = h.closing_time.split(':').map(Number); + + const openDate = new Date(now); + openDate.setHours(openH, openM, 0, 0); + const closeDate = new Date(now); + closeDate.setHours(closeH, closeM, 0, 0); + + if (now >= openDate && now <= closeDate) return true; + } + + return false; +} + +export default function DirectoryListingItem({ + post, + imageSrc = '', + imageMediaID = null, + restRoot = '', // must pass multisite REST root from View.js + onQuickView, + style = 'hero', + displayCategoryTitle = '', + displayCategoryURL = '#', + operatingHours = [], + titleTag, + postDescription, +}) { + const [finalImageSrc, setFinalImageSrc] = useState(imageSrc); + const [isOpen, setIsOpen] = useState(null); + const TitleTag = titleTag || 'h3'; + + useEffect(() => { + setIsOpen(isOpenNow(operatingHours)); + }, [operatingHours]); + + // Fetch image URL if imageMediaID is provided + useEffect(() => { + if (!imageMediaID) return; + if (!restRoot) { + console.error('DirectoryListingItem: restRoot is required for multisite media fetch.'); + return; + } + + let isMounted = true; + const url = `${restRoot}wp/v2/media/${imageMediaID}`; + console.log('Fetching media URL:', url); + + fetch(url) + .then(res => { + if (!res.ok) throw new Error(`Media fetch failed: ${res.status}`); + return res.json(); + }) + .then(data => { + if (isMounted && data?.source_url) { + setFinalImageSrc(data.source_url); + } + }) + .catch(err => console.error('Failed to fetch media:', err)); + + return () => { isMounted = false; }; + }, [imageMediaID, restRoot]); + + const heroStyle = style === 'hero' && finalImageSrc ? { backgroundImage: `url(${finalImageSrc})` } : {}; + + const content = ( + <> + {style !== 'hero' && ( +
+ {finalImageSrc ? ( + {post.title?.rendered + ) : ( +
No Image
+ )} +
+ )} + +
+ {displayCategoryTitle && ( + + {displayCategoryTitle} + + )} + + + {post.title?.rendered ? ( + + ) : ( + No title + )} + + + {postDescription ? ( +

+) : null} + + {isOpen !== null && ( + + {isOpen ? 'Open' : 'Closed'} + + )} + + +

+ + ); + + return style === 'hero' ? ( + + {content} + + ) : ( +
+ {content} +
+ ); +} diff --git a/JustificationControl.js b/JustificationControl.js new file mode 100644 index 0000000..81df59d --- /dev/null +++ b/JustificationControl.js @@ -0,0 +1,32 @@ +import { ToolbarGroup, ToolbarButton } from '@wordpress/components'; +import { justifyLeft, justifyCenter, justifyRight } from '@wordpress/icons'; +import { __ } from '@wordpress/i18n'; + +export default function JustificationControl({ value, onChange, label = __('Justification', 'directory-listings') }) { + const options = [ + { icon: justifyLeft, value: 'left', label: __('Left', 'directory-listings') }, + { icon: justifyCenter, value: 'center', label: __('Center', 'directory-listings') }, + { icon: justifyRight, value: 'right', label: __('Right', 'directory-listings') }, + ]; + + return ( +
+ {/* Label above toolbar */} +
+ {label} +
+ + + {options.map((option) => ( + onChange(option.value)} + /> + ))} + +
+ ); +} diff --git a/QuickViewModal.js b/QuickViewModal.js new file mode 100644 index 0000000..ee9e4c6 --- /dev/null +++ b/QuickViewModal.js @@ -0,0 +1,15 @@ +import { Fragment } from '@wordpress/element'; + +export default function QuickViewModal({ post, onClose }) { + if (!post) return null; + + return ( +
+
e.stopPropagation()}> + +

+
+
+

+ ); +} \ No newline at end of file diff --git a/ShareModal.js b/ShareModal.js new file mode 100644 index 0000000..3adef7f --- /dev/null +++ b/ShareModal.js @@ -0,0 +1,48 @@ +function ShareModal({ postUrl }) { + const [visible, setVisible] = useState(false); + + if (!postUrl) return null; + + return ( + <> + + {visible && ( +
+
+ +

Share this listing

+ + +
+
+ )} + + ); +} diff --git a/SpacingControl.js b/SpacingControl.js new file mode 100644 index 0000000..e974153 --- /dev/null +++ b/SpacingControl.js @@ -0,0 +1,156 @@ +import { useState } from '@wordpress/element'; + +import { + BaseControl, + Button, + RangeControl, + __experimentalHStack as HStack, + __experimentalVStack as VStack, + __experimentalUnitControl as UnitControl, + Icon +} from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; +import { sidesHorizontal, sidesVertical, sidesTop, sidesBottom, sidesLeft, sidesRight, settings } from '@wordpress/icons'; + +export default function SpacingControl({ label, value = {}, onChange, themeSpacing = {} }) { + const [verticalAdvanced, setVerticalAdvanced] = useState(false); + const [horizontalAdvanced, setHorizontalAdvanced] = useState(false); + const [advancedMode, setAdvancedMode] = useState(false); // full advanced toggle + + const parseValue = (val) => (val ? parseInt(val) : 0); + + const handleChange = (sides, val) => { + const unit = val.toString().replace(/[0-9]/g, '') || 'px'; + const newValue = { ...value }; + sides.forEach((side) => { + newValue[side] = `${parseInt(val)}${unit}`; + }); + onChange(newValue); + }; + + // --- Generate marks for RangeControl from theme.json spacing --- + const spacingMarks = themeSpacing?.spacingSizes?.map((s) => ({ + value: parseInt(s.size), // you may need to strip "px" or clamp() + label: s.name, + })) || []; + + // --- Advanced 4-row mode --- + if (advancedMode) { + const sides = [ + { key: 'top', sideIcon: sidesTop }, + { key: 'bottom', sideIcon: sidesBottom }, + { key: 'left', sideIcon: sidesLeft }, + { key: 'right', sideIcon: sidesRight }, + ]; + + return ( + + + + + {sides.map(({ key, sideIcon }) => ( + + + +
+ handleChange([key], val)} + min={0} + max={Math.max(...spacingMarks.map((m) => m.value))} + step={1} + marks={spacingMarks} + withInputField={false} + /> +
+ handleChange([key], val)} + min={0} + max={Math.max(...spacingMarks.map((m) => m.value))} + /> +
+
+ ))} +
+
+ ); + } + + // --- Simple vertical + horizontal mode --- + return ( + + + + + {/* Vertical */} + + + + {verticalAdvanced && ( + handleChange(['top', 'bottom'], val)} + min={0} + max={Math.max(...spacingMarks.map((m) => m.value))} + /> + )} +
+ handleChange(['top', 'bottom'], val)} + min={0} + max={Math.max(...spacingMarks.map((m) => m.value))} + step={1} + marks={spacingMarks} + withInputField={false} + /> +
+