let config = {}; let player = null; let currentDurationSec = 0; // Wait for DOM to finish loading document.addEventListener('DOMContentLoaded', () => { fetchConfig(); }); // Setup YouTube API function onYouTubeIframeAPIReady() { // We don't initialize a player yet until the user provides a URL } // Format seconds into M:SS function formatTime(seconds) { const m = Math.floor(seconds / 60); const s = Math.floor(seconds % 60); return `${m}:${s.toString().padStart(2, '0')}`; } // Extract YouTube ID from various URL formats function extractVideoID(url) { const regExp = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|\&v=)([^#\&\?]*).*/; const match = url.match(regExp); return (match && match[2].length === 11) ? match[2] : null; } // Fetch and load configuration async function fetchConfig() { try { const res = await fetch('config.json'); config = await res.json(); populateUI(); setupEventListeners(); loadLogo(); } catch (error) { console.error("Failed to load config.json", error); document.getElementById('site-title').innerText = "Error loading config"; document.getElementById('site-title').classList.remove('glitch-text'); } } // Check for avatar.png or logo.png to display in header function loadLogo() { const logoEl = document.getElementById('site-logo'); const imagesToTry = ['avatar.png', 'logo.png']; function tryNextImage(index) { if (index >= imagesToTry.length) return; // None found const img = new Image(); img.onload = () => { logoEl.src = imagesToTry[index]; logoEl.classList.remove('hidden'); }; img.onerror = () => { tryNextImage(index + 1); }; img.src = imagesToTry[index]; } tryNextImage(0); } // Render dynamic elements based on config function populateUI() { // Populate Text document.getElementById('site-title').innerText = config.site.headerTitle; document.getElementById('site-title').setAttribute('data-text', config.site.headerTitle); document.getElementById('site-intro').innerText = config.site.introText; document.getElementById('site-bottom').innerText = config.site.bottomText; document.getElementById('base-price-display').innerText = `$${config.pricing.basePricePerMinute}`; // Populate Social Links const socialContainer = document.getElementById('social-links'); socialContainer.innerHTML = ''; config.site.socialLinks.forEach(link => { const a = document.createElement('a'); a.href = link.url; a.className = 'social-link'; a.target = '_blank'; a.title = link.label; a.innerHTML = ``; socialContainer.appendChild(a); }); // Populate Difficulties (Base and Extra) const baseDiffDiv = document.getElementById('base-difficulty-options'); const extraDiffDiv = document.getElementById('extra-difficulty-options'); config.pricing.difficulties.forEach((diff, index) => { // Base Check const baseItem = createOptionItem('radio', 'base-diff', diff, index === 0); // Extra Check const extraItem = createOptionItem('checkbox', 'extra-diff', diff, false); baseDiffDiv.appendChild(baseItem); extraDiffDiv.appendChild(extraItem); }); // Populate Lighting const lightingDiv = document.getElementById('lighting-options'); config.pricing.lighting.forEach((light, index) => { lightingDiv.appendChild(createOptionItem('radio', 'lighting', light, index === 0)); }); // Populate Addons const addonsDiv = document.getElementById('addons-options'); if (config.pricing.addons) { config.pricing.addons.forEach(addon => { addonsDiv.appendChild(createOptionItem('checkbox', 'addon', addon, false)); }); } updateSelectionState(); } // Helper to create UI options function createOptionItem(type, name, itemData, isChecked) { const label = document.createElement('label'); label.className = `option-item ${isChecked ? 'active' : ''}`; label.dataset.id = itemData.id; label.dataset.price = itemData.price; const input = document.createElement('input'); input.type = type; input.name = name; input.value = itemData.id; input.checked = isChecked; const indicator = document.createElement('div'); indicator.className = 'indicator'; const content = document.createElement('div'); content.className = 'option-content'; const header = document.createElement('div'); header.className = 'option-header'; const title = document.createElement('span'); title.className = 'option-name'; title.innerText = itemData.name; const price = document.createElement('span'); price.className = 'option-price'; price.innerText = itemData.price === 0 ? 'FREE' : `+$${itemData.price}`; // Base difficulties don't show a price tag in their block because they are included if(name === 'base-diff') price.style.display = 'none'; header.appendChild(title); header.appendChild(price); const desc = document.createElement('span'); desc.className = 'option-description'; desc.innerText = itemData.description; content.appendChild(header); content.appendChild(desc); label.appendChild(input); label.appendChild(indicator); label.appendChild(content); return label; } // Add DOM event listeners function setupEventListeners() { document.getElementById('fetch-btn').addEventListener('click', handleFetchVideo); document.getElementById('youtube-url').addEventListener('keypress', (e) => { if(e.key === 'Enter') handleFetchVideo(); }); // Listen for changes in the form elements const container = document.getElementById('options-container'); container.addEventListener('change', (e) => { if (e.target.tagName === 'INPUT') { updateSelectionState(); calculateTotal(); } }); } function updateSelectionState() { // 1. Get currently selected base difficulty const baseSelected = document.querySelector('input[name="base-diff"]:checked').value; // 2. Loop through Extra Difficulties const extraInputs = document.querySelectorAll('input[name="extra-diff"]'); extraInputs.forEach(input => { const label = input.closest('.option-item'); if (input.value === baseSelected) { // Uncheck and hide/disable input.checked = false; label.style.display = 'none'; } else { label.style.display = 'flex'; } }); // 3. Update active classes for styling document.querySelectorAll('.option-item').forEach(label => { const input = label.querySelector('input'); if (input.checked) { label.classList.add('active'); } else { label.classList.remove('active'); } }); } function handleFetchVideo() { const url = document.getElementById('youtube-url').value; const vid = extractVideoID(url); const errorMsg = document.getElementById('error-message'); if (!vid) { errorMsg.innerText = "Invalid YouTube URL."; errorMsg.classList.remove('hidden'); return; } errorMsg.classList.add('hidden'); document.getElementById('fetch-btn').innerText = "Loading..."; // Create YouTube iframe to fetch duration if (player) { player.destroy(); } player = new YT.Player('yt-player-container', { height: '0', width: '0', videoId: vid, events: { 'onReady': onPlayerReady, 'onError': onPlayerError } }); } function onPlayerReady(event) { // Player is ready, try to get duration currentDurationSec = event.target.getDuration(); if (currentDurationSec === 0) { // Sometimes it needs a slight delay or needs to buffer (cue) event.target.playVideo(); setTimeout(() => { currentDurationSec = event.target.getDuration(); event.target.stopVideo(); updateVideoInfo(); }, 1000); } else { updateVideoInfo(); } } function onPlayerError(event) { const errorMsg = document.getElementById('error-message'); errorMsg.innerText = "Error loading video (Maybe region locked or unplayable embed)."; errorMsg.classList.remove('hidden'); document.getElementById('fetch-btn').innerHTML = 'Fetch Info '; } function updateVideoInfo() { document.getElementById('fetch-btn').innerHTML = ' Loaded'; document.getElementById('video-info').classList.remove('hidden'); document.getElementById('options-container').classList.remove('disabled-until-fetch'); // Try to get title from player data let videoTitle = "Unknown Song"; if (player && player.getVideoData) { const data = player.getVideoData(); if (data && data.title) { videoTitle = data.title; } } document.getElementById('video-title-display').innerText = videoTitle; document.getElementById('video-title-display').title = videoTitle; // Add tooltip for truncated titles document.getElementById('duration-display').innerText = formatTime(currentDurationSec); calculateTotal(); } function calculateTotal() { let extraRates = 0; const summaryItems = []; // Add Extra Difficulties const extraInputs = document.querySelectorAll('input[name="extra-diff"]:checked'); extraInputs.forEach(input => { const itemLabel = input.closest('.option-item').querySelector('.option-name').innerText; extraRates += parseFloat(input.closest('.option-item').dataset.price); summaryItems.push(itemLabel); }); // Add Lighting const lightingInput = document.querySelector('input[name="lighting"]:checked'); if (lightingInput) { const itemLabel = lightingInput.closest('.option-item').querySelector('.option-name').innerText; extraRates += parseFloat(lightingInput.closest('.option-item').dataset.price); if (!itemLabel.includes('Free')) { summaryItems.push(itemLabel); } } // Add Addons const addonInputs = document.querySelectorAll('input[name="addon"]:checked'); addonInputs.forEach(input => { const itemLabel = input.closest('.option-item').querySelector('.option-name').innerText; extraRates += parseFloat(input.closest('.option-item').dataset.price); summaryItems.push(itemLabel); }); // Calculate Total Cost let totalRatePerMinute = config.pricing.basePricePerMinute + extraRates; let totalCost = (totalRatePerMinute / 60) * currentDurationSec; // Check for Discounts in URL const urlParams = new URLSearchParams(window.location.search); const discountPct = urlParams.get('discount_pct'); const discountTotal = urlParams.get('discount_total'); const discountBase = urlParams.get('discount_base'); let discountStr = ""; const discountDiv = document.getElementById('discount-summary'); // Apply discount_base directly to the basePricePerMinute rate let currentBasePricePerMinute = config.pricing.basePricePerMinute; if (discountBase && !isNaN(discountBase)) { const amount = parseFloat(discountBase); currentBasePricePerMinute -= amount; if (currentBasePricePerMinute < 0) currentBasePricePerMinute = 0; discountDiv.innerText = `$${amount.toFixed(2)} Off Base Rate Applied!`; discountDiv.classList.remove('hidden'); discountStr = ` | Base Rate Discount: -$${amount.toFixed(2)}/min`; } // Calculate base cost and total cost with the potentially discounted base rate let baseCost = (currentBasePricePerMinute / 60) * currentDurationSec; totalRatePerMinute = currentBasePricePerMinute + extraRates; totalCost = (totalRatePerMinute / 60) * currentDurationSec; if (discountTotal && !isNaN(discountTotal)) { const amount = parseFloat(discountTotal); totalCost -= amount; if (totalCost < 0) totalCost = 0; // Only overwrite discount summary if base wasn't already set, or append it? // Assuming mutually exclusive discounts for simplicity, but we can just overwrite discountDiv.innerText = `$${amount.toFixed(2)} Off Total Applied!`; discountDiv.classList.remove('hidden'); discountStr = ` | Total Discount: -$${amount.toFixed(2)}`; } else if (discountPct && !isNaN(discountPct)) { const percent = parseFloat(discountPct); if (percent > 0 && percent <= 100) { const discountAmount = totalCost * (percent / 100); totalCost -= discountAmount; discountDiv.innerText = `${percent}% Discount Applied! (-$${discountAmount.toFixed(2)})`; discountDiv.classList.remove('hidden'); discountStr = ` | Discount: ${percent}%`; } else if (!discountBase) { discountDiv.classList.add('hidden'); } } else if (!discountBase) { discountDiv.classList.add('hidden'); } // Base Cost Display document.getElementById('base-cost-display').innerText = `$${baseCost.toFixed(2)}`; // Update Summary Display const baseDiffInput = document.querySelector('input[name="base-diff"]:checked'); const baseDiffName = baseDiffInput ? baseDiffInput.closest('.option-item').querySelector('.option-name').innerText : 'None'; document.getElementById('selected-summary').innerText = `Included: ${baseDiffName} | Extras: ${summaryItems.length ? summaryItems.join(', ') : 'None'} | Rate: $${totalRatePerMinute}/min`; // Update Total Bar document.getElementById('total-price').innerText = `$${totalCost.toFixed(2)}`; // Update PayPal Button const paypalBtn = document.getElementById('paypal-btn'); if (config.site.paypalUser && totalCost > 0) { const amountStr = totalCost.toFixed(2); // Construct order note with video ID and selected options const vid = extractVideoID(document.getElementById('youtube-url').value); const selectedOptions = []; if (baseDiffInput) selectedOptions.push(`Base: ${baseDiffInput.value}`); extraInputs.forEach(input => selectedOptions.push(`Extra: ${input.value}`)); if (lightingInput) selectedOptions.push(`Lighting: ${lightingInput.value}`); addonInputs.forEach(input => selectedOptions.push(`Addon: ${input.value}`)); const orderNote = `Video: ${vid} | Options: ${selectedOptions.join(', ')}${discountStr}`; paypalBtn.href = `https://paypal.me/${config.site.paypalUser}/${amountStr}usd?item_name=${encodeURIComponent(orderNote)}`; paypalBtn.classList.remove('hidden'); } else { paypalBtn.classList.add('hidden'); } }