Add Dockerfile, Nomad spec, Gitea CI/CD, requirements and health endpoint
Some checks failed
Build and Deploy BlaaAi / build-and-deploy (push) Has been cancelled
Some checks failed
Build and Deploy BlaaAi / build-and-deploy (push) Has been cancelled
This commit is contained in:
536
templates/index.html
Normal file
536
templates/index.html
Normal file
@@ -0,0 +1,536 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="da">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>BlaaAi — Find den bedste annonce</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script>
|
||||
tailwind.config = {
|
||||
theme: {
|
||||
extend: {
|
||||
fontFamily: { sans: ['Space Grotesk', 'sans-serif'] },
|
||||
colors: {
|
||||
ink: { DEFAULT: '#09090b', 50: '#f4f4f5', 100: '#e4e4e7', 200: '#27272a', 300: '#3f3f46' }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
body { background: #fafaf9; color: #0c0a09; font-family: 'Space Grotesk', sans-serif; }
|
||||
.card-enter { animation: fadeUp .25s ease both; }
|
||||
@keyframes fadeUp { from { opacity:0; transform:translateY(10px); } to { opacity:1; transform:translateY(0); } }
|
||||
.spinner { animation: spin .9s linear infinite; }
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
.score-fill { background: #0c0a09; transition: width .6s cubic-bezier(.4,0,.2,1); }
|
||||
@keyframes shimmer { 0%{background-position:-400% 0} 100%{background-position:400% 0} }
|
||||
.skeleton { background:linear-gradient(90deg,#f0efed 25%,#e8e5e1 50%,#f0efed 75%);background-size:400% 100%;animation:shimmer 1.6s ease-in-out infinite;border-radius:4px; }
|
||||
input, textarea {
|
||||
background: #fff;
|
||||
border: 1px solid #e7e5e4;
|
||||
color: #0c0a09;
|
||||
outline: none;
|
||||
transition: border-color .15s;
|
||||
}
|
||||
input:focus, textarea:focus { border-color: #a8a29e; }
|
||||
input::placeholder, textarea::placeholder { color: #a8a29e; }
|
||||
.btn-primary {
|
||||
background: #0c0a09; color: #fafaf9;
|
||||
font-weight: 600; letter-spacing: -.01em;
|
||||
transition: background .15s, transform .1s;
|
||||
}
|
||||
.btn-primary:hover:not(:disabled) { background: #292524; }
|
||||
.btn-primary:active:not(:disabled) { transform: scale(.98); }
|
||||
.btn-primary:disabled { opacity: .4; cursor: default; }
|
||||
.card {
|
||||
background: #fff;
|
||||
border: 1px solid #e7e5e4;
|
||||
transition: border-color .15s, box-shadow .15s;
|
||||
}
|
||||
.card:hover { border-color: #d6d3d1; box-shadow: 0 1px 8px rgba(0,0,0,.05); }
|
||||
/* Video modal */
|
||||
#video-modal { display:none; position:fixed; inset:0; z-index:50; align-items:center; justify-content:center; }
|
||||
#video-modal.open { display:flex; }
|
||||
#video-backdrop { position:absolute; inset:0; background:rgba(0,0,0,.7); backdrop-filter:blur(4px); }
|
||||
#video-box {
|
||||
position:relative; z-index:1; width:min(860px,92vw);
|
||||
background:#0f172a; border-radius:16px; overflow:hidden;
|
||||
box-shadow:0 24px 80px rgba(0,0,0,.6);
|
||||
animation: modalIn .2s ease both;
|
||||
}
|
||||
@keyframes modalIn { from{opacity:0;transform:scale(.96)} to{opacity:1;transform:scale(1)} }
|
||||
#video-box video { display:block; width:100%; }
|
||||
#video-close {
|
||||
position:absolute; top:12px; right:12px; z-index:2;
|
||||
background:rgba(255,255,255,.1); border:none; color:#fff;
|
||||
width:32px; height:32px; border-radius:50%; cursor:pointer;
|
||||
font-size:1rem; display:flex; align-items:center; justify-content:center;
|
||||
transition:background .15s;
|
||||
}
|
||||
#video-close:hover { background:rgba(255,255,255,.25); }
|
||||
.btn-help {
|
||||
display:inline-flex; align-items:center; gap:6px;
|
||||
font-size:.75rem; font-weight:500; color:#78716c;
|
||||
border:1px solid #e7e5e4; border-radius:8px;
|
||||
padding:6px 12px; cursor:pointer; background:transparent;
|
||||
transition:color .15s, border-color .15s;
|
||||
}
|
||||
.btn-help:hover { color:#0c0a09; border-color:#a8a29e; }
|
||||
.tag-top { background:#f0fdf4; color:#15803d; border:1px solid #bbf7d0; }
|
||||
.tag-good { background:#eff6ff; color:#1d4ed8; border:1px solid #bfdbfe; }
|
||||
.tag-mid { background:#fefce8; color:#a16207; border:1px solid #fde68a; }
|
||||
a.annonce-link {
|
||||
color: #a8a29e; font-size:.75rem; letter-spacing:.02em;
|
||||
text-decoration: none; transition: color .15s;
|
||||
}
|
||||
a.annonce-link:hover { color: #0c0a09; }
|
||||
header { border-bottom: 1px solid #e7e5e4; background: #fafaf9; }
|
||||
</style>
|
||||
</head>
|
||||
<body style="
|
||||
background-color: #fafaf9;
|
||||
background-image: url('/static/background.png');
|
||||
background-repeat: no-repeat;
|
||||
background-position: bottom center;
|
||||
background-size: clamp(600px, 80vw, 1100px) auto;
|
||||
background-attachment: fixed;
|
||||
">
|
||||
|
||||
<!-- Top bar (always visible) -->
|
||||
<header>
|
||||
<div class="max-w-2xl mx-auto px-6 py-4 flex items-center justify-between">
|
||||
<button onclick="resetForm()" class="flex items-center gap-2 hover:opacity-60 transition-opacity">
|
||||
<span style="font-size:1.1rem;line-height:1">◈</span>
|
||||
<span class="font-semibold tracking-tight">BlaaAi</span>
|
||||
</button>
|
||||
<div class="flex items-center gap-3">
|
||||
<span id="header-status" class="text-xs" style="color:#a8a29e"></span>
|
||||
<button class="btn-help" onclick="openVideoModal()">
|
||||
<svg width="13" height="13" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
||||
<circle cx="12" cy="12" r="10"/><polygon points="10 8 16 12 10 16 10 8" fill="currentColor" stroke="none"/>
|
||||
</svg>
|
||||
Video hjælp
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- ── VIDEO MODAL ── -->
|
||||
<div id="video-modal" role="dialog" aria-modal="true" aria-label="Video hjælp">
|
||||
<div id="video-backdrop" onclick="closeVideoModal()"></div>
|
||||
<div id="video-box">
|
||||
<button id="video-close" onclick="closeVideoModal()" aria-label="Luk video">✕</button>
|
||||
<video id="help-video" controls preload="metadata"
|
||||
poster=""
|
||||
src="/static/blaaai_tutorial.mp4">
|
||||
</video>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<main class="max-w-2xl mx-auto px-6" style="min-height:100vh;display:flex;flex-direction:column;justify-content:center;padding-top:2rem;padding-bottom:22rem">
|
||||
|
||||
<!-- ── FORM ── -->
|
||||
<section id="form-section" class="py-12 relative">
|
||||
<p class="text-xs font-medium tracking-widest uppercase mb-5" style="color:#a8a29e">AI-powered annonce analyse</p>
|
||||
<h1 class="text-4xl font-bold tracking-tight leading-tight mb-10" style="letter-spacing:-.03em">
|
||||
Find den bedste<br>DBA-annonce.
|
||||
</h1>
|
||||
|
||||
<div class="space-y-3">
|
||||
<input
|
||||
id="url-input"
|
||||
type="url"
|
||||
placeholder="https://www.dba.dk/mobility/search/…"
|
||||
class="w-full px-4 py-3 rounded-lg text-sm"
|
||||
/>
|
||||
|
||||
<div id="prefs-section" class="hidden">
|
||||
<textarea
|
||||
id="prefs-input"
|
||||
rows="2"
|
||||
placeholder="Dine præferencer — fx 'ingen franske biler, helst automatgear'"
|
||||
class="w-full px-4 py-3 rounded-lg text-sm resize-none"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-3 pt-1">
|
||||
<button
|
||||
id="submit-btn"
|
||||
onclick="submitSearch()"
|
||||
class="btn-primary px-6 py-3 rounded-lg text-sm flex items-center gap-2"
|
||||
>
|
||||
<span>Analyser</span>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14 5l7 7m0 0l-7 7m7-7H3"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button onclick="togglePrefs()" id="prefs-btn"
|
||||
class="text-sm px-4 py-3 rounded-lg transition-colors"
|
||||
style="color:#78716c;border:1px solid #e7e5e4">
|
||||
+ Præferencer
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="text-xs mt-8 leading-relaxed" style="color:#a8a29e">
|
||||
Paste en DBA søge-URL — AI'en gennemgår alle annoncer og rangerer dem efter pris, stand og kvalitet.<br>
|
||||
<span style="color:#d6d3d1">AI kan tage fejl. Brug det som inspiration, ikke som facit.</span>
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<!-- ── STATUS ── -->
|
||||
<section id="status-section" class="hidden pt-8 pb-20">
|
||||
<div class="flex items-center gap-3 mb-1">
|
||||
<svg class="spinner w-4 h-4 shrink-0" fill="none" viewBox="0 0 24 24" style="color:#a8a29e">
|
||||
<circle class="opacity-20" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="3"/>
|
||||
<path class="opacity-80" fill="currentColor" d="M4 12a8 8 0 018-8v8z"/>
|
||||
</svg>
|
||||
<p id="status-text" class="font-medium text-sm">Henter annoncer…</p>
|
||||
<span id="status-progress" class="text-xs ml-auto" style="color:#a8a29e"></span>
|
||||
</div>
|
||||
<p id="status-sub" class="text-xs mb-6 ml-7" style="color:#a8a29e;min-height:1.2em"></p>
|
||||
<div id="skeleton-container" class="space-y-2"></div>
|
||||
</section>
|
||||
|
||||
<!-- ── RESULTS ── -->
|
||||
<section id="results-section" class="hidden pt-8 pb-20">
|
||||
|
||||
<div class="flex items-baseline justify-between mb-8">
|
||||
<div>
|
||||
<h2 class="text-xl font-bold tracking-tight">Resultater</h2>
|
||||
<p id="result-count" class="text-xs mt-1" style="color:#a8a29e"></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="listings-container" class="space-y-2"></div>
|
||||
|
||||
<!-- Email -->
|
||||
<div class="mt-12 pt-8" style="border-top:1px solid #e7e5e4">
|
||||
<p class="text-sm font-medium mb-3">Send top-10 på email</p>
|
||||
<div class="flex gap-2">
|
||||
<input
|
||||
id="email-input"
|
||||
type="email"
|
||||
placeholder="din@email.dk"
|
||||
class="flex-1 px-4 py-2.5 rounded-lg text-sm"
|
||||
/>
|
||||
<button
|
||||
onclick="sendEmail()"
|
||||
id="email-btn"
|
||||
class="btn-primary px-5 py-2.5 rounded-lg text-sm"
|
||||
>Send</button>
|
||||
</div>
|
||||
<p id="email-status" class="text-xs mt-2 hidden" style="color:#71717a"></p>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
|
||||
</main>
|
||||
|
||||
<script>
|
||||
function openVideoModal() {
|
||||
document.getElementById('video-modal').classList.add('open');
|
||||
document.body.style.overflow = 'hidden';
|
||||
const v = document.getElementById('help-video');
|
||||
v.currentTime = 0;
|
||||
v.play();
|
||||
}
|
||||
function closeVideoModal() {
|
||||
document.getElementById('video-modal').classList.remove('open');
|
||||
document.body.style.overflow = '';
|
||||
const v = document.getElementById('help-video');
|
||||
v.pause();
|
||||
v.currentTime = 0;
|
||||
}
|
||||
document.getElementById('help-video').addEventListener('ended', closeVideoModal);
|
||||
document.addEventListener('keydown', e => {
|
||||
if (e.key === 'Escape') closeVideoModal();
|
||||
});
|
||||
|
||||
const PRELOAD_ID = {{ search_id | tojson if search_id is defined else 'null' }};
|
||||
let currentSearchId = null;
|
||||
let pollInterval = null;
|
||||
let skeletonsRendered = false;
|
||||
let msgInterval = null;
|
||||
|
||||
const statusMessages = {
|
||||
queued: ["Stiller i kø…", "Venter på ledig plads"],
|
||||
fetching: ["Henter annoncer fra DBA…", "Indlæser titler, priser og beskrivelser"],
|
||||
scoring: ["AI analyserer annoncerne…", ""],
|
||||
ready: ["Analyse færdig", ""],
|
||||
error: ["Noget gik galt", "Tjek søgelinket og prøv igen"],
|
||||
};
|
||||
|
||||
const funMessages = [
|
||||
"Vurderer bilers pålidelighed og historik…",
|
||||
"Sammenligner kilometertal og prisforhold…",
|
||||
"Tjekker om sælger lyder troværdig…",
|
||||
"Vurderer forventede vedligeholdelsesomkostninger…",
|
||||
"Sammenligner mod aktuelle markedspriser…",
|
||||
"Analyserer annoncebeskrivelserne for røde flag…",
|
||||
"Overvejer årstal og udstyrsgrad…",
|
||||
"Ranker efter kvalitet og pris…",
|
||||
"Tjekker km-stand mod forventet for alderen…",
|
||||
"Næsten færdig — finpudser rangeringen…",
|
||||
];
|
||||
|
||||
function togglePrefs() {
|
||||
const s = document.getElementById("prefs-section");
|
||||
const b = document.getElementById("prefs-btn");
|
||||
s.classList.toggle("hidden");
|
||||
b.textContent = s.classList.contains("hidden") ? "+ Præferencer" : "− Præferencer";
|
||||
}
|
||||
|
||||
async function submitSearch() {
|
||||
const url = document.getElementById("url-input").value.trim();
|
||||
const prefs = document.getElementById("prefs-input").value.trim();
|
||||
|
||||
if (!url.startsWith("https://www.dba.dk")) {
|
||||
alert("Indsæt et gyldigt DBA søgelink");
|
||||
return;
|
||||
}
|
||||
|
||||
const btn = document.getElementById("submit-btn");
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = `<svg class="spinner w-4 h-4" fill="none" viewBox="0 0 24 24"><circle class="opacity-20" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="3"/><path class="opacity-80" fill="currentColor" d="M4 12a8 8 0 018-8v8z"/></svg><span>Sender…</span>`;
|
||||
|
||||
try {
|
||||
const res = await fetch("/api/searches", {
|
||||
method: "POST",
|
||||
headers: {"Content-Type": "application/json"},
|
||||
body: JSON.stringify({url, prefs}),
|
||||
});
|
||||
const data = await res.json();
|
||||
currentSearchId = data.id;
|
||||
|
||||
showStatus();
|
||||
startPolling(data.id);
|
||||
} catch(e) {
|
||||
alert("Fejl: " + e.message);
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = `<span>Analyser</span><svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14 5l7 7m0 0l-7 7m7-7H3"/></svg>`;
|
||||
}
|
||||
}
|
||||
|
||||
function showStatus() {
|
||||
document.getElementById("form-section").classList.add("hidden");
|
||||
document.getElementById("status-section").classList.remove("hidden");
|
||||
document.getElementById("results-section").classList.add("hidden");
|
||||
}
|
||||
|
||||
function startPolling(id) {
|
||||
history.pushState(null, "", `/search/${id}`);
|
||||
pollInterval = setInterval(() => poll(id), 2000);
|
||||
}
|
||||
|
||||
function startFunMessages() {
|
||||
let i = 0;
|
||||
const el = document.getElementById("status-sub");
|
||||
el.textContent = funMessages[0];
|
||||
msgInterval = setInterval(() => {
|
||||
i = (i + 1) % funMessages.length;
|
||||
el.textContent = funMessages[i];
|
||||
}, 3500);
|
||||
}
|
||||
|
||||
function renderSkeletons(count) {
|
||||
if (skeletonsRendered) return;
|
||||
skeletonsRendered = true;
|
||||
const container = document.getElementById("skeleton-container");
|
||||
container.innerHTML = "";
|
||||
for (let i = 0; i < count; i++) {
|
||||
const card = document.createElement("div");
|
||||
card.className = "card rounded-xl p-5";
|
||||
card.style.animationDelay = `${i * 40}ms`;
|
||||
card.innerHTML = `
|
||||
<div class="flex items-start gap-4">
|
||||
<div class="skeleton shrink-0 mt-1" style="width:1rem;height:.75rem;border-radius:3px"></div>
|
||||
<div class="flex-1 space-y-2.5">
|
||||
<div class="flex gap-2">
|
||||
<div class="skeleton" style="height:.8rem;width:7rem;border-radius:3px"></div>
|
||||
<div class="skeleton" style="height:.8rem;width:4rem;border-radius:3px"></div>
|
||||
</div>
|
||||
<div class="skeleton" style="height:.4rem;width:100%;border-radius:9999px"></div>
|
||||
<div class="skeleton" style="height:.7rem;width:75%;border-radius:3px"></div>
|
||||
<div class="skeleton" style="height:.7rem;width:55%;border-radius:3px"></div>
|
||||
</div>
|
||||
<div class="shrink-0 ml-2 space-y-2">
|
||||
<div class="skeleton" style="height:.9rem;width:5rem;border-radius:3px"></div>
|
||||
<div class="skeleton" style="height:.7rem;width:3rem;border-radius:3px;margin-left:auto"></div>
|
||||
</div>
|
||||
</div>`;
|
||||
container.appendChild(card);
|
||||
}
|
||||
}
|
||||
|
||||
function stopFunMessages() {
|
||||
if (msgInterval) { clearInterval(msgInterval); msgInterval = null; }
|
||||
}
|
||||
|
||||
async function poll(id) {
|
||||
try {
|
||||
const res = await fetch(`/api/searches/${id}`);
|
||||
const data = await res.json();
|
||||
|
||||
const total = data.listing_count || 0;
|
||||
const scored = data.scored_count || 0;
|
||||
|
||||
// Show skeletons as soon as we know the count
|
||||
if (total > 0) renderSkeletons(total);
|
||||
|
||||
// Update status text
|
||||
const [main] = statusMessages[data.status] || ["Arbejder…"];
|
||||
document.getElementById("status-text").textContent = main;
|
||||
document.getElementById("header-status").textContent = data.status === "ready" ? "" : main;
|
||||
|
||||
// Progress counter during scoring
|
||||
if (data.status === "scoring" && total > 0) {
|
||||
document.getElementById("status-progress").textContent = `${scored}/${total}`;
|
||||
if (!msgInterval) startFunMessages();
|
||||
} else if (data.status !== "scoring") {
|
||||
const [, sub] = statusMessages[data.status] || ["", ""];
|
||||
document.getElementById("status-sub").textContent = sub;
|
||||
}
|
||||
|
||||
if (data.status === "ready") {
|
||||
stopFunMessages();
|
||||
clearInterval(pollInterval);
|
||||
showResults(data);
|
||||
} else if (data.status === "error") {
|
||||
stopFunMessages();
|
||||
clearInterval(pollInterval);
|
||||
}
|
||||
} catch(e) {
|
||||
console.error("Poll fejl:", e);
|
||||
}
|
||||
}
|
||||
|
||||
function showResults(data) {
|
||||
document.getElementById("status-section").classList.add("hidden");
|
||||
document.getElementById("results-section").classList.remove("hidden");
|
||||
document.getElementById("header-status").textContent = "";
|
||||
|
||||
const listings = data.listings || [];
|
||||
document.getElementById("result-count").textContent =
|
||||
`${listings.length} annoncer analyseret · sorteret efter AI-score`;
|
||||
|
||||
const container = document.getElementById("listings-container");
|
||||
container.innerHTML = "";
|
||||
|
||||
listings.forEach((item, i) => {
|
||||
const score = item.ai_score || 0;
|
||||
const pct = Math.round(score * 10);
|
||||
const warn = item.ai_warnings
|
||||
? `<p class="text-xs mt-2" style="color:#dc2626">↑ ${item.ai_warnings}</p>` : "";
|
||||
const tag = rankTag(score);
|
||||
|
||||
const card = document.createElement("div");
|
||||
card.className = "card rounded-xl p-5 card-enter";
|
||||
card.style.animationDelay = `${i * 30}ms`;
|
||||
card.innerHTML = `
|
||||
<div class="flex items-start gap-4">
|
||||
<span class="text-xs font-mono pt-0.5 shrink-0" style="color:#d6d3d1;min-width:1.5rem">${i+1}</span>
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center gap-2 mb-2 flex-wrap">
|
||||
<span class="font-semibold text-sm tracking-tight">${item.name}</span>
|
||||
<span class="text-xs" style="color:#a8a29e">${item.description || ""}</span>
|
||||
${tag}
|
||||
</div>
|
||||
<div class="flex items-center gap-3 mb-3">
|
||||
<div class="flex-1 rounded-full" style="background:#f5f5f4;height:3px">
|
||||
<div class="score-fill rounded-full" style="height:3px;width:${pct}%"></div>
|
||||
</div>
|
||||
<span class="text-xs font-bold tabular-nums shrink-0">${score.toFixed(1)}<span style="color:#d6d3d1">/10</span></span>
|
||||
</div>
|
||||
<p class="text-xs leading-relaxed" style="color:#78716c">${item.ai_reason || ""}</p>
|
||||
${warn}
|
||||
</div>
|
||||
<div class="shrink-0 text-right ml-2">
|
||||
<p class="font-semibold text-sm tabular-nums">${Number(item.price_dkk || 0).toLocaleString("da-DK")}<span class="text-xs font-normal ml-0.5" style="color:#a8a29e">kr</span></p>
|
||||
<a href="${item.url}" target="_blank" class="annonce-link mt-1.5 block">Se →</a>
|
||||
</div>
|
||||
</div>`;
|
||||
container.appendChild(card);
|
||||
});
|
||||
}
|
||||
|
||||
function rankTag(score) {
|
||||
if (score >= 8.5) return '<span class="tag-top text-xs px-2 py-0.5 rounded font-medium">Topvalg</span>';
|
||||
if (score >= 7.5) return '<span class="tag-good text-xs px-2 py-0.5 rounded font-medium">Godt køb</span>';
|
||||
if (score >= 6.5) return '<span class="tag-mid text-xs px-2 py-0.5 rounded font-medium">Middel</span>';
|
||||
return '';
|
||||
}
|
||||
|
||||
async function sendEmail() {
|
||||
const email = document.getElementById("email-input").value.trim();
|
||||
const btn = document.getElementById("email-btn");
|
||||
const status = document.getElementById("email-status");
|
||||
|
||||
if (!email) { alert("Indtast en email-adresse"); return; }
|
||||
if (!currentSearchId) return;
|
||||
|
||||
btn.disabled = true;
|
||||
btn.textContent = "Sender…";
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/searches/${currentSearchId}/email`, {
|
||||
method: "POST",
|
||||
headers: {"Content-Type": "application/json"},
|
||||
body: JSON.stringify({email}),
|
||||
});
|
||||
const data = await res.json();
|
||||
status.classList.remove("hidden");
|
||||
if (res.ok) {
|
||||
status.style.color = "#4ade80";
|
||||
status.textContent = `Sendt til ${email}`;
|
||||
} else {
|
||||
status.style.color = "#f87171";
|
||||
status.textContent = data.detail || "Fejl ved afsendelse";
|
||||
btn.disabled = false; btn.textContent = "Send";
|
||||
}
|
||||
} catch(e) {
|
||||
status.classList.remove("hidden");
|
||||
status.style.color = "#f87171";
|
||||
status.textContent = "Netværksfejl — prøv igen";
|
||||
btn.disabled = false; btn.textContent = "Send";
|
||||
}
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
if (pollInterval) clearInterval(pollInterval);
|
||||
stopFunMessages();
|
||||
skeletonsRendered = false;
|
||||
currentSearchId = null;
|
||||
document.getElementById("form-section").classList.remove("hidden");
|
||||
document.getElementById("status-section").classList.add("hidden");
|
||||
document.getElementById("results-section").classList.add("hidden");
|
||||
document.getElementById("skeleton-container").innerHTML = "";
|
||||
document.getElementById("status-progress").textContent = "";
|
||||
document.getElementById("listings-container").innerHTML = "";
|
||||
document.getElementById("url-input").value = "";
|
||||
document.getElementById("prefs-input").value = "";
|
||||
document.getElementById("email-input").value = "";
|
||||
document.getElementById("header-status").textContent = "";
|
||||
document.getElementById("email-status").classList.add("hidden");
|
||||
const btn = document.getElementById("submit-btn");
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = `<span>Analyser</span><svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14 5l7 7m0 0l-7 7m7-7H3"/></svg>`;
|
||||
history.pushState(null, "", "/");
|
||||
}
|
||||
|
||||
if (PRELOAD_ID) {
|
||||
currentSearchId = PRELOAD_ID;
|
||||
showStatus();
|
||||
poll(PRELOAD_ID).then(() => {
|
||||
if (!document.getElementById("status-section").classList.contains("hidden")) {
|
||||
startPolling(PRELOAD_ID);
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user