386 lines
13 KiB
JavaScript
386 lines
13 KiB
JavaScript
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();
|
|
} 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');
|
|
}
|
|
}
|
|
|
|
// 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 = `<i class="${link.icon}"></i>`;
|
|
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 <i class="fa-solid fa-arrow-right"></i>';
|
|
}
|
|
|
|
function updateVideoInfo() {
|
|
document.getElementById('fetch-btn').innerHTML = '<i class="fa-solid fa-check"></i> 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');
|
|
}
|
|
}
|