PluginProbe
Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) / 1.8.13
Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) v1.8.13
1.8.13.1 1.8.13 1.8.12.3 1.8.12.2 1.8.12.1 1.8.12 1.8.11.3 1.8.11.2 1.8.11.1 1.8.11 1.6.6 1.6.60 1.6.7 1.6.8 1.6.9 1.7.0 1.7.0.1 1.7.0.11 1.7.0.12 1.7.0.14 1.7.0.2 1.7.0.3 1.7.0.5 1.7.0.6 1.7.0.7 All 212 releases
← All changes | includes/admin/notifications/class-charitable-notifications.php +280 -34 1.8.11 → 1.8.13 View file →
@@ -66,8 +66,17 @@
66 66 */
67 67 private $license_type;
68 68
69 69 /**
70 + * Cached result of has_alert_notifications() for the current request.
71 + *
72 + * @since 1.8.12
73 + *
74 + * @var bool|null
75 + */
76 + private $has_alerts_cache = null;
77 +
78 + /**
70 79 * Returns and/or create the single instance of this class.
71 80 *
72 81 * @since 1.2.0
73 82 *
@@ -111,8 +120,9 @@
111 120 public function hooks() {
112 121 add_action( 'charitable_maybe_show_plugin_notifications', [ $this, 'output' ], 99 ); // where the notifications comes out in the admin.
113 122 add_action( 'charitable_admin_notifications_update', [ $this, 'update' ] );
114 123 add_action( 'deactivate_plugin', [ $this, 'delete' ], 10, 2 );
124 + add_filter( 'charitable_notifications_get', [ $this, 'priority_sort' ], 99 );
115 125 }
116 126
117 127 /**
118 128 * Check if user has access and is enabled.
@@ -158,9 +168,10 @@
158 168 );
159 169
160 170 // if there are new notifications add the indicator to the menu title.
161 171 if ( $this->get_new_notifications_count() ) {
162 - $notifications_menu_item['menu_title'] .= '<div class="charitable-menu-notification-indicator"></div>';
172 + $dot_type = $this->has_alert_notifications() ? 'alert' : 'info';
173 + $notifications_menu_item['menu_title'] .= '<div class="charitable-menu-notification-indicator ' . $dot_type . '"></div>';
163 174 }
164 175
165 176 // If the checklist exists in the submenu, add this item after it.
166 177 $checklist_key = array_search( 'charitable-setup-checklist', array_column( $submenu, 'menu_slug' ), true );
@@ -202,15 +213,18 @@
202 213 return $this->option;
203 214 }
204 215
205 216 $option = (array) get_option( 'charitable_notifications', [] );
206 - // $option = [];
207 217
208 218 $this->option = [
209 - 'update' => ! empty( $option['update'] ) ? (int) $option['update'] : 0,
210 - 'feed' => ! empty( $option['feed'] ) ? (array) $option['feed'] : [],
211 - 'events' => ! empty( $option['events'] ) ? (array) $option['events'] : [],
212 - 'dismissed' => ! empty( $option['dismissed'] ) ? (array) $option['dismissed'] : [],
219 + 'update' => ! empty( $option['update'] ) ? (int) $option['update'] : 0,
220 + 'feed' => ! empty( $option['feed'] ) ? (array) $option['feed'] : [],
221 + 'events' => ! empty( $option['events'] ) ? (array) $option['events'] : [],
222 + 'dismissed' => ! empty( $option['dismissed'] ) ? (array) $option['dismissed'] : [],
223 + 'local' => ! empty( $option['local'] ) ? (array) $option['local'] : [],
224 + 'new_feed' => ! empty( $option['new_feed'] ) ? (array) $option['new_feed'] : [],
225 + 'dismissed_times' => ! empty( $option['dismissed_times'] ) ? (array) $option['dismissed_times'] : [],
226 + 'settings' => ! empty( $option['settings'] ) ? (array) $option['settings'] : [],
213 227 ];
214 228
215 229 return $this->option;
216 230 }
@@ -323,9 +337,10 @@
323 337
324 338 /**
325 339 * Get notification data.
326 340 *
327 - * @since 1.8.3
341 + * @since 1.8.3
342 + * @version 1.8.12
328 343 *
329 344 * @return array
330 345 */
331 346 public function get() {
@@ -347,15 +362,81 @@
347 362 // regardless of the update time, if there are no notifications, update attempt.
348 363 $option = $this->update();
349 364 }
350 365
351 - $feed = ! empty( $option['feed'] ) ? $this->verify_active( $option['feed'] ) : [];
352 - $events = ! empty( $option['events'] ) ? $this->verify_active( $option['events'] ) : [];
366 + $legacy_enabled = ! class_exists( 'Charitable_Notification_Settings' ) ||
367 + Charitable_Notification_Settings::get_instance()->is_category_enabled( 'legacy_feed' );
353 368
354 - return array_merge( $feed, $events );
369 + $feed = ( $legacy_enabled && ! empty( $option['feed'] ) ) ? $this->verify_active( $option['feed'] ) : [];
370 + $events = ( $legacy_enabled && ! empty( $option['events'] ) ) ? $this->verify_active( $option['events'] ) : [];
371 +
372 + $notifications = array_merge( $feed, $events );
373 +
374 + /**
375 + * Filter the final notifications array before output.
376 + *
377 + * Used by addons (e.g. Pro's Charitable_Dynamic_Notifications) to prepend license-state
378 + * notifications (expiring/expired) above the remote feed.
379 + *
380 + * @since 1.8.12
381 + *
382 + * @param array $notifications Merged feed + events notifications.
383 + */
384 + return (array) apply_filters( 'charitable_notifications_get', $notifications );
355 385 }
356 386
357 387 /**
388 + * Final sort — enforces spec priority order regardless of filter injection order.
389 + *
390 + * Order: new_feed (non-dismissible first) → license → health → legal →
391 + * lifecycle → milestone → nudge → everything else (legacy feed).
392 + *
393 + * @since 1.8.12
394 + *
395 + * @param array $notifications
396 + * @return array
397 + */
398 + public function priority_sort( array $notifications ) {
399 + $order = [
400 + 'new_feed_pinned' => [],
401 + 'new_feed' => [],
402 + 'license' => [],
403 + 'health' => [],
404 + 'legal' => [],
405 + 'lifecycle' => [],
406 + 'milestone' => [],
407 + 'nudge' => [],
408 + 'other' => [],
409 + ];
410 +
411 + $license_ids = [ 'license_expired', 'license_expiring' ];
412 +
413 + foreach ( $notifications as $n ) {
414 + $id = isset( $n['id'] ) ? (string) $n['id'] : '';
415 + $category = isset( $n['category'] ) ? $n['category'] : '';
416 +
417 + if ( 'new_feed' === $category ) {
418 + $dismissible = ! isset( $n['dismissible'] ) || (bool) $n['dismissible'];
419 + $order[ $dismissible ? 'new_feed' : 'new_feed_pinned' ][] = $n;
420 + } elseif ( in_array( $id, $license_ids, true ) ) {
421 + $order['license'][] = $n;
422 + } elseif ( isset( $order[ $category ] ) ) {
423 + $order[ $category ][] = $n;
424 + } else {
425 + $order['other'][] = $n;
426 + }
427 + }
428 +
429 + $result = [];
430 + foreach ( $order as $group ) {
431 + foreach ( $group as $item ) {
432 + $result[] = $item;
433 + }
434 + }
435 + return $result;
436 + }
437 +
438 + /**
358 439 * Get notification count in the feed, regardless if they are dismissed or not.
359 440 *
360 441 * @since 1.8.3
361 442 *
@@ -395,8 +476,46 @@
395 476 return (int) $count;
396 477 }
397 478
398 479 /**
480 + * Check if any undismissed notifications are alerts (health or legal category).
481 + *
482 + * @since 1.8.12
483 + *
484 + * @return bool
485 + */
486 + public function has_alert_notifications() {
487 +
488 + if ( null !== $this->has_alerts_cache ) {
489 + return $this->has_alerts_cache;
490 + }
491 +
492 + $option = $this->get_option();
493 + $dismissed_ids = ! empty( $option['dismissed'] ) ? $option['dismissed'] : [];
494 + $notification_feed = $this->get();
495 +
496 + if ( empty( $notification_feed ) ) {
497 + $this->has_alerts_cache = false;
498 + return $this->has_alerts_cache;
499 + }
500 +
501 + $alert_categories = [ 'health', 'legal' ];
502 +
503 + foreach ( $notification_feed as $notification ) {
504 + if ( in_array( (string) $notification['id'], $dismissed_ids, true ) ) {
505 + continue;
506 + }
507 + if ( ! empty( $notification['category'] ) && in_array( $notification['category'], $alert_categories, true ) ) {
508 + $this->has_alerts_cache = true;
509 + return $this->has_alerts_cache;
510 + }
511 + }
512 +
513 + $this->has_alerts_cache = false;
514 + return $this->has_alerts_cache;
515 + }
516 +
517 + /**
399 518 * Add a new Event Driven notification.
400 519 *
401 520 * @since 1.8.3
402 521 *
@@ -542,12 +661,16 @@
542 661
543 662 $option = $this->get_option();
544 663
545 664 $data = [
546 - 'update' => time(),
547 - 'feed' => $this->fetch_feed(),
548 - 'events' => $option['events'],
549 - 'dismissed' => $option['dismissed'],
665 + 'update' => time(),
666 + 'feed' => $this->fetch_feed(),
667 + 'events' => $option['events'],
668 + 'dismissed' => $option['dismissed'],
669 + 'local' => $option['local'],
670 + 'new_feed' => $option['new_feed'],
671 + 'dismissed_times' => $option['dismissed_times'],
672 + 'settings' => $option['settings'],
550 673 ];
551 674
552 675 // phpcs:disable Charitable.PHP.ValidateHooks.InvalidHookName
553 676 /**
@@ -565,8 +688,24 @@
565 688 return $data;
566 689 }
567 690
568 691 /**
692 + * Save specific keys back to the option without triggering a full update.
693 + *
694 + * @since 1.8.12
695 + *
696 + * @param array $partial Associative array of keys to merge into the stored option.
697 + * Intended keys: 'local', 'new_feed', 'dismissed_times', 'settings'.
698 + * @return void
699 + */
700 + public function save_option_partial( array $partial ) {
701 + $option = $this->get_option( false ); // bypass cache
702 + $merged = array_merge( $option, $partial );
703 + update_option( 'charitable_notifications', $merged );
704 + $this->option = false; // invalidate cache
705 + }
706 +
707 + /**
569 708 * Remove notification data from database before a plugin is deactivated.
570 709 *
571 710 * @since 1.8.3
572 711 *
@@ -598,9 +737,10 @@
598 737
599 738 /**
600 739 * Output notifications on Form Overview admin area.
601 740 *
602 - * @since 1.8.3
741 + * @since 1.8.3
742 + * @version 1.8.12
603 743 *
604 744 * @param string $location Location where the notifications are output.
605 745 * @param int $limit_active Limit of active notifications to show.
606 746 */
@@ -627,8 +767,9 @@
627 767 'em' => [],
628 768 'strong' => [],
629 769 'span' => [
630 770 'style' => [],
771 + 'class' => [],
631 772 ],
632 773 'a' => [
633 774 'href' => [],
634 775 'target' => [],
@@ -675,11 +816,22 @@
675 816 if ( ! $title && ! $content ) {
676 817 continue;
677 818 }
678 819
820 + // Non-dismissible notifications (e.g. expired license) always render as active. @since 1.8.12
821 + $is_dismissible = ! isset( $notification['dismissible'] ) || false !== $notification['dismissible'];
822 +
823 + // Dynamic notifications (those with an explicit 'dismissible' key) suppress the relative date. @since 1.8.12
824 + $is_dynamic = isset( $notification['dismissible'] );
825 + $date_string = ! empty( $notification['start'] ) ? $this->human_readable_date_from_mysql( $notification['start'] ) : '';
826 + $date_html = $date_string ? '<span class="charitable-notification-date">' . esc_html( $date_string ) . '</span>' : '';
827 +
679 828 // If the notification ID is not a part of the dismissed notifications, add it to the list.
680 - if ( ! $this->is_dismissed( $notification ) ) {
829 + if ( ! $is_dismissible || ! $this->is_dismissed( $notification ) ) {
681 830
831 + // Dismiss link is suppressed for non-dismissible notifications (e.g. expired license). @since 1.8.12
832 + $dismiss_link = $is_dismissible ? '<a href="#" class="dismiss charitable-notif-dismiss-x" aria-label="Dismiss"><svg viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg" width="10" height="10"><path d="M11.8211 1.3415L10.6451 0.166504L5.98305 4.82484L1.32097 0.166504L0.14502 1.3415L4.80711 5.99984L0.14502 10.6582L1.32097 11.8332L5.98305 7.17484L10.6451 11.8332L11.8211 10.6582L7.159 5.99984L11.8211 1.3415Z" fill="currentColor"/></svg></a>' : '';
833 +
682 834 if ( 'dashboard' === $location ) {
683 835
684 836 // Notification HTML, slightyl different for dashboard.
685 837 $active_notifications_html .= sprintf(
@@ -693,9 +845,9 @@
693 845 </div>
694 846 <div class="notification-content">%2$s</div>
695 847 <div class="actions">
696 848 %4$s
697 - <!----><a href="#" class="dismiss">Dismiss</a>
849 + %9$s
698 850 </div>
699 851 </div>
700 852 </div>
701 853 </div>',
@@ -700,14 +852,15 @@
700 852 </div>
701 853 </div>',
702 854 esc_html( $title ),
703 855 wp_kses( $content, $content_allowed_tags ),
704 - ! empty( $notification['start'] ) ? $this->human_readable_date_from_mysql( $notification['start'] ) : '',
856 + $date_html,
705 857 $this->get_notification_buttons_html( $notification ),
706 858 esc_attr( $notification['id'] ),
707 859 esc_attr( $notification['id'] ),
708 860 $this->get_notification_icon_html( $notification ),
709 - esc_attr( $current_class )
861 + esc_attr( $current_class ),
862 + $dismiss_link // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
710 863 // $this->get_video_badge_html( $this->get_component_data( $notification['video'] ) )
711 864 );
712 865
713 866 } else {
@@ -712,21 +865,26 @@
712 865
713 866 } else {
714 867
715 868 // Notification HTML.
869 + $badge_html = $this->get_notification_badge_html( $notification );
870 + $border_type = ! empty( $notification['badge_type'] ) ? sanitize_html_class( $notification['badge_type'] ) : '';
871 + $border_class = $border_type ? ' charitable-notif-border--' . $border_type : '';
872 +
716 873 $active_notifications_html .= sprintf(
717 874 '<div aria-expanded="true" class="charitable-notification charitable-notification-%5$s %8$s" data-notification-id="%6$s">
718 - <div>
719 - %7$s
875 + <div class="charitable-notification-card-inner">
876 + %9$s
877 + <div class="notif-left">
878 + %10$s
879 + %7$s
880 + </div>
720 881 <div class="body">
721 - <div class="title">
722 - <div class="date">%3$s</div>
723 - <div>%1$s</div>
724 - </div>
882 + %3$s
883 + <h4>%1$s</h4>
725 884 <div class="notification-content">%2$s</div>
726 885 <div class="actions">
727 886 %4$s
728 - <!----><a href="#" class="dismiss">Dismiss</a>
729 887 </div>
730 888 </div>
731 889 </div>
732 890 </div>',
@@ -731,15 +889,16 @@
731 889 </div>
732 890 </div>',
733 891 esc_html( $title ),
734 892 wp_kses( $content, $content_allowed_tags ),
735 - ! empty( $notification['start'] ) ? $this->human_readable_date_from_mysql( $notification['start'] ) : '',
893 + $date_html,
736 894 $this->get_notification_buttons_html( $notification ),
737 895 esc_attr( $notification['id'] ),
738 896 esc_attr( $notification['id'] ),
739 897 $this->get_notification_icon_html( $notification ),
740 - esc_attr( $current_class )
741 - // $this->get_video_badge_html( $this->get_component_data( $notification['video'] ) )
898 + esc_attr( $current_class ),
899 + $dismiss_link, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
900 + $badge_html // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
742 901 );
743 902
744 903 }
745 904
@@ -765,9 +924,9 @@
765 924 </div>
766 925 </div>',
767 926 esc_html( $title ),
768 927 wp_kses( $content, $content_allowed_tags ),
769 - ! empty( $notification['start'] ) ? $this->human_readable_date_from_mysql( $notification['start'] ) : '',
928 + $date_html,
770 929 $this->get_notification_buttons_html( $notification ),
771 930 esc_attr( $notification['id'] ),
772 931 esc_attr( $notification['id'] ),
773 932 $this->get_notification_icon_html( $notification ),
@@ -823,14 +982,75 @@
823 982 * @return string
824 983 */
825 984 private function get_notification_icon_html( $notification ) {
826 985
827 - $icon = '';
986 + $notification_type = ! empty( $notification['notification_type'] ) ? sanitize_text_field( $notification['notification_type'] ) : '';
987 + $category = ! empty( $notification['category'] ) ? sanitize_text_field( $notification['category'] ) : '';
828 988
829 - $notification_type = ! empty( $notification['notification_type'] ) ? sanitize_text_field( $notification['notification_type'] ) : '';
989 + // Category-specific icons take priority over generic notification_type icons.
990 + if ( 'celebration' === $notification_type ) {
991 + // Trophy — campaign goal reached.
992 + return '<div class="icon">
993 + <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" class="charitable-icon-celebration" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
994 + <path d="M8 21h8M12 17v4"/>
995 + <path d="M7 4H5a2 2 0 0 0-2 2v1a4 4 0 0 0 4 4h.5"/>
996 + <path d="M17 4h2a2 2 0 0 1 2 2v1a4 4 0 0 1-4 4h-.5"/>
997 + <path d="M7 4h10v7a5 5 0 0 1-10 0V4z"/>
998 + </svg>
999 + </div>';
1000 + }
830 1001
1002 + if ( 'legal' === $category ) {
1003 + // Same circle exclamation as health alert, orange.
1004 + return '<div class="icon">
1005 + <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" class="charitable-icon-compliance">
1006 + <path fill-rule="evenodd" clip-rule="evenodd" d="M11.99 2.00012C6.47 2.00012 2 6.48012 2 12.0001C2 17.5201 6.47 22.0001 11.99 22.0001C17.52 22.0001 22 17.5201 22 12.0001C22 6.48012 17.52 2.00012 11.99 2.00012ZM13 13.0001V7.00012H11V13.0001H13ZM13 17.0001V15.0001H11V17.0001H13ZM4 12.0001C4 16.4201 7.58 20.0001 12 20.0001C16.42 20.0001 20 16.4201 20 12.0001C20 7.58012 16.42 4.00012 12 4.00012C7.58 4.00012 4 7.58012 4 12.0001Z" fill="currentColor"></path>
1007 + </svg>
1008 + </div>';
1009 + }
1010 +
1011 + if ( 'milestone' === $category ) {
1012 + // Star — donor milestone.
1013 + return '<div class="icon">
1014 + <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" class="charitable-icon-milestone" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
1015 + <path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/>
1016 + </svg>
1017 + </div>';
1018 + }
1019 +
1020 + if ( 'lifecycle' === $category ) {
1021 + // Flag — campaign live or ended.
1022 + return '<div class="icon">
1023 + <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" class="charitable-icon-lifecycle" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
1024 + <path d="M4 15s1-1 4-1 5 2 8 2 4-1 4-1V3s-1 1-4 1-5-2-8-2-4 1-4 1z"/>
1025 + <line x1="4" y1="22" x2="4" y2="15"/>
1026 + </svg>
1027 + </div>';
1028 + }
1029 +
1030 + if ( 'nudge' === $category ) {
1031 + // Lightbulb — engagement nudge.
1032 + return '<div class="icon">
1033 + <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" class="charitable-icon-nudge" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
1034 + <path d="M9 18h6M10 22h4M12 2a7 7 0 00-4 12.87V17h8v-2.13A7 7 0 0012 2z"/>
1035 + </svg>
1036 + </div>';
1037 + }
1038 +
1039 + if ( 'new_feed' === $category ) {
1040 + // Megaphone — announcement.
1041 + return '<div class="icon">
1042 + <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" class="charitable-icon-announcement" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
1043 + <path d="M19 3v18M5 8H3a1 1 0 00-1 1v4a1 1 0 001 1h2l4 3V5L5 8z"/>
1044 + <path d="M19 7l-10 1M19 17L9 16"/>
1045 + </svg>
1046 + </div>';
1047 + }
1048 +
1049 + // Generic fallback by notification_type.
831 1050 switch ( $notification_type ) {
832 1051 case 'warning':
1052 + // Circle exclamation — health alert (keep as-is).
833 1053 $html = '<div class="icon">
834 1054 <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" class="charitable-circle-exclamation warning">
835 1055 <path fill-rule="evenodd" clip-rule="evenodd" d="M11.99 2.00012C6.47 2.00012 2 6.48012 2 12.0001C2 17.5201 6.47 22.0001 11.99 22.0001C17.52 22.0001 22 17.5201 22 12.0001C22 6.48012 17.52 2.00012 11.99 2.00012ZM13 13.0001V7.00012H11V13.0001H13ZM13 17.0001V15.0001H11V17.0001H13ZM4 12.0001C4 16.4201 7.58 20.0001 12 20.0001C16.42 20.0001 20 16.4201 20 12.0001C20 7.58012 16.42 4.00012 12 4.00012C7.58 4.00012 4 7.58012 4 12.0001Z" fill="currentColor"></path>
836 1056 </svg>
@@ -836,16 +1056,42 @@
836 1056 </svg>
837 1057 </div>';
838 1058 break;
839 1059 case 'success':
840 - $html = '<div class="icon"><svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" class="charitable-circle-check success""><path fill-rule="evenodd" clip-rule="evenodd" d="M12 2C6.48 2 2 6.48 2 12C2 17.52 6.48 22 12 22C17.52 22 22 17.52 22 12C22 6.48 17.52 2 12 2ZM12 20C7.59 20 4 16.41 4 12C4 7.59 7.59 4 12 4C16.41 4 20 7.59 20 12C20 16.41 16.41 20 12 20ZM10 14.17L16.59 7.58L18 9L10 17L6 13L7.41 11.59L10 14.17Z" fill="currentColor"></path></svg></div>';
1060 + $html = '<div class="icon"><svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" class="charitable-circle-check success"><path fill-rule="evenodd" clip-rule="evenodd" d="M12 2C6.48 2 2 6.48 2 12C2 17.52 6.48 22 12 22C17.52 22 22 17.52 22 12C22 6.48 17.52 2 12 2ZM12 20C7.59 20 4 16.41 4 12C4 7.59 7.59 4 12 4C16.41 4 20 7.59 20 12C20 16.41 16.41 20 12 20ZM10 14.17L16.59 7.58L18 9L10 17L6 13L7.41 11.59L10 14.17Z" fill="currentColor"></path></svg></div>';
841 1061 break;
842 1062 default:
843 - $html = '<div class="icon"><svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" class="charitable-circle-check success""><path fill-rule="evenodd" clip-rule="evenodd" d="M12 2C6.48 2 2 6.48 2 12C2 17.52 6.48 22 12 22C17.52 22 22 17.52 22 12C22 6.48 17.52 2 12 2ZM12 20C7.59 20 4 16.41 4 12C4 7.59 7.59 4 12 4C16.41 4 20 7.59 20 12C20 16.41 16.41 20 12 20ZM10 14.17L16.59 7.58L18 9L10 17L6 13L7.41 11.59L10 14.17Z" fill="currentColor"></path></svg></div>';
1063 + $html = '<div class="icon"><svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" class="charitable-circle-check success"><path fill-rule="evenodd" clip-rule="evenodd" d="M12 2C6.48 2 2 6.48 2 12C2 17.52 6.48 22 12 22C17.52 22 22 17.52 22 12C22 6.48 17.52 2 12 2ZM12 20C7.59 20 4 16.41 4 12C4 7.59 7.59 4 12 4C16.41 4 20 7.59 20 12C20 16.41 16.41 20 12 20ZM10 14.17L16.59 7.58L18 9L10 17L6 13L7.41 11.59L10 14.17Z" fill="currentColor"></path></svg></div>';
844 1064 break;
845 1065 }
846 1066
847 1067 return $html;
1068 + }
1069 +
1070 + /**
1071 + * Get badge HTML for a notification card.
1072 + *
1073 + * Badge displays the category label (e.g. "HEALTH ALERT", "MILESTONE").
1074 + * Returns empty string if no badge_label is set.
1075 + *
1076 + * @since 1.8.12
1077 + *
1078 + * @param array $notification Notification data.
1079 + * @return string
1080 + */
1081 + private function get_notification_badge_html( $notification ) {
1082 + if ( empty( $notification['badge_label'] ) ) {
1083 + return '';
1084 + }
1085 +
1086 + $badge_type = ! empty( $notification['badge_type'] ) ? sanitize_html_class( $notification['badge_type'] ) : 'info';
1087 + $label = esc_html( strtoupper( $notification['badge_label'] ) );
1088 +
1089 + return sprintf(
1090 + '<span class="charitable-notif-badge charitable-notif-badge--%s">%s</span>',
1091 + $badge_type,
1092 + $label
1093 + );
848 1094 }
849 1095
850 1096 /**
851 1097 * Get human readable date from MySQL timestamp.