diff --git a/assets/apps/customizer-controls/src/@types/utils.d.ts b/assets/apps/customizer-controls/src/@types/utils.d.ts index 41e1756532..37b02ae15e 100644 --- a/assets/apps/customizer-controls/src/@types/utils.d.ts +++ b/assets/apps/customizer-controls/src/@types/utils.d.ts @@ -114,6 +114,19 @@ declare global { nonce: string; hideConditionalHeaderSelector: boolean; dashUpdatesMessage: string; + deal?: { + active?: boolean; + dealSlug?: string; + urgencyText?: string; + remaningTime?: string; + bannerUrl?: string; + customizerBannerUrl?: string; + linkDashboard?: string; + linkGlobal?: string; + linkCustomizer?: string; + customizerBannerAlt?: string; + bannerAlt?: string; + }; }; NeveProReactCustomize: undefined | StringObjectKeys; } diff --git a/assets/apps/customizer-controls/src/builder-upsell/Upsells.tsx b/assets/apps/customizer-controls/src/builder-upsell/Upsells.tsx index b9a44f186f..b1a1590211 100644 --- a/assets/apps/customizer-controls/src/builder-upsell/Upsells.tsx +++ b/assets/apps/customizer-controls/src/builder-upsell/Upsells.tsx @@ -11,6 +11,31 @@ const Upsells: React.FC = ({ control }) => { const { params } = control; const { title, url } = params; + if (window?.NeveReactCustomize?.deal?.active) { + return ( +
+ + { + +
+ ); + } + return (
{title && ( diff --git a/assets/apps/dashboard/src/Components/App.js b/assets/apps/dashboard/src/Components/App.js index cdce482050..1e356e1da9 100644 --- a/assets/apps/dashboard/src/Components/App.js +++ b/assets/apps/dashboard/src/Components/App.js @@ -9,6 +9,7 @@ import { fetchOptions } from '../utils/rest'; import { withDispatch, withSelect } from '@wordpress/data'; import { compose } from '@wordpress/compose'; import { useState, Fragment, useEffect } from '@wordpress/element'; +import Deal from './Deal'; const App = ({ setSettings, toast, currentTab, setTab }) => { const [loading, setLoading] = useState(true); @@ -27,6 +28,7 @@ const App = ({ setSettings, toast, currentTab, setTab }) => {
+ {'starter-sites' !== currentTab && }
diff --git a/assets/apps/dashboard/src/Components/Deal.js b/assets/apps/dashboard/src/Components/Deal.js new file mode 100644 index 0000000000..24123ba08d --- /dev/null +++ b/assets/apps/dashboard/src/Components/Deal.js @@ -0,0 +1,25 @@ +/* global neveDash */ + +const Deal = () => { + if (!Boolean(window.neveDash?.deal?.active)) { + return <>; + } + + return ( +
+ + {neveDash?.deal?.bannerAlt} +
{neveDash?.deal?.urgencyText}
+
+
+ ); +}; + +export default Deal; diff --git a/assets/apps/dashboard/src/scss/content/_start.scss b/assets/apps/dashboard/src/scss/content/_start.scss index 02c6955db0..4596eb26f3 100644 --- a/assets/apps/dashboard/src/scss/content/_start.scss +++ b/assets/apps/dashboard/src/scss/content/_start.scss @@ -48,3 +48,46 @@ } } } + +.nv-deal { + margin-bottom: 10px; + display: flex; + + a { + position: relative; + width: 100%; + } + + img { + width: 100%; + } + + .nv-urgency { + position: absolute; + + top: 10%; + left: 2.7%; + + color: #FFF; + font-family: -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", sans-serif; + font-size: 14px; + font-style: normal; + font-weight: 700; + line-height: normal; + letter-spacing: 0.3px; + text-transform: uppercase; + } +} + +@media(max-width: 480px) { + .nv-deal .nv-urgency { + font-size: 7px; + } +} + +@media (min-width: 481px) and (max-width: 1024px) { + .nv-deal .nv-urgency { + font-size: 10px; + } +} + diff --git a/assets/img/dashboard/black-friday-banner.png b/assets/img/dashboard/black-friday-banner.png new file mode 100644 index 0000000000..173ec596f8 Binary files /dev/null and b/assets/img/dashboard/black-friday-banner.png differ diff --git a/assets/img/dashboard/black-friday-customizer-banner.png b/assets/img/dashboard/black-friday-customizer-banner.png new file mode 100644 index 0000000000..9ff77fb156 Binary files /dev/null and b/assets/img/dashboard/black-friday-customizer-banner.png differ diff --git a/globals/google-fonts.php b/globals/google-fonts.php index 0580c9b446..61b18b83f4 100644 --- a/globals/google-fonts.php +++ b/globals/google-fonts.php @@ -1,6 +1,6 @@ array( '300', '400', '500', '600', '700', '800', '900', '300italic', '400italic', '500italic', '600italic', '700italic', '800italic', '900italic',), 'Playfair Display' => array( '400', '500', '600', '700', '800', '900', '400italic', '500italic', '600italic', '700italic', '800italic', '900italic',), 'Playfair Display SC' => array( '400', '700', '900', '400italic', '700italic', '900italic',), + 'Playpen Sans' => array( '100', '200', '300', '400', '500', '600', '700', '800',), 'Plus Jakarta Sans' => array( '200', '300', '400', '500', '600', '700', '800', '200italic', '300italic', '400italic', '500italic', '600italic', '700italic', '800italic',), 'Podkova' => array( '400', '500', '600', '700', '800',), 'Poiret One' => array( '400',), @@ -1290,7 +1291,7 @@ 'Risque' => array( '400',), 'Road Rage' => array( '400',), 'Roboto' => array( '100', '300', '400', '500', '700', '900', '100italic', '300italic', '400italic', '500italic', '700italic', '900italic',), - 'Roboto Condensed' => array( '300', '400', '700', '300italic', '400italic', '700italic',), + 'Roboto Condensed' => array( '100', '200', '300', '400', '500', '600', '700', '800', '900', '100italic', '200italic', '300italic', '400italic', '500italic', '600italic', '700italic', '800italic', '900italic',), 'Roboto Flex' => array( '400',), 'Roboto Mono' => array( '100', '200', '300', '400', '500', '600', '700', '100italic', '200italic', '300italic', '400italic', '500italic', '600italic', '700italic',), 'Roboto Serif' => array( '100', '200', '300', '400', '500', '600', '700', '800', '900', '100italic', '200italic', '300italic', '400italic', '500italic', '600italic', '700italic', '800italic', '900italic',), @@ -1422,6 +1423,7 @@ 'Sofia Sans Semi Condensed' => array( '100', '200', '300', '400', '500', '600', '700', '800', '900', '100italic', '200italic', '300italic', '400italic', '500italic', '600italic', '700italic', '800italic', '900italic',), 'Solitreo' => array( '400',), 'Solway' => array( '300', '400', '500', '700', '800',), + 'Sometype Mono' => array( '400', '500', '600', '700', '400italic', '500italic', '600italic', '700italic',), 'Song Myung' => array( '400',), 'Sono' => array( '200', '300', '400', '500', '600', '700', '800',), 'Sonsie One' => array( '400',), diff --git a/inc/admin/dashboard/main.php b/inc/admin/dashboard/main.php index bc9cf741ab..2820aec000 100755 --- a/inc/admin/dashboard/main.php +++ b/inc/admin/dashboard/main.php @@ -7,6 +7,7 @@ namespace Neve\Admin\Dashboard; +use Neve\Core\Limited_Offers; use Neve\Core\Theme_Info; /** * Class Main @@ -315,6 +316,9 @@ public function enqueue() { * @return array */ private function get_localization() { + + $offer = new Limited_Offers(); + $old_about_config = apply_filters( 'ti_about_config_filter', [ 'useful_plugins' => true ] ); $theme_name = apply_filters( 'ti_wl_theme_name', $this->theme_args['name'] ); $plugin_name = apply_filters( 'ti_wl_plugin_name', 'Neve Pro' ); @@ -376,6 +380,7 @@ private function get_localization() { 'getPluginStateBaseURL' => esc_url( rest_url( '/nv/v1/dashboard/plugin-state/' ) ), 'canInstallPlugins' => current_user_can( 'install_plugins' ), 'canActivatePlugins' => current_user_can( 'activate_plugins' ), + 'deal' => ! defined( 'NEVE_PRO_VERSION' ) ? $offer->get_localized_data() : array(), ]; if ( defined( 'NEVE_PRO_PATH' ) ) { diff --git a/inc/core/admin.php b/inc/core/admin.php index 4f0338d0db..225ef585c7 100644 --- a/inc/core/admin.php +++ b/inc/core/admin.php @@ -85,6 +85,14 @@ function () { add_action( 'rest_api_init', [ $this, 'register_rest_routes' ] ); add_filter( 'neve_pro_react_controls_localization', [ $this, 'adapt_conditional_headers' ] ); + + + if ( ! defined( 'NEVE_PRO_VERSION' ) ) { + $offer = new Limited_Offers(); + if ( $offer->can_show_dashboard_banner() && $offer->is_active() ) { + $offer->load_dashboard_hooks(); + } + } } /** diff --git a/inc/core/limited_offers.php b/inc/core/limited_offers.php new file mode 100644 index 0000000000..409849aec1 --- /dev/null +++ b/inc/core/limited_offers.php @@ -0,0 +1,350 @@ + + * Created on: 17/10/2023 + * + * @package Neve\Core + */ + +namespace Neve\Core; + +use DateTime; +use DateTimeZone; +use Exception; + +/** + * Class LimitedOffers + */ +class Limited_Offers { + + /** + * Active deal. + * + * @var string + */ + private $active = ''; + + /** + * The key for WP Options to disable the dashboard notification. + * + * @var string + */ + public $wp_option_dismiss_notification_key_base = 'dismiss_themeisle_notice_event_'; + + /** + * Offer Links + * + * @var array + */ + public $offer_metadata = array(); + + /** + * Timeline for the offers. + * + * @var array[] + */ + public $timelines = array( + 'bf' => array( + 'start' => '2023-11-20 00:00:00', + 'end' => '2023-11-27 23:59:00', + ), + ); + + /** + * LimitedOffers constructor. + */ + public function __construct() { + try { + if ( $this->is_deal_active( 'bf' ) ) { + $this->activate_bff(); + } + } catch ( Exception $e ) { + if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { + error_log( $e->getMessage() ); // phpcs:ignore + } + } + } + + /** + * Load hooks for the dashboard. + * + * @return void + */ + public function load_dashboard_hooks() { + add_filter( 'themeisle_products_deal_priority', array( $this, 'add_priority' ) ); + add_action( 'admin_notices', array( $this, 'render_notice' ) ); + add_action( 'wp_ajax_dismiss_themeisle_event_notice_neve', array( $this, 'disable_notification_ajax' ) ); + } + + /** + * Check if we have an active deal. + * + * @return bool True if the deal is active. + */ + public function is_active() { + return ! empty( $this->active ); + } + + /** + * Activate the Black Friday deal. + * + * @return void + */ + public function activate_bff() { + $this->active = 'bf'; + + $this->offer_metadata = array( + 'bannerUrl' => get_template_directory_uri() . '/assets/img/dashboard/black-friday-banner.png', + 'bannerAlt' => 'Neve Black Friday Sale', + 'customizerBannerUrl' => get_template_directory_uri() . '/assets/img/dashboard/black-friday-customizer-banner.png', + 'customizerBannerAlt' => 'Neve Black Friday Sale', + 'linkDashboard' => tsdk_utmify( 'https://themeisle.com/themes/neve/blackfriday/', 'blackfridayltd23', 'dashboard' ), + 'linkGlobal' => tsdk_utmify( 'https://themeisle.com/themes/neve/blackfriday/', 'blackfridayltd23', 'globalnotice' ), + 'linkCustomizer' => tsdk_utmify( 'https://themeisle.com/themes/neve/upgrade', 'blackfriday23', 'customizer' ), + ); + } + + /** + * Get the slug of the active deal. + * + * @return string Active deal. + */ + public function get_active_deal() { + return $this->active; + } + + /** + * Check if the deal is active with the given slug. + * + * @param string $slug Slug of the deal. + * + * @throws Exception When date is invalid. + */ + public function is_deal_active( $slug ) { + + if ( empty( $slug ) || ! array_key_exists( $slug, $this->timelines ) ) { + return false; + } + + return $this->check_date_range( $this->timelines[ $slug ]['start'], $this->timelines[ $slug ]['end'] ); + } + + /** + * Get the remaining time for the deal in a human readable format. + * + * @param string $slug Slug of the deal. + * @return string Remaining time for the deal. + */ + public function get_remaining_time_for_deal( $slug ) { + if ( empty( $slug ) || ! array_key_exists( $slug, $this->timelines ) ) { + return ''; + } + + try { + $end_date = new DateTime( $this->timelines[ $slug ]['end'], new DateTimeZone( 'GMT' ) ); + $current_date = new DateTime( 'now', new DateTimeZone( 'GMT' ) ); + $diff = $end_date->diff( $current_date ); + + if ( $diff->days > 0 ) { + return $diff->days === 1 ? $diff->format( '%a day' ) : $diff->format( '%a days' ); + } + + if ( $diff->h > 0 ) { + return $diff->h === 1 ? $diff->format( '%h hour' ) : $diff->format( '%h hours' ); + } + + if ( $diff->i > 0 ) { + return $diff->i === 1 ? $diff->format( '%i minute' ) : $diff->format( '%i minutes' ); + } + + return $diff->s === 1 ? $diff->format( '%s second' ) : $diff->format( '%s seconds' ); + } catch ( Exception $e ) { + if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { + error_log( $e->getMessage() ); // phpcs:ignore + } + } + + return ''; + } + + /** + * Check if the current date is in the range of the offer. + * + * @param string $start Start date. + * @param string $end End date. + * + * @throws Exception When date is invalid. + */ + public function check_date_range( $start, $end ) { + + $start_date = new DateTime( $start, new DateTimeZone( 'GMT' ) ); + $end_date = new DateTime( $end, new DateTimeZone( 'GMT' ) ); + $current_date = new DateTime( 'now', new DateTimeZone( 'GMT' ) ); + + return $start_date <= $current_date && $current_date <= $end_date; + } + + /** + * Get the localized data for the plugin. + * + * @return array Localized data. + */ + public function get_localized_data() { + return array_merge( + array( + 'active' => $this->is_active(), + 'dealSlug' => $this->get_active_deal(), + 'remainingTime' => $this->get_remaining_time_for_deal( $this->get_active_deal() ), + 'urgencyText' => 'Hurry Up! Only ' . $this->get_remaining_time_for_deal( $this->get_active_deal() ) . ' left', + ), + $this->offer_metadata + ); + } + + /** + * Disable the notification via ajax. + * + * @return void + */ + public function disable_notification_ajax() { + if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_key( $_POST['nonce'] ), 'dismiss_themeisle_event_notice_neve' ) ) { + wp_die( 'Invalid nonce! Refresh the page and try again.' ); + } + + // We record the time and the plugin of the dismissed notification. + update_option( $this->wp_option_dismiss_notification_key_base . $this->active, 'neve_' . $this->active . '_' . current_time( 'Y_m_d' ) ); + wp_die( 'success' ); + } + + /** + * Render the dashboard banner. + * + * @return void + */ + public function render_notice() { + + if ( ! $this->has_priority() ) { + return; + } + + $message = 'Neve Black Friday Sale - Save big with a Lifetime License of Neve Agency Plan. Only 100 licenses, for a limited time!'; + + ?> + +
+
+ + + + + + + + Learn more + + + +
+ + wp_option_dismiss_notification_key_base . $this->active, false ); + } + + /** + * Add product priority to the filter. + * + * @param array $products Registered products. + * @return array Array enhanced with Neve priority. + */ + public function add_priority( $products ) { + $products['neve'] = 0; + return $products; + } + + /** + * Check if the current product has priority. + * Use this for conditional rendering if you want to show the banner only for one product. + * + * @return bool True if the current product has priority. + */ + public function has_priority() { + $products = apply_filters( 'themeisle_products_deal_priority', [] ); + + if ( empty( $products ) ) { + return true; + } + + $highest_priority = array_search( min( $products ), $products ); + return 'neve' === $highest_priority; + } +} diff --git a/inc/customizer/loader.php b/inc/customizer/loader.php index e5b7b9624d..bb3d10a91d 100644 --- a/inc/customizer/loader.php +++ b/inc/customizer/loader.php @@ -10,6 +10,7 @@ use HFG\Core\Components\Utility\SearchIconButton; use Neve\Core\Factory; +use Neve\Core\Limited_Offers; use Neve\Core\Settings\Config; use Neve\Customizer\Options\Colors_Background; @@ -112,6 +113,7 @@ public function enqueue_customizer_controls() { true ); + $offer = new Limited_Offers(); $bundle_path = get_template_directory_uri() . '/assets/apps/customizer-controls/build/'; $dependencies = ( include get_template_directory() . '/assets/apps/customizer-controls/build/controls.asset.php' ); wp_register_script( 'react-controls', $bundle_path . 'controls.js', $dependencies['dependencies'], $dependencies['version'], true ); @@ -150,6 +152,7 @@ public function enqueue_customizer_controls() { 'customIconKey' => SearchIconButton::CUSTOM_ICON, ], ], + 'deal' => ! defined( 'NEVE_PRO_VERSION' ) ? $offer->get_localized_data() : array(), ) ) );