chore: checkpoint initial — Zektyc dataset, avant humanisation

- site vitrine + fingerprint test + zmap + z-panel
- remplacement duckdns -> riricdev.tail5ea5cd.ts.net (canonical/og/alternates/docs)
- suppression du token DuckDNS (plus utilisé)
This commit is contained in:
riricdev 2026-09-06 13:03:25 +02:00
commit d7ec32a458
92 changed files with 17476 additions and 0 deletions

229
public/js/components.js Normal file
View file

@ -0,0 +1,229 @@
(function () {
var SVG = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 16" role="presentation" focusable="false"><path d="M5 4h22L5 12h22" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/></svg>';
var BRAND = '<span class="brand-mark" aria-hidden="true">' + SVG + '</span><span class="brand-name">Zektyc</span>';
var SUN = '<svg class="icon-sun" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg>';
var MOON = '<svg class="icon-moon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>';
var THEME_BTN = '<button type="button" class="theme-btn" aria-label="Theme">' + MOON + SUN + '</button>';
function path() { return location.pathname; }
function lang() { return /^\/en(\/|$)/.test(path()) ? 'en' : 'fr'; }
function isHome() { var p = path(); return p === '/' || p === '/index.html' || p === '/en/' || p === '/en/index.html'; }
function isFpTest() { return /\/fingerprint-test/.test(path()); }
function isSocial() { return /\/social/.test(path()); }
function frLink() {
var p = path();
if (p === '/en/') return '/';
var m = p.match(/^(\/[^/]+)\/en\/?$/);
if (m) return m[1] + '/';
return p;
}
function enLink() {
var p = path();
if (p === '/' || p === '/index.html') return '/en/';
if (/\/en\/?$/.test(p)) return p;
return p.replace(/\/?$/, '/en/');
}
function buildHeader() {
var h = lang();
var skip = h === 'fr' ? 'Aller au contenu' : 'Skip to content';
var html = '<a class="skip-link" href="#main">' + skip + '</a>';
html += '<a class="brand" href="/">' + BRAND + '</a>';
html += '<input type="checkbox" id="nav-toggle" class="nav-toggle-input" />';
html += '<label for="nav-toggle" class="nav-burger" aria-label="Menu">';
html += '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M4 6h16M4 12h16M4 18h16"/></svg>';
html += '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M6 6l12 12M18 6L6 18"/></svg>';
html += '</label>';
if (isSocial()) {
html += '<nav id="nav" class="site-nav" aria-label="Navigation">';
html += '<div class="lang-switch" role="group" aria-label="Langue / Language">';
html += '<button type="button" data-lang="fr" class="lang-btn' + (h === 'fr' ? ' active' : '') + '">' + (h === 'fr' ? '<strong>FR</strong>' : 'FR') + '</button>';
html += '<button type="button" data-lang="en" class="lang-btn' + (h === 'en' ? ' active' : '') + '">' + (h === 'en' ? '<strong>EN</strong>' : 'EN') + '</button>';
html += '</div>';
html += THEME_BTN;
html += '</nav>';
} else {
html += '<nav class="site-nav" aria-label="Navigation">';
html += '<a href="/blog" data-lang-path="/blog" data-i18n="nav.blog">Blog</a>';
html += '<a href="/projets" data-lang-path="/projets" data-i18n="nav.projects">Projets</a>';
html += '<a href="/equipe" data-lang-path="/equipe" data-i18n="nav.team">\u00c9quipe</a>';
if (isHome()) {
html += '<a href="#principes" data-i18n="nav.principes">Nos principes</a>';
html += '<a href="#faq" data-i18n="nav.faq">FAQ</a>';
html += '<a href="#contact" data-i18n="nav.contact">Contact</a>';
}
html += '<div class="lang-switch" role="group" aria-label="Langue / Language">';
html += '<button type="button" data-lang="fr" class="lang-btn' + (h === 'fr' ? ' active' : '') + '">' + (h === 'fr' ? '<strong>FR</strong>' : 'FR') + '</button>';
html += '<button type="button" data-lang="en" class="lang-btn' + (h === 'en' ? ' active' : '') + '">' + (h === 'en' ? '<strong>EN</strong>' : 'EN') + '</button>';
html += '</div>';
html += THEME_BTN;
html += '</nav>';
}
return '<header class="site-header">' + html + '</header>';
}
function buildFooter() {
var h = lang();
var tos = h === 'fr' ? "Conditions d\u2019utilisation" : 'Terms of Service';
var priv = h === 'fr' ? 'Confidentialit\u00e9' : 'Privacy';
var note = h === 'fr' ? 'Ce site ne collecte rien. Aucun cookie, aucun tracker.' : 'This site collects nothing. No cookies, no trackers.';
var tosHref = h === 'fr' ? '/tos' : '/en/tos';
var privHref = h === 'fr' ? '/privacy-policy' : '/en/privacy-policy';
var html = '';
if (isSocial()) {
var backText = h === 'fr' ? 'Retour au site' : 'Back to site';
html += '<p><a href="/" class="footer-back">' + backText + ' \u2190</a></p>';
}
html += '<p>Zektyc \u00b7 France</p>';
html += '<p class="footer-note">' + note + '</p>';
html += '<nav class="footer-nav" aria-label="Legal">';
html += '<a rel="me" href="https://piaille.fr/@zektyc"><img src="/icons/mastodon.svg" alt="" width="14" height="14" class="footer-icon" /> Mastodon</a>';
html += '<span class="footer-sep" aria-hidden="true">\u00b7</span>';
html += '<a href="' + tosHref + '">' + tos + '</a>';
html += '<span class="footer-sep" aria-hidden="true">\u00b7</span>';
html += '<a href="' + privHref + '">' + priv + '</a>';
html += '<span class="footer-sep" aria-hidden="true">\u00b7</span>';
html += '<a href="/docs/maxspeed">API</a>';
html += '<span class="footer-sep" aria-hidden="true">\u00b7</span>';
html += '<a href="/.well-known/security.txt">security.txt</a>';
html += '<span class="footer-sep" aria-hidden="true">\u00b7</span>';
html += '<a href="http://l5cbkqmok4rnkhbtpltmtlf3pa5i5rfw23z2mtj5tugpo3quta3b5hqd.onion">Tor</a>';
html += '</nav>';
return '<footer class="site-footer">' + html + '</footer>';
}
function inject() {
var hs = document.getElementById('h');
var fs = document.getElementById('f');
if (hs) hs.outerHTML = buildHeader();
if (fs) fs.outerHTML = buildFooter();
var nav = document.getElementById('nav-toggle');
if (nav) {
document.addEventListener('click', function (e) {
if (e.target.closest && e.target.closest('.site-nav')) nav.checked = false;
});
}
}
function isTorBrowser() {
var dbg = {
hostname: location.hostname,
userAgent: navigator.userAgent,
hasTorSensor: !!window.TorSensor,
uaMatch: /TorBrowser|Tor\/|Tor Project/i.test(navigator.userAgent),
timezoneOffset: new Date().getTimezoneOffset(),
gmt: (new Date().getTimezoneOffset() === 0),
screenW: window.screen && window.screen.width,
screenH: window.screen && window.screen.height,
innerW: window.innerWidth,
innerH: window.innerHeight,
letterboxed: !!(window.screen &&
window.screen.width === window.innerWidth &&
window.screen.height === window.innerHeight),
pluginCount: (navigator.plugins && navigator.plugins.length) || 0,
noPlugins: !navigator.plugins || navigator.plugins.length === 0,
vendor: navigator.vendor
};
if (isOnion()) {
dbg.result = 'onion:' + location.hostname;
console.log('[TorToast] on .onion, skip', dbg);
return true;
}
if (dbg.hasTorSensor) {
dbg.result = 'TorSensor';
console.log('[TorToast] detected via TorSensor', dbg);
return true;
}
if (dbg.uaMatch) {
dbg.result = 'userAgent';
console.log('[TorToast] detected via userAgent', dbg);
return true;
}
if (dbg.gmt && dbg.letterboxed) {
dbg.result = 'gmt+letterbox';
console.log('[TorToast] detected via gmt+letterbox', dbg);
return true;
}
if (dbg.gmt && dbg.letterboxed && dbg.noPlugins) {
dbg.result = 'gmt+letterbox+noPlugins';
console.log('[TorToast] detected via gmt+letterbox+noPlugins', dbg);
return true;
}
if (dbg.vendor === '' && dbg.gmt && dbg.noPlugins) {
dbg.result = 'vendorEmpty+gmt+noPlugins';
console.log('[TorToast] detected via vendorEmpty+gmt+noPlugins', dbg);
return true;
}
if (dbg.gmt && dbg.letterboxed && dbg.vendor === '') {
dbg.result = 'gmt+letterbox+vendorEmpty';
console.log('[TorToast] detected via gmt+letterbox+vendorEmpty', dbg);
return true;
}
dbg.result = 'NOT tor';
console.log('[TorToast] not detected as Tor', dbg);
return false;
}
function isOnion() {
return location.hostname.indexOf('.onion') !== -1;
}
function showTorToast() {
if (!isTorBrowser() || isOnion()) return;
var onionBase = 'http://l5cbkqmok4rnkhbtpltmtlf3pa5i5rfw23z2mtj5tugpo3quta3b5hqd.onion';
var path = location.pathname + location.search + location.hash;
var onionUrl = onionBase + path;
var toast = document.createElement('div');
toast.className = 'tor-toast';
toast.setAttribute('role', 'alert');
var h = lang();
var msg = h === 'fr'
? 'Vous utilisez Tor. Acc\u00e9dez \u00e0 la version .onion pour plus de s\u00e9curit\u00e9.'
: 'You are using Tor. Access the .onion version for better security.';
toast.innerHTML =
'<p>' + msg + '</p>' +
'<a href="' + onionUrl + '" class="tor-toast-btn">' + (h === 'fr' ? 'Ouvrir en .onion' : 'Open .onion') + '</a>' +
'<button type="button" class="tor-toast-close" aria-label="Fermer">&times;</button>';
document.body.appendChild(toast);
toast.querySelector('.tor-toast-close').addEventListener('click', function () {
toast.remove();
});
setTimeout(function () { toast.remove(); }, 30000);
}
function boot() {
inject();
showTorToast();
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', boot);
} else {
boot();
}
})();

39
public/js/discord.js Normal file
View file

@ -0,0 +1,39 @@
(function () {
var el = document.getElementById("discord-presence")
if (!el) return
function render() {
var lang = document.documentElement.lang
var n = parseInt(el.getAttribute("data-count") || "0", 10)
if (!(n > 0)) return
var text =
n === 1
? lang === "fr"
? "1 membre en ligne"
: "1 member online"
: lang === "fr"
? n + " membres en ligne"
: n + " members online"
el.textContent = text
el.classList.add("is-online")
}
fetch("/api/discord")
.then(function (res) {
return res.ok ? res.json() : null
})
.then(function (data) {
if (data && typeof data.presence_count === "number") {
el.setAttribute("data-count", String(data.presence_count))
}
render()
})
.catch(function () {
el.classList.remove("is-online")
})
document.addEventListener("click", function (e) {
var btn = e.target.closest ? e.target.closest("[data-lang]") : null
if (btn) render()
})
})()

141
public/js/i18n.js Normal file
View file

@ -0,0 +1,141 @@
(function () {
var STORAGE_KEY = "zektyc-lang"
var DEFAULT_LANG = "fr"
var translations = {}
function urlLang() {
var path = window.location.pathname
if (path === "/en" || path.indexOf("/en/") === 0) return "en"
if (path === "/fr" || path.indexOf("/fr/") === 0) return "fr"
return null
}
function detect() {
var forced = urlLang()
if (forced) return forced
try {
var stored = localStorage.getItem(STORAGE_KEY)
if (stored && translations[stored]) return stored
} catch (e) {}
var candidates = []
if (navigator.languages && navigator.languages.length) {
candidates = candidates.concat(navigator.languages)
}
if (navigator.language) candidates.push(navigator.language)
if (navigator.userLanguage) candidates.push(navigator.userLanguage)
for (var i = 0; i < candidates.length; i++) {
var raw = String(candidates[i] || "").trim().toLowerCase()
if (!raw) continue
if (raw.indexOf("en") === 0) return "en"
if (raw.indexOf("fr") === 0) return "fr"
}
return DEFAULT_LANG
}
function apply(lang) {
var dict = translations[lang]
if (!dict) return
document.documentElement.lang = lang
if (dict["meta.title"]) document.title = dict["meta.title"]
var desc = document.querySelector('meta[name="description"]')
if (desc && dict["meta.description"]) {
desc.setAttribute("content", dict["meta.description"])
}
var ogTitle = document.querySelector('meta[property="og:title"]')
if (ogTitle && dict["meta.title"]) {
ogTitle.setAttribute("content", dict["meta.title"])
}
var ogDesc = document.querySelector('meta[property="og:description"]')
if (ogDesc && dict["meta.description"]) {
ogDesc.setAttribute("content", dict["meta.description"])
}
var nodes = document.querySelectorAll("[data-i18n]")
for (var i = 0; i < nodes.length; i++) {
var key = nodes[i].getAttribute("data-i18n")
if (dict[key] !== undefined) nodes[i].textContent = dict[key]
}
var btns = document.querySelectorAll("[data-lang]")
for (var j = 0; j < btns.length; j++) {
btns[j].classList.toggle(
"active",
btns[j].getAttribute("data-lang") === lang
)
}
var langLinks = document.querySelectorAll("[data-lang-path]")
for (var k = 0; k < langLinks.length; k++) {
langLinks[k].setAttribute(
"href",
"/" + lang + langLinks[k].getAttribute("data-lang-path")
)
}
}
fetch("/data/i18n.json", { cache: "no-cache" })
.then(function (res) {
if (!res.ok) throw new Error("status " + res.status)
return res.json()
})
.then(function (data) {
if (data && typeof data === "object") translations = data
})
.catch(function (e) {
if (window.console && console.warn) {
console.warn("[i18n] impossible de charger les traductions:", e)
}
})
.then(function () {
var detected = detect()
if (window.console && console.info) {
console.info(
"[i18n] langue détectée:",
detected,
"| navigator.language:",
navigator.language,
"| navigator.languages:",
navigator.languages
)
}
apply(detected)
document.addEventListener("click", function (e) {
var btn = e.target.closest ? e.target.closest("[data-lang]") : null
if (!btn) return
var lang = btn.getAttribute("data-lang")
if (!translations[lang]) return
try {
localStorage.setItem(STORAGE_KEY, lang)
} catch (err) {}
apply(lang)
var currentPath = window.location.pathname
var isHome = currentPath === "/" || currentPath === "/index.html" || currentPath === "/en/" || currentPath === "/en/index.html"
if (isHome) {
var newUrl = lang === "fr" ? "/" : "/en/"
if (currentPath !== newUrl) history.pushState(null, "", newUrl)
return
}
var target = "/" + lang + "/" + (window.location.hash || "")
if (window.location.pathname !== "/" + lang + "/") {
window.location.href = target
}
})
var navToggle = document.getElementById("nav-toggle")
if (navToggle) {
document.addEventListener("click", function (e) {
if (e.target.closest && e.target.closest(".site-nav")) {
navToggle.checked = false
}
})
}
})
})()

10
public/js/legal-lang.js Normal file
View file

@ -0,0 +1,10 @@
(function () {
var lang = document.documentElement.lang === "en" ? "en" : "fr"
var links = document.querySelectorAll("[data-lang-path]")
for (var i = 0; i < links.length; i++) {
links[i].setAttribute(
"href",
"/" + lang + links[i].getAttribute("data-lang-path")
)
}
})()

2
public/js/social-auth.js Normal file
View file

@ -0,0 +1,2 @@
// Zektyc Social — Auth views (moved to social-views.js)
// This file is kept for backward compatibility but functions are now in social-views.js

554
public/js/social-core.js Normal file
View file

@ -0,0 +1,554 @@
// Zektyc Social — Core v4 (CSP-safe)
window.ZS = {
user: null,
view: "feed",
viewData: null,
lang: document.documentElement.lang === "en" ? "en" : "fr",
_navOpen: false,
_unreadNotifs: 0,
_unreadMessages: 0,
icons: {
home: '<svg viewBox="0 0 24 24"><path d="M3 9.5L12 3l9 6.5V20a1 1 0 01-1 1H4a1 1 0 01-1-1V9.5z"/><polyline points="9 21 9 12 15 12 15 21"/></svg>',
users: '<svg viewBox="0 0 24 24"><path d="M17 21v-2a4 4 0 00-4-4H5a4 4 0 00-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 00-3-3.87"/><path d="M16 3.13a4 4 0 010 7.75"/></svg>',
mail: '<svg viewBox="0 0 24 24"><rect x="2" y="4" width="20" height="16" rx="2"/><polyline points="22 6 12 13 2 6"/></svg>',
bell: '<svg viewBox="0 0 24 24"><path d="M18 8A6 6 0 006 8c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 01-3.46 0"/></svg>',
user: '<svg viewBox="0 0 24 24"><path d="M20 21v-2a4 4 0 00-4-4H8a4 4 0 00-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>',
shield: '<svg viewBox="0 0 24 24"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>',
search: '<svg viewBox="0 0 24 24"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>',
pen: '<svg viewBox="0 0 24 24"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 013 3L7 19l-4 1 1-4L16.5 3.5z"/></svg>',
heart: '<svg viewBox="0 0 24 24"><path d="M20.84 4.61a5.5 5.5 0 00-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 00-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 000-7.78z"/></svg>',
heartFilled: '<svg viewBox="0 0 24 24"><path d="M20.84 4.61a5.5 5.5 0 00-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 00-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 000-7.78z" fill="currentColor"/></svg>',
comment: '<svg viewBox="0 0 24 24"><path d="M21 15a2 2 0 01-2 2H7l-4 4V5a2 2 0 012-2h14a2 2 0 012 2z"/></svg>',
note: '<svg viewBox="0 0 24 24"><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>',
flag: '<svg viewBox="0 0 24 24"><path d="M4 15s1-1 4-1 5 2 8 2 4-1 4-1V3s-1 1-4 1-5-2-8-2-4 1-4 1z"/><line x1="4" y1="22" x2="4" y2="15"/></svg>',
check: '<svg viewBox="0 0 24 24"><polyline points="20 6 9 17 4 12"/></svg>',
verifiedBlue: '<svg viewBox="0 0 24 24"><path d="M9 12l2 2 4-4" fill="none" stroke="#1d9bf0" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/><circle cx="12" cy="12" r="10" fill="none" stroke="#1d9bf0" stroke-width="2"/></svg>',
verifiedGold: '<svg viewBox="0 0 24 24"><path d="M9 12l2 2 4-4" fill="none" stroke="#f5a623" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/><circle cx="12" cy="12" r="10" fill="none" stroke="#f5a623" stroke-width="2"/></svg>',
close: '<svg viewBox="0 0 24 24"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>',
arrowLeft: '<svg viewBox="0 0 24 24"><line x1="19" y1="12" x2="5" y2="12"/><polyline points="12 19 5 12 12 5"/></svg>',
arrowRight: '<svg viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg>',
star: '<svg viewBox="0 0 24 24"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>',
lock: '<svg viewBox="0 0 24 24"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0110 0v4"/></svg>',
globe: '<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 014 10 15.3 15.3 0 01-4 10 15.3 15.3 0 01-4-10 15.3 15.3 0 014-10z"/></svg>',
settings: '<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 010 2.83 2 2 0 01-2.83 0l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-4 0v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 01-2.83-2.83l.06-.06A1.65 1.65 0 004.68 15a1.65 1.65 0 00-1.51-1H3a2 2 0 010-4h.09A1.65 1.65 0 004.6 9a1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 012.83-2.83l.06.06A1.65 1.65 0 009 4.68a1.65 1.65 0 001-1.51V3a2 2 0 014 0v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 012.83 2.83l-.06.06A1.65 1.65 0 0019.4 9a1.65 1.65 0 001.51 1H21a2 2 0 010 4h-.09a1.65 1.65 0 00-1.51 1z"/></svg>',
logout: '<svg viewBox="0 0 24 24"><path d="M9 21H5a2 2 0 01-2-2V5a2 2 0 012-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" y1="12" x2="9" y2="12"/></svg>',
menu: '<svg viewBox="0 0 24 24"><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="18" x2="21" y2="18"/></svg>',
plus: '<svg viewBox="0 0 24 24"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>',
send: '<svg viewBox="0 0 24 24"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg>',
trash: '<svg viewBox="0 0 24 24"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"/></svg>',
reply: '<svg viewBox="0 0 24 24"><polyline points="9 17 4 12 9 7"/><path d="M20 18v-2a4 4 0 00-4-4H4"/></svg>',
user: '<svg viewBox="0 0 24 24"><path d="M20 21v-2a4 4 0 00-4-4H8a4 4 0 00-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>',
},
_updateSEO: function(title, desc, url) {
if (title) document.title = title;
var md = document.querySelector('meta[name="description"]');
if (md && desc) md.content = desc;
var og = document.querySelector('meta[property="og:title"]');
if (og && title) og.content = title;
var ogd = document.querySelector('meta[property="og:description"]');
if (ogd && desc) ogd.content = desc;
var ogu = document.querySelector('meta[property="og:url"]');
if (ogu && url) ogu.content = url;
},
icon(name, cls) {
var svg = this.icons[name] || '';
if (cls) return '<span class="social-icon ' + cls + '">' + svg + '</span>';
return '<span class="social-icon">' + svg + '</span>';
},
// ── API ──
async api(method, path, body) {
var opts = { method: method, credentials: "same-origin" };
if (body instanceof FormData) {
opts.body = body;
} else if (body) {
opts.headers = { "Content-Type": "application/json" };
opts.body = JSON.stringify(body);
}
try { var res = await fetch(path, opts); return res.json(); }
catch (e) { return { error: "Erreur réseau" }; }
},
esc(s) {
if (!s) return "";
var d = document.createElement("div");
d.textContent = s;
return d.innerHTML;
},
navigate(view, data) {
this.view = view;
this.viewData = data || null;
var basePath = location.pathname.replace(/\/(profile|post|chat|settings|notifications|community|mod|messages)\/?.*$/, "").replace(/\/$/, "");
var newPath = basePath;
if (view === "profile-view" && data) newPath += "/profile/" + encodeURIComponent(data);
else if (view === "chat" && data) newPath += "/chat/" + encodeURIComponent(data);
else if (view === "post" && data) newPath += "/post/" + encodeURIComponent(data);
else if (view !== "feed") newPath += "/" + view;
if (location.pathname !== newPath || location.hash) {
history.pushState({ view: view, data: data }, "", newPath);
}
window.scrollTo(0, 0);
if (window._socialRender) window._socialRender();
},
_parsePath() {
var path = location.pathname;
var profileMatch = path.match(/\/social\/profile\/([a-zA-Z0-9_]+)/);
if (profileMatch) return { view: "profile-view", data: profileMatch[1] };
var chatMatch = path.match(/\/social\/chat\/([a-zA-Z0-9_]+)/);
if (chatMatch) return { view: "chat", data: chatMatch[1] };
var postMatch = path.match(/\/social\/post\/(\d+)/);
if (postMatch) return { view: "post", data: postMatch[1] };
var viewMatch = path.match(/\/social\/(settings|notifications|community|mod|messages|login|signup)/);
if (viewMatch) return { view: viewMatch[1], data: null };
var hashView = location.hash.replace(/^#\/?/, "");
if (hashView) {
var parts = hashView.split("/");
return { view: parts[0], data: parts[1] ? decodeURIComponent(parts[1]) : null };
}
return { view: "feed", data: null };
},
t(fr, en) { return this.lang === "fr" ? fr : en; },
// ── Layout shell ──
renderShell(sidebarLeft, mainContent, sidebarRight) {
var header = document.getElementById("social-header");
if (header) header.innerHTML = this._buildHeader();
var layout = document.getElementById("social-layout");
if (!layout) return;
var left = sidebarLeft != null ? sidebarLeft : this._buildSidebarLeft();
var right = sidebarRight != null ? sidebarRight : this._buildSidebarRight();
layout.innerHTML =
'<aside class="social-sidebar-left">' + left + '</aside>' +
'<section class="social-main" id="social-main">' + (typeof mainContent === "string" ? mainContent : "") + '</section>' +
'<aside class="social-sidebar-right">' + right + '</aside>';
var navEl = document.getElementById("socialHeaderNav");
if (navEl) navEl.innerHTML = this._buildNavLinks();
if (typeof _applyAvatarStyles === "function") _applyAvatarStyles();
},
_buildHeader() {
return '' +
'<a href="#" class="social-logo" data-action="navFeed"><img src="/favicon.svg" alt="Zektyc" class="social-logo-img"> Zektyc Social</a>' +
'<div class="social-header-search">' +
'<span class="social-header-search-icon">' + this.icon("search") + '</span>' +
'<input type="text" placeholder="' + (this.lang === "fr" ? "Rechercher" : "Search") + '" id="socialSearchInput" data-key="Enter" data-key-action="doSearch">' +
'</div>' +
'<nav class="social-header-nav" id="socialHeaderNav">' + this._buildNavLinks() + '</nav>' +
(this.user
? '<div class="social-header-user"><div class="social-header-avatar" data-action="navProfile">' + this.esc(this.user.username).charAt(0).toUpperCase() + '</div></div>'
: '') +
'<button class="social-mobile-toggle" data-action="toggleMobileNav">' + this.icon("menu") + '</button>';
},
_buildNavLinks() {
var L = this.lang;
var v = this.view;
var self = this;
function link(view, iconName, label, badge) {
var cls = v === view ? " nav-active" : "";
return '<a href="#" data-action="nav" data-arg1="' + view + '" class="' + cls + '">' +
self.icon(iconName) + '<span>' + label + '</span>' +
(badge > 0 ? '<span class="nav-badge">' + badge + '</span>' : '') + '</a>';
}
var html = "";
if (this.user) {
html = link("feed", "home", L ? "Accueil" : "Home", 0) +
link("community", "users", L ? "Communauté" : "Community", 0) +
link("messages", "mail", L ? "Messages" : "Messages", this._unreadMessages) +
link("notifications", "bell", L ? "Alertes" : "Alerts", this._unreadNotifs) +
link("profile", "user", L ? "Profil" : "Profile", 0);
if (this.user.role === "admin" || this.user.role === "moderator") {
html += link("mod", "shield", L ? "Modération" : "Mod", 0);
}
html += '<a href="#" data-action="logout" title="' + (L ? "Déconnexion" : "Logout") + '">' + this.icon("logout") + '</a>';
} else {
html = link("feed", "home", L ? "Accueil" : "Home", 0) +
'<a href="#" data-action="nav" data-arg1="login" class="' + (v === "login" ? " nav-active" : "") + '">' +
this.icon("arrowRight") + '<span>' + (L ? "Connexion" : "Login") + '</span></a>' +
'<a href="#" data-action="nav" data-arg1="signup" class="' + (v === "signup" ? " nav-active" : "") + '">' +
this.icon("star") + '<span>' + (L ? "Inscription" : "Sign up") + '</span></a>';
}
return html;
},
_buildSidebarLeft() {
var L = this.lang;
var v = this.view;
var self = this;
var html = "";
if (this.user) {
html += '<div class="sidebar-profile" data-action="navProfile">' +
'<div class="sidebar-profile-avatar">' +
(this.user.avatarUrl ? '<img src="' + this.esc(this.user.avatarUrl) + '" alt="' + this.esc(this.user.username) + '" loading="lazy">' : this.esc(this.user.username).charAt(0).toUpperCase()) +
'</div>' +
'<div><div class="sidebar-profile-name">' + this.esc(this.user.username) + '</div>' +
'<div class="sidebar-profile-handle">@' + this.esc(this.user.username) + '</div></div></div>';
}
function navLink(view, iconName, label, badge) {
var cls = v === view ? " active" : "";
return '<a href="#" class="sidebar-nav-link' + cls + '" data-action="nav" data-arg1="' + view + '">' +
self.icon(iconName) +
'<span class="sidebar-nav-label">' + label + '</span>' +
(badge > 0 ? '<span class="sidebar-nav-badge">' + badge + '</span>' : '') + '</a>';
}
if (this.user) {
html += '<div class="mt-16">' +
navLink("feed", "home", L ? "Fil d'actualité" : "Feed", 0) +
navLink("community", "users", L ? "Communauté" : "Community", 0) +
navLink("messages", "mail", L ? "Messages" : "Messages", this._unreadMessages) +
navLink("notifications", "bell", L ? "Alertes" : "Alerts", this._unreadNotifs) +
navLink("profile", "user", L ? "Mon profil" : "My profile", 0) +
(this.user.role === "admin" || this.user.role === "moderator" ? navLink("mod", "shield", L ? "Modération" : "Moderation", 0) : '') +
'</div>';
} else {
html += '<div class="mt-16">' +
navLink("login", "arrowRight", L ? "Se connecter" : "Log in", 0) +
navLink("signup", "star", L ? "S'inscrire" : "Sign up", 0) + '</div>';
}
html += '<div class="sidebar-footer mt-24"><a href="/tos">CGU</a> · <a href="/privacy-policy">' + (L ? "Confidentialité" : "Privacy") + '</a><br>© 2025 Zektyc</div>';
return html;
},
_buildSidebarRight() {
var L = this.lang;
return '<div class="sidebar-widget"><h3>' + (L ? "À propos" : "About") + '</h3>' +
'<p class="text-sm text-muted lh-15">' +
(L ? "Réseau social respectueux de la vie privée. Pas d'algorithme, pas de pub, pas de tracking."
: "Privacy-respecting social network. No algorithm, no ads, no tracking.") + '</p></div>';
},
_renderMain(html) {
var el = document.getElementById("social-main");
if (el) el.innerHTML = html;
},
toggleMobileNav() {
var nav = document.getElementById("socialHeaderNav");
if (!nav) return;
this._navOpen = !this._navOpen;
nav.classList.toggle("open", this._navOpen);
},
doSearch() {
var q = document.getElementById("socialSearchInput");
if (!q || !q.value.trim()) return;
this.navigate("community");
setTimeout(function() {
var si = document.getElementById("searchInput");
if (si) { si.value = q.value; si.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter" })); }
}, 100);
},
async logout() {
await this.api("POST", "/api/social/auth/logout");
this.user = null;
this._unreadNotifs = 0;
this._unreadMessages = 0;
this.navigate("feed");
},
async checkSession() {
var data = await this.api("GET", "/api/social/auth/me");
if (data.error) return false;
this.user = data.user;
this._unreadNotifs = data.unreadNotifs || 0;
this._unreadMessages = data.unreadMessages || 0;
return true;
},
async refreshCounts() {
if (!this.user) return;
var data = await this.api("GET", "/api/social/auth/me");
if (!data.error) {
this._unreadNotifs = data.unreadNotifs || 0;
this._unreadMessages = data.unreadMessages || 0;
this._updateNavBadges();
}
},
_updateNavBadges() {
var nav = document.getElementById("socialHeaderNav");
if (nav) nav.innerHTML = this._buildNavLinks();
var layout = document.getElementById("social-layout");
if (layout) {
var left = layout.querySelector(".social-sidebar-left");
if (left) left.innerHTML = this._buildSidebarLeft();
}
},
};
// ═══════════════════════════════════════
// EVENT DELEGATION (CSP-safe)
// ═══════════════════════════════════════
(function() {
// Click delegation
document.addEventListener("click", function(e) {
var el = e.target.closest("[data-action]");
if (!el) return;
e.preventDefault();
var action = el.getAttribute("data-action");
var arg1 = el.getAttribute("data-arg1");
var arg2 = el.getAttribute("data-arg2");
switch (action) {
case "nav": ZS.navigate(arg1, arg2); break;
case "navFeed": ZS.navigate("feed"); break;
case "navProfile": ZS.navigate("profile"); break;
case "toggleMobileNav": ZS.toggleMobileNav(); break;
case "doSearch": ZS.doSearch(); break;
case "logout": ZS.logout(); break;
// Posts
case "toggleLike": toggleLike(parseInt(arg1), el); break;
case "toggleComments": toggleComments(parseInt(arg1)); break;
case "toggleNotes": toggleNotes(parseInt(arg1)); break;
case "reportItem": reportItem(arg1, parseInt(arg2)); break;
case "postComment": postComment(parseInt(arg1)); break;
case "deleteComment": deleteComment(parseInt(arg1), parseInt(arg2)); break;
case "submitPost": submitPost(); break;
case "submitFullPost": submitFullPost(); break;
// Emoji
case "toggleEmoji": _togglePicker("emojiPicker"); break;
case "toggleEmojiFull": _togglePicker("fullEmojiPicker"); break;
case "insertEmoji": _insertEmoji("composeText", arg1); break;
case "insertEmojiFull": _insertEmoji("fullComposeText", arg1); break;
// Auth
case "selectPrivacy": selectPrivacy(arg1); break;
case "confirmPrivacy": confirmPrivacy(); break;
// Profile
case "navigatePost": ZS.navigate("post"); break;
case "navigateSettings": ZS.navigate("settings"); break;
case "sendConnection": sendConnection(arg1); break;
case "removeConnection": removeConnection(arg1); break;
case "chatWith": ZS.navigate("chat", arg1); break;
// Follow
case "follow": followUser(parseInt(arg1)); break;
case "unfollow": unfollowUser(parseInt(arg1)); break;
// Dislike
case "toggleDislike": toggleDislike(parseInt(arg1)); break;
// Community
case "searchUsers": searchUsers(); break;
case "acceptConnection": acceptConnection(arg1); break;
case "rejectConnection": rejectConnection(arg1); break;
// Messages
case "chatWithUser": ZS.navigate("chat", arg1); break;
case "sendMsg": sendMsg(arg1); break;
// Notifications
case "markAllRead": markAllRead(); break;
// Settings
case "saveBio": saveBio(); break;
case "saveDisplayName": saveDisplayName(); break;
case "saveUsername": saveUsername(); break;
case "savePassword": savePassword(); break;
case "savePrivacy": savePrivacy(); break;
case "deleteAccount": deleteAccount(); break;
case "showSettingsTab": showSettingsTab(arg1, el); break;
case "deleteAvatar": deleteAvatar(); break;
// Verification
case "proposeVerify": proposeVerify(arg1); break;
case "acceptVerify": acceptVerify(parseInt(arg1)); break;
case "rejectVerify": rejectVerify(parseInt(arg1)); break;
// Mod
case "modAction": modAction(arg1, parseInt(arg2), el.getAttribute("data-action-type")); break;
case "modNoteAction": modNoteAction(parseInt(arg2), el.getAttribute("data-action-type")); break;
case "resolveReport": resolveReport(parseInt(arg1)); break;
case "switchModTab": switchModTab(arg1, el); break;
// Mobile nav: close on link click
default:
if (el.closest("#socialHeaderNav a")) {
var nav = document.getElementById("socialHeaderNav");
if (nav) nav.classList.remove("open");
ZS._navOpen = false;
}
}
});
function _togglePicker(id) {
var picker = document.getElementById(id);
if (picker) picker.classList.toggle("hidden");
}
function _insertEmoji(textareaId, emoji) {
var ta = document.getElementById(textareaId);
if (!ta) return;
var start = ta.selectionStart;
var end = ta.selectionEnd;
ta.value = ta.value.substring(0, start) + emoji + ta.value.substring(end);
ta.selectionStart = ta.selectionEnd = start + emoji.length;
ta.focus();
var pickerId = textareaId === "fullComposeText" ? "fullEmojiPicker" : "emojiPicker";
var picker = document.getElementById(pickerId);
if (picker) picker.classList.add("hidden");
}
// Keyboard delegation (Enter key for inputs)
document.addEventListener("keydown", function(e) {
if (e.key !== "Enter") return;
var el = e.target.closest("[data-key-action]");
if (!el) return;
var action = el.getAttribute("data-key-action");
var arg1 = el.getAttribute("data-arg1");
switch (action) {
case "doSearch": ZS.doSearch(); break;
case "searchUsers": searchUsers(); break;
case "postComment": postComment(parseInt(arg1)); break;
case "sendMsg": sendMsg(arg1); break;
}
});
// Form submit delegation
document.addEventListener("submit", function(e) {
e.preventDefault();
var el = e.target.closest("[data-form]");
if (!el) return;
var form = el.getAttribute("data-form");
switch (form) {
case "loginForm": _handleLogin(el); break;
case "signupForm": _handleSignup(el); break;
}
});
// Input delegation (for character counters, strength bars)
document.addEventListener("input", function(e) {
var el = e.target;
// Character counter
if (el.id === "composeText") {
var c = document.getElementById("composeCounter");
if (c) c.textContent = el.value.length + "/5000";
}
if (el.id === "fullComposeText") {
var c2 = document.getElementById("fullComposeCounter");
if (c2) c2.textContent = el.value.length + "/5000";
}
// Password strength
if (el.id === "suPass") {
var v = el.value, s = 0;
if (v.length >= 12) s++; if (v.length >= 16) s++;
if (/[A-Z]/.test(v)) s++; if (/[a-z]/.test(v)) s++;
if (/[0-9]/.test(v)) s++; if (/[^A-Za-z0-9]/.test(v)) s++;
var fill = document.getElementById("strFill");
var txt = document.getElementById("strText");
if (fill && txt) {
fill.style.width = Math.min((s / 6) * 100, 100) + "%";
if (s <= 2) { fill.style.background = "#d32f2f"; txt.textContent = ZS.t("Faible", "Weak"); }
else if (s <= 4) { fill.style.background = "#f57c00"; txt.textContent = ZS.t("Moyen", "Medium"); }
else { fill.style.background = "#2e7d32"; txt.textContent = ZS.t("Fort", "Strong"); }
}
}
});
// Avatar file input
document.addEventListener("change", function(e) {
var el = e.target;
if (el.id === "avatarInput") uploadAvatar(el);
});
})();
// Avatar upload
async function uploadAvatar(input) {
var file = input.files && input.files[0];
if (!file) return;
var status = document.getElementById("avatarStatus");
if (status) { status.classList.remove("hidden"); status.textContent = ZS.t("Envoi en cours...", "Uploading..."); status.className = "form-hint"; }
var fd = new FormData();
fd.append("avatar", file);
var data = await ZS.api("POST", "/api/social/user/avatar", fd);
if (data.error) {
if (status) { status.textContent = data.error; status.className = "form-hint text-error"; }
return;
}
ZS.user.avatarUrl = data.avatar_url;
var preview = document.getElementById("avatarPreview");
if (preview) preview.innerHTML = _avatar(ZS.user.username, 80, ZS.user.avatarUrl);
if (status) { status.textContent = ZS.t("Avatar mis à jour", "Avatar updated"); status.className = "form-hint text-success"; }
}
async function deleteAvatar() {
var data = await ZS.api("DELETE", "/api/social/user/avatar");
if (data.error) return;
ZS.user.avatarUrl = null;
var preview = document.getElementById("avatarPreview");
if (preview) preview.innerHTML = _avatar(ZS.user.username, 80, null);
showSettingsTab("bio", document.querySelector(".tab-btn"));
}
// Verification
async function proposeVerify(userId) {
if (!confirm(ZS.t("Envoyer une proposition de vérification ?", "Send a verification proposal?"))) return;
var data = await ZS.api("POST", "/api/social/verification/propose/" + userId);
if (data.error) { alert(data.error); return; }
alert(data.message);
}
async function acceptVerify(requestId) {
var data = await ZS.api("POST", "/api/social/verification/accept/" + requestId);
if (data.error) { alert(data.error); return; }
ZS.user.verifiedLevel = 1;
renderNotifications();
}
async function rejectVerify(requestId) {
var data = await ZS.api("POST", "/api/social/verification/reject/" + requestId);
if (data.error) { alert(data.error); return; }
renderNotifications();
}
// Login handler
async function _handleLogin(form) {
var errEl = document.getElementById("loginError");
if (errEl) errEl.style.display = "none";
var btn = form.querySelector('button[type="submit"]');
if (btn) btn.disabled = true;
var data = await ZS.api("POST", "/api/social/auth/login", {
login: form.querySelector('[name="login"]').value,
password: form.querySelector('[name="password"]').value,
});
if (data.error) {
if (errEl) { errEl.textContent = data.error; errEl.style.display = "block"; }
if (btn) btn.disabled = false;
return;
}
ZS.user = data.user;
if (data.needsPrivacyChoice) ZS.navigate("privacy-choice");
else ZS.navigate("feed");
}
// Signup handler
async function _handleSignup(form) {
var errEl = document.getElementById("suError");
if (errEl) errEl.style.display = "none";
var btn = form.querySelector('button[type="submit"]');
if (btn) btn.disabled = true;
var data = await ZS.api("POST", "/api/social/auth/signup", {
username: form.querySelector('[name="username"]').value,
email: form.querySelector('[name="email"]').value || undefined,
password: form.querySelector('[name="password"]').value,
birthdate: form.querySelector('[name="birthdate"]').value,
});
if (data.error) {
if (errEl) { errEl.textContent = data.error; errEl.style.display = "block"; }
if (btn) btn.disabled = false;
return;
}
ZS.user = data.user;
if (data.needsPrivacyChoice) ZS.navigate("privacy-choice");
else ZS.navigate("feed");
}

View file

@ -0,0 +1,2 @@
// Zektyc Social — Privacy views (moved to social-views.js)
// This file is kept for backward compatibility but functions are now in social-views.js

1007
public/js/social-views.js Normal file

File diff suppressed because it is too large Load diff

98
public/js/social.js Normal file
View file

@ -0,0 +1,98 @@
// Zektyc Social — Router v3 (CSP-safe)
(function() {
"use strict";
window._socialRender = function() {
switch (ZS.view) {
case "login": renderLogin(); break;
case "signup": renderSignup(); break;
case "privacy-choice": renderPrivacyChoice(); break;
case "profile": renderProfile(); break;
case "profile-view": renderProfileView(ZS.viewData); break;
case "settings": renderSettings(); break;
case "post": _renderNewPost(); break;
case "community": renderCommunity(); break;
case "messages": renderMessages(); break;
case "chat": renderChat(ZS.viewData); break;
case "notifications": renderNotifications(); break;
case "mod": renderMod(); break;
default: renderFeed(); break;
}
var nav = document.getElementById("socialHeaderNav");
if (nav) nav.classList.remove("open");
ZS._navOpen = false;
if (typeof _applyAvatarStyles === "function") _applyAvatarStyles();
};
function _renderNewPost() {
if (!ZS.user) { ZS.navigate("login"); return; }
var avatarHtml = ZS.user.avatarUrl
? '<div class="compose-avatar"><img src="' + ZS.esc(ZS.user.avatarUrl) + '" alt=""></div>'
: '<div class="compose-avatar">' + ZS.esc(ZS.user.username).charAt(0).toUpperCase() + '</div>';
var html = '<h2 class="mb-16">' + ZS.icon("pen") + ' ' + ZS.t("Nouvelle publication", "New post") + '</h2>' +
'<div class="compose-box">' +
'<div class="compose-row">' +
avatarHtml +
'<div class="compose-input-wrap">' +
'<textarea id="fullComposeText" placeholder="' + ZS.t("Quoi de neuf ?", "What's new?") + '" rows="4" maxlength="5000" class="auto-resize"></textarea>' +
'<div id="fullEmojiPicker" class="emoji-picker hidden">' +
'<span class="emoji-pick" data-action="insertEmojiFull" data-arg1="😀">😀</span>' +
'<span class="emoji-pick" data-action="insertEmojiFull" data-arg1="😂">😂</span>' +
'<span class="emoji-pick" data-action="insertEmojiFull" data-arg1="❤️">❤️</span>' +
'<span class="emoji-pick" data-action="insertEmojiFull" data-arg1="🔥">🔥</span>' +
'<span class="emoji-pick" data-action="insertEmojiFull" data-arg1="👍">👍</span>' +
'<span class="emoji-pick" data-action="insertEmojiFull" data-arg1="👀">👀</span>' +
'<span class="emoji-pick" data-action="insertEmojiFull" data-arg1="💯">💯</span>' +
'<span class="emoji-pick" data-action="insertEmojiFull" data-arg1="🎉">🎉</span>' +
'<span class="emoji-pick" data-action="insertEmojiFull" data-arg1="🤔">🤔</span>' +
'<span class="emoji-pick" data-action="insertEmojiFull" data-arg1="😎">😎</span>' +
'<span class="emoji-pick" data-action="insertEmojiFull" data-arg1="🚀">🚀</span>' +
'<span class="emoji-pick" data-action="insertEmojiFull" data-arg1="💡">💡</span>' +
'</div>' +
'</div></div>' +
'<div class="compose-footer">' +
'<div class="compose-meta">' +
'<button type="button" class="compose-emoji-btn" data-action="toggleEmojiFull">' + ZS.icon("star") + '</button>' +
'<span class="compose-counter" id="fullComposeCounter">0/5000</span>' +
'<select class="compose-vis-select" id="fullComposeVis">' +
'<option value="public">' + ZS.icon("globe") + ' ' + ZS.t("Public", "Public") + '</option>' +
'<option value="connections_only">' + ZS.icon("users") + ' ' + ZS.t("Connexions", "Connections") + '</option>' +
'<option value="private">' + ZS.icon("lock") + ' ' + ZS.t("Privé", "Private") + '</option>' +
'</select></div>' +
'<button class="btn-primary btn-primary-sm" data-action="submitFullPost">' + ZS.icon("send") + ' ' + ZS.t("Publier", "Post") + '</button>' +
'</div></div>' +
'<div class="info-box mt-12">' + ZS.icon("note") + ' ' + ZS.t("Vérifié par un modérateur avant publication.", "Reviewed by a moderator before publishing.") + '</div>' +
'<p class="mt-12"><a href="#" class="text-sm" data-action="nav" data-arg1="feed">' + ZS.icon("arrowLeft") + ' ' + ZS.t("Retour au fil", "Back to feed") + '</a></p>';
ZS.renderShell(null, html, null);
document.getElementById("fullComposeVis").value = ZS.user.privacyLevel || "connections_only";
var ta = document.getElementById("fullComposeText");
if (ta) ta.focus();
}
window.submitFullPost = async function() {
var textarea = document.getElementById("fullComposeText");
if (!textarea || !textarea.value.trim()) return;
var vis = document.getElementById("fullComposeVis").value;
var data = await ZS.api("POST", "/api/social/posts", { content: textarea.value.trim(), visibility: vis });
if (data.error) { alert(data.error); return; }
ZS.navigate("feed");
};
// Init
window.addEventListener("popstate", function(e) {
var parsed = e.state && e.state.view ? e.state : ZS._parsePath();
ZS.view = parsed.view;
ZS.viewData = parsed.data;
if (window._socialRender) window._socialRender();
});
ZS.checkSession().then(function() {
var parsed = ZS._parsePath();
if (parsed.view !== "feed" || parsed.data) {
ZS.view = parsed.view;
ZS.viewData = parsed.data;
}
ZS.navigate(ZS.view, ZS.viewData);
});
})();

29
public/js/theme.js Normal file
View file

@ -0,0 +1,29 @@
(function () {
var STORAGE_KEY = "zektyc-theme"
var DEFAULT = "dark"
function getPreferred() {
try {
var stored = localStorage.getItem(STORAGE_KEY)
if (stored === "light" || stored === "dark") return stored
} catch (e) {}
return DEFAULT
}
function apply(theme) {
document.documentElement.setAttribute("data-theme", theme)
}
apply(getPreferred())
document.addEventListener("click", function (e) {
var btn = e.target.closest ? e.target.closest(".theme-btn") : null
if (!btn) return
var current = document.documentElement.getAttribute("data-theme") || DEFAULT
var next = current === "dark" ? "light" : "dark"
apply(next)
try {
localStorage.setItem(STORAGE_KEY, next)
} catch (e) {}
})
})();

134
public/js/zf-worker.js Normal file
View file

@ -0,0 +1,134 @@
"use strict"
var FONT_LIST = [
"Arial", "Arial Black", "Arial Narrow", "Arial Unicode MS", "Bahnschrift", "Baskerville",
"Calibri", "Cambria", "Cambria Math", "Candara", "Century Gothic", "Charter", "Cochin",
"Comic Sans MS", "Consolas", "Constantia", "Corbel", "Courier", "Courier New", "DejaVu Sans",
"DejaVu Sans Mono", "DejaVu Serif", "Didot", "Franklin Gothic Medium", "Futura", "Geneva",
"Georgia", "Gill Sans", "Helvetica", "Helvetica Neue", "Impact", "Iowan Old Style", "Lato",
"Liberation Sans", "Liberation Serif", "Lucida Console", "Malgun Gothic", "Menlo", "Microsoft JhengHei",
"Microsoft Sans Serif", "Microsoft YaHei", "Monaco", "Noto Sans", "Noto Sans Mono", "Noto Serif",
"Open Sans", "Optima", "Palatino", "Palatino Linotype", "Roboto", "Segoe UI", "Segoe UI Emoji",
"Sitka", "Symbol", "Tahoma", "Times New Roman", "Trebuchet MS", "Ubuntu", "Ubuntu Mono",
"Verdana", "Webdings", "Wingdings", "Zapfino"
]
function probeCanvasFonts(list) {
var found = []
try {
var cv = new OffscreenCanvas(300, 60)
var cx = cv.getContext("2d")
cx.font = "48px serif"
var base = cx.measureText("mmllii").width
for (var i = 0; i < list.length; i++) {
cx.font = "48px '" + list[i] + "', serif"
if (cx.measureText("mmllii").width !== base) found.push(list[i])
}
} catch (e) {}
return found
}
function probeScreen() {
var o = {}
try {
if (typeof self.screen !== "undefined" && self.screen) {
o.colorDepth = String(self.screen.colorDepth || "")
o.orientation = (self.screen.orientation && self.screen.orientation.type) || ""
}
} catch (e) {}
return o
}
function probeCodecs() {
var got = {}
try {
if (navigator.mediaCapabilities && navigator.mediaCapabilities.decodingInfo) {
var tests = {
vp9: { type: "file", video: { contentType: 'video/webm;codecs="vp09.00.10.08"', width: 1920, height: 1080, bitrate: 4000000, framerate: 30 } },
av1: { type: "file", video: { contentType: 'video/webm;codecs="av01.0.04M.08"', width: 1920, height: 1080, bitrate: 4000000, framerate: 30 } },
opus: { type: "file", audio: { contentType: 'audio/ogg;codecs="opus"' } }
}
return Promise.all(
["vp9", "av1", "opus"].map(function (k) {
return navigator.mediaCapabilities
.decodingInfo(tests[k])
.then(function (r) { got[k] = r && r.supported ? "1" : "0" })
.catch(function () { got[k] = "0" })
})
).then(function () { return got })
}
} catch (e) {}
return Promise.resolve(got)
}
self.onmessage = function () {
var out = {}
try {
var ops = ["sin", "cos", "tan", "log", "exp", "sqrt", "asin", "acos", "atan2", "sinh", "cosh", "tanh", "cbrt"]
var m = []
for (var i = 0; i < ops.length; i++) {
m.push(Math[ops[i]](i * 0.123456789 + 1).toPrecision(17))
}
out.math = m.join(":")
} catch (e) {
out.math = null
}
try {
out.calendars = typeof Intl.supportedValuesOf === "function" ? Intl.supportedValuesOf("calendar").join(",") : null
} catch (e) {
out.calendars = null
}
try {
out.locale = Intl.DateTimeFormat().resolvedOptions().locale
} catch (e) {
out.locale = null
}
try {
out.numbering = new Intl.NumberFormat().resolvedOptions().numberingSystem
} catch (e) {
out.numbering = null
}
out.wasm = !!self.WebAssembly
try {
out.simd = self.WebAssembly.validate(
new Uint8Array([
0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00,
0x01, 0x05, 0x01, 0x60, 0x00, 0x01, 0x7b,
0x03, 0x02, 0x01, 0x00,
0x0a, 0x17, 0x01, 0x15, 0x00,
0x41, 0x00, 0xfd, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0b
])
)
} catch (e) {
out.simd = false
}
var t0 = performance.now()
var x = 0
for (var k = 0; k < 2000000; k++) {
x = (x + k) % 97
x = Math.sqrt(x) + Math.log(k + 1)
}
var dt = performance.now() - t0
out.bench = dt > 0 ? Math.round(2000000 / dt) : 0
out.fonts = probeCanvasFonts(FONT_LIST)
var scr = probeScreen()
out.colorDepth = scr.colorDepth || ""
out.orientation = scr.orientation || ""
try {
out.fontLocalApi = typeof FontFace === "function" && typeof self.fonts === "object" && self.fonts && typeof self.fonts.add === "function" ? "local" : "unsupported"
} catch (e) {
out.fontLocalApi = "unsupported"
}
probeCodecs().then(function (codecs) {
out.codecs = codecs
postMessage(out)
})
}

2409
public/js/zfprobe.js Normal file

File diff suppressed because it is too large Load diff