PluginProbe
Ultimate Cursor – Interactive and Animated Custom Cursor and Background Effects Toolkit / trunk
Ultimate Cursor – Interactive and Animated Custom Cursor and Background Effects Toolkit vtrunk
2.4.1 2.4.0 2.3.3 2.3.2 2.3.1 2.3.0 2.2.3 2.2.2 2.2.1 trunk 1.0.0 1.1.0 1.2.0 1.2.1 1.2.2 1.2.3 1.3.0 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 All 56 releases
ultimate-cursor / classes / class-dashboard-widget.php

class-dashboard-widget.php in Ultimate Cursor – Interactive and Animated Custom Cursor and Background Effects Toolkit trunk, at classes/class-dashboard-widget.php

1,077 lines 31.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * Dashboard Widget & Admin Promotional Notice for Ultimate Cursor Plugin.
5 *
6 * Displays a seasonal upgrade CTA on the WP Dashboard (widget) and on other
7 * admin pages (admin notice) when the user does not have a Pro license.
8 * Both widget and notice can be dismissed for 30 days, after which they
9 * automatically re-appear.
10 *
11 * @package ultimate-cursor
12 */
13
14 if ( ! defined( 'ABSPATH' ) ) {
15 exit;
16 }
17
18 /**
19 * Ultimate Cursor Dashboard Widget class.
20 */
21 class Ultimate_Cursor_Dashboard_Widget {
22
23 /** @var self|null Singleton instance. */
24 private static $instance = null;
25
26 /** @var string Pricing page URL. */
27 const PRICING_URL = 'https://wpxero.com/plugins/ultimate-cursor/pricing';
28
29 /** @var int Number of days before a dismissed promo re-appears. */
30 const DISMISS_DAYS = 30;
31
32 /**
33 * Get singleton instance.
34 */
35 public static function instance() {
36 if ( is_null( self::$instance ) ) {
37 self::$instance = new self();
38 }
39 return self::$instance;
40 }
41
42 /**
43 * Constructor — register hooks.
44 */
45 private function __construct() {
46 // Never show promos to Pro users.
47 if ( class_exists( 'UltimateCursor' ) && UltimateCursor::is_premium_active() ) {
48 return;
49 }
50
51 add_action( 'wp_dashboard_setup', array( $this, 'add_dashboard_widget' ) );
52 add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_assets' ) );
53 add_action( 'admin_notices', array( $this, 'show_promotional_notice' ) );
54 add_action( 'wp_ajax_uc_dismiss_promo_widget', array( $this, 'ajax_dismiss_widget' ) );
55 add_action( 'wp_ajax_uc_dismiss_promo_notice', array( $this, 'ajax_dismiss_notice' ) );
56 }
57
58 /*
59 ------------------------------------------------------------------
60 * Campaign configuration (single source of truth)
61 * ----------------------------------------------------------------*/
62
63 /**
64 * Get the active campaign configuration or null when outside promo windows.
65 *
66 * @return array|null {
67 * @type string $key Internal key.
68 * @type int $discount Discount percentage.
69 * @type string $end_date Y-m-d sale end date.
70 * @type string $coupon Coupon code.
71 * @type string $widget_title Dashboard widget title.
72 * @type string $notice_title Admin notice headline.
73 * @type string $description Short CTA text.
74 * @type string $button_text CTA button label.
75 * @type string $accent Primary accent hex colour.
76 * @type string $accent_secondary Secondary accent hex colour.
77 * @type string $gradient CSS background gradient.
78 * @type string $icon Campaign icon key (see get_icon_svg()).
79 * @type array $features Feature highlight list.
80 * }
81 */
82 private function get_campaign() {
83 // Allow testing via URL parameter.
84 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only preview toggle gated by capability check, no data is processed.
85 $test_halloween = isset( $_GET['halloween'] ) && current_user_can( 'manage_options' );
86 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only preview toggle gated by capability check, no data is processed.
87 $test_black_friday = isset( $_GET['black_friday'] ) && current_user_can( 'manage_options' );
88 $now = current_time( 'Y-m-d' );
89 $year = current_time( 'Y' );
90
91 $features = array(
92 __( 'Multiple Cursor & Background Effects Configurations', 'ultimate-cursor' ),
93 __( 'Element-Specific Cursors & Background Effects', 'ultimate-cursor' ),
94 __( 'Custom Cursor & Background Effects for Specific Pages', 'ultimate-cursor' ),
95 __( 'Advanced Animation Options', 'ultimate-cursor' ),
96 __( 'Priority Support', 'ultimate-cursor' ),
97 );
98
99 // Halloween: October 15 – October 31.
100 if ( $test_halloween || ( $now >= "$year-10-15" && $now <= "$year-10-31" ) ) {
101 return array(
102 'key' => 'halloween',
103 'discount' => 25,
104 'end_date' => "$year-10-31",
105 'coupon' => 'HALLOWEEN',
106 'widget_title' => __( 'Ultimate Cursor — Halloween Sale', 'ultimate-cursor' ),
107 'notice_title' => __( 'Halloween Sale — Ultimate Cursor Pro', 'ultimate-cursor' ),
108 'description' => __( 'Unlock spooky-good premium cursor effects, advanced customisation & priority support this Halloween!', 'ultimate-cursor' ),
109 'button_text' => __( 'Grab 25% OFF', 'ultimate-cursor' ),
110 'accent' => '#ff6600',
111 'accent_secondary' => '#a855f7',
112 'gradient' => 'linear-gradient(135deg, #1a0a2e 0%, #2d1150 35%, #4c1d95 70%, #7c3aed 100%)',
113 'icon' => 'pumpkin',
114 'features' => $features,
115 );
116 }
117
118 // Black Friday / Cyber Monday: November 1 – December 5.
119 if ( $test_black_friday || ( $now >= "$year-11-01" && $now <= "$year-12-05" ) ) {
120 return array(
121 'key' => 'black_friday',
122 'discount' => 25,
123 'end_date' => "$year-12-05",
124 'coupon' => 'BFCM',
125 'widget_title' => __( 'Ultimate Cursor — Black Friday Sale', 'ultimate-cursor' ),
126 'notice_title' => __( 'Black Friday Sale — Ultimate Cursor Pro', 'ultimate-cursor' ),
127 'description' => __( 'The biggest sale of the year! Unlock 10+ premium cursor effects, advanced customisation & priority support.', 'ultimate-cursor' ),
128 'button_text' => __( 'Grab 25% OFF', 'ultimate-cursor' ),
129 'accent' => '#f43f5e',
130 'accent_secondary' => '#ec4899',
131 'gradient' => 'linear-gradient(135deg, #0f172a 0%, #1e1b4b 40%, #581c87 75%, #7c3aed 100%)',
132 'icon' => 'flame',
133 'features' => $features,
134 );
135 }
136
137 // Regular promo windows: 20th of current month to 10th of next month.
138 $day = (int) current_time( 'j' );
139 $month = (int) current_time( 'n' );
140
141 if ( $day >= 20 || $day <= 10 ) {
142 if ( $day >= 20 ) {
143 $next_month = $month === 12 ? 1 : $month + 1;
144 $next_year = $month === 12 ? (int) $year + 1 : (int) $year;
145 $end_date = sprintf( '%04d-%02d-10', $next_year, $next_month );
146 } else {
147 $end_date = sprintf( '%s-%02d-10', $year, $month );
148 }
149
150 return array(
151 'key' => 'regular',
152 'discount' => 20,
153 'end_date' => $end_date,
154 'coupon' => 'UNLOCKPRO',
155 'widget_title' => __( 'Ultimate Cursor — Limited Offer', 'ultimate-cursor' ),
156 'notice_title' => __( 'Limited Time Offer — Ultimate Cursor Pro', 'ultimate-cursor' ),
157 'description' => __( 'Upgrade to Ultimate Cursor Pro — premium effects, advanced customisation & priority support.', 'ultimate-cursor' ),
158 'button_text' => __( 'Get Pro — 20% OFF', 'ultimate-cursor' ),
159 'accent' => '#6366f1',
160 'accent_secondary' => '#818cf8',
161 'gradient' => 'linear-gradient(135deg, #0f172a 0%, #1e293b 50%, #334155 100%)',
162 'icon' => 'rocket',
163 'features' => $features,
164 );
165 }
166
167 return null;
168 }
169
170 /*
171 ------------------------------------------------------------------
172 * 30-day dismiss helpers (server-side via user meta)
173 * ----------------------------------------------------------------*/
174
175 /**
176 * Inline SVG for a campaign icon key.
177 *
178 * Emojis render inconsistently (or as tofu boxes) on some OS/browser
179 * combinations, so campaign icons ship as SVGs drawn in the campaign
180 * accent color instead.
181 *
182 * @param string $key Icon key: 'pumpkin', 'flame', or 'rocket'.
183 * @return string SVG markup (safe, static; echo through wp_kses with
184 * self::get_svg_kses_allowed()).
185 */
186 private function get_icon_svg( $key ) {
187 $icons = array(
188 'pumpkin' => '<svg width="1em" height="1em" viewBox="0 0 24 24" fill="var(--uc-accent)" aria-hidden="true" focusable="false"><path d="M13.5 4.5c.8-1.4 2-2.2 3.5-2.3l.6 1.9c-1.1.1-2 .6-2.6 1.5 3.9.4 7 3.5 7 8 0 4.7-3.4 8.4-7.6 8.4-.9 0-1.7-.2-2.4-.5-.7.3-1.5.5-2.4.5C5.4 22 2 18.3 2 13.6c0-4.4 3-7.5 6.8-8 1.3-.7 2.9-1.1 4.7-1.1zM12 7.6c-.9 0-1.7 2.7-1.7 6s.8 6 1.7 6 1.7-2.7 1.7-6-.8-6-1.7-6z"/></svg>',
189 'flame' => '<svg width="1em" height="1em" viewBox="0 0 24 24" fill="var(--uc-accent)" aria-hidden="true" focusable="false"><path d="M12 2c.5 3.5-2.9 5.4-2.9 9a3.4 3.4 0 006.8.3c.7.9 1.1 2 1.1 3.2A5.5 5.5 0 0112 20a5.5 5.5 0 01-5.5-5.5c0-2.3 1.1-3.9 2.2-5.5C9.8 7.4 11.5 5.2 12 2zm5.8 7.2c1.4 1.6 2.2 3.5 2.2 5.3A8 8 0 0112 22a8 8 0 01-8-7.5c0-.2 0-.4 0-.6A7.6 7.6 0 0012 22a7.6 7.6 0 007.6-7.6c0-1.9-.7-3.7-1.8-5.2z"/></svg>',
190 'rocket' => '<svg width="1em" height="1em" viewBox="0 0 24 24" fill="var(--uc-accent)" aria-hidden="true" focusable="false"><path d="M12 2c3 1.8 5 5.5 5 9.6l2.2 3.3-3.2-.5c-.9 1.4-2.3 2.5-4 3.1-1.7-.6-3.1-1.7-4-3.1l-3.2.5L7 11.6C7 7.5 9 3.8 12 2zm0 5.2a1.9 1.9 0 100 3.8 1.9 1.9 0 000-3.8zM8.5 18.7c-.3 1.2-1.2 2.3-2.7 3.3.4-1.7.8-2.9 1.4-3.8.4.2.8.4 1.3.5zm7 0c.5-.1.9-.3 1.3-.5.6.9 1 2.1 1.4 3.8-1.5-1-2.4-2.1-2.7-3.3z"/></svg>',
191 );
192 return isset( $icons[ $key ] ) ? $icons[ $key ] : '';
193 }
194
195 /**
196 * Allowed tags/attributes for echoing the campaign icon SVGs.
197 *
198 * @return array wp_kses allowed-HTML array.
199 */
200 private function get_svg_kses_allowed() {
201 return array(
202 'svg' => array(
203 'width' => true,
204 'height' => true,
205 'viewbox' => true,
206 'fill' => true,
207 'aria-hidden' => true,
208 'focusable' => true,
209 ),
210 'path' => array(
211 'd' => true,
212 'fill' => true,
213 ),
214 );
215 }
216
217 /**
218 * Check if a promo type was dismissed within the last 30 days.
219 *
220 * @param string $type 'widget' or 'notice'.
221 * @return bool
222 */
223 private function is_dismissed( $type ) {
224 $meta_key = 'uc_dismissed_promo_' . $type;
225 $dismissed = get_user_meta( get_current_user_id(), $meta_key, true );
226
227 if ( empty( $dismissed ) ) {
228 return false;
229 }
230
231 $dismissed_time = (int) $dismissed;
232 $elapsed_days = ( time() - $dismissed_time ) / DAY_IN_SECONDS;
233
234 if ( $elapsed_days >= self::DISMISS_DAYS ) {
235 // Expired — remove stale meta and allow display.
236 delete_user_meta( get_current_user_id(), $meta_key );
237 return false;
238 }
239
240 return true;
241 }
242
243 /**
244 * Record a dismissal timestamp for a promo type.
245 *
246 * @param string $type 'widget' or 'notice'.
247 */
248 private function dismiss( $type ) {
249 $meta_key = 'uc_dismissed_promo_' . $type;
250 update_user_meta( get_current_user_id(), $meta_key, time() );
251 }
252
253 /*
254 ------------------------------------------------------------------
255 * Dashboard Widget
256 * ----------------------------------------------------------------*/
257
258 /**
259 * Register the dashboard widget.
260 */
261 public function add_dashboard_widget() {
262 if ( ! current_user_can( 'manage_options' ) ) {
263 return;
264 }
265
266 $campaign = $this->get_campaign();
267 if ( ! $campaign ) {
268 return;
269 }
270
271 if ( $this->is_dismissed( 'widget' ) ) {
272 return;
273 }
274
275 wp_add_dashboard_widget(
276 'ultimate_cursor_promo_widget',
277 $campaign['widget_title'],
278 array( $this, 'render_dashboard_widget' ),
279 null,
280 null,
281 'column4',
282 'high'
283 );
284 }
285
286 /**
287 * Render the dashboard widget body.
288 */
289 public function render_dashboard_widget() {
290 $c = $this->get_campaign();
291 if ( ! $c ) {
292 return;
293 }
294 $nonce = wp_create_nonce( 'uc_dismiss_promo_widget' );
295 $campaign_class = 'uc-campaign-' . $c['key'];
296 ?>
297 <div class="uc-promo-widget <?php echo esc_attr( $campaign_class ); ?>"
298 style="--uc-accent:<?php echo esc_attr( $c['accent'] ); ?>;--uc-accent-secondary:<?php echo esc_attr( $c['accent_secondary'] ); ?>;background:<?php echo esc_attr( $c['gradient'] ); ?>">
299
300 <div class="uc-pw-glow"></div>
301
302 <button type="button" class="uc-pw-dismiss" data-nonce="<?php echo esc_attr( $nonce ); ?>" title="<?php esc_attr_e( 'Dismiss for 30 days', 'ultimate-cursor' ); ?>">
303 <svg width="14" height="14" viewBox="0 0 14 14" fill="none">
304 <path d="M1 1l12 12M13 1L1 13" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
305 </svg>
306 </button>
307
308 <div class="uc-pw-header">
309 <span class="uc-pw-icon"><?php echo wp_kses( $this->get_icon_svg( $c['icon'] ), $this->get_svg_kses_allowed() ); ?></span>
310 <span class="uc-pw-badge"><?php echo esc_html( $c['discount'] ); ?>% OFF</span>
311 </div>
312
313 <p class="uc-pw-desc"><?php echo esc_html( $c['description'] ); ?></p>
314
315 <ul class="uc-pw-features">
316 <?php foreach ( $c['features'] as $feature ) : ?>
317 <li>
318 <svg width="14" height="14" viewBox="0 0 14 14" fill="none">
319 <path d="M2.5 7l3 3 6-6" stroke="var(--uc-accent)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
320 </svg>
321 <?php echo esc_html( $feature ); ?>
322 </li>
323 <?php endforeach; ?>
324 </ul>
325
326 <div class="uc-pw-countdown" data-end="<?php echo esc_attr( $c['end_date'] ); ?>">
327 <svg width="14" height="14" viewBox="0 0 14 14" fill="none">
328 <circle cx="7" cy="7" r="6" stroke="#94a3b8" stroke-width="1.2" />
329 <path d="M7 4v3.5l2.5 1.5" stroke="#94a3b8" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round" />
330 </svg>
331 <span class="uc-pw-cd-label"><?php esc_html_e( 'Ends in:', 'ultimate-cursor' ); ?></span>
332 <span class="uc-pw-cd-value" data-role="countdown">--</span>
333 </div>
334
335 <div class="uc-pw-coupon">
336 <span class="uc-pw-coupon-label"><?php esc_html_e( 'Use coupon:', 'ultimate-cursor' ); ?></span>
337 <button type="button" class="uc-pw-coupon-code" data-code="<?php echo esc_attr( $c['coupon'] ); ?>">
338 <span class="uc-pw-code-text"><?php echo esc_html( $c['coupon'] ); ?></span>
339 <span class="uc-pw-code-copied"><?php esc_html_e( 'Copied!', 'ultimate-cursor' ); ?></span>
340 <svg class="uc-pw-copy-icon" width="12" height="12" viewBox="0 0 12 12" fill="none">
341 <rect x="4" y="4" width="7" height="7" rx="1.5" stroke="currentColor" stroke-width="1.2" />
342 <path d="M8 4V2.5A1.5 1.5 0 006.5 1h-4A1.5 1.5 0 001 2.5v4A1.5 1.5 0 002.5 8H4" stroke="currentColor" stroke-width="1.2" />
343 </svg>
344 </button>
345 </div>
346
347 <a href="<?php echo esc_url( self::PRICING_URL ); ?>" class="uc-pw-cta" target="_blank" rel="noopener">
348 <?php echo esc_html( $c['button_text'] ); ?>
349 <svg width="14" height="14" viewBox="0 0 14 14" fill="none">
350 <path d="M3 7h8m0 0L8 4m3 3L8 10" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
351 </svg>
352 </a>
353 </div>
354 <?php
355 }
356
357 /*
358 ------------------------------------------------------------------
359 * Admin Notice (non-dashboard pages)
360 * ----------------------------------------------------------------*/
361
362 /**
363 * Show a slim promotional admin notice.
364 */
365 public function show_promotional_notice() {
366 if ( ! current_user_can( 'manage_options' ) ) {
367 return;
368 }
369
370 // Don't show on dashboard — the widget is already there.
371 global $pagenow;
372 if ( $pagenow === 'index.php' ) {
373 return;
374 }
375
376 $c = $this->get_campaign();
377 if ( ! $c ) {
378 return;
379 }
380
381 if ( $this->is_dismissed( 'notice' ) ) {
382 return;
383 }
384
385 $notice_id = 'uc-promo-notice-' . $c['key'] . '-' . current_time( 'Y' );
386 $nonce = wp_create_nonce( 'uc_dismiss_promo_notice' );
387 $campaign_class = 'uc-campaign-' . $c['key'];
388 ?>
389 <div id="<?php echo esc_attr( $notice_id ); ?>"
390 class="notice uc-promo-notice <?php echo esc_attr( $campaign_class ); ?>"
391 style="--uc-accent:<?php echo esc_attr( $c['accent'] ); ?>;--uc-accent-secondary:<?php echo esc_attr( $c['accent_secondary'] ); ?>;background:<?php echo esc_attr( $c['gradient'] ); ?>"
392 data-campaign="<?php echo esc_attr( $c['key'] ); ?>"
393 data-nonce="<?php echo esc_attr( $nonce ); ?>">
394
395 <div class="uc-pn-glow"></div>
396
397 <div class="uc-pn-inner">
398 <div class="uc-pn-badge-wrap">
399 <span class="uc-pn-icon"><?php echo wp_kses( $this->get_icon_svg( $c['icon'] ), $this->get_svg_kses_allowed() ); ?></span>
400 <span class="uc-pn-discount"><?php echo esc_html( $c['discount'] ); ?>% OFF</span>
401 </div>
402
403 <div class="uc-pn-content">
404 <strong class="uc-pn-title"><?php echo esc_html( $c['notice_title'] ); ?></strong>
405 <span class="uc-pn-desc"><?php echo esc_html( $c['description'] ); ?></span>
406 </div>
407
408 <div class="uc-pn-actions">
409 <span class="uc-pn-timer" data-end="<?php echo esc_attr( $c['end_date'] ); ?>">
410 <svg width="12" height="12" viewBox="0 0 14 14" fill="none">
411 <circle cx="7" cy="7" r="6" stroke="currentColor" stroke-width="1.2" />
412 <path d="M7 4v3.5l2.5 1.5" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round" />
413 </svg>
414 <strong data-role="countdown">--</strong>
415 </span>
416
417 <a href="<?php echo esc_url( self::PRICING_URL ); ?>" class="uc-pn-btn" target="_blank" rel="noopener">
418 <?php echo esc_html( $c['button_text'] ); ?>
419 <svg width="12" height="12" viewBox="0 0 14 14" fill="none">
420 <path d="M3 7h8m0 0L8 4m3 3L8 10" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
421 </svg>
422 </a>
423 </div>
424
425 <button type="button" class="uc-pn-dismiss" title="<?php esc_attr_e( 'Dismiss for 30 days', 'ultimate-cursor' ); ?>">
426 <svg width="12" height="12" viewBox="0 0 14 14" fill="none">
427 <path d="M1 1l12 12M13 1L1 13" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
428 </svg>
429 </button>
430 </div>
431 </div>
432 <?php
433 }
434
435 /*
436 ------------------------------------------------------------------
437 * AJAX dismiss handlers (30-day server-side persistence)
438 * ----------------------------------------------------------------*/
439
440 /**
441 * Persist widget dismissal for 30 days.
442 */
443 public function ajax_dismiss_widget() {
444 check_ajax_referer( 'uc_dismiss_promo_widget', 'nonce' );
445
446 if ( ! current_user_can( 'manage_options' ) ) {
447 wp_send_json_error( 'Forbidden', 403 );
448 }
449
450 $this->dismiss( 'widget' );
451 wp_send_json_success();
452 }
453
454 /**
455 * Persist notice dismissal for 30 days.
456 */
457 public function ajax_dismiss_notice() {
458 check_ajax_referer( 'uc_dismiss_promo_notice', 'nonce' );
459
460 if ( ! current_user_can( 'manage_options' ) ) {
461 wp_send_json_error( 'Forbidden', 403 );
462 }
463
464 $this->dismiss( 'notice' );
465 wp_send_json_success();
466 }
467
468 /*
469 ------------------------------------------------------------------
470 * Assets (CSS + JS)
471 * ----------------------------------------------------------------*/
472
473 /**
474 * Enqueue inline styles and footer scripts on relevant admin pages.
475 */
476 public function enqueue_assets( $hook ) {
477 if ( ! current_user_can( 'manage_options' ) ) {
478 return;
479 }
480
481 $campaign = $this->get_campaign();
482 if ( ! $campaign ) {
483 return;
484 }
485
486 // Attach inline CSS to an existing core handle.
487 wp_add_inline_style( 'wp-admin', $this->get_css() );
488
489 // Print JS in footer.
490 add_action( 'admin_footer', array( $this, 'print_scripts' ) );
491 }
492
493 /**
494 * All CSS in one method — widget + notice.
495 */
496 private function get_css() {
497 return '
498 /* === Ultimate Cursor Promo Widget === */
499 #ultimate_cursor_promo_widget .inside { padding: 0; margin:0; }
500 #ultimate_cursor_promo_widget .postbox-header { display: none; }
501
502 .uc-promo-widget {
503 position: relative;
504 color: #f1f5f9;
505 // border-radius: 14px;
506 padding: 28px 22px 22px;
507 text-align: center;
508 font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, sans-serif;
509 overflow: hidden;
510 }
511
512 /* Ambient glow effect */
513 .uc-pw-glow {
514 position: absolute;
515 top: -40%;
516 right: -20%;
517 width: 200px;
518 height: 200px;
519 background: var(--uc-accent, #6366f1);
520 border-radius: 50%;
521 opacity: 0.12;
522 filter: blur(60px);
523 pointer-events: none;
524 animation: uc-glow-pulse 4s ease-in-out infinite;
525 }
526
527 @keyframes uc-glow-pulse {
528 0%, 100% { opacity: 0.10; transform: scale(1); }
529 50% { opacity: 0.20; transform: scale(1.15); }
530 }
531
532 /* Header */
533 .uc-pw-header {
534 display: flex;
535 align-items: center;
536 justify-content: center;
537 gap: 10px;
538 margin-bottom: 16px;
539 }
540
541 .uc-pw-icon {
542 font-size: 28px;
543 line-height: 1;
544 animation: uc-icon-bounce 2s ease-in-out infinite;
545 }
546
547 @keyframes uc-icon-bounce {
548 0%, 100% { transform: translateY(0); }
549 50% { transform: translateY(-4px); }
550 }
551
552 .uc-pw-badge {
553 display: inline-flex;
554 align-items: center;
555 background: var(--uc-accent, #6366f1);
556 color: #fff;
557 font-size: 12px;
558 font-weight: 800;
559 padding: 5px 14px;
560 border-radius: 20px;
561 letter-spacing: 0.8px;
562 text-transform: uppercase;
563 box-shadow: 0 2px 12px rgba(0,0,0,0.25), inset 0 1px 0 rgba(255,255,255,0.15);
564 animation: uc-badge-glow 3s ease-in-out infinite;
565 }
566
567 @keyframes uc-badge-glow {
568 0%, 100% { box-shadow: 0 2px 12px rgba(0,0,0,0.25), inset 0 1px 0 rgba(255,255,255,0.15); }
569 50% { box-shadow: 0 2px 20px var(--uc-accent, rgba(99,102,241,0.5)), inset 0 1px 0 rgba(255,255,255,0.15); }
570 }
571
572 /* Description */
573 .uc-pw-desc {
574 font-size: 13px;
575 line-height: 1.65;
576 color: #cbd5e1;
577 margin: 0 0 16px;
578 }
579
580 /* Feature list */
581 .uc-pw-features {
582 list-style: none;
583 margin: 0 0 18px;
584 padding: 0;
585 display: flex;
586 flex-direction: column;
587 gap: 8px;
588 }
589
590 .uc-pw-features li {
591 display: flex;
592 align-items: center;
593 gap: 8px;
594 font-size: 12px;
595 color: #e2e8f0;
596 justify-content: center;
597 }
598
599 .uc-pw-features li svg {
600 flex-shrink: 0;
601 }
602
603 /* Countdown */
604 .uc-pw-countdown {
605 display: inline-flex;
606 align-items: center;
607 gap: 6px;
608 font-size: 12px;
609 color: #94a3b8;
610 margin-bottom: 16px;
611 background: rgba(255,255,255,0.05);
612 padding: 6px 14px;
613 border-radius: 8px;
614 border: 1px solid rgba(255,255,255,0.08);
615 }
616
617 .uc-pw-cd-value {
618 color: #f8fafc;
619 font-weight: 700;
620 font-variant-numeric: tabular-nums;
621 font-size: 13px;
622 }
623
624 /* Coupon */
625 .uc-pw-coupon {
626 display: flex;
627 align-items: center;
628 justify-content: center;
629 gap: 8px;
630 margin-bottom: 18px;
631 font-size: 12px;
632 color: #94a3b8;
633 }
634
635 .uc-pw-coupon-code {
636 position: relative;
637 display: inline-flex;
638 align-items: center;
639 gap: 6px;
640 background: rgba(255,255,255,0.08);
641 border: 1px dashed rgba(255,255,255,0.25);
642 color: #f8fafc;
643 font-family: "SF Mono", "Fira Code", "Courier New", monospace;
644 font-size: 13px;
645 font-weight: 700;
646 letter-spacing: 2px;
647 padding: 6px 14px;
648 border-radius: 8px;
649 cursor: pointer;
650 transition: all 0.25s ease;
651 }
652
653 .uc-pw-coupon-code:hover {
654 background: rgba(255,255,255,0.15);
655 border-color: rgba(255,255,255,0.4);
656 transform: translateY(-1px);
657 }
658
659 .uc-pw-copy-icon { opacity: 0.6; transition: opacity 0.2s; }
660 .uc-pw-coupon-code:hover .uc-pw-copy-icon { opacity: 1; }
661
662 .uc-pw-code-copied {
663 display: none;
664 position: absolute;
665 inset: 0;
666 background: #22c55e;
667 color: #fff;
668 border-radius: 7px;
669 font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
670 font-size: 11px;
671 font-weight: 600;
672 letter-spacing: 0;
673 align-items: center;
674 justify-content: center;
675 }
676
677 .uc-pw-coupon-code.is-copied .uc-pw-code-copied { display: flex; }
678 .uc-pw-coupon-code.is-copied .uc-pw-code-text,
679 .uc-pw-coupon-code.is-copied .uc-pw-copy-icon { visibility: hidden; }
680
681 /* CTA Button */
682 .uc-pw-cta {
683 display: inline-flex;
684 align-items: center;
685 justify-content: center;
686 gap: 6px;
687 background: var(--uc-accent, #6366f1);
688 color: #fff !important;
689 text-decoration: none !important;
690 font-weight: 700;
691 font-size: 13px;
692 padding: 11px 28px;
693 border-radius: 10px;
694 transition: all 0.25s ease;
695 box-shadow: 0 4px 14px rgba(0,0,0,0.3);
696 width: 100%;
697 box-sizing: border-box;
698 }
699
700 .uc-pw-cta:hover {
701 transform: translateY(-2px);
702 box-shadow: 0 6px 20px rgba(0,0,0,0.4);
703 color: #fff !important;
704 filter: brightness(1.1);
705 }
706
707 .uc-pw-cta:active {
708 transform: translateY(0);
709 }
710
711 /* Dismiss button */
712 .uc-pw-dismiss {
713 position: absolute;
714 top: 10px;
715 right: 10px;
716 background: rgba(255,255,255,0.08);
717 border: none;
718 color: #64748b;
719 font-size: 14px;
720 cursor: pointer;
721 line-height: 1;
722 padding: 5px;
723 border-radius: 6px;
724 transition: all 0.2s;
725 display: flex;
726 align-items: center;
727 justify-content: center;
728 z-index: 2;
729 }
730
731 .uc-pw-dismiss:hover {
732 color: #f1f5f9;
733 background: rgba(255,255,255,0.15);
734 }
735
736 /* === Campaign-specific widget styles === */
737
738 /* Halloween */
739 .uc-campaign-halloween .uc-pw-glow {
740 background: #ff6600;
741 }
742
743 .uc-campaign-halloween .uc-pw-badge {
744 background: linear-gradient(135deg, #ff6600, #ff8c00);
745 }
746
747 .uc-campaign-halloween .uc-pw-cta {
748 background: linear-gradient(135deg, #ff6600, #ff8c00);
749 }
750
751 /* Black Friday */
752 .uc-campaign-black_friday .uc-pw-glow {
753 background: #f43f5e;
754 }
755
756 .uc-campaign-black_friday .uc-pw-badge {
757 background: linear-gradient(135deg, #f43f5e, #ec4899);
758 }
759
760 .uc-campaign-black_friday .uc-pw-cta {
761 background: linear-gradient(135deg, #f43f5e, #ec4899);
762 }
763
764 /* Regular */
765 .uc-campaign-regular .uc-pw-glow {
766 background: #6366f1;
767 }
768
769 .uc-campaign-regular .uc-pw-badge {
770 background: linear-gradient(135deg, #6366f1, #818cf8);
771 }
772
773 .uc-campaign-regular .uc-pw-cta {
774 background: linear-gradient(135deg, #6366f1, #818cf8);
775 }
776
777 /* === Ultimate Cursor Promo Notice === */
778 .uc-promo-notice {
779 border: none !important;
780 border-radius: 8px !important;
781 padding: 0 !important;
782 overflow: hidden;
783 margin: 15px 0 !important;
784 position: relative;
785 box-shadow: 0 4px 20px rgba(0,0,0,0.15);
786 }
787
788 .uc-pn-glow {
789 position: absolute;
790 top: -50%;
791 right: -10%;
792 width: 160px;
793 height: 160px;
794 background: var(--uc-accent, #6366f1);
795 border-radius: 50%;
796 opacity: 0.1;
797 filter: blur(50px);
798 pointer-events: none;
799 animation: uc-glow-pulse 4s ease-in-out infinite;
800 }
801
802 .uc-pn-inner {
803 display: flex;
804 align-items: center;
805 gap: 16px;
806 padding: 14px 20px;
807 color: #f1f5f9;
808 flex-wrap: wrap;
809 position: relative;
810 }
811
812 .uc-pn-badge-wrap {
813 display: flex;
814 align-items: center;
815 gap: 8px;
816 flex-shrink: 0;
817 }
818
819 .uc-pn-icon {
820 font-size: 22px;
821 line-height: 1;
822 animation: uc-icon-bounce 2s ease-in-out infinite;
823 }
824
825 .uc-pn-discount {
826 display: inline-flex;
827 align-items: center;
828 background: var(--uc-accent, #6366f1);
829 color: #fff;
830 font-size: 11px;
831 font-weight: 800;
832 padding: 4px 10px;
833 border-radius: 14px;
834 letter-spacing: 0.6px;
835 text-transform: uppercase;
836 box-shadow: 0 2px 8px rgba(0,0,0,0.2);
837 }
838
839 .uc-pn-content {
840 display: flex;
841 flex-direction: column;
842 gap: 2px;
843 flex: 1;
844 min-width: 200px;
845 }
846
847 .uc-pn-title {
848 font-size: 13px;
849 font-weight: 700;
850 color: #fff;
851 line-height: 1.3;
852 }
853
854 .uc-pn-desc {
855 font-size: 12px;
856 color: #cbd5e1;
857 line-height: 1.4;
858 }
859
860 .uc-pn-actions {
861 display: flex;
862 align-items: center;
863 gap: 14px;
864 flex-shrink: 0;
865 margin-right:30px;
866 }
867
868 .uc-pn-timer {
869 display: inline-flex;
870 align-items: center;
871 gap: 5px;
872 font-size: 12px;
873 color: #94a3b8;
874 white-space: nowrap;
875 background: rgba(255,255,255,0.06);
876 padding: 5px 10px;
877 border-radius: 6px;
878 }
879
880 .uc-pn-timer svg { opacity: 0.7; }
881
882 .uc-pn-timer strong {
883 color: #fff;
884 font-variant-numeric: tabular-nums;
885 }
886
887 .uc-pn-btn {
888 display: inline-flex;
889 align-items: center;
890 gap: 5px;
891 background: var(--uc-accent, #6366f1) !important;
892 color: #fff !important;
893 text-decoration: none !important;
894 font-size: 12px;
895 font-weight: 700;
896 padding: 8px 18px;
897 border-radius: 8px;
898 white-space: nowrap;
899 transition: all 0.25s ease;
900 box-shadow: 0 2px 10px rgba(0,0,0,0.2);
901 }
902
903 .uc-pn-btn:hover {
904 transform: translateY(-1px);
905 box-shadow: 0 4px 16px rgba(0,0,0,0.3);
906 color: #fff !important;
907 filter: brightness(1.1);
908 }
909
910 .uc-pn-dismiss {
911 position: absolute;
912 top: 10px;
913 right: 0px;
914 transform: translateY(-50%);
915 background: rgba(255,255,255,0.08);
916 border: none;
917 color: #64748b;
918 cursor: pointer;
919 padding: 5px;
920 border-radius: 6px;
921 transition: all 0.2s;
922 display: flex;
923 align-items: center;
924 justify-content: center;
925 z-index: 2;
926 }
927
928 .uc-pn-dismiss:hover {
929 color: #f1f5f9;
930 background: rgba(255,255,255,0.15);
931 }
932
933 /* Campaign-specific notice styles */
934 .uc-promo-notice.uc-campaign-halloween .uc-pn-discount {
935 background: linear-gradient(135deg, #ff6600, #ff8c00);
936 }
937 .uc-promo-notice.uc-campaign-halloween .uc-pn-btn {
938 background: linear-gradient(135deg, #ff6600, #ff8c00) !important;
939 }
940
941 .uc-promo-notice.uc-campaign-black_friday .uc-pn-discount {
942 background: linear-gradient(135deg, #f43f5e, #ec4899);
943 }
944 .uc-promo-notice.uc-campaign-black_friday .uc-pn-btn {
945 background: linear-gradient(135deg, #f43f5e, #ec4899) !important;
946 }
947
948 .uc-promo-notice.uc-campaign-regular .uc-pn-discount {
949 background: linear-gradient(135deg, #6366f1, #818cf8);
950 }
951 .uc-promo-notice.uc-campaign-regular .uc-pn-btn {
952 background: linear-gradient(135deg, #6366f1, #818cf8) !important;
953 }
954
955 @media (max-width: 782px) {
956 .uc-pn-inner {
957 flex-direction: column;
958 align-items: flex-start;
959 gap: 10px;
960 padding: 14px 44px 14px 16px;
961 }
962 .uc-pn-actions { flex-wrap: wrap; gap: 8px; }
963 .uc-pn-btn { margin-left: 0; }
964 }
965 ';
966 }
967
968 /**
969 * Minimal JS — handles countdowns, clipboard copy, and dismiss persistence.
970 */
971 public function print_scripts() {
972 ?>
973 <script>
974 (function() {
975 /* --- Countdown helper --- */
976 function ucUpdateCountdowns() {
977 document.querySelectorAll('[data-role="countdown"]').forEach(function(el) {
978 var container = el.closest('[data-end]');
979 if (!container) return;
980 var end = new Date(container.getAttribute('data-end') + 'T23:59:59').getTime();
981 var diff = end - Date.now();
982 if (diff <= 0) {
983 el.textContent = '<?php echo esc_js( __( 'Ended', 'ultimate-cursor' ) ); ?>';
984 return;
985 }
986 var d = Math.floor(diff / 864e5);
987 var h = Math.floor((diff % 864e5) / 36e5);
988 var m = Math.floor((diff % 36e5) / 6e4);
989 var s = Math.floor((diff % 6e4) / 1e3);
990 if (d > 0) {
991 el.textContent = d + 'd ' + h + 'h ' + m + 'm';
992 } else {
993 el.textContent = h + 'h ' + m + 'm ' + s + 's';
994 }
995 });
996 }
997 ucUpdateCountdowns();
998 setInterval(ucUpdateCountdowns, 1000);
999
1000 /* --- Coupon copy --- */
1001 document.querySelectorAll('.uc-pw-coupon-code').forEach(function(btn) {
1002 btn.addEventListener('click', function() {
1003 var code = btn.getAttribute('data-code');
1004 if (!code) return;
1005 var done = function() {
1006 btn.classList.add('is-copied');
1007 setTimeout(function() {
1008 btn.classList.remove('is-copied');
1009 }, 1500);
1010 };
1011 if (navigator.clipboard) {
1012 navigator.clipboard.writeText(code).then(done).catch(done);
1013 } else {
1014 var ta = document.createElement('textarea');
1015 ta.value = code;
1016 ta.style.cssText = 'position:fixed;left:-9999px';
1017 document.body.appendChild(ta);
1018 ta.select();
1019 try {
1020 document.execCommand('copy');
1021 } catch (e) {}
1022 document.body.removeChild(ta);
1023 done();
1024 }
1025 });
1026 });
1027
1028 /* --- Widget dismiss (AJAX — 30-day server-side) --- */
1029 document.querySelectorAll('.uc-pw-dismiss').forEach(function(btn) {
1030 btn.addEventListener('click', function() {
1031 var widget = btn.closest('.uc-promo-widget');
1032 if (widget) {
1033 widget.style.opacity = '0';
1034 widget.style.transform = 'scale(0.95)';
1035 widget.style.transition = 'all 0.3s ease';
1036 }
1037 var wrap = btn.closest('.postbox');
1038 setTimeout(function() {
1039 if (wrap) wrap.style.display = 'none';
1040 }, 300);
1041 var nonce = btn.getAttribute('data-nonce');
1042 var xhr = new XMLHttpRequest();
1043 xhr.open('POST', '<?php echo esc_url( admin_url( 'admin-ajax.php' ) ); ?>');
1044 xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
1045 xhr.send('action=uc_dismiss_promo_widget&nonce=' + encodeURIComponent(nonce));
1046 });
1047 });
1048
1049 /* --- Notice dismiss (AJAX — 30-day server-side) --- */
1050 document.querySelectorAll('.uc-pn-dismiss').forEach(function(btn) {
1051 btn.addEventListener('click', function() {
1052 var notice = btn.closest('.uc-promo-notice');
1053 if (notice) {
1054 notice.style.opacity = '0';
1055 notice.style.transform = 'translateY(-10px)';
1056 notice.style.transition = 'all 0.3s ease';
1057 setTimeout(function() {
1058 notice.style.display = 'none';
1059 }, 300);
1060 }
1061 var nonce = notice ? notice.getAttribute('data-nonce') : '';
1062 if (nonce) {
1063 var xhr = new XMLHttpRequest();
1064 xhr.open('POST', '<?php echo esc_url( admin_url( 'admin-ajax.php' ) ); ?>');
1065 xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
1066 xhr.send('action=uc_dismiss_promo_notice&nonce=' + encodeURIComponent(nonce));
1067 }
1068 });
1069 });
1070 })();
1071 </script>
1072 <?php
1073 }
1074 }
1075
1076 Ultimate_Cursor_Dashboard_Widget::instance();
1077