Initial
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
node_modules/
|
||||||
126
BorderRadiusControl.js
Normal file
126
BorderRadiusControl.js
Normal file
@ -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 (
|
||||||
|
<BaseControl label={label}>
|
||||||
|
<HStack gap={8} align="flex-start">
|
||||||
|
{/* Left column: controls */}
|
||||||
|
<VStack gap={8} style={{ flex: 1 }}>
|
||||||
|
{!advanced && (
|
||||||
|
<HStack gap={8} align="center">
|
||||||
|
<UnitControl
|
||||||
|
value={value.radius || '0px'}
|
||||||
|
onChange={handleFlatChange}
|
||||||
|
label={__('Radius', 'directory-listings')}
|
||||||
|
min={0}
|
||||||
|
max={500}
|
||||||
|
style={{ flex: '0 0 auto' }}
|
||||||
|
/>
|
||||||
|
<div style={{ flex: 1 }}>
|
||||||
|
<RangeControl
|
||||||
|
value={parseValue(value)}
|
||||||
|
withInputField={false}
|
||||||
|
onChange={handleFlatRangeChange}
|
||||||
|
min={0}
|
||||||
|
max={500}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</HStack>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{advanced && (
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '8px' }}>
|
||||||
|
<UnitControl
|
||||||
|
label={__('Top Left', 'directory-listings')}
|
||||||
|
value={value.topLeft || '0px'}
|
||||||
|
onChange={(val) => handleCornerChange('topLeft', val)}
|
||||||
|
min={0}
|
||||||
|
max={500}
|
||||||
|
/>
|
||||||
|
<UnitControl
|
||||||
|
label={__('Top Right', 'directory-listings')}
|
||||||
|
value={value.topRight || '0px'}
|
||||||
|
onChange={(val) => handleCornerChange('topRight', val)}
|
||||||
|
min={0}
|
||||||
|
max={500}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<UnitControl
|
||||||
|
label={__('Bottom Left', 'directory-listings')}
|
||||||
|
value={value.bottomLeft || '0px'}
|
||||||
|
onChange={(val) => handleCornerChange('bottomLeft', val)}
|
||||||
|
min={0}
|
||||||
|
max={500}
|
||||||
|
/>
|
||||||
|
<UnitControl
|
||||||
|
label={__('Bottom Right', 'directory-listings')}
|
||||||
|
value={value.bottomRight || '0px'}
|
||||||
|
onChange={(val) => handleCornerChange('bottomRight', val)}
|
||||||
|
min={0}
|
||||||
|
max={500}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</VStack>
|
||||||
|
|
||||||
|
{/* Right column: toggle button */}
|
||||||
|
<VStack>
|
||||||
|
<Button
|
||||||
|
icon={advanced ? linkOff : link}
|
||||||
|
iconSize={24}
|
||||||
|
isSecondary
|
||||||
|
onClick={() => setAdvanced(!advanced)}
|
||||||
|
aria-label={__('Toggle advanced border radius', 'directory-listings')}
|
||||||
|
/>
|
||||||
|
</VStack>
|
||||||
|
</HStack>
|
||||||
|
</BaseControl>
|
||||||
|
);
|
||||||
|
}
|
||||||
107
BoxShadowControl.js
Normal file
107
BoxShadowControl.js
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
import { useState } from '@wordpress/element';
|
||||||
|
import {
|
||||||
|
Dropdown,
|
||||||
|
Button,
|
||||||
|
__experimentalHStack as HStack,
|
||||||
|
} from '@wordpress/components';
|
||||||
|
import { solidLine } from '@wordpress/icons';
|
||||||
|
|
||||||
|
export default function BoxShadowControl({
|
||||||
|
label = 'Box Shadow',
|
||||||
|
value = '',
|
||||||
|
onSelect,
|
||||||
|
}) {
|
||||||
|
const boxShadows = [
|
||||||
|
'rgba(0, 0, 0, 0.1) 0px 0px 10px 0px',
|
||||||
|
'0 4px 6px rgba(0,0,0,0.1), 0 2px 4px rgba(0,0,0,0.06)',
|
||||||
|
'0 10px 15px rgba(0,0,0,0.2), 0 4px 6px rgba(0,0,0,0.1)',
|
||||||
|
'0 20px 25px rgba(0,0,0,0.3), 0 10px 10px rgba(0,0,0,0.2)',
|
||||||
|
'0 0 10px rgba(0,0,0,0.5)',
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ width: '100%' }}>
|
||||||
|
<Dropdown
|
||||||
|
className="box-shadow-control"
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
popoverProps={{ placement: 'bottom-start' }}
|
||||||
|
renderToggle={({ isOpen, onToggle }) => (
|
||||||
|
<HStack
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
onClick={onToggle}
|
||||||
|
onKeyDown={(e) => e.key === 'Enter' && onToggle()}
|
||||||
|
gap={6}
|
||||||
|
align="center"
|
||||||
|
style={{
|
||||||
|
width: '100%', // full width toggle
|
||||||
|
padding: '6px 10px',
|
||||||
|
border: '1px solid #ccc',
|
||||||
|
borderRadius: '4px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
userSelect: 'none',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span>{label}</span>
|
||||||
|
|
||||||
|
{value && (
|
||||||
|
<Button
|
||||||
|
icon={solidLine}
|
||||||
|
isSecondary
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
onSelect(''); // clear the shadow
|
||||||
|
}}
|
||||||
|
aria-label="Clear box shadow"
|
||||||
|
style={{ marginLeft: 'auto' }} // optional: push to right
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</HStack>
|
||||||
|
)}
|
||||||
|
renderContent={() => (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: '8px',
|
||||||
|
padding: '8px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{boxShadows.map((shadow, index) => (
|
||||||
|
<Button
|
||||||
|
key={index}
|
||||||
|
style={{
|
||||||
|
width: '60px',
|
||||||
|
height: '40px',
|
||||||
|
backgroundColor: 'white',
|
||||||
|
boxShadow: shadow,
|
||||||
|
border: '1px solid #ddd',
|
||||||
|
}}
|
||||||
|
onClick={() => onSelect(shadow)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<Button
|
||||||
|
isSecondary
|
||||||
|
style={{
|
||||||
|
width: '60px',
|
||||||
|
height: '40px',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
fontSize: '12px',
|
||||||
|
border: '1px solid #ddd',
|
||||||
|
}}
|
||||||
|
onClick={() => onSelect('')}
|
||||||
|
>
|
||||||
|
None
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
);
|
||||||
|
}
|
||||||
66
CodeControl.js
Normal file
66
CodeControl.js
Normal file
@ -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 (
|
||||||
|
<div style={{ fontFamily: 'monospace', whiteSpace: 'pre' }}>
|
||||||
|
<TextareaControl
|
||||||
|
label={label + (error ? ' ⚠️ Invalid ' + language : '')}
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
placeholder={`Enter ${language} code here...`}
|
||||||
|
rows={rows}
|
||||||
|
style={{
|
||||||
|
borderColor: error ? 'red' : undefined,
|
||||||
|
background: '#f5f5f5',
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
83
ColorControl.js
Normal file
83
ColorControl.js
Normal file
@ -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 (
|
||||||
|
<>
|
||||||
|
<ToolbarButton
|
||||||
|
ref={buttonRef}
|
||||||
|
className={className ? className : ''}
|
||||||
|
onClick={() => setIsOpen(!isOpen)}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
display: 'inline-block',
|
||||||
|
width: '20px',
|
||||||
|
height: '20px',
|
||||||
|
borderRadius: '50%',
|
||||||
|
backgroundColor: selectedColor,
|
||||||
|
border: '1px solid #ccc',
|
||||||
|
marginRight: '8px',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{name}
|
||||||
|
</ToolbarButton>
|
||||||
|
|
||||||
|
{isOpen && (
|
||||||
|
<Popover
|
||||||
|
anchorRef={buttonRef}
|
||||||
|
placement="bottom-start"
|
||||||
|
onClose={() => setIsOpen(false)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
padding: '16px',
|
||||||
|
background: '#f9f9f9',
|
||||||
|
borderRadius: '8px',
|
||||||
|
minWidth: '250px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ColorPalette
|
||||||
|
colors={colors}
|
||||||
|
value={selectedColor}
|
||||||
|
onChange={handleChange}
|
||||||
|
enableAlpha
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Popover>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ColorControl;
|
||||||
139
DirectoryListingItem.js
Normal file
139
DirectoryListingItem.js
Normal file
@ -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' && (
|
||||||
|
<div className="listing-left">
|
||||||
|
{finalImageSrc ? (
|
||||||
|
<img src={finalImageSrc} alt={post.title?.rendered || 'Post image'} />
|
||||||
|
) : (
|
||||||
|
<div className="placeholder-image">No Image</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="listing-right">
|
||||||
|
{displayCategoryTitle && (
|
||||||
|
<a
|
||||||
|
className="listing-category-button"
|
||||||
|
href={displayCategoryURL}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
{displayCategoryTitle}
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<TitleTag>
|
||||||
|
{post.title?.rendered ? (
|
||||||
|
<a href={post.link} dangerouslySetInnerHTML={{ __html: post.title.rendered }} />
|
||||||
|
) : (
|
||||||
|
<span>No title</span>
|
||||||
|
)}
|
||||||
|
</TitleTag>
|
||||||
|
|
||||||
|
{postDescription ? (
|
||||||
|
<p dangerouslySetInnerHTML={{ __html: postDescription }} />
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{isOpen !== null && (
|
||||||
|
<span className={`open-status ${isOpen ? 'open' : 'closed'}`}>
|
||||||
|
{isOpen ? 'Open' : 'Closed'}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<button className="quickview-button" onClick={() => onQuickView(post)}>
|
||||||
|
QuickView
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
return style === 'hero' ? (
|
||||||
|
<a href={post.link} className={`directory-listing ${style}`} style={heroStyle}>
|
||||||
|
{content}
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
<article className={`directory-listing ${style}`} style={heroStyle}>
|
||||||
|
{content}
|
||||||
|
</article>
|
||||||
|
);
|
||||||
|
}
|
||||||
32
JustificationControl.js
Normal file
32
JustificationControl.js
Normal file
@ -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 (
|
||||||
|
<div className="justification-control">
|
||||||
|
{/* Label above toolbar */}
|
||||||
|
<div className="justification-control__label" style={{ marginBottom: '4px', display: 'block' }}>
|
||||||
|
{label}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ToolbarGroup label={label}>
|
||||||
|
{options.map((option) => (
|
||||||
|
<ToolbarButton
|
||||||
|
key={option.value}
|
||||||
|
icon={option.icon}
|
||||||
|
label={option.label} // accessible for screen readers
|
||||||
|
isPressed={value === option.value}
|
||||||
|
onClick={() => onChange(option.value)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</ToolbarGroup>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
15
QuickViewModal.js
Normal file
15
QuickViewModal.js
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { Fragment } from '@wordpress/element';
|
||||||
|
|
||||||
|
export default function QuickViewModal({ post, onClose }) {
|
||||||
|
if (!post) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="quickview-modal" onClick={onClose}>
|
||||||
|
<div className="quickview-content" onClick={e => e.stopPropagation()}>
|
||||||
|
<button className="quickview-close" onClick={onClose}>×</button>
|
||||||
|
<h3 dangerouslySetInnerHTML={{ __html: post.title?.rendered || '' }} />
|
||||||
|
<div dangerouslySetInnerHTML={{ __html: post.content?.rendered || '' }} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
48
ShareModal.js
Normal file
48
ShareModal.js
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
function ShareModal({ postUrl }) {
|
||||||
|
const [visible, setVisible] = useState(false);
|
||||||
|
|
||||||
|
if (!postUrl) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<button onClick={() => setVisible(true)}>Share</button>
|
||||||
|
{visible && (
|
||||||
|
<div className="listing-share-modal" style={{
|
||||||
|
display: 'flex',
|
||||||
|
position: 'fixed',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
backgroundColor: 'rgba(0,0,0,0.5)'
|
||||||
|
}}>
|
||||||
|
<div style={{
|
||||||
|
background: '#fff',
|
||||||
|
padding: '20px',
|
||||||
|
maxWidth: '400px',
|
||||||
|
width: '90%',
|
||||||
|
borderRadius: '6px',
|
||||||
|
position: 'relative'
|
||||||
|
}}>
|
||||||
|
<button
|
||||||
|
aria-label="Close"
|
||||||
|
style={{ position: 'absolute', top: 10, right: 10, fontSize: 24, background: 'none', border: 'none', cursor: 'pointer' }}
|
||||||
|
onClick={() => setVisible(false)}
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
<h2>Share this listing</h2>
|
||||||
|
<input type="text" className="share-link" readOnly value={postUrl} style={{ width: '100%', marginBottom: 10 }} />
|
||||||
|
<div className="listing-share-links">
|
||||||
|
<a href={`https://twitter.com/share?url=${encodeURIComponent(postUrl)}`} target="_blank">Twitter</a>
|
||||||
|
<a href={`https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(postUrl)}`} target="_blank">Facebook</a>
|
||||||
|
<a href={`mailto:?body=${encodeURIComponent(postUrl)}`} target="_blank">Email</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
156
SpacingControl.js
Normal file
156
SpacingControl.js
Normal file
@ -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 (
|
||||||
|
<BaseControl label={label}>
|
||||||
|
<VStack gap={16}>
|
||||||
|
<Button variant="secondary" onClick={() => setAdvancedMode(false)}>
|
||||||
|
{__('Back to Simple Mode', 'directory-listings')}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{sides.map(({ key, sideIcon }) => (
|
||||||
|
<VStack key={key} gap={4}>
|
||||||
|
<HStack align="center" gap={8}>
|
||||||
|
<Icon icon={sideIcon} />
|
||||||
|
<div style={{ flex: 1 }}>
|
||||||
|
<RangeControl
|
||||||
|
value={parseValue(value[key] || 0)}
|
||||||
|
onChange={(val) => handleChange([key], val)}
|
||||||
|
min={0}
|
||||||
|
max={Math.max(...spacingMarks.map((m) => m.value))}
|
||||||
|
step={1}
|
||||||
|
marks={spacingMarks}
|
||||||
|
withInputField={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<UnitControl
|
||||||
|
value={value[key] || '0px'}
|
||||||
|
onChange={(val) => handleChange([key], val)}
|
||||||
|
min={0}
|
||||||
|
max={Math.max(...spacingMarks.map((m) => m.value))}
|
||||||
|
/>
|
||||||
|
</HStack>
|
||||||
|
</VStack>
|
||||||
|
))}
|
||||||
|
</VStack>
|
||||||
|
</BaseControl>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Simple vertical + horizontal mode ---
|
||||||
|
return (
|
||||||
|
<BaseControl label={label}>
|
||||||
|
<VStack gap={16}>
|
||||||
|
<Button variant="secondary" onClick={() => setAdvancedMode(true)}>
|
||||||
|
{__('Advanced Mode', 'directory-listings')}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{/* Vertical */}
|
||||||
|
<VStack gap={4}>
|
||||||
|
<HStack align="center" gap={8}>
|
||||||
|
<Icon icon={sidesVertical} />
|
||||||
|
{verticalAdvanced && (
|
||||||
|
<UnitControl
|
||||||
|
value={value.top || '0px'}
|
||||||
|
onChange={(val) => handleChange(['top', 'bottom'], val)}
|
||||||
|
min={0}
|
||||||
|
max={Math.max(...spacingMarks.map((m) => m.value))}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div style={{ flex: 1 }}>
|
||||||
|
<RangeControl
|
||||||
|
value={parseValue(value.top || 0)}
|
||||||
|
onChange={(val) => handleChange(['top', 'bottom'], val)}
|
||||||
|
min={0}
|
||||||
|
max={Math.max(...spacingMarks.map((m) => m.value))}
|
||||||
|
step={1}
|
||||||
|
marks={spacingMarks}
|
||||||
|
withInputField={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
icon={settings}
|
||||||
|
isSecondary
|
||||||
|
onClick={() => setVerticalAdvanced(!verticalAdvanced)}
|
||||||
|
aria-label={__('Toggle vertical spacing input', 'directory-listings')}
|
||||||
|
/>
|
||||||
|
</HStack>
|
||||||
|
</VStack>
|
||||||
|
|
||||||
|
{/* Horizontal */}
|
||||||
|
<VStack gap={4}>
|
||||||
|
<HStack align="center" gap={8}>
|
||||||
|
<Icon icon={sidesHorizontal} />
|
||||||
|
{horizontalAdvanced && (
|
||||||
|
<UnitControl
|
||||||
|
value={value.left || '0px'}
|
||||||
|
onChange={(val) => handleChange(['left', 'right'], val)}
|
||||||
|
min={0}
|
||||||
|
max={Math.max(...spacingMarks.map((m) => m.value))}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div style={{ flex: 1 }}>
|
||||||
|
<RangeControl
|
||||||
|
value={parseValue(value.left || 0)}
|
||||||
|
onChange={(val) => handleChange(['left', 'right'], val)}
|
||||||
|
min={0}
|
||||||
|
max={Math.max(...spacingMarks.map((m) => m.value))}
|
||||||
|
step={1}
|
||||||
|
marks={spacingMarks}
|
||||||
|
withInputField={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
icon={settings}
|
||||||
|
isSecondary
|
||||||
|
onClick={() => setHorizontalAdvanced(!horizontalAdvanced)}
|
||||||
|
aria-label={__('Toggle horizontal spacing input', 'directory-listings')}
|
||||||
|
/>
|
||||||
|
</HStack>
|
||||||
|
</VStack>
|
||||||
|
</VStack>
|
||||||
|
</BaseControl>
|
||||||
|
);
|
||||||
|
}
|
||||||
60
TextSizeControl.js
Normal file
60
TextSizeControl.js
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import { useState, Fragment } from '@wordpress/element';
|
||||||
|
import { __experimentalToggleGroupControl as ToggleGroupControl, __experimentalToggleGroupControlOption as ToggleGroupControlOption, __experimentalUnitControl as UnitControl, Icon } from '@wordpress/components';
|
||||||
|
import { useSettings } from '@wordpress/block-editor';
|
||||||
|
import { settings } from '@wordpress/icons';
|
||||||
|
|
||||||
|
export default function TextSizeControl({ value = '', onChange, title = 'Font Size' }) {
|
||||||
|
const { typography } = useSettings() || {};
|
||||||
|
const fontSizes = (typography && typography.fontSizes) || [];
|
||||||
|
|
||||||
|
const letters = ['S', 'M', 'L', 'XL', 'XXL'];
|
||||||
|
const sizeMap = {};
|
||||||
|
fontSizes.forEach((fs, index) => {
|
||||||
|
const cssSize = fs.fluid?.max || fs.size;
|
||||||
|
const key = letters[index] || fs.slug || fs.name;
|
||||||
|
sizeMap[key] = cssSize;
|
||||||
|
});
|
||||||
|
|
||||||
|
const initialKey = Object.keys(sizeMap).find(k => sizeMap[k] === value) || Object.keys(sizeMap)[0];
|
||||||
|
const [selected, setSelected] = useState(initialKey);
|
||||||
|
const [advanced, setAdvanced] = useState(false); // toggle between preset and UnitControl
|
||||||
|
|
||||||
|
const handleToggleChange = (key) => {
|
||||||
|
setSelected(key);
|
||||||
|
if (onChange) onChange(sizeMap[key]);
|
||||||
|
console.log('[TextSizeControl] Toggle selected:', key, '=>', sizeMap[key]);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '0.5rem', cursor: 'pointer' }}
|
||||||
|
onClick={() => setAdvanced(!advanced)}
|
||||||
|
>
|
||||||
|
<span style={{ fontWeight: 'bold' }}>{title}</span>
|
||||||
|
<Icon icon={settings} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!advanced ? (
|
||||||
|
<ToggleGroupControl
|
||||||
|
label=""
|
||||||
|
isBlock
|
||||||
|
value={selected}
|
||||||
|
onChange={handleToggleChange}
|
||||||
|
>
|
||||||
|
{Object.keys(sizeMap).map((key) => (
|
||||||
|
<ToggleGroupControlOption key={key} value={key} label={key} />
|
||||||
|
))}
|
||||||
|
</ToggleGroupControl>
|
||||||
|
) : (
|
||||||
|
<UnitControl
|
||||||
|
value={value || ''}
|
||||||
|
onChange={(newValue) => {
|
||||||
|
console.log('[TextSizeControl] UnitControl value:', newValue);
|
||||||
|
if (onChange) onChange(newValue);
|
||||||
|
}}
|
||||||
|
label="" // optional, already have header
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
11
package.json
Normal file
11
package.json
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"name": "@rangeldigital/wp-components",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"main": "index.js",
|
||||||
|
"private": false,
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^18.0.0",
|
||||||
|
"react-dom": "^18.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Reference in New Issue
Block a user