← All changes
|
includes/admin/dashboard/class-charitable-dashboard.php
+538
-29
1.8.11
→
1.8.13
View file →
| @@ -58,8 +58,16 @@ | ||
| 58 | 58 | */ |
| 59 | 59 | const BLOG_POSTS_SOURCE_URL = 'https://plugin.wpcharitable.com/wp-content/charitable-blog-posts.json'; |
| 60 | 60 | |
| 61 | 61 | /** |
| 62 | + * URL for the v2 Latest Updates feed (hosted on Cloudflare Pages). | |
| 63 | + * | |
| 64 | + * @since 1.8.12 | |
| 65 | + * @var string | |
| 66 | + */ | |
| 67 | + const LATEST_UPDATES_SOURCE_URL = 'https://plugin.wpcharitable.com/wp-content/charitable-latest-updates.json'; | |
| 68 | + | |
| 69 | + /** | |
| 62 | 70 | * Cached version information from the API. |
| 63 | 71 | * |
| 64 | 72 | * @since 1.8.8 |
| 65 | 73 | * |
| @@ -593,41 +601,51 @@ | ||
| 593 | 601 | * @param string $time_period The time period. |
| 594 | 602 | * @return array |
| 595 | 603 | */ |
| 596 | 604 | public function get_date_range_for_period( $time_period ) { |
| 597 | - $end_date = gmdate( 'Y/m/d' ); | |
| 605 | + /* | |
| 606 | + * Site-local, not UTC: this range feeds Charitable_Reports::get_data(), | |
| 607 | + * which filters on p.post_date (site-local time), so a bound built from | |
| 608 | + * bare UTC excludes part of today's donations on any site whose | |
| 609 | + * timezone is ahead of UTC. Every case below is anchored to the same | |
| 610 | + * site-local "now" so the range is internally consistent. | |
| 611 | + * | |
| 612 | + * @since 1.8.13 | |
| 613 | + */ | |
| 614 | + $now = current_time( 'timestamp', 0 ); | |
| 615 | + $end_date = gmdate( 'Y/m/d', $now ); | |
| 598 | 616 | |
| 599 | 617 | switch ( $time_period ) { |
| 600 | 618 | case 'last-7-days': |
| 601 | - $start_date = gmdate( 'Y/m/d', strtotime( '-7 days' ) ); | |
| 619 | + $start_date = gmdate( 'Y/m/d', strtotime( '-7 days', $now ) ); | |
| 602 | 620 | $days = 7; |
| 603 | 621 | break; |
| 604 | 622 | case 'last-14-days': |
| 605 | - $start_date = gmdate( 'Y/m/d', strtotime( '-14 days' ) ); | |
| 623 | + $start_date = gmdate( 'Y/m/d', strtotime( '-14 days', $now ) ); | |
| 606 | 624 | $days = 14; |
| 607 | 625 | break; |
| 608 | 626 | case 'last-30-days': |
| 609 | - $start_date = gmdate( 'Y/m/d', strtotime( '-30 days' ) ); | |
| 627 | + $start_date = gmdate( 'Y/m/d', strtotime( '-30 days', $now ) ); | |
| 610 | 628 | $days = 30; |
| 611 | 629 | break; |
| 612 | 630 | case 'this-month': |
| 613 | - $start_date = gmdate( 'Y/m/d', strtotime( 'first day of this month' ) ); | |
| 614 | - $days = gmdate( 'j' ); // Current day of month | |
| 631 | + $start_date = gmdate( 'Y/m/d', strtotime( 'first day of this month', $now ) ); | |
| 632 | + $days = gmdate( 'j', $now ); // Current day of month | |
| 615 | 633 | break; |
| 616 | 634 | case 'last-3-months': |
| 617 | - $start_date = gmdate( 'Y/m/d', strtotime( '-3 months' ) ); | |
| 635 | + $start_date = gmdate( 'Y/m/d', strtotime( '-3 months', $now ) ); | |
| 618 | 636 | $days = 90; |
| 619 | 637 | break; |
| 620 | 638 | case 'last-6-months': |
| 621 | - $start_date = gmdate( 'Y/m/d', strtotime( '-6 months' ) ); | |
| 639 | + $start_date = gmdate( 'Y/m/d', strtotime( '-6 months', $now ) ); | |
| 622 | 640 | $days = 180; |
| 623 | 641 | break; |
| 624 | 642 | case 'last-year': |
| 625 | - $start_date = gmdate( 'Y/m/d', strtotime( '-1 year' ) ); | |
| 643 | + $start_date = gmdate( 'Y/m/d', strtotime( '-1 year', $now ) ); | |
| 626 | 644 | $days = 365; |
| 627 | 645 | break; |
| 628 | 646 | default: |
| 629 | - $start_date = gmdate( 'Y/m/d', strtotime( '-7 days' ) ); | |
| 647 | + $start_date = gmdate( 'Y/m/d', strtotime( '-7 days', $now ) ); | |
| 630 | 648 | $days = 7; |
| 631 | 649 | break; |
| 632 | 650 | } |
| 633 | 651 | |
| @@ -856,10 +874,11 @@ | ||
| 856 | 874 | $wpdb->esc_like( '_transient_timeout_charitable_dashboard_stats_changes_' ) . '%' |
| 857 | 875 | ) |
| 858 | 876 | ); |
| 859 | 877 | |
| 860 | - // Clear blog posts cache | |
| 878 | + // Clear blog posts cache and Latest Updates feed cache. | |
| 861 | 879 | delete_transient( 'charitable_blog_posts_cache' ); |
| 880 | + delete_transient( 'charitable_latest_updates_cache' ); | |
| 862 | 881 | } |
| 863 | 882 | |
| 864 | 883 | /** |
| 865 | 884 | * Get the default tab from URL parameter. |
| @@ -1170,11 +1189,18 @@ | ||
| 1170 | 1189 | } |
| 1171 | 1190 | |
| 1172 | 1191 | // Get latest donations using Charitable_Reports |
| 1173 | 1192 | $charitable_reports = Charitable_Reports::get_instance(); |
| 1193 | + /* | |
| 1194 | + * Site-local, not UTC: post_date is stored in site-local time, so a | |
| 1195 | + * bound built from bare UTC excludes part of today's donations on any | |
| 1196 | + * site whose timezone is ahead of UTC. | |
| 1197 | + * | |
| 1198 | + * @since 1.8.13 | |
| 1199 | + */ | |
| 1174 | 1200 | $args = array( |
| 1175 | - 'start_date' => gmdate( 'Y/m/d', strtotime( '-30 days' ) ), | |
| 1176 | - 'end_date' => gmdate( 'Y/m/d' ), | |
| 1201 | + 'start_date' => gmdate( 'Y/m/d', current_time( 'timestamp', 0 ) - ( 30 * DAY_IN_SECONDS ) ), | |
| 1202 | + 'end_date' => gmdate( 'Y/m/d', current_time( 'timestamp', 0 ) ), | |
| 1177 | 1203 | 'days' => 30, |
| 1178 | 1204 | ); |
| 1179 | 1205 | $charitable_reports->init_with_array( 'dashboard', $args ); |
| 1180 | 1206 | $donations = $charitable_reports->get_donations(); |
| @@ -1941,8 +1967,100 @@ | ||
| 1941 | 1967 | endforeach; |
| 1942 | 1968 | } |
| 1943 | 1969 | |
| 1944 | 1970 | /** |
| 1971 | + * Render the "Free Site Analysis" promo card in the dashboard right column. Links to the | |
| 1972 | + * Reports > Site Analysis tab, where the user runs their free Fundraising Score analysis. | |
| 1973 | + * | |
| 1974 | + * @since 1.8.12 | |
| 1975 | + * | |
| 1976 | + * @return void | |
| 1977 | + */ | |
| 1978 | + public function render_site_analysis_section() { | |
| 1979 | + $charitable_sa_url = admin_url( 'admin.php?page=charitable-reports&tab=site-analysis' ); | |
| 1980 | + $charitable_sa_nonce = wp_create_nonce( 'charitable_site_analysis_click' ); | |
| 1981 | + ?> | |
| 1982 | + <style> | |
| 1983 | + /* This card puts content directly in section-content, which has no horizontal padding by | |
| 1984 | + default; restore the standard section padding for this card only. */ | |
| 1985 | + #charitable-dashboard-v2-site-analysis .charitable-dashboard-v2-section-content { padding: 0 20px 20px; } | |
| 1986 | + #charitable-dashboard-v2-site-analysis .charitable-sa-dash-new { | |
| 1987 | + display: inline-block; margin-left: 8px; vertical-align: middle; | |
| 1988 | + background: #5AA152; color: #fff; font-size: 10px; font-weight: 700; | |
| 1989 | + line-height: 1; letter-spacing: .04em; text-transform: uppercase; | |
| 1990 | + padding: 3px 7px; border-radius: 3px; | |
| 1991 | + } | |
| 1992 | + #charitable-dashboard-v2-site-analysis .charitable-sa-dash-body { display: flex; align-items: flex-start; gap: 14px; } | |
| 1993 | + #charitable-dashboard-v2-site-analysis .charitable-sa-dash-icon { | |
| 1994 | + flex: none; width: 40px; height: 40px; border-radius: 8px; | |
| 1995 | + display: flex; align-items: center; justify-content: center; background: #eaf4e8; color: #5AA152; | |
| 1996 | + } | |
| 1997 | + #charitable-dashboard-v2-site-analysis .charitable-sa-dash-icon .dashicons { font-size: 22px; width: 22px; height: 22px; } | |
| 1998 | + #charitable-dashboard-v2-site-analysis .charitable-sa-dash-text { margin: 0 0 12px; color: #50575e; font-size: 13.5px; line-height: 1.5; } | |
| 1999 | + #charitable-dashboard-v2-site-analysis .charitable-sa-dash-button { background-color: #5AA152 !important; border-color: #5AA152 !important; color: #fff !important; text-shadow: none; } | |
| 2000 | + #charitable-dashboard-v2-site-analysis .charitable-sa-dash-button:hover, | |
| 2001 | + #charitable-dashboard-v2-site-analysis .charitable-sa-dash-button:focus { background-color: #3f7539 !important; border-color: #3f7539 !important; color: #fff !important; } | |
| 2002 | + </style> | |
| 2003 | + | |
| 2004 | + <section id="charitable-dashboard-v2-site-analysis" class="charitable-dashboard-v2-section"> | |
| 2005 | + <header class="charitable-dashboard-v2-section-header"> | |
| 2006 | + <h3> | |
| 2007 | + <?php esc_html_e( 'Free Site Analysis', 'charitable' ); ?> | |
| 2008 | + <span class="charitable-sa-dash-new"><?php esc_html_e( 'New', 'charitable' ); ?></span> | |
| 2009 | + </h3> | |
| 2010 | + </header> | |
| 2011 | + <div class="charitable-dashboard-v2-section-content"> | |
| 2012 | + <div class="charitable-sa-dash-body"> | |
| 2013 | + <span class="charitable-sa-dash-icon"><span class="dashicons dashicons-chart-area" aria-hidden="true"></span></span> | |
| 2014 | + <div> | |
| 2015 | + <p class="charitable-sa-dash-text"> | |
| 2016 | + <?php esc_html_e( 'See how your fundraising setup measures up and get specific, prioritized steps to raise more. Free to run, based on your current Charitable configuration.', 'charitable' ); ?> | |
| 2017 | + </p> | |
| 2018 | + <a href="<?php echo esc_url( $charitable_sa_url ); ?>" class="button button-primary charitable-sa-dash-button"> | |
| 2019 | + <?php esc_html_e( 'Run Your Free Analysis', 'charitable' ); ?> | |
| 2020 | + </a> | |
| 2021 | + </div> | |
| 2022 | + </div> | |
| 2023 | + </div> | |
| 2024 | + </section> | |
| 2025 | + <script> | |
| 2026 | + /* Record the click via Charitable's consent-gated usage tracking (sendBeacon so it fires | |
| 2027 | + before the link navigates away). No effect if the user hasn't opted into usage tracking. */ | |
| 2028 | + ( function () { | |
| 2029 | + var link = document.querySelector( '#charitable-dashboard-v2-site-analysis .charitable-sa-dash-button' ); | |
| 2030 | + if ( ! link || ! navigator.sendBeacon ) { return; } | |
| 2031 | + link.addEventListener( 'click', function () { | |
| 2032 | + try { | |
| 2033 | + var fd = new FormData(); | |
| 2034 | + fd.append( 'action', 'charitable_track_site_analysis_click' ); | |
| 2035 | + fd.append( 'nonce', '<?php echo esc_js( $charitable_sa_nonce ); ?>' ); | |
| 2036 | + navigator.sendBeacon( '<?php echo esc_url_raw( admin_url( 'admin-ajax.php' ) ); ?>', fd ); | |
| 2037 | + } catch ( e ) {} | |
| 2038 | + } ); | |
| 2039 | + } )(); | |
| 2040 | + </script> | |
| 2041 | + <?php | |
| 2042 | + } | |
| 2043 | + | |
| 2044 | + /** | |
| 2045 | + * AJAX: record a click on the dashboard "Run Your Free Analysis" button via the same | |
| 2046 | + * consent-gated usage-tracking channel as the recommendation cards. Lands in the | |
| 2047 | + * charitable_recommended_plugins_clicked option, which Charitable_Tracking reports to | |
| 2048 | + * awesomemotive as recommended_plugins_clicked (filter by context 'dashboard_site_analysis'). | |
| 2049 | + * | |
| 2050 | + * @since 1.8.12 | |
| 2051 | + * | |
| 2052 | + * @return void | |
| 2053 | + */ | |
| 2054 | + public function ajax_track_site_analysis_click() { | |
| 2055 | + if ( ! check_ajax_referer( 'charitable_site_analysis_click', 'nonce', false ) || ! current_user_can( 'manage_charitable_settings' ) ) { | |
| 2056 | + wp_send_json_error(); | |
| 2057 | + } | |
| 2058 | + $this->track_recommendation_click( 'clicked', 'site-analysis', 'feature', 'dashboard_site_analysis' ); | |
| 2059 | + wp_send_json_success(); | |
| 2060 | + } | |
| 2061 | + | |
| 2062 | + /** | |
| 1945 | 2063 | * Render the enhance campaign section. |
| 1946 | 2064 | * |
| 1947 | 2065 | * @since 1.8.8 |
| 1948 | 2066 | */ |
| @@ -2935,16 +3053,46 @@ | ||
| 2935 | 3053 | * |
| 2936 | 3054 | * @since 1.8.8 |
| 2937 | 3055 | */ |
| 2938 | 3056 | public function render_latest_updates_section() { |
| 2939 | - // Check if there are blog posts to determine footer link text | |
| 2940 | - $posts = $this->get_blog_posts(); | |
| 2941 | - $has_posts = ! empty( $posts ) && ! $this->has_blog_posts_error( $posts ); | |
| 2942 | - $footer_link_text = $has_posts ? __( 'Read More', 'charitable' ) : __( 'Read Blog', 'charitable' ); | |
| 3057 | + // Try v2 feed first, fall back to legacy blog posts. | |
| 3058 | + $updates = $this->get_latest_updates(); | |
| 3059 | + $has_posts = ! empty( $updates ); | |
| 3060 | + if ( ! $has_posts ) { | |
| 3061 | + $posts = $this->get_blog_posts(); | |
| 3062 | + $has_posts = ! empty( $posts ) && ! $this->has_blog_posts_error( $posts ); | |
| 3063 | + } | |
| 3064 | + $footer_link_text = __( 'See More Updates', 'charitable' ); | |
| 2943 | 3065 | |
| 2944 | - // Check if user is more than 3 updates behind | |
| 3066 | + // Threshold resolves in order: feed meta.behind_threshold → WP filter → hardcoded default (3). | |
| 2945 | 3067 | $updates_behind = $this->get_updates_behind(); |
| 2946 | - $show_version_update = $updates_behind > 3; | |
| 3068 | + $threshold = 3; | |
| 3069 | + // isset (not ! empty) so a feed-supplied 0 correctly overrides to "always alert". | |
| 3070 | + if ( isset( $this->version_info['behind_threshold'] ) ) { | |
| 3071 | + $threshold = (int) $this->version_info['behind_threshold']; | |
| 3072 | + } | |
| 3073 | + /** | |
| 3074 | + * Filter the "behind versions" threshold above which the big update alert renders. | |
| 3075 | + * | |
| 3076 | + * @since 1.8.12 | |
| 3077 | + * | |
| 3078 | + * @param int $threshold Default 3, or value supplied by the feed. | |
| 3079 | + */ | |
| 3080 | + $threshold = (int) apply_filters( 'charitable_latest_updates_behind_threshold', $threshold ); | |
| 3081 | + $show_version_update = $updates_behind > $threshold; | |
| 3082 | + | |
| 3083 | + // Version-state pill resolution (mutually exclusive with the big alert). @since 1.8.12 | |
| 3084 | + // - on_latest : remote latest is known AND user is 0 behind. | |
| 3085 | + // - mildly_behind : 1+ behind but not past the threshold (shows a compact amber nudge). | |
| 3086 | + $latest_known_version = $this->get_latest_version(); | |
| 3087 | + $on_latest = ! empty( $latest_known_version ) && 0 === $updates_behind; | |
| 3088 | + $mildly_behind = ! empty( $latest_known_version ) && $updates_behind > 0 && ! $show_version_update; | |
| 3089 | + | |
| 3090 | + // If blog posts can't load and there's no version update to show, don't display the section at all | |
| 3091 | + if ( ! $has_posts && ! $show_version_update ) { | |
| 3092 | + return; | |
| 3093 | + } | |
| 3094 | + | |
| 2947 | 3095 | $section_class = 'charitable-dashboard-v2-section'; |
| 2948 | 3096 | if ( ! $show_version_update ) { |
| 2949 | 3097 | $section_class .= ' no-update-alert'; |
| 2950 | 3098 | } |
| @@ -2952,8 +3100,49 @@ | ||
| 2952 | 3100 | <section id="charitable-dashboard-v2-latest-updates" class="<?php echo esc_attr( $section_class ); ?>"> |
| 2953 | 3101 | <header class="charitable-dashboard-v2-section-header"> |
| 2954 | 3102 | <h3>Latest Updates</h3> |
| 2955 | 3103 | </header> |
| 3104 | + <?php if ( $on_latest ) : ?> | |
| 3105 | + <div class="charitable-dashboard-v2-latest-updates-uptodate"> | |
| 3106 | + <svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg" width="14" height="14" aria-hidden="true"> | |
| 3107 | + <path d="M16.7 5.3L7.5 14.6L3.3 10.4" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/> | |
| 3108 | + </svg> | |
| 3109 | + <span><?php | |
| 3110 | + echo wp_kses( | |
| 3111 | + sprintf( | |
| 3112 | + /* translators: %s: current installed version number */ | |
| 3113 | + __( "You're running the latest version (<strong>%s</strong>).", 'charitable' ), | |
| 3114 | + esc_html( charitable()->get_version() ) | |
| 3115 | + ), | |
| 3116 | + array( 'strong' => array() ) | |
| 3117 | + ); | |
| 3118 | + ?></span> | |
| 3119 | + </div> | |
| 3120 | + <?php elseif ( $mildly_behind ) : ?> | |
| 3121 | + <div class="charitable-dashboard-v2-latest-updates-behind"> | |
| 3122 | + <svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg" width="14" height="14" aria-hidden="true"> | |
| 3123 | + <circle cx="10" cy="10" r="8" stroke="currentColor" stroke-width="2"/> | |
| 3124 | + <path d="M10 9v4.5" stroke="currentColor" stroke-width="2" stroke-linecap="round"/> | |
| 3125 | + <circle cx="10" cy="6.3" r="1.1" fill="currentColor"/> | |
| 3126 | + </svg> | |
| 3127 | + <span><?php | |
| 3128 | + $versions_label = sprintf( | |
| 3129 | + /* translators: %d: number of versions behind */ | |
| 3130 | + _n( '%d version', '%d versions', $updates_behind, 'charitable' ), | |
| 3131 | + $updates_behind | |
| 3132 | + ); | |
| 3133 | + echo wp_kses( | |
| 3134 | + sprintf( | |
| 3135 | + /* translators: 1: versions-behind label (e.g. "2 versions"), 2: plugins page URL */ | |
| 3136 | + __( 'You are behind <strong>%1$s</strong>. <a href="%2$s">Update now</a>', 'charitable' ), | |
| 3137 | + esc_html( $versions_label ), | |
| 3138 | + esc_url( admin_url( 'plugins.php?s=charitable' ) ) | |
| 3139 | + ), | |
| 3140 | + array( 'strong' => array(), 'a' => array( 'href' => array() ) ) | |
| 3141 | + ); | |
| 3142 | + ?></span> | |
| 3143 | + </div> | |
| 3144 | + <?php endif; ?> | |
| 2956 | 3145 | <div class="charitable-dashboard-v2-section-content"> |
| 2957 | 3146 | <?php if ( $show_version_update ) : ?> |
| 2958 | 3147 | <div class="charitable-dashboard-v2-latest-updates-background"> |
| 2959 | 3148 | <div class="charitable-dashboard-v2-latest-updates-background-content"> |
| @@ -2959,29 +3148,37 @@ | ||
| 2959 | 3148 | <div class="charitable-dashboard-v2-latest-updates-background-content"> |
| 2960 | 3149 | <div class="charitable-dashboard-v2-latest-updates-flex"> |
| 2961 | 3150 | <div class="charitable-dashboard-v2-latest-updates-icon"> |
| 2962 | 3151 | <svg class="charitable-dashboard-v2-latest-updates-icon-placeholder" width="26" height="26" viewBox="0 0 26 26" fill="none" xmlns="http://www.w3.org/2000/svg"> |
| 2963 | - <circle cx="13" cy="13" r="13" fill="#eaa251"/> | |
| 2964 | - <path d="M13 6.5L19.5 17.5H6.5L13 6.5Z" fill="white"/> | |
| 2965 | - <path d="M13 10.5V14.5" stroke="#eaa251" stroke-width="2" stroke-linecap="round"/> | |
| 2966 | - <circle cx="13" cy="17" r="1" fill="#eaa251"/> | |
| 3152 | + <path d="M13 2L24 22H2L13 2Z" stroke="#fff" stroke-width="2" stroke-linejoin="round"/> | |
| 3153 | + <path d="M13 10V16" stroke="#fff" stroke-width="2" stroke-linecap="round"/> | |
| 3154 | + <circle cx="13" cy="19.5" r="1.25" fill="#fff"/> | |
| 2967 | 3155 | </svg> |
| 2968 | 3156 | </div> |
| 2969 | 3157 | <div class="charitable-dashboard-v2-latest-updates-content"> |
| 2970 | - <h4 class="charitable-dashboard-v2-latest-updates-headline">You are currently <?php echo esc_html( $updates_behind ); ?> updates behind. Please upgrade Charitable to take advantage of:</h4> | |
| 2971 | - <div class="charitable-dashboard-v2-latest-updates-features"> | |
| 2972 | - <?php $this->render_update_features(); ?> | |
| 2973 | - </div> | |
| 3158 | + <h4 class="charitable-dashboard-v2-latest-updates-headline"><?php | |
| 3159 | + $version_label = sprintf( '<strong>%s</strong>', esc_html( sprintf( _n( '%d version', '%d versions', $updates_behind, 'charitable' ), $updates_behind ) ) ); | |
| 3160 | + $update_link = sprintf( '<a href="%s">%s</a>', esc_url( admin_url( 'plugins.php?s=charitable' ) ), esc_html__( 'Update now', 'charitable' ) ); | |
| 3161 | + printf( | |
| 3162 | + wp_kses( | |
| 3163 | + /* translators: 1: versions-behind label (e.g. "4 versions"), 2: "Update now" link */ | |
| 3164 | + __( 'You are currently %1$s behind. Please upgrade Charitable to take advantage of the features and addons below. %2$s', 'charitable' ), | |
| 3165 | + array( 'strong' => array(), 'a' => array( 'href' => array() ) ) | |
| 3166 | + ), | |
| 3167 | + $version_label, | |
| 3168 | + $update_link | |
| 3169 | + ); | |
| 3170 | + ?></h4> | |
| 2974 | 3171 | </div> |
| 2975 | 3172 | </div> |
| 2976 | 3173 | </div> |
| 2977 | 3174 | </div> |
| 2978 | 3175 | <?php endif; ?> |
| 2979 | - <?php $this->render_blog_posts(); ?> | |
| 3176 | + <?php $this->render_latest_updates_items(); ?> | |
| 2980 | 3177 | <footer class="charitable-dashboard-v2-latest-updates-footer"> |
| 2981 | 3178 | <div class="charitable-dashboard-v2-latest-updates-footer-content"> |
| 2982 | 3179 | <div class="charitable-dashboard-v2-latest-updates-footer-left"> |
| 2983 | - <a href="https://wpcharitable.com/blog" target="_blank" rel="noopener noreferrer" class="charitable-dashboard-v2-blog-link"> | |
| 3180 | + <a href="https://www.wpcharitable.com/charitable-changelog/" target="_blank" rel="noopener noreferrer" class="charitable-dashboard-v2-blog-link"> | |
| 2984 | 3181 | <span class="charitable-dashboard-v2-blog-text"><?php echo esc_html( $footer_link_text ); ?></span> |
| 2985 | 3182 | <svg class="charitable-dashboard-v2-arrow-icon" width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> |
| 2986 | 3183 | <path d="M6 12L10 8L6 4" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> |
| 2987 | 3184 | </svg> |
| @@ -2990,8 +3187,320 @@ | ||
| 2990 | 3187 | </div> |
| 2991 | 3188 | </footer> |
| 2992 | 3189 | </div> |
| 2993 | 3190 | </section> |
| 3191 | + <?php | |
| 3192 | + } | |
| 3193 | + | |
| 3194 | + | |
| 3195 | + /** | |
| 3196 | + * Render the "Are you enjoying Charitable?" section on the dashboard. | |
| 3197 | + * Mirrors the top admin notice (five-star review) but styled to match the dashboard. | |
| 3198 | + * Only shows when the same eligibility conditions are met and it hasn't been dismissed. | |
| 3199 | + * | |
| 3200 | + * @since 1.8.12 | |
| 3201 | + */ | |
| 3202 | + public function render_enjoying_section() { | |
| 3203 | + // ?preview_enjoy=1 bypasses all eligibility checks for testing (admins only). | |
| 3204 | + $preview = ! empty( $_GET['preview_enjoy'] ) && current_user_can( 'manage_options' ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended | |
| 3205 | + | |
| 3206 | + if ( ! $preview ) { | |
| 3207 | + $activated_datetime = get_option( 'wpcharitable_activated_datetime', false ); | |
| 3208 | + $days = 0; | |
| 3209 | + if ( $activated_datetime ) { | |
| 3210 | + $diff = current_time( 'timestamp' ) - $activated_datetime; | |
| 3211 | + $days = (int) abs( round( $diff / 86400 ) ); | |
| 3212 | + } | |
| 3213 | + | |
| 3214 | + $count_campaigns = wp_count_posts( 'campaign' ); | |
| 3215 | + $total_campaigns = isset( $count_campaigns->publish ) ? (int) $count_campaigns->publish : 0; | |
| 3216 | + $count_donations = wp_count_posts( 'donation' ); | |
| 3217 | + $total_donations = isset( $count_donations->{'charitable-completed'} ) ? (int) $count_donations->{'charitable-completed'} : 0; | |
| 3218 | + | |
| 3219 | + if ( $days < apply_filters( 'charitable_days_since_activated', 14 ) || $total_campaigns < 1 || $total_donations < 1 ) { | |
| 3220 | + return; | |
| 3221 | + } | |
| 3222 | + | |
| 3223 | + // Use the same transient as the top notice so dismissing either dismisses both. | |
| 3224 | + if ( get_transient( 'charitable_five-star-review_banner' ) ) { | |
| 3225 | + return; | |
| 3226 | + } | |
| 3227 | + } | |
| 3228 | + | |
| 3229 | + $feedback_url = 'https://wpcharitable.com/charitable-admin-review/'; | |
| 3230 | + $rating_url = 'https://wpcharitable.com/charitable-admin-review/'; | |
| 3231 | + ?> | |
| 3232 | + <section id="charitable-dashboard-enjoy-section" class="charitable-dashboard-v2-section charitable-dashboard-enjoy-section"> | |
| 3233 | + <button type="button" class="charitable-enjoy-dismiss-btn" aria-label="<?php esc_attr_e( 'Dismiss', 'charitable' ); ?>"> | |
| 3234 | + <svg width="10" height="10" viewBox="0 0 12 12" fill="none"><path d="M1 1L11 11M11 1L1 11" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg> | |
| 3235 | + </button> | |
| 3236 | + | |
| 3237 | + <div class="charitable-enjoy-step" data-step="1"> | |
| 3238 | + <p class="charitable-enjoy-question"><?php esc_html_e( 'Are You Enjoying Charitable?', 'charitable' ); ?></p> | |
| 3239 | + <p class="charitable-enjoy-actions"> | |
| 3240 | + <button type="button" class="charitable-enjoy-link charitable-enjoy-link--yes" data-navigate="3"><?php esc_html_e( 'Yes', 'charitable' ); ?></button> | |
| 3241 | + <button type="button" class="charitable-enjoy-link charitable-enjoy-link--no" data-navigate="2"><?php esc_html_e( 'Not Really', 'charitable' ); ?></button> | |
| 3242 | + </p> | |
| 3243 | + </div> | |
| 3244 | + | |
| 3245 | + <div class="charitable-enjoy-step" data-step="2" style="display:none;"> | |
| 3246 | + <p class="charitable-enjoy-question charitable-enjoy-question--title"><?php esc_html_e( "Sorry To Hear That!", 'charitable' ); ?></p> | |
| 3247 | + <p class="charitable-enjoy-question"><?php esc_html_e( "We're sorry to hear you aren't enjoying Charitable. We would love a chance to improve. Could you take a minute and let us know what we can do better?", 'charitable' ); ?></p> | |
| 3248 | + <p class="charitable-enjoy-actions"> | |
| 3249 | + <a href="<?php echo esc_url( $feedback_url ); ?>" class="charitable-enjoy-link charitable-enjoy-link--yes charitable-enjoy-dismiss-action" target="_blank" rel="noopener noreferrer"><?php esc_html_e( 'Give Feedback', 'charitable' ); ?></a> | |
| 3250 | + <button type="button" class="charitable-enjoy-link charitable-enjoy-link--no charitable-enjoy-dismiss-action"><?php esc_html_e( 'No Thanks', 'charitable' ); ?></button> | |
| 3251 | + </p> | |
| 3252 | + </div> | |
| 3253 | + | |
| 3254 | + <div class="charitable-enjoy-step" data-step="3" style="display:none;"> | |
| 3255 | + <p class="charitable-enjoy-question charitable-enjoy-question--title"><?php esc_html_e( "That's Awesome!", 'charitable' ); ?></p> | |
| 3256 | + <p class="charitable-enjoy-question"><?php esc_html_e( "Could you please do us a BIG favor and give it a 5-star rating on WordPress to help us spread the word and boost our team motivation?", 'charitable' ); ?></p> | |
| 3257 | + <p class="charitable-enjoy-team"><strong><?php esc_html_e( '~ WP Charitable Team', 'charitable' ); ?></strong></p> | |
| 3258 | + <p class="charitable-enjoy-actions"> | |
| 3259 | + <a href="<?php echo esc_url( $rating_url ); ?>" class="charitable-enjoy-link charitable-enjoy-link--yes charitable-enjoy-dismiss-action" target="_blank" rel="noopener noreferrer"><?php esc_html_e( 'Ok, You Deserve It', 'charitable' ); ?></a> | |
| 3260 | + <button type="button" class="charitable-enjoy-link charitable-enjoy-link--no charitable-enjoy-dismiss-action"><?php esc_html_e( 'I Already Did', 'charitable' ); ?></button> | |
| 3261 | + </p> | |
| 3262 | + </div> | |
| 3263 | + </section> | |
| 3264 | + <script> | |
| 3265 | + ( function ( $ ) { | |
| 3266 | + var section = $( '#charitable-dashboard-enjoy-section' ); | |
| 3267 | + | |
| 3268 | + // Navigate between steps. | |
| 3269 | + section.on( 'click', '[data-navigate]', function () { | |
| 3270 | + var target = $( this ).data( 'navigate' ); | |
| 3271 | + section.find( '.charitable-enjoy-step' ).hide(); | |
| 3272 | + section.find( '.charitable-enjoy-step[data-step="' + target + '"]' ).show(); | |
| 3273 | + } ); | |
| 3274 | + | |
| 3275 | + // Dismiss — fires AJAX then removes the section. | |
| 3276 | + function dismissSection() { | |
| 3277 | + $.post( ajaxurl, { | |
| 3278 | + action: 'charitable_dismiss_admin_five_star_rating', | |
| 3279 | + banner_id: 'five-star-review', | |
| 3280 | + } ); | |
| 3281 | + section.fadeOut( 300, function () { $( this ).remove(); } ); | |
| 3282 | + } | |
| 3283 | + | |
| 3284 | + section.on( 'click', '.charitable-enjoy-dismiss-btn', dismissSection ); | |
| 3285 | + section.on( 'click', '.charitable-enjoy-dismiss-action', function () { | |
| 3286 | + setTimeout( dismissSection, 150 ); | |
| 3287 | + } ); | |
| 3288 | + }( jQuery ) ); | |
| 3289 | + </script> | |
| 3290 | + <?php | |
| 3291 | + } | |
| 3292 | + /** | |
| 3293 | + * Map a URL to a local filesystem path if it lives under WP_CONTENT_URL. | |
| 3294 | + * | |
| 3295 | + * @param string $url URL to check. | |
| 3296 | + * @return string|false Absolute file path, or false if the URL is external. | |
| 3297 | + */ | |
| 3298 | + private function get_local_file_path_for_url( $url ) { | |
| 3299 | + $content_url = untrailingslashit( content_url() ); | |
| 3300 | + // Normalize schemes so http/https mismatch doesn't break the comparison. | |
| 3301 | + $url_norm = preg_replace( '#^https?://#', '//', $url ); | |
| 3302 | + $content_norm = preg_replace( '#^https?://#', '//', $content_url ); | |
| 3303 | + if ( strpos( $url_norm, $content_norm ) !== 0 ) { | |
| 3304 | + return false; | |
| 3305 | + } | |
| 3306 | + $relative = substr( $url_norm, strlen( $content_norm ) ); | |
| 3307 | + $file = WP_CONTENT_DIR . $relative; | |
| 3308 | + return file_exists( $file ) ? $file : false; | |
| 3309 | + } | |
| 3310 | + /** | |
| 3311 | + * Return a local video URL for a given post title, if one exists. | |
| 3312 | + * Used to override missing video_url values from the remote feed. | |
| 3313 | + * | |
| 3314 | + * @param string $title Post title from the feed. | |
| 3315 | + * @return string URL or empty string. | |
| 3316 | + */ | |
| 3317 | + private function get_local_video_url( $title ) { | |
| 3318 | + $map = array( | |
| 3319 | + 'Mini Donation Widget' => site_url( '/_promo_videos/charitable_dashboard_updates_video_mini-widget.mp4' ), | |
| 3320 | + ); | |
| 3321 | + | |
| 3322 | + foreach ( $map as $keyword => $url ) { | |
| 3323 | + if ( false !== stripos( $title, $keyword ) ) { | |
| 3324 | + return $url; | |
| 3325 | + } | |
| 3326 | + } | |
| 3327 | + | |
| 3328 | + return ''; | |
| 3329 | + } | |
| 3330 | + /** | |
| 3331 | + * Return a local thumbnail URL for a given post title, if one exists. | |
| 3332 | + * | |
| 3333 | + * @param string $title Post title from the feed. | |
| 3334 | + * @return string URL or empty string. | |
| 3335 | + */ | |
| 3336 | + private function get_local_thumbnail_url( $title ) { | |
| 3337 | + $base = charitable()->get_path( 'assets', false ) . 'images/dashboard/'; | |
| 3338 | + $map = array( | |
| 3339 | + 'Mini Donation Widget' => site_url( '/_promo_videos/mini-widget-thumbnail.svg' ), | |
| 3340 | + 'Donations Feed' => site_url( '/_promo_videos/donations-feed-thumbnail.svg' ), | |
| 3341 | + 'Campaign Showcase' => $base . 'thumb-campaign-showcase.png', | |
| 3342 | + 'Campaign Updates' => $base . 'thumb-campaign-updates.svg', | |
| 3343 | + ); | |
| 3344 | + | |
| 3345 | + foreach ( $map as $keyword => $url ) { | |
| 3346 | + if ( false !== stripos( $title, $keyword ) ) { | |
| 3347 | + return $url; | |
| 3348 | + } | |
| 3349 | + } | |
| 3350 | + | |
| 3351 | + return ''; | |
| 3352 | + } | |
| 3353 | + /** | |
| 3354 | + * Return a local badge type for a given post title, if one should be overridden. | |
| 3355 | + * Values: 'pro', 'addon', or empty string. | |
| 3356 | + * | |
| 3357 | + * @param string $title Post title from the feed. | |
| 3358 | + * @return string Badge type or empty string. | |
| 3359 | + */ | |
| 3360 | + private function get_local_badge( $title ) { | |
| 3361 | + $pro_keywords = array( 'Mini Donation Widget', 'Donations Feed', 'Campaign Showcase' ); | |
| 3362 | + foreach ( $pro_keywords as $keyword ) { | |
| 3363 | + if ( false !== stripos( $title, $keyword ) ) { | |
| 3364 | + return 'pro'; | |
| 3365 | + } | |
| 3366 | + } | |
| 3367 | + | |
| 3368 | + $addon_keywords = array( 'Campaign Updates Addon', 'Campaigns Update Addon' ); | |
| 3369 | + foreach ( $addon_keywords as $keyword ) { | |
| 3370 | + if ( false !== stripos( $title, $keyword ) ) { | |
| 3371 | + return 'addon'; | |
| 3372 | + } | |
| 3373 | + } | |
| 3374 | + | |
| 3375 | + return ''; | |
| 3376 | + } | |
| 3377 | + /** | |
| 3378 | + * Fetch and return the v2 Latest Updates feed. | |
| 3379 | + * | |
| 3380 | + * @since 1.8.12 | |
| 3381 | + * @return array Array of update items, or empty array on failure. | |
| 3382 | + */ | |
| 3383 | + private function get_latest_updates() { | |
| 3384 | + $cached = get_transient( 'charitable_latest_updates_cache' ); | |
| 3385 | + if ( false !== $cached ) { | |
| 3386 | + if ( isset( $cached['version_info'] ) ) { | |
| 3387 | + $this->version_info = $cached['version_info']; | |
| 3388 | + } | |
| 3389 | + return $cached['updates'] ?? []; | |
| 3390 | + } | |
| 3391 | + | |
| 3392 | + $response = wp_remote_get( self::LATEST_UPDATES_SOURCE_URL, [ | |
| 3393 | + 'timeout' => 10, | |
| 3394 | + 'sslverify' => false, | |
| 3395 | + ] ); | |
| 3396 | + | |
| 3397 | + if ( is_wp_error( $response ) ) { | |
| 3398 | + return []; | |
| 3399 | + } | |
| 3400 | + | |
| 3401 | + $body = wp_remote_retrieve_body( $response ); | |
| 3402 | + if ( empty( $body ) ) { | |
| 3403 | + return []; | |
| 3404 | + } | |
| 3405 | + | |
| 3406 | + $decoded = json_decode( $body, true ); | |
| 3407 | + if ( json_last_error() !== JSON_ERROR_NONE || empty( $decoded['updates'] ) ) { | |
| 3408 | + return []; | |
| 3409 | + } | |
| 3410 | + | |
| 3411 | + // Store version info from meta. | |
| 3412 | + if ( isset( $decoded['meta'] ) && is_array( $decoded['meta'] ) ) { | |
| 3413 | + $this->version_info = [ | |
| 3414 | + 'current_charitable_version_lite' => ! empty( $decoded['meta']['current_charitable_version_lite'] ) ? sanitize_text_field( $decoded['meta']['current_charitable_version_lite'] ) : '', | |
| 3415 | + 'current_charitable_version_pro' => ! empty( $decoded['meta']['current_charitable_version_pro'] ) ? sanitize_text_field( $decoded['meta']['current_charitable_version_pro'] ) : '', | |
| 3416 | + ]; | |
| 3417 | + } | |
| 3418 | + | |
| 3419 | + // Sanitize each update item. | |
| 3420 | + $updates = []; | |
| 3421 | + foreach ( $decoded['updates'] as $item ) { | |
| 3422 | + if ( empty( $item['title'] ) || empty( $item['url'] ) ) { | |
| 3423 | + continue; | |
| 3424 | + } | |
| 3425 | + $updates[] = [ | |
| 3426 | + 'id' => sanitize_key( $item['id'] ?? '' ), | |
| 3427 | + 'title' => sanitize_text_field( $item['title'] ), | |
| 3428 | + 'description' => sanitize_text_field( $item['description'] ?? '' ), | |
| 3429 | + 'url' => esc_url_raw( $item['url'] ), | |
| 3430 | + 'date' => sanitize_text_field( $item['date'] ?? '' ), | |
| 3431 | + 'badge' => sanitize_key( $item['badge'] ?? '' ), | |
| 3432 | + 'is_new' => ! empty( $item['is_new'] ), | |
| 3433 | + 'image' => ! empty( $item['image'] ) ? esc_url_raw( $item['image'] ) : '', | |
| 3434 | + ]; | |
| 3435 | + } | |
| 3436 | + | |
| 3437 | + // Cache for 24 hours. | |
| 3438 | + set_transient( 'charitable_latest_updates_cache', [ | |
| 3439 | + 'updates' => $updates, | |
| 3440 | + 'version_info' => $this->version_info, | |
| 3441 | + ], DAY_IN_SECONDS ); | |
| 3442 | + | |
| 3443 | + return $updates; | |
| 3444 | + } | |
| 3445 | + /** | |
| 3446 | + * Render the v2 Latest Updates items. | |
| 3447 | + * | |
| 3448 | + * @since 1.8.12 | |
| 3449 | + */ | |
| 3450 | + private function render_latest_updates_items() { | |
| 3451 | + $updates = $this->get_latest_updates(); | |
| 3452 | + | |
| 3453 | + if ( empty( $updates ) ) { | |
| 3454 | + // Fall back to legacy blog posts. | |
| 3455 | + $this->render_blog_posts(); | |
| 3456 | + return; | |
| 3457 | + } | |
| 3458 | + | |
| 3459 | + ?> | |
| 3460 | + <div class="charitable-dashboard-v2-blog-posts" id="charitable-blog-posts-container"> | |
| 3461 | + <?php $count = 0; foreach ( $updates as $update ) : if ( $count >= 4 ) break; $count++; ?> | |
| 3462 | + <div class="charitable-dashboard-v2-blog-post-row"> | |
| 3463 | + <div class="charitable-dashboard-v2-blog-post-left"> | |
| 3464 | + <div class="charitable-dashboard-v2-blog-post-timestamp-row"> | |
| 3465 | + <?php if ( ! empty( $update['date'] ) ) : ?> | |
| 3466 | + <span class="charitable-dashboard-v2-blog-post-timestamp"><?php echo esc_html( $this->calculate_relative_time( $update['date'] ) ); ?></span> | |
| 3467 | + <?php endif; ?> | |
| 3468 | + <?php if ( ! empty( $update['is_new'] ) ) : ?> | |
| 3469 | + <span class="charitable-dashboard-v2-blog-post-badge-new"><?php esc_html_e( 'New', 'charitable' ); ?></span> | |
| 3470 | + <?php endif; ?> | |
| 3471 | + <?php if ( 'pro' === $update['badge'] ) : ?> | |
| 3472 | + <span class="charitable-dashboard-v2-blog-post-badge-pro"><?php esc_html_e( 'Pro', 'charitable' ); ?></span> | |
| 3473 | + <?php elseif ( 'addon' === $update['badge'] ) : ?> | |
| 3474 | + <span class="charitable-dashboard-v2-blog-post-badge-addon"><?php esc_html_e( 'Addon', 'charitable' ); ?></span> | |
| 3475 | + <?php endif; ?> | |
| 3476 | + </div> | |
| 3477 | + <h5 class="charitable-dashboard-v2-blog-post-title"> | |
| 3478 | + <a href="<?php echo esc_url( $update['url'] ); ?>" target="_blank" rel="noopener noreferrer"><?php echo esc_html( $update['title'] ); ?></a> | |
| 3479 | + </h5> | |
| 3480 | + <?php if ( ! empty( $update['description'] ) ) : ?> | |
| 3481 | + <p class="charitable-dashboard-v2-blog-post-desc"><?php echo esc_html( $update['description'] ); ?> <a href="<?php echo esc_url( $update['url'] ); ?>" target="_blank" rel="noopener noreferrer" class="charitable-dashboard-v2-blog-post-learn-more"><?php esc_html_e( 'Learn More', 'charitable' ); ?> →</a></p> | |
| 3482 | + <?php endif; ?> | |
| 3483 | + </div> | |
| 3484 | + <div class="charitable-dashboard-v2-blog-post-right"> | |
| 3485 | + <?php if ( ! empty( $update['image'] ) ) : ?> | |
| 3486 | + <a href="<?php echo esc_url( $update['url'] ); ?>" target="_blank" rel="noopener noreferrer" class="charitable-dashboard-v2-blog-post-thumb charitable-dashboard-v2-blog-post-thumb--bg" style="background-image:url('<?php echo esc_url( $update['image'] ); ?>');"> | |
| 3487 | + </a> | |
| 3488 | + <?php else : ?> | |
| 3489 | + <a href="<?php echo esc_url( $update['url'] ); ?>" target="_blank" rel="noopener noreferrer" class="charitable-dashboard-v2-blog-post-thumb"> | |
| 3490 | + <svg width="96" height="64" viewBox="0 0 96 64" fill="none" xmlns="http://www.w3.org/2000/svg"> | |
| 3491 | + <rect width="96" height="64" rx="6" fill="#E5E7EB"/> | |
| 3492 | + <path d="M16 46L30 29L46 39L62 21L82 33" stroke="#9CA3AF" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> | |
| 3493 | + <circle cx="30" cy="29" r="2.5" fill="#9CA3AF"/> | |
| 3494 | + <circle cx="46" cy="39" r="2.5" fill="#9CA3AF"/> | |
| 3495 | + <circle cx="62" cy="21" r="2.5" fill="#9CA3AF"/> | |
| 3496 | + </svg> | |
| 3497 | + </a> | |
| 3498 | + <?php endif; ?> | |
| 3499 | + </div> | |
| 3500 | + </div> | |
| 3501 | + <?php endforeach; ?> | |
| 3502 | + </div> | |
| 2994 | 3503 | <?php |
| 2995 | 3504 | } |
| 2996 | 3505 | |
| 2997 | 3506 | /** |