/** * setup-wizard.tsx - Dailymotion Pro Setup Wizard * * This script handles the setup wizard functionality for the Dailymotion Pro plugin. * It manages the UI interactions, API calls, and data storage for the setup process. * * @file setup-wizard.tsx * @requires None * * Functions: * - openPopupBtn(): Opens the popup overlay * - closePopup(): Closes the popup overlay * - nextStep(step): Advances to the next step in the wizard * - updateSlots(step): Updates the UI to reflect the current step * - changeButtonStatus(buttonId, status, text): Changes the status of a button * - storeApi(): Stores the API credentials and player IDs * - setupAutomated(): Fetches and stores player IDs * - updatePlayerIdsUI(elementId, data): Updates the UI with player IDs * - storeContextualEmbedSettings(): Stores the contextual embed settings * - skipSetup(): Skips the setup process * - updateFeedback(message, status, duration): Updates the feedback component * - hideFeedback(): Hides the feedback element * - initializeSetupWizard(): Initializes the setup wizard * * Event Listeners: * - DOMContentLoaded: Calls initializeSetupWizard() when the page loads * - click on popupOverlay: Closes the popup when clicking outside * * @author Yudhi Satrio * @version 2.1.0 */ // Import Motion library import { animate } from "motion"; // Step transition animations function animateStepIn(stepElement: HTMLElement, duration = 0.5) { return animate(stepElement, { opacity: [0, 1], scale: [0.98, 1] }, { duration: duration, easing: [0.22, 0.03, 0.26, 1] } ); } function animateStepOut(stepElement: HTMLElement, duration = 0.3) { return animate(stepElement, { opacity: [1, 0], scale: [1, 0.98] }, { duration: duration, easing: [0.22, 0.03, 0.26, 1] } ); } // Progress bar animations function animateProgressBar(slots: NodeListOf, currentStep: number) { slots.forEach((slot, index) => { const step = index + 1; if (step < currentStep) { // Passed steps animate(slot, { scale: [1, 1.05, 1] }, { duration: 0.3, delay: index * 0.1 }); } else if (step === currentStep) { // Current step animate(slot, { scale: [1, 1.1, 1] }, { duration: 0.4, delay: index * 0.1 }); } }); } // Get elements const popupOverlay = document.getElementById('popupOverlay') as HTMLElement; const slots = document.querySelectorAll('.slot'); // Open pop-up with animation function openPopupBtn(): void { // First make it visible but transparent popupOverlay.style.display = 'flex'; popupOverlay.style.opacity = '0'; // Get the popup box for separate animation const popupBox = popupOverlay.querySelector('.popup-box') as HTMLElement; // Animate both the overlay and the popup box separately animate(popupOverlay, { opacity: [0, 1] }, { duration: 0.3 }); animate(popupBox, { opacity: [0, 1], scale: [0.9, 1] }, { duration: 0.3 }); } // Close pop-up with animation function closePopup(): void { const popupBox = popupOverlay.querySelector('.popup-box') as HTMLElement; // Animate the overlay fading out animate( popupOverlay, { opacity: [1, 0] }, { duration: 0.2 } ).finished.then(() => { // Hide the overlay after animation completes popupOverlay.style.display = 'none'; }); // Animate the popup box scaling down animate( popupBox, { opacity: [1, 0], scale: [1, 0.9] }, { duration: 0.2 } ); } // Close pop-up if clicked outside the pop-up box popupOverlay.addEventListener('click', function (event) { if (event.target === popupOverlay) { closePopup(); } }); /** * Advances to the next step in the setup wizard with animations. * * This function updates the URL with the current step parameter, * then updates the UI to reflect the new step with smooth animations. * It also registers the step in browser history for back/forward navigation. * * @param {number} step - The step number to advance to. * * @description * 1. Updates the URL with the step parameter * 2. Registers the step in browser history * 3. Animates the transition between steps: * - Fades out the current step * - Checks the appropriate radio button for the new step * - Fades in the new step * - Animates the progress bar * 4. Handles steps 1, 2, 3, 3.1, 3.2, and 4. * 5. Also sends a POST request to update the step on the server for compatibility * * @throws {Error} Logs any errors to the console. */ function nextStep(step: number): void { // Find the currently active step const currentStepRadio = document.querySelector('input[type="radio"].wizard-step:checked') as HTMLInputElement; const currentStepContent = currentStepRadio ? document.querySelector(`.wizard-content:nth-of-type(${Array.from(document.querySelectorAll('input[type="radio"].wizard-step')).indexOf(currentStepRadio) + 1})`) as HTMLElement : null; // Determine which step to show next let nextStepRadio: HTMLInputElement | null = null; if (step === 1) { nextStepRadio = document.getElementById('step-1') as HTMLInputElement; } else if (step === 2) { nextStepRadio = document.getElementById('step-2') as HTMLInputElement; } else if (step === 3) { nextStepRadio = document.getElementById('step-3') as HTMLInputElement; } else if (step === 3.1) { nextStepRadio = document.getElementById('step-3-1') as HTMLInputElement; } else if (step === 3.2) { nextStepRadio = document.getElementById('step-3-2') as HTMLInputElement; } else if (step === 4) { nextStepRadio = document.getElementById('step-4') as HTMLInputElement; } if (!nextStepRadio) return; const nextStepContent = document.querySelector(`.wizard-content:nth-of-type(${Array.from(document.querySelectorAll('input[type="radio"].wizard-step')).indexOf(nextStepRadio) + 1})`) as HTMLElement; // Update URL with step parameter and add to browser history const url = new URL(window.location.href); url.searchParams.set('step', step.toString()); window.history.pushState({ step: step }, '', url.toString()); // Also send request to update the step on the server for compatibility fetch(dmProSetupWizard.apiUrl + "/wizard-step", { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': dmProSetupWizard.wpNonce }, body: JSON.stringify({ step: step }) }) .then(res => res.json()) .catch(error => { console.error('Error updating server step:', error); }); // Animate the transition if (currentStepContent) { // Animate out the current step animateStepOut(currentStepContent).finished.then(() => { // Check the next step radio button nextStepRadio!.checked = true; // Update the progress bar with animation updateSlots(step); // Animate in the next step animateStepIn(nextStepContent); }); } else { // No current step (first load), just show the next step nextStepRadio.checked = true; updateSlots(step); animateStepIn(nextStepContent); } } /** * Updates the progress bar slots with animation. * * @param {number} step - The current step number. */ function updateSlots(step: number): void { // First update the classes slots.forEach((slot, index) => { const progressStep = index + 1; // Slot step starts from 1 if (progressStep < step) { // Passed steps slot.classList.remove('current'); slot.classList.add('passed'); } else if (step === progressStep) { // Current step slot.classList.remove('passed'); slot.classList.add('current'); } else { // Future steps (no class) slot.classList.remove('passed', 'current'); } }); // Then animate the progress bar animateProgressBar(slots, step); } /** * Changes the status of a button. * @param {string} buttonId - The ID of the button element. * @param {string} status - The status to set ('loading', 'success', 'error', or 'reset'). * @param {string} [text] - The text to display on the button (optional). */ function changeButtonStatus(buttonId: string, status: 'loading' | 'success' | 'error' | 'reset', text?: string): void { const button = document.getElementById(buttonId); if (!button) return; // Remove all status classes button.classList.remove('loading', 'success', 'error', 'dm-pro--button-processing'); switch (status) { case 'loading': button.disabled = true; button.classList.add('loading', 'dm-pro--button-processing'); button.textContent = text || 'Loading...'; break; case 'success': button.disabled = true; button.classList.add('success'); button.textContent = text || 'Success!'; break; case 'error': button.disabled = false; button.classList.add('error'); button.textContent = text || 'Error. Try Again'; break; case 'reset': button.disabled = false; button.textContent = text || 'Submit'; break; } } /** * Stores the API credentials and player IDs. * * The function is not only store the data. It also updates the UI with the fetched player IDs. * If the API request fails, it will revert the button status to 'error'. * */ function storeApi(): void { event?.preventDefault(); // Prevent form submission and page reload changeButtonStatus('api-store__button', 'loading', 'Storing...'); // Get the values from the form fields const apiKey = (document.getElementById('api-key') as HTMLInputElement).value; const apiSecret = (document.getElementById('api-secret') as HTMLInputElement).value; const channelId = (document.getElementById('channel-id') as HTMLInputElement).value; // Validate the inputs (optional) if (!apiKey || !apiSecret || !channelId) { alert('Please fill out all fields.'); changeButtonStatus('api-store__button', 'reset', 'Submit'); return; } // Prepare the data for the API request const apiData = { api_key: apiKey, api_secret: apiSecret, channel_id: channelId, }; fetch(dmProSetupWizard.apiUrl + "/store-api", { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': dmProSetupWizard.wpNonce }, body: JSON.stringify(apiData) }) .then(res => { if (!res.ok) { // If the HTTP status code is not in the 200-299 range return res.json().then(err => Promise.reject(err)); } return res.json() }) .then(data => { if (data.data.status === 200) { changeButtonStatus('api-store__button', 'success', 'Stored Successfully'); // Use the nextStep function which now has animations nextStep(3); // Proceed to next step or other actions updateFeedback(data.message, 'feedback-success'); } else { changeButtonStatus('api-store__button', 'error'); updateFeedback(data.message, 'feedback-error'); } }) .catch(error => { console.error('Error:', error); changeButtonStatus('api-store__button', 'error'); updateFeedback(error.message, 'feedback-error'); }) } function setupAutomated(): void { fetch(dmProSetupWizard.apiUrl + "/get-player-ids", { method: 'GET', credentials: 'same-origin', headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': dmProSetupWizard.wpNonce } }) .then(res => { if (!res.ok) { // If the HTTP status code is not in the 200-299 range return res.json().then(err => Promise.reject(err)); } return res.json() }) .then(data => { // Store the fetched data in localStorage localStorage.setItem('playerIds', JSON.stringify(data)); updatePlayerIdsUI('contextual-player-ids', data.contextual_ids); // Use the nextStep function which now has animations setTimeout(() => { nextStep(3.1); }, 300) }) .catch(error => { console.error('Error:', error); updateFeedback(error.message, 'feedback-error'); }) } /** * It's updating the UI with the fetched player IDs. This is only for the player ID select element. * * @param elementId * @param data */ function updatePlayerIdsUI(elementId: string, data: Array<{id: string, label: string}>): void { // Wait for DOM to be fully loaded if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', function() { updatePlayerIdsUI(elementId, data); }); return; } // Update UI with the player IDs // Fix for the mismatch between 'contextual-player-ids' and 'contextual-player-id' const actualElementId = elementId === 'contextual-player-ids' ? 'contextual-player-id' : elementId; const playerIdsList = document.getElementById(actualElementId); if (playerIdsList) { playerIdsList.innerHTML = data.map(player => ``).join(''); } else { console.warn(`Element with ID '${actualElementId}' not found in the DOM.`); } } function storeContextualEmbedSettings(): void { event?.preventDefault(); // Prevent form submission and page reload changeButtonStatus('contextual-store__button', 'loading', 'Storing...'); // Get the values from the form fields const playerId = (document.getElementById('contextual-player-id') as HTMLSelectElement).value; const selectedPosition = document.querySelector('input[name="position"]:checked') as HTMLInputElement; const position = selectedPosition ? selectedPosition.value : null; const videoHeading = (document.getElementById('contextual-video-heading') as HTMLInputElement).checked; const videoHeadingText = (document.getElementById('contextual-video-heading-text') as HTMLInputElement).value; const mute = (document.getElementById('contextual-mute') as HTMLInputElement).checked; const showVideoTitle = (document.getElementById('contextual-video-title') as HTMLInputElement).checked; // Prepare the data for the API request const settingsData = { contextual_embed: true, contextual_player_id: playerId, position: position, contextual_video_heading: videoHeading, contextual_video_heading_text: videoHeadingText, contextual_mute: mute, contextual_video_title: showVideoTitle }; fetch(dmProSetupWizard.apiUrl + "/store-contextual-embed-settings", { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': dmProSetupWizard.wpNonce }, body: JSON.stringify(settingsData) }) .then(res => { if (!res.ok) { // If the HTTP status code is not in the 200-299 range return res.json().then(err => Promise.reject(err)); } return res.json() }) .then(data => { if (data.data.status === 200) { changeButtonStatus('contextual-store__button', 'success', 'Stored Successfully'); // Use the nextStep function which now has animations nextStep(3.2); updateFeedback(data.message, 'feedback-success'); } else { changeButtonStatus('contextual-store__button', 'error' ); updateFeedback(data.message, 'feedback-error'); } }) .catch(error => { console.error('Error:', error); changeButtonStatus('contextual-store__button', 'error' ); updateFeedback(error.message, 'feedback-error'); }) } function skipSetup(): void { // Update URL with step parameter and add to browser history const url = new URL(window.location.href); url.searchParams.set('step', '-1'); window.history.pushState({ step: -1 }, '', url.toString()); // Also send request to update the step on the server for compatibility fetch(dmProSetupWizard.apiUrl + "/wizard-step", { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': dmProSetupWizard.wpNonce }, body: JSON.stringify({ step: -1 }) }) .then(res => { if (!res.ok) { // If the HTTP status code is not in the 200-299 range return res.json().then(err => Promise.reject(err)); } return res.json() }) .then(data => { setTimeout(() => { window.location.href = "admin.php?page=dm-pro-admin-panel&subpage=dashboard"; }, 300) }) .catch(error => { console.error('Error:', error); updateFeedback(error.message, 'feedback-error', 0); }) } /** * Updates the feedback component with a message and status, using animations. * @param {string} message - The message to display in the feedback component. * @param {string} status - The status of the feedback ('feedback-success' or 'feedback-error'). * @param {number} [duration=2000] - The duration in milliseconds to show the feedback (default: 2000ms). */ function updateFeedback(message: string, status: 'feedback-success' | 'feedback-error', duration: number = 5000): void { const feedbackElement = document.querySelector('.feedback') as HTMLElement; const messageElement = feedbackElement.querySelector('.feedback-message') as HTMLElement; // Update text and status messageElement.textContent = message; feedbackElement.classList.remove('feedback-success', 'feedback-error'); feedbackElement.classList.add(status); // Animate the feedback in animate(feedbackElement, { opacity: [0, 1], y: [20, 0] }, { duration: 0.4, easing: 'ease-out' } ); if (duration > 0) { // Hide feedback after duration setTimeout(() => { hideFeedback(); }, duration); } } /** * Hides the feedback element with animation. */ function hideFeedback(): void { const feedbackElement = document.querySelector('.feedback') as HTMLElement; if (!feedbackElement) return; animate(feedbackElement, { opacity: [1, 0], y: [0, 20] }, { duration: 0.3, easing: 'ease-in' } ).finished.then(() => { feedbackElement.classList.remove('showing', 'feedback-success', 'feedback-error'); }); } /** * Initializes the setup wizard by populating player IDs from local storage * and handling URL parameters for step navigation. * * This function is called when the page loads. It checks if there are any * stored player IDs in the browser's local storage. If found, it updates * the UI with these IDs, specifically for the contextual player. * * It also checks for a 'step' parameter in the URL and navigates to that step. * * @function * @name initializeSetupWizard * * @description * 1. Retrieves 'playerIds' from localStorage. * 2. If valid data exists, parses the JSON string. * 3. If contextual IDs are present, updates the UI using updatePlayerIdsUI function. * 4. Checks for a 'step' parameter in the URL and navigates to that step. * 5. Sets up event listener for browser back/forward buttons. * * @throws {SyntaxError} Possible error when parsing invalid JSON from localStorage. * * @example * // This function is typically called when the DOM is fully loaded: * document.addEventListener('DOMContentLoaded', initializeSetupWizard); */ function initializeSetupWizard(): void { // Check if we have stored player IDs const storedPlayerIds = localStorage.getItem('playerIds'); if (storedPlayerIds && storedPlayerIds !== '{}' && storedPlayerIds !== '[]') { // If we have stored data, use it to update the UI try { const playerIds = JSON.parse(storedPlayerIds); if (playerIds.contextual_ids && playerIds.contextual_ids.length > 0) { updatePlayerIdsUI('contextual-player-id', playerIds.contextual_ids); } } catch (error) { console.error('Error parsing stored player IDs:', error); } } // Check for step parameter in URL const urlParams = new URLSearchParams(window.location.search); const stepParam = urlParams.get('step'); if (stepParam) { // Convert to number or float if needed const step = stepParam.includes('.') ? parseFloat(stepParam) : parseInt(stepParam); // Only navigate if it's a valid step if (!isNaN(step) && (step === -1 || (step >= 1 && step <= 4) || step === 3.1 || step === 3.2)) { // Find the appropriate radio button let stepRadio: HTMLInputElement | null = null; if (step === -1 || step === 1) { stepRadio = document.getElementById('step-1') as HTMLInputElement; } else if (step === 2) { stepRadio = document.getElementById('step-2') as HTMLInputElement; } else if (step === 3) { stepRadio = document.getElementById('step-3') as HTMLInputElement; } else if (step === 3.1) { stepRadio = document.getElementById('step-3-1') as HTMLInputElement; } else if (step === 3.2) { stepRadio = document.getElementById('step-3-2') as HTMLInputElement; } else if (step === 4) { stepRadio = document.getElementById('step-4') as HTMLInputElement; } if (stepRadio) { // Check the radio button stepRadio.checked = true; // Update the progress bar updateSlots(step); } } } // Set up event listener for browser back/forward buttons window.addEventListener('popstate', function(event) { if (event.state && event.state.step !== undefined) { const step = event.state.step; // Find the currently active step const currentStepRadio = document.querySelector('input[type="radio"].wizard-step:checked') as HTMLInputElement; const currentStepContent = currentStepRadio ? document.querySelector(`.wizard-content:nth-of-type(${Array.from(document.querySelectorAll('input[type="radio"].wizard-step')).indexOf(currentStepRadio) + 1})`) as HTMLElement : null; // Find the appropriate radio button for the new step let nextStepRadio: HTMLInputElement | null = null; if (step === -1 || step === 1) { nextStepRadio = document.getElementById('step-1') as HTMLInputElement; } else if (step === 2) { nextStepRadio = document.getElementById('step-2') as HTMLInputElement; } else if (step === 3) { nextStepRadio = document.getElementById('step-3') as HTMLInputElement; } else if (step === 3.1) { nextStepRadio = document.getElementById('step-3-1') as HTMLInputElement; } else if (step === 3.2) { nextStepRadio = document.getElementById('step-3-2') as HTMLInputElement; } else if (step === 4) { nextStepRadio = document.getElementById('step-4') as HTMLInputElement; } if (nextStepRadio) { const nextStepContent = document.querySelector(`.wizard-content:nth-of-type(${Array.from(document.querySelectorAll('input[type="radio"].wizard-step')).indexOf(nextStepRadio) + 1})`) as HTMLElement; // Animate the transition if (currentStepContent) { // Animate out the current step animateStepOut(currentStepContent).finished.then(() => { // Check the next step radio button nextStepRadio!.checked = true; // Update the progress bar with animation updateSlots(step); // Animate in the next step animateStepIn(nextStepContent); }); } else { // No current step (first load), just show the next step nextStepRadio.checked = true; updateSlots(step); animateStepIn(nextStepContent); } } } }); } // Add event listeners to elements when the DOM is loaded function addEventListeners(): void { // Get elements that need event listeners const notPartnerBtn = document.querySelector('.dm-pro--secondary-button[onclick="openPopupBtn()"]'); const goToStep2Btn = document.getElementById('go-to-step-2'); const closePopupBtns = document.querySelectorAll('[onclick="closePopup()"]'); const skipSetupBtns = document.querySelectorAll('[onclick="skipSetup()"]'); const apiForm = document.querySelector('form[onsubmit="storeApi()"]'); const backToStep1Btn = document.querySelector('button[onclick="nextStep(1)"]'); const setupAutomatedBtn = document.querySelector('button[onclick="setupAutomated()"]'); const backToStep3Btn = document.querySelector('button[onclick="nextStep(3)"]'); const contextualEmbedForm = document.querySelector('form[onsubmit="storeContextualEmbedSettings()"]'); const goToStep4Btn = document.getElementById('go-to-step-4'); const hideFeedbackBtn = document.querySelector('.feedback-close'); // Add event listeners if (notPartnerBtn) { notPartnerBtn.removeAttribute('onclick'); notPartnerBtn.addEventListener('click', openPopupBtn); } if (goToStep2Btn) { goToStep2Btn.removeAttribute('onclick'); goToStep2Btn.addEventListener('click', () => nextStep(2)); } closePopupBtns.forEach(btn => { btn.removeAttribute('onclick'); btn.addEventListener('click', closePopup); }); skipSetupBtns.forEach(btn => { btn.removeAttribute('onclick'); btn.addEventListener('click', skipSetup); }); if (apiForm) { apiForm.removeAttribute('onsubmit'); apiForm.addEventListener('submit', (e) => { e.preventDefault(); storeApi(); }); } if (backToStep1Btn) { backToStep1Btn.removeAttribute('onclick'); backToStep1Btn.addEventListener('click', () => nextStep(1)); } if (setupAutomatedBtn) { setupAutomatedBtn.removeAttribute('onclick'); setupAutomatedBtn.addEventListener('click', setupAutomated); } if (backToStep3Btn) { backToStep3Btn.removeAttribute('onclick'); backToStep3Btn.addEventListener('click', () => nextStep(3)); } if (contextualEmbedForm) { contextualEmbedForm.removeAttribute('onsubmit'); contextualEmbedForm.addEventListener('submit', (e) => { e.preventDefault(); storeContextualEmbedSettings(); }); } if (goToStep4Btn) { goToStep4Btn.removeAttribute('onclick'); goToStep4Btn.addEventListener('click', () => nextStep(4)); } if (hideFeedbackBtn) { hideFeedbackBtn.removeAttribute('onclick'); hideFeedbackBtn.addEventListener('click', hideFeedback); } } // Call these functions when the page loads document.addEventListener('DOMContentLoaded', () => { initializeSetupWizard(); addEventListeners(); }); // Get PHP variables from wp_localize_script // @ts-ignore - dmProSetupWizard is defined by wp_localize_script in SetupWizardController.php declare const dmProSetupWizard: { apiUrl: string; wpNonce: string; adminUrl: string; };