NovaStar H15

Olympian LED - NovaStar LED
πŸ“ž 321-747-3220 M-F 10AM-5PM EST
πŸ›’ Cart

NovaStar H15

$6,438
πŸ›‘οΈ 3-Year NovaStar Warranty
πŸ“‹ Pricing Notice: Pricing shown above is MAP pricing. Please contact us if you're a reseller.
πŸ“·
Image Coming Soon

βš™οΈ Modular Configuration

The H15 features a modular design - configure it with your choice of input and output cards

πŸ“₯ Input Slots: 30
πŸ“€ Output Slots: 10
🎬 Layers per Output: 16
⚑ Redundant Power:Yes

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

Technical Specifications

Video Processing

Layers160
Maximum Pixels208 million pixels

πŸ“Š HDMI 2.1 Resolution & Format Capabilities

Resolution Color Space Sampling Bit Depth Supported Frame Rates (Hz)
β€” 8192Γ—4320@30Hz
RGB 4:4:4 8bit 30
YCbCr 4:4:4 8bit 30
YCbCr 4:2:2 8bit 30
RGB 4:4:4 10bit 30
YCbCr 4:4:4 10bit 30
YCbCr 4:2:2 10bit 30

πŸ“Š HDMI 2.0 Resolution & Format Capabilities

Resolution Color Space Sampling Bit Depth Supported Frame Rates (Hz)
β€” 4096Γ—2160@60Hz
RGB 4:4:4 8bit 60
YCbCr 4:4:4 8bit 60
YCbCr 4:2:2 8bit 60
β€” 4096Γ—2160@30Hz
RGB 4:4:4 10bit 30
YCbCr 4:4:4 10bit 30
β€” 4096Γ—2160@60Hz
YCbCr 4:2:2 10bit 60

πŸ“Š DP 1.4 Resolution & Format Capabilities

Resolution Color Space Sampling Bit Depth Supported Frame Rates (Hz)
β€” 7680Γ—4320@30Hz
RGB 4:4:4 8bit 30
YCbCr 4:4:4 8bit 30
β€” 8192Γ—4320@30Hz
YCbCr 4:2:2 8bit 30
YCbCr 4:2:2 10bit 30
β€” 7680Γ—4320@24Hz
RGB 4:4:4 10bit 24
YCbCr 4:4:4 10bit 24

πŸ“Š DP 1.2 Resolution & Format Capabilities

Resolution Color Space Sampling Bit Depth Supported Frame Rates (Hz)
β€” 4096Γ—2160@60Hz
RGB 4:4:4 8bit 60
YCbCr 4:4:4 8bit 60
YCbCr 4:2:2 8bit 60
β€” 4096Γ—2160@30Hz
RGB 4:4:4 10bit 30
YCbCr 4:4:4 10bit 30
β€” 4096Γ—2160@60Hz
YCbCr 4:2:2 10bit 60

πŸ“Š HDMI 1.4 / DP 1.1 Resolution & Format Capabilities

Resolution Color Space Sampling Bit Depth Supported Frame Rates (Hz)
β€” 4096Γ—1080@60Hz
RGB 4:4:4 8bit 60
YCbCr 4:4:4 8bit 60
YCbCr 4:2:2 8bit 60
β€” 2048Γ—1152@60Hz
RGB 4:4:4 10bit 60
YCbCr 4:4:4 10bit 60
β€” 4096Γ—1080@60Hz
YCbCr 4:2:2 10bit 60

πŸ“Š HDMI 1.3 Resolution & Format Capabilities

Resolution Color Space Sampling Bit Depth Supported Frame Rates (Hz)
β€” 2048Γ—1152@60Hz
RGB 4:4:4 8bit 60
YCbCr 4:4:4 8bit 60
YCbCr 4:2:2 8bit 60
RGB 4:4:4 10bit 60
YCbCr 4:4:4 10bit 60
YCbCr 4:2:2 10bit 60

πŸ“Š NDI Resolution & Format Capabilities

Resolution Color Space Sampling Bit Depth Supported Frame Rates (Hz)
β€” 4096Γ—2160@60Hz
YCbCr 4:4:4 8bit 60
YCbCr 4:4:0 8bit 60

πŸ“Š ST 2110 (25G OPT port) Resolution & Format Capabilities

Resolution Color Space Sampling Bit Depth Supported Frame Rates (Hz)
β€” 4096Γ—2160@60Hz
RGB 4:4:4 8bit 60
YCbCr 4:4:4 8bit 60
YCbCr 4:2:2 8bit 60
RGB 4:4:4 10bit 60
YCbCr 4:4:4 10bit 60
YCbCr 4:2:2 10bit 60

πŸ“Š 12G-SDI Resolution & Format Capabilities

Resolution Color Space Sampling Bit Depth Supported Frame Rates (Hz)
β€” 4096Γ—2160@60Hz
YCbCr 4:2:2 8bit/10bit/12bit 60

πŸ“Š 3G-SDI Resolution & Format Capabilities

Resolution Color Space Sampling Bit Depth Supported Frame Rates (Hz)
β€” 1920Γ—1080@60Hz
YCbCr 4:2:2 8bit/10bit 60

πŸ“Š DL-DVI Resolution & Format Capabilities

Resolution Color Space Sampling Bit Depth Supported Frame Rates (Hz)
β€” 3840Γ—1080@60Hz
RGB 4:4:4 8bit 60

πŸ“Š SL-DVI Resolution & Format Capabilities

Resolution Color Space Sampling Bit Depth Supported Frame Rates (Hz)
β€” 2048Γ—1152@60Hz
RGB 4:4:4 8bit 60

πŸ“Š VGA Resolution & Format Capabilities

Resolution Color Space Sampling Bit Depth Supported Frame Rates (Hz)
β€” 1920Γ—1080@60Hz
RGB 4:4:4 8bit 60

Physical Specifications

Packing box775 mm Γ— 675 mm Γ— 845 mm (30.5" Γ— 26.6" Γ— 33.3")

✨ Key Features

  • HDR
  • 10G Fiber
  • 4K

Software

Firmware 1.9.9.2⬇️ Download

Why Choose the H15?

βœ“ 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.

Frequently Purchased Together

Customers who bought this product also purchased:

H Series

H_4x HDMI Input Card

4x HDMI 1.3 (or 2x HDMI 1.4)...

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

Ready to Get Started?

For pricing, availability, or technical questions about the H15, contact Olympian LED today.

Configure Your H15

Available Input Cards

Available Output Cards

Accessories

Input Slots (30)

Output Slots (10)

Configuration Summary

Total Price: $6,438
// 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 `
${cell.label}
Built-in
`; } if (cell.type === 'blank') { return `
${cell.label}
`; } // 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(); } });