aee0f09db8
- Datafabrik: Dockerfile fix, agentorkestrering fungerar - Vision: Identify-modell, FAISS, OCR alla testade - API: Alla 7 integrationstester passerade - Upplösare: Entitetsupplösning verifierad
425 lines
19 KiB
HTML
425 lines
19 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="sv">
|
||
<head>
|
||
<meta charset="UTF-8"/>
|
||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||
<title>quiXzoom — Beställ ett uppdrag</title>
|
||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"/>
|
||
<style>
|
||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||
:root {
|
||
--bg: #f2f2f7;
|
||
--surface: #ffffff;
|
||
--surface2: #f2f2f7;
|
||
--border: rgba(0,0,0,0.1);
|
||
--accent: #ff3b30;
|
||
--accent2: #34c759;
|
||
--blue: #007aff;
|
||
--text: #1c1c1e;
|
||
--text2: #6e6e73;
|
||
--font: -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'Helvetica Neue', sans-serif;
|
||
}
|
||
@media (prefers-color-scheme: dark) {
|
||
:root { --bg:#0a0a0f; --surface:#1c1c1e; --surface2:#2c2c2e; --border:rgba(255,255,255,0.08); --text:#f5f5f7; --text2:#8e8e93; }
|
||
}
|
||
body { background: var(--bg); color: var(--text); font-family: var(--font); min-height: 100vh; padding: 0; }
|
||
|
||
/* Header */
|
||
.header {
|
||
background: var(--surface); border-bottom: 1px solid var(--border);
|
||
padding: 16px 20px; display: flex; align-items: center; gap: 12px;
|
||
position: sticky; top: 0; z-index: 100;
|
||
}
|
||
.logo { font-size: 20px; font-weight: 700; letter-spacing: -0.5px; }
|
||
.logo span { color: var(--accent); }
|
||
.header-title { font-size: 15px; color: var(--text2); }
|
||
|
||
/* Layout */
|
||
.container { max-width: 680px; margin: 0 auto; padding: 24px 16px 48px; }
|
||
|
||
/* Section */
|
||
.section { background: var(--surface); border-radius: 16px; padding: 20px; margin-bottom: 16px; }
|
||
.section-title { font-size: 17px; font-weight: 600; margin-bottom: 16px; display: flex; align-items: center; gap: 8px; }
|
||
|
||
/* Form fields */
|
||
.field { margin-bottom: 16px; }
|
||
.field:last-child { margin-bottom: 0; }
|
||
.field-label { font-size: 13px; font-weight: 500; color: var(--text2); margin-bottom: 6px; text-transform: uppercase; letter-spacing: 0.3px; }
|
||
.field-input {
|
||
width: 100%; background: var(--surface2); border: 1.5px solid var(--border);
|
||
border-radius: 12px; padding: 12px 14px; font-size: 15px; color: var(--text);
|
||
font-family: var(--font); transition: border-color 0.15s;
|
||
}
|
||
.field-input:focus { outline: none; border-color: var(--blue); }
|
||
.field-input::placeholder { color: var(--text2); }
|
||
textarea.field-input { resize: vertical; min-height: 80px; }
|
||
select.field-input { appearance: none; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8'%3E%3Cpath d='M1 1l5 5 5-5' stroke='%236e6e73' stroke-width='1.5' fill='none' stroke-linecap='round'/%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: right 14px center; padding-right: 36px; }
|
||
|
||
/* Map picker */
|
||
#location-map { width: 100%; height: 220px; border-radius: 12px; overflow: hidden; margin-top: 10px; border: 1.5px solid var(--border); }
|
||
.location-addr { margin-top: 8px; font-size: 13px; color: var(--text2); display: flex; align-items: center; gap: 6px; }
|
||
.location-pin { color: var(--accent); font-size: 16px; }
|
||
|
||
/* Budget slider */
|
||
.slider-wrap { margin-top: 8px; }
|
||
.budget-display { font-size: 24px; font-weight: 700; color: var(--accent); margin-bottom: 10px; }
|
||
input[type="range"] {
|
||
-webkit-appearance: none; width: 100%; height: 4px; border-radius: 2px;
|
||
background: linear-gradient(to right, var(--accent) 0%, var(--accent) var(--pct, 20%), var(--surface2) var(--pct, 20%), var(--surface2) 100%);
|
||
outline: none; cursor: pointer;
|
||
}
|
||
input[type="range"]::-webkit-slider-thumb {
|
||
-webkit-appearance: none; width: 22px; height: 22px; border-radius: 50%;
|
||
background: var(--accent); box-shadow: 0 2px 8px rgba(0,0,0,0.25); cursor: pointer;
|
||
}
|
||
.budget-labels { display: flex; justify-content: space-between; font-size: 11px; color: var(--text2); margin-top: 4px; }
|
||
|
||
/* Camera count */
|
||
.camera-selector { display: flex; align-items: center; gap: 14px; }
|
||
.count-btn {
|
||
width: 36px; height: 36px; border-radius: 50%; background: var(--surface2);
|
||
border: 1.5px solid var(--border); font-size: 20px; cursor: pointer; display: flex;
|
||
align-items: center; justify-content: center; color: var(--text); transition: background 0.1s;
|
||
}
|
||
.count-btn:hover { background: var(--border); }
|
||
.count-val { font-size: 22px; font-weight: 700; min-width: 36px; text-align: center; }
|
||
|
||
/* Type selector */
|
||
.type-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; }
|
||
.type-btn {
|
||
background: var(--surface2); border: 1.5px solid var(--border); border-radius: 12px;
|
||
padding: 12px 8px; text-align: center; cursor: pointer; transition: all 0.15s;
|
||
}
|
||
.type-btn:hover { border-color: var(--blue); }
|
||
.type-btn.active { background: rgba(0,122,255,0.1); border-color: var(--blue); }
|
||
.type-icon { font-size: 22px; margin-bottom: 4px; }
|
||
.type-label { font-size: 12px; font-weight: 500; }
|
||
|
||
/* Submit */
|
||
.submit-section { padding: 0 0 8px; }
|
||
.submit-btn {
|
||
width: 100%; padding: 16px; background: var(--accent); color: white; border: none;
|
||
border-radius: 16px; font-size: 17px; font-weight: 600; cursor: pointer;
|
||
font-family: var(--font); transition: opacity 0.15s; display: flex; align-items: center; justify-content: center; gap: 8px;
|
||
}
|
||
.submit-btn:hover:not(:disabled) { opacity: 0.88; }
|
||
.submit-btn:disabled { opacity: 0.45; cursor: not-allowed; }
|
||
|
||
/* Confirmation */
|
||
.confirm-screen { display: none; text-align: center; padding: 40px 20px; }
|
||
.confirm-icon { font-size: 64px; margin-bottom: 16px; }
|
||
.confirm-title { font-size: 26px; font-weight: 700; letter-spacing: -0.5px; margin-bottom: 8px; }
|
||
.confirm-sub { font-size: 15px; color: var(--text2); margin-bottom: 28px; line-height: 1.5; }
|
||
.mission-id-box {
|
||
background: var(--surface2); border: 1px solid var(--border); border-radius: 14px;
|
||
padding: 16px; margin-bottom: 24px;
|
||
}
|
||
.mission-id-label { font-size: 12px; color: var(--text2); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 4px; }
|
||
.mission-id-val { font-size: 22px; font-weight: 700; font-feature-settings: 'tnum'; letter-spacing: 2px; }
|
||
.track-btn {
|
||
display: inline-block; padding: 14px 32px; background: var(--blue); color: white;
|
||
border-radius: 14px; font-size: 16px; font-weight: 600; text-decoration: none;
|
||
transition: opacity 0.15s;
|
||
}
|
||
.track-btn:hover { opacity: 0.88; }
|
||
@media (max-width: 640px) {
|
||
.hero h1 { font-size: 2rem !important; }
|
||
.hero p { font-size: 1rem !important; }
|
||
.card { padding: 1.5rem !important; }
|
||
.grid { grid-template-columns: 1fr !important; }
|
||
.btn { padding: 0.75rem 1.5rem !important; font-size: 1rem !important; }
|
||
}
|
||
</style>
|
||
|
||
<style id="wcag-contrast-enforcement">
|
||
/* AAMOS GLOBAL CONTRAST ENFORCEMENT — WCAG AA (4.5:1 minimum) */
|
||
/* Regel: Ingen text under 70% opacity. Aldrig. */
|
||
body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; }
|
||
/* Fix common low-contrast patterns */
|
||
[style*="rgba(60,60,67,0.4)"] { color: rgba(60,60,67,0.65) !important; }
|
||
[style*="rgba(60,60,67,0.3)"] { color: rgba(60,60,67,0.65) !important; }
|
||
[style*="rgba(255,255,255,0.4)"] { color: rgba(255,255,255,0.72) !important; }
|
||
[style*="rgba(255,255,255,0.3)"] { color: rgba(255,255,255,0.72) !important; }
|
||
[style*="opacity: 0.4"] { opacity: 0.72 !important; }
|
||
[style*="opacity: 0.3"] { opacity: 0.72 !important; }
|
||
@media (max-width: 640px) {
|
||
.hero h1 { font-size: 2rem !important; }
|
||
.hero p { font-size: 1rem !important; }
|
||
.card { padding: 1.5rem !important; }
|
||
.grid { grid-template-columns: 1fr !important; }
|
||
.btn { padding: 0.75rem 1.5rem !important; font-size: 1rem !important; }
|
||
}
|
||
</style>
|
||
<script src="i18n.js"></script>
|
||
<meta property="og:type" content="website">
|
||
<meta property="og:title" content="quiXzoom — Beställ ett uppdrag">
|
||
<meta property="og:description" content="quiXzoom — Earn per Mission. No Platform Fee.">
|
||
<meta property="og:url" content="https://www.quixzoom.com/request-mission.html">
|
||
<meta property="og:image" content="https://www.quixzoom.com/images/mission-map.jpg">
|
||
<meta property="og:image:width" content="1200">
|
||
<meta property="og:image:height" content="630">
|
||
<meta property="og:image:alt" content="quiXzoom — request a mission in your area">
|
||
<meta name="twitter:card" content="summary_large_image">
|
||
<meta name="twitter:image" content="https://www.quixzoom.com/images/mission-map.jpg">
|
||
<meta name="twitter:title" content="quiXzoom — Beställ ett uppdrag">
|
||
</head>
|
||
<body>
|
||
|
||
<div class="header">
|
||
<div class="logo">qui<span>X</span>zoom</div>
|
||
<div class="header-title">/ Beställ uppdrag</div>
|
||
</div>
|
||
|
||
<div class="container" id="form-container">
|
||
|
||
<!-- Location -->
|
||
<div class="section">
|
||
<div class="section-title">📍 Plats</div>
|
||
<div class="field">
|
||
<div class="field-label">Adress</div>
|
||
<input class="field-input" id="address" type="text" placeholder="Ange adress eller klicka på kartan…" oninput="updateAddress(this.value)"/>
|
||
</div>
|
||
<div id="location-map"></div>
|
||
<div class="location-addr">
|
||
<span class="location-pin">📌</span>
|
||
<span id="chosen-location">Klicka på kartan för att välja plats</span>
|
||
</div>
|
||
<input type="hidden" id="lat" value="7.9519"/>
|
||
<input type="hidden" id="lng" value="98.2849"/>
|
||
</div>
|
||
|
||
<!-- Mission details -->
|
||
<div class="section">
|
||
<div class="section-title">📋 Uppdragsdetaljer</div>
|
||
<div class="field">
|
||
<div class="field-label">Titel / Beskrivning av uppdrag</div>
|
||
<input class="field-input" id="title" type="text" placeholder="T.ex. Hotell-lobby fotografering…"/>
|
||
</div>
|
||
<div class="field">
|
||
<div class="field-label">Vad ska fotograferas/filmas?</div>
|
||
<textarea class="field-input" id="description" placeholder="Beskriv detaljerat vad du vill att Zoomern ska fånga. Inkludera specifika vinklar, motiv eller krav."></textarea>
|
||
</div>
|
||
<div class="field">
|
||
<div class="field-label">Typ av uppdrag</div>
|
||
<div class="type-grid">
|
||
<div class="type-btn active" data-type="photography" onclick="selectType(this)">
|
||
<div class="type-icon">📷</div>
|
||
<div class="type-label">Foto</div>
|
||
</div>
|
||
<div class="type-btn" data-type="video" onclick="selectType(this)">
|
||
<div class="type-icon">🎬</div>
|
||
<div class="type-label">Video</div>
|
||
</div>
|
||
<div class="type-btn" data-type="drone" onclick="selectType(this)">
|
||
<div class="type-icon">🚁</div>
|
||
<div class="type-label">Drone</div>
|
||
</div>
|
||
<div class="type-btn" data-type="livestream" onclick="selectType(this)">
|
||
<div class="type-icon">📡</div>
|
||
<div class="type-label">Livestream</div>
|
||
</div>
|
||
<div class="type-btn" data-type="360" onclick="selectType(this)">
|
||
<div class="type-icon">🔄</div>
|
||
<div class="type-label">360°</div>
|
||
</div>
|
||
<div class="type-btn" data-type="documentary" onclick="selectType(this)">
|
||
<div class="type-icon">🎥</div>
|
||
<div class="type-label">Dokumentär</div>
|
||
</div>
|
||
</div>
|
||
<input type="hidden" id="mission-type" value="photography"/>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Date & time -->
|
||
<div class="section">
|
||
<div class="section-title">📅 Datum & Tid</div>
|
||
<div class="field">
|
||
<div class="field-label">Startdatum och tid</div>
|
||
<input class="field-input" id="date-time" type="datetime-local"/>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Cameras -->
|
||
<div class="section">
|
||
<div class="section-title">📸 Antal Zoomers</div>
|
||
<div class="field">
|
||
<div class="field-label">Önskat antal kameror / Zoomers</div>
|
||
<div class="camera-selector">
|
||
<button class="count-btn" onclick="changeCount(-1)">−</button>
|
||
<div class="count-val" id="camera-count">1</div>
|
||
<button class="count-btn" onclick="changeCount(1)">+</button>
|
||
<span style="color:var(--text2);font-size:13px;">Zoomer(s)</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Budget -->
|
||
<div class="section">
|
||
<div class="section-title">💰 Budget</div>
|
||
<div class="field">
|
||
<div class="field-label">Budget (SEK)</div>
|
||
<div class="slider-wrap">
|
||
<div class="budget-display" id="budget-display">5 000 kr</div>
|
||
<input type="range" id="budget-slider" min="500" max="50000" value="5000" step="500" oninput="updateBudget(this.value)"/>
|
||
<div class="budget-labels"><span>500 kr</span><span>50 000 kr</span></div>
|
||
</div>
|
||
<input type="hidden" id="budget" value="5000"/>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Contact -->
|
||
<div class="section">
|
||
<div class="section-title">👤 Dina uppgifter</div>
|
||
<div class="field">
|
||
<div class="field-label">Namn</div>
|
||
<input class="field-input" id="contact-name" type="text" placeholder="För- och efternamn"/>
|
||
</div>
|
||
<div class="field">
|
||
<div class="field-label">E-post</div>
|
||
<input class="field-input" id="contact-email" type="email" placeholder="din@email.com"/>
|
||
</div>
|
||
<div class="field">
|
||
<div class="field-label">Telefon (valfritt)</div>
|
||
<input class="field-input" id="contact-phone" type="tel" placeholder="+46 70 123 45 67"/>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Submit -->
|
||
<div class="submit-section">
|
||
<button class="submit-btn" id="submit-btn" onclick="submitMission()">
|
||
📤 Skicka beställning
|
||
</button>
|
||
<div style="text-align:center;font-size:12px;color:var(--text2);margin-top:10px;">
|
||
Du bekräftar att uppdraget följer quiXzooms riktlinjer. Vi återkommer inom 30 minuter.
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<!-- Confirmation screen -->
|
||
<div class="confirm-screen" id="confirm-screen">
|
||
<div class="confirm-icon">🎉</div>
|
||
<div class="confirm-title">Uppdrag beställt!</div>
|
||
<div class="confirm-sub">Vi har tagit emot din beställning och matchar dig med rätt Zoomer. Du får en bekräftelse via e-post inom kort.</div>
|
||
<div class="mission-id-box">
|
||
<div class="mission-id-label">Uppdragets ID</div>
|
||
<div class="mission-id-val" id="confirm-mission-id">—</div>
|
||
</div>
|
||
<a class="track-btn" id="track-link" href="#">🗺️ Följ uppdraget live</a>
|
||
</div>
|
||
|
||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||
<script>
|
||
// ── Map ───────────────────────────────────────────────────────────────────
|
||
const map = L.map('location-map', { center: [7.9519, 98.2849], zoom: 12 });
|
||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||
attribution: '© OpenStreetMap',
|
||
}).addTo(map);
|
||
|
||
let marker = L.marker([7.9519, 98.2849], { draggable: true }).addTo(map);
|
||
marker.on('dragend', e => {
|
||
const pos = e.target.getLatLng();
|
||
updateCoords(pos.lat, pos.lng);
|
||
});
|
||
|
||
map.on('click', e => {
|
||
marker.setLatLng(e.latlng);
|
||
updateCoords(e.latlng.lat, e.latlng.lng);
|
||
});
|
||
|
||
function updateCoords(lat, lng) {
|
||
document.getElementById('lat').value = lat.toFixed(6);
|
||
document.getElementById('lng').value = lng.toFixed(6);
|
||
document.getElementById('chosen-location').textContent = `${lat.toFixed(5)}°N, ${lng.toFixed(5)}°E`;
|
||
}
|
||
|
||
function updateAddress(addr) {
|
||
// Geocoding could be added here
|
||
document.getElementById('chosen-location').textContent = addr || 'Klicka på kartan för att välja plats';
|
||
}
|
||
|
||
// ── Type selector ─────────────────────────────────────────────────────────
|
||
function selectType(el) {
|
||
document.querySelectorAll('.type-btn').forEach(b => b.classList.remove('active'));
|
||
el.classList.add('active');
|
||
document.getElementById('mission-type').value = el.dataset.type;
|
||
}
|
||
|
||
// ── Camera count ──────────────────────────────────────────────────────────
|
||
let cameraCount = 1;
|
||
function changeCount(delta) {
|
||
cameraCount = Math.max(1, Math.min(10, cameraCount + delta));
|
||
document.getElementById('camera-count').textContent = cameraCount;
|
||
}
|
||
|
||
// ── Budget slider ─────────────────────────────────────────────────────────
|
||
function updateBudget(val) {
|
||
document.getElementById('budget').value = val;
|
||
document.getElementById('budget-display').textContent = parseInt(val).toLocaleString('sv-SE') + ' kr';
|
||
const pct = ((val - 500) / (50000 - 500)) * 100;
|
||
document.getElementById('budget-slider').style.setProperty('--pct', pct + '%');
|
||
}
|
||
updateBudget(5000);
|
||
|
||
// ── Default date/time (tomorrow 10:00) ────────────────────────────────────
|
||
const tomorrow = new Date();
|
||
tomorrow.setDate(tomorrow.getDate() + 1);
|
||
tomorrow.setHours(10, 0, 0, 0);
|
||
document.getElementById('date-time').value = tomorrow.toISOString().slice(0, 16);
|
||
|
||
// ── Submit ────────────────────────────────────────────────────────────────
|
||
async function submitMission() {
|
||
const title = document.getElementById('title').value.trim();
|
||
const contactName = document.getElementById('contact-name').value.trim();
|
||
const contactEmail = document.getElementById('contact-email').value.trim();
|
||
|
||
if (!title) { alert('Ange en titel för uppdraget.'); return; }
|
||
if (!contactEmail) { alert('Ange din e-postadress.'); return; }
|
||
|
||
const btn = document.getElementById('submit-btn');
|
||
btn.disabled = true;
|
||
btn.textContent = '⏳ Skickar…';
|
||
|
||
const payload = {
|
||
title,
|
||
lat: document.getElementById('lat').value,
|
||
lng: document.getElementById('lng').value,
|
||
description: document.getElementById('description').value,
|
||
type: document.getElementById('mission-type').value,
|
||
date_time: document.getElementById('date-time').value,
|
||
cameras: cameraCount,
|
||
budget: document.getElementById('budget').value,
|
||
customer: contactName || contactEmail,
|
||
contact_name: contactName,
|
||
contact_email: contactEmail,
|
||
contact_phone: document.getElementById('contact-phone').value,
|
||
};
|
||
|
||
try {
|
||
const r = await fetch('https://api.quixzoom.com/api/qz/missions', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(payload),
|
||
});
|
||
const d = await r.json();
|
||
if (d.ok && d.mission) {
|
||
document.getElementById('form-container').style.display = 'none';
|
||
document.getElementById('confirm-screen').style.display = 'block';
|
||
document.getElementById('confirm-mission-id').textContent = d.mission.id.toUpperCase();
|
||
document.getElementById('track-link').href = `/quixzoom/mission-map.html?mission=${d.mission.id}`;
|
||
} else {
|
||
btn.disabled = false;
|
||
btn.textContent = '📤 Skicka beställning';
|
||
alert(d.error || 'Något gick fel. Försök igen.');
|
||
}
|
||
} catch(e) {
|
||
btn.disabled = false;
|
||
btn.textContent = '📤 Skicka beställning';
|
||
alert('Nätverksfel. Kontrollera din anslutning.');
|
||
}
|
||
}
|
||
</script>
|
||
</body>
|
||
</html>
|