diff --git a/assets/apps/dashboard/src/Components/App.js b/assets/apps/dashboard/src/Components/App.js index c533454b7e..74734e0323 100644 --- a/assets/apps/dashboard/src/Components/App.js +++ b/assets/apps/dashboard/src/Components/App.js @@ -45,7 +45,9 @@ const App = () => {
{tabs[currentTab].render(setTab)}
- {!['starter-sites', 'settings'].includes(currentTab) && ( + {!['starter-sites', 'settings', 'launch-progress'].includes( + currentTab + ) && ( diff --git a/assets/apps/dashboard/src/Components/Content/LaunchProgress.js b/assets/apps/dashboard/src/Components/Content/LaunchProgress.js new file mode 100644 index 0000000000..934cf42791 --- /dev/null +++ b/assets/apps/dashboard/src/Components/Content/LaunchProgress.js @@ -0,0 +1,652 @@ +/* global neveDash */ +import { __ } from '@wordpress/i18n'; +import { useState, useEffect } from '@wordpress/element'; +import { LucideExternalLink, LucideCheck } from 'lucide-react'; +import Card from '../../Layout/Card'; +import Button from '../Common/Button'; +import TransitionWrapper from '../Common/TransitionWrapper'; +import cn from 'classnames'; +import apiFetch from '@wordpress/api-fetch'; + +/** + * Initialize step state with auto-detection and saved progress + * + * @param {Array} steps - Array of step objects + * @param {string} sectionKey - Section key (identity, content, performance) + * @param {Object} savedProgress - Saved progress from server + * @param {Object} autoDetections - Auto-detection overrides by index + * @return {Array} Initialized state array + */ +const initializeStepState = ( + steps, + sectionKey, + savedProgress, + autoDetections = {} +) => { + return steps.map((step, index) => { + // Auto-detection overrides saved false values + if (autoDetections[index]) { + return true; + } + + // Use saved progress if available + if ( + savedProgress[sectionKey] && + savedProgress[sectionKey][index] !== undefined + ) { + return savedProgress[sectionKey][index]; + } + + return step.completed; + }); +}; + +const { plugins } = neveDash; + +const activeTPC = + plugins['templates-patterns-collection'] && + plugins['templates-patterns-collection'].cta === 'deactivate'; + +const LaunchProgress = () => { + // Get checks from neveDash + const checks = neveDash.launchProgress || {}; + const autoDetected = checks.autoDetected || {}; + const savedProgress = checks.savedProgress || {}; + + // State to track completion for all steps + const [stepsState, setStepsState] = useState({ + identity: initializeStepState( + identitySteps, + 'identity', + savedProgress, + { + 1: autoDetected.hasLogo, + 3: autoDetected.hasFavicon, + } + ), + content: initializeStepState(contentSteps, 'content', savedProgress), + performance: initializeStepState( + performanceSteps, + 'performance', + savedProgress, + { + 0: autoDetected.hasCustomPermalink, + 1: autoDetected.hasSeoPlugin, + 3: autoDetected.hasPrivacyPage, + } + ), + }); + + // Save progress whenever it changes + useEffect(() => { + const timeoutId = setTimeout(() => { + apiFetch({ + path: '/nv/v1/dashboard/launch-progress', + method: 'POST', + data: { progress: stepsState }, + }).catch((error) => { + // eslint-disable-next-line no-console + console.error('Failed to save progress:', error); + }); + }, 500); // Debounce for 500ms + + return () => clearTimeout(timeoutId); + }, [stepsState]); + + // Calculate total steps and completed steps + const allCompleted = [ + ...stepsState.identity, + ...stepsState.content, + ...stepsState.performance, + ]; + const completedSteps = allCompleted.filter((completed) => completed).length; + const totalSteps = allCompleted.length; + const progressPercentage = (completedSteps / totalSteps) * 100; + + // Trigger confetti when all steps are completed + useEffect(() => { + if (completedSteps === totalSteps && totalSteps > 0) { + triggerConfetti(); + } + }, [completedSteps, totalSteps]); + + return ( + + {/* Skip Setup Banner */} +
+
+
+
+ + ⚡ + +

+ {__( + 'Import ready-made websites with a single click', + 'neve' + )} +

+
+

+ {__( + 'Explore a vast library of pre-designed sites within Neve. Visit our constantly growing collection of demos to find the perfect starting point for your project.', + 'neve' + )} +

+
+ +
+
+ + {/* Progress Bar */} + + {completedSteps === totalSteps && totalSteps > 0 ? ( + // Completion message +
+

+ + 🎉 + + {__( + 'Congratulations! Your Website is Ready for Launch!', + 'neve' + )} +

+

+ {__( + "You've completed all essential setup steps. Take your site to the next level with Pro features!", + 'neve' + )} +

+ {!neveDash.isValidLicense && ( + + {__('View Pro Plans', 'neve')} + + )} +
+ ) : ( + // Progress tracking + <> +
+
+

+ + 🚀 + + {__('Launch Progress', 'neve')} +

+

+ {__( + 'Complete these essential steps to launch your website', + 'neve' + )} +

+
+
+
+ {completedSteps}/{totalSteps} +
+
+ {__('Steps Completed', 'neve')} +
+
+
+ {/* Progress Bar */} +
+
+
+ + )} + + + + + + + + + ); +}; + +const StepSection = ({ + title, + steps, + sectionKey, + stepsState, + setStepsState, + startIndex = 1, +}) => { + const stepsCount = steps.length; + + return ( + +
+

{title}

+ + {stepsCount} {__('Steps', 'neve')} + +
+
+ {steps.map((step, index) => ( + { + setStepsState((prev) => ({ + ...prev, + [sectionKey]: prev[sectionKey].map( + (completed, i) => + i === index ? !completed : completed + ), + })); + }} + /> + ))} +
+
+ ); +}; + +const StepItem = ({ step, index, isCompleted, onToggle }) => { + const checkboxClasses = cn( + 'size-6 rounded border-2 flex items-center justify-center cursor-pointer transition-all shrink-0', + { + 'bg-blue-600 border-blue-600': isCompleted, + 'bg-white border-gray-300 hover:border-gray-400': !isCompleted, + } + ); + + const handleToggle = (e) => { + e.preventDefault(); + e.stopPropagation(); + onToggle(); + }; + + const handleKeyDown = (e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + e.stopPropagation(); + onToggle(); + } + }; + + const handleButtonClick = (e) => { + e.stopPropagation(); + + if (!isCompleted) { + onToggle(); + } + + if (step.link.startsWith('http')) { + window.open(step.link, '_blank', 'noopener,noreferrer'); + } else { + window.location.href = step.link; + } + }; + + const handleRowClick = () => { + onToggle(); + }; + + const handleRowKeyDown = (e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + onToggle(); + } + }; + + return ( +
+
+ {isCompleted && ( + + )} +
+
+ {index} +
+
+

+ {step.title} +

+

{step.description}

+
+ +
+ ); +}; + +// Step data +const urls = neveDash.launchProgressUrls || {}; + +const identitySteps = [ + { + title: __('Site Title', 'neve') + ' & ' + __('Site Tagline', 'neve'), + description: __( + 'Replace "Just Another WordPress Site" with your brand name and description', + 'neve' + ), + link: urls.siteIdentity, + completed: false, + }, + { + title: __('Upload Logo', 'neve'), + description: __( + 'Add your custom logo to the header for a professional look', + 'neve' + ), + link: urls.logo, + completed: false, + }, + { + title: __('Set Colors', 'neve'), + description: __( + "Customize your site's color scheme to match your brand identity", + 'neve' + ), + link: urls.colors, + completed: false, + }, + { + title: __('Add Site Icon (Favicon)', 'neve'), + description: __( + 'Display your brand in browser tabs, bookmarks, and mobile home screens', + 'neve' + ), + link: urls.favicon, + completed: false, + }, +]; + +const contentSteps = [ + { + title: __('Create Your Homepage', 'neve'), + description: __( + 'Add compelling content that tells visitors what you do and why it matters', + 'neve' + ), + link: urls.homepage, + completed: false, + }, + { + title: + __('About', 'neve') + + ' & ' + + __('Contact', 'neve') + + ' ' + + __('Pages', 'neve'), + description: __( + 'Create essential pages so visitors can learn about you and get in touch', + 'neve' + ), + link: urls.pages, + completed: false, + }, + { + title: __('Navigation Menu', 'neve'), + description: __( + 'Make it easy for visitors to find their way around your website', + 'neve' + ), + link: urls.menus, + completed: false, + }, + { + title: __('Footer', 'neve'), + description: __( + 'Add copyright info, social links, and contact details to your footer', + 'neve' + ), + link: urls.footer, + completed: false, + }, +]; + +const performanceSteps = [ + { + title: __('Set Permalink Structure', 'neve'), + description: __( + 'Configure SEO-friendly URLs (recommended: Post name)', + 'neve' + ), + link: urls.permalinks, + completed: false, + }, + { + title: __('Install SEO Plugin', 'neve'), + description: __( + 'Optimize your site for search engines with Yoast SEO or RankMath', + 'neve' + ), + link: urls.plugins, + completed: false, + }, + { + title: __('Test Site Speed', 'neve'), + description: __( + 'Check your website speed and performance using free testing tools', + 'neve' + ), + link: urls.speedTest, + completed: false, + }, + { + title: __('Create Privacy Policy Page', 'neve'), + description: __( + 'Meet legal requirements with essential privacy and terms pages', + 'neve' + ), + link: urls.privacyPolicy, + completed: false, + }, +]; + +/** + * Trigger confetti animation + */ +const triggerConfetti = () => { + const duration = 3000; + const animationEnd = Date.now() + duration; + const defaults = { + startVelocity: 30, + spread: 360, + ticks: 60, + zIndex: 999999, + }; + + const randomInRange = (min, max) => Math.random() * (max - min) + min; + + const interval = setInterval(() => { + const timeLeft = animationEnd - Date.now(); + + if (timeLeft <= 0) { + return clearInterval(interval); + } + + const particleCount = 50 * (timeLeft / duration); + + // Create confetti from two origins + createConfetti( + Object.assign({}, defaults, { + particleCount, + origin: { x: randomInRange(0.1, 0.3), y: Math.random() - 0.2 }, + }) + ); + createConfetti( + Object.assign({}, defaults, { + particleCount, + origin: { x: randomInRange(0.7, 0.9), y: Math.random() - 0.2 }, + }) + ); + }, 250); +}; + +/** + * Create confetti particles + * + * @param {Object} options - Confetti options + */ +const createConfetti = (options) => { + const canvas = document.createElement('canvas'); + canvas.style.position = 'fixed'; + canvas.style.top = '0'; + canvas.style.left = '0'; + canvas.style.width = '100%'; + canvas.style.height = '100%'; + canvas.style.pointerEvents = 'none'; + canvas.style.zIndex = options.zIndex || 999999; + document.body.appendChild(canvas); + + const ctx = canvas.getContext('2d'); + canvas.width = window.innerWidth; + canvas.height = window.innerHeight; + + const particles = []; + const colors = ['#3b82f6', '#8b5cf6', '#ec4899', '#10b981', '#f59e0b']; + + const randomInRange = (min, max) => Math.random() * (max - min) + min; + + // Create particles + for (let i = 0; i < options.particleCount; i++) { + particles.push({ + x: canvas.width * options.origin.x, + y: canvas.height * options.origin.y, + angle: randomInRange(0, 360), + velocity: options.startVelocity + randomInRange(-5, 5), + color: colors[Math.floor(Math.random() * colors.length)], + size: randomInRange(5, 10), + rotation: randomInRange(0, 360), + rotationSpeed: randomInRange(-10, 10), + gravity: 0.5, + decay: 0.95, + tick: 0, + }); + } + + // Animate particles + const animate = () => { + ctx.clearRect(0, 0, canvas.width, canvas.height); + let activeParticles = 0; + + particles.forEach((particle) => { + if (particle.tick < options.ticks) { + activeParticles++; + particle.tick++; + particle.velocity *= particle.decay; + particle.x += + Math.cos((particle.angle * Math.PI) / 180) * + particle.velocity; + particle.y += + Math.sin((particle.angle * Math.PI) / 180) * + particle.velocity + + particle.gravity; + particle.rotation += particle.rotationSpeed; + + ctx.save(); + ctx.translate(particle.x, particle.y); + ctx.rotate((particle.rotation * Math.PI) / 180); + ctx.fillStyle = particle.color; + ctx.fillRect( + -particle.size / 2, + -particle.size / 2, + particle.size, + particle.size + ); + ctx.restore(); + } + }); + + if (activeParticles > 0) { + window.requestAnimationFrame(animate); + } else { + document.body.removeChild(canvas); + } + }; + + animate(); +}; + +export default LaunchProgress; diff --git a/assets/apps/dashboard/src/utils/common.js b/assets/apps/dashboard/src/utils/common.js index f55d146078..0ebe8f1f6a 100644 --- a/assets/apps/dashboard/src/utils/common.js +++ b/assets/apps/dashboard/src/utils/common.js @@ -6,6 +6,7 @@ import Welcome from '../Components/Content/Welcome'; import FreePro from '../Components/Content/FreePro'; import Settings from '../Components/Content/Settings'; import Changelog from '../Components/Content/Changelog'; +import LaunchProgress from '../Components/Content/LaunchProgress'; import { __ } from '@wordpress/i18n'; @@ -18,6 +19,10 @@ const tabs = { label: __('Starter Sites', 'neve'), render: () => , }, + 'launch-progress': { + label: __('Launch Progress', 'neve'), + render: () => , + }, 'free-pro': { label: __('Free vs Pro', 'neve'), render: () => , @@ -36,6 +41,11 @@ const tabs = { }, }; +// Conditionally remove launch-progress tab if not a new user +if (Boolean(neveDash.showLaunchProgress) === false) { + delete tabs['launch-progress']; +} + const { plugins } = neveDash; const activeTPC = plugins['templates-patterns-collection'] && diff --git a/inc/admin/dashboard/main.php b/inc/admin/dashboard/main.php index 62381d5e2a..6e71cd4164 100755 --- a/inc/admin/dashboard/main.php +++ b/inc/admin/dashboard/main.php @@ -402,6 +402,44 @@ private function get_localization() { $lang_code = isset( $available_languages[ $language ] ) ? 'de' : 'en'; $data['lang'] = $lang_code; + // Launch Progress checks + $launch_progress_data = $this->get_launch_progress_checks(); + $data['showLaunchProgress'] = $launch_progress_data['showLaunchProgress']; + $data['launchProgress'] = [ + 'autoDetected' => $launch_progress_data['autoDetected'], + 'savedProgress' => $launch_progress_data['savedProgress'], + ]; + + $screen = get_current_screen(); + if ( ! isset( $screen->id ) ) { + return $data; + } + + $theme = $this->theme_args; + $theme_page = ! empty( $theme['template'] ) ? $theme['template'] . '-welcome' : $theme['slug'] . '-welcome'; + + // Check if front page exists + $page_on_front = get_option( 'page_on_front' ); + $homepage_url = $page_on_front ? admin_url( 'post.php?post=' . $page_on_front . '&action=edit' ) : admin_url( 'edit.php?post_type=page' ); + + // Launch Progress step URLs + $data['launchProgressUrls'] = [ + 'upgradeURL' => apply_filters( 'neve_upgrade_link_from_child_theme_filter', tsdk_translate_link( tsdk_utmify( 'https://themeisle.com/themes/neve/upgrade/', 'getpronow', 'launchprogress' ) ) ), + 'starterSites' => admin_url( 'admin.php?page=' . $theme_page . '#starter-sites' ), + 'siteIdentity' => add_query_arg( [ 'autofocus[section]' => 'title_tagline' ], admin_url( 'customize.php' ) ), + 'logo' => add_query_arg( [ 'autofocus[control]' => 'custom_logo' ], admin_url( 'customize.php' ) ), + 'colors' => add_query_arg( [ 'autofocus[section]' => 'neve_colors_background_section' ], admin_url( 'customize.php' ) ), + 'favicon' => add_query_arg( [ 'autofocus[control]' => 'site_icon' ], admin_url( 'customize.php' ) ), + 'homepage' => $homepage_url, + 'pages' => admin_url( 'edit.php?post_type=page' ), + 'menus' => admin_url( 'nav-menus.php' ), + 'footer' => add_query_arg( [ 'autofocus[panel]' => 'hfg_footer' ], admin_url( 'customize.php' ) ), + 'permalinks' => admin_url( 'options-permalink.php' ), + 'plugins' => admin_url( 'plugin-install.php?s=seo&tab=search' ), + 'speedTest' => 'https://pagespeed.web.dev/analysis?url=' . urlencode( get_site_url() ), + 'privacyPolicy' => admin_url( 'options-privacy.php' ), + ]; + return $data; } @@ -542,6 +580,66 @@ private function get_customizer_shortcuts() { ]; } + /** + * Get launch progress checks. + * + * @return array{ + * showLaunchProgress: bool, + * autoDetected: array{ + * hasLogo: bool, + * hasFavicon: bool, + * hasCustomPermalink: bool, + * hasSeoPlugin: bool, + * hasPrivacyPage: bool + * }, + * savedProgress: array> + * } + */ + private function get_launch_progress_checks() { + // Check if we should show the Launch Progress tab + $show_launch_progress = get_option( \Neve\Core\Admin::$launch_progress_option ); + if ( false === $show_launch_progress ) { + $install_time = get_option( 'neve_install' ); + if ( ! empty( $install_time ) ) { + $one_week_ago = time() - WEEK_IN_SECONDS; + $show_launch_progress = ( intval( $install_time ) > $one_week_ago ) ? 'yes' : 'no'; + update_option( \Neve\Core\Admin::$launch_progress_option, $show_launch_progress, false ); + } else { + $show_launch_progress = 'no'; + } + } + + $has_logo = (bool) get_theme_mod( 'custom_logo' ); + $has_favicon = (bool) get_site_icon_url(); + $permalink_structure = get_option( 'permalink_structure' ); + + // Check if SEO plugin is active (Yoast, RankMath, or AIOSEO) + $has_seo_plugin = ( + class_exists( 'WPSEO_Options' ) || // Yoast SEO + class_exists( 'RankMath' ) || // RankMath + function_exists( 'aioseo' ) // All in One SEO + ); + + // Check if privacy policy page exists + $privacy_page_id = (int) get_option( 'wp_page_for_privacy_policy' ); + $has_privacy_page = $privacy_page_id > 0 && get_post_status( $privacy_page_id ) === 'publish'; + + // Get saved progress from option + $saved_progress = get_option( 'neve_launch_progress', [] ); + + return [ + 'showLaunchProgress' => ( $show_launch_progress === 'yes' ), + 'autoDetected' => [ + 'hasLogo' => $has_logo, + 'hasFavicon' => $has_favicon, + 'hasCustomPermalink' => ! empty( $permalink_structure ), + 'hasSeoPlugin' => $has_seo_plugin, + 'hasPrivacyPage' => $has_privacy_page, + ], + 'savedProgress' => $saved_progress, + ]; + } + /** * Get doc link. * diff --git a/inc/core/admin.php b/inc/core/admin.php index 592158b9b4..27e97823a1 100644 --- a/inc/core/admin.php +++ b/inc/core/admin.php @@ -22,6 +22,13 @@ class Admin { use Theme_Info; + /** + * Launch progress option key. + * + * @var string + */ + public static $launch_progress_option = 'neve_show_launch_progress'; + /** * Dismiss notice key. * @@ -322,6 +329,26 @@ public function register_rest_routes() { ) ); + register_rest_route( + 'nv/v1/dashboard', + '/launch-progress', + [ + 'methods' => \WP_REST_Server::EDITABLE, + 'callback' => [ $this, 'save_launch_progress' ], + 'permission_callback' => function() { + return current_user_can( 'manage_options' ); + }, + 'args' => array( + 'progress' => array( + 'required' => true, + 'sanitize_callback' => function( $value ) { + return is_array( $value ) ? $value : []; + }, + ), + ), + ] + ); + register_rest_route( 'nv/v1/dashboard', '/activate-module', @@ -469,6 +496,64 @@ public function activate_module( $request ) { wp_send_json_success( $module_value ? __( 'Module Activated', 'neve' ) : __( 'Module Deactivated.', 'neve' ) ); } + /** + * Save launch progress state. + * + * @param \WP_REST_Request> $request The request object. + * @return \WP_REST_Response + */ + public function save_launch_progress( $request ) { + $progress = $request->get_param( 'progress' ); + + if ( ! is_array( $progress ) ) { + return new \WP_REST_Response( + [ + 'success' => false, + 'message' => 'Invalid progress data', + ], + 400 + ); + } + + // Validate progress structure + $valid_keys = [ 'identity', 'content', 'performance' ]; + foreach ( $valid_keys as $key ) { + if ( ! isset( $progress[ $key ] ) || ! is_array( $progress[ $key ] ) ) { + return new \WP_REST_Response( + [ + 'success' => false, + 'message' => 'Invalid progress structure', + ], + 400 + ); + } + + // Validate all values are booleans + foreach ( $progress[ $key ] as $value ) { + if ( ! is_bool( $value ) ) { + return new \WP_REST_Response( + [ + 'success' => false, + 'message' => 'Progress values must be boolean', + ], + 400 + ); + } + } + } + + // Save to option + update_option( 'neve_launch_progress', $progress, false ); + + return new \WP_REST_Response( + [ + 'success' => true, + 'message' => 'Progress saved', + ], + 200 + ); + } + /** * Drop `Background` submenu item. */