Svenn-Konkurransesøk

by SvennMats

HTML

<div id="svenn-market-analyzer">
  <div class="analyzer-container">
    
    <div class="search-section manual-box">
      <h3>Sjekk markedet i din kommune</h3>
      <p class="search-subtitle">Få full oversikt over konkurrenter, prisnivå og muligheter der du bor.</p>
      
      <div class="manual-grid">
        <div class="input-wrapper">
          <label class="field-label">Søk og velg Kommune</label>
          <input type="text" id="muniSearch" placeholder="F.eks. Trondheim..." autocomplete="off">
          <div id="muniResults" class="autocomplete-dropdown"></div>
        </div>
        
        <div class="input-wrapper dropdown-wrapper">
          <label class="field-label">Velg Bransje</label>
          <input type="text" id="industrySearch" placeholder="Velg fra listen..." autocomplete="off" readonly onfocus="this.removeAttribute('readonly');">
          <span class="dropdown-arrow">▼</span>
          <div id="industryResults" class="autocomplete-dropdown"></div>
        </div>
      </div>
      
      <button id="executeManualSearch" class="svenn-btn-main">Analyser markedet</button>
    </div>

    <div id="resultsArea" class="hidden">
      
      <div class="result-card">
        <div id="resultHeaderInfo" class="header-info"></div>

        <div class="count-section">
          <span class="res-label">Konkurrenter i ditt område:</span>
          <div id="resCount" class="res-number">0</div>
        </div>
        
        <div id="dynamicAnalysis" class="analysis-box"></div>

        <div class="market-stats">
          <div class="stat-box">
            <span class="stat-val" id="countAS">-</span>
            <span class="stat-label">Aktive AS</span>
          </div>
          <div class="stat-box">
            <span class="stat-val" id="countENK">-</span>
            <span class="stat-label">Aktive ENK</span>
          </div>
          <div class="stat-box">
            <span class="stat-val"...

CSS

:root {
  --svenn-orange: #F96529;
  --svenn-dark: #241D2D;
  --svenn-res-bg: #EDF3E3;
  --svenn-bg: #F8F9FA;
  --white: #FFFFFF;
  
  --font-h2: 28px;
  --font-h3: 24px;
  --font-h4: 18px;
  --font-body: 16px;
}

body { background-color: #fff; padding: 20px; font-family: 'Magnet', 'Inter', sans-serif; }

#svenn-market-analyzer { color: var(--svenn-dark); max-width: 1000px; margin: 0 auto; }

.analyzer-container { background: var(--white); border-radius: 12px; box-shadow: none; padding: 40px 0; }

/* TYPOGRAFI & LUFT */
h2 { font-size: var(--font-h2); color: var(--svenn-dark); margin: 0 0 10px 0; text-align: center; }
h3 { font-size: var(--font-h3); color: var(--svenn-dark); margin-bottom: 25px; text-align: center; }
.section-container > p, header > p { text-align: center; color: #666; font-size: var(--font-body); margin-bottom: 30px; max-width: 600px; margin-left: auto; margin-right: auto; line-height: 1.5; }
h4 { font-size: var(--font-h4); color: var(--svenn-dark); margin-bottom: 5px; }

/* SECTIONS */
.section-container { margin-top: 60px; padding: 0 30px; }

/* SØKESEKSJON */
.manual-box { background: var(--svenn-bg); padding: 30px; border-radius: 12px; }
.manual-box h3 { font-size: 26px; margin-bottom: 10px; }
.search-subtitle { margin-bottom: 40px; color: #666; font-size: 16px; text-align: center; max-width: 600px; margin-left: auto; margin-right: auto; line-height: 1.5; }

.manual-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }
.input-wrapper { position: relative; margin-bottom: 15px; }
.field-label { display: block; font-size: 12px; font-weight: bold; margin-bottom: 5px; text-transform: uppercase; color: #777; }

input[type="text"] { width: 100%; padding: 15px; border: 2px solid #EEE; border-radius: 8px; font-size: 16px; box-sizing: border-box; color: var(--svenn-dark); box-shadow: none; -webkit-appearance: none; appearance: none; }
input[type="text"]:focus { border-color: var(--svenn-orange);...

JavaScript

// --- 1. KONFIGURASJON ---

let currentSearchId = 0;

// BRANSJELISTE (DIN NYE OPPDATERTE LISTE)
const industryMap = [
  { label: "Tømrer / Snekker", codes: ['41.000', '43.320', '41.200'], avgPrice: 50000 },
  { label: "Maler / Tapetserer", codes: ['43.340', '43.341'], avgPrice: 25000 },
  { label: "Murer", codes: ['43.910', '43.990'], avgPrice: 60000 },
  { label: "Gulv, Flis & Tapet", codes: ['43.330'], avgPrice: 30000 }, 
  { label: "Taktekker / Blikkenslager", codes: ['43.410', '43.911', '43.912'], avgPrice: 80000 },
  { label: "Anleggsgartner", codes: ['81.300'], avgPrice: 70000 }
];

// Fjerner punktum for å sammenligne koder trygt
function normalizeCode(code) {
    if (!code) return "";
    return code.replace(/\./g, '');
}

// Hjelpefunksjon for dato (12 mnd siden) i YYYY-MM-DD format
function getOneYearAgoDate() {
    const d = new Date();
    d.setFullYear(d.getFullYear() - 1);
    return d.toISOString().split('T')[0];
}

// --- FAST FETCH (RACE MODE) ---
async function fetchFastest(targetUrl) {
    const encodedUrl = encodeURIComponent(targetUrl);
    
    // Definer alle metodene vi vil prøve
    const promises = [
        fetch("https://corsproxy.io/?" + encodedUrl).then(async r => {
            if(!r.ok) throw new Error("CP failed");
            return r.json();
        }),
        fetch("https://api.allorigins.win/get?url=" + encodedUrl).then(async r => {
            if(!r.ok) throw new Error("AO failed");
            const data = await r.json();
            if (data.contents) {
                return typeof data.contents === 'string' ? JSON.parse(data.contents) : data.contents;
            }
            throw new Error("AO no content");
        }),
        fetch("https://api.codetabs.com/v1/proxy?quest=" + encodedUrl).then(async r => {
            if(!r.ok) throw new Error("CT failed");
            return r.json();
        })
    ];

    try {
        return await Promise.any(promises);
    }...