diff --git a/includes/class-newspack.php b/includes/class-newspack.php index 606fcf9762..67297b2495 100644 --- a/includes/class-newspack.php +++ b/includes/class-newspack.php @@ -159,6 +159,7 @@ private function includes() { include_once NEWSPACK_ABSPATH . 'includes/wizards/class-setup-wizard.php'; include_once NEWSPACK_ABSPATH . 'includes/wizards/class-components-demo.php'; + include_once NEWSPACK_ABSPATH . 'includes/wizards/class-rolling-content-demo.php'; include_once NEWSPACK_ABSPATH . 'includes/wizards/traits/trait-wizards-admin-header.php'; @@ -176,6 +177,7 @@ private function includes() { include_once NEWSPACK_ABSPATH . 'includes/wizards/class-setup-wizard.php'; include_once NEWSPACK_ABSPATH . 'includes/wizards/class-components-demo.php'; + include_once NEWSPACK_ABSPATH . 'includes/wizards/class-rolling-content-demo.php'; // Listings Wizard. include_once NEWSPACK_ABSPATH . 'includes/wizards/class-listings-wizard.php'; diff --git a/includes/class-wizards.php b/includes/class-wizards.php index 873b72558f..d222257914 100644 --- a/includes/class-wizards.php +++ b/includes/class-wizards.php @@ -42,6 +42,7 @@ public static function init() { public static function init_wizards() { self::$wizards = [ 'components-demo' => new Components_Demo(), + 'rolling-content-demo' => new Rolling_Content_Demo(), // v2 Information Architecture. 'newspack-dashboard' => new Newspack_Dashboard(), 'setup' => new Setup_Wizard(), diff --git a/includes/wizards/class-rolling-content-demo.php b/includes/wizards/class-rolling-content-demo.php new file mode 100644 index 0000000000..8b7f0985e0 --- /dev/null +++ b/includes/wizards/class-rolling-content-demo.php @@ -0,0 +1,209 @@ +slug, self::SLUG_ADD ], true ); + } + + /** + * Render the container div. Override the parent so the id matches the current page slug, + * not `$this->slug`. Required because React mounts into `getElementById(pageParam)`. + */ + public function render_wizard() { + // phpcs:ignore WordPress.Security.NonceVerification.Recommended + $page = isset( $_GET['page'] ) ? sanitize_key( wp_unslash( $_GET['page'] ) ) : ''; + $id = in_array( $page, [ $this->slug, self::SLUG_ADD ], true ) ? $page : $this->slug; + ?> +
+ is_wizard_page() ) { + return; + } + + add_filter( 'parent_file', [ $this, 'set_parent_file' ] ); + add_filter( 'submenu_file', [ $this, 'set_submenu_file' ] ); + + $icon = sprintf( + 'data:image/svg+xml;base64,%s', + base64_encode( Newspack_UI_Icons::get_svg( 'collections' ) ) + ); + + // Anchor the menu near the top of the sidebar (just after Dashboard at pos 2) + // so the demo is easy to find — `add_menu_page` defaults to appending, which + // pushes the demo below every other plugin's menu. + add_menu_page( + $this->get_name(), + $this->get_name(), + $this->capability, + $this->slug, + [ $this, 'render_wizard' ], + $icon, + '2.5' + ); + add_submenu_page( + $this->slug, + $this->get_name(), + __( 'All Rolling Content', 'newspack-plugin' ), + $this->capability, + $this->slug, + [ $this, 'render_wizard' ] + ); + add_submenu_page( + $this->slug, + __( 'Add Rolling Content', 'newspack-plugin' ), + __( 'Add Rolling Content', 'newspack-plugin' ), + $this->capability, + self::SLUG_ADD, + [ $this, 'render_wizard' ] + ); + } + + /** + * Force-highlight the top-level menu when on any rolling-content page. + * Required because WP's automatic highlighting can lose the trail through + * the conditional menu registration. + * + * @param string $parent_file The current parent file. + * @return string + */ + public function set_parent_file( $parent_file ) { + return $this->slug; + } + + /** + * Highlight the correct submenu item for the current page. + * + * @param string $submenu_file The current submenu file. + * @return string + */ + public function set_submenu_file( $submenu_file ) { + // phpcs:ignore WordPress.Security.NonceVerification.Recommended + $page = isset( $_GET['page'] ) ? sanitize_key( wp_unslash( $_GET['page'] ) ) : ''; + return in_array( $page, [ $this->slug, self::SLUG_ADD ], true ) ? $page : $submenu_file; + } + + /** + * Enqueue scripts and styles. + * + * Replicates the relevant parts of the parent class's enqueue logic. We can't + * just call `parent::enqueue_scripts_and_styles()` because its slug check uses + * `filter_input(INPUT_GET, 'page')`, which reads from the frozen request state + * and ignores any local `$_GET` mutations — so it would bail on the SLUG_ADD + * page. + */ + public function enqueue_scripts_and_styles() { + if ( ! $this->is_wizard_page() ) { + return; + } + + Newspack::load_common_assets(); + + // Data carrier (no source script). + wp_register_script( 'newspack_data', '', [], '1.0', false ); + + $plugin_data = get_plugin_data( NEWSPACK_PLUGIN_FILE ); + $urls = [ + 'dashboard' => Wizards::get_url( 'newspack-dashboard' ), + 'public_path' => Newspack::plugin_url() . '/dist/', + 'bloginfo' => [ 'name' => get_bloginfo( 'name' ) ], + 'plugin_version' => [ 'label' => $plugin_data['Name'] . ' ' . $plugin_data['Version'] ], + 'homepage' => get_edit_post_link( get_option( 'page_on_front', false ) ), + 'site' => get_site_url(), + 'support' => esc_url( 'https://help.newspack.com/' ), + 'support_email' => false, + ]; + + $aux_data = [ + 'is_e2e' => Starter_Content::is_e2e(), + 'is_debug_mode' => Newspack::is_debug_mode(), + 'has_completed_setup' => get_option( NEWSPACK_SETUP_COMPLETE ), + 'site_title' => get_option( 'blogname' ), + 'is_managed' => method_exists( 'Newspack_Manager', 'is_connected_to_manager' ) && \Newspack_Manager::is_connected_to_manager(), + ]; + + wp_localize_script( 'newspack_data', 'newspack_urls', $urls ); + wp_localize_script( 'newspack_data', 'newspack_aux_data', $aux_data ); + wp_enqueue_script( 'newspack_data' ); + + wp_register_script( + 'newspack-wizards', + Newspack::plugin_url() . '/dist/wizards.js', + $this->get_script_dependencies(), + NEWSPACK_PLUGIN_VERSION, + true + ); + wp_enqueue_script( 'newspack-wizards' ); + } +} diff --git a/src/wizards/index.tsx b/src/wizards/index.tsx index 7e23bf9041..bbf7f51e32 100644 --- a/src/wizards/index.tsx +++ b/src/wizards/index.tsx @@ -60,6 +60,14 @@ const components: Record< string, any > = { label: __( 'Premium newsletters', 'newspack-plugin' ), component: lazy( () => import( /* webpackChunkName: "newsletters-wizards" */ './newsletters/views/premium-newsletters' ) ), }, + 'newspack-rolling-content': { + label: __( 'Rolling Content', 'newspack-plugin' ), + component: lazy( () => import( /* webpackChunkName: "rolling-content-wizards" */ './rollingContent/views/all' ) ), + }, + 'newspack-rolling-content-add': { + label: __( 'Add Rolling Content', 'newspack-plugin' ), + component: lazy( () => import( /* webpackChunkName: "rolling-content-wizards" */ './rollingContent/views/add' ) ), + }, } as const; // Conditionally add the Audience Integrations page if the feature is enabled. diff --git a/src/wizards/rollingContent/components/status-pill.tsx b/src/wizards/rollingContent/components/status-pill.tsx new file mode 100644 index 0000000000..4d331ca61e --- /dev/null +++ b/src/wizards/rollingContent/components/status-pill.tsx @@ -0,0 +1,28 @@ +/** + * Rolling Content demo — shared status indicator. + * + * Renders a small icon + label for an item's status. Each caller passes its + * own labels and icons keyed by its status union. + */ + +/** + * WordPress dependencies + */ +import { Icon } from '@wordpress/components'; + +export default function StatusPill< S extends string >( { + status, + labels, + icons, +}: { + status: S; + labels: Record< S, string >; + icons: Record< S, JSX.Element >; +} ) { + return ( + + + { labels[ status ] } + + ); +} diff --git a/src/wizards/rollingContent/data.ts b/src/wizards/rollingContent/data.ts new file mode 100644 index 0000000000..4285bc5fbc --- /dev/null +++ b/src/wizards/rollingContent/data.ts @@ -0,0 +1,132 @@ +/** + * Rolling Content demo — fake in-memory dataset. + * + * 30 rolling contents, each with 5-25 deterministic entries. All values are + * stable across renders (no Math.random, no Date.now at module scope). + */ + +const ROLLING_TITLES: string[] = [ + 'Election Night 2026: Live Updates', + 'City Council Budget Hearings', + 'Wildfire Response Coverage', + 'School Board Recall Effort', + 'Hurricane Season Live Coverage', + 'Mayoral Debate Aftermath', + 'Transit Strike Day Two', + 'Federal Inquiry Hearings', + 'Stadium Vote Live', + 'Climate Protest Coverage', + 'Tech Company Layoffs Tracker', + 'Inauguration Day', + 'Special Session Coverage', + 'Tax Reform Town Halls', + 'Water Restriction Updates', + 'Election Recount Results', + 'Public Health Emergency Briefings', + 'Budget Override Vote', + 'Police Reform Hearings', + 'Bond Measure Election Night', + 'School District Negotiations', + 'Housing Crisis Forum', + 'Power Grid Outage Updates', + 'Recall Petition Tracker', + 'City Charter Vote', + 'Special Counsel Updates', + 'Park District Vote', + 'Casino Referendum Coverage', + 'Voter Roll Audit Hearings', + 'Annexation Vote Live', +]; + +const ENTRY_TITLES: string[] = [ + 'Polls close in District 4', + 'Mayor responds to opposition statement', + 'Vote count delayed by ballot challenge', + 'Lawmakers reach tentative deal', + 'Live update: returns from precinct 12', + 'Statement from city attorney', + 'Crowd gathers outside city hall', + 'Press conference scheduled for 8 PM', + 'New witness called to testify', + 'Council moves into closed session', + 'Update: highway reopens to traffic', + 'Officials confirm timeline for vote', + 'Spokesperson denies allegations', + 'Crowd estimate revised upward', + 'Decision expected by tomorrow morning', +]; + +const AUTHORS: string[] = [ 'Alex Rivera', 'Jamie Chen', 'Morgan Patel', 'Sam Johnson', 'Riley Cooper', 'Jordan Kim' ]; + +const TAGS: string[] = [ 'breaking', 'politics', 'local', 'national', 'elections', 'transit', 'weather', 'courts', 'education', 'climate' ]; + +const BASE_DATE = new Date( '2026-05-01T12:00:00Z' ); + +function makeDate( seed: number ): string { + const dayOffset = ( seed * 17 ) % 90; + const d = new Date( BASE_DATE ); + d.setDate( d.getDate() - dayOffset ); + return d.toISOString(); +} + +function entryStatusFor( id: number ): EntryStatus { + const statusRoll = ( id * 13 ) % 100; + if ( statusRoll < 70 ) { + return 'published'; + } + if ( statusRoll < 90 ) { + return 'draft'; + } + return 'scheduled'; +} + +function rollingStatusFor( idx: number ): RollingContentStatus { + if ( idx < 12 ) { + return 'active'; + } + if ( idx < 22 ) { + return 'archived'; + } + return 'scheduled'; +} + +function makeEntries( parentId: number, count: number ): Entry[] { + const entries: Entry[] = []; + for ( let i = 0; i < count; i++ ) { + const id = parentId * 1000 + i; + const status: EntryStatus = entryStatusFor( id ); + + const tagCount = 1 + ( id % 3 ); + const tags: string[] = []; + for ( let t = 0; t < tagCount; t++ ) { + tags.push( TAGS[ ( id * 7 + t * 11 ) % TAGS.length ] ); + } + + entries.push( { + id, + title: ENTRY_TITLES[ ( id * 7 ) % ENTRY_TITLES.length ], + date: makeDate( id ), + author: AUTHORS[ id % AUTHORS.length ], + featuredImage: `https://picsum.photos/seed/entry-${ id }/200/120`, + status, + tags, + } ); + } + return entries; +} + +export const ROLLING_CONTENTS: RollingContent[] = ROLLING_TITLES.map( ( title, idx ) => { + const id = idx + 1; + const status: RollingContentStatus = rollingStatusFor( idx ); + const entryCount = 5 + ( ( id * 11 ) % 21 ); + return { + id, + title, + date: makeDate( id ), + featuredImage: `https://picsum.photos/seed/rolling-${ id }/200/120`, + status, + entries: makeEntries( id, entryCount ), + }; +} ); + +export const getRollingContent = ( id: number ): RollingContent | undefined => ROLLING_CONTENTS.find( r => r.id === id ); diff --git a/src/wizards/rollingContent/modals/add-entry-info.tsx b/src/wizards/rollingContent/modals/add-entry-info.tsx new file mode 100644 index 0000000000..f90e76d38f --- /dev/null +++ b/src/wizards/rollingContent/modals/add-entry-info.tsx @@ -0,0 +1,40 @@ +/** + * Rolling Content demo — Add Entry info modal. + * + * Explains the implicit parent association: in a real implementation, + * choosing "Add New Entry" from a Rolling Content row (or from inside the + * Manage Entries modal) would open the block editor with the chosen + * Rolling Content preselected as the parent. + */ + +/** + * WordPress dependencies + */ +import { __, sprintf } from '@wordpress/i18n'; +import { Button, Modal } from '@wordpress/components'; +import { createInterpolateElement } from '@wordpress/element'; + +export default function AddEntryInfoModal( { parentTitle, onClose }: { parentTitle: string; onClose: () => void } ) { + return ( + +

+ { createInterpolateElement( + sprintf( + /* translators: %s: parent rolling content title, wrapped in . */ + __( + 'Selecting "Add New Entry" would open the post editor with %s preselected as the parent. The link between an entry and its parent is implicit in how you enter the editor — no manual selection required.', + 'newspack-plugin' + ), + parentTitle + ), + { name: } + ) } +

+
+ +
+
+ ); +} diff --git a/src/wizards/rollingContent/modals/delete-confirm.tsx b/src/wizards/rollingContent/modals/delete-confirm.tsx new file mode 100644 index 0000000000..75dfd70934 --- /dev/null +++ b/src/wizards/rollingContent/modals/delete-confirm.tsx @@ -0,0 +1,63 @@ +/** + * Rolling Content demo — Delete confirmation modal. + * + * Shared destructive confirmation used for both Rolling Content and Entry deletes. + * On confirm, calls `onConfirm`; the caller mutates local state. + */ + +/** + * WordPress dependencies + */ +import { __, sprintf } from '@wordpress/i18n'; +import { Button, Modal } from '@wordpress/components'; + +type ItemType = 'rolling-content' | 'entry'; + +export default function DeleteConfirmModal( { + itemType, + title, + onConfirm, + onClose, +}: { + itemType: ItemType; + title: string; + onConfirm: () => void; + onClose: () => void; +} ) { + const itemNoun = itemType === 'entry' ? __( 'entry', 'newspack-plugin' ) : __( 'rolling content', 'newspack-plugin' ); + + return ( + +

+ { sprintf( + /* translators: %s: item type. */ + __( 'This will permanently delete this %s. This action cannot be undone.', 'newspack-plugin' ), + itemNoun + ) } +

+
+ + +
+
+ ); +} diff --git a/src/wizards/rollingContent/modals/edit-info.tsx b/src/wizards/rollingContent/modals/edit-info.tsx new file mode 100644 index 0000000000..811d1c8cb5 --- /dev/null +++ b/src/wizards/rollingContent/modals/edit-info.tsx @@ -0,0 +1,43 @@ +/** + * Rolling Content demo — Edit info modal. + * + * Shared info modal used for both Rolling Content and Entry "Edit" row actions. + * Explains that, in a real implementation, this would open the block editor. + */ + +/** + * WordPress dependencies + */ +import { __, sprintf } from '@wordpress/i18n'; +import { Button, Modal } from '@wordpress/components'; + +type ItemType = 'rolling-content' | 'entry'; + +export default function EditInfoModal( { itemType, title, onClose }: { itemType: ItemType; title: string; onClose: () => void } ) { + const itemNoun = itemType === 'entry' ? __( 'entry', 'newspack-plugin' ) : __( 'rolling content', 'newspack-plugin' ); + + return ( + +

+ { sprintf( + /* translators: %s: item title. */ + __( 'This would open the block editor for %s.', 'newspack-plugin' ), + title + ) } +

+
+ +
+
+ ); +} diff --git a/src/wizards/rollingContent/modals/manage-entries.tsx b/src/wizards/rollingContent/modals/manage-entries.tsx new file mode 100644 index 0000000000..5fdbda2f13 --- /dev/null +++ b/src/wizards/rollingContent/modals/manage-entries.tsx @@ -0,0 +1,212 @@ +/** + * Rolling Content demo — Manage Entries modal. + * + * Full-screen modal containing a nested DataViews of all entries belonging + * to one parent Rolling Content. Reuses the shared Edit Info, Delete + * Confirm, and Add Entry Info modals for row-level and toolbar actions. + */ + +/** + * WordPress dependencies + */ +import { __, sprintf } from '@wordpress/i18n'; +import { useState, useMemo } from '@wordpress/element'; +import { Button, Modal } from '@wordpress/components'; +import { filterSortAndPaginate } from '@wordpress/dataviews'; +import type { Action, Field, View } from '@wordpress/dataviews'; +import { published, drafts, scheduled } from '@wordpress/icons'; + +/** + * Internal dependencies + */ +import { DataViews } from '../../../../packages/components/src'; +import StatusPill from '../components/status-pill'; +import EditInfoModal from './edit-info'; +import DeleteConfirmModal from './delete-confirm'; +import AddEntryInfoModal from './add-entry-info'; + +const STATUS_LABELS: Record< EntryStatus, string > = { + published: __( 'Published', 'newspack-plugin' ), + draft: __( 'Draft', 'newspack-plugin' ), + scheduled: __( 'Scheduled', 'newspack-plugin' ), +}; + +const STATUS_ICONS: Record< EntryStatus, JSX.Element > = { + published, + draft: drafts, + scheduled, +}; + +const DEFAULT_VIEW: View = { + type: 'table', + page: 1, + perPage: 20, + sort: { field: 'date', direction: 'desc' }, + search: '', + fields: [ 'date', 'author', 'status', 'tags' ], + filters: [], + layout: {}, + titleField: 'title', + mediaField: 'featured_image', +}; + +export default function ManageEntriesModal( { + parent, + entries, + onEntriesChange, + onClose, +}: { + parent: RollingContent; + entries: Entry[]; + onEntriesChange: ( next: Entry[] ) => void; + onClose: () => void; +} ) { + const [ view, setView ] = useState< View >( DEFAULT_VIEW ); + const [ isAddingEntry, setIsAddingEntry ] = useState( false ); + + const fields: Field< Entry >[] = useMemo( + () => [ + { + id: 'featured_image', + label: __( 'Featured image', 'newspack-plugin' ), + type: 'media', + render: ( { item } ) => {, + enableSorting: false, + }, + { + id: 'title', + label: __( 'Title', 'newspack-plugin' ), + enableGlobalSearch: true, + getValue: ( { item } ) => item.title, + render: ( { item } ) => { item.title }, + }, + { + id: 'date', + label: __( 'Date', 'newspack-plugin' ), + getValue: ( { item } ) => item.date, + render: ( { item } ) => + new Date( item.date ).toLocaleDateString( undefined, { + year: 'numeric', + month: 'short', + day: 'numeric', + } ), + }, + { + id: 'author', + label: __( 'Author', 'newspack-plugin' ), + getValue: ( { item } ) => item.author, + }, + { + id: 'status', + label: __( 'Status', 'newspack-plugin' ), + getValue: ( { item } ) => item.status, + render: ( { item } ) => , + elements: ( Object.keys( STATUS_LABELS ) as EntryStatus[] ).map( value => ( { + value, + label: STATUS_LABELS[ value ], + } ) ), + }, + { + id: 'tags', + label: __( 'Tags', 'newspack-plugin' ), + getValue: ( { item } ) => item.tags.join( ', ' ), + render: ( { item } ) => ( + + { item.tags.map( tag => ( + + { tag } + + ) ) } + + ), + enableSorting: false, + }, + ], + [] + ); + + const actions: Action< Entry >[] = useMemo( + () => [ + { + id: 'edit', + label: __( 'Edit', 'newspack-plugin' ), + isPrimary: true, + supportsBulk: false, + RenderModal: ( { items, closeModal }: { items: Entry[]; closeModal: () => void } ) => ( + + ), + }, + { + id: 'delete', + label: __( 'Delete', 'newspack-plugin' ), + isDestructive: true, + supportsBulk: false, + RenderModal: ( { items, closeModal }: { items: Entry[]; closeModal: () => void } ) => ( + onEntriesChange( entries.filter( e => e.id !== items[ 0 ].id ) ) } + onClose={ closeModal } + /> + ), + }, + ], + [ entries, onEntriesChange ] + ); + + const { data: processedData, paginationInfo } = useMemo( () => filterSortAndPaginate( entries, view, fields ), [ entries, view, fields ] ); + + return ( + +
+

+ { sprintf( + /* translators: %s: parent rolling content title. */ + __( 'Entries for: %s', 'newspack-plugin' ), + parent.title + ) } +

+ +
+ + String( item.id ) } + search + /> + + { isAddingEntry && setIsAddingEntry( false ) } /> } +
+ ); +} diff --git a/src/wizards/rollingContent/types.d.ts b/src/wizards/rollingContent/types.d.ts new file mode 100644 index 0000000000..fdf7c04a8f --- /dev/null +++ b/src/wizards/rollingContent/types.d.ts @@ -0,0 +1,29 @@ +/** + * Rolling Content demo — type definitions. + */ + +declare global { + type RollingContentStatus = 'active' | 'archived' | 'scheduled'; + type EntryStatus = 'published' | 'draft' | 'scheduled'; + + interface Entry { + id: number; + title: string; + date: string; + author: string; + featuredImage: string; + status: EntryStatus; + tags: string[]; + } + + interface RollingContent { + id: number; + title: string; + date: string; + featuredImage: string; + status: RollingContentStatus; + entries: Entry[]; + } +} + +export {}; diff --git a/src/wizards/rollingContent/views/add.tsx b/src/wizards/rollingContent/views/add.tsx new file mode 100644 index 0000000000..67f145cdee --- /dev/null +++ b/src/wizards/rollingContent/views/add.tsx @@ -0,0 +1,50 @@ +/** + * Rolling Content — Add view (placeholder). + */ + +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { useEffect } from '@wordpress/element'; +import { useDispatch } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import { Wizard } from '../../../../packages/components/src'; +import { WIZARD_STORE_NAMESPACE } from '../../../../packages/components/src/wizard/store'; +import WizardSection from '../../wizards-section'; + +function AddRollingContent() { + const { setHeaderData } = useDispatch( WIZARD_STORE_NAMESPACE ); + + useEffect( () => { + setHeaderData( { + sectionName: __( 'Add Rolling Content', 'newspack-plugin' ), + actions: [ + { + type: 'secondary', + label: __( 'Back to All Rolling Content', 'newspack-plugin' ), + href: 'admin.php?page=newspack-rolling-content', + }, + ], + } ); + }, [ setHeaderData ] ); + + return ( + + <> + + ); +} + +export default function Add() { + return } ] } />; +} diff --git a/src/wizards/rollingContent/views/all.tsx b/src/wizards/rollingContent/views/all.tsx new file mode 100644 index 0000000000..a7ef4c81e6 --- /dev/null +++ b/src/wizards/rollingContent/views/all.tsx @@ -0,0 +1,220 @@ +/** + * Rolling Content — All view (DataViews). + */ + +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { useState, useMemo, useEffect } from '@wordpress/element'; +import { Button } from '@wordpress/components'; +import { useDispatch } from '@wordpress/data'; +import { filterSortAndPaginate } from '@wordpress/dataviews'; +import type { Action, Field, View } from '@wordpress/dataviews'; +import { published, scheduled, archive } from '@wordpress/icons'; + +/** + * Internal dependencies + */ +import { DataViews, Wizard } from '../../../../packages/components/src'; +import { WIZARD_STORE_NAMESPACE } from '../../../../packages/components/src/wizard/store'; +import { ROLLING_CONTENTS } from '../data'; +import StatusPill from '../components/status-pill'; +import EditInfoModal from '../modals/edit-info'; +import DeleteConfirmModal from '../modals/delete-confirm'; +import AddEntryInfoModal from '../modals/add-entry-info'; +import ManageEntriesModal from '../modals/manage-entries'; + +const STATUS_LABELS: Record< RollingContentStatus, string > = { + active: __( 'Active', 'newspack-plugin' ), + archived: __( 'Archived', 'newspack-plugin' ), + scheduled: __( 'Scheduled', 'newspack-plugin' ), +}; + +const STATUS_ICONS: Record< RollingContentStatus, JSX.Element > = { + active: published, + archived: archive, + scheduled, +}; + +const DEFAULT_VIEW: View = { + type: 'table', + page: 1, + perPage: 20, + sort: { field: 'date', direction: 'desc' }, + search: '', + fields: [ 'date', 'entries_count', 'last_updated', 'status' ], + filters: [], + layout: {}, + titleField: 'title', + mediaField: 'featured_image', +}; + +function AllRollingContent() { + const { setHeaderData } = useDispatch( WIZARD_STORE_NAMESPACE ); + const [ data, setData ] = useState< RollingContent[] >( ROLLING_CONTENTS ); + const [ view, setView ] = useState< View >( DEFAULT_VIEW ); + const [ managingEntriesForId, setManagingEntriesForId ] = useState< number | null >( null ); + const [ addingEntryForId, setAddingEntryForId ] = useState< number | null >( null ); + + const managingEntriesFor = managingEntriesForId === null ? null : data.find( r => r.id === managingEntriesForId ) ?? null; + const addingEntryFor = addingEntryForId === null ? null : data.find( r => r.id === addingEntryForId ) ?? null; + + useEffect( () => { + setHeaderData( { + sectionName: __( 'Rolling Content', 'newspack-plugin' ), + actions: [ + { + type: 'primary', + label: __( 'Add Rolling Content', 'newspack-plugin' ), + href: 'admin.php?page=newspack-rolling-content-add', + }, + ], + } ); + }, [ setHeaderData ] ); + + const fields: Field< RollingContent >[] = useMemo( + () => [ + { + id: 'featured_image', + label: __( 'Featured image', 'newspack-plugin' ), + type: 'media', + render: ( { item } ) => {, + enableSorting: false, + }, + { + id: 'title', + label: __( 'Title', 'newspack-plugin' ), + enableGlobalSearch: true, + getValue: ( { item } ) => item.title, + render: ( { item } ) => { item.title }, + }, + { + id: 'date', + label: __( 'Date', 'newspack-plugin' ), + getValue: ( { item } ) => item.date, + render: ( { item } ) => + new Date( item.date ).toLocaleDateString( undefined, { + year: 'numeric', + month: 'short', + day: 'numeric', + } ), + }, + { + id: 'entries_count', + label: __( 'Entries', 'newspack-plugin' ), + getValue: ( { item } ) => item.entries.length, + render: ( { item } ) => ( +
+ { item.entries.length } + + +
+ ), + }, + { + id: 'last_updated', + label: __( 'Last updated', 'newspack-plugin' ), + getValue: ( { item } ) => { + if ( item.entries.length === 0 ) { + return 0; + } + return Math.max( ...item.entries.map( e => new Date( e.date ).getTime() ) ); + }, + render: ( { item } ) => { + if ( item.entries.length === 0 ) { + return '—'; + } + const ms = Math.max( ...item.entries.map( e => new Date( e.date ).getTime() ) ); + return new Date( ms ).toLocaleDateString( undefined, { + year: 'numeric', + month: 'short', + day: 'numeric', + } ); + }, + }, + { + id: 'status', + label: __( 'Status', 'newspack-plugin' ), + getValue: ( { item } ) => item.status, + render: ( { item } ) => , + elements: ( Object.keys( STATUS_LABELS ) as RollingContentStatus[] ).map( value => ( { + value, + label: STATUS_LABELS[ value ], + } ) ), + }, + ], + [] + ); + + const actions: Action< RollingContent >[] = useMemo( + () => [ + { + id: 'edit', + label: __( 'Edit', 'newspack-plugin' ), + isPrimary: true, + supportsBulk: false, + RenderModal: ( { items, closeModal }: { items: RollingContent[]; closeModal: () => void } ) => ( + + ), + }, + { + id: 'delete', + label: __( 'Delete', 'newspack-plugin' ), + isDestructive: true, + supportsBulk: false, + RenderModal: ( { items, closeModal }: { items: RollingContent[]; closeModal: () => void } ) => ( + setData( prev => prev.filter( r => r.id !== items[ 0 ].id ) ) } + onClose={ closeModal } + /> + ), + }, + ], + [] + ); + + const { data: processedData, paginationInfo } = useMemo( () => filterSortAndPaginate( data, view, fields ), [ data, view, fields ] ); + + return ( + <> + String( item.id ) } + search + /> + { managingEntriesFor && ( + + setData( prev => prev.map( r => ( r.id === managingEntriesFor.id ? { ...r, entries: nextEntries } : r ) ) ) + } + onClose={ () => setManagingEntriesForId( null ) } + /> + ) } + { addingEntryFor && setAddingEntryForId( null ) } /> } + + ); +} + +export default function All() { + return ( + , fullWidth: true } ] } + /> + ); +}