NovaStar H9 Enhanced
NovaStar H9 Enhanced
Enhanced 9U chassis with 15 input card slots and 10 output card slots. Enhanced model with increased output capacity for larger LED installations.
π‘οΈ 3-Year NovaStar Warranty
π Pricing Notice: Pricing shown above is MAP pricing. Please
contact us if you're a reseller.
βοΈ Modular Configuration
The H9 Enhanced features a modular design - configure it with your choice of input and output cards
π₯ Input Slots:
15
π€ Output Slots:
10
π¬ Layers per Output:
16
π οΈ Configure Your System
Available Input Cards (20)
H_1x DP1.2 Input Card
1x DisplayPort 1.2
$1,127
H_2x DP1.1 Input Card
2x DisplayPort 1.1
$845
H_1x HDMI2.0 Input Card
1x HDMI 2.0
$1,127
H_2x HDMI2.0 Input Card
2x HDMI 2.0
$1,644
H_4x HDMI Input Card
4x HDMI 1.3 (or 2x HDMI 1.4)
$845
H_1xHDMI2.0+1xDP1.2 Input Card
1x HDMI 2.0 + 1x DisplayPort 1.2
$1,798
H_2xHDMI2.0+2xDP1.2 Input Card
2x HDMI 2.0 + 2x DisplayPort 1.2
$2,320
H_1xHDMI2.1+1xDP1.4 (8K@30Hz) Input Card
1x HDMI 2.1 + 1x DisplayPort 1.4 (8K@30Hz)
$3,184
H_1x 12G SDI Input Card
1x 12G-SDI
$1,825
H_4x 3G SDI Input Card
4x 3G-SDI
$1,644
H_4x DVI Input Card
4x DVI
$845
H_4x HDBaseT Input Card
4x HDBaseT
$1,759
H_1x NDI Input Card
1x NDI
$1,399
H_4x Fiber Input Card
4x 10G Fiber (OPT)
$2,999
H_1xST2110 Input Card
1x SMPTE ST 2110 (25G Fiber)
$3,999
H_2x IP Input Card
2x IP Streams (RTSP, GB28181, ONVIF)
$1,479
H_STD I/O Card
Standard I/O Control Card
$649
H_2ΓAudio input +2ΓAudio Output Card
2x Audio Input + 2x Audio Output
$854
H_2xRJ45+1xHDMI1.3 Preview Card (MVR)
2x RJ45 + 1x HDMI 1.3 Multi-Viewer Preview
$1,233
H_2x IP Input Card (RTSP, GB28181, ONVIF)
2x IP Streams (RTSP, GB28181, ONVIF)
$1,479
Available Output Cards (6)
H_16xRJ45+2xFiber Output Card
16x Gigabit Ethernet + 2x 10G Fiber
$1,644
H_20xRJ45 Output Card
20x Gigabit Ethernet
$1,703
H_4x Fiber Output Card
4x 10G Fiber
$3,218
H_1 HDMI2.0 Output Card
1x HDMI 2.0
$1,127
H_4x HDMI Output Card
4x HDMI
$915
H_4 HDBaseT Output Card
4x HDBaseT
$1,759
Why Choose the H9 Enhanced?
β All-in-One Design
Integrated solution saves rack space and simplifies setup
β Professional Features
Advanced processing with low latency and pixel-level calibration
β Maximum Reliability
Backed by 3-year NovaStar warranty plus Olympian LED support
β Proven Performance
Trusted by production companies and rental houses nationwide
Frequently Asked Questions
Q: What warranty is included?
All NovaStar products include a full 3-year manufacturer warranty when purchased from authorized distributors like Olympian LED.
Q: Do you offer volume pricing?
Yes! As the largest NovaStar distributor in the USA, we offer special pricing for bulk orders and repeat customers. Contact our sales team for a custom quote.
Q: Do you have this in stock?
We maintain over $2 million in inventory at our Titusville, Florida warehouse. Most orders ship same-day. Contact us to confirm current availability.
Q: Can I get NET 30 payment terms?
NET 30 terms are available for qualified businesses and established customers. Please contact our sales team to discuss payment options.
Why Buy from Olympian LED?
π
Largest US Distributor
Best pricing guaranteed as the largest NovaStar distributor in America
π¦
Ships Same Day
Over $2M in stock at our Florida warehouse for immediate delivery
π³
Flexible Terms
NET 30 payment terms and volume discounts for qualified businesses
π οΈ
Expert Support
Technical support from our team of LED display specialists
β
Input Slots (15)
Output Slots (10)
Configuration Summary
Total Price:
$2,972
π Add Configuration to Cart
π Share Configuration Link
π Export Configuration Report
ποΈ Clear Configuration
// HDMI connector (icon_030.png)
hdmi: ' ',
// DisplayPort (icon_074.png)
dp: ' ',
// DVI (icon_014.png)
dvi: ' ',
// BNC/SDI (icon_069.png)
sdi: ' ',
// RJ45 Ethernet (actual connector)
rj45: ' ',
// Fiber optic (icon_021.png)
fiber: ' ',
// HDBaseT (uses RJ45 connector)
hdbaset: ' ',
// IP/Network (actual connector icon)
ip: ' ',
// Audio (1/4" or XLR-style circular - icon_109.png)
audio: ' ',
// NDI (uses ethernet/RJ45 since it's network-based)
ndi: ' ',
// ST2110 (icon_091.png)
st2110: ' ',
// Standard I/O (generic connector)
io: ' ',
// Preview/Monitor output
preview: ' '
};
function renderAvailableCards() {
const inputDiv = document.getElementById('availableInputCards');
const outputDiv = document.getElementById('availableOutputCards');
const accessDiv = document.getElementById('availableAccessories');
try {
inputDiv.innerHTML = hSeriesConfig.inputCards.map((card, i) => {
const slug = card.name.toLowerCase().replace(/ /g, '-').replace(/[_+()\u00d7]/g, c => {
if (c === '_') return '-';
if (c === '+') return 'plus';
if (c === '\u00d7') return 'x';
return '';
}).replace(/,/g, '');
return `
${card.name.replace('H_', '')}
${card.port_badges_html || ''}
`;
}).join('');
outputDiv.innerHTML = hSeriesConfig.outputCards.map((card, i) => {
const slug = card.name.toLowerCase().replace(/ /g, '-').replace(/[_+()\u00d7]/g, c => {
if (c === '_') return '-';
if (c === '+') return 'plus';
if (c === '\u00d7') return 'x';
return '';
}).replace(/,/g, '');
return `
${card.name.replace('H_', '')}
${card.port_badges_html || ''}
`;
}).join('');
if (accessDiv && hSeriesConfig.accessories.length > 0) {
accessDiv.innerHTML = hSeriesConfig.accessories.map((acc, i) => {
const slug = acc.name.toLowerCase().replace(/ /g, '-').replace(/[_+()\u00d7]/g, c => {
if (c === '_') return '-';
if (c === '+') return 'plus';
if (c === '\u00d7') return 'x';
return '';
}).replace(/,/g, '');
return `
${acc.name}
${generatePortBadges(acc.name)}
`;
}).join('');
}
} catch (error) {
console.error('β Error in renderAvailableCards:', error);
console.error('Stack:', error.stack);
}
}
function renderSlots() {
try {
// Check if we have a grid layout (like H2)
if (hSeriesConfig.layoutGrid) {
renderGridLayout();
} else {
renderListLayout();
}
} catch (error) {
console.error('β Error in renderSlots:', error);
console.error('Stack:', error.stack);
}
}
function renderGridLayout() {
try {
const inputDiv = document.getElementById('inputSlots');
const outputDiv = document.getElementById('outputSlots');
// Hide the output div and its heading (but not the parent panel)
outputDiv.style.display = 'none';
const outputHeading = outputDiv.previousElementSibling;
if (outputHeading && outputHeading.tagName === 'H3') {
outputHeading.style.display = 'none';
}
// Change the input heading
const inputHeading = inputDiv.previousElementSibling;
if (inputHeading && inputHeading.tagName === 'H3') {
inputHeading.textContent = 'Slot Configuration';
}
// Change inputDiv display from grid to block to allow custom layout
inputDiv.style.display = 'block';
// Check if it's a column-based layout (H5+) or row-based layout (H2)
const isColumnLayout = hSeriesConfig.layoutGrid.some(cell => cell.column);
if (isColumnLayout) {
// Separate into columns
const leftColumn = hSeriesConfig.layoutGrid.filter(cell => cell.column === 'left');
const rightColumn = hSeriesConfig.layoutGrid.filter(cell => cell.column === 'right');
const htmlContent = `
${leftColumn.map(cell => renderGridCell(cell)).join('')}
${rightColumn.map(cell => renderGridCell(cell)).join('')}
`;
inputDiv.innerHTML = htmlContent;
} else {
// Row-based layout (H2)
inputDiv.innerHTML = `
${hSeriesConfig.layoutGrid.map((row, rowIdx) => `
${row.map((cell, colIdx) => renderGridCell(cell, rowIdx, colIdx)).join('')}
`).join('')}
`;
}
} catch (error) {
console.error('β Error in renderGridLayout:', error);
console.error('Stack:', error.stack);
}
}
function renderGridCell(cell, rowIdx, colIdx) {
// Check if this slot is blocked by a double-slot card below (H2 only)
if (hSeriesConfig.frameName === 'H2' && rowIdx !== undefined && colIdx !== undefined) {
const rowBelow = hSeriesConfig.layoutGrid[rowIdx + 1];
if (rowBelow && rowBelow[colIdx]) {
const cellBelow = rowBelow[colIdx];
if (cellBelow.type === 'output' && cellBelow.slot !== undefined) {
const cardBelow = selectedCards.output[cellBelow.slot];
if (cardBelow && cardBelow.slots_required === 2) {
// This slot is blocked by a double-slot card below
return `${cell.label}
β¬οΈ Occupied by ${cardBelow.name.replace('H_', '').substring(0, 15)}...
`;
}
}
}
}
if (cell.type === 'fixed') {
return ``;
}
if (cell.type === 'blank') {
return ``;
}
// Optional slots (like MVR) are now treated like regular input slots but with special styling
if (cell.slot_type === 'optional') {
const card = selectedCards[cell.type][cell.slot];
const slotClass = card ? 'h-slot-filled' : 'h-slot-empty h-slot-optional';
const isPending = pendingCardSelection && pendingCardSelection.type === cell.type && pendingCardSelection.slot === cell.slot;
const pendingClass = isPending ? 'h-slot-pending' : '';
return `
${cell.label}
${card ? `
${card.port_badges_html || ''}
Γ ` : `
${isPending ? 'Slot selected β' : 'Optional MVR'}
`}
`;
}
const card = selectedCards[cell.type][cell.slot];
const slotClass = card ? 'h-slot-filled' : 'h-slot-empty';
const isPending = pendingCardSelection && pendingCardSelection.type === cell.type && pendingCardSelection.slot === cell.slot;
const pendingClass = isPending ? 'h-slot-pending' : '';
// Check if this is a double-slot card (for visual badge)
const doubleSlotClass = (card && card.slots_required === 2) ? 'h-slot-double' : '';
const doubleSlotBadge = (card && card.slots_required === 2) ? '2 SLOTS ' : '';
return `
${cell.label}
${card ? `
${card.port_badges_html || ''}
${doubleSlotBadge}
Γ ` : `
${isPending ? 'Slot selected β' : 'Click to select'}
`}
`;
}
function getSlotLabel(type, slot) {
if (hSeriesConfig.layoutGrid) {
const cell = hSeriesConfig.layoutGrid.find(c => c.type === type && c.slot === slot);
return cell ? cell.label : `${type.toUpperCase()}-${slot + 1}`;
}
if (hSeriesConfig.slotLabels) {
return hSeriesConfig.slotLabels[type][slot];
}
return `${type.toUpperCase()}-${slot + 1}`;
}
function renderListLayout() {
const inputDiv = document.getElementById('inputSlots');
const outputDiv = document.getElementById('outputSlots');
const inputLabels = hSeriesConfig.slotLabels ? hSeriesConfig.slotLabels.input : Array(hSeriesConfig.inputSlots).fill(0).map((_, i) => `I-${i+1}`);
const outputLabels = hSeriesConfig.slotLabels ? hSeriesConfig.slotLabels.output : Array(hSeriesConfig.outputSlots).fill(0).map((_, i) => `O-${i+1}`);
inputDiv.innerHTML = inputLabels.map((label, i) => {
const card = selectedCards.input[i];
const isPending = pendingCardSelection && pendingCardSelection.type === 'input' && pendingCardSelection.slot === i;
const pendingClass = isPending ? 'h-slot-pending' : '';
return `
${label}
${card ? `
${card.port_badges_html || ''}
Γ ` : `
${isPending ? 'Slot selected β' : 'Click to select'}
`}
`;
}).join('');
outputDiv.innerHTML = outputLabels.map((label, i) => {
const card = selectedCards.output[i];
const isPending = pendingCardSelection && pendingCardSelection.type === 'output' && pendingCardSelection.slot === i;
const pendingClass = isPending ? 'h-slot-pending' : '';
return `
${label}
${card ? `
${card.port_badges_html || ''}
Γ ` : `
${isPending ? 'Slot selected β' : 'Click to select'}
`}
`;
}).join('');
}
function promptCardSelection(type, slot) {
pendingCardSelection = { type, slot };
// Highlight the selected slot
renderSlots();
// Highlight and scroll to the appropriate card selection panel
const inputPanel = document.getElementById('inputCardsPanel');
const outputPanel = document.getElementById('outputCardsPanel');
// Remove previous highlights
if (inputPanel) inputPanel.classList.remove('h-cards-panel-highlight');
if (outputPanel) outputPanel.classList.remove('h-cards-panel-highlight');
// Add highlight to the relevant panel
const targetPanel = type === 'input' ? inputPanel : outputPanel;
if (targetPanel) {
targetPanel.classList.add('h-cards-panel-highlight');
targetPanel.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
// Show a message
const slotLabel = getSlotLabel(type, slot);
}
let pendingCardSelection = null;
function clearCardPanelHighlights() {
const inputPanel = document.getElementById('inputCardsPanel');
const outputPanel = document.getElementById('outputCardsPanel');
if (inputPanel) inputPanel.classList.remove('h-cards-panel-highlight');
if (outputPanel) outputPanel.classList.remove('h-cards-panel-highlight');
}
function addCardToSlot(type, cardIndex) {
const card = type === 'input' ? hSeriesConfig.inputCards[cardIndex] : hSeriesConfig.outputCards[cardIndex];
// If we have a pending slot selection, use that
if (pendingCardSelection && pendingCardSelection.type === type) {
selectedCards[type][pendingCardSelection.slot] = card;
pendingCardSelection = null;
clearCardPanelHighlights();
renderSlots();
updateSummary();
saveConfiguration();
return;
}
// Special handling for MVR cards - find the MVR slot
if (card.name.includes('MVR') || card.name.includes('Preview')) {
let mvrSlot = -1;
// Check layoutGrid first
if (hSeriesConfig.layoutGrid) {
const mvrCell = hSeriesConfig.layoutGrid.find(cell =>
cell.type === type && cell.label && cell.label.includes('MVR')
);
if (mvrCell) {
mvrSlot = mvrCell.slot;
}
}
// Fallback to slotLabels
if (mvrSlot === -1 && hSeriesConfig.slotLabels) {
const labels = type === 'input' ? hSeriesConfig.slotLabels.input : hSeriesConfig.slotLabels.output;
mvrSlot = labels.findIndex(label => label && label.includes('MVR'));
}
// If found MVR slot, use it (even if occupied - will alert user)
if (mvrSlot !== -1) {
if (selectedCards[type][mvrSlot]) {
if (!confirm(`MVR slot is occupied by ${selectedCards[type][mvrSlot].name}. Replace it?`)) {
return;
}
}
selectedCards[type][mvrSlot] = card;
clearCardPanelHighlights();
renderSlots();
updateSummary();
saveConfiguration();
return;
}
}
// Otherwise find first empty slot
const emptySlot = selectedCards[type].findIndex(s => !s);
if (emptySlot === -1) {
alert(`All ${type} slots are full! Click on a specific empty slot first to replace a card.`);
return;
}
selectedCards[type][emptySlot] = card;
clearCardPanelHighlights();
renderSlots();
updateSummary();
saveConfiguration();
}
function removeCard(type, slotIndex) {
selectedCards[type][slotIndex] = null;
renderSlots();
updateSummary();
saveConfiguration();
}
let draggedCard = null;
function handleDragStart(event, type, slot) {
draggedCard = {
type: type,
slot: slot,
card: selectedCards[type][slot]
};
event.dataTransfer.effectAllowed = 'move';
event.dataTransfer.setData('text/html', event.target.innerHTML);
event.target.style.opacity = '0.5';
// Highlight only valid drop zones (same type)
document.querySelectorAll('.h-slot').forEach(slotEl => {
const slotType = slotEl.getAttribute('data-type');
const isFixed = slotEl.classList.contains('h-slot-fixed');
const isBlank = slotEl.classList.contains('h-slot-blank');
// Only highlight slots of the same type that aren't fixed or blank
if (slotType === type && !isFixed && !isBlank) {
slotEl.classList.add('h-slot-drop-zone');
}
});
}
function handleDragOver(event) {
if (event.preventDefault) {
event.preventDefault();
}
event.dataTransfer.dropEffect = 'move';
return false;
}
function handleDrop(event, targetType, targetSlot) {
if (event.stopPropagation) {
event.stopPropagation();
}
event.preventDefault();
if (!draggedCard) {
return false;
}
// Prevent dropping input cards in output slots and vice versa
if (draggedCard.type !== targetType) {
alert(`Cannot place an ${draggedCard.type} card into an ${targetType} slot!`);
// Remove drop zone highlights
document.querySelectorAll('.h-slot-drop-zone').forEach(slot => {
slot.classList.remove('h-slot-drop-zone');
});
draggedCard = null;
return false;
}
// Swap or move cards
const sourceCard = draggedCard.card;
const targetCard = selectedCards[targetType][targetSlot];
// Move source card to target
selectedCards[targetType][targetSlot] = sourceCard;
// Clear source slot (or put target card there if swapping)
selectedCards[draggedCard.type][draggedCard.slot] = targetCard;
// Remove drop zone highlights
document.querySelectorAll('.h-slot-drop-zone').forEach(slot => {
slot.classList.remove('h-slot-drop-zone');
});
draggedCard = null;
renderSlots();
updateSummary();
saveConfiguration();
return false;
}
function toggleAccessory(accIndex) {
const acc = hSeriesConfig.accessories[accIndex];
const idx = selectedCards.accessories.findIndex(a => a && a.name === acc.name);
if (idx > -1) {
selectedCards.accessories.splice(idx, 1);
} else {
selectedCards.accessories.push(acc);
}
updateSummary();
saveConfiguration();
}
function updateSummary() {
let total = hSeriesConfig.framePrice;
let inputSubtotal = 0;
let outputSubtotal = 0;
let accessoriesSubtotal = 0;
let summary = `Frame: ${hSeriesConfig.frameName} - $${hSeriesConfig.framePrice.toLocaleString()}
`;
// Input cards
const inputCards = selectedCards.input.filter(c => c);
if (inputCards.length > 0) {
summary += `Input Cards (${inputCards.length}):
`;
inputCards.forEach(card => {
inputSubtotal += card.price;
summary += `${card.name} - $${card.price.toLocaleString()}
`;
});
summary += `Subtotal: $${inputSubtotal.toLocaleString()}
`;
}
// Output cards
const outputCards = selectedCards.output.filter(c => c);
if (outputCards.length > 0) {
summary += `Output Cards (${outputCards.length}):
`;
outputCards.forEach(card => {
outputSubtotal += card.price;
summary += `${card.name} - $${card.price.toLocaleString()}
`;
});
summary += `Subtotal: $${outputSubtotal.toLocaleString()}
`;
}
// Accessories
if (selectedCards.accessories.length > 0) {
summary += `Accessories (${selectedCards.accessories.length}):
`;
selectedCards.accessories.forEach(acc => {
accessoriesSubtotal += acc.price;
summary += `${acc.name} - $${acc.price.toLocaleString()}
`;
});
summary += `Subtotal: $${accessoriesSubtotal.toLocaleString()}
`;
}
// Capacity info
const inputSlotsUsed = selectedCards.input.filter(c => c).length;
const outputSlotsUsed = selectedCards.output.filter(c => c).length;
const totalLayers = outputSlotsUsed * (hSeriesConfig.layersPerOutput || 16);
// Calculate total pixel capacity from output cards
let totalPixelCapacity = 0;
outputCards.forEach(card => {
// Parse RJ45 (Ethernet) ports: 650,000 pixels per port
const rj45Match = card.name.match(/(\d+)x\s*RJ45/i);
if (rj45Match) {
totalPixelCapacity += parseInt(rj45Match[1]) * 650000;
}
// Parse Fiber ports: 5,200,000 pixels per 10G fiber port
const fiberMatch = card.name.match(/(\d+)x\s*Fiber/i);
if (fiberMatch) {
totalPixelCapacity += parseInt(fiberMatch[1]) * 5200000;
}
// Parse HDMI outputs (assume 4K max: 8.3M pixels)
const hdmiMatch = card.name.match(/(\d+)x\s*HDMI/i) || card.name.match(/HDMI.*Output.*Card/i);
if (hdmiMatch) {
const hdmiCount = hdmiMatch[1] ? parseInt(hdmiMatch[1]) : 1;
const hdmiPixels = hdmiCount * 8300000;
totalPixelCapacity += hdmiPixels;
}
// Parse HDBaseT outputs (assume 1080p: 2.1M pixels)
const hdbasetMatch = card.name.match(/(\d+)x?\s*HDBaseT/i);
if (hdbasetMatch) {
const hdbasetCount = parseInt(hdbasetMatch[1]);
const hdbasetPixels = hdbasetCount * 2100000;
totalPixelCapacity += hdbasetPixels;
}
});
summary += `Capacity:
`;
summary += `Input Slots: ${inputSlotsUsed} / ${hSeriesConfig.inputSlots}
`;
summary += `Output Slots: ${outputSlotsUsed} / ${hSeriesConfig.outputSlots}
`;
if (outputSlotsUsed > 0) {
summary += `Total Layers: ${totalLayers}
`;
}
if (totalPixelCapacity > 0) {
summary += `Pixel Capacity: ${(totalPixelCapacity / 1000000).toFixed(1)}M pixels
`;
}
total = hSeriesConfig.framePrice + inputSubtotal + outputSubtotal + accessoriesSubtotal;
document.getElementById('configSummary').innerHTML = summary;
document.getElementById('totalPrice').textContent = `$${total.toLocaleString()}`;
}
function addConfigurationToCart() {
const totalPrice = parseInt(document.getElementById('totalPrice').textContent.replace(/[$,]/g, ''));
if (totalPrice === hSeriesConfig.framePrice) {
alert('Please add at least one card to your configuration before adding to cart.');
return;
}
const config = {
name: `${hSeriesConfig.frameName} - Custom Configuration`,
price: totalPrice,
quantity: 1,
configuration: {
frame: hSeriesConfig.frameName,
inputCards: selectedCards.input.filter(c => c),
outputCards: selectedCards.output.filter(c => c),
accessories: selectedCards.accessories
}
};
// Use the global addToCart function
addToCart(config.name, config.price, 1, JSON.stringify(config.configuration));
alert('β
Configuration added to cart!');
closeHSeriesCustomizer();
}
function exportConfiguration() {
const totalPrice = parseInt(document.getElementById('totalPrice').textContent.replace(/[$,]/g, ''));
if (totalPrice === hSeriesConfig.framePrice) {
alert('Please configure at least one card before exporting.');
return;
}
if (!window.jspdf || !window.jspdf.jsPDF) {
alert('PDF library not loaded. Please try again.');
return;
}
const { jsPDF } = window.jspdf;
const doc = new jsPDF();
// Calculate totals and capacity
const inputSlotsUsed = selectedCards.input.filter(c => c).length;
const outputSlotsUsed = selectedCards.output.filter(c => c).length;
const totalLayers = outputSlotsUsed * (hSeriesConfig.layersPerOutput || 16);
let inputSubtotal = 0;
let outputSubtotal = 0;
let accessoriesSubtotal = 0;
// Calculate pixel capacity
let totalPixelCapacity = 0;
const outputCards = selectedCards.output.filter(c => c);
outputCards.forEach(card => {
// Check if card has max loading capacity in description
if (card.description && card.description.includes('10,400,000')) {
totalPixelCapacity += 10400000;
} else if (card.description && card.description.includes('13,000,000')) {
totalPixelCapacity += 13000000;
} else if (card.description && card.description.includes('20,800,000')) {
totalPixelCapacity += 20800000;
} else {
// Fallback to port-based calculation
const rj45Match = card.name.match(/(\d+)x\s*RJ45/i);
if (rj45Match) {
totalPixelCapacity += parseInt(rj45Match[1]) * 650000;
}
const fiberMatch = card.name.match(/(\d+)x\s*Fiber/i);
if (fiberMatch) {
totalPixelCapacity += parseInt(fiberMatch[1]) * 5200000;
}
}
});
// Load and add logo
const logo = new Image();
logo.onload = function() {
let yPos = 15;
// Add logo
try {
doc.addImage(logo, 'PNG', 15, yPos, 40, 10);
} catch (e) {
// Fallback to text if logo fails
doc.setFontSize(16);
doc.setFont(undefined, 'bold');
doc.text('Olympian LED', 15, yPos + 5);
}
// Header info
doc.setFontSize(10);
doc.setFont(undefined, 'normal');
doc.text(`Generated: ${new Date().toLocaleString()}`, 200, yPos + 5, { align: 'right' });
yPos += 20;
// Title
doc.setFontSize(18);
doc.setFont(undefined, 'bold');
doc.setTextColor(220, 53, 69);
doc.text('H SERIES CONFIGURATION REPORT', 15, yPos);
doc.setTextColor(0, 0, 0);
yPos += 15;
// Frame Configuration Box
doc.setFillColor(240, 240, 240);
doc.rect(15, yPos - 5, 180, 45, 'F');
doc.setFontSize(12);
doc.setFont(undefined, 'bold');
doc.text('Frame Configuration', 20, yPos);
yPos += 8;
doc.setFontSize(10);
doc.setFont(undefined, 'normal');
doc.text(`Model: ${hSeriesConfig.frameName}`, 20, yPos);
yPos += 6;
doc.text(`Rack Size: ${hSeriesConfig.frameName.replace('H', '') + 'U'}`, 20, yPos);
yPos += 6;
doc.text(`Input Slots: ${inputSlotsUsed} / ${hSeriesConfig.inputSlots} used`, 20, yPos);
yPos += 6;
doc.text(`Output Slots: ${outputSlotsUsed} / ${hSeriesConfig.outputSlots} used`, 20, yPos);
yPos += 6;
if (outputSlotsUsed > 0) {
doc.text(`Total Layers: ${totalLayers}`, 20, yPos);
yPos += 6;
}
if (totalPixelCapacity > 0) {
doc.setFont(undefined, 'bold');
doc.setTextColor(220, 53, 69);
doc.text(`Pixel Capacity: ${(totalPixelCapacity / 1000000).toFixed(1)}M pixels`, 20, yPos);
doc.setTextColor(0, 0, 0);
doc.setFont(undefined, 'normal');
}
yPos += 12;
// Input Cards Section
if (selectedCards.input.filter(c => c).length > 0) {
doc.setFontSize(12);
doc.setFont(undefined, 'bold');
doc.text(`Input Cards (${selectedCards.input.filter(c => c).length})`, 15, yPos);
yPos += 2;
const inputData = [];
selectedCards.input.filter(c => c).forEach(card => {
inputSubtotal += card.price;
inputData.push([
card.name.replace('H_', ''),
card.description.substring(0, 50) + (card.description.length > 50 ? '...' : ''),
`$${card.price.toLocaleString()}`
]);
});
doc.autoTable({
startY: yPos,
head: [['Card Name', 'Description', 'Price']],
body: inputData,
theme: 'grid',
headStyles: { fillColor: [220, 53, 69], fontSize: 9 },
bodyStyles: { fontSize: 9 },
margin: { left: 15, right: 15 },
columnStyles: {
0: { cellWidth: 60 },
1: { cellWidth: 90 },
2: { cellWidth: 30, halign: 'right' }
}
});
yPos = doc.lastAutoTable.finalY + 3;
doc.setFontSize(10);
doc.setFont(undefined, 'bold');
doc.text(`Subtotal: $${inputSubtotal.toLocaleString()}`, 195, yPos, { align: 'right' });
yPos += 8;
}
// Output Cards Section
if (selectedCards.output.filter(c => c).length > 0) {
doc.setFontSize(12);
doc.setFont(undefined, 'bold');
doc.text(`Output Cards (${selectedCards.output.filter(c => c).length})`, 15, yPos);
yPos += 2;
const outputData = [];
selectedCards.output.filter(c => c).forEach(card => {
outputSubtotal += card.price;
let desc = card.description.substring(0, 40);
// Add pixel capacity to description if it's a sending card
if (card.description.includes('Gigabit Ethernet') || card.description.includes('Fiber')) {
const cap = card.description.match(/([\d,]+)\s*pixels/);
if (cap) desc += ` (Max: ${cap[1]})`;
}
outputData.push([
card.name.replace('H_', ''),
desc + (card.description.length > 40 ? '...' : ''),
`$${card.price.toLocaleString()}`
]);
});
doc.autoTable({
startY: yPos,
head: [['Card Name', 'Description', 'Price']],
body: outputData,
theme: 'grid',
headStyles: { fillColor: [220, 53, 69], fontSize: 9 },
bodyStyles: { fontSize: 9 },
margin: { left: 15, right: 15 },
columnStyles: {
0: { cellWidth: 60 },
1: { cellWidth: 90 },
2: { cellWidth: 30, halign: 'right' }
}
});
yPos = doc.lastAutoTable.finalY + 3;
doc.setFontSize(10);
doc.setFont(undefined, 'bold');
doc.text(`Subtotal: $${outputSubtotal.toLocaleString()}`, 195, yPos, { align: 'right' });
yPos += 8;
}
// Accessories Section
if (selectedCards.accessories.length > 0) {
doc.setFontSize(12);
doc.setFont(undefined, 'bold');
doc.text(`Accessories (${selectedCards.accessories.length})`, 15, yPos);
yPos += 2;
const accessoryData = [];
selectedCards.accessories.forEach(acc => {
accessoriesSubtotal += acc.price;
accessoryData.push([
acc.name,
acc.description.substring(0, 50) + (acc.description.length > 50 ? '...' : ''),
`$${acc.price.toLocaleString()}`
]);
});
doc.autoTable({
startY: yPos,
head: [['Accessory Name', 'Description', 'Price']],
body: accessoryData,
theme: 'grid',
headStyles: { fillColor: [220, 53, 69], fontSize: 9 },
bodyStyles: { fontSize: 9 },
margin: { left: 15, right: 15 },
columnStyles: {
0: { cellWidth: 60 },
1: { cellWidth: 90 },
2: { cellWidth: 30, halign: 'right' }
}
});
yPos = doc.lastAutoTable.finalY + 3;
doc.setFontSize(10);
doc.setFont(undefined, 'bold');
doc.text(`Subtotal: $${accessoriesSubtotal.toLocaleString()}`, 195, yPos, { align: 'right' });
yPos += 8;
}
// Price Summary Box
yPos += 5;
doc.setFillColor(220, 53, 69);
doc.rect(15, yPos - 5, 180, 30, 'F');
doc.setTextColor(255, 255, 255);
doc.setFontSize(11);
doc.setFont(undefined, 'bold');
doc.text('PRICE SUMMARY', 20, yPos);
yPos += 6;
doc.setFontSize(10);
doc.setFont(undefined, 'normal');
doc.text(`Frame:`, 20, yPos);
doc.text(`$${hSeriesConfig.framePrice.toLocaleString()}`, 190, yPos, { align: 'right' });
yPos += 5;
if (inputSubtotal > 0) {
doc.text(`Input Cards:`, 20, yPos);
doc.text(`$${inputSubtotal.toLocaleString()}`, 190, yPos, { align: 'right' });
yPos += 5;
}
if (outputSubtotal > 0) {
doc.text(`Output Cards:`, 20, yPos);
doc.text(`$${outputSubtotal.toLocaleString()}`, 190, yPos, { align: 'right' });
yPos += 5;
}
if (accessoriesSubtotal > 0) {
doc.text(`Accessories:`, 20, yPos);
doc.text(`$${accessoriesSubtotal.toLocaleString()}`, 190, yPos, { align: 'right' });
yPos += 5;
}
doc.setDrawColor(255, 255, 255);
doc.line(20, yPos, 190, yPos);
yPos += 5;
doc.setFontSize(12);
doc.setFont(undefined, 'bold');
doc.text(`TOTAL:`, 20, yPos);
doc.text(`$${totalPrice.toLocaleString()}`, 190, yPos, { align: 'right' });
doc.setTextColor(0, 0, 0);
// Footer
doc.setFontSize(8);
doc.setFont(undefined, 'normal');
doc.text('For more information visit: novastarled.com', 105, 285, { align: 'center' });
doc.text('Contact: sales@olympianled.com | 321-747-3220', 105, 290, { align: 'center' });
// Save PDF
doc.save(`${hSeriesConfig.frameName.replace(/ /g, '_')}_Configuration.pdf`);
};
logo.onerror = function() {
// Continue without logo
alert('Logo failed to load, generating PDF without logo...');
logo.onload();
};
logo.src = '/logos/LOGO_MSB_BLKBack.png';
}
function shareConfiguration() {
const totalPrice = parseInt(document.getElementById('totalPrice').textContent.replace(/[$,]/g, ''));
if (totalPrice === hSeriesConfig.framePrice) {
alert('Please configure at least one card before sharing.');
return;
}
// Create shareable configuration object
const shareConfig = {
input: selectedCards.input.map((card, i) => card ? { slot: i, name: card.name } : null).filter(c => c),
output: selectedCards.output.map((card, i) => card ? { slot: i, name: card.name } : null).filter(c => c),
accessories: selectedCards.accessories.map(acc => acc.name)
};
// Encode configuration as base64
const configJson = JSON.stringify(shareConfig);
const configEncoded = btoa(configJson);
// Generate shareable URL
const currentUrl = window.location.origin + window.location.pathname;
const shareUrl = `${currentUrl}?config=${configEncoded}`;
// Copy to clipboard
navigator.clipboard.writeText(shareUrl).then(() => {
alert(`Configuration link copied to clipboard!\n\nShare this link:\n${shareUrl}`);
}).catch(() => {
// Fallback if clipboard fails
prompt('Copy this link to share your configuration:', shareUrl);
});
}
function loadSharedConfiguration() {
const urlParams = new URLSearchParams(window.location.search);
const configParam = urlParams.get('config');
if (configParam) {
try {
const configJson = atob(configParam);
const shareConfig = JSON.parse(configJson);
// Map card names back to card objects
shareConfig.input.forEach(item => {
const card = hSeriesConfig.inputCards.find(c => c.name === item.name);
if (card) {
selectedCards.input[item.slot] = card;
} else {
console.warn(`Card not found: ${item.name}`);
}
});
shareConfig.output.forEach(item => {
const card = hSeriesConfig.outputCards.find(c => c.name === item.name);
if (card) {
selectedCards.output[item.slot] = card;
} else {
console.warn(`Card not found: ${item.name}`);
}
});
shareConfig.accessories.forEach(name => {
const acc = hSeriesConfig.accessories.find(a => a.name === name);
if (acc) {
selectedCards.accessories.push(acc);
}
});
return true;
} catch (e) {
console.error('Failed to load shared config:', e);
return false;
}
}
return false;
}
// Auto-open customizer on page load if URL parameter is set
window.addEventListener('DOMContentLoaded', () => {
const urlParams = new URLSearchParams(window.location.search);
const configParam = urlParams.get('config');
// Open customizer if ?configure=true or if there's a shared config
if (urlParams.get('configure') === 'true' || configParam) {
// Small delay to ensure page is fully loaded
setTimeout(() => {
openHSeriesCustomizer();
// Load shared configuration AFTER customizer is open
if (configParam) {
setTimeout(() => {
if (loadSharedConfiguration()) {
// Refresh the UI with loaded config
renderSlots();
updateSummary();
// Save to sessionStorage so it persists
saveConfiguration();
}
}, 100);
}
}, 500);
}
});
// Close modals on outside click
document.addEventListener('click', (e) => {
const customizerModal = document.getElementById('hSeriesCustomizerModal');
const detailsModal = document.getElementById('cardDetailsModal');
if (e.target === customizerModal) closeHSeriesCustomizer();
if (e.target === detailsModal) closeCardDetails();
});
// Close card details modal on Escape key
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && document.getElementById('cardDetailsModal').style.display === 'flex') {
closeCardDetails();
}
});