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
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>
255 lines
13 KiB
HTML
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">−</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">×</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>
|