← 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. |