feat: Switch→Transceiver Finder tab in dashboard
- New 'Finder' tab between Switches and Blog Engine - Search by switch model (free text), filter by speed - Quick-access buttons: Nexus 93180, Nexus 9332D, Arista 7280R3A, Juniper QFX5120 - Results grouped by speed class (400G QSFP-DD, 100G QSFP28, etc.) - Shows: part number, vendor, reach, fiber type, connector, verified price, stock status - Flexoptix products highlighted with orange left border + FLEXOPTIX badge - Buy link → flexoptix.net for each result - Uses existing /api/finder endpoint with 33,993 compatibility entries
This commit is contained in:
parent
174078efdb
commit
6a6a22d303
@ -678,6 +678,7 @@
|
||||
<div class="tab" data-tab="transceivers">Transceivers</div>
|
||||
<div class="tab" data-tab="switches">Switches</div>
|
||||
<div class="tab" data-tab="news">News</div>
|
||||
<div class="tab" data-tab="finder">Finder</div>
|
||||
<div class="tab" data-tab="blog">Blog Engine</div>
|
||||
</div>
|
||||
|
||||
@ -859,6 +860,39 @@
|
||||
<div class="card"><div id="news-list"></div></div>
|
||||
</div>
|
||||
|
||||
<!-- FINDER -->
|
||||
<div id="tab-finder" class="hidden">
|
||||
<div style="margin-bottom:1.2rem">
|
||||
<h3 style="font-size:1rem;font-weight:600;margin-bottom:0.3rem">Switch → Transceiver Finder <span style="font-size:0.7rem;color:var(--text-dim);font-weight:400">Find the right Flexoptix transceiver for your switch</span></h3>
|
||||
<div style="display:flex;gap:0.5rem;align-items:center;flex-wrap:wrap">
|
||||
<input id="finder-switch-input" type="text" placeholder="Enter switch model, e.g. N9K-C93180YC-FX3 or Nexus 93180..."
|
||||
style="flex:1;min-width:280px;padding:10px 14px;border:1px solid var(--border);border-radius:8px;background:var(--bg2);color:var(--text);font-size:0.9rem"
|
||||
onkeydown="if(event.key==='Enter') runFinder()">
|
||||
<select id="finder-speed-filter" style="padding:10px;border:1px solid var(--border);border-radius:8px;background:var(--bg2);color:var(--text);font-size:0.85rem">
|
||||
<option value="">All Speeds</option>
|
||||
<option value="10">10G</option>
|
||||
<option value="25">25G</option>
|
||||
<option value="40">40G</option>
|
||||
<option value="100">100G</option>
|
||||
<option value="400">400G</option>
|
||||
<option value="800">800G</option>
|
||||
</select>
|
||||
<button onclick="runFinder()" style="background:var(--accent);color:white;border:none;padding:10px 20px;border-radius:8px;cursor:pointer;font-weight:600;font-size:0.9rem">Find Transceivers</button>
|
||||
</div>
|
||||
<!-- Quick examples -->
|
||||
<div style="margin-top:0.5rem;display:flex;gap:0.4rem;flex-wrap:wrap">
|
||||
<span style="font-size:0.7rem;color:var(--text-dim)">Quick:</span>
|
||||
<button onclick="finderQuick('N9K-C93180YC-FX3')" class="b b-dim" style="font-size:0.7rem;padding:2px 8px;border-radius:4px">Nexus 93180YC-FX3</button>
|
||||
<button onclick="finderQuick('N9K-C9332D-GX2B')" class="b b-dim" style="font-size:0.7rem;padding:2px 8px;border-radius:4px">Nexus 9332D-GX2B</button>
|
||||
<button onclick="finderQuick('7280R3A-48D5')" class="b b-dim" style="font-size:0.7rem;padding:2px 8px;border-radius:4px">Arista 7280R3A</button>
|
||||
<button onclick="finderQuick('QFX5120-48Y')" class="b b-dim" style="font-size:0.7rem;padding:2px 8px;border-radius:4px">Juniper QFX5120</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Results area -->
|
||||
<div id="finder-results"></div>
|
||||
</div>
|
||||
|
||||
<!-- BLOG -->
|
||||
<div id="tab-blog" class="hidden">
|
||||
<div style="margin-bottom:0.8rem;display:flex;justify-content:space-between;align-items:center">
|
||||
@ -1154,6 +1188,7 @@ function goToTab(tabName) {
|
||||
if (tabName === 'switches') searchSwitches();
|
||||
if (tabName === 'news') loadNews();
|
||||
if (tabName === 'blog') loadBlogDrafts();
|
||||
if (tabName === 'finder') document.getElementById('finder-switch-input').focus();
|
||||
}
|
||||
|
||||
document.querySelectorAll('.tab').forEach(function(tab) {
|
||||
@ -2320,6 +2355,125 @@ function pollBlogLlm(id, attempt) {
|
||||
|
||||
// Hot topics loaded dynamically via hot-topics.js
|
||||
|
||||
// ══════════════════════════════════════════════════════
|
||||
// FINDER — Switch → Transceiver
|
||||
// ══════════════════════════════════════════════════════
|
||||
|
||||
function finderQuick(model) {
|
||||
document.getElementById('finder-switch-input').value = model;
|
||||
runFinder();
|
||||
}
|
||||
|
||||
async function runFinder() {
|
||||
var model = (document.getElementById('finder-switch-input').value || '').trim();
|
||||
var speed = document.getElementById('finder-speed-filter').value;
|
||||
var results = document.getElementById('finder-results');
|
||||
if (!model) { results.innerHTML = '<p style="color:var(--text-dim)">Enter a switch model to search.</p>'; return; }
|
||||
|
||||
results.innerHTML = '<div class="loading pulse">Searching compatibility database...</div>';
|
||||
|
||||
var url = '/api/finder?switch=' + encodeURIComponent(model) + (speed ? '&speed=' + speed : '');
|
||||
try {
|
||||
var data = await api(url);
|
||||
|
||||
if (data.error) {
|
||||
results.innerHTML = '<div class="card" style="border-left:3px solid #c1121f"><b>Not found:</b> ' + data.error +
|
||||
(data.suggestion ? '<br><span style="color:var(--text-dim);font-size:0.85rem">' + data.suggestion + '</span>' : '') + '</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
var sw = data.switch;
|
||||
var transceivers = data.compatible_transceivers || [];
|
||||
var total = data.total || 0;
|
||||
|
||||
// Switch info header
|
||||
var swHtml = '<div class="card mb" style="display:flex;gap:1rem;align-items:center">' +
|
||||
(sw.image_url ? '<img src="' + sw.image_url + '" style="height:60px;border-radius:6px;object-fit:contain" onerror="this.style.display=\'none\'">' : '') +
|
||||
'<div style="flex:1">' +
|
||||
'<div style="font-size:1.1rem;font-weight:700">' + sw.vendor + ' ' + sw.model + '</div>' +
|
||||
'<div style="color:var(--text-dim);font-size:0.8rem">' +
|
||||
(sw.series ? sw.series + ' · ' : '') +
|
||||
'Max speed: ' + (sw.max_speed_gbps || '?') + 'G' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div style="text-align:right;font-size:0.8rem;color:var(--text-dim)">' +
|
||||
'<b style="font-size:1.1rem;color:var(--text)">' + total + '</b> compatible transceivers' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
|
||||
if (transceivers.length === 0) {
|
||||
results.innerHTML = swHtml + '<div class="card" style="color:var(--text-dim)">No compatible transceivers found for this switch. Try removing the speed filter.</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Group by speed class
|
||||
var bySpeed = {};
|
||||
for (var t of transceivers) {
|
||||
var key = t.speed_gbps + 'G ' + t.form_factor;
|
||||
if (!bySpeed[key]) bySpeed[key] = [];
|
||||
bySpeed[key].push(t);
|
||||
}
|
||||
|
||||
var speedColors = { 800: '#c1121f', 400: '#FF8100', 100: '#e6a800', 25: '#2d6a4f', 10: '#888' };
|
||||
|
||||
var tcvrHtml = Object.entries(bySpeed).sort(function(a, b) {
|
||||
return parseInt(b[0]) - parseInt(a[0]);
|
||||
}).map(function(entry) {
|
||||
var speedClass = entry[0];
|
||||
var items = entry[1];
|
||||
var speedGbps = items[0].speed_gbps;
|
||||
var color = speedColors[speedGbps] || '#888';
|
||||
|
||||
var cards = items.slice(0, 12).map(function(t) {
|
||||
var isFlexoptix = (t.vendor || '').toUpperCase() === 'FLEXOPTIX';
|
||||
var hasPrice = t.price != null;
|
||||
var priceHtml = hasPrice
|
||||
? '<span style="color:var(--accent);font-weight:700">' + (t.currency || 'EUR') + ' ' + parseFloat(t.price).toFixed(2) + '</span>'
|
||||
: '<span style="color:var(--text-dim)">Price on request</span>';
|
||||
var stockHtml = t.stock === 'in_stock' ? '<span style="color:#2d6a4f;font-size:0.65rem">● In Stock</span>'
|
||||
: t.stock === 'limited' ? '<span style="color:#e6a800;font-size:0.65rem">● Limited</span>'
|
||||
: '';
|
||||
var partNum = t.part_number || t.slug || t.id;
|
||||
|
||||
return '<div class="card" style="padding:0.8rem;' + (isFlexoptix ? 'border-left:3px solid var(--accent)' : '') + '">' +
|
||||
'<div style="display:flex;justify-content:space-between;align-items:flex-start;gap:0.5rem">' +
|
||||
'<div style="flex:1;min-width:0">' +
|
||||
(isFlexoptix ? '<span style="font-size:0.6rem;background:var(--accent);color:white;padding:1px 5px;border-radius:3px;margin-right:4px">FLEXOPTIX</span>' : '') +
|
||||
'<span style="font-size:0.7rem;color:var(--text-dim)">' + (t.vendor || '') + '</span><br>' +
|
||||
'<b style="font-size:0.85rem;word-break:break-all">' + partNum + '</b><br>' +
|
||||
'<span style="font-size:0.75rem;color:var(--text-dim)">' +
|
||||
(t.reach || '') + (t.fiber_type ? ' · ' + t.fiber_type : '') +
|
||||
(t.connector ? ' · ' + t.connector : '') +
|
||||
'</span>' +
|
||||
'</div>' +
|
||||
'<div style="text-align:right;flex-shrink:0">' +
|
||||
priceHtml + '<br>' + stockHtml +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
(t.buy_url ? '<a href="' + t.buy_url + '" target="_blank" style="display:inline-block;margin-top:0.5rem;font-size:0.7rem;color:var(--accent)">Buy at Flexoptix →</a>' : '') +
|
||||
'</div>';
|
||||
}).join('');
|
||||
|
||||
return '<div style="margin-bottom:1.2rem">' +
|
||||
'<div style="font-size:0.8rem;font-weight:700;color:' + color + ';margin-bottom:0.5rem;text-transform:uppercase;letter-spacing:0.05em">' +
|
||||
speedClass + ' <span style="color:var(--text-dim);font-weight:400">(' + items.length + ' options)</span>' +
|
||||
'</div>' +
|
||||
'<div class="grid g3">' + cards + '</div>' +
|
||||
'</div>';
|
||||
}).join('');
|
||||
|
||||
results.innerHTML = swHtml +
|
||||
'<div style="font-size:0.75rem;color:var(--text-dim);margin-bottom:0.8rem">' +
|
||||
'Showing ' + Math.min(transceivers.length, total) + ' of ' + total + ' compatible transceivers · ' +
|
||||
'<span style="color:#2d6a4f">Orange border = Flexoptix product</span>' +
|
||||
'</div>' +
|
||||
tcvrHtml;
|
||||
|
||||
} catch(e) {
|
||||
results.innerHTML = '<div class="card" style="border-left:3px solid #c1121f">Error: ' + e.message + '</div>';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
async function loadBlogDrafts() {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user