instant has configurable options, custom too
This commit is contained in:
@@ -3378,6 +3378,7 @@ var wpcf7 = {
|
||||
let configIds = {};
|
||||
let storageSelection = {}; // Track individual drive quantities
|
||||
let isInstantCustomized = false; // Track customization state for instant servers
|
||||
let verifiedDefaults = {};
|
||||
|
||||
// --- Static Fallback Data (Generated from Live Site) ---
|
||||
const FALLBACK_CONFIG_OPTIONS = [
|
||||
@@ -3763,10 +3764,37 @@ var wpcf7 = {
|
||||
|
||||
return { id, label, type, values, formName };
|
||||
}
|
||||
|
||||
async checkStock(configUrl) {
|
||||
let lastError = null;
|
||||
for (const proxy of this.proxies) {
|
||||
try {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), 4000);
|
||||
const response = await fetch(proxy + encodeURIComponent(configUrl), { signal: controller.signal });
|
||||
clearTimeout(timeoutId);
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||
const html = await response.text();
|
||||
const doc = new DOMParser().parseFromString(html, 'text/html');
|
||||
const bodyText = (doc.body?.innerText || '').toLowerCase();
|
||||
const dangerBox = doc.querySelector('.alert-danger');
|
||||
const dangerText = dangerBox ? dangerBox.innerText.toLowerCase() : '';
|
||||
if (bodyText.includes('out of stock') || dangerText.includes('out of stock')) return true;
|
||||
return false;
|
||||
} catch (err) {
|
||||
lastError = err;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const scraper = new WHMCSScraper();
|
||||
|
||||
// Cache for OOS states to avoid repeated checks
|
||||
const oosCache = new Map();
|
||||
let oosCheckInProgress = new Set();
|
||||
|
||||
// Update Technical Specs Section (New Smart Card Layout)
|
||||
function updateSpecs(server) {
|
||||
const set = (id, val) => {
|
||||
@@ -3793,8 +3821,10 @@ var wpcf7 = {
|
||||
set('spec-storage-config-hero', server.storage || 'Configurable');
|
||||
|
||||
// Network
|
||||
const netSpeed = (server.network || '10Gbps').replace('Gbps', '');
|
||||
set('spec-network-speed-hero', netSpeed);
|
||||
const netSrc = verifiedDefaults.network || server.network || '';
|
||||
const netSpeedMatch = netSrc ? netSrc.match(/(\d+)\s*Gbps/i) : null;
|
||||
const netSpeed = netSpeedMatch ? netSpeedMatch[1] : (netSrc ? netSrc.replace('Gbps','') : '');
|
||||
set('spec-network-speed-hero', netSpeed || '-');
|
||||
set('spec-network-bandwidth-hero', server.bandwidth || 'Unmetered');
|
||||
}
|
||||
|
||||
@@ -3804,6 +3834,7 @@ var wpcf7 = {
|
||||
grid.innerHTML = instantServers.map(server => `
|
||||
<div class="server-option instant ${selectedServer?.id === server.id ? 'active' : ''}" data-id="${server.id}" onclick="selectInstantServer('${server.id}')">
|
||||
<div class="server-badge instant">⚡ INSTANT</div>
|
||||
${server.oos ? '<div class="server-badge" style="background:#ef4444;color:#fff;margin-left:8px;">OOS</div>' : ''}
|
||||
<div class="server-title">${server.cpu}</div>
|
||||
<div class="server-specs-row">
|
||||
<span class="spec-pill">${server.cores} cores</span>
|
||||
@@ -3817,6 +3848,80 @@ var wpcf7 = {
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
// Pre-fetch stock data in background for all instant servers
|
||||
prefetchStockData();
|
||||
}
|
||||
|
||||
// Pre-fetch stock data for all instant servers
|
||||
async function prefetchStockData() {
|
||||
const batchSize = 3; // Limit concurrent requests
|
||||
const queue = [...instantServers];
|
||||
|
||||
async function processQueue() {
|
||||
while (queue.length > 0) {
|
||||
const batch = queue.splice(0, batchSize);
|
||||
await Promise.all(batch.map(async (server) => {
|
||||
const url = server.orderUrl;
|
||||
|
||||
// Skip if already cached or currently checking
|
||||
if (oosCache.has(url) || oosCheckInProgress.has(url)) {
|
||||
return;
|
||||
}
|
||||
|
||||
oosCheckInProgress.add(url);
|
||||
|
||||
try {
|
||||
const isOutOfStock = await scraper.checkStock(url);
|
||||
if (isOutOfStock !== server.oos) { // Only update if changed
|
||||
server.oos = isOutOfStock;
|
||||
oosCache.set(url, isOutOfStock);
|
||||
updateServerCardOOS(server); // Update specific card
|
||||
if (selectedServer?.id === server.id) {
|
||||
updateSummary();
|
||||
}
|
||||
} else {
|
||||
oosCache.set(url, isOutOfStock);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Failed to check stock for ${server.id}:`, err);
|
||||
oosCache.set(url, false); // Assume in stock on error
|
||||
} finally {
|
||||
oosCheckInProgress.delete(url);
|
||||
}
|
||||
}));
|
||||
// Small delay between batches to be nice to proxies
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
}
|
||||
}
|
||||
|
||||
processQueue();
|
||||
}
|
||||
|
||||
// Helper to update just the OOS badge on a specific card
|
||||
function updateServerCardOOS(server) {
|
||||
const card = document.querySelector(`.server-option.instant[data-id="${server.id}"]`);
|
||||
if (!card) return;
|
||||
|
||||
const existingBadge = card.querySelector('.server-badge[style*="background:#ef4444"]');
|
||||
|
||||
if (server.oos) {
|
||||
if (!existingBadge) {
|
||||
// Insert badge after the INSTANT badge
|
||||
const instantBadge = card.querySelector('.server-badge.instant');
|
||||
if (instantBadge) {
|
||||
const oosBadge = document.createElement('div');
|
||||
oosBadge.className = 'server-badge';
|
||||
oosBadge.style.cssText = 'background:#ef4444;color:#fff;margin-left:8px;';
|
||||
oosBadge.textContent = 'OOS';
|
||||
instantBadge.after(oosBadge);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (existingBadge) {
|
||||
existingBadge.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Render Custom Servers
|
||||
@@ -3849,6 +3954,8 @@ var wpcf7 = {
|
||||
// Reset Customization State
|
||||
isInstantCustomized = false;
|
||||
document.getElementById('instantOptions').style.display = 'none';
|
||||
const instantContainer = document.getElementById('instantDynamicConfigContainer');
|
||||
if (instantContainer) instantContainer.innerHTML = '';
|
||||
document.getElementById('instantServersGrid').style.display = 'grid'; // Ensure grid is visible
|
||||
|
||||
updateSummary();
|
||||
@@ -3877,59 +3984,43 @@ var wpcf7 = {
|
||||
serversGrid.style.display = 'none';
|
||||
instantOptions.style.display = 'block';
|
||||
|
||||
// Fetch options if container is empty OR if we want to refresh ensures all options are loaded
|
||||
// Checking if container is empty is usually enough unless server changed.
|
||||
// However, to be safe and ensure "all customizable options" are shown as requested:
|
||||
if (container.innerHTML.trim() === '') {
|
||||
loader.style.display = 'flex';
|
||||
loader.style.display = 'flex';
|
||||
container.innerHTML = '';
|
||||
|
||||
configState = {};
|
||||
storageSelection = {};
|
||||
|
||||
try {
|
||||
let scrapeUrl = selectedServer.orderUrl;
|
||||
if (scrapeUrl.includes('&configoption')) {
|
||||
scrapeUrl = scrapeUrl.split('&configoption')[0];
|
||||
}
|
||||
|
||||
// Reset selection state
|
||||
configState = {};
|
||||
storageSelection = {};
|
||||
|
||||
try {
|
||||
// 1. CLEAN THE URL: Remove specific config options to get the full configuration page
|
||||
let scrapeUrl = selectedServer.orderUrl;
|
||||
if (scrapeUrl.includes('&configoption')) {
|
||||
scrapeUrl = scrapeUrl.split('&configoption')[0];
|
||||
}
|
||||
|
||||
// 2. Parse Pre-selected Options from the Original URL
|
||||
const preselected = {};
|
||||
const urlParams = new URLSearchParams(selectedServer.orderUrl);
|
||||
for (const [key, value] of urlParams.entries()) {
|
||||
if (key.startsWith('configoption[')) {
|
||||
// Extract ID: configoption[34] -> 34
|
||||
const idMatch = key.match(/\[(\d+)\]/);
|
||||
if (idMatch) {
|
||||
preselected[idMatch[1]] = value;
|
||||
}
|
||||
const preselected = {};
|
||||
const urlParams = new URLSearchParams(selectedServer.orderUrl);
|
||||
for (const [key, value] of urlParams.entries()) {
|
||||
if (key.startsWith('configoption[')) {
|
||||
const idMatch = key.match(/\[(\d+)\]/);
|
||||
if (idMatch) {
|
||||
preselected[idMatch[1]] = value;
|
||||
}
|
||||
}
|
||||
console.log("Pre-selected options for instant:", preselected);
|
||||
|
||||
// Use the scraper with the Clean URL
|
||||
const optionsData = await scraper.getProductConfig(scrapeUrl, selectedServer);
|
||||
|
||||
if (optionsData && optionsData.options.length > 0) {
|
||||
renderDynamicOptions(optionsData.options, true, preselected);
|
||||
} else {
|
||||
// Fallback if optionsData is null or empty
|
||||
console.warn("No live options found, using fallback.");
|
||||
renderFallbackOptions(container);
|
||||
|
||||
// Add a visual warning
|
||||
const warning = document.createElement('div');
|
||||
warning.style.cssText = "background: #fff3cd; color: #856404; padding: 10px; margin-bottom: 15px; border-radius: 4px; font-size: 0.9rem; text-align: center;";
|
||||
warning.textContent = "Could not load live customization options. Standard configuration shown.";
|
||||
container.insertBefore(warning, container.firstChild);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Error in customize click:", err);
|
||||
renderFallbackOptions(container);
|
||||
} finally {
|
||||
loader.style.display = 'none';
|
||||
}
|
||||
|
||||
const optionsData = await scraper.getProductConfig(scrapeUrl, selectedServer);
|
||||
if (optionsData && optionsData.options.length > 0) {
|
||||
renderDynamicOptions(optionsData.options, true, preselected);
|
||||
} else {
|
||||
renderFallbackOptions(container);
|
||||
const warning = document.createElement('div');
|
||||
warning.style.cssText = "background: #fff3cd; color: #856404; padding: 10px; margin-bottom: 15px; border-radius: 4px; font-size: 0.9rem; text-align: center;";
|
||||
warning.textContent = "Could not load live customization options. Standard configuration shown.";
|
||||
container.insertBefore(warning, container.firstChild);
|
||||
}
|
||||
} catch (err) {
|
||||
renderFallbackOptions(container);
|
||||
} finally {
|
||||
loader.style.display = 'none';
|
||||
}
|
||||
|
||||
updateSummary();
|
||||
@@ -3939,7 +4030,6 @@ var wpcf7 = {
|
||||
async function selectCustomServer(id) {
|
||||
selectedServer = customServers.find(s => s.id === id);
|
||||
renderCustomServers();
|
||||
updateSpecs(selectedServer);
|
||||
|
||||
document.getElementById('instantToggleContainer').style.display = 'none';
|
||||
|
||||
@@ -3965,6 +4055,16 @@ var wpcf7 = {
|
||||
loader.style.display = 'none';
|
||||
|
||||
if (optionsData && optionsData.options.length > 0) {
|
||||
const netOpt = optionsData.options.find(o => o.type === 'network' || (o.label && o.label.toLowerCase().includes('network')) || (o.label && o.label.toLowerCase().includes('uplink')));
|
||||
if (netOpt && netOpt.values && netOpt.values.length > 0) {
|
||||
const defVal = netOpt.values[0];
|
||||
verifiedDefaults.network = defVal.text;
|
||||
configState[netOpt.label] = defVal.price || 0;
|
||||
configIds[netOpt.id] = defVal.id;
|
||||
} else {
|
||||
verifiedDefaults.network = null;
|
||||
}
|
||||
updateSpecs(selectedServer);
|
||||
renderDynamicOptions(optionsData.options);
|
||||
} else {
|
||||
// Fallback if scrape fails
|
||||
@@ -4154,7 +4254,7 @@ var wpcf7 = {
|
||||
if(type === 'ram') {
|
||||
configState['RAM Configuration'] = price;
|
||||
document.getElementById('summaryRam').textContent = name;
|
||||
const specRam = document.getElementById('spec-ram-capacity');
|
||||
const specRam = document.getElementById('spec-ram-capacity-hero');
|
||||
if(specRam) specRam.innerText = name;
|
||||
} else if(type === 'location') {
|
||||
configState['Location'] = price;
|
||||
@@ -4162,7 +4262,7 @@ var wpcf7 = {
|
||||
} else if(type === 'network') {
|
||||
configState['Network'] = price;
|
||||
document.getElementById('summaryNetwork').textContent = name;
|
||||
const specNetwork = document.getElementById('spec-network-speed');
|
||||
const specNetwork = document.getElementById('spec-network-speed-hero');
|
||||
if(specNetwork) specNetwork.innerText = name;
|
||||
}
|
||||
|
||||
@@ -4255,7 +4355,7 @@ var wpcf7 = {
|
||||
document.getElementById('summaryStorage').innerText = summaryText;
|
||||
|
||||
// Also update specs table if visible
|
||||
const specStorage = document.getElementById('spec-storage-config');
|
||||
const specStorage = document.getElementById('spec-storage-config-hero');
|
||||
if(specStorage) specStorage.innerText = summaryParts.length > 0 ? summaryParts.join(' + ') : 'Configurable';
|
||||
|
||||
updateSummary();
|
||||
@@ -4618,7 +4718,7 @@ var wpcf7 = {
|
||||
|
||||
const storageText = storageNames.length > 0 ? storageNames.join(' + ') : 'None';
|
||||
document.getElementById('summaryStorage').textContent = storageText;
|
||||
const specStorage = document.getElementById('spec-storage-config');
|
||||
const specStorage = document.getElementById('spec-storage-config-hero');
|
||||
if(specStorage) specStorage.innerText = storageText;
|
||||
};
|
||||
|
||||
@@ -4645,13 +4745,13 @@ var wpcf7 = {
|
||||
if (opt.type === 'ram') {
|
||||
const cleanName = val.text.replace(/\s?\(.*?\)/, '');
|
||||
document.getElementById('summaryRam').textContent = cleanName;
|
||||
const specRam = document.getElementById('spec-ram-capacity');
|
||||
const specRam = document.getElementById('spec-ram-capacity-hero');
|
||||
if(specRam) specRam.innerText = cleanName;
|
||||
}
|
||||
if (opt.type === 'location') document.getElementById('summaryLocation').textContent = val.text;
|
||||
if (opt.type === 'network') {
|
||||
document.getElementById('summaryNetwork').textContent = val.text;
|
||||
const specNet = document.getElementById('spec-network-speed');
|
||||
const specNet = document.getElementById('spec-network-speed-hero');
|
||||
if(specNet) specNet.innerText = val.text;
|
||||
}
|
||||
|
||||
@@ -4709,7 +4809,7 @@ var wpcf7 = {
|
||||
document.getElementById('summaryStorage').textContent = storageText;
|
||||
|
||||
// Update Tech Specs
|
||||
const specStorage = document.getElementById('spec-storage-config');
|
||||
const specStorage = document.getElementById('spec-storage-config-hero');
|
||||
if(specStorage) specStorage.innerText = storageText;
|
||||
}
|
||||
|
||||
@@ -4764,6 +4864,22 @@ var wpcf7 = {
|
||||
if(countEl) countEl.textContent = `${filled}/${slots.length}`;
|
||||
}
|
||||
|
||||
function buildInstantOrderUrl() {
|
||||
if (!selectedServer || !selectedServer.orderUrl) return null;
|
||||
try {
|
||||
const base = selectedServer.orderUrl.split('&configoption')[0];
|
||||
const u = new URL(base);
|
||||
const p = new URLSearchParams(u.search);
|
||||
Object.entries(configIds).forEach(([id, val]) => {
|
||||
if (id && val) p.set(`configoption[${id}]`, val);
|
||||
});
|
||||
p.set('billingcycle', 'monthly');
|
||||
return u.origin + u.pathname + '?' + p.toString();
|
||||
} catch (e) {
|
||||
return selectedServer.orderUrl;
|
||||
}
|
||||
}
|
||||
|
||||
// Update Summary
|
||||
function updateSummary() {
|
||||
const pill = document.getElementById('summaryHeaderPill');
|
||||
@@ -4811,7 +4927,8 @@ var wpcf7 = {
|
||||
const totalPrice = selectedServer.price + addonPrice;
|
||||
document.getElementById('totalPrice').textContent = selectedServer.currency + totalPrice.toFixed(2);
|
||||
|
||||
btnDeployInstant.href = selectedServer.orderUrl;
|
||||
const customUrl = buildInstantOrderUrl();
|
||||
btnDeployInstant.href = customUrl || selectedServer.orderUrl;
|
||||
|
||||
// Update Button Labels for Customized State
|
||||
const mainLabel = btnDeployInstant.querySelector('.main-label');
|
||||
@@ -4825,9 +4942,9 @@ var wpcf7 = {
|
||||
pill.style.color = 'white';
|
||||
subtext.textContent = 'Ready in 15 minutes';
|
||||
document.getElementById('totalPrice').textContent = selectedServer.currency + selectedServer.price;
|
||||
|
||||
|
||||
btnDeployInstant.href = selectedServer.orderUrl;
|
||||
|
||||
|
||||
// Update Button Labels for Default State
|
||||
const mainLabel = btnDeployInstant.querySelector('.main-label');
|
||||
const subLabel = btnDeployInstant.querySelector('.sub-label');
|
||||
@@ -4863,9 +4980,42 @@ var wpcf7 = {
|
||||
if (selectedServer.configUrl) {
|
||||
configureBtn.href = selectedServer.configUrl;
|
||||
}
|
||||
|
||||
const netEl = document.getElementById('summaryNetwork');
|
||||
if (netEl && (netEl.textContent === '-' || netEl.textContent.trim() === '')) {
|
||||
netEl.textContent = verifiedDefaults.network || netEl.textContent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('btnDeployInstant').addEventListener('click', async (e) => {
|
||||
if (!selectedServer) return;
|
||||
e.preventDefault();
|
||||
const url = isInstantCustomized ? (buildInstantOrderUrl() || selectedServer.orderUrl) : selectedServer.orderUrl;
|
||||
|
||||
// Check cache first for instant OOS response
|
||||
let outOfStock = oosCache.get(url);
|
||||
|
||||
// If not cached, check stock (this should rarely happen now due to pre-fetch)
|
||||
if (outOfStock === undefined) {
|
||||
try {
|
||||
outOfStock = await scraper.checkStock(url);
|
||||
oosCache.set(url, outOfStock);
|
||||
} catch (err) {
|
||||
outOfStock = false;
|
||||
oosCache.set(url, false);
|
||||
}
|
||||
}
|
||||
|
||||
if (outOfStock) {
|
||||
selectedServer.oos = true;
|
||||
renderInstantServers();
|
||||
updateSummary();
|
||||
} else {
|
||||
window.location.href = url;
|
||||
}
|
||||
});
|
||||
|
||||
// Tab switching
|
||||
document.querySelectorAll('.config-tab').forEach(tab => {
|
||||
tab.addEventListener('click', () => {
|
||||
@@ -4886,10 +5036,13 @@ var wpcf7 = {
|
||||
renderInstantServers();
|
||||
renderCustomServers();
|
||||
|
||||
// Trigger initial OOS check for all instant servers on page load
|
||||
prefetchStockData();
|
||||
|
||||
// Pre-select the first instant server so the UI is active immediately
|
||||
if(instantServers.length > 0) {
|
||||
selectInstantServer(instantServers[0].id);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user