Files

189 lines
16 KiB
Plaintext

<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Market Intelligence — Mek-Tech</title><link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&display=swap" rel="stylesheet"><link rel="stylesheet" href="/css/style.css"><link rel="stylesheet" href="/css/dynamic.css"><link rel="icon" href="/img/favicon.svg"><script src="https://unpkg.com/lucide@latest"></script><script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script><link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"><style>
.mi-wrap{max-width:1200px;margin:0 auto;padding:1.25rem 1.5rem 3rem}
.mi-hdr h1{font-size:1.4rem;font-weight:800;letter-spacing:-.4px}.mi-hdr p{font-size:.72rem;color:var(--text-muted)}
.mi-kpi{display:flex;gap:.4rem;margin:.75rem 0 1rem;flex-wrap:wrap}
.mi-kpi .mk{border:1px solid var(--border);border-radius:8px;padding:.45rem .75rem;background:var(--bg-card);display:flex;align-items:center;gap:.4rem;font-size:.68rem}
.mi-kpi .mk b{font-size:1.05rem;font-weight:700;color:var(--accent)}.mi-kpi .mk span{color:var(--text-muted);font-size:.58rem}
.mi-tabs{display:flex;margin-bottom:.75rem;background:var(--bg-card);border:1px solid var(--border);border-radius:8px;overflow:hidden}
.mi-tab{flex:1;padding:.4rem .8rem;font-size:.72rem;font-weight:600;color:var(--text-muted);cursor:pointer;border:none;background:none;font-family:inherit;border-right:1px solid var(--border);display:flex;align-items:center;justify-content:center;gap:.3rem}
.mi-tab:last-child{border-right:none}.mi-tab:hover{color:var(--text);background:var(--border-light)}.mi-tab.active{color:#fff;background:var(--accent)}
.mi-tab svg{width:13px;height:13px}
.mi-pane{display:none}.mi-pane.active{display:block}
.mi-fbar{display:flex;gap:.3rem;margin-bottom:.6rem;flex-wrap:wrap}
.mi-fbar input,.mi-fbar select{padding:.32rem .5rem;font-size:.68rem;background:var(--bg);border:1px solid var(--border);border-radius:5px;color:var(--text);font-family:inherit}
.mi-fbar input:focus,.mi-fbar select:focus{outline:none;border-color:var(--accent)}
.dc-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(240px,1fr));gap:.45rem}
.dc-card{background:var(--bg-card);border:1px solid var(--border);border-radius:8px;padding:.7rem;position:relative;transition:all .12s}
.dc-card:hover{border-color:var(--accent);transform:translateY(-1px);box-shadow:0 3px 12px rgba(0,0,0,.08)}
.dc-card h3{font-size:.78rem;font-weight:700;color:var(--text)}
.dc-card .dc-loc{font-size:.6rem;color:var(--text-dim);margin:2px 0}
.dc-card .dc-tags{display:flex;gap:3px;flex-wrap:wrap;margin:4px 0}
.dc-card .dc-tag{font-size:.52rem;font-weight:600;padding:2px 6px;border-radius:3px}
.dc-tag.tier{background:rgba(130,80,223,.12);color:#a855f7}.dc-tag.type{background:rgba(88,166,255,.1);color:var(--accent)}.dc-tag.op{background:rgba(63,185,80,.1);color:var(--green)}
.dc-card .dc-spec{font-size:.58rem;color:var(--text-dim);line-height:1.4}
.comp-table{width:100%;border-collapse:collapse;font-size:.68rem}
.comp-table th{text-align:left;padding:.4rem .5rem;font-size:.56rem;font-weight:700;text-transform:uppercase;letter-spacing:.3px;color:var(--text-muted);background:var(--bg);border-bottom:1px solid var(--border)}
.comp-table td{padding:.4rem .5rem;border-bottom:1px solid var(--border-light);vertical-align:top}
.comp-table tr:hover td{background:var(--border-light)}
.comp-table .c-name{font-weight:700;color:var(--text)}.comp-table .c-sub{font-size:.58rem;color:var(--text-dim)}
.c-chip{display:inline-block;padding:1px 5px;border-radius:3px;font-size:.54rem;font-weight:600;margin:1px}
.c-chip.dw{background:rgba(130,80,223,.12);color:#a855f7}.c-chip.bi{background:rgba(88,166,255,.1);color:var(--accent)}.c-chip.cl{background:rgba(63,185,80,.1);color:var(--green)}.c-chip.ai{background:rgba(210,153,34,.12);color:var(--orange)}.c-chip.db{background:rgba(247,119,186,.1);color:var(--pink)}
.c-score{display:inline-flex;align-items:center;justify-content:center;width:26px;height:26px;border-radius:50%;font-weight:800;font-size:.64rem}
.c-score.h90{background:var(--green-bg);color:var(--green)}.c-score.h70{background:rgba(88,166,255,.1);color:var(--accent)}.c-score.h50{background:var(--orange-bg);color:var(--orange)}.c-score.low{background:var(--border-light);color:var(--text-dim)}
#map{height:480px;border-radius:10px;border:1px solid var(--border);z-index:1}
.map-legend{display:flex;gap:.75rem;flex-wrap:wrap;margin-top:.5rem;font-size:.62rem;color:var(--text-muted)}
.map-legend span{display:flex;align-items:center;gap:.25rem}
.map-legend .dot{width:10px;height:10px;border-radius:50%}
.mi-empty{text-align:center;padding:1.5rem;color:var(--text-dim);font-size:.72rem}
.mi-result-count{font-size:.62rem;color:var(--text-dim);margin-bottom:.4rem}
.leaflet-popup-content{font-size:.7rem}.leaflet-popup-content h4{margin:0 0 3px;font-size:.75rem}.leaflet-popup-content p{margin:1px 0;font-size:.62rem;color:#666}
</style></head><body>
<nav class="navbar"><div class="nav-inner" style="gap:.5rem"><a href="/" class="nav-brand"><img src="/img/logo.svg" class="nav-logo" alt="Mek-Tech"></a><div class="nav-links"><a href="/" class="nav-link">Dashboard</a><a href="/clients" class="nav-link">Clients</a><a href="/engagements" class="nav-link">Engagements</a><a href="/finance" class="nav-link">Finance</a><a href="/market" class="nav-link active">Market</a><a href="/consulting" class="nav-link">Consulting</a><a href="/quality" class="nav-link">Data</a><a href="/settings" class="nav-link">Settings</a><a href="/auth/logout" class="nav-link logout">Logout</a></div><button class="theme-toggle" id="themeToggle" onclick="toggleTheme()"></button></div></nav>
<div class="mi-wrap">
<div class="mi-hdr"><h1>Market Intelligence</h1><p>Dutch datacenters & data-driven enterprises — filter, search and explore on the map</p></div>
<div class="mi-kpi">
<div class="mk"><b id="kpiDc">0</b><span>datacenters</span></div>
<div class="mk"><b id="kpiPwr">0 MW</b><span>power</span></div>
<div class="mk"><b id="kpiCo">0</b><span>prospects</span></div>
<div class="mk"><b id="kpiDw">0</b><span>DW vendors</span></div>
<div class="mk"><b id="kpiInd">0</b><span>industries</span></div>
<div class="mk"><b id="kpiReg">0</b><span>regions</span></div>
</div>
<div class="mi-tabs">
<button class="mi-tab active" data-tab="datacenters"><i data-lucide="database"></i> Datacenters</button>
<button class="mi-tab" data-tab="companies"><i data-lucide="building-2"></i> Prospects</button>
<button class="mi-tab" data-tab="map"><i data-lucide="map"></i> Map</button>
</div>
<div class="mi-fbar">
<input type="text" id="searchInput" placeholder="Search..." style="width:160px" oninput="liveFilter()">
<select id="filterType" onchange="liveFilter()"><option value="">All types</option><option value="colo">Colo</option><option value="hyperscale">Hyperscale</option><option value="edge">Edge</option><option value="regional">Regional</option><option value="modular">Modular</option></select>
<select id="filterRegion" onchange="liveFilter()"></select>
<select id="filterVendor" onchange="liveFilter()"></select>
<select id="filterIndustry" onchange="liveFilter()" style="display:none"></select>
<select id="filterDw" onchange="liveFilter()" style="display:none"></select>
<select id="filterCity" onchange="liveFilter()" style="display:none"></select>
<select id="filterSort" onchange="liveFilter()"><option value="power_mw">Power (desc)</option><option value="name">Name (A-Z)</option><option value="lead_score">Lead score</option><option value="employees">Employees</option></select>
</div>
<div class="mi-pane active" id="pane-datacenters">
<div class="mi-result-count" id="dcCount"></div>
<div class="dc-grid" id="dcGrid"></div>
</div>
<div class="mi-pane" id="pane-companies">
<div class="mi-result-count" id="coCount"></div>
<table class="comp-table"><thead><tr><th style="width:36px"></th><th>Company</th><th>Data Warehouse</th><th>BI / Cloud</th><th>AI / Database</th><th>Contact</th></tr></thead><tbody id="compBody"></tbody></table>
</div>
<div class="mi-pane" id="pane-map">
<div id="map"></div>
<div class="map-legend">
<span><span class="dot" style="background:#a855f7"></span>Tier IV</span>
<span><span class="dot" style="background:#58a6ff"></span>Tier III</span>
<span><span class="dot" style="background:#3fb950"></span>Hyperscale</span>
<span><span class="dot" style="background:#f85149"></span>Company HQ</span>
<span><span class="dot" style="background:rgba(130,80,223,.12)"></span>Edge/Regional</span>
</div>
</div>
</div>
<script src="/js/theme.js"></script>
<script>
var currentTab='datacenters', allDc=[], allCo=[];
var map=null, dcLayer=null, coLayer=null, fullDcMap=[], dwVendorsArr=[], industriesArr=[], citiesArr=[], regionsArr=[], vendorsArr=[];
var totalPwr=0;
function loadFilters() {
fetch('/market/filters.json').then(r=>r.json()).then(d=>{
regionsArr=d.regions||[];dwVendorsArr=d.dwVendors||[];industriesArr=d.industries||[];citiesArr=d.cities||[];vendorsArr=d.vendors||[];
var fr=document.getElementById('filterRegion');fr.innerHTML='<option value="">All regions</option>'+regionsArr.map(r=>'<option value="'+r+'">'+r+'</option>').join('');
var fv=document.getElementById('filterVendor');fv.innerHTML='<option value="">All vendors</option>'+vendorsArr.map(v=>'<option value="'+v+'">'+v+'</option>').join('');
var fi=document.getElementById('filterIndustry');fi.innerHTML='<option value="">All industries</option>'+industriesArr.map(i=>'<option value="'+i+'">'+i+'</option>').join('');
var fd=document.getElementById('filterDw');fd.innerHTML='<option value="">DW vendor</option>'+dwVendorsArr.map(v=>'<option value="'+v+'">'+v+'</option>').join('');
var fc=document.getElementById('filterCity');fc.innerHTML='<option value="">City</option>'+citiesArr.map(c=>'<option value="'+c+'">'+c+'</option>').join('');
document.getElementById('kpiDw').textContent=dwVendorsArr.length;document.getElementById('kpiInd').textContent=industriesArr.length;document.getElementById('kpiReg').textContent=regionsArr.length;
});
}
function liveFilter() {
var s=document.getElementById('searchInput').value;
var region=document.getElementById('filterRegion').value;
var vendor=document.getElementById('filterVendor').value;
var industry=document.getElementById('filterIndustry').value;
var dw=document.getElementById('filterDw').value;
var city=document.getElementById('filterCity').value;
var sort=document.getElementById('filterSort').value;
var params=new URLSearchParams();
if(s)params.set('search',s);if(region)params.set('region',region);if(vendor)params.set('vendor',vendor);
if(industry)params.set('industry',industry);if(dw)params.set('dw',dw);if(city)params.set('city',city);if(sort)params.set('sort',sort);
// Datacenters
fetch('/market/search/dc?'+params.toString()).then(r=>r.json()).then(dc=>{
allDc=dc;renderDc(dc);
document.getElementById('kpiDc').textContent=dc.length;
totalPwr=dc.reduce((s,d)=>s+(d.power_mw||0),0);
document.getElementById('kpiPwr').textContent=Math.round(totalPwr)+' MW';
});
// Companies
fetch('/market/search/co?'+params.toString()).then(r=>r.json()).then(co=>{
allCo=co;renderCo(co);document.getElementById('kpiCo').textContent=co.length;
});
// Map
fetch('/market/map.json').then(r=>r.json()).then(d=>{fullDcMap=d;renderMap(d)});
}
function renderDc(dc) {
document.getElementById('dcCount').textContent=dc.length+' datacenters found';
document.getElementById('dcGrid').innerHTML=dc.length===0?'<div class="mi-empty">No datacenters found</div>':dc.map(d=>'<div class="dc-card"><h3>'+d.name+'</h3><div class="dc-loc">'+(d.city||'')+', '+(d.region||'')+(d.address?' · '+d.address:'')+'</div><div class="dc-loc">'+(d.phone?'📞 '+d.phone+' · ':'')+(d.website?'🌐 '+d.website:'')+'</div><div class="dc-tags"><span class="dc-tag tier">T'+d.tier+'</span><span class="dc-tag type">'+d.type+'</span><span class="dc-tag op">'+(d.operator||'')+'</span></div><div class="dc-spec"><strong>'+(d.power_mw||0)+' MW</strong> · '+(d.size_sqm||0).toLocaleString()+' m²</div>'+(d.network_vendors?'<div class="dc-spec"><b>Network:</b> '+d.network_vendors+'</div>':'')+(d.server_vendors?'<div class="dc-spec"><b>Servers:</b> '+d.server_vendors+'</div>':'')+(d.storage_vendors?'<div class="dc-spec"><b>Storage:</b> '+d.storage_vendors+'</div>':'')+(d.certifications?'<div class="dc-spec"><b>Certs:</b> '+d.certifications+'</div>':'')+'</div>').join('');
}
function renderCo(co) {
document.getElementById('coCount').textContent=co.length+' companies found';
document.getElementById('compBody').innerHTML=co.length===0?'<tr><td colspan="6" class="mi-empty">No companies found</td></tr>':co.map(c=>{
var sc=c.lead_score||0,cls=sc>=90?'h90':sc>=70?'h70':sc>=50?'h50':'low';
var dw=(c.data_arehouse||'').split(',').filter(Boolean).slice(0,5).map(v=>'<span class="c-chip dw">'+v.trim()+'</span>').join('');
var bi=(c.bi_tools||'').split(',').filter(Boolean).slice(0,3).map(v=>'<span class="c-chip bi">'+v.trim()+'</span>').join('');
var cl=(c.cloud_provider||'').split(',').filter(Boolean).slice(0,3).map(v=>'<span class="c-chip cl">'+v.trim()+'</span>').join('');
var ai=(c.ai_ml||'').split(',').filter(Boolean).slice(0,2).map(v=>'<span class="c-chip ai">'+v.trim()+'</span>').join('');
var db=(c.database_vendor||'').split(',').filter(Boolean).slice(0,2).map(v=>'<span class="c-chip db">'+v.trim()+'</span>').join('');
return '<tr><td><div class="c-score '+cls+'">'+sc+'</div></td><td><div class="c-name">'+c.name+'</div><div class="c-sub">'+c.industry+' · '+c.city+' · ~'+(c.employees||0).toLocaleString()+'</div>'+(c.revenue_range?'<div class="c-sub">Rev: '+c.revenue_range+'</div>':'')+'</td><td>'+dw+'</td><td>'+bi+cl+'</td><td>'+ai+db+'</td><td style="font-size:.6rem;color:var(--text-dim)">'+(c.phone?'📞 '+c.phone+'<br>':'')+(c.email?'✉ '+c.email+'<br>':'')+(c.website?'🌐 '+c.website:'')+'</td></tr>';
}).join('');
}
function renderMap(d) {
if(map){map.invalidateSize();}else{
map=L.map('map',{zoomControl:true,scrollWheelZoom:false}).setView([52.1,5.3],7);
L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png',{attribution:'&copy; OpenStreetMap, &copy; CARTO',maxZoom:18}).addTo(map);
}
if(dcLayer)dcLayer.remove();if(coLayer)coLayer.remove();
dcLayer=L.layerGroup().addTo(map);coLayer=L.layerGroup().addTo(map);
(d.datacenters||[]).forEach(dc=>{
var color=dc.tier==='IV'?'#a855f7':dc.type==='hyperscale'?'#3fb950':'#58a6ff';
var r=Math.max(8,Math.min(40,(dc.power_mw||1)/5));
L.circleMarker([dc.lat,dc.lng],{radius:r,color:color,fillColor:color,fillOpacity:.4,weight:2}).bindPopup('<h4>'+dc.name+'</h4><p>'+(dc.city||'')+', '+(dc.region||'')+'</p><p>Tier '+dc.tier+' · '+dc.type+'</p><p>'+(dc.power_mw||0)+' MW · '+(dc.operator||'')+'</p>').addTo(dcLayer);
});
(d.companies||[]).forEach(c=>{
L.circleMarker([c.lat,c.lng],{radius:4,color:'#f85149',fillColor:'#f85149',fillOpacity:.3,weight:1}).bindPopup('<h4>'+c.name+'</h4><p>'+c.industry+' · '+c.city+'</p><p>~'+(c.employees||0).toLocaleString()+' employees</p>').addTo(coLayer);
});
}
document.querySelectorAll('.mi-tab').forEach(function(t){
t.addEventListener('click',function(){
document.querySelectorAll('.mi-tab').forEach(x=>x.classList.remove('active'));this.classList.add('active');
document.querySelectorAll('.mi-pane').forEach(x=>x.classList.remove('active'));document.getElementById('pane-'+this.dataset.tab).classList.add('active');currentTab=this.dataset.tab;
if(currentTab==='datacenters'){document.getElementById('filterType').style.display='';document.getElementById('filterRegion').style.display='';document.getElementById('filterVendor').style.display='';document.getElementById('filterIndustry').style.display='none';document.getElementById('filterDw').style.display='none';document.getElementById('filterCity').style.display='none'}
else if(currentTab==='companies'){document.getElementById('filterType').style.display='none';document.getElementById('filterRegion').style.display='none';document.getElementById('filterVendor').style.display='none';document.getElementById('filterIndustry').style.display='';document.getElementById('filterDw').style.display='';document.getElementById('filterCity').style.display=''}
if(currentTab==='map'&&map){setTimeout(()=>map.invalidateSize(),100)}
});
});
document.getElementById('filterType').addEventListener('change',liveFilter);
loadFilters();liveFilter();lucide.createIcons();
</script>
</body></html>