Jake Vanderwerf
2026-01-01 5b5f37de365ff84fc231e414a719d1b2ff4ceff6
src/feed/edit.js
@@ -1,114 +1,256 @@
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>
   );
}