Jake Vanderwerf
2026-05-31 d7e7d248cbe41cd7a9ef9c2fb022b6c4831f99a3
src/feed/edit.js
New file
@@ -0,0 +1,256 @@
/**
 * 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 { __ } from '@wordpress/i18n';
export default function Edit({ attributes, setAttributes }) {
   const [feedTypes, setFeedTypes] = useState(null);
   const [loading, setLoading] = useState(true);
   const [error, setError] = useState(null);
   const blockProps = useBlockProps({
      className: 'feed-block-editor'
   });
   /**
    * 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);
            // Store Last-Modified for future requests
            // (apiFetch doesn't expose response headers easily,
            // but the server will handle 304s)
            // 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]);
   /**
    * Toggle a content type in the selection
    */
   const toggleContentType = (slug, checked) => {
      const currentTypes = attributes.contentTypes || [];
      const newTypes = checked
         ? [...currentTypes, slug]
         : currentTypes.filter(t => t !== slug);
      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>
   );
}