PluginProbe ʕ •ᴥ•ʔ
EmbedPress – PDF Embedder, Embed PDF viewer, YouTube Videos, 3D FlipBook, Social feeds & more / 4.4.7
EmbedPress – PDF Embedder, Embed PDF viewer, YouTube Videos, 3D FlipBook, Social feeds & more v4.4.7
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 8 months 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 3 years ago GoogleCalendar.php 8 months ago GoogleDocs.php 2 years ago GoogleDrive.php 2 years ago GoogleMaps.php 2 years ago GooglePhotos.php 7 months ago Gumroad.php 2 years ago InstagramFeed.php 6 months ago LinkedIn.php 2 years ago Meetup.php 8 months ago NRKRadio.php 2 years ago OneDrive.php 10 months ago OpenSea.php 9 months ago SelfHosted.php 2 years ago Spreaker.php 1 year ago TikTok.php 2 years ago Twitch.php 2 years ago Wrapper.php 3 years ago X.php 2 years ago Youtube.php 9 months ago index.html 7 years ago
Meetup.php
788 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 /** inline {@inheritdoc} */
46 public function validateUrl(Url $url)
47 {
48 return (bool) (
49 preg_match('~meetup\.com/(?:.+)~i', (string) $url) ||
50 preg_match('~meetu\.ps/(?:\w+)/?$~i', (string) $url)
51 );
52 }
53
54 /**
55 * Check if the URL is an RSS feed URL or a group URL that should be treated as RSS
56 */
57 private function isRssUrl($url)
58 {
59 // Check if it's already an RSS URL
60 if (preg_match('~meetup\.com/.+/events/rss~i', $url)) {
61 return true;
62 }
63
64 // Check if it's a group URL that should be converted to RSS
65 if (preg_match('~meetup\.com/[^/]+/?$~i', $url)) {
66 return true;
67 }
68
69 // Check if it's an events URL that should be converted to RSS
70 if (preg_match('~meetup\.com/[^/]+/events/?$~i', $url)) {
71 return true;
72 }
73
74 return false;
75 }
76
77 /**
78 * Check if pro features are enabled
79 */
80 protected function isProFeaturesEnabled()
81 {
82 // Use the Helper class to check pro features
83 if (class_exists('\EmbedPress\Includes\Classes\Helper')) {
84 return Helper::is_pro_features_enabled();
85 }
86
87 // Fallback: check if pro plugin is active
88 return function_exists('is_embedpress_pro_active') && is_embedpress_pro_active();
89 }
90
91 /**
92 * Get pro upgrade message for RSS feeds using EmbedPress styling
93 */
94 private function getProUpgradeMessage($response)
95 {
96 // Get the alert icon URL
97 $alert_icon_url = defined('EMBEDPRESS_URL_ASSETS') ? EMBEDPRESS_URL_ASSETS . 'images/alert.svg' : '';
98
99 $response['html'] = '<div class="embedpress-meetup-upgrade-notice" style="
100 width: calc(100% - 30px);
101 max-width: 500px;
102 margin: 20px auto;
103 background: #fff;
104 border-radius: 20px;
105 padding: 30px;
106 display: flex;
107 flex-direction: column;
108 align-items: center;
109 text-align: center;
110 ">
111 ' . ($alert_icon_url ? '<img src="' . esc_url($alert_icon_url) . '" alt="" style="height: 100px; margin-bottom: 20px;">' : '') . '
112 <h2 style="
113 font-size: 32px;
114 font-weight: 450;
115 color: #131f4d;
116 margin-bottom: 15px;
117 margin-top: 0;
118 ">
119 ' . __('Meetup Events Feed', 'embedpress') . '
120 </h2>
121 <p style="
122 font-size: 14px;
123 font-weight: 400;
124 color: #7c8db5;
125 margin-top: 10px;
126 margin-bottom: 15px;
127 line-height: 1.5;
128 ">
129 ' . sprintf(
130 __('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'),
131 'https://wpdeveloper.com/in/upgrade-embedpress'
132 ) . '
133 </p>
134 <p style="
135 font-size: 12px;
136 font-weight: 400;
137 color: #7c8db5;
138 margin: 0;
139 ">
140 ' . __('For single events, use the individual event URL instead of the RSS feed.', 'embedpress') . '
141 </p>
142 </div>
143
144 <style>
145 .embedpress-meetup-upgrade-notice p a {
146 text-decoration: underline;
147 font-weight: 700;
148 color: #131f4d;
149 }
150 .embedpress-meetup-upgrade-notice p a:hover {
151 color: #0f1a3a;
152 }
153 </style>';
154
155 return $response;
156 }
157
158 /** inline {@inheritdoc} */
159 public function normalizeUrl(Url $url)
160 {
161 $url->convertToHttps();
162 $url->removeQueryString();
163
164 $url_string = (string) $url;
165
166 // If it's a group URL without /events/rss, append it
167 if (preg_match('~meetup\.com/[^/]+/?$~i', $url_string)) {
168 // Don't append if it already has /events/rss
169 if (!preg_match('~meetup\.com/.+/events/rss~i', $url_string)) {
170 $url_string = rtrim($url_string, '/') . '/events/rss';
171 $url = new Url($url_string);
172 }
173 }
174
175 // If it's an events URL without /rss, append it
176 if (preg_match('~meetup\.com/[^/]+/events/?$~i', $url_string)) {
177 // Don't append if it already has /rss
178 if (!preg_match('~meetup\.com/.+/events/rss~i', $url_string)) {
179 $url_string = rtrim($url_string, '/') . '/rss';
180 $url = new Url($url_string);
181 }
182 }
183
184 return $url;
185 }
186
187 public function getStaticResponse()
188 {
189 $meetup_website = 'https://meetup.com';
190 $response = [];
191 $response['type'] = 'rich';
192 $response['provider_name'] = 'Meetup';
193 $response['provider_url'] = $meetup_website;
194 $response['url'] = $this->getUrl();
195
196 add_filter('safe_style_css', [$this, 'safe_style_css']);
197 $allowed_protocols = wp_allowed_protocols();
198 $allowed_protocols[] = 'data';
199
200
201
202 // Check if this is an RSS feed URL
203 if ($this->isRssUrl($this->getUrl())) {
204 // Check if pro features are enabled for RSS feeds
205 if (!$this->isProFeaturesEnabled()) {
206 return $this->getProUpgradeMessage($response);
207 }
208
209 // Delegate RSS feed handling to the pro plugin
210 if (class_exists('\Embedpress\Pro\Providers\Meetup')) {
211 $hash = 'mu_' . md5($this->getUrl() . serialize($this->config));
212 $cache_duration = apply_filters('embedpress_meetup_rss_cache_duration', 3600 * 6); // 6 hour default
213 $filename = wp_get_upload_dir()['basedir'] . "/embedpress/{$hash}.txt";
214
215 return \Embedpress\Pro\Providers\Meetup::handleRssFeed($response, $filename, $this->getUrl(), $cache_duration, $this->config);
216 } else {
217 // Pro plugin not available, show upgrade message
218 return $this->getProUpgradeMessage($response);
219 }
220 }
221
222 // Get cached data or fetch new data
223 $event_data = $this->getCachedEventData();
224
225 if (!$event_data) {
226 $t = wp_remote_get($this->getUrl(), ['timeout' => 10]);
227 if (!is_wp_error($t)) {
228 if ($meetup_page_content = wp_remote_retrieve_body($t)) {
229 $dom = str_get_html($meetup_page_content);
230 $event_data = $this->extractEventDataFromDom($dom);
231 if ($event_data) {
232 $this->cacheEventData($event_data);
233 }
234 }
235 }
236 }
237
238
239 if (!$event_data) {
240 $response['html'] = $this->getUrl();
241 return $response;
242 }
243
244 // Generate HTML from cached data
245 $response['html'] = $this->generateEventHtml($event_data, $allowed_protocols);
246 remove_filter('safe_style_css', [$this, 'safe_style_css']);
247 return $response;
248 }
249
250 /**
251 * Get cached event data using transients
252 */
253 private function getCachedEventData()
254 {
255 $url_hash = md5($this->getUrl());
256 $data_transient_key = 'meetup_event_data_' . $url_hash;
257
258 return get_transient($data_transient_key);
259 }
260
261 /**
262 * Cache event data using transients
263 */
264 private function cacheEventData($event_data, $expiration = 3600)
265 {
266 $url_hash = md5($this->getUrl());
267 $data_transient_key = 'meetup_event_data_' . $url_hash;
268
269 set_transient($data_transient_key, $event_data, $expiration);
270 }
271
272 /**
273 * Extract event data from Next.js JSON or fallback to DOM parsing
274 */
275 private function extractEventDataFromDom($dom)
276 {
277 if (empty($dom) || !is_object($dom)) {
278 return false;
279 }
280
281 // First, try to extract from __NEXT_DATA__ JSON (new Meetup structure)
282 $next_data_script = $dom->find('script#__NEXT_DATA__', 0);
283 if ($next_data_script) {
284 $json_data = $next_data_script->innertext();
285 $data = json_decode($json_data, true);
286
287 if ($data && isset($data['props']['pageProps']['event'])) {
288 $event = $data['props']['pageProps']['event'];
289 return $this->extractFromNextData($event);
290 }
291 }
292
293 // Fallback to old DOM parsing method
294 $header_dom = $dom->find('div[data-event-label="top"]', 0);
295 $body_dom = $dom->find('div[data-event-label="body"]', 0);
296 $event_location_info = $dom->find('div[data-event-label="info"] .sticky', 0);
297
298 if (empty($header_dom) || empty($body_dom) || empty($event_location_info)) {
299 return false;
300 }
301
302 return $this->extractFromDomElements($header_dom, $body_dom, $event_location_info);
303 }
304
305 /**
306 * Extract event data from Next.js __NEXT_DATA__ JSON
307 */
308 private function extractFromNextData($event)
309 {
310 // Extract basic event info
311 $title = isset($event['title']) ? $event['title'] : '';
312 $description = isset($event['description']) ? $event['description'] : '';
313 $event_url = isset($event['eventUrl']) ? $event['eventUrl'] : $this->getUrl();
314
315 // Extract date/time
316 $date_time = '';
317 if (isset($event['dateTime'])) {
318 $timestamp = strtotime($event['dateTime']);
319 $date_time = date('l, F j, Y · g:i A', $timestamp);
320 if (isset($event['endTime'])) {
321 $end_timestamp = strtotime($event['endTime']);
322 $date_time .= ' to ' . date('g:i A T', $end_timestamp);
323 }
324 }
325
326 // Extract hosts
327 $hosts_html = '';
328 if (isset($event['eventHosts']) && is_array($event['eventHosts'])) {
329 $host_names = array();
330 foreach ($event['eventHosts'] as $host) {
331 if (isset($host['name'])) {
332 $host_names[] = esc_html($host['name']);
333 }
334 }
335 if (!empty($host_names)) {
336 $hosts_html = '<div class="ep-event-hosts">Hosted by ' . implode(', ', $host_names) . '</div>';
337 }
338 }
339
340 // Extract venue/location
341 $location_html = '';
342 if (isset($event['eventType']) && $event['eventType'] === 'ONLINE') {
343 $location_html = '<div class="ep-event-location"><strong>📍 Online event</strong></div>';
344 } elseif (isset($event['venue'])) {
345 $venue = $event['venue'];
346 $location_parts = array();
347 if (!empty($venue['name'])) $location_parts[] = $venue['name'];
348 if (!empty($venue['address'])) $location_parts[] = $venue['address'];
349 if (!empty($venue['city'])) $location_parts[] = $venue['city'];
350 if (!empty($venue['state'])) $location_parts[] = $venue['state'];
351
352 if (!empty($location_parts)) {
353 $location_html = '<div class="ep-event-location"><strong>📍 ' . esc_html(implode(', ', $location_parts)) . '</strong></div>';
354 }
355 }
356
357 // Extract featured image
358 $image_html = '';
359 if (isset($event['featuredEventPhoto']['source'])) {
360 $image_url = esc_url($event['featuredEventPhoto']['source']);
361 $image_html = '<div class="ep-event-single-image"><img src="' . $image_url . '" alt="' . esc_attr($title) . '" /></div>';
362 }
363
364 // Format description (convert markdown-style links to HTML)
365 $description_html = $this->formatDescription($description);
366
367 return array(
368 'date' => $date_time,
369 'title' => $title,
370 'content' => $image_html . '<div class="ep-event-description">' . $description_html . '</div>',
371 'host_info' => $hosts_html,
372 'event_location_info' => $location_html,
373 'url' => $event_url
374 );
375 }
376
377 /**
378 * Format description text (convert markdown-style links to HTML)
379 */
380 private function formatDescription($description)
381 {
382 // Convert markdown links [text](url) to HTML
383 $description = preg_replace('/\[([^\]]+)\]\(([^\)]+)\)/', '<a href="$2" target="_blank" rel="noopener">$1</a>', $description);
384
385 // Convert **bold** to <strong>
386 $description = preg_replace('/\*\*([^\*]+)\*\*/', '<strong>$1</strong>', $description);
387
388 // Convert line breaks to <br>
389 $description = nl2br($description);
390
391 return $description;
392 }
393
394 /**
395 * Extract event data from DOM elements (legacy method)
396 */
397 private function extractFromDomElements($header_dom, $body_dom, $event_location_info)
398 {
399
400 // Process location info images
401 $dewqijm = $event_location_info->find('.dewqijm', 0)->find('span', 0);
402 if (!empty($dewqijm)) {
403 $img = $dewqijm->find('noscript', 0)->innertext();
404 $dewqijm->removeChild($dewqijm->find('img', 1));
405 $dewqijm->find('noscript', 0)->remove();
406 $dewqijm->outertext = $dewqijm->makeup() . $dewqijm->innertext . $img . '</span>';
407 }
408
409 $date = $this->embedpress_get_markup_from_node($header_dom->find('time', 0));
410 $title = $this->embedpress_get_markup_from_node($header_dom->find('h1', 0));
411 $emrv9za = $body_dom->find('div.emrv9za', 0);
412 $picture = $emrv9za->find('picture[data-testid="event-description-image"]', 0);
413 if (!empty($picture) && $picture->find('img', 0)) {
414 if ($picture->find('noscript', 0)) {
415 $picture->find('img', 0)->remove();
416 $img = $picture->find('noscript', 0)->innertext();
417 $img = str_replace('/_next/image/', 'https://www.meetup.com/_next/image/', $img);
418 $picture->find('noscript', 0)->remove();
419 $span = $picture->find('div', 0)->find('span', 0);
420 $span->outertext = $span->makeup() . $span->innertext . $img . '</span>';
421 } else {
422 $img = $picture->find('img', 0);
423 $src = $img->src;
424 if ($src && strpos($src, '/_next/image/') === 0) {
425 $img->src = 'https://www.meetup.com' . $img->src;
426 } else if (strpos($src, '//') === false && $srcset = $img->srcset) {
427 $img->src = $this->getLargestImage($srcset);
428 if (strpos($img->src, '//') === false) {
429 $img->src = 'https://www.meetup.com' . $img->src;
430 }
431 }
432 }
433 }
434
435 $content = $this->embedpress_get_markup_from_node($emrv9za);
436
437 $host_info = $header_dom->find('a[data-event-label="hosted-by"]', 0);
438 ob_start();
439 echo $host_info;
440 $host_info = ob_get_clean();
441
442 ob_start();
443 echo $event_location_info;
444 $event_location_info = ob_get_clean();
445
446 // Return structured data instead of generating HTML
447 return [
448 'date' => $date,
449 'title' => $title,
450 'content' => $content,
451 'host_info' => $host_info,
452 'event_location_info' => $event_location_info,
453 'url' => $this->getUrl()
454 ];
455 }
456
457 /**
458 * Generate HTML from cached event data
459 */
460 private function generateEventHtml($event_data, $allowed_protocols)
461 {
462 ob_start();
463 ?>
464 <article class="embedpress-event embedpress-event--modern">
465 <div class="ep-event-card">
466 <header class="ep-event-header">
467 <div class="ep-event-meta">
468 <span class="ep-event--date">
469 <svg class="ep-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
470 <rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect>
471 <line x1="16" y1="2" x2="16" y2="6"></line>
472 <line x1="8" y1="2" x2="8" y2="6"></line>
473 <line x1="3" y1="10" x2="21" y2="10"></line>
474 </svg>
475 <?php echo esc_html($event_data['date']); ?>
476 </span>
477 <?php if (!empty($event_data['event_location_info'])): ?>
478 <div class="ep-event--location">
479 <?php echo wp_kses($event_data['event_location_info'], 'post', $allowed_protocols); ?>
480 </div>
481 <?php endif; ?>
482 </div>
483
484 <a class="ep-event-link" href="<?php echo esc_url($event_data['url']); ?>" target="_blank" rel="noopener noreferrer">
485 <h2 class="ep-event--title"><?php echo esc_html($event_data['title']); ?></h2>
486 </a>
487
488 <?php if (!empty($event_data['host_info'])): ?>
489 <div class="ep-event--host">
490 <?php echo wp_kses_post($event_data['host_info']); ?>
491 </div>
492 <?php endif; ?>
493 </header>
494
495 <section class="ep-event-content">
496 <div class="ep-event--description">
497 <?php echo wp_kses_post($event_data['content']); ?>
498 </div>
499 </section>
500
501 <footer class="ep-event-footer">
502 <a href="<?php echo esc_url($event_data['url']); ?>" target="_blank" rel="noopener noreferrer" class="ep-event-cta">
503 View Event Details
504 <svg class="ep-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
505 <line x1="5" y1="12" x2="19" y2="12"></line>
506 <polyline points="12 5 19 12 12 19"></polyline>
507 </svg>
508 </a>
509 </footer>
510 </div>
511 </article>
512
513 <style>
514 /* Modern Meetup Event Card Styles */
515 .embedpress-event--modern {
516 font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
517 margin: 24px 0;
518 max-width: 100%;
519 }
520
521 .embedpress-event--modern .ep-event-card {
522 background: #ffffff;
523 border: 1px solid #e5e7eb;
524 border-radius: 12px;
525 overflow: hidden;
526 box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px -1px rgba(0, 0, 0, 0.1);
527 transition: all 0.3s ease;
528 }
529
530 .embedpress-event--modern .ep-event-card:hover {
531 box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1);
532 transform: translateY(-2px);
533 }
534
535 /* Header Section */
536 .embedpress-event--modern .ep-event-header {
537 padding: 24px;
538 border-bottom: 1px solid #f3f4f6;
539 }
540
541 .embedpress-event--modern .ep-event-meta {
542 display: flex;
543 flex-wrap: wrap;
544 gap: 16px;
545 margin-bottom: 16px;
546 }
547
548 .embedpress-event--modern .ep-event--date {
549 display: inline-flex;
550 align-items: center;
551 gap: 6px;
552 color: #6b7280;
553 font-size: 14px;
554 font-weight: 500;
555 padding: 6px 12px;
556 background: #f9fafb;
557 border-radius: 6px;
558 }
559
560 .embedpress-event--modern .ep-event--location {
561 display: inline-flex;
562 align-items: center;
563 gap: 6px;
564 color: #059669;
565 font-size: 14px;
566 font-weight: 500;
567 padding: 6px 12px;
568 background: #ecfdf5;
569 border-radius: 6px;
570 }
571
572 .embedpress-event--modern .ep-icon {
573 flex-shrink: 0;
574 width: 16px;
575 height: 16px;
576 }
577
578 .embedpress-event--modern .ep-event-link {
579 text-decoration: none;
580 color: inherit;
581 display: block;
582 transition: color 0.2s ease;
583 }
584
585 .embedpress-event--modern .ep-event-link:hover {
586 color: #007cba;
587 }
588
589 .embedpress-event--modern .ep-event--title {
590 font-size: 24px;
591 font-weight: 700;
592 line-height: 1.3;
593 color: #111827;
594 margin: 0 0 16px 0;
595 transition: color 0.2s ease;
596 }
597
598 .embedpress-event--modern .ep-event-link:hover .ep-event--title {
599 color: #007cba;
600 }
601
602 .embedpress-event--modern .ep-event--host {
603 display: flex;
604 align-items: center;
605 gap: 8px;
606 color: #6b7280;
607 font-size: 14px;
608 }
609
610 .embedpress-event--modern .ep-event--host::before {
611 content: "👤";
612 font-size: 16px;
613 }
614
615 /* Content Section */
616 .embedpress-event--modern .ep-event-content {
617 padding: 24px;
618 }
619
620 .embedpress-event--modern .ep-event--description {
621 color: #374151;
622 font-size: 15px;
623 line-height: 1.6;
624 }
625
626 .embedpress-event--modern .ep-event-image {
627 margin-bottom: 16px;
628 border-radius: 8px;
629 overflow: hidden;
630 }
631
632 .embedpress-event--modern .ep-event-image img {
633 width: 100%;
634 height: auto;
635 display: block;
636 transition: transform 0.3s ease;
637 }
638
639 .embedpress-event--modern .ep-event-card:hover .ep-event-image img {
640 transform: scale(1.02);
641 }
642
643 .embedpress-event--modern .ep-event-description p {
644 margin: 0 0 12px 0;
645 }
646
647 .embedpress-event--modern .ep-event-description p:last-child {
648 margin-bottom: 0;
649 }
650
651 .embedpress-event--modern .ep-event-description a {
652 color: #007cba;
653 text-decoration: underline;
654 transition: color 0.2s ease;
655 }
656
657 .embedpress-event--modern .ep-event-description a:hover {
658 color: #005a87;
659 }
660
661 .embedpress-event--modern .ep-event-description strong {
662 color: #111827;
663 font-weight: 600;
664 }
665
666 /* Footer Section */
667 .embedpress-event--modern .ep-event-footer {
668 padding: 20px 24px;
669 background: #f9fafb;
670 border-top: 1px solid #e5e7eb;
671 }
672
673 .embedpress-event--modern .ep-event-cta {
674 display: inline-flex;
675 align-items: center;
676 gap: 8px;
677 padding: 10px 20px;
678 background: #007cba;
679 color: #ffffff;
680 font-size: 14px;
681 font-weight: 600;
682 border-radius: 6px;
683 text-decoration: none;
684 transition: all 0.2s ease;
685 }
686
687 .embedpress-event--modern .ep-event-cta:hover {
688 background: #005a87;
689 transform: translateX(2px);
690 color: #ffffff;
691 }
692
693 .embedpress-event--modern .ep-event-cta .ep-icon {
694 transition: transform 0.2s ease;
695 }
696
697 .embedpress-event--modern .ep-event-cta:hover .ep-icon {
698 transform: translateX(3px);
699 }
700
701 /* Responsive Design */
702 @media (max-width: 640px) {
703 .embedpress-event--modern .ep-event-header,
704 .embedpress-event--modern .ep-event-content,
705 .embedpress-event--modern .ep-event-footer {
706 padding: 16px;
707 }
708
709 .embedpress-event--modern .ep-event--title {
710 font-size: 20px;
711 }
712
713 .embedpress-event--modern .ep-event-meta {
714 flex-direction: column;
715 gap: 8px;
716 }
717
718 .embedpress-event--modern .ep-event-cta {
719 width: 100%;
720 justify-content: center;
721 }
722 }
723 </style>
724
725 <?php
726 return ob_get_clean();
727 }
728
729 public function safe_style_css($styles)
730 {
731 $styles[] = 'position';
732 $styles[] = 'display';
733 $styles[] = 'opacity';
734 $styles[] = 'box-sizing';
735 $styles[] = 'left';
736 $styles[] = 'bottom';
737 $styles[] = 'right';
738 $styles[] = 'top';
739 return $styles;
740 }
741
742 /**
743 * It checks for data in the node before returning.
744 *
745 * @param \simple_html_dom_node $node
746 * @param string $method
747 * @param string $attr_name
748 *
749 * @return string it returns data from the node if found or empty strings otherwise.
750 */
751 public function embedpress_get_markup_from_node($node, $method = 'innertext', $attr_name = '')
752 {
753 if (!empty($node) && is_object($node)) {
754 if (!empty($attr_name)) {
755 return $node->getAttribute($attr_name);
756 }
757 if (!empty($method) && method_exists($node, $method)) {
758 return $node->{$method}();
759 }
760 return '';
761 }
762 return '';
763 }
764
765 function getLargestImage($srcsetString)
766 {
767 $images = array();
768 // split on comma
769 $srcsetArray = explode(",", $srcsetString);
770 foreach ($srcsetArray as $srcString) {
771 // split on whitespace - optional descriptor
772 $imgArray = explode(" ", trim($srcString));
773 // cast w or x descriptor as an Integer
774 $images[(int)$imgArray[1]] = $imgArray[0];
775 }
776 // find the max
777 $maxIndex = max(array_keys($images));
778 return $images[$maxIndex];
779 }
780
781
782
783
784
785
786
787 }
788