Files
internetforkids/layouts/shortcodes/world-map.html
Christian Gick 82760e3263
All checks were successful
Deploy Internet for Kids / Build & Push (push) Successful in 11s
Deploy Internet for Kids / Deploy (push) Successful in 6s
Deploy Internet for Kids / Health Check (push) Successful in 2s
Deploy Internet for Kids / Smoke Tests (push) Successful in 3s
Deploy Internet for Kids / IndexNow Ping (push) Successful in 7s
Deploy Internet for Kids / Promote to Latest (push) Successful in 2s
Deploy Internet for Kids / Rollback (push) Has been skipped
Deploy Internet for Kids / Audit (push) Successful in 2s
fix: move ocean background to SVG element itself
GPU tile seams show parent container background through gaps. Moving
background to the SVG element puts it in the same compositing layer
as the path fills, so tile seams split both identically.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 10:08:50 +03:00

255 lines
13 KiB
HTML

{{ $lang := .Page.Language.Lang }}
{{ $countries := hugo.Data.countries }}
<style>
.ifk-map-wrap { max-width: 100%; margin: 2rem 0; }
.ifk-map-wrap h3 { text-align: center; margin-bottom: 0.5rem; font-size: 1.25rem; }
.ifk-map-subtitle { text-align: center; color: #666; font-size: 0.85rem; margin-bottom: 1rem; }
.ifk-map-container { position: relative; width: 100%; height: 420px; border-radius: 8px; overflow: hidden; }
.ifk-map-svg { width: 100%; height: 100%; display: block; cursor: grab; background: #c8d3df; }
.ifk-map-svg:active { cursor: grabbing; }
.ifk-map-svg .country { stroke: #94a3b8; stroke-width: 0.3; transition: stroke-width 0.15s; }
.ifk-map-svg .country.has-data:hover { stroke: #667eea; stroke-width: 1.2; cursor: pointer; }
.ifk-map-controls { position: absolute; top: 10px; right: 10px; display: flex; flex-direction: column; gap: 4px; z-index: 10; }
.ifk-map-controls button { width: 30px; height: 30px; background: #fff; border: 1px solid #ccc; border-radius: 4px; font-size: 16px; cursor: pointer; display: flex; align-items: center; justify-content: center; color: #333; }
.ifk-map-controls button:hover { background: #f0f0f0; }
.ifk-map-popup { position: absolute; background: #fff; border-radius: 8px; box-shadow: 0 2px 12px rgba(0,0,0,0.15); padding: 0.75rem; font-size: 0.85rem; max-width: 280px; line-height: 1.4; z-index: 20; pointer-events: auto; display: none; }
.ifk-map-popup .popup-close { position: absolute; top: 4px; right: 8px; background: none; border: none; font-size: 16px; cursor: pointer; color: #999; }
.ifk-popup-status { display: inline-block; padding: 0.15rem 0.5rem; border-radius: 4px; font-weight: 600; font-size: 0.8rem; color: white; margin: 0.3rem 0; }
.ifk-popup-age { display: flex; align-items: center; gap: 0.35rem; margin: 0.3rem 0; font-size: 0.8rem; color: #555; }
.ifk-popup-detail { margin: 0.4rem 0; font-size: 0.82rem; color: #444; }
.ifk-popup-link { display: inline-block; margin-top: 0.4rem; font-size: 0.8rem; color: #667eea; text-decoration: none; font-weight: 600; }
.ifk-popup-link:hover { text-decoration: underline; }
.ifk-map-legend {
display: flex; flex-wrap: wrap; gap: 1rem; justify-content: center;
margin-top: 1rem; font-size: 0.8rem;
}
.ifk-map-legend-item { display: flex; align-items: center; gap: 0.35rem; }
.ifk-map-legend-swatch { width: 14px; height: 14px; border-radius: 3px; border: 1px solid rgba(0,0,0,0.1); }
@media (max-width: 640px) { .ifk-map-container { height: 280px; } }
</style>
<div class="ifk-map-wrap">
<h3>{{ if eq $lang "de" }}Interaktive Weltkarte: Kinderschutzgesetze{{ else if eq $lang "fr" }}Carte interactive : lois de protection de l'enfance{{ else }}Interactive Map: Child Protection Laws{{ end }}</h3>
<p class="ifk-map-subtitle">{{ if eq $lang "de" }}Klicken Sie auf ein Land für Details{{ else if eq $lang "fr" }}Cliquez sur un pays pour plus de détails{{ else }}Click a country for details{{ end }}</p>
<div class="ifk-map-container" id="ifk-map-container">
<svg class="ifk-map-svg" id="ifk-map-svg" xmlns="http://www.w3.org/2000/svg"></svg>
<div class="ifk-map-controls">
<button id="ifk-zoom-in" title="Zoom in">+</button>
<button id="ifk-zoom-out" title="Zoom out">&minus;</button>
<button id="ifk-zoom-reset" title="Reset"><svg width="14" height="14" viewBox="0 0 14 14"><path d="M2 9H0v5h5v-2H2V9zm0-4h3V3H2V0H0v5zm10 7H9v2h5V9h-2v3zM9 0v2h3v3h2V0H9z" fill="currentColor"/></svg></button>
</div>
<div class="ifk-map-popup" id="ifk-map-popup"><button class="popup-close" id="ifk-popup-close">&times;</button><div id="ifk-popup-content"></div></div>
</div>
<div class="ifk-map-legend">
<div class="ifk-map-legend-item"><div class="ifk-map-legend-swatch" style="background:#667eea"></div>{{ if eq $lang "de" }}In Kraft{{ else if eq $lang "fr" }}En vigueur{{ else }}Enforced{{ end }}</div>
<div class="ifk-map-legend-item"><div class="ifk-map-legend-swatch" style="background:#764ba2"></div>{{ if eq $lang "de" }}Verabschiedet{{ else if eq $lang "fr" }}Adopté{{ else }}Passed{{ end }}</div>
<div class="ifk-map-legend-item"><div class="ifk-map-legend-swatch" style="background:#a5b4fc"></div>{{ if eq $lang "de" }}In Bearbeitung{{ else if eq $lang "fr" }}En cours{{ else }}In Progress{{ end }}</div>
<div class="ifk-map-legend-item"><div class="ifk-map-legend-swatch" style="background:#e0e7ff"></div>{{ if eq $lang "de" }}Richtlinien{{ else if eq $lang "fr" }}Directives{{ else }}Guidelines{{ end }}</div>
<div class="ifk-map-legend-item"><div class="ifk-map-legend-swatch" style="background:#e2e8f0"></div>{{ if eq $lang "de" }}Keine Daten{{ else if eq $lang "fr" }}Pas de données{{ else }}No Data{{ end }}</div>
</div>
</div>
<script src="{{ "js/topojson-client.min.js" | relURL }}"></script>
<script>
(function() {
var LANG = "{{ $lang }}";
var STATUS_COLORS = { enforced: '#667eea', passed: '#764ba2', progress: '#a5b4fc', guidelines: '#e0e7ff' };
var STATUS_LABELS = {
en: { enforced: 'Enforced', passed: 'Passed', progress: 'In Progress', guidelines: 'Guidelines' },
de: { enforced: 'In Kraft', passed: 'Verabschiedet', progress: 'In Bearbeitung', guidelines: 'Richtlinien' },
fr: { enforced: 'En vigueur', passed: 'Adopté', progress: 'En cours', guidelines: 'Directives' }
};
var AGE_LABELS = { en: 'Min. social media age', de: 'Min. Social-Media-Alter', fr: '\u00c2ge min. r\u00e9seaux sociaux' };
var ARTICLE_ANCHORS = { AUS: 'australia', DEU: 'germany', USA: 'the-united-states', FRA: 'france', BRA: 'brazil' };
if (LANG === 'de') { ARTICLE_ANCHORS.AUS = 'australien'; ARTICLE_ANCHORS.USA = 'die-vereinigten-staaten'; ARTICLE_ANCHORS.DEU = 'deutschland'; ARTICLE_ANCHORS.BRA = 'brasilien'; }
var countries = {{ $countries | jsonify | safeJS }};
var byIsoNum = {};
countries.forEach(function(c) { byIsoNum[c.isoNum] = c; });
// Map dimensions (Equirectangular projection)
var W = 960, H = 480;
var svg = document.getElementById('ifk-map-svg');
var container = document.getElementById('ifk-map-container');
var popup = document.getElementById('ifk-map-popup');
var popupContent = document.getElementById('ifk-popup-content');
// ViewBox state
var vb = { x: 0, y: 0, w: W, h: H };
var VB_INIT = { x: 0, y: 0, w: W, h: H };
var MIN_W = W / 16; // max zoom in
function setViewBox() {
svg.setAttribute('viewBox', vb.x + ' ' + vb.y + ' ' + vb.w + ' ' + vb.h);
}
// Equirectangular projection
function projectCoord(lon, lat) {
return [(lon + 180) * (W / 360), (90 - lat) * (H / 180)];
}
function projectRing(ring) {
var d = '';
for (var i = 0; i < ring.length; i++) {
var p = projectCoord(ring[i][0], ring[i][1]);
d += (i === 0 ? 'M' : 'L') + p[0].toFixed(1) + ',' + p[1].toFixed(1);
}
return d + 'Z';
}
function projectGeometry(geom) {
var d = '';
if (geom.type === 'Polygon') {
geom.coordinates.forEach(function(ring) { d += projectRing(ring); });
} else if (geom.type === 'MultiPolygon') {
geom.coordinates.forEach(function(poly) {
poly.forEach(function(ring) { d += projectRing(ring); });
});
}
return d;
}
// Build SVG
fetch('{{ "data/countries-50m.json" | relURL }}')
.then(function(r) { return r.json(); })
.then(function(topo) {
var geo = topojson.feature(topo, topo.objects.countries);
var borders = topojson.mesh(topo, topo.objects.countries, function(a, b) { return a !== b; });
// Land base fill (covers gaps at poles between country polygons)
var land = topojson.feature(topo, topo.objects.land);
var landPath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
landPath.setAttribute('d', projectGeometry(land.features ? land.features[0].geometry : land.geometry));
landPath.setAttribute('fill', '#ffffff');
landPath.setAttribute('stroke', 'none');
landPath.setAttribute('pointer-events', 'none');
svg.appendChild(landPath);
// Country fills (ocean is CSS background on container)
geo.features.forEach(function(f) {
var id = String(f.id).padStart(3, '0');
var c = byIsoNum[id];
var path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
var d = projectGeometry(f.geometry);
if (!d) return;
path.setAttribute('d', d);
path.setAttribute('fill', c ? STATUS_COLORS[c.status] : '#ffffff');
path.setAttribute('fill-opacity', c ? '0.65' : '1');
path.setAttribute('class', 'country' + (c ? ' has-data' : ''));
path.setAttribute('data-iso', id);
svg.appendChild(path);
});
// Borders (shared edges only)
var bordersD = projectGeometry(borders);
if (bordersD) {
var borderPath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
borderPath.setAttribute('d', bordersD);
borderPath.setAttribute('fill', 'none');
borderPath.setAttribute('stroke', '#94a3b8');
borderPath.setAttribute('stroke-width', '0.3');
borderPath.setAttribute('pointer-events', 'none');
svg.appendChild(borderPath);
}
setViewBox();
// Click handler
svg.addEventListener('click', function(e) {
var target = e.target;
if (!target.classList.contains('has-data')) { popup.style.display = 'none'; return; }
var id = target.getAttribute('data-iso');
var c = byIsoNum[id];
if (!c) return;
var loc = c[LANG] || c.en;
var labels = STATUS_LABELS[LANG] || STATUS_LABELS.en;
var ageLabel = AGE_LABELS[LANG] || AGE_LABELS.en;
var html = '<strong>' + c.flag + ' ' + loc.name + '</strong><br>' +
'<em>' + loc.law + '</em><br>' +
'<span class="ifk-popup-status" style="background:' + STATUS_COLORS[c.status] + '">' + labels[c.status] + ' (' + c.year + ')</span>' +
'<div class="ifk-popup-age">\u{1F464} ' + ageLabel + ': <strong>' + c.ageLimitSocial + '+</strong></div>' +
'<div class="ifk-popup-detail">' + loc.detail + '</div>';
var anchor = ARTICLE_ANCHORS[c.iso3];
if (anchor) {
var readMore = { en: 'Read more \u2193', de: 'Weiterlesen \u2193', fr: 'En savoir plus \u2193' };
html += '<a class="ifk-popup-link" href="#' + anchor + '">' + (readMore[LANG] || readMore.en) + '</a>';
}
popupContent.innerHTML = html;
// Position popup near click
var rect = container.getBoundingClientRect();
var px = e.clientX - rect.left;
var py = e.clientY - rect.top;
// Keep popup within container
popup.style.display = 'block';
var pw = popup.offsetWidth;
var ph = popup.offsetHeight;
var left = Math.min(px + 10, rect.width - pw - 10);
var top = Math.max(py - ph - 10, 10);
if (top < 10) top = py + 10;
popup.style.left = left + 'px';
popup.style.top = top + 'px';
});
// Close popup
document.getElementById('ifk-popup-close').addEventListener('click', function(e) {
e.stopPropagation();
popup.style.display = 'none';
});
// Zoom controls
function zoomBy(factor) {
var cx = vb.x + vb.w / 2;
var cy = vb.y + vb.h / 2;
var nw = Math.max(MIN_W, Math.min(VB_INIT.w, vb.w * factor));
var nh = nw * (H / W);
vb.w = nw; vb.h = nh;
vb.x = Math.max(0, Math.min(W - vb.w, cx - vb.w / 2));
vb.y = Math.max(0, Math.min(H - vb.h, cy - vb.h / 2));
setViewBox();
}
document.getElementById('ifk-zoom-in').addEventListener('click', function() { zoomBy(0.5); });
document.getElementById('ifk-zoom-out').addEventListener('click', function() { zoomBy(2); });
document.getElementById('ifk-zoom-reset').addEventListener('click', function() {
vb.x = VB_INIT.x; vb.y = VB_INIT.y; vb.w = VB_INIT.w; vb.h = VB_INIT.h;
setViewBox();
});
// Pan via drag
var dragging = false, dragStart = null;
svg.addEventListener('mousedown', function(e) {
if (e.target.closest('.ifk-map-controls') || e.target.closest('.ifk-map-popup')) return;
dragging = true;
dragStart = { x: e.clientX, y: e.clientY, vbx: vb.x, vby: vb.y };
e.preventDefault();
});
window.addEventListener('mousemove', function(e) {
if (!dragging) return;
var scale = vb.w / svg.clientWidth;
vb.x = Math.max(0, Math.min(W - vb.w, dragStart.vbx - (e.clientX - dragStart.x) * scale));
vb.y = Math.max(0, Math.min(H - vb.h, dragStart.vby - (e.clientY - dragStart.y) * scale));
setViewBox();
});
window.addEventListener('mouseup', function() { dragging = false; });
// Touch support
svg.addEventListener('touchstart', function(e) {
if (e.touches.length !== 1) return;
dragging = true;
dragStart = { x: e.touches[0].clientX, y: e.touches[0].clientY, vbx: vb.x, vby: vb.y };
}, { passive: true });
svg.addEventListener('touchmove', function(e) {
if (!dragging || e.touches.length !== 1) return;
var scale = vb.w / svg.clientWidth;
vb.x = Math.max(0, Math.min(W - vb.w, dragStart.vbx - (e.touches[0].clientX - dragStart.x) * scale));
vb.y = Math.max(0, Math.min(H - vb.h, dragStart.vby - (e.touches[0].clientY - dragStart.y) * scale));
setViewBox();
}, { passive: true });
svg.addEventListener('touchend', function() { dragging = false; }, { passive: true });
});
})();
</script>