zektyc/public/js/social-core.js
riricdev d7ec32a458 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é)
2026-09-06 13:03:25 +02:00

554 lines
26 KiB
JavaScript

// 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");
}