PluginProbe ʕ •ᴥ•ʔ
EmbedPress – PDF Embedder, Embed PDF viewer, YouTube Videos, 3D FlipBook, Social feeds & more / trunk
EmbedPress – PDF Embedder, Embed PDF viewer, YouTube Videos, 3D FlipBook, Social feeds & more vtrunk
4.5.6 4.5.5 4.5.4 4.5.3 4.5.2 trunk 1.0.0 1.1.0 1.1.1 1.1.2 1.1.3 1.2.0 1.3.0 1.3.1 1.4.0 1.4.1 1.4.2 1.4.3 1.4.4 1.5.0 1.6.0 1.6.1 1.6.2 1.6.3 1.7.0 1.7.1 1.7.2 1.7.3 1.7.4 1.7.5 2.0.0 2.0.1 2.0.2 2.0.3 2.1.0 2.1.1 2.1.2 2.1.3 2.1.4 2.1.5 2.1.6 2.2.0 2.2.1 2.2.2 2.3.0 2.3.1 2.3.2 2.3.3 2.4.0 2.4.1 2.5.0 2.5.1 2.5.2 2.5.3 2.5.4 2.5.5 2.6.0 2.6.1 2.6.2 2.7.0 2.7.1 2.7.2 2.7.3 2.7.4 2.7.5 2.7.6 2.7.7 3.0.0 3.0.1 3.0.2 3.0.3 3.0.4 3.1.0 3.1.1 3.1.2 3.1.3 3.2.0 3.2.1 3.3.0 3.3.1 3.3.2 3.3.3 3.3.4 3.3.5 3.3.6 3.3.7 3.4.0 3.4.1 3.4.2 3.4.3 3.5.0 3.5.1 3.5.2 3.5.3 3.6.0 3.6.1 3.6.2 3.6.3 3.6.4 3.6.5 3.6.6 3.6.7 3.6.8 3.7.0 3.7.1 3.7.2 3.7.3 3.8.0 3.8.1 3.8.2 3.8.3 3.8.4 3.8.5 3.9.0 3.9.1 3.9.10 3.9.11 3.9.12 3.9.13 3.9.14 3.9.15 3.9.16 3.9.17 3.9.2 3.9.3 3.9.4 3.9.5 3.9.6 3.9.7 3.9.8 3.9.9 4.0.0 4.0.1 4.0.10 4.0.11 4.0.12 4.0.13 4.0.14 4.0.2 4.0.3 4.0.4 4.0.5 4.0.6 4.0.7 4.0.8 4.0.9 4.1.0 4.1.1 4.1.10 4.1.2 4.1.3 4.1.4 4.1.5 4.1.6 4.1.7 4.1.8 4.1.9 4.2.0 4.2.1 4.2.2 4.2.3 4.2.4 4.2.5 4.2.6 4.2.7 4.2.8 4.2.9 4.3.0 4.3.1 4.4.0 4.4.1 4.4.10 4.4.11 4.4.2 4.4.3 4.4.4 4.4.5 4.4.6 4.4.7 4.4.8 4.4.9 4.5.0 4.5.1
embedpress / EmbedPress / Providers / Meetup.php
embedpress / EmbedPress / Providers Last commit date
TemplateLayouts 3 weeks ago AirTable.php 1 year ago Boomplay.php 1 year ago Calendly.php 2 years ago Canva.php 1 year ago FITE.php 1 year ago GettyImages.php 9 months ago Giphy.php 2 years ago GitHub.php 2 years ago GoogleCalendar.php 7 months ago GoogleDocs.php 2 years ago GoogleDrive.php 2 years ago GoogleMaps.php 2 years ago GooglePhotos.php 3 months ago Gumroad.php 2 years ago InstagramFeed.php 3 weeks ago LinkedIn.php 2 years ago Meetup.php 5 months ago NRKRadio.php 2 years ago OneDrive.php 10 months ago OpenSea.php 9 months ago SelfHosted.php 1 month ago Spreaker.php 1 year ago TikTok.php 2 years ago Twitch.php 2 years ago Wistia.php 3 months ago Wrapper.php 2 years ago X.php 2 years ago Youtube.php 3 weeks ago index.html 7 years ago
Meetup.php
949 lines
1 <?php
2
3 /**
4 * Meetup.php
5 *
6 * @package Embera
7 * @author Michael Pratt <yo@michael-pratt.com>
8 * @link http://www.michael-pratt.com/
9 *
10 * For the full copyright and license information, please view the LICENSE
11 * file that was distributed with this source code.
12 */
13
14 namespace EmbedPress\Providers;
15
16 use EmbedPress\Includes\Classes\Helper;
17 use Embera\Provider\ProviderAdapter;
18 use Embera\Provider\ProviderInterface;
19 use Embera\Url;
20
21 (defined('ABSPATH') && defined('EMBEDPRESS_IS_LOADED')) or die("No direct script access allowed.");
22
23 /**
24 * Meetup Provider
25 * @link https://meetup.com
26 */
27 class Meetup extends ProviderAdapter implements ProviderInterface
28 {
29 /** inline {@inheritdoc} */
30 protected $shouldSendRequest = false;
31 /** inline {@inheritdoc} */
32 protected $endpoint = 'https://api.meetup.com/oembed?format=json';
33
34 /** inline {@inheritdoc} */
35 protected static $hosts = [
36 'meetup.com'
37 ];
38
39 /** inline {@inheritdoc} */
40 protected $httpsSupport = true;
41
42 /** inline {@inheritdoc} */
43 protected $responsiveSupport = true;
44
45 /** @var array Array with allowed params for the Meetup Provider */
46 protected $allowedParams = [
47 'maxwidth',
48 'maxheight',
49 'timezone',
50 'date_format',
51 'time_format',
52 'orderby',
53 'order',
54 'per_page',
55 'enable_pagination'
56 ];
57
58 /** inline {@inheritdoc} */
59 public function validateUrl(Url $url)
60 {
61 return (bool) (
62 preg_match('~meetup\.com/(?:.+)~i', (string) $url) ||
63 preg_match('~meetu\.ps/(?:\w+)/?$~i', (string) $url)
64 );
65 }
66
67 /**
68 * Check if the URL is an RSS feed URL or a group URL that should be treated as RSS
69 */
70 private function isRssUrl($url)
71 {
72 // Check if it's already an RSS URL
73 if (preg_match('~meetup\.com/.+/events/rss~i', $url)) {
74 return true;
75 }
76
77 // Check if it's a group URL that should be converted to RSS
78 if (preg_match('~meetup\.com/[^/]+/?$~i', $url)) {
79 return true;
80 }
81
82 // Check if it's an events URL that should be converted to RSS
83 if (preg_match('~meetup\.com/[^/]+/events/?$~i', $url)) {
84 return true;
85 }
86
87 return false;
88 }
89
90 /**
91 * Check if pro features are enabled
92 */
93 protected function isProFeaturesEnabled()
94 {
95 // Use the Helper class to check pro features
96 if (class_exists('\EmbedPress\Includes\Classes\Helper')) {
97 return Helper::is_pro_features_enabled();
98 }
99
100 // Fallback: check if pro plugin is active
101 return function_exists('is_embedpress_pro_active') && is_embedpress_pro_active();
102 }
103
104 /**
105 * Get pro upgrade message for RSS feeds using EmbedPress styling
106 */
107 private function getProUpgradeMessage($response)
108 {
109 // Get the alert icon URL
110 $alert_icon_url = defined('EMBEDPRESS_URL_ASSETS') ? EMBEDPRESS_URL_ASSETS . 'images/alert.svg' : '';
111
112 $response['html'] = '<div class="embedpress-meetup-upgrade-notice" style="
113 width: calc(100% - 30px);
114 max-width: 500px;
115 margin: 20px auto;
116 background: #fff;
117 border-radius: 20px;
118 padding: 30px;
119 display: flex;
120 flex-direction: column;
121 align-items: center;
122 text-align: center;
123 ">
124 ' . ($alert_icon_url ? '<img src="' . esc_url($alert_icon_url) . '" alt="" style="height: 100px; margin-bottom: 20px;">' : '') . '
125 <h2 style="
126 font-size: 32px;
127 font-weight: 450;
128 color: #131f4d;
129 margin-bottom: 15px;
130 margin-top: 0;
131 ">
132 ' . __('Meetup Events Feed', 'embedpress') . '
133 </h2>
134 <p style="
135 font-size: 14px;
136 font-weight: 400;
137 color: #7c8db5;
138 margin-top: 10px;
139 margin-bottom: 15px;
140 line-height: 1.5;
141 ">
142 ' . sprintf(
143 __('Display multiple Meetup events from RSS feeds is a premium feature. You need to upgrade to the <a href="%s" target="_blank">Premium</a> Version to use this feature.', 'embedpress'),
144 'https://wpdeveloper.com/in/upgrade-embedpress'
145 ) . '
146 </p>
147 <p style="
148 font-size: 12px;
149 font-weight: 400;
150 color: #7c8db5;
151 margin: 0;
152 ">
153 ' . __('For single events, use the individual event URL instead of the RSS feed.', 'embedpress') . '
154 </p>
155 </div>
156
157 <style>
158 .embedpress-meetup-upgrade-notice p a {
159 text-decoration: underline;
160 font-weight: 700;
161 color: #131f4d;
162 }
163 .embedpress-meetup-upgrade-notice p a:hover {
164 color: #0f1a3a;
165 }
166 </style>';
167
168 return $response;
169 }
170
171 /** inline {@inheritdoc} */
172 public function normalizeUrl(Url $url)
173 {
174 $url->convertToHttps();
175 $url->removeQueryString();
176
177 $url_string = (string) $url;
178
179 // If it's a group URL without /events/rss, append it
180 if (preg_match('~meetup\.com/[^/]+/?$~i', $url_string)) {
181 // Don't append if it already has /events/rss
182 if (!preg_match('~meetup\.com/.+/events/rss~i', $url_string)) {
183 $url_string = rtrim($url_string, '/') . '/events/rss';
184 $url = new Url($url_string);
185 }
186 }
187
188 // If it's an events URL without /rss, append it
189 if (preg_match('~meetup\.com/[^/]+/events/?$~i', $url_string)) {
190 // Don't append if it already has /rss
191 if (!preg_match('~meetup\.com/.+/events/rss~i', $url_string)) {
192 $url_string = rtrim($url_string, '/') . '/rss';
193 $url = new Url($url_string);
194 }
195 }
196
197 return $url;
198 }
199
200 public function getStaticResponse()
201 {
202 $meetup_website = 'https://meetup.com';
203 $response = [];
204 $response['type'] = 'rich';
205 $response['provider_name'] = 'Meetup';
206 $response['provider_url'] = $meetup_website;
207 $response['url'] = $this->getUrl();
208
209 add_filter('safe_style_css', [$this, 'safe_style_css']);
210 $allowed_protocols = wp_allowed_protocols();
211 $allowed_protocols[] = 'data';
212
213 // Get parameters using getParams() instead of direct config access
214 $params = $this->getParams();
215
216 // Check if this is an RSS feed URL
217 if ($this->isRssUrl($this->getUrl())) {
218 // Check if pro features are enabled for RSS feeds
219 if (!$this->isProFeaturesEnabled()) {
220 return $this->getProUpgradeMessage($response);
221 }
222
223 // Delegate RSS feed handling to the pro plugin
224 if (class_exists('\Embedpress\Pro\Providers\Meetup')) {
225 $hash = 'mu_' . md5($this->getUrl() . serialize($params));
226 $cache_duration = apply_filters('embedpress_meetup_rss_cache_duration', 3600 * 6); // 6 hour default
227 $filename = wp_get_upload_dir()['basedir'] . "/embedpress/{$hash}.txt";
228
229 return \Embedpress\Pro\Providers\Meetup::handleRssFeed($response, $filename, $this->getUrl(), $cache_duration, $params);
230 } else {
231 // Pro plugin not available, show upgrade message
232 return $this->getProUpgradeMessage($response);
233 }
234 }
235
236 // Get cached data or fetch new data
237 $event_data = $this->getCachedEventData();
238
239 if (!$event_data) {
240 $t = wp_remote_get($this->getUrl(), ['timeout' => 10]);
241 if (!is_wp_error($t)) {
242 if ($meetup_page_content = wp_remote_retrieve_body($t)) {
243 $dom = str_get_html($meetup_page_content);
244 $event_data = $this->extractEventDataFromDom($dom);
245 if ($event_data) {
246 $this->cacheEventData($event_data);
247 }
248 }
249 }
250 }
251
252
253 if (!$event_data) {
254 $response['html'] = $this->getUrl();
255 return $response;
256 }
257
258 // Generate HTML from cached data
259 $response['html'] = $this->generateEventHtml($event_data, $allowed_protocols);
260 remove_filter('safe_style_css', [$this, 'safe_style_css']);
261 return $response;
262 }
263
264 /**
265 * Get cached event data using transients
266 */
267 private function getCachedEventData()
268 {
269 // Include params in cache key so different timezone/format settings create different cache entries
270 $params = $this->getParams();
271 $cache_key_data = $this->getUrl() . serialize($params);
272 $url_hash = md5($cache_key_data);
273 $data_transient_key = 'meetup_event_data_' . $url_hash;
274
275 return get_transient($data_transient_key);
276 }
277
278 /**
279 * Cache event data using transients
280 */
281 private function cacheEventData($event_data, $expiration = 3600)
282 {
283 // Include params in cache key so different timezone/format settings create different cache entries
284 $params = $this->getParams();
285 $cache_key_data = $this->getUrl() . serialize($params);
286 $url_hash = md5($cache_key_data);
287 $data_transient_key = 'meetup_event_data_' . $url_hash;
288
289 set_transient($data_transient_key, $event_data, $expiration);
290 }
291
292 /**
293 * Extract event data from Next.js JSON or fallback to DOM parsing
294 */
295 private function extractEventDataFromDom($dom)
296 {
297 if (empty($dom) || !is_object($dom)) {
298 return false;
299 }
300
301 // First, try to extract from __NEXT_DATA__ JSON (new Meetup structure)
302 $next_data_script = $dom->find('script#__NEXT_DATA__', 0);
303 if ($next_data_script) {
304 $json_data = $next_data_script->innertext();
305 $data = json_decode($json_data, true);
306
307 if ($data && isset($data['props']['pageProps']['event'])) {
308 $event = $data['props']['pageProps']['event'];
309 return $this->extractFromNextData($event);
310 }
311 }
312
313 // Fallback to old DOM parsing method
314 $header_dom = $dom->find('div[data-event-label="top"]', 0);
315 $body_dom = $dom->find('div[data-event-label="body"]', 0);
316 $event_location_info = $dom->find('div[data-event-label="info"] .sticky', 0);
317
318 if (empty($header_dom) || empty($body_dom) || empty($event_location_info)) {
319 return false;
320 }
321
322 return $this->extractFromDomElements($header_dom, $body_dom, $event_location_info);
323 }
324
325 /**
326 * Format event date with timezone conversion and custom formatting
327 *
328 * @param string $date_string The date string to format
329 * @param array $params Parameters containing timezone and format settings
330 * @return string Formatted date string or HTML with data attributes for JS conversion
331 */
332 private function formatEventDate($date_string, $params = [])
333 {
334 // Get timezone setting
335 $timezone_setting = isset($params['timezone']) ? $params['timezone'] : 'visitor_timezone';
336
337 // Get date and time format settings
338 $date_format = isset($params['date_format']) ? $params['date_format'] : 'wp_date_format';
339 $time_format = isset($params['time_format']) ? $params['time_format'] : 'wp_time_format';
340
341 // Resolve WordPress format placeholders
342 if ($date_format === 'wp_date_format') {
343 $date_format = get_option('date_format', 'F j, Y');
344 }
345 if ($time_format === 'wp_time_format') {
346 $time_format = get_option('time_format', 'g:i A');
347 }
348
349 // Parse the date string to timestamp
350 // Ensure we're parsing as UTC by using DateTime
351 try {
352 $date_obj = new \DateTime($date_string, new \DateTimeZone('UTC'));
353 $date_timestamp = $date_obj->getTimestamp();
354 } catch (\Exception $e) {
355 // Fallback to strtotime if DateTime fails
356 $date_timestamp = strtotime($date_string);
357 }
358
359 // If visitor timezone is selected, return HTML with data attributes for JS conversion
360 if ($timezone_setting === 'visitor_timezone') {
361 return sprintf(
362 '<span class="ep-event-date" data-visitor-timezone="true" data-utc-timestamp="%d" data-date-format="%s" data-time-format="%s">%s</span>',
363 $date_timestamp,
364 esc_attr($date_format),
365 esc_attr($time_format),
366 esc_html(gmdate('M j, Y, g:i A', $date_timestamp) . ' UTC')
367 );
368 }
369
370 // Get timezone object
371 if ($timezone_setting === 'wp_timezone') {
372 $timezone = wp_timezone();
373 } else {
374 try {
375 $timezone = new \DateTimeZone($timezone_setting);
376 } catch (\Exception $e) {
377 // Fallback to WordPress timezone if invalid timezone provided
378 $timezone = wp_timezone();
379 }
380 }
381
382 // Create DateTime object in UTC (Meetup dates are in UTC)
383 $date = new \DateTime('@' . $date_timestamp);
384
385 // Convert to target timezone
386 $date->setTimezone($timezone);
387
388 // Format the date and time
389 $formatted_date = wp_date($date_format, $date->getTimestamp(), $timezone);
390 $formatted_time = wp_date($time_format, $date->getTimestamp(), $timezone);
391 $timezone_abbr = $date->format('T');
392
393 // Combine date, time, and timezone
394 $full_date = $formatted_date . ', ' . $formatted_time . ' ' . $timezone_abbr;
395
396 return $full_date;
397 }
398
399 /**
400 * Format event time only (for end time)
401 *
402 * @param string $date_string The date string to format
403 * @param array $params Parameters containing timezone and format settings
404 * @return string Formatted time string or HTML with data attributes for JS conversion
405 */
406 private function formatEventTime($date_string, $params = [])
407 {
408 // Get timezone setting
409 $timezone_setting = isset($params['timezone']) ? $params['timezone'] : 'visitor_timezone';
410
411 // Get time format setting
412 $time_format = isset($params['time_format']) ? $params['time_format'] : 'wp_time_format';
413
414 // Resolve WordPress format placeholder
415 if ($time_format === 'wp_time_format') {
416 $time_format = get_option('time_format', 'g:i A');
417 }
418
419 // Parse the date string to timestamp
420 // Ensure we're parsing as UTC by using DateTime
421 try {
422 $date_obj = new \DateTime($date_string, new \DateTimeZone('UTC'));
423 $date_timestamp = $date_obj->getTimestamp();
424 } catch (\Exception $e) {
425 // Fallback to strtotime if DateTime fails
426 $date_timestamp = strtotime($date_string);
427 }
428
429 // If visitor timezone is selected, return HTML with data attributes for JS conversion
430 if ($timezone_setting === 'visitor_timezone') {
431 return sprintf(
432 '<span class="ep-event-end-time" data-visitor-timezone="true" data-utc-timestamp="%d" data-time-format="%s">%s</span>',
433 $date_timestamp,
434 esc_attr($time_format),
435 esc_html(gmdate('g:i A', $date_timestamp) . ' UTC')
436 );
437 }
438
439 // Get timezone object
440 if ($timezone_setting === 'wp_timezone') {
441 $timezone = wp_timezone();
442 } else {
443 try {
444 $timezone = new \DateTimeZone($timezone_setting);
445 } catch (\Exception $e) {
446 // Fallback to WordPress timezone if invalid timezone provided
447 $timezone = wp_timezone();
448 }
449 }
450
451 // Create DateTime object in UTC (Meetup dates are in UTC)
452 $date = new \DateTime('@' . $date_timestamp);
453
454 // Convert to target timezone
455 $date->setTimezone($timezone);
456
457 // Format the time and timezone
458 $formatted_time = wp_date($time_format, $date->getTimestamp(), $timezone);
459 $timezone_abbr = $date->format('T');
460
461 return $formatted_time . ' ' . $timezone_abbr;
462 }
463
464 /**
465 * Extract event data from Next.js __NEXT_DATA__ JSON
466 */
467 private function extractFromNextData($event)
468 {
469 // Get parameters for timezone and format settings
470 $params = $this->getParams();
471
472 // Extract basic event info
473 $title = isset($event['title']) ? $event['title'] : '';
474 $description = isset($event['description']) ? $event['description'] : '';
475 $event_url = isset($event['eventUrl']) ? $event['eventUrl'] : $this->getUrl();
476
477 // Extract date/time with timezone and format settings
478 $date_time = '';
479 if (isset($event['dateTime'])) {
480 $date_time = $this->formatEventDate($event['dateTime'], $params);
481 if (isset($event['endTime'])) {
482 $end_time = $this->formatEventTime($event['endTime'], $params);
483 $date_time .= ' to ' . $end_time;
484 }
485 }
486
487 // Extract hosts
488 $hosts_html = '';
489 if (isset($event['eventHosts']) && is_array($event['eventHosts'])) {
490 $host_names = array();
491 foreach ($event['eventHosts'] as $host) {
492 if (isset($host['name'])) {
493 $host_names[] = esc_html($host['name']);
494 }
495 }
496 if (!empty($host_names)) {
497 $hosts_html = '<div class="ep-event-hosts">Hosted by ' . implode(', ', $host_names) . '</div>';
498 }
499 }
500
501 // Extract venue/location
502 $location_html = '';
503 if (isset($event['eventType']) && $event['eventType'] === 'ONLINE') {
504 $location_html = '<div class="ep-event-location"><strong>📍 Online event</strong></div>';
505 } elseif (isset($event['venue'])) {
506 $venue = $event['venue'];
507 $location_parts = array();
508 if (!empty($venue['name'])) $location_parts[] = $venue['name'];
509 if (!empty($venue['address'])) $location_parts[] = $venue['address'];
510 if (!empty($venue['city'])) $location_parts[] = $venue['city'];
511 if (!empty($venue['state'])) $location_parts[] = $venue['state'];
512
513 if (!empty($location_parts)) {
514 $location_html = '<div class="ep-event-location"><strong>📍 ' . esc_html(implode(', ', $location_parts)) . '</strong></div>';
515 }
516 }
517
518 // Extract featured image
519 $image_html = '';
520 if (isset($event['featuredEventPhoto']['source'])) {
521 $image_url = esc_url($event['featuredEventPhoto']['source']);
522 $image_html = '<div class="ep-event-single-image"><img src="' . $image_url . '" alt="' . esc_attr($title) . '" /></div>';
523 }
524
525 // Format description (convert markdown-style links to HTML)
526 $description_html = $this->formatDescription($description);
527
528 return array(
529 'date' => $date_time,
530 'title' => $title,
531 'content' => $image_html . '<div class="ep-event-description">' . $description_html . '</div>',
532 'host_info' => $hosts_html,
533 'event_location_info' => $location_html,
534 'url' => $event_url
535 );
536 }
537
538 /**
539 * Format description text (convert markdown-style links to HTML)
540 */
541 private function formatDescription($description)
542 {
543 // Convert markdown links [text](url) to HTML
544 $description = preg_replace('/\[([^\]]+)\]\(([^\)]+)\)/', '<a href="$2" target="_blank" rel="noopener">$1</a>', $description);
545
546 // Convert **bold** to <strong>
547 $description = preg_replace('/\*\*([^\*]+)\*\*/', '<strong>$1</strong>', $description);
548
549 // Convert line breaks to <br>
550 $description = nl2br($description);
551
552 return $description;
553 }
554
555 /**
556 * Extract event data from DOM elements (legacy method)
557 */
558 private function extractFromDomElements($header_dom, $body_dom, $event_location_info)
559 {
560
561 // Process location info images
562 $dewqijm = $event_location_info->find('.dewqijm', 0)->find('span', 0);
563 if (!empty($dewqijm)) {
564 $img = $dewqijm->find('noscript', 0)->innertext();
565 $dewqijm->removeChild($dewqijm->find('img', 1));
566 $dewqijm->find('noscript', 0)->remove();
567 $dewqijm->outertext = $dewqijm->makeup() . $dewqijm->innertext . $img . '</span>';
568 }
569
570 $date = $this->embedpress_get_markup_from_node($header_dom->find('time', 0));
571 $title = $this->embedpress_get_markup_from_node($header_dom->find('h1', 0));
572 $emrv9za = $body_dom->find('div.emrv9za', 0);
573 $picture = $emrv9za->find('picture[data-testid="event-description-image"]', 0);
574 if (!empty($picture) && $picture->find('img', 0)) {
575 if ($picture->find('noscript', 0)) {
576 $picture->find('img', 0)->remove();
577 $img = $picture->find('noscript', 0)->innertext();
578 $img = str_replace('/_next/image/', 'https://www.meetup.com/_next/image/', $img);
579 $picture->find('noscript', 0)->remove();
580 $span = $picture->find('div', 0)->find('span', 0);
581 $span->outertext = $span->makeup() . $span->innertext . $img . '</span>';
582 } else {
583 $img = $picture->find('img', 0);
584 $src = $img->src;
585 if ($src && strpos($src, '/_next/image/') === 0) {
586 $img->src = 'https://www.meetup.com' . $img->src;
587 } else if (strpos($src, '//') === false && $srcset = $img->srcset) {
588 $img->src = $this->getLargestImage($srcset);
589 if (strpos($img->src, '//') === false) {
590 $img->src = 'https://www.meetup.com' . $img->src;
591 }
592 }
593 }
594 }
595
596 $content = $this->embedpress_get_markup_from_node($emrv9za);
597
598 $host_info = $header_dom->find('a[data-event-label="hosted-by"]', 0);
599 ob_start();
600 echo $host_info;
601 $host_info = ob_get_clean();
602
603 ob_start();
604 echo $event_location_info;
605 $event_location_info = ob_get_clean();
606
607 // Return structured data instead of generating HTML
608 return [
609 'date' => $date,
610 'title' => $title,
611 'content' => $content,
612 'host_info' => $host_info,
613 'event_location_info' => $event_location_info,
614 'url' => $this->getUrl()
615 ];
616 }
617
618 /**
619 * Generate HTML from cached event data
620 */
621 private function generateEventHtml($event_data, $allowed_protocols)
622 {
623 ob_start();
624 ?>
625 <article class="embedpress-event embedpress-event--modern">
626 <div class="ep-event-card">
627 <header class="ep-event-header">
628 <div class="ep-event-meta">
629 <span class="ep-event--date">
630 <svg class="ep-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
631 <rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect>
632 <line x1="16" y1="2" x2="16" y2="6"></line>
633 <line x1="8" y1="2" x2="8" y2="6"></line>
634 <line x1="3" y1="10" x2="21" y2="10"></line>
635 </svg>
636 <?php echo wp_kses_post($event_data['date']); ?>
637 </span>
638 <?php if (!empty($event_data['event_location_info'])): ?>
639 <div class="ep-event--location">
640 <?php echo wp_kses($event_data['event_location_info'], 'post', $allowed_protocols); ?>
641 </div>
642 <?php endif; ?>
643 </div>
644
645 <a class="ep-event-link" href="<?php echo esc_url($event_data['url']); ?>" target="_blank" rel="noopener noreferrer">
646 <h2 class="ep-event--title"><?php echo esc_html($event_data['title']); ?></h2>
647 </a>
648
649 <?php if (!empty($event_data['host_info'])): ?>
650 <div class="ep-event--host">
651 <?php echo wp_kses_post($event_data['host_info']); ?>
652 </div>
653 <?php endif; ?>
654 </header>
655
656 <section class="ep-event-content">
657 <div class="ep-event--description">
658 <?php echo wp_kses_post($event_data['content']); ?>
659 </div>
660 </section>
661
662 <footer class="ep-event-footer">
663 <a href="<?php echo esc_url($event_data['url']); ?>" target="_blank" rel="noopener noreferrer" class="ep-event-cta">
664 View Event Details
665 <svg class="ep-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
666 <line x1="5" y1="12" x2="19" y2="12"></line>
667 <polyline points="12 5 19 12 12 19"></polyline>
668 </svg>
669 </a>
670 </footer>
671 </div>
672 </article>
673
674 <style>
675 /* Modern Meetup Event Card Styles */
676 .embedpress-event--modern {
677 font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
678 margin: 24px 0;
679 max-width: 100%;
680 }
681
682 .embedpress-event--modern .ep-event-card {
683 background: #ffffff;
684 border: 1px solid #e5e7eb;
685 border-radius: 12px;
686 overflow: hidden;
687 box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px -1px rgba(0, 0, 0, 0.1);
688 transition: all 0.3s ease;
689 }
690
691 .embedpress-event--modern .ep-event-card:hover {
692 box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1);
693 transform: translateY(-2px);
694 }
695
696 /* Header Section */
697 .embedpress-event--modern .ep-event-header {
698 padding: 24px;
699 border-bottom: 1px solid #f3f4f6;
700 }
701
702 .embedpress-event--modern .ep-event-meta {
703 display: flex;
704 flex-wrap: wrap;
705 gap: 16px;
706 margin-bottom: 16px;
707 }
708
709 .embedpress-event--modern .ep-event--date {
710 display: inline-flex;
711 align-items: center;
712 gap: 6px;
713 color: #6b7280;
714 font-size: 14px;
715 font-weight: 500;
716 padding: 6px 12px;
717 background: #f9fafb;
718 border-radius: 6px;
719 }
720
721 .embedpress-event--modern .ep-event--location {
722 display: inline-flex;
723 align-items: center;
724 gap: 6px;
725 color: #059669;
726 font-size: 14px;
727 font-weight: 500;
728 padding: 6px 12px;
729 background: #ecfdf5;
730 border-radius: 6px;
731 }
732
733 .embedpress-event--modern .ep-icon {
734 flex-shrink: 0;
735 width: 16px;
736 height: 16px;
737 }
738
739 .embedpress-event--modern .ep-event-link {
740 text-decoration: none;
741 color: inherit;
742 display: block;
743 transition: color 0.2s ease;
744 }
745
746 .embedpress-event--modern .ep-event-link:hover {
747 color: #007cba;
748 }
749
750 .embedpress-event--modern .ep-event--title {
751 font-size: 24px;
752 font-weight: 700;
753 line-height: 1.3;
754 color: #111827;
755 margin: 0 0 16px 0;
756 transition: color 0.2s ease;
757 }
758
759 .embedpress-event--modern .ep-event-link:hover .ep-event--title {
760 color: #007cba;
761 }
762
763 .embedpress-event--modern .ep-event--host {
764 display: flex;
765 align-items: center;
766 gap: 8px;
767 color: #6b7280;
768 font-size: 14px;
769 }
770
771 .embedpress-event--modern .ep-event--host::before {
772 content: "👤";
773 font-size: 16px;
774 }
775
776 /* Content Section */
777 .embedpress-event--modern .ep-event-content {
778 padding: 24px;
779 }
780
781 .embedpress-event--modern .ep-event--description {
782 color: #374151;
783 font-size: 15px;
784 line-height: 1.6;
785 }
786
787 .embedpress-event--modern .ep-event-image {
788 margin-bottom: 16px;
789 border-radius: 8px;
790 overflow: hidden;
791 }
792
793 .embedpress-event--modern .ep-event-image img {
794 width: 100%;
795 height: auto;
796 display: block;
797 transition: transform 0.3s ease;
798 }
799
800 .embedpress-event--modern .ep-event-card:hover .ep-event-image img {
801 transform: scale(1.02);
802 }
803
804 .embedpress-event--modern .ep-event-description p {
805 margin: 0 0 12px 0;
806 }
807
808 .embedpress-event--modern .ep-event-description p:last-child {
809 margin-bottom: 0;
810 }
811
812 .embedpress-event--modern .ep-event-description a {
813 color: #007cba;
814 text-decoration: underline;
815 transition: color 0.2s ease;
816 }
817
818 .embedpress-event--modern .ep-event-description a:hover {
819 color: #005a87;
820 }
821
822 .embedpress-event--modern .ep-event-description strong {
823 color: #111827;
824 font-weight: 600;
825 }
826
827 /* Footer Section */
828 .embedpress-event--modern .ep-event-footer {
829 padding: 20px 24px;
830 background: #f9fafb;
831 border-top: 1px solid #e5e7eb;
832 }
833
834 .embedpress-event--modern .ep-event-cta {
835 display: inline-flex;
836 align-items: center;
837 gap: 8px;
838 padding: 10px 20px;
839 background: #007cba;
840 color: #ffffff;
841 font-size: 14px;
842 font-weight: 600;
843 border-radius: 6px;
844 text-decoration: none;
845 transition: all 0.2s ease;
846 }
847
848 .embedpress-event--modern .ep-event-cta:hover {
849 background: #005a87;
850 transform: translateX(2px);
851 color: #ffffff;
852 }
853
854 .embedpress-event--modern .ep-event-cta .ep-icon {
855 transition: transform 0.2s ease;
856 }
857
858 .embedpress-event--modern .ep-event-cta:hover .ep-icon {
859 transform: translateX(3px);
860 }
861
862 /* Responsive Design */
863 @media (max-width: 640px) {
864 .embedpress-event--modern .ep-event-header,
865 .embedpress-event--modern .ep-event-content,
866 .embedpress-event--modern .ep-event-footer {
867 padding: 16px;
868 }
869
870 .embedpress-event--modern .ep-event--title {
871 font-size: 20px;
872 }
873
874 .embedpress-event--modern .ep-event-meta {
875 flex-direction: column;
876 gap: 8px;
877 }
878
879 .embedpress-event--modern .ep-event-cta {
880 width: 100%;
881 justify-content: center;
882 }
883 }
884 </style>
885
886 <?php
887 return ob_get_clean();
888 }
889
890 public function safe_style_css($styles)
891 {
892 $styles[] = 'position';
893 $styles[] = 'display';
894 $styles[] = 'opacity';
895 $styles[] = 'box-sizing';
896 $styles[] = 'left';
897 $styles[] = 'bottom';
898 $styles[] = 'right';
899 $styles[] = 'top';
900 return $styles;
901 }
902
903 /**
904 * It checks for data in the node before returning.
905 *
906 * @param \simple_html_dom_node $node
907 * @param string $method
908 * @param string $attr_name
909 *
910 * @return string it returns data from the node if found or empty strings otherwise.
911 */
912 public function embedpress_get_markup_from_node($node, $method = 'innertext', $attr_name = '')
913 {
914 if (!empty($node) && is_object($node)) {
915 if (!empty($attr_name)) {
916 return $node->getAttribute($attr_name);
917 }
918 if (!empty($method) && method_exists($node, $method)) {
919 return $node->{$method}();
920 }
921 return '';
922 }
923 return '';
924 }
925
926 function getLargestImage($srcsetString)
927 {
928 $images = array();
929 // split on comma
930 $srcsetArray = explode(",", $srcsetString);
931 foreach ($srcsetArray as $srcString) {
932 // split on whitespace - optional descriptor
933 $imgArray = explode(" ", trim($srcString));
934 // cast w or x descriptor as an Integer
935 $images[(int)$imgArray[1]] = $imgArray[0];
936 }
937 // find the max
938 $maxIndex = max(array_keys($images));
939 return $images[$maxIndex];
940 }
941
942
943
944
945
946
947
948 }
949