Initial commit of complete Beat Saber Pricing Website
This commit is contained in:
commit
ac0249ceb9
5 changed files with 1232 additions and 0 deletions
68
README.md
Normal file
68
README.md
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
# Beat Saber Custom Mapping Pricing
|
||||||
|
|
||||||
|
A modern, responsive, configuration-driven pricing calculator for Beat Saber custom mapping commissions. Simply paste a YouTube URL and the tool automatically grabs the actual duration to exactly calculate standard and fractional pricing dynamically.
|
||||||
|
|
||||||
|
It outputs a pre-filled PayPal.Me link with the final cost and an itemized order note tracking the requested map and selected options!
|
||||||
|
|
||||||
|
## Features
|
||||||
|
* **YouTube Ingestion:** Invisible IFrame API queries the video and extracts exact duration down to the second.
|
||||||
|
* **Exact Fractional Pricing:** `(Base Price Per Minute + Selected Extras Per Minute) / 60 * Total Seconds`
|
||||||
|
* **Included Difficulty Logic:** Your Base Price always accounts for exactly 1 included difficulty. When a user selects their "Base Difficulty", it is prevented from being added as an "Extra" paid difficulty.
|
||||||
|
* **Smart Discounts:** Support for Percentages, Flat Total, and Flat Base discounts using easy URL query parameters.
|
||||||
|
* **Glassmorphism Aesthetic:** Animated, glowing neon red and blue UI styling without external bloated CSS frameworks.
|
||||||
|
* **Fully Configured by JSON:** You do not need to rewrite HTML to change descriptions, names, social links, or complex pricing logic.
|
||||||
|
|
||||||
|
## How to Set Up
|
||||||
|
Due to browser CORS permissions, the system cannot fetch the localized `config.json` configuration file simply by double-clicking `index.html`. It must be run on a local HTTP server or hosted on an active website.
|
||||||
|
|
||||||
|
**To run locally for testing:**
|
||||||
|
```bash
|
||||||
|
python3 -m http.server 8000
|
||||||
|
```
|
||||||
|
Then visit `http://localhost:8000` in your browser.
|
||||||
|
|
||||||
|
## Configuration Options (`config.json`)
|
||||||
|
|
||||||
|
All primary values are declared inside `config.json`:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"site": {
|
||||||
|
"headerTitle": "Your custom title",
|
||||||
|
"introText": "Your custom introduction rules/text",
|
||||||
|
"bottomText": "Your custom footer disclaimer text",
|
||||||
|
"socialLinks": [
|
||||||
|
{
|
||||||
|
"icon": "fa-brands fa-discord", // FontAwesome CSS classes
|
||||||
|
"url": "https://discord.com",
|
||||||
|
"label": "Discord"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"paypalUser": "YourPaypalMeUsername" // E.g., paypal.me/Joetastic
|
||||||
|
},
|
||||||
|
"pricing": {
|
||||||
|
"basePricePerMinute": 15, // The $ rate per minute of mapping
|
||||||
|
"difficulties": [...], // Available difficulties options
|
||||||
|
"lighting": [...], // Available lighting tiers
|
||||||
|
"addons": [...] // Misc options
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
*For Difficulties, Lighting, and Addons, each object accepts:*
|
||||||
|
* `id`: The programmatic reference ID
|
||||||
|
* `name`: The user-facing display name
|
||||||
|
* `description`: The subtext beneath the option in the UI
|
||||||
|
* `price`: The cost of the extra. Note: For Difficulties, this price is per *minute*. It is added to the base rate *before* the duration calculation happens.
|
||||||
|
|
||||||
|
## Applying Custom Discounts (`?discount=`)
|
||||||
|
|
||||||
|
You can generate special links to automatically apply discounts when users load the page. It will prominently display a green banner explaining the discount to the user and recalculate the final cost before creating the tracked PayPal link.
|
||||||
|
|
||||||
|
* `?discount_pct=20` Takes **20% Off** the final calculated total cost.
|
||||||
|
* `?discount_total=10` Takes a flat **$10.00 Off** the final calculated total cost.
|
||||||
|
* `?discount_base=5` Deducts **$5.00** from your standard `basePricePerMinute` configuration *before* any duration or extras logic is calculated.
|
||||||
|
|
||||||
|
**Example Link:**
|
||||||
|
```
|
||||||
|
http://localhost:8000/?discount_pct=15
|
||||||
|
```
|
||||||
111
config.json
Normal file
111
config.json
Normal file
|
|
@ -0,0 +1,111 @@
|
||||||
|
{
|
||||||
|
"site": {
|
||||||
|
"headerTitle": "Beat Saber Custom Mapping",
|
||||||
|
"introText": "Get a custom map for your favorite song. Paste a YouTube URL to get started and see estimated pricing based on song length, difficulties, and custom lighting.",
|
||||||
|
"bottomText": "Pricing is an estimate. Final cost may vary based on song complexity. Contact me for details.",
|
||||||
|
"socialLinks": [
|
||||||
|
{
|
||||||
|
"icon": "fa-brands fa-discord",
|
||||||
|
"url": "https://discord.com",
|
||||||
|
"label": "Discord"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"icon": "fa-brands fa-twitter",
|
||||||
|
"url": "https://twitter.com",
|
||||||
|
"label": "Twitter"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"icon": "fa-brands fa-youtube",
|
||||||
|
"url": "https://youtube.com",
|
||||||
|
"label": "YouTube"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"paypalUser": "Joetastic"
|
||||||
|
},
|
||||||
|
"pricing": {
|
||||||
|
"basePricePerMinute": 40,
|
||||||
|
"difficulties": [
|
||||||
|
{
|
||||||
|
"id": "easy",
|
||||||
|
"name": "Easy",
|
||||||
|
"description": "Slow, relaxed mapping perfect for beginners.",
|
||||||
|
"price": 15,
|
||||||
|
"tier": "low"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "normal",
|
||||||
|
"name": "Normal",
|
||||||
|
"description": "A step up in speed, introducing basic patterns.",
|
||||||
|
"price": 15,
|
||||||
|
"tier": "low"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "hard",
|
||||||
|
"name": "Hard",
|
||||||
|
"description": "Faster pacing with more complex rhythmic patterns.",
|
||||||
|
"price": 20,
|
||||||
|
"tier": "high"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "expert",
|
||||||
|
"name": "Expert",
|
||||||
|
"description": "Challenging patterns requiring good flow and stamina.",
|
||||||
|
"price": 20,
|
||||||
|
"tier": "high"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "expertPlus",
|
||||||
|
"name": "Expert+",
|
||||||
|
"description": "Maximum difficulty. Fast, complex, and stamina-draining.",
|
||||||
|
"price": 20,
|
||||||
|
"tier": "expert"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"lighting": [
|
||||||
|
{
|
||||||
|
"id": "automated",
|
||||||
|
"name": "Automated (Free)",
|
||||||
|
"description": "Basic auto-generated lights using Beat Saber's default system.",
|
||||||
|
"price": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "standard",
|
||||||
|
"name": "Standard Custom",
|
||||||
|
"description": "Hand-crafted lighting events synced nicely to the music.",
|
||||||
|
"price": 20
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "chromaBasic",
|
||||||
|
"name": "Chroma+ / Basic V3",
|
||||||
|
"description": "Custom colors and basic V3 environment enhancements.",
|
||||||
|
"price": 30
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "chromaAdvanced",
|
||||||
|
"name": "Chroma++ / Advanced V3",
|
||||||
|
"description": "Extensive use of Chroma features, lasers, and complex V3 environment manipulation.",
|
||||||
|
"price": 40
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"addons": [
|
||||||
|
{
|
||||||
|
"id": "oneSaber",
|
||||||
|
"name": "One Saber",
|
||||||
|
"description": "Add a dedicated One Saber difficulty.",
|
||||||
|
"price": 10
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "noArrows",
|
||||||
|
"name": "No Arrows",
|
||||||
|
"description": "Add a No Arrows difficulty.",
|
||||||
|
"price": 5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "customColors",
|
||||||
|
"name": "Custom Color Scheme",
|
||||||
|
"description": "Specific player and environment colors chosen for the map.",
|
||||||
|
"price": 5
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
137
index.html
Normal file
137
index.html
Normal file
|
|
@ -0,0 +1,137 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Custom Mapping Pricing</title>
|
||||||
|
|
||||||
|
<!-- Google Fonts: Outfit for a modern, geometric look -->
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600;800&display=swap" rel="stylesheet">
|
||||||
|
|
||||||
|
<!-- Font Awesome for Social Icons -->
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||||
|
|
||||||
|
<!-- Custom CSS -->
|
||||||
|
<link rel="stylesheet" href="styles.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<!-- Background Animated Elements -->
|
||||||
|
<div class="bg-glow bg-blue"></div>
|
||||||
|
<div class="bg-glow bg-red"></div>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
|
||||||
|
<!-- Dynamic Header Section -->
|
||||||
|
<header class="glass-panel text-center">
|
||||||
|
<h1 id="site-title" class="glitch-text" data-text="Loading...">Loading...</h1>
|
||||||
|
<p id="site-intro" class="subtitle">Fetching configuration...</p>
|
||||||
|
|
||||||
|
<div id="social-links" class="social-container">
|
||||||
|
<!-- Social links injected here -->
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- Main Content Grid -->
|
||||||
|
<main class="grid-content">
|
||||||
|
|
||||||
|
<!-- Video Input / Duration Section -->
|
||||||
|
<section class="glass-panel main-section">
|
||||||
|
<h2><i class="fa-brands fa-youtube icon-red"></i> Song Source</h2>
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="text" id="youtube-url" placeholder="Paste YouTube URL here..." autocomplete="off">
|
||||||
|
<button id="fetch-btn" class="neon-btn neon-blue">Fetch Info <i class="fa-solid fa-arrow-right"></i></button>
|
||||||
|
</div>
|
||||||
|
<div class="video-info hidden" id="video-info">
|
||||||
|
<div class="info-row">
|
||||||
|
<span class="label">Song:</span>
|
||||||
|
<span class="value yt-title" id="video-title-display">Loading...</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-row">
|
||||||
|
<span class="label">Duration:</span>
|
||||||
|
<span class="value" id="duration-display">0:00</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-row highlight">
|
||||||
|
<span class="label">Base Cost (<span id="base-price-display">$0</span>/min):</span>
|
||||||
|
<span class="value neon-text-blue" id="base-cost-display">$0.00</span>
|
||||||
|
</div>
|
||||||
|
<p class="help-text">The base cost covers the mapping time and includes exactly ONE difficulty of your choice.</p>
|
||||||
|
</div>
|
||||||
|
<div id="error-message" class="error-msg hidden"></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Options Container -->
|
||||||
|
<div class="options-grid disabled-until-fetch" id="options-container">
|
||||||
|
|
||||||
|
<!-- Base Difficulty Selection -->
|
||||||
|
<section class="glass-panel">
|
||||||
|
<h2><i class="fa-solid fa-cube icon-blue"></i> Included Difficulty</h2>
|
||||||
|
<p class="section-desc">Select the difficulty included in your base price.</p>
|
||||||
|
<div id="base-difficulty-options" class="option-list">
|
||||||
|
<!-- Radios injected here -->
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Extra Difficulties Selection -->
|
||||||
|
<section class="glass-panel">
|
||||||
|
<h2><i class="fa-solid fa-cubes icon-red"></i> Extra Difficulties</h2>
|
||||||
|
<p class="section-desc">Add additional difficulties to your map.</p>
|
||||||
|
<div id="extra-difficulty-options" class="option-list">
|
||||||
|
<!-- Checkboxes injected here -->
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Lighting Selection -->
|
||||||
|
<section class="glass-panel">
|
||||||
|
<h2><i class="fa-solid fa-lightbulb icon-blue"></i> Lighting Options</h2>
|
||||||
|
<div id="lighting-options" class="option-list">
|
||||||
|
<!-- Radios injected here -->
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Addons Selection -->
|
||||||
|
<section class="glass-panel">
|
||||||
|
<h2><i class="fa-solid fa-puzzle-piece icon-red"></i> Extras / Addons</h2>
|
||||||
|
<div id="addons-options" class="option-list">
|
||||||
|
<!-- Checkboxes injected here -->
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<!-- Bottom Dynamic Text -->
|
||||||
|
<footer class="footer-text text-center">
|
||||||
|
<p id="site-bottom">Loading...</p>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Sticky Total Pricing Bar -->
|
||||||
|
<div class="total-bar glass-panel-heavy">
|
||||||
|
<div class="total-content">
|
||||||
|
<div class="total-info">
|
||||||
|
<div class="total-label">Estimated Total</div>
|
||||||
|
<div class="selected-summary" id="selected-summary">Rate: $0/min</div>
|
||||||
|
<div class="discount-summary hidden" id="discount-summary">10% Discount Applied!</div>
|
||||||
|
</div>
|
||||||
|
<div class="price-container">
|
||||||
|
<div class="total-price neon-text-red" id="total-price">$0.00</div>
|
||||||
|
<a href="#" id="paypal-btn" class="neon-btn checkout-btn hidden" target="_blank">
|
||||||
|
<i class="fa-brands fa-paypal"></i> Pay via PayPal
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Hidden YouTube Player for API -->
|
||||||
|
<div id="yt-player-container" style="display: none;"></div>
|
||||||
|
|
||||||
|
<!-- Scripts -->
|
||||||
|
<script src="https://www.youtube.com/iframe_api"></script>
|
||||||
|
<script src="script.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
386
script.js
Normal file
386
script.js
Normal file
|
|
@ -0,0 +1,386 @@
|
||||||
|
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');
|
||||||
|
}
|
||||||
|
}
|
||||||
530
styles.css
Normal file
530
styles.css
Normal file
|
|
@ -0,0 +1,530 @@
|
||||||
|
:root {
|
||||||
|
--bg-dark: #090B10;
|
||||||
|
--bg-panel: rgba(18, 22, 36, 0.7);
|
||||||
|
--bg-panel-heavy: rgba(18, 22, 36, 0.95);
|
||||||
|
--neon-blue: #00E5FF;
|
||||||
|
--neon-red: #FF0055;
|
||||||
|
--text-main: #FFFFFF;
|
||||||
|
--text-muted: #A0AABF;
|
||||||
|
--text-dark: #121A2F;
|
||||||
|
|
||||||
|
--border-radius: 12px;
|
||||||
|
--transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Outfit', sans-serif;
|
||||||
|
background-color: var(--bg-dark);
|
||||||
|
color: var(--text-main);
|
||||||
|
line-height: 1.6;
|
||||||
|
min-height: 100vh;
|
||||||
|
position: relative;
|
||||||
|
overflow-x: hidden;
|
||||||
|
padding-bottom: 100px; /* Space for total bar */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Background Glows */
|
||||||
|
.bg-glow {
|
||||||
|
position: fixed;
|
||||||
|
width: 50vw;
|
||||||
|
height: 50vw;
|
||||||
|
border-radius: 50%;
|
||||||
|
filter: blur(150px);
|
||||||
|
z-index: -1;
|
||||||
|
opacity: 0.15;
|
||||||
|
animation: float 10s infinite alternate ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-blue {
|
||||||
|
background: var(--neon-blue);
|
||||||
|
top: -10%;
|
||||||
|
left: -10%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-red {
|
||||||
|
background: var(--neon-red);
|
||||||
|
bottom: -10%;
|
||||||
|
right: -10%;
|
||||||
|
animation-delay: -5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes float {
|
||||||
|
0% { transform: translateY(0) scale(1); }
|
||||||
|
100% { transform: translateY(50px) scale(1.1); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Layout */
|
||||||
|
.container {
|
||||||
|
max-width: 900px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Glass Panels */
|
||||||
|
.glass-panel {
|
||||||
|
background: var(--bg-panel);
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
padding: 2rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
box-shadow: 0 10px 30px rgba(0,0,0,0.5);
|
||||||
|
transition: var(--transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.glass-panel:hover {
|
||||||
|
border-color: rgba(255, 255, 255, 0.1);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.glass-panel-heavy {
|
||||||
|
background: var(--bg-panel-heavy);
|
||||||
|
backdrop-filter: blur(25px);
|
||||||
|
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Typography */
|
||||||
|
h1.glitch-text {
|
||||||
|
font-size: 3rem;
|
||||||
|
font-weight: 800;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
background: linear-gradient(90deg, var(--neon-blue), var(--neon-red));
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 1.1rem;
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 0 auto 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-desc {
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 0.95rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-center { text-align: center; }
|
||||||
|
.hidden { display: none !important; }
|
||||||
|
|
||||||
|
/* Colors & Neons */
|
||||||
|
.icon-blue { color: var(--neon-blue); text-shadow: 0 0 10px var(--neon-blue); }
|
||||||
|
.icon-red { color: var(--neon-red); text-shadow: 0 0 10px var(--neon-red); }
|
||||||
|
.neon-text-blue { color: var(--neon-blue); text-shadow: 0 0 8px rgba(0,229,255,0.6); font-weight: 800; }
|
||||||
|
.neon-text-red { color: var(--neon-red); text-shadow: 0 0 8px rgba(255,0,85,0.6); font-weight: 800; }
|
||||||
|
|
||||||
|
/* Social Links */
|
||||||
|
.social-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 1.5rem;
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-link {
|
||||||
|
color: var(--text-main);
|
||||||
|
font-size: 1.5rem;
|
||||||
|
transition: var(--transition);
|
||||||
|
text-decoration: none;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-link:hover {
|
||||||
|
opacity: 1;
|
||||||
|
color: var(--neon-blue);
|
||||||
|
transform: scale(1.2);
|
||||||
|
text-shadow: 0 0 10px var(--neon-blue);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Input Group */
|
||||||
|
.input-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="text"] {
|
||||||
|
flex: 1;
|
||||||
|
background: rgba(0,0,0,0.3);
|
||||||
|
border: 1px solid rgba(255,255,255,0.1);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
color: white;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-family: inherit;
|
||||||
|
transition: var(--transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="text"]:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--neon-blue);
|
||||||
|
box-shadow: 0 0 15px rgba(0,229,255,0.2);
|
||||||
|
background: rgba(0,0,0,0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Buttons */
|
||||||
|
.neon-btn {
|
||||||
|
background: transparent;
|
||||||
|
border: 2px solid var(--neon-blue);
|
||||||
|
color: var(--neon-blue);
|
||||||
|
padding: 1rem 2rem;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: var(--transition);
|
||||||
|
text-shadow: 0 0 5px var(--neon-blue);
|
||||||
|
box-shadow: inset 0 0 10px rgba(0,229,255,0.1), 0 0 10px rgba(0,229,255,0.1);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.neon-btn:hover {
|
||||||
|
background: var(--neon-blue);
|
||||||
|
color: var(--text-dark);
|
||||||
|
box-shadow: 0 0 20px var(--neon-blue);
|
||||||
|
text-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.neon-btn:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
box-shadow: none;
|
||||||
|
border-color: var(--text-muted);
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Video Info */
|
||||||
|
.video-info {
|
||||||
|
background: rgba(0,0,0,0.3);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 1.5rem;
|
||||||
|
border: 1px solid rgba(255,255,255,0.05);
|
||||||
|
animation: fadeIn 0.4s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-row.highlight {
|
||||||
|
font-size: 1.3rem;
|
||||||
|
margin-top: 1rem;
|
||||||
|
padding-top: 1rem;
|
||||||
|
border-top: 1px dashed rgba(255,255,255,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-title {
|
||||||
|
color: var(--neon-blue);
|
||||||
|
font-weight: 600;
|
||||||
|
max-width: 60%;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.help-text {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-msg {
|
||||||
|
color: var(--neon-red);
|
||||||
|
background: rgba(255,0,85,0.1);
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid rgba(255,0,85,0.3);
|
||||||
|
margin-top: 1rem;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
animation: fadeIn 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Options Grid */
|
||||||
|
.options-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 1.5rem;
|
||||||
|
transition: opacity 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.options-grid .glass-panel {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.disabled-until-fetch {
|
||||||
|
opacity: 0.4;
|
||||||
|
pointer-events: none;
|
||||||
|
filter: grayscale(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Option Items (Radio / Checkbox) */
|
||||||
|
.option-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.option-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
padding: 1rem;
|
||||||
|
background: rgba(0,0,0,0.2);
|
||||||
|
border: 1px solid rgba(255,255,255,0.05);
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: var(--transition);
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.option-item:hover {
|
||||||
|
background: rgba(255,255,255,0.05);
|
||||||
|
transform: translateX(4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.option-item input {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.option-content {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.option-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.option-name {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 1.05rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.option-price {
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
color: var(--neon-blue);
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.option-description {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Checkbox/Radio Indicator */
|
||||||
|
.indicator {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border: 2px solid var(--text-muted);
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-right: 1rem;
|
||||||
|
margin-top: 2px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
transition: var(--transition);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="radio"] + .indicator {
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:checked + .indicator {
|
||||||
|
border-color: var(--neon-blue);
|
||||||
|
background: rgba(0,229,255,0.2);
|
||||||
|
box-shadow: 0 0 10px rgba(0,229,255,0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="radio"]:checked + .indicator::after {
|
||||||
|
content: '';
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
background: var(--neon-blue);
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="checkbox"]:checked + .indicator::after {
|
||||||
|
content: '✓';
|
||||||
|
color: var(--neon-blue);
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Glow Borders for Active Items */
|
||||||
|
.option-item.active {
|
||||||
|
border-color: rgba(0,229,255,0.5);
|
||||||
|
background: rgba(0,229,255,0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Base vs Extra Highlights */
|
||||||
|
#base-difficulty-options .option-item.active {
|
||||||
|
border-color: rgba(0,229,255,0.5);
|
||||||
|
}
|
||||||
|
#extra-difficulty-options .option-item.active {
|
||||||
|
border-color: rgba(255,0,85,0.5);
|
||||||
|
}
|
||||||
|
#extra-difficulty-options input:checked + .indicator {
|
||||||
|
border-color: var(--neon-red);
|
||||||
|
background: rgba(255,0,85,0.2);
|
||||||
|
box-shadow: 0 0 10px rgba(255,0,85,0.4);
|
||||||
|
}
|
||||||
|
#extra-difficulty-options input[type="checkbox"]:checked + .indicator::after {
|
||||||
|
color: var(--neon-red);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Footer Section */
|
||||||
|
.footer-text {
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
opacity: 0.6;
|
||||||
|
margin-top: 2rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Total Sticky Bar */
|
||||||
|
.total-bar {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
padding: 1.5rem 2rem;
|
||||||
|
z-index: 100;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
box-shadow: 0 -10px 40px rgba(0,0,0,0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.total-content {
|
||||||
|
max-width: 900px;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkout-btn {
|
||||||
|
font-size: 1rem;
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
text-decoration: none;
|
||||||
|
background: var(--neon-blue);
|
||||||
|
color: var(--text-dark);
|
||||||
|
border: none;
|
||||||
|
box-shadow: 0 0 15px var(--neon-blue);
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkout-btn:hover {
|
||||||
|
background: white;
|
||||||
|
color: var(--text-dark);
|
||||||
|
box-shadow: 0 0 25px white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkout-btn i {
|
||||||
|
margin-right: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.total-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-summary {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
max-width: 500px;
|
||||||
|
line-height: 1.3;
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.discount-summary {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
color: #00E676; /* Green for discount */
|
||||||
|
font-weight: 600;
|
||||||
|
margin-top: 0.2rem;
|
||||||
|
animation: fadeIn 0.3s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.total-label {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.total-price {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animations */
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from { opacity: 0; transform: translateY(-10px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.options-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-group {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.neon-btn {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1.glitch-text {
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.total-label {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.total-price {
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue