| | |
| | | import { __ } from '@wordpress/i18n'; |
| | | import { useBlockProps, InspectorControls } from '@wordpress/block-editor'; |
| | | import { PanelBody, TextControl, ToggleControl, SelectControl, CheckboxControl } from '@wordpress/components'; |
| | | /** |
| | | * Feed Block - Edit Component |
| | | * Fetches available feed types from /jvb/v1/feed/types |
| | | * Allows configuration of content types and inherit query setting |
| | | */ |
| | | |
| | | import { useEffect, useState } from '@wordpress/element'; |
| | | import { useBlockProps, InspectorControls } from '@wordpress/block-editor'; |
| | | import { |
| | | PanelBody, |
| | | CheckboxControl, |
| | | ToggleControl, |
| | | Spinner, |
| | | Notice |
| | | } from '@wordpress/components'; |
| | | import apiFetch from '@wordpress/api-fetch'; |
| | | import './editor.scss'; |
| | | import { __ } from '@wordpress/i18n'; |
| | | |
| | | export default function Edit({ attributes, setAttributes }) { |
| | | const blockProps = useBlockProps(); |
| | | const [availableTypes, setAvailableTypes] = useState({}); |
| | | const [feedTypes, setFeedTypes] = useState(null); |
| | | const [loading, setLoading] = useState(true); |
| | | const [error, setError] = useState(null); |
| | | |
| | | useEffect(() => { |
| | | apiFetch({ path: '/jvb/v1/types' }).then(types => { |
| | | setAvailableTypes(JSON.parse(types)); |
| | | }); |
| | | }, []); |
| | | const blockProps = useBlockProps({ |
| | | className: 'feed-block-editor' |
| | | }); |
| | | |
| | | return ( |
| | | <> |
| | | <InspectorControls> |
| | | <PanelBody title={__('Feed Settings', 'jvb')}> |
| | | <TextControl |
| | | label={__('Title', 'jvb')} |
| | | value={attributes.title} |
| | | onChange={(title) => setAttributes({ title })} |
| | | /> |
| | | /** |
| | | * Fetch available feed types on component mount |
| | | */ |
| | | useEffect(() => { |
| | | apiFetch({ |
| | | path: '/jvb/v1/feed/types', |
| | | headers: { |
| | | 'If-Modified-Since': localStorage.getItem('feed_types_modified'), |
| | | } |
| | | }) |
| | | .then(types => { |
| | | setFeedTypes(types); |
| | | setLoading(false); |
| | | |
| | | <ToggleControl |
| | | label={__('Inherit Current Query', 'jvb')} |
| | | help={__('Inherit filters from the current archive or taxonomy query', 'jvb')} |
| | | checked={attributes.inheritQuery} |
| | | onChange={(inheritQuery) => setAttributes({ inheritQuery })} |
| | | /> |
| | | // Store Last-Modified for future requests |
| | | // (apiFetch doesn't expose response headers easily, |
| | | // but the server will handle 304s) |
| | | |
| | | {!attributes.inheritQuery && ( |
| | | <div className="feed-content-types"> |
| | | <p className="components-base-control__label"> |
| | | {__('Content Types', 'jvb')} |
| | | </p> |
| | | <div className="checkbox-list"> |
| | | {Object.entries(availableTypes).map(([id, label]) => ( |
| | | <CheckboxControl |
| | | key={id} |
| | | label={label} |
| | | checked={attributes.contentTypes.includes(id)} |
| | | onChange={(isChecked) => { |
| | | const newTypes = isChecked |
| | | ? [...attributes.contentTypes, id] |
| | | : attributes.contentTypes.filter(t => t !== id); |
| | | setAttributes({contentTypes: newTypes}); |
| | | }} |
| | | /> |
| | | ))} |
| | | </div> |
| | | <div className="select-all-wrapper"> |
| | | <CheckboxControl |
| | | label={__('Select All', 'jvb')} |
| | | checked={attributes.contentTypes.length === Object.keys(availableTypes).length} |
| | | onChange={(isChecked) => { |
| | | setAttributes({ |
| | | contentTypes: isChecked ? Object.keys(availableTypes) : [] |
| | | }); |
| | | }} |
| | | /> |
| | | </div> |
| | | </div> |
| | | )} |
| | | // Initialize contentTypes if not set and not inheriting |
| | | if (!attributes.contentTypes && !attributes.inheritQuery) { |
| | | const firstType = Object.keys(types)[0]; |
| | | if (firstType) { |
| | | setAttributes({ contentTypes: [firstType] }); |
| | | } |
| | | } |
| | | }) |
| | | .catch(err => { |
| | | console.error('Error loading feed types:', err); |
| | | setError(err.message); |
| | | setLoading(false); |
| | | }); |
| | | }, [attributes.inheritQuery]); |
| | | |
| | | <SelectControl |
| | | label={__('Items Per Page', 'jvb')} |
| | | value={attributes.itemsPerPage} |
| | | options={[ |
| | | {label: '12', value: 12}, |
| | | {label: '24', value: 24}, |
| | | {label: '36', value: 36} |
| | | ]} |
| | | onChange={(itemsPerPage) => setAttributes({itemsPerPage: parseInt(itemsPerPage)})} |
| | | /> |
| | | /** |
| | | * Toggle a content type in the selection |
| | | */ |
| | | const toggleContentType = (slug, checked) => { |
| | | const currentTypes = attributes.contentTypes || []; |
| | | |
| | | <SelectControl |
| | | label={__('Default Order', 'jvb')} |
| | | value={attributes.defaultOrder} |
| | | options={[ |
| | | {label: __('Newest First', 'jvb'), value: 'date_desc' }, |
| | | { label: __('Oldest First', 'jvb'), value: 'date_asc' }, |
| | | { label: __('Random', 'jvb'), value: 'random' } |
| | | ]} |
| | | onChange={(defaultOrder) => setAttributes({ defaultOrder })} |
| | | /> |
| | | </PanelBody> |
| | | </InspectorControls> |
| | | const newTypes = checked |
| | | ? [...currentTypes, slug] |
| | | : currentTypes.filter(t => t !== slug); |
| | | |
| | | <div {...blockProps}> |
| | | <div className="feed-block-preview"> |
| | | <h2>{attributes.title}</h2> |
| | | <div className="feed-filters"> |
| | | <div className="filter-preview"> |
| | | {attributes.contentTypes.map(type => ( |
| | | <span key={type} className="content-type-badge"> |
| | | {availableTypes[type]} |
| | | </span> |
| | | ))} |
| | | </div> |
| | | </div> |
| | | <div className="feed-grid-placeholder"> |
| | | {[...Array(6)].map((_, i) => ( |
| | | <div key={i} className="grid-item-placeholder" /> |
| | | ))} |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </> |
| | | ); |
| | | setAttributes({ contentTypes: newTypes }); |
| | | }; |
| | | |
| | | /** |
| | | * Get friendly label for content type |
| | | */ |
| | | const getTypeLabel = (slug, config) => { |
| | | return `${config.plural} (${config.type})`; |
| | | }; |
| | | |
| | | /** |
| | | * Group types by category for better UX |
| | | */ |
| | | const groupedTypes = feedTypes ? { |
| | | content: Object.entries(feedTypes) |
| | | .filter(([_, config]) => config.type === 'content'), |
| | | taxonomy: Object.entries(feedTypes) |
| | | .filter(([_, config]) => config.type === 'taxonomy') |
| | | } : { content: [], taxonomy: [] }; |
| | | |
| | | return ( |
| | | <div {...blockProps}> |
| | | <InspectorControls> |
| | | <PanelBody |
| | | title={__('Feed Settings', 'jvb')} |
| | | initialOpen={true} |
| | | > |
| | | <ToggleControl |
| | | label={__('Inherit from Page Context', 'jvb')} |
| | | help={ |
| | | attributes.inheritQuery |
| | | ? __('Feed will adapt to the current page (profile, taxonomy, etc.)', 'jvb') |
| | | : __('Manually select content types to display', 'jvb') |
| | | } |
| | | checked={attributes.inheritQuery} |
| | | onChange={(value) => setAttributes({ inheritQuery: value })} |
| | | /> |
| | | |
| | | {!attributes.inheritQuery && ( |
| | | <> |
| | | {loading && ( |
| | | <div style={{ textAlign: 'center', padding: '20px' }}> |
| | | <Spinner /> |
| | | <p>{__('Loading feed types...', 'jvb')}</p> |
| | | </div> |
| | | )} |
| | | |
| | | {error && ( |
| | | <Notice status="error" isDismissible={false}> |
| | | {__('Error loading feed types: ', 'jvb')} {error} |
| | | </Notice> |
| | | )} |
| | | |
| | | {!loading && !error && feedTypes && ( |
| | | <> |
| | | {groupedTypes.content.length > 0 && ( |
| | | <> |
| | | <h4>{__('Content Types', 'jvb')}</h4> |
| | | {groupedTypes.content.map(([slug, config]) => ( |
| | | <CheckboxControl |
| | | key={slug} |
| | | label={getTypeLabel(slug, config)} |
| | | checked={ |
| | | attributes.contentTypes?.includes(slug) || false |
| | | } |
| | | onChange={(checked) => |
| | | toggleContentType(slug, checked) |
| | | } |
| | | help={ |
| | | config.taxonomies?.length > 0 |
| | | ? `Filters: ${config.taxonomies.join(', ')}` |
| | | : null |
| | | } |
| | | /> |
| | | ))} |
| | | </> |
| | | )} |
| | | |
| | | {groupedTypes.taxonomy.length > 0 && ( |
| | | <> |
| | | <h4 style={{ marginTop: '20px' }}> |
| | | {__('Content Taxonomies', 'jvb')} |
| | | </h4> |
| | | <p style={{ fontSize: '12px', color: '#757575' }}> |
| | | {__('These are collections that group other content', 'jvb')} |
| | | </p> |
| | | {groupedTypes.taxonomy.map(([slug, config]) => ( |
| | | <CheckboxControl |
| | | key={slug} |
| | | label={getTypeLabel(slug, config)} |
| | | checked={ |
| | | attributes.contentTypes?.includes(slug) || false |
| | | } |
| | | onChange={(checked) => |
| | | toggleContentType(slug, checked) |
| | | } |
| | | help={ |
| | | config.for_content?.length > 0 |
| | | ? `Contains: ${config.for_content.join(', ')}` |
| | | : null |
| | | } |
| | | /> |
| | | ))} |
| | | </> |
| | | )} |
| | | |
| | | {!attributes.contentTypes?.length && ( |
| | | <Notice status="warning" isDismissible={false}> |
| | | {__('Please select at least one content type', 'jvb')} |
| | | </Notice> |
| | | )} |
| | | </> |
| | | )} |
| | | </> |
| | | )} |
| | | </PanelBody> |
| | | |
| | | <PanelBody |
| | | title={__('Display Settings', 'jvb')} |
| | | initialOpen={false} |
| | | > |
| | | <ToggleControl |
| | | label={__('Show Gallery View', 'jvb')} |
| | | help={__('Enable lightbox for images', 'jvb')} |
| | | checked={attributes.enableGallery || false} |
| | | onChange={(value) => |
| | | setAttributes({ enableGallery: value }) |
| | | } |
| | | /> |
| | | </PanelBody> |
| | | </InspectorControls> |
| | | |
| | | <div className="feed-block-placeholder"> |
| | | <div className="feed-block-icon"> |
| | | <svg width="48" height="48" viewBox="0 0 24 24" fill="none"> |
| | | <rect x="3" y="3" width="7" height="7" fill="currentColor" opacity="0.3" /> |
| | | <rect x="13" y="3" width="7" height="7" fill="currentColor" opacity="0.3" /> |
| | | <rect x="3" y="13" width="7" height="7" fill="currentColor" opacity="0.3" /> |
| | | <rect x="13" y="13" width="7" height="7" fill="currentColor" opacity="0.3" /> |
| | | </svg> |
| | | </div> |
| | | |
| | | <h3>{__('Feed Block', 'jvb')}</h3> |
| | | |
| | | {attributes.inheritQuery ? ( |
| | | <p className="feed-block-description"> |
| | | {__('📍 Inheriting from page context', 'jvb')} |
| | | </p> |
| | | ) : ( |
| | | <div className="feed-block-description"> |
| | | {attributes.contentTypes?.length > 0 ? ( |
| | | <> |
| | | <p><strong>{__('Showing:', 'jvb')}</strong></p> |
| | | <ul style={{ |
| | | listStyle: 'none', |
| | | padding: '0', |
| | | margin: '8px 0' |
| | | }}> |
| | | {attributes.contentTypes.map(type => { |
| | | const config = feedTypes?.[type]; |
| | | return ( |
| | | <li key={type} style={{ |
| | | padding: '4px 0', |
| | | color: '#2271b1' |
| | | }}> |
| | | ✓ {config?.plural || type} |
| | | </li> |
| | | ); |
| | | })} |
| | | </ul> |
| | | </> |
| | | ) : ( |
| | | <p style={{ color: '#d63638' }}> |
| | | {__('⚠️ No content types selected', 'jvb')} |
| | | </p> |
| | | )} |
| | | </div> |
| | | )} |
| | | |
| | | <p className="feed-block-note"> |
| | | {__('Feed will be displayed on the frontend', 'jvb')} |
| | | </p> |
| | | </div> |
| | | </div> |
| | | ); |
| | | } |