Files
boc/quixzoom-landing-fixed/rating-widget.html
T
Bernt 48ea61cdcc docs: add quixzoom-auth-core product to AAMOS
- Product documentation in docs/products/
- Updated MEMORY.md with product info
- quiXzoom Auth Core as AAMOS Identity product
2026-07-14 09:58:53 +00:00

272 lines
11 KiB
HTML

<!DOCTYPE html>
<html lang="sv">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<title>quiXzoom — Betygsätt din Zoomer</title>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #f2f2f7;
--surface: #ffffff;
--border: rgba(0,0,0,0.1);
--accent: #ff3b30;
--accent2: #34c759;
--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; --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; display: flex; align-items: center; justify-content: center; padding: 16px; }
.widget {
background: var(--surface); border-radius: var(--radius-xl); padding: 28px 24px;
max-width: 380px; width: 100%; box-shadow: 0 4px 32px rgba(0,0,0,0.12);
}
.widget-header { text-align: center; margin-bottom: 24px; }
.widget-title { font-size: 22px; font-weight: 700; letter-spacing: -0.5px; }
.widget-title span { color: var(--accent); }
.widget-subtitle { font-size: 14px; color: var(--text2); margin-top: 4px; }
/* Zoomer profile */
.zoomer-profile { display: flex; align-items: center; gap: 14px; padding: 14px; background: var(--bg); border-radius: var(--radius-lg); margin-bottom: 20px; }
.zoomer-avatar {
width: 48px; height: 48px; border-radius: var(--radius-full); background: linear-gradient(135deg, #34c759, #30b952);
display: flex; align-items: center; justify-content: center; font-weight: 700; color: white; font-size: 18px; flex-shrink: 0;
}
.zoomer-name { font-size: 16px; font-weight: 600; }
.zoomer-meta { font-size: 12px; color: var(--text2); margin-top: 2px; }
.zoomer-score { margin-left: auto; text-align: center; }
.zoomer-score-val { font-size: 24px; font-weight: 700; color: #ff9f0a; }
.zoomer-score-label { font-size: 10px; color: var(--text2); text-transform: uppercase; }
/* Star rating */
.stars-label { font-size: 13px; color: var(--text2); margin-bottom: 8px; text-align: center; }
.stars { display: flex; justify-content: center; gap: 8px; margin-bottom: 20px; }
.star { font-size: 36px; cursor: pointer; transition: transform 0.1s; user-select: none; filter: grayscale(1) brightness(0.5); }
.star:hover, .star.active { filter: none; transform: scale(1.15); }
.star-labels { display: flex; justify-content: space-between; font-size: 11px; color: var(--text2); margin-bottom: 20px; padding: 0 4px; }
/* Comment */
.comment-label { font-size: 13px; font-weight: 500; margin-bottom: 6px; }
.comment-input {
width: 100%; background: var(--bg); border: 1.5px solid var(--border); border-radius: var(--radius-md);
padding: 12px 14px; font-size: 14px; color: var(--text); font-family: var(--font);
resize: none; transition: border-color 0.15s;
}
.comment-input:focus { outline: none; border-color: var(--accent); }
.comment-input::placeholder { color: var(--text2); }
/* Submit */
.submit-btn {
width: 100%; margin-top: 16px; padding: 14px; background: var(--accent);
color: white; border: none; border-radius: var(--radius-lg); font-size: 16px; font-weight: 600;
cursor: pointer; font-family: var(--font); transition: opacity 0.15s;
}
.submit-btn:hover:not(:disabled) { opacity: 0.88; }
.submit-btn:disabled { opacity: 0.4; cursor: not-allowed; }
/* Recent reviews */
.reviews-section { margin-top: 24px; border-top: 1px solid var(--border); padding-top: 20px; }
.reviews-title { font-size: 15px; font-weight: 600; margin-bottom: 14px; }
.review { padding: 12px 0; border-bottom: 1px solid var(--border); }
.review:last-child { border-bottom: none; }
.review-stars { font-size: 13px; margin-bottom: 4px; }
.review-comment { font-size: 13px; color: var(--text); margin-bottom: 4px; }
.review-meta { font-size: 11px; color: var(--text2); }
/* Success state */
.success { text-align: center; padding: 20px 0; display: none; }
.success-icon { font-size: 48px; margin-bottom: 12px; }
.success-title { font-size: 20px; font-weight: 700; margin-bottom: 6px; }
.success-sub { font-size: 14px; color: var(--text2); }
@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>
<script type="module" src="https://auth.quixzoom.com/site-auth-snippet.js"></script>
</head>
<body>
<div class="widget">
<div class="widget-header">
<div class="widget-title">qui<span>X</span>zoom</div>
<div class="widget-subtitle">Betygsätt din Zoomer</div>
</div>
<div class="zoomer-profile">
<div class="zoomer-avatar" id="zoomer-initials">AK</div>
<div>
<div class="zoomer-name" id="zoomer-name">Anan K.</div>
<div class="zoomer-meta" id="zoomer-meta">128 uppdrag · Phuket</div>
</div>
<div class="zoomer-score">
<div class="zoomer-score-val" id="zoomer-rating-val"></div>
<div class="zoomer-score-label">Betyg</div>
</div>
</div>
<div id="rating-form">
<div class="stars-label">Hur var upplevelsen?</div>
<div class="stars" id="stars">
<span class="star" data-val="1"></span>
<span class="star" data-val="2"></span>
<span class="star" data-val="3"></span>
<span class="star" data-val="4"></span>
<span class="star" data-val="5"></span>
</div>
<div class="star-labels">
<span>Dålig</span>
<span>Okej</span>
<span>Bra</span>
<span>Mycket bra</span>
<span>Utmärkt</span>
</div>
<div class="comment-label">Kommentar (valfritt)</div>
<textarea class="comment-input" id="comment" rows="3" placeholder="Berätta om din upplevelse med Zoomern..."></textarea>
<button class="submit-btn" id="submit-btn" onclick="submitRating()" disabled>
Skicka betyg
</button>
</div>
<div class="success" id="success">
<div class="success-icon"></div>
<div class="success-title">Tack för ditt betyg!</div>
<div class="success-sub">Ditt omdöme hjälper andra kunder att välja rätt Zoomer.</div>
</div>
<div class="reviews-section" id="reviews-section">
<div class="reviews-title">Senaste omdömen</div>
<div id="reviews-list">
<div style="color:var(--text2);font-size:13px;text-align:center;padding:20px 0;">Laddar omdömen…</div>
</div>
</div>
</div>
<script>
// Read query params
const params = new URLSearchParams(location.search);
const zoomerId = params.get('zoomerId') || 'z001';
const missionId = params.get('missionId') || 'm001';
let selectedRating = 0;
// Stars interaction
const stars = document.querySelectorAll('.star');
stars.forEach(star => {
star.addEventListener('mouseenter', () => highlightStars(parseInt(star.dataset.val)));
star.addEventListener('mouseleave', () => highlightStars(selectedRating));
star.addEventListener('click', () => {
selectedRating = parseInt(star.dataset.val);
highlightStars(selectedRating);
document.getElementById('submit-btn').disabled = false;
});
});
function highlightStars(n) {
stars.forEach(s => {
const val = parseInt(s.dataset.val);
s.classList.toggle('active', val <= n);
});
}
// Load zoomer info
async function loadZoomer() {
try {
const r = await fetch(`/api/quixzoom/ratings/zoomer/${zoomerId}`);
const d = await r.json();
if (d.ok) {
document.getElementById('zoomer-rating-val').textContent = d.public_score ?? (d.total_ratings < 3 ? '—' : d.avg_rating);
if (d.recent && d.recent.length > 0) {
renderReviews(d.recent);
}
}
} catch(e) {
console.log('Could not load zoomer:', e);
}
}
function renderReviews(reviews) {
const list = document.getElementById('reviews-list');
if (!reviews.length) {
list.innerHTML = '<div style="color:var(--text2);font-size:13px;text-align:center;padding:20px 0;">Inga omdömen ännu</div>';
return;
}
list.innerHTML = reviews.map(r => `
<div class="review">
<div class="review-stars">${''.repeat(r.rating)}${''.repeat(5 - r.rating)}</div>
${r.comment ? `<div class="review-comment">"${r.comment}"</div>` : ''}
<div class="review-meta">${new Date(r.createdAt).toLocaleDateString('sv-SE')}</div>
</div>
`).join('');
}
// Submit rating
async function submitRating() {
const comment = document.getElementById('comment').value;
const btn = document.getElementById('submit-btn');
btn.disabled = true;
btn.textContent = 'Skickar…';
try {
const r = await fetch('https://api.quixzoom.com/api/qz/ratings', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ missionId, zoomerId, rating: selectedRating, comment }),
});
const d = await r.json();
if (d.ok) {
document.getElementById('rating-form').style.display = 'none';
document.getElementById('success').style.display = 'block';
} else {
btn.disabled = false;
btn.textContent = d.error || 'Fel — försök igen';
}
} catch(e) {
btn.disabled = false;
btn.textContent = 'Nätverksfel — försök igen';
}
}
// Set zoomer display
const ZOOMER_NAMES = { z001:'Anan K.', z002:'Pim S.', z003:'Chai W.', z004:'Nong T.', z005:'Fah R.', z006:'Lek P.', z007:'Mook J.' };
const name = ZOOMER_NAMES[zoomerId] || zoomerId;
const initials = name.split(' ').map(p => p[0]).join('');
document.getElementById('zoomer-name').textContent = name;
document.getElementById('zoomer-initials').textContent = initials;
document.getElementById('zoomer-meta').textContent = `Phuket, Thailand`;
loadZoomer();
</script>
</body>
</html>