Skip to content

Commit 9d652d2

Browse files
committed
Events: Restore local time when showing events
This moves date rendering back to JavaScript, and posts the grouping there as well. That's necessary because the month boundaries are affected by the user's timezone.
1 parent 406ffc3 commit 9d652d2

File tree

5 files changed

+253
-68
lines changed

5 files changed

+253
-68
lines changed

public_html/wp-content/themes/wporg-events-2023/postcss/base/layout.pcss

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,7 @@ body {
1010
max-width: var(--wp--custom--layout--wide-size) !important;
1111
}
1212
}
13+
14+
.wporg-events__hidden {
15+
display: none;
16+
}

public_html/wp-content/themes/wporg-events-2023/postcss/blocks/event-list.pcss

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@
4949
}
5050

5151
& .wporg-marker-list-item__location {
52+
&::first-letter {
53+
text-transform: capitalize;
54+
}
5255

5356
@media (--medium-small) {
5457
margin-top: 2px;
@@ -96,3 +99,14 @@
9699
}
97100
}
98101
}
102+
103+
body.home .wporg-marker-list__loading {
104+
/* The final height could be anywhere between 50px and 750px, so split the difference in order to minimize the
105+
* amount of layout shift. */
106+
height: 350px;
107+
}
108+
109+
body.page-slug-upcoming-events .wporg-marker-list__loading {
110+
/* Take up most of the viewport to avoid a large layout shift. We know this list will have a lot of entries. */
111+
height: 90vh;
112+
}

public_html/wp-content/themes/wporg-events-2023/src/event-list/block.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,5 +38,6 @@
3838
"lineHeight": true
3939
}
4040
},
41-
"editorScript": "file:./index.js"
41+
"editorScript": "file:./index.js",
42+
"viewScript": [ "wp-a11y", "file:./view.js" ]
4243
}

public_html/wp-content/themes/wporg-events-2023/src/event-list/index.php

Lines changed: 50 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
*/
88

99
namespace WordPressdotorg\Theme\Events_2023\WordPress_Event_List;
10+
1011
use WordPressdotorg\Events_2023;
1112
use WP_Block;
1213
use WordPressdotorg\MU_Plugins\Google_Map;
@@ -57,63 +58,61 @@ function ( $a, $b ) {
5758
return get_no_result_view();
5859
}
5960

60-
if ( (bool) $attributes['groupByMonth'] ) {
61-
// Group events by month year.
62-
$grouped_events = array();
63-
foreach ( $filtered_events as $event ) {
64-
$event_month_year = gmdate( 'F Y', esc_html( $event->timestamp ) );
65-
$grouped_events[ $event_month_year ][] = $event;
66-
}
61+
// Prune to only the used properties, to reduce the size of the payload.
62+
$filtered_events = array_map(
63+
function ( $event ) {
64+
return array(
65+
'title' => $event->title,
66+
'url' => $event->url,
67+
'location' => $event->location,
68+
'timestamp' => $event->timestamp,
69+
);
70+
},
71+
$filtered_events
72+
);
6773

68-
$content = '';
69-
foreach ( $grouped_events as $month_year => $events ) {
70-
$content .= get_section_title( $month_year );
71-
$content .= get_list_markup( $events );
72-
}
73-
} else {
74-
$content = get_list_markup( $filtered_events );
75-
}
74+
$payload = array(
75+
'events' => $filtered_events,
76+
'groupByMonth' => $attributes['groupByMonth'],
77+
);
78+
79+
wp_add_inline_script(
80+
// `generate_block_asset_handle()` includes the index if `viewScript` is an array, so this is fragile.
81+
// There isn't a way to get it programmatically, though, so it just has to manually be kept in sync.
82+
'wporg-event-list-view-script-2',
83+
'globalEventsPayload = ' . wp_json_encode( $payload ) . ';',
84+
'before'
85+
);
86+
87+
ob_start();
88+
89+
?>
90+
91+
<p class="wporg-marker-list__loading">
92+
Loading global events...
93+
<img
94+
src="<?php echo esc_url( includes_url( 'images/spinner-2x.gif' ) ); ?>"
95+
width="20"
96+
height="20"
97+
alt=""
98+
/>
99+
</p>
100+
101+
<?php
102+
103+
$content = ob_get_clean();
76104

77105
$wrapper_attributes = get_block_wrapper_attributes();
106+
78107
return sprintf(
79108
'<div %1$s>%2$s</div>',
80109
$wrapper_attributes,
81110
do_blocks( $content )
82111
);
83112
}
84113

85-
/**
86-
* Returns the event-list markup.
87-
*
88-
* @param array $events Array of events.
89-
*
90-
* @return string
91-
*/
92-
function get_list_markup( $events ) {
93-
$block_markup = '<ul class="wporg-marker-list__container">';
94-
95-
foreach ( $events as $event ) {
96-
$block_markup .= '<li class="wporg-marker-list-item">';
97-
$block_markup .= '<h3 class="wporg-marker-list-item__title"><a class="external-link" href="' . esc_url( $event->url ) . '">' . esc_html( $event->title ) . '</a></h3>';
98-
$block_markup .= '<div class="wporg-marker-list-item__location">' . ucfirst( esc_html( $event->location ) ). '</div>';
99-
$block_markup .= sprintf(
100-
'<time class="wporg-marker-list-item__date-time" date-time="%1$s" title="%1$s"><span class="wporg-google-map__date">%2$s</span><span class="wporg-google-map__time">%3$s</span></time>',
101-
gmdate( 'c', esc_html( $event->timestamp ) ),
102-
gmdate( 'l, M j', esc_html( $event->timestamp ) ),
103-
esc_html( gmdate('H:i', $event->timestamp) . ' UTC' ),
104-
);
105-
$block_markup .= '</li>';
106-
}
107-
108-
$block_markup .= '</ul>';
109-
110-
return $block_markup;
111-
}
112-
113114
/**
114115
* Get a list of the currently-applied filters.
115-
*
116-
* @return array
117116
*/
118117
function filter_events( array $events ): array {
119118
global $wp_query;
@@ -143,32 +142,14 @@ function filter_events( array $events ): array {
143142
$filtered_events = array();
144143
foreach ( $events as $event ) {
145144
// Assuming each event has a 'type' property.
146-
if ( isset($event->type) && in_array($event->type, $terms) ) {
145+
if ( isset( $event->type ) && in_array( $event->type, $terms ) ) {
147146
$filtered_events[] = $event;
148147
}
149148
}
150149

151150
return $filtered_events;
152151
}
153152

154-
/**
155-
* Returns core heading block markup for the date groups.
156-
*
157-
* @param string $heading_text Heading text.
158-
*
159-
* @return string
160-
*/
161-
function get_section_title( $heading_text ) {
162-
$block_markup = '<!-- wp:heading {"style":{"elements":{"link":{"color":{"text":"var:preset|color|charcoal-1"}}},"typography":{"fontStyle":"normal","fontWeight":"700"},"spacing":{"margin":{"top":"var:preset|spacing|40","bottom":"var:preset|spacing|20"}}},"textColor":"charcoal-1","fontSize":"medium","fontFamily":"inter"} -->';
163-
$block_markup .= sprintf(
164-
'<h2 class="wp-block-heading has-charcoal-1-color has-text-color has-link-color has-inter-font-family has-medium-font-size" style="margin-top:var(--wp--preset--spacing--40);margin-bottom:var(--wp--preset--spacing--20);font-style:normal;font-weight:700">%s</h2>',
165-
esc_html( $heading_text )
166-
);
167-
$block_markup .= '<!-- /wp:heading -->';
168-
169-
return $block_markup;
170-
}
171-
172153
/**
173154
* Returns a block driven view when no results are found.
174155
*
@@ -186,9 +167,11 @@ function get_no_result_view() {
186167
'<!-- wp:paragraph {"align":"center"} --><p class="has-text-align-center">%s</p><!-- /wp:paragraph -->',
187168
sprintf(
188169
wp_kses_post(
189-
/* translators: %s is url of the event archives. */
190-
__( 'View <a href="%s">upcoming events</a> or try a different search.', 'wporg' ) ),
191-
esc_url( home_url( '/upcoming-events/' ) ) )
170+
/* translators: %s is the URL of the event archives. */
171+
__( 'View <a href="%s">upcoming events</a> or try a different search.', 'wporg' )
172+
),
173+
esc_url( home_url( '/upcoming-events/' ) )
174+
)
192175
);
193176
$content .= '</div><!-- /wp:group -->';
194177

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
/* global globalEventsPayload */
2+
3+
document.addEventListener( 'DOMContentLoaded', function() {
4+
const speak = wp.a11y.speak;
5+
const globalEventList = document.querySelector( '.wp-block-wporg-event-list' );
6+
7+
/**
8+
* Initialize the component.
9+
*/
10+
function init() {
11+
if ( 'undefined' === typeof globalEventsPayload ) {
12+
// eslint-disable-next-line no-console
13+
console.error( 'Missing globalEventsPayload' );
14+
return;
15+
}
16+
17+
renderGlobalEvents( globalEventsPayload.events, globalEventsPayload.groupByMonth );
18+
}
19+
20+
/**
21+
* Render global events
22+
*
23+
* @param {Array} events
24+
* @param {boolean} groupByMonth
25+
*/
26+
function renderGlobalEvents( events, groupByMonth ) {
27+
const loadingElement = globalEventList.querySelector( '.wporg-marker-list__loading' );
28+
const groupedEvents = {};
29+
let markup = '';
30+
31+
if ( groupByMonth ) {
32+
for ( let i = 0; i < events.length; i++ ) {
33+
const eventMonthYear = new Date( events[ i ].timestamp * 1000 ).toLocaleDateString( [], {
34+
year: 'numeric',
35+
month: 'long',
36+
} );
37+
38+
groupedEvents[ eventMonthYear ] = groupedEvents[ eventMonthYear ] || [];
39+
groupedEvents[ eventMonthYear ].push( events[ i ] );
40+
}
41+
42+
for ( const [ month, eventGroup ] of Object.entries( groupedEvents ) ) {
43+
markup += renderEventGroup( eventGroup, month );
44+
}
45+
} else {
46+
markup = renderEventList( events );
47+
}
48+
49+
globalEventList.innerHTML = markup;
50+
51+
loadingElement.classList.add( 'wporg-events__hidden' );
52+
speak( 'Global events loaded.' );
53+
}
54+
55+
/**
56+
* Encode any HTML in a string to prevent XSS.
57+
*
58+
* @param {string} unsafe
59+
*
60+
* @return {string}
61+
*/
62+
function escapeHtml( unsafe ) {
63+
const safe = document.createTextNode( unsafe ).textContent;
64+
65+
return safe;
66+
}
67+
68+
/**
69+
* Render a group of events for a given month
70+
*
71+
* @param {Array} group
72+
* @param {string} month
73+
*
74+
* @return {string}
75+
*/
76+
function renderEventGroup( group, month ) {
77+
let markup = `
78+
<h2
79+
class="wp-block-heading has-charcoal-1-color has-text-color has-link-color has-inter-font-family has-medium-font-size"
80+
style="margin-top:var(--wp--preset--spacing--40);margin-bottom:var(--wp--preset--spacing--20);font-style:normal;font-weight:700">
81+
${ escapeHtml( month ) }
82+
</h2>`;
83+
84+
markup += renderEventList( group );
85+
86+
return markup;
87+
}
88+
89+
/**
90+
* Render a list of events
91+
*
92+
* @param {Array} events
93+
*
94+
* @return {string}
95+
*/
96+
function renderEventList( events ) {
97+
let markup = '<ul class="wporg-marker-list__container">';
98+
99+
for ( let i = 0; i < events.length; i++ ) {
100+
markup += renderEvent( events[ i ] );
101+
}
102+
103+
markup += '</ul>';
104+
105+
return markup;
106+
}
107+
108+
/**
109+
* Render a single event
110+
*
111+
* @param {Object} event
112+
* @param {string} event.title
113+
* @param {string} event.url
114+
* @param {string} event.location
115+
* @param {number} event.timestamp
116+
*
117+
* @return {string}
118+
*/
119+
function renderEvent( { title, url, location, timestamp } ) {
120+
const markup = `
121+
<li class="wporg-marker-list-item">
122+
<h3 class="wporg-marker-list-item__title">
123+
<a class="external-link" href="${ escapeHtml( url ) }">
124+
${ escapeHtml( title ) }
125+
</a>
126+
</h3>
127+
128+
<div class="wporg-marker-list-item__location">
129+
${ escapeHtml( location ) }
130+
</div>
131+
132+
${ getEventDateTime( title, timestamp ) }
133+
</li>
134+
`;
135+
136+
return markup;
137+
}
138+
139+
/**
140+
* Display a timestamp in the user's timezone and locale format.
141+
*
142+
* Note: The start time and day of the week are important pieces of information to include, since that helps
143+
* attendees know at a glance if it's something they can attend. Otherwise they have to click to open it. The
144+
* timezone is also important to make it clear that we're showing the user's timezone, not the venue's.
145+
*
146+
* @see https://make.wordpress.org/community/2017/03/23/showing-upcoming-local-events-in-wp-admin/#comment-23297
147+
* @see https://make.wordpress.org/community/2017/03/23/showing-upcoming-local-events-in-wp-admin/#comment-23307
148+
*
149+
* @param {string} title
150+
* @param {number} timestamp
151+
*
152+
* @return {string} The formatted date and time.
153+
*/
154+
function getEventDateTime( title, timestamp ) {
155+
const eventDate = new Date( parseInt( timestamp ) * 1000 );
156+
157+
const localeDate = eventDate.toLocaleDateString( [], {
158+
weekday: 'short',
159+
year: 'numeric',
160+
month: 'short',
161+
day: 'numeric',
162+
} );
163+
164+
const localeTime = eventDate.toLocaleString( [], {
165+
timeZoneName: 'short',
166+
hour: 'numeric',
167+
minute: '2-digit',
168+
} );
169+
170+
return `
171+
<time
172+
class="wporg-marker-list-item__date-time"
173+
datetime="${ eventDate.toISOString() }"
174+
title="${ escapeHtml( title ) }"
175+
>
176+
<span class="wporg-google-map__date">${ localeDate }</span>
177+
<span class="wporg-google-map__time">${ localeTime }</span>
178+
</time>
179+
`;
180+
}
181+
182+
init();
183+
} );

0 commit comments

Comments
 (0)