// 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: '', users: '', mail: '', bell: '', user: '', shield: '', search: '', pen: '', heart: '', heartFilled: '', comment: '', note: '', flag: '', check: '', verifiedBlue: '', verifiedGold: '', close: '', arrowLeft: '', arrowRight: '', star: '', lock: '', globe: '', settings: '', logout: '', menu: '', plus: '', send: '', trash: '', reply: '', user: '', }, _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 '' + svg + ''; return '' + svg + ''; }, // ── 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 = '' + '
' + (typeof mainContent === "string" ? mainContent : "") + '
' + ''; var navEl = document.getElementById("socialHeaderNav"); if (navEl) navEl.innerHTML = this._buildNavLinks(); if (typeof _applyAvatarStyles === "function") _applyAvatarStyles(); }, _buildHeader() { return '' + '' + '' + '' + (this.user ? '
' + this.esc(this.user.username).charAt(0).toUpperCase() + '
' : '') + ''; }, _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 '' + self.icon(iconName) + '' + label + '' + (badge > 0 ? '' + badge + '' : '') + ''; } 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 += '' + this.icon("logout") + ''; } else { html = link("feed", "home", L ? "Accueil" : "Home", 0) + '' + this.icon("arrowRight") + '' + (L ? "Connexion" : "Login") + '' + '' + this.icon("star") + '' + (L ? "Inscription" : "Sign up") + ''; } return html; }, _buildSidebarLeft() { var L = this.lang; var v = this.view; var self = this; var html = ""; if (this.user) { html += ''; } function navLink(view, iconName, label, badge) { var cls = v === view ? " active" : ""; return '' + self.icon(iconName) + '' + label + '' + (badge > 0 ? '' + badge + '' : '') + ''; } if (this.user) { html += '
' + 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) : '') + '
'; } else { html += '
' + navLink("login", "arrowRight", L ? "Se connecter" : "Log in", 0) + navLink("signup", "star", L ? "S'inscrire" : "Sign up", 0) + '
'; } html += ''; return html; }, _buildSidebarRight() { var L = this.lang; return ''; }, _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"); }