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:
commit
8e7ec41f18
104 changed files with 19256 additions and 0 deletions
229
public/js/components.js
Normal file
229
public/js/components.js
Normal 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">×</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
39
public/js/discord.js
Normal 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
141
public/js/i18n.js
Normal 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
10
public/js/legal-lang.js
Normal 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
2
public/js/social-auth.js
Normal 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
554
public/js/social-core.js
Normal 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");
|
||||
}
|
||||
2
public/js/social-privacy.js
Normal file
2
public/js/social-privacy.js
Normal 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
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
98
public/js/social.js
Normal 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
29
public/js/theme.js
Normal 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
134
public/js/zf-worker.js
Normal 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
2409
public/js/zfprobe.js
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue