diff --git a/CHANGELOG.MD b/CHANGELOG.MD index b8e7e7cc..6f45d5f7 100644 --- a/CHANGELOG.MD +++ b/CHANGELOG.MD @@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## [Unreleased] +## [1.54.7] - 2024-03-26 + +- TMS-914: + - Add new anchor-links block & functionalities + - Add anchor id-elements to blocks & components +- TMS-1013: Add entries fetching to Eventz event-integration +- TMS-968: Add recurring events to event-component - TMS-1021: Fix image alignment in image-carousel modal ## [1.54.6] - 2024-03-18 diff --git a/assets/scripts/anchor-links.js b/assets/scripts/anchor-links.js new file mode 100644 index 00000000..1c83b3b3 --- /dev/null +++ b/assets/scripts/anchor-links.js @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2024. Hion Digital + */ + +// Use jQuery as $ within this file scope. +const $ = jQuery; // eslint-disable-line no-unused-vars + +/** + * Class AnchorLinks + */ +export default class AnchorLinks { + + /** + * Scroll to anchor element + * + * @param {Object} event The click event object. + * + * @return {void} + */ + scrollToDiv( event ) { + $( 'html, body' ).animate( { + scrollTop: $( $( event.target ).attr( 'href' ) ).offset().top, + }, 200 ); + } + + /** + * Run when the document is ready. + * + * @return {void} + */ + docReady() { + $( '.anchor-links__list a' ).on( 'click', this.scrollToDiv.bind( this ) ); + } +} diff --git a/assets/scripts/theme.js b/assets/scripts/theme.js index f9e86e23..a36c1c2f 100755 --- a/assets/scripts/theme.js +++ b/assets/scripts/theme.js @@ -5,6 +5,7 @@ import Common from './common'; import Accordion from './accordion'; +import AnchorLinks from './anchor-links'; import MapLayout from './map-layout'; import CopyToClipboard from './copy-to-clipboard'; import Hero from './hero'; @@ -32,6 +33,7 @@ import FocusOnSearch from './focus-on-search'; const globalControllers = { Common, Accordion, + AnchorLinks, MapLayout, CopyToClipboard, Hero, diff --git a/assets/styles/blocks/custom/_anchor-links.scss b/assets/styles/blocks/custom/_anchor-links.scss new file mode 100644 index 00000000..2d01728d --- /dev/null +++ b/assets/styles/blocks/custom/_anchor-links.scss @@ -0,0 +1,6 @@ +.anchor-links { + &__list { + padding-left: $theme-spacing; + border-left: 2px solid $primary; + } +} \ No newline at end of file diff --git a/assets/styles/blocks/custom/index.scss b/assets/styles/blocks/custom/index.scss index 0c93c91f..23be7a0f 100644 --- a/assets/styles/blocks/custom/index.scss +++ b/assets/styles/blocks/custom/index.scss @@ -5,6 +5,7 @@ @import "accordion"; @import "grid"; @import "image-gallery"; +@import "anchor-links"; @import "subpages"; @import "notice-banner"; @import "contacts"; diff --git a/lib/ACF/Fields/AnchorLinksFields.php b/lib/ACF/Fields/AnchorLinksFields.php new file mode 100644 index 00000000..d008cb60 --- /dev/null +++ b/lib/ACF/Fields/AnchorLinksFields.php @@ -0,0 +1,100 @@ +add_fields( $this->sub_fields() ); + } + catch ( \Exception $e ) { + ( new Logger() )->error( $e->getMessage(), $e->getTrace() ); + } + } + + /** + * This returns all sub fields of the parent groupable. + * + * @return array + * @throws Exception In case of invalid ACF option. + */ + protected function sub_fields() : array { + $strings = [ + 'title' => [ + 'label' => 'Otsikko', + 'instructions' => '', + ], + 'description' => [ + 'label' => 'Kuvaus', + 'instructions' => '', + ], + 'anchor_links' => [ + 'label' => 'Ankkurilinkit', + 'instructions' => '', + 'button' => 'Lisää linkki', + ], + 'anchor_link' => [ + 'label' => 'Ankkurilinkki', + 'instructions' => 'Kirjoita URL-kenttään "#" ja haluamasi lohkon tai komponentin HTML-ankkuri, esim. #lohkon-ankkuri', + ], + ]; + + $key = $this->get_key(); + + $title_field = ( new Field\Text( $strings['title']['label'] ) ) + ->set_key( "{$key}_title" ) + ->set_name( 'title' ) + ->set_instructions( $strings['title']['instructions'] ); + + $description_field = ( new Field\ExtendedWysiwyg( $strings['description']['label'] ) ) + ->set_key( "{$key}_description" ) + ->set_name( 'description' ) + ->set_tabs( 'visual' ) + ->set_toolbar( 'tms-minimal' ) + ->disable_media_upload() + ->set_height( 100 ) + ->set_instructions( $strings['description']['instructions'] ); + + $anchor_links_field = ( new Field\Repeater( $strings['anchor_links']['label'] ) ) + ->set_key( "{$key}_anchor_links" ) + ->set_name( 'anchor_links' ) + ->set_layout( 'block' ) + ->set_button_label( $strings['anchor_links']['button'] ) + ->set_instructions( $strings['anchor_links']['instructions'] ); + + $anchor_link_field = ( new Field\Link( $strings['anchor_link']['label'] ) ) + ->set_key( "{$key}_anchor_link" ) + ->set_name( 'anchor_link' ) + ->set_instructions( $strings['anchor_link']['instructions'] ); + + $anchor_links_field->add_field( $anchor_link_field ); + + return [ + $title_field, + $description_field, + $anchor_links_field, + ]; + } +} diff --git a/lib/ACF/Fields/LinkListFields.php b/lib/ACF/Fields/LinkListFields.php index afaebfe4..098ec837 100644 --- a/lib/ACF/Fields/LinkListFields.php +++ b/lib/ACF/Fields/LinkListFields.php @@ -64,12 +64,12 @@ protected function sub_fields() : array { $key = $this->get_key(); $title_field = ( new Field\Text( $strings['title']['label'] ) ) - ->set_key( "${key}_title" ) + ->set_key( "{$key}_title" ) ->set_name( 'title' ) ->set_instructions( $strings['title']['instructions'] ); $description_field = ( new Field\ExtendedWysiwyg( $strings['description']['label'] ) ) - ->set_key( "${key}_description" ) + ->set_key( "{$key}_description" ) ->set_name( 'description' ) ->set_tabs( 'visual' ) ->set_toolbar( 'tms-minimal' ) @@ -78,14 +78,14 @@ protected function sub_fields() : array { ->set_instructions( $strings['description']['instructions'] ); $links_field = ( new Field\Repeater( $strings['links']['label'] ) ) - ->set_key( "${key}_links" ) + ->set_key( "{$key}_links" ) ->set_name( 'links' ) ->set_layout( 'block' ) ->set_button_label( $strings['links']['button'] ) ->set_instructions( $strings['links']['instructions'] ); $link_field = ( new Field\Link( $strings['link']['label'] ) ) - ->set_key( "${key}_link" ) + ->set_key( "{$key}_link" ) ->set_name( 'link' ) ->set_instructions( $strings['link']['instructions'] ); diff --git a/lib/ACF/PageGroup.php b/lib/ACF/PageGroup.php index b97abb3a..ae0c5d64 100644 --- a/lib/ACF/PageGroup.php +++ b/lib/ACF/PageGroup.php @@ -24,11 +24,13 @@ class PageGroup { * PageGroup constructor. */ public function __construct() { - add_action( + \add_action( 'init', \Closure::fromCallable( [ $this, 'register_fields' ] ), 100 ); + + $this->add_anchor_fields(); } /** @@ -106,7 +108,7 @@ protected function register_fields() : void { ); $field_group->add_fields( - apply_filters( + \apply_filters( 'tms/acf/group/' . $field_group->get_key() . '/fields', [ $this->get_components_field( $field_group->get_key() ), @@ -114,7 +116,7 @@ protected function register_fields() : void { ) ); - $field_group = apply_filters( + $field_group = \apply_filters( 'tms/acf/group/' . $field_group->get_key(), $field_group ); @@ -143,11 +145,11 @@ protected function get_components_field( string $key ) : Field\FlexibleContent { ]; $components_field = ( new Field\FlexibleContent( $strings['components']['title'] ) ) - ->set_key( "${key}_components" ) + ->set_key( "{$key}_components" ) ->set_name( 'components' ) ->set_instructions( $strings['components']['instructions'] ); - $component_layouts = apply_filters( + $component_layouts = \apply_filters( 'tms/acf/field/' . $components_field->get_key() . '/layouts', [ Layouts\ImageBannerLayout::class, @@ -182,6 +184,60 @@ protected function get_components_field( string $key ) : Field\FlexibleContent { return $components_field; } + + /** + * Add HTML-anchor field to layout fields. + */ + private function add_anchor_fields() : void { + $keys = [ + 'image_banner', + 'call_to_action', + 'content_columns', + 'logo_wall', + 'map', + 'icon_links', + 'social_media', + 'image_carousel', + 'subpages' , + 'textblock', + 'grid', + 'events', + 'articles', + 'blog_articles', + 'sitemap', + 'notice_banner', + 'gravityform', + 'contacts', + 'acc_icon_links', + 'share_links', + 'countdown', + 'video', + ]; + + foreach ( $keys as $component ) { + if ( empty( $component ) ) { + continue; + } + + \add_filter( + "tms/acf/layout/fg_page_components_$component/fields", + function ( $fields ) use ( $component ) { + $anchor_field = ( new Field\Text( 'HTML-ankkuri' ) ) + ->set_key( $component . '_anchor' ) + ->set_name( 'anchor' ) + ->set_instructions( 'Kirjoita sana tai pari, ilman välilyöntejä, + luodaksesi juuri tälle lohkolle uniikki verkko-osoite, jota kutsutaan "ankkuriksi". + Sen avulla voit luoda linkin suoraan tähän osioon sivullasi.' ); + + array_unshift( $fields, $anchor_field ); + + return $fields; + }, + 10, + 1 + ); + } + } } ( new PageGroup() ); diff --git a/lib/Blocks/AnchorLinksBlock.php b/lib/Blocks/AnchorLinksBlock.php new file mode 100644 index 00000000..8b7c402c --- /dev/null +++ b/lib/Blocks/AnchorLinksBlock.php @@ -0,0 +1,89 @@ +title = 'Ankkurilinkit'; + + parent::__construct(); + } + + /** + * Create block fields. + * + * @return array + */ + protected function fields() : array { + $group = new AnchorLinksFields( $this->title, self::NAME ); + + return \apply_filters( + 'tms/block/' . self::KEY . '/fields', + $group->get_fields(), + self::KEY + ); + } + + /** + * This filters the block ACF data. + * + * @param array $data Block's ACF data. + * @param Block $instance The block instance. + * @param array $block The original ACF block array. + * @param string $content The HTML content. + * @param bool $is_preview A flag that shows if we're in preview. + * @param int $post_id The parent post's ID. + * + * @return array The block data. + */ + public function filter_data( $data, $instance, $block, $content, $is_preview, $post_id ) : array { // phpcs:ignore + if ( empty( $data['anchor_links'] ) ) { + return $data; + } + + foreach ( $data['anchor_links'] as $key => $link ) { + if ( ! isset( $link['anchor_link']['url'] ) ) { + continue; + } + } + + return \apply_filters( 'tms/acf/block/' . self::KEY . '/data', $data ); + } + +} diff --git a/lib/BlocksController.php b/lib/BlocksController.php index a7b6e3fa..fb2e0d8e 100644 --- a/lib/BlocksController.php +++ b/lib/BlocksController.php @@ -313,6 +313,14 @@ private function allowed_block_types( $allowed_blocks, $context ) { PostType\Post::SLUG, ], ], + 'acf/anchor-links' => [ + 'post_types' => [ + PostType\Page::SLUG, + PostType\Post::SLUG, + PostType\BlogArticle::SLUG, + PostType\DynamicEvent::SLUG, + ], + ], ]; $blocks = apply_filters( diff --git a/lib/Eventz.php b/lib/Eventz.php index fb84fc17..662756cb 100644 --- a/lib/Eventz.php +++ b/lib/Eventz.php @@ -96,6 +96,17 @@ public static function normalize_event( $event ) : array { $image = $event->images->imageMobile->url; } + // Not recurring by default. + $is_recurring = false; + + // Check if event has recurring dates or weekly entries + if ( isset( $event->event->dates ) && count( $event->event->dates ) > 1 ) { + $is_recurring = true; + } + elseif ( isset( $event->event->entries ) && count( $event->event->entries ) >= 1 ) { + $is_recurring = true; + } + return [ 'name' => $event->name ?? null, 'short_description' => static::get_short_description( $event ) ?? null, @@ -103,7 +114,8 @@ public static function normalize_event( $event ) : array { 'date_title' => __( 'Dates', 'tms-theme-base' ), 'date' => static::get_event_date( $event ), 'dates' => static::get_event_dates( $event ), - 'recurring' => isset( $event->event->dates ) ? count( $event->event->dates ) > 1 : null, + 'entries' => static::get_event_entries( $event ), + 'recurring' => $is_recurring, 'time_title' => __( 'Time', 'tms-theme-base' ), 'time' => static::get_event_time( $event ), // Include raw dates for possible sorting. @@ -393,6 +405,64 @@ public static function get_event_dates( $event ) { return $dates; } + /** + * Get event entries info + * Entries occur if the event is held weekly for specific days + * + * @param object $event Event object. + * + * @return array + */ + public static function get_event_entries( $event ) { + $entries = []; + $entry_data = []; + + if ( empty( $event->event->entries ) ) { + return $entries; + } + + // Get single entry data + foreach ( $event->event->entries as $entry ) { + $entry_data[] = [ + 'day_of_week' => $entry->dayOfWeek, + 'start_time' => $entry->startTimeLocal, + 'end_time' => $entry->endTimeLocal, + 'sold_out' => $entry->isSoldOut, + ]; + } + + $start_date = \DateTime::createFromFormat( 'Y-m-d H:i:s', date( 'Y-m-d H:i:s', strtotime( $event->event->start ) ) ); + $end_date = \DateTime::createFromFormat( 'Y-m-d H:i:s', date( 'Y-m-d H:i:s', strtotime( $event->event->end ) ) ); + + // Loop through days and get the dates each week + foreach ( $entry_data as $entry ) { + if ( date( 'D', strtotime( $entry['day_of_week'] ) ) !== $start_date->format( 'D' ) ) { + $day_of_week = date( 'D', strtotime( $entry['day_of_week'] ) ); + $start_date->modify( "next $day_of_week" ); + } + + // Get all occurences of day in the time range + while ( $start_date <= $end_date ) { + $current_start = new \DateTime( $start_date->format( 'Y-m-d' ) . ' ' . $entry['start_time'] ); + $current_end = new \DateTime( $start_date->format( 'Y-m-d' ) . ' ' . $entry['end_time'] ); + $event_dates = sprintf( + '%s - %s', + $current_start->format( 'j.n.Y H.i' ), + $current_end->format( 'H.i' ) + ); + + $entries[] = [ + 'date' => $event_dates, + 'is_sold_out' => $entry['sold_out'] ?? '', + ]; + + $start_date->modify( '+1 week' ); + } + } + + return $entries; + } + /** * Get event url * diff --git a/lib/Formatters/EventzFormatter.php b/lib/Formatters/EventzFormatter.php index 24940049..6e083c9b 100644 --- a/lib/Formatters/EventzFormatter.php +++ b/lib/Formatters/EventzFormatter.php @@ -62,15 +62,16 @@ public function format( array $layout ) : array { // Create recurring events $event_data['events'] = $events ?? []; if ( ! empty( $event_data['events'] ) ) { - $events = self::create_recurring_events( $event_data ); + $events = self::create_recurring_events( $event_data, $query_params ); } $manual_events = []; if ( ! empty( $layout['manual_event_categories'] ) ) { - $manual_events = self::get_manual_events( $layout['manual_event_categories'] ); + $manual_events = self::get_manual_events( $layout['manual_event_categories'] ); + $recurring_manual_events = self::get_recurring_manual_events( $layout['manual_event_categories'] ); } - $events = array_merge( $events['events'] ?? [], $manual_events ); + $events = array_merge( $events['events'] ?? [], $manual_events ?? [], $recurring_manual_events ?? [] ); if ( empty( $events ) ) { return $layout; @@ -101,22 +102,38 @@ public function format( array $layout ) : array { * Create recurring events as single item. * * @param array $events Events. + * @param array $query_params Query parameters. * * @return void */ - public static function create_recurring_events( $events ) { + public static function create_recurring_events( $events, $query_params ) { $recurring_events = []; if( ! empty( $events['events'] ) ) { foreach ( $events['events'] as $event ) { + + // Chek if event has dates or entries if ( count( $event['dates'] ) > 1 ) { - foreach ( $event['dates'] as $date ) { + $recurring_event_dates = $event['dates']; + } else if ( ! empty( $event['entries'] ) ) { + $recurring_event_dates = $event['entries']; + } + + // Get recurring event single dates + if ( isset( $recurring_event_dates ) ) { + foreach ( $recurring_event_dates as $date ) { $clone = $event; + unset( $endDate ); // Split the dates and times into parts list( $startPart, $endPart ) = explode( ' - ', $date['date'], 2 ); list( $startDate, $startTime ) = explode( ' ', $startPart, 2 ); + // Show only events with dates after start_date in query parameters + if ( isset( $query_params['start'] ) && strtotime( $query_params['start'] ) > strtotime( $startDate ) ) { + continue; + } + // Check if endPart includes date & time if ( strpos($endPart, ' ') ) { list( $endDate, $endTime ) = explode( ' ', $endPart, 2 ); @@ -343,4 +360,68 @@ public static function get_manual_events( array $category_ids = null ) : array { return $events; } + + /** + * Get recurring manual events. + * + * @param array $category_ids List of taxonomy ids. + * + * @return array + */ + protected function get_recurring_manual_events( array $category_ids = null ) : array { + $args = [ + 'post_type' => PostType\ManualEvent::SLUG, + 'posts_per_page' => 200, // phpcs:ignore + 'meta_query' => [ + [ + 'key' => 'recurring_event', + 'value' => 1, + ], + ], + ]; + + if ( ! empty( $category_ids ) ) { + $args['tax_query'] = [ + [ + 'taxonomy' => Taxonomy\ManualEventCategory::SLUG, + 'field' => 'term_id', + 'terms' => array_values( $category_ids ), + 'operator' => 'IN', + ], + ]; + } + + $query = new \WP_Query( $args ); + + if ( empty( $query->posts ) ) { + return []; + } + + // Loop through events + $recurring_events = array_map( function ( $e ) { + $id = $e->ID; + $event = (object) \get_fields( $id ); + + foreach ( $event->dates as $date ) { + date_default_timezone_set( 'Europe/Helsinki' ); + $time_now = \current_datetime()->getTimestamp(); + $event_start = strtotime( $date['start'] ); + $event_end = strtotime( $date['end'] ); + + // Return only ongoing or next upcoming event + if ( ( $time_now > $event_start && $time_now < $event_end ) || $time_now < $event_start ) { + $event->id = $id; + $event->title = \get_the_title( $id ); + $event->url = \get_permalink( $id ); + $event->image = \has_post_thumbnail( $id ) ? \get_the_post_thumbnail_url( $id, 'medium_large' ) : null; + $event->start_datetime = $date['start']; + $event->end_datetime = $date['end']; + + return PostType\ManualEvent::normalize_event( $event ); + } + } + }, $query->posts ); + + return array_filter( $recurring_events ); + } } diff --git a/lib/Traits/Sharing.php b/lib/Traits/Sharing.php index c069ef6e..db3ee69a 100644 --- a/lib/Traits/Sharing.php +++ b/lib/Traits/Sharing.php @@ -64,8 +64,8 @@ protected function get_share_links() : ?array { $selected_channels = array_filter( $selected_channels, fn( $item ) => isset( $channels[ $item ] ) ); $current_post = get_queried_object(); $event_query_var = get_query_var( 'event-id' ); - $event_date = $_GET['date']; - $event_time = $_GET['time']; + $event_date = $_GET['date'] ?? ''; + $event_time = $_GET['time'] ?? ''; $overwrite_url = ''; if ( ! $current_post instanceof \WP_Post ) { diff --git a/models/page-events-search.php b/models/page-events-search.php index b11a8bd4..a4570c33 100644 --- a/models/page-events-search.php +++ b/models/page-events-search.php @@ -238,13 +238,13 @@ protected function do_get_events( array $params ) : array { if ( ! empty( $event_data['events'] ) ) { - $event_data = EventzFormatter::create_recurring_events( $event_data ); + $event_data = EventzFormatter::create_recurring_events( $event_data, $params ); $event_data['events'] = ( new EventzFormatter() )->format_events( $event_data['events'] ); $event_data['events'] = array_map( function ( $item ) { $item['short_description'] = wp_trim_words( $item['short_description'], 30 ); - $item['location_icon'] = $item['is_virtual_event'] + $item['location_icon'] = isset( $item['is_virtual_event'] ) ? 'globe' : 'location'; diff --git a/partials/blocks/block-acc-icon-links.dust b/partials/blocks/block-acc-icon-links.dust index 420335b9..3e3e4844 100644 --- a/partials/blocks/block-acc-icon-links.dust +++ b/partials/blocks/block-acc-icon-links.dust @@ -1,6 +1,6 @@ {?rows} -