diff --git a/includes/class-blocks.php b/includes/class-blocks.php index 5636608ab2..4a2adf28a9 100644 --- a/includes/class-blocks.php +++ b/includes/class-blocks.php @@ -97,7 +97,8 @@ public static function enqueue_frontend_assets() { private static function should_load_block_assets() { return Collections::is_module_active() && ( ( is_singular() && has_block( \Newspack\Blocks\Collections\Collections_Block::BLOCK_NAME, get_the_ID() ) ) || - is_post_type_archive( \Newspack\Collections\Post_Type::get_post_type() ) + is_post_type_archive( \Newspack\Collections\Post_Type::get_post_type() ) || + is_singular( \Newspack\Collections\Post_Type::get_post_type() ) ); } } diff --git a/includes/collections/class-template-helper.php b/includes/collections/class-template-helper.php index 856135b596..889ef13c9e 100644 --- a/includes/collections/class-template-helper.php +++ b/includes/collections/class-template-helper.php @@ -315,9 +315,7 @@ public static function render_cta( $cta ) { } $html = sprintf( - '
- %3$s -
', + '%3$s', esc_attr( $cta['class'] ?? '' ), esc_url( $cta['url'] ?? '' ), esc_html( $cta['label'] ?? '' ) @@ -507,6 +505,67 @@ public static function render_collections_grid( $collections ) { ); } + /** + * Render collections intro using the Collections block. + * + * @param int|WP_Post $post Post ID or post object. + * @param array $args Optional arguments for the intro section. + * @return string The rendered collections intro HTML. + */ + public static function render_collections_intro( $post, $args = [] ) { + $collection = $post instanceof \WP_Post ? $post : get_post( $post ); + if ( ! $collection instanceof \WP_Post ) { + return ''; + } + + $attrs = wp_parse_args( + $args, + [ + 'selectedCollections' => [ $collection ], + 'layout' => 'list', + 'imageSize' => 'small', + 'showExcerpt' => true, + 'showCategory' => false, + 'numberOfCTAs' => -1, + 'showSeeAllLink' => false, + 'headingText' => '', + 'noPermalinks' => false, + ] + ); + + /** + * Filters the attributes before rendering the collections intro block. + * + * @param array $attrs The attributes for the collections block. + * @param WP_Post $collection The collection being rendered. + * @param array $args The original arguments passed to the function. + */ + $attrs = apply_filters( 'newspack_collections_render_intro_attrs', $attrs, $collection, $args ); + + /** + * Fires before the collection intro section. + * + * @param WP_Post $collection The collection post. + */ + do_action( 'newspack_collections_intro_before', $collection ); + + $output = render_block( + [ + 'blockName' => 'newspack/collections', + 'attrs' => $attrs, + ] + ); + + /** + * Fires after the collection intro section. + * + * @param WP_Post $collection The collection post. + */ + do_action( 'newspack_collections_intro_after', $collection ); + + return $output; + } + /** * Normalize an array that may contain WP_Post objects, IDs, or mixed. * diff --git a/includes/templates/collections/archive-newspack-collection.php b/includes/templates/collections/archive-newspack-collection.php index fc6d325f95..163c81a086 100644 --- a/includes/templates/collections/archive-newspack-collection.php +++ b/includes/templates/collections/archive-newspack-collection.php @@ -30,19 +30,17 @@ true, - 'permalink' => true, - ] - ); + $latest_collection = $wp_query->posts[0] ?? null; + if ( $latest_collection ) { + echo wp_kses_post( Template_Helper::render_collections_intro( $latest_collection, [ 'headingText' => __( 'Latest', 'newspack-plugin' ) ] ) ); + } echo wp_kses_post( Template_Helper::render_separator( 'is-latest-collection' ) ); endif; @@ -98,7 +96,6 @@ posts; // Determine if first collection should be excluded (already shown in intro). diff --git a/includes/templates/collections/parts/newspack-collection-intro.php b/includes/templates/collections/parts/newspack-collection-intro.php deleted file mode 100644 index 831d5f2777..0000000000 --- a/includes/templates/collections/parts/newspack-collection-intro.php +++ /dev/null @@ -1,106 +0,0 @@ - $args['permalink'], // If the argument is a string, use it as the permalink. - ( $args['permalink'] ?? false ) === true => get_the_permalink( $collection ), // If a `true` boolean, use the collection permalink. - default => false, // If not provided (or if a falsey value), don't use a permalink. -}; - -/** - * Fires before the collection intro section. - * - * @param WP_Post $collection The collection post. - */ -do_action( 'newspack_collections_intro_before', $collection ); -?> - - -
- - -
- -
- - -
- - - - -
- -
- - -
- - - -
- -
-
- - diff --git a/includes/templates/collections/single-newspack-collection.php b/includes/templates/collections/single-newspack-collection.php index db263e9347..2bc54592b8 100644 --- a/includes/templates/collections/single-newspack-collection.php +++ b/includes/templates/collections/single-newspack-collection.php @@ -29,7 +29,7 @@ */ do_action( 'newspack_collections_single_start', $post ); - get_template_part( Template_Helper::TEMPLATE_PARTS_DIR . 'newspack-collection-intro' ); + echo wp_kses_post( Template_Helper::render_collections_intro( get_the_ID(), [ 'noPermalinks' => true ] ) ); /** * Fires after the collection intro section. diff --git a/src/blocks/collections/class-collections-block.php b/src/blocks/collections/class-collections-block.php index 684855ed79..c3c239f8ab 100644 --- a/src/blocks/collections/class-collections-block.php +++ b/src/blocks/collections/class-collections-block.php @@ -56,6 +56,8 @@ final class Collections_Block { 'specificCTAs' => '', 'showSeeAllLink' => true, 'seeAllLinkText' => '', + 'headingText' => '', + 'noPermalinks' => false, ]; /** @@ -96,7 +98,7 @@ public static function render_block( array $attributes ) { $attributes['numberOfItems'] = max( 1, absint( $attributes['numberOfItems'] ) ); $attributes['offset'] = max( 0, absint( $attributes['offset'] ) ); $attributes['columns'] = max( 1, absint( $attributes['columns'] ) ); - $attributes['numberOfCTAs'] = max( 1, absint( $attributes['numberOfCTAs'] ) ); + $attributes['numberOfCTAs'] = ( -1 === (int) $attributes['numberOfCTAs'] ) ? -1 : max( 1, absint( $attributes['numberOfCTAs'] ) ); // Normalize selectedCollections to determine if we have post objects or IDs. $normalized_posts = Template_Helper::normalize_post_list( (array) $attributes['selectedCollections'] ); @@ -203,7 +205,10 @@ protected static function render_collection( $collection, $attributes ) {
- ID, $collection_url, $image_size ) ); ?> + ID, $image_permalink, $image_size ) ); + ?> @@ -211,16 +216,26 @@ protected static function render_collection( $collection, $attributes ) {
+ + + + -

- +

+ + + + + - -

+ +

{ { showCTAs && filteredCtas.length > 0 && (
{ filteredCtas.map( ( cta, index ) => ( -
- - { cta.label } - -
+ + { cta.label } + ) ) }
) } diff --git a/src/blocks/collections/styles/_ctas.scss b/src/blocks/collections/styles/_ctas.scss index 28719fd810..4d02fe9ac7 100644 --- a/src/blocks/collections/styles/_ctas.scss +++ b/src/blocks/collections/styles/_ctas.scss @@ -4,6 +4,10 @@ display: flex; flex-wrap: wrap; gap: 8px; + + a { + width: 100%; + } } // See all collections link. diff --git a/src/blocks/collections/styles/_layout-grid.scss b/src/blocks/collections/styles/_layout-grid.scss index 44b453c394..e4bf1cb654 100644 --- a/src/blocks/collections/styles/_layout-grid.scss +++ b/src/blocks/collections/styles/_layout-grid.scss @@ -56,6 +56,7 @@ .wp-block-newspack-collections__title { margin: 24px 0 0; + font-size: var(--newspack-theme-font-size-base); } } } diff --git a/src/blocks/collections/styles/_layout-list.scss b/src/blocks/collections/styles/_layout-list.scss index c6d44bd99b..ceb70ff03a 100644 --- a/src/blocks/collections/styles/_layout-list.scss +++ b/src/blocks/collections/styles/_layout-list.scss @@ -3,91 +3,169 @@ .wp-block-newspack-collections { // Layout: List. &.layout-list { + margin-bottom: 32px; + .wp-block-newspack-collections__item { - display: flex; + display: grid; + grid-template-columns: 1fr; + gap: 16px; margin-bottom: 2em; align-items: flex-start; &:last-child { margin-bottom: 0; } - } - &.image-left { - .wp-block-newspack-collections__image { - margin-right: 1.5em; - flex-shrink: 0; + @media #{bp.$media-sm-up} { + grid-template-columns: repeat(3, minmax(0, 1fr)); + } + + @media #{bp.$media-lg-up} { + grid-template-columns: repeat(6, minmax(0, 1fr)); + gap: 32px; } } + &.image-left, &.image-right { - .wp-block-newspack-collections__item { - flex-direction: row-reverse; + // Small size (default). + &.image-size-small { + .wp-block-newspack-collections__image { + grid-column: span 1; + } + + .wp-block-newspack-collections__content { + @media #{bp.$media-sm-up} { + grid-column: span 2; + } + + @media #{bp.$media-lg-up} { + grid-column: span 5; + } + } } - .wp-block-newspack-collections__image { - margin-left: 1.5em; - flex-shrink: 0; + // Medium size - slightly larger image. + &.image-size-medium { + .wp-block-newspack-collections__image { + grid-column: span 1; + + @media #{bp.$media-lg-up} { + grid-column: span 2; + } + } + + .wp-block-newspack-collections__content { + @media #{bp.$media-sm-up} { + grid-column: span 2; + } + + @media #{bp.$media-lg-up} { + grid-column: span 4; + } + } + } + + // Large size - larger image. + &.image-size-large { + .wp-block-newspack-collections__image { + grid-column: span 1; + + @media #{bp.$media-sm-up} { + grid-column: span 2; + } + + @media #{bp.$media-lg-up} { + grid-column: span 3; + } + } + + .wp-block-newspack-collections__content { + @media #{bp.$media-sm-up} { + grid-column: span 1; + } + + @media #{bp.$media-lg-up} { + grid-column: span 3; + } + } } } - // Image sizes for left and right alignments. - &.image-left, + // For the right alignment, reorder the grid items. &.image-right { - @media #{bp.$media-sm-up} { - &.image-size-small .wp-block-newspack-collections__image { - width: 150px; - } - &.image-size-medium .wp-block-newspack-collections__image { - width: 30%; + .wp-block-newspack-collections__item { + .wp-block-newspack-collections__image { + order: 2; } - &.image-size-large .wp-block-newspack-collections__image { - width: 50%; + .wp-block-newspack-collections__content { + order: 1; } } } + // For the top alignment, stack the grid items vertically. &.image-top { .wp-block-newspack-collections__item { - flex-direction: column; + grid-template-columns: 1fr; + } + + .wp-block-newspack-collections__image, + .wp-block-newspack-collections__content { + grid-column: span 1; } .wp-block-newspack-collections__image { - margin-bottom: 1em; + max-width: 100%; } - // Image sizes for top alignment (sm+ only). + // Apply size constraints only on larger screens. @media #{bp.$media-sm-up} { &.image-size-small .wp-block-newspack-collections__image { - width: 30%; + max-width: 30%; } &.image-size-medium .wp-block-newspack-collections__image { - width: 50%; + max-width: 50%; } &.image-size-large .wp-block-newspack-collections__image { - width: 70%; + max-width: 70%; } } } - // On extra-small screens, ignore image size presets; stack and use full width. - @media #{bp.$media-xs-only} { - // Ensure stacking wins over the more specific .image-right rule. - &.image-right .wp-block-newspack-collections__item, - .wp-block-newspack-collections__item { - flex-direction: column; + + .wp-block-newspack-collections__title { + font-size: var(--newspack-theme-font-size-lg); + margin: 0; + + @media #{bp.$media-md-up} { + font-size: var(--newspack-theme-font-size-xl); } + } - &.image-left, - &.image-right, - &.image-top { - .wp-block-newspack-collections__image { - width: 100%; - max-width: 100%; - margin: 0 0 1em 0; + .wp-block-newspack-collections__content { + display: flex; + flex-direction: column; + gap: 0.5em; + } + + .wp-block-newspack-collections__meta { + margin: 0; + p { + margin: 0; + } + } + + .wp-block-newspack-collections__excerpt { + margin: 22px 0; + } + + .wp-block-newspack-collections__ctas { + a { + @media #{bp.$media-md-up} { + width: auto; } } } } } - diff --git a/src/blocks/collections/styles/_text.scss b/src/blocks/collections/styles/_text.scss index 95699ac58b..db4e4f280d 100644 --- a/src/blocks/collections/styles/_text.scss +++ b/src/blocks/collections/styles/_text.scss @@ -1,4 +1,10 @@ .wp-block-newspack-collections { + // Collection heading. + .wp-block-newspack-collections__heading { + margin: 0; + text-transform: uppercase; + } + // Title. .wp-block-newspack-collections__title { margin: 0 0 24px; @@ -12,7 +18,6 @@ .wp-block-newspack-collections__excerpt { margin-bottom: 1em; color: var(--wp--preset--font-size--small); - line-height: 1.4; } // Meta information (Period, Volume, Number). diff --git a/src/collections/frontend/_common.scss b/src/collections/frontend/_common.scss index f6d30ef57f..603ab01fc3 100644 --- a/src/collections/frontend/_common.scss +++ b/src/collections/frontend/_common.scss @@ -41,17 +41,9 @@ } } -.collection-cta { - width: 100%; - - &.cta--subscribe_link { - a { - background-color: var(--newspack-primary-color); - color: var(--newspack-primary-contrast-color); - } - } - - @media #{breakpoints.$media-sm-up} { - width: auto; +.wp-block-newspack-collections__ctas { + a.cta--subscribe_link { + background-color: var(--newspack-primary-color); + color: var(--newspack-primary-contrast-color); } } diff --git a/tests/unit-tests/collections/class-test-collections-block.php b/tests/unit-tests/collections/class-test-collections-block.php index c131ae8059..6c33aa0db0 100644 --- a/tests/unit-tests/collections/class-test-collections-block.php +++ b/tests/unit-tests/collections/class-test-collections-block.php @@ -31,7 +31,7 @@ public function set_up() { // Ensure the block is registered. require_once NEWSPACK_ABSPATH . 'src/blocks/collections/index.php'; - + if ( ! \WP_Block_Type_Registry::get_instance()->is_registered( Collections_Block::BLOCK_NAME ) ) { Collections_Block::register_block(); } @@ -144,6 +144,47 @@ public function test_render_block_with_default_see_all_link() { $this->assertStringContainsString( 'See all', $output, 'Should contain default see all text' ); } + /** + * Test numberOfCTAs attribute handles -1 correctly for showing all CTAs. + * + * @covers \Newspack\Blocks\Collections\Collections_Block::render_block + * @covers \Newspack\Blocks\Collections\Collections_Block::render_collection_ctas + * @covers \Newspack\Collections\Template_Helper::render_cta + */ + public function test_render_block_with_all_ctas() { + $collection_id = $this->create_test_collection(); + + // Create multiple CTAs using a loop. + $ctas_data = []; + $total_ctas = 5; + for ( $i = 1; $i <= $total_ctas; $i++ ) { + $ctas_data[] = [ + 'type' => 'link', + 'label' => "CTA $i", + 'url' => "https://example.com/$i", + ]; + } + Collection_Meta::set( $collection_id, 'ctas', $ctas_data ); + + $attributes = [ + 'selectedCollections' => [ $collection_id ], + 'numberOfCTAs' => -1, + 'showCTAs' => true, + 'showSeeAllLink' => false, + ]; + + $output = $this->render_collections_block( $attributes ); + + // When numberOfCTAs is -1, all CTAs should be displayed. + for ( $i = 1; $i <= $total_ctas; $i++ ) { + $this->assertStringContainsString( "CTA $i", $output ); + } + + // Count CTA elements to verify total count. + $cta_count = substr_count( $output, 'wp-block-button__link' ); + $this->assertEquals( $total_ctas, $cta_count, "Should render all $total_ctas CTAs when numberOfCTAs is -1" ); + } + /** * Test get_block_classes method. * diff --git a/tests/unit-tests/collections/class-test-template-helper.php b/tests/unit-tests/collections/class-test-template-helper.php index ed34c824ba..83f374dcd8 100644 --- a/tests/unit-tests/collections/class-test-template-helper.php +++ b/tests/unit-tests/collections/class-test-template-helper.php @@ -260,12 +260,6 @@ public function test_render_see_all_link() { * @covers \Newspack\Collections\Template_Helper::load_template_part */ public function test_load_template_part() { - ob_start(); - Template_Helper::load_template_part( Template_Helper::TEMPLATE_PARTS_DIR . 'newspack-collection-intro', null, [], [] ); - $output = ob_get_clean(); - $this->assertNotEmpty( $output, 'Collections template part should be processed.' ); - $this->assertStringContainsString( 'collection-intro', $output, 'Collections template part should contain "collection-intro".' ); - // Test collections template part with name parameter. ob_start(); Template_Helper::load_template_part( Template_Helper::TEMPLATE_PARTS_DIR . 'newspack-collection-intro', 'variant', [], [] ); @@ -369,4 +363,21 @@ public function test_normalize_post_list() { $this->assertEquals( 'ids', $result['type'] ); $this->assertEquals( [ $post_id_1, 1 ], $result['items'] ); // 0 filtered out, -1 becomes 1. } + + /** + * Test render_collections_intro generates collections block HTML. + * + * @covers \Newspack\Collections\Template_Helper::render_collections_intro + */ + public function test_render_collections_intro() { + $collection = get_post( $this->create_test_collection() ); + + // Test basic rendering. + $html = Template_Helper::render_collections_intro( $collection ); + $this->assertIsString( $html, 'Intro HTML should be a string.' ); + + // Test with invalid collection. + $html = Template_Helper::render_collections_intro( null ); + $this->assertEmpty( $html, 'Invalid collection should return empty string.' ); + } }