zektyc/public/js/social-views.js
riricdev 8e7ec41f18 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

1007 lines
55 KiB
JavaScript

// Zektyc Social — Views v4 (CSP-safe, zero inline handlers)
function _spinner() {
return '<div class="social-spinner"><div class="spinner-dot"></div><div class="spinner-dot"></div><div class="spinner-dot"></div></div>';
}
function _timeAgo(iso) {
if (!iso) return "";
var diff = (Date.now() - new Date(iso + "Z").getTime()) / 1000;
if (diff < 60) return ZS.t("à l'instant", "just now");
if (diff < 3600) return Math.floor(diff / 60) + " min";
if (diff < 86400) return Math.floor(diff / 3600) + "h";
if (diff < 2592000) return Math.floor(diff / 86400) + "j";
return new Date(iso + "Z").toLocaleDateString(ZS.lang === "fr" ? "fr-FR" : "en-US", { day: "numeric", month: "short" });
}
function _visIcon(v) {
if (v === "public") return ZS.icon("globe");
if (v === "connections_only") return ZS.icon("users");
return ZS.icon("lock");
}
function _visLabel(v) {
if (v === "public") return ZS.t("Public", "Public");
if (v === "connections_only") return ZS.t("Connexions", "Connections");
return ZS.t("Privé", "Private");
}
function _avatar(username, size, avatarUrl) {
size = size || 40;
if (avatarUrl) {
return '<div class="post-avatar post-avatar-img" data-avatar-size="' + size + '">' +
'<img src="' + ZS.esc(avatarUrl) + '" alt="' + ZS.esc(username || "?") + '" loading="lazy"></div>';
}
var fs = Math.round(size * 0.4);
return '<div class="post-avatar" data-avatar-size="' + size + '" data-avatar-fs="' + fs + '">' +
ZS.esc(username || "?").charAt(0).toUpperCase() + '</div>';
}
function _applyAvatarStyles() {
document.querySelectorAll("[data-avatar-size]").forEach(function(el) {
var s = parseInt(el.dataset.avatarSize, 10);
var f = parseInt(el.dataset.avatarFs, 10);
el.style.width = s + "px";
el.style.height = s + "px";
el.style.fontSize = f + "px";
});
}
function _verifiedBadge(level, username, verifiedByUsername, verifiedByDisplayName, verifiedByAvatar) {
if (level === 2) {
return '<span class="verified-badge verified-gold" title="Compte officiel">' + ZS.icon("verifiedGold") + '</span>';
}
if (level === 1) {
var verifierName = ZS.esc(verifiedByDisplayName || verifiedByUsername || "");
var verifierUsername = ZS.esc(verifiedByUsername || "");
var badge = '<span class="verified-badge verified-blue" title="Affilié à ' + verifierName + '">' + ZS.icon("verifiedBlue");
if (verifiedByUsername) {
badge += '<a href="#" class="verified-verifier" data-action="nav" data-arg1="profile-view" data-arg2="' + verifierUsername + '" title="' + verifierName + '">';
if (verifiedByAvatar) {
badge += '<img src="' + ZS.esc(verifiedByAvatar) + '" alt="' + verifierName + '" class="verified-verifier-avatar">';
} else {
badge += '<span class="verified-verifier-letter">' + verifierName.charAt(0).toUpperCase() + '</span>';
}
badge += '</a>';
}
badge += '</span>';
return badge;
}
return '';
}
// ── Post Card ──
function _postCard(p) {
var date = _timeAgo(p.created_at);
var verified = p.is_verified
? '<span class="post-verified-badge">' + ZS.icon("check") + ' ' + ZS.t("Vérifié", "Verified") + '</span>'
: '<span class="post-pending-badge">' + ZS.icon("note") + ' ' + ZS.t("En attente", "Pending") + '</span>';
var likeClass = p.user_liked ? ' liked' : '';
var html = '<article class="post-card" id="post-' + p.id + '">' +
'<div class="post-top">' +
_avatar(p.username, 40, p.avatar_url) +
'<div class="post-user-info">' +
'<span class="post-username" data-action="nav" data-arg1="profile-view" data-arg2="' + ZS.esc(p.username) + '">' + ZS.esc(p.display_name || p.username) + '</span>' +
_verifiedBadge(p.verified_level, p.username, p.verified_by_username, p.verified_by_display_name, p.verified_by_avatar) +
'<span class="post-timestamp">' + date + '</span>' +
'</div>' +
'<div class="post-vis-badge">' + _visIcon(p.visibility) + ' ' + _visLabel(p.visibility) + '</div>' +
verified + '</div>' +
'<div class="post-content">' + ZS.esc(p.content) + '</div>';
if (p.media_url) {
html += '<img class="post-image" src="' + ZS.esc(p.media_url) + '" alt="" loading="lazy">';
}
html += '<div class="post-actions">' +
'<button class="post-action' + likeClass + '" data-action="toggleLike" data-arg1="' + p.id + '">' +
ZS.icon("heart") + '<span class="post-action-count">' + (p.like_count || 0) + '</span></button>' +
'<button class="post-action' + (p.user_disliked ? ' disliked' : '') + '" data-action="toggleDislike" data-arg1="' + p.id + '">' +
ZS.icon("close") + '</button>' +
'<button class="post-action" data-action="toggleComments" data-arg1="' + p.id + '">' +
ZS.icon("comment") + '<span class="post-action-count">' + (p.comment_count || 0) + '</span></button>' +
'<button class="post-action" data-action="toggleNotes" data-arg1="' + p.id + '">' +
ZS.icon("note") + '<span class="post-action-count">' + (p.note_count || 0) + '</span></button>' +
'<button class="post-action" data-action="reportItem" data-arg1="post" data-arg2="' + p.id + '">' +
ZS.icon("flag") + '</button>' +
'</div>';
html += '<div class="post-notes-section" id="notes-' + p.id + '"></div>';
html += '<div class="post-comments-section" id="comments-' + p.id + '">';
if (ZS.user) {
html += '<div class="comment-compose">' +
'<input type="text" id="commentInput-' + p.id + '" placeholder="' + ZS.t("Ajouter un commentaire...", "Add a comment...") + '" data-key="Enter" data-key-action="postComment" data-arg1="' + p.id + '">' +
'</div>';
}
html += '<div id="commentsList-' + p.id + '"></div></div>';
html += '</article>';
return html;
}
// ═══════════════════════════════════════
// FEED
// ═══════════════════════════════════════
async function renderFeed() {
var L = ZS.lang;
var mainHtml = "";
if (ZS.user) {
mainHtml += '<div class="compose-box">' +
'<div class="compose-row">' +
_avatar(ZS.user.username, 40, ZS.user.avatarUrl) +
'<div class="compose-input-wrap">' +
'<textarea id="composeText" placeholder="' + ZS.t("Quoi de neuf ?", "What's new?") + '" rows="2" maxlength="5000" class="auto-resize"></textarea>' +
'<div id="emojiPicker" class="emoji-picker hidden">' +
'<span class="emoji-pick" data-action="insertEmoji" data-arg1="😀">😀</span>' +
'<span class="emoji-pick" data-action="insertEmoji" data-arg1="😂">😂</span>' +
'<span class="emoji-pick" data-action="insertEmoji" data-arg1="❤️">❤️</span>' +
'<span class="emoji-pick" data-action="insertEmoji" data-arg1="🔥">🔥</span>' +
'<span class="emoji-pick" data-action="insertEmoji" data-arg1="👍">👍</span>' +
'<span class="emoji-pick" data-action="insertEmoji" data-arg1="👀">👀</span>' +
'<span class="emoji-pick" data-action="insertEmoji" data-arg1="💯">💯</span>' +
'<span class="emoji-pick" data-action="insertEmoji" data-arg1="🎉">🎉</span>' +
'<span class="emoji-pick" data-action="insertEmoji" data-arg1="🤔">🤔</span>' +
'<span class="emoji-pick" data-action="insertEmoji" data-arg1="😎">😎</span>' +
'<span class="emoji-pick" data-action="insertEmoji" data-arg1="🚀">🚀</span>' +
'<span class="emoji-pick" data-action="insertEmoji" data-arg1="💡">💡</span>' +
'</div>' +
'</div></div>' +
'<div class="compose-footer">' +
'<div class="compose-meta">' +
'<button type="button" class="compose-emoji-btn" data-action="toggleEmoji">' + ZS.icon("star") + '</button>' +
'<span class="compose-counter" id="composeCounter">0/5000</span>' +
'<select class="compose-vis-select" id="composeVis">' +
'<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>' +
'<div class="compose-actions">' +
'<button class="btn-primary btn-primary-sm" data-action="submitPost">' + ZS.t("Publier", "Post") + '</button>' +
'</div></div></div>';
}
mainHtml += '<div id="feedPosts">' + _spinner() + '</div>';
ZS.renderShell(null, mainHtml, null);
var composeVis = document.getElementById("composeVis");
if (composeVis && ZS.user) composeVis.value = ZS.user.privacyLevel || "connections_only";
var data = await ZS.api("GET", "/api/social/posts");
var el = document.getElementById("feedPosts");
if (!el) return;
if (data.error) {
el.innerHTML = '<div class="empty-state"><div class="empty-state-icon">' + ZS.icon("flag") + '</div><h3>Erreur</h3><p>' + ZS.esc(data.error) + '</p></div>';
return;
}
if (!data.posts || data.posts.length === 0) {
el.innerHTML = '<div class="empty-state"><div class="empty-state-icon">' + ZS.icon("note") + '</div>' +
'<h3>' + (L ? "Le fil est vide" : "Feed is empty") + '</h3>' +
'<p>' + (ZS.user
? (L ? "Sois le premier à publier quelque chose." : "Be the first to post something.")
: (L ? "Connectez-vous pour voir les publications." : "Log in to see posts.")) + '</p>' +
(ZS.user ? ''
: '<button class="btn-primary mt-16 mw-200" data-action="nav" data-arg1="login">' + (L ? "Se connecter" : "Log in") + '</button>') +
'</div>';
return;
}
var html = "";
for (var i = 0; i < data.posts.length; i++) {
html += _postCard(data.posts[i]);
}
el.innerHTML = html;
}
async function submitPost() {
var textarea = document.getElementById("composeText");
if (!textarea || !textarea.value.trim()) return;
var vis = document.getElementById("composeVis").value;
var btn = document.querySelector(".compose-actions .btn-primary");
if (btn) { btn.disabled = true; btn.textContent = ZS.t("Envoi…", "Posting…"); }
var data = await ZS.api("POST", "/api/social/posts", { content: textarea.value.trim(), visibility: vis });
if (data.error) { alert(data.error); if (btn) { btn.disabled = false; btn.textContent = ZS.t("Publier", "Post"); } return; }
renderFeed();
}
// ═══════════════════════════════════════
// POST ACTIONS
// ═══════════════════════════════════════
async function toggleLike(postId, btn) {
if (!ZS.user) { ZS.navigate("login"); return; }
var data = await ZS.api("POST", "/api/social/posts/" + postId + "/like");
if (data.error) return;
btn.classList.toggle("liked", data.liked);
btn.innerHTML = ZS.icon("heart") + '<span class="post-action-count">' + data.count + '</span>';
}
async function toggleDislike(postId) {
if (!ZS.user) { ZS.navigate("login"); return; }
var data = await ZS.api("POST", "/api/social/posts/" + postId + "/dislike");
if (data.error) return;
if (data.disliked) {
var card = document.getElementById("post-" + postId);
if (card) card.style.opacity = "0.3";
}
}
async function followUser(userId) {
if (!ZS.user) { ZS.navigate("login"); return; }
var data = await ZS.api("POST", "/api/social/follows/" + userId);
if (data.error) { alert(data.error); return; }
renderProfileView(ZS.viewData);
}
async function unfollowUser(userId) {
if (!ZS.user) { ZS.navigate("login"); return; }
var data = await ZS.api("DELETE", "/api/social/follows/" + userId);
if (data.error) { alert(data.error); return; }
renderProfileView(ZS.viewData);
}
async function saveUsername() {
var data = await ZS.api("POST", "/api/social/user/update-username", { username: document.getElementById("settingsUsername").value });
if (data.error) { alert(data.error); return; }
ZS.user.username = data.username;
alert(ZS.t("Username mis à jour", "Username updated"));
}
async function toggleComments(postId) {
var el = document.getElementById("comments-" + postId);
if (!el) return;
if (el.classList.contains("open")) { el.classList.remove("open"); return; }
el.classList.add("open");
var list = document.getElementById("commentsList-" + postId);
list.innerHTML = _spinner();
var data = await ZS.api("GET", "/api/social/posts/" + postId + "/comments");
if (!data.comments || data.comments.length === 0) {
list.innerHTML = '<div class="note-empty">' + ZS.t("Aucun commentaire", "No comments") + '</div>';
return;
}
list.innerHTML = data.comments.map(function(c) {
return '<div class="comment-item" id="comment-' + c.id + '">' +
_avatar(c.username, 28, c.avatar_url) +
'<div class="comment-body">' +
'<div class="comment-header">' +
'<span class="comment-author">' + ZS.esc(c.username) + '</span>' +
'<span class="comment-time">' + _timeAgo(c.created_at) + '</span>' +
((ZS.user && (ZS.user.id === c.user_id || ZS.user.role === "admin"))
? '<button class="comment-delete-btn" data-action="deleteComment" data-arg1="' + c.id + '" data-arg2="' + postId + '">' + ZS.icon("close") + '</button>' : '') +
'</div>' +
'<div class="comment-text">' + ZS.esc(c.content) + '</div></div></div>';
}).join("");
}
async function postComment(postId) {
var input = document.getElementById("commentInput-" + postId);
if (!input || !input.value.trim()) return;
var data = await ZS.api("POST", "/api/social/posts/" + postId + "/comments", { content: input.value.trim() });
if (data.error) { alert(data.error); return; }
input.value = "";
toggleComments(postId);
setTimeout(function() { toggleComments(postId); }, 50);
}
async function deleteComment(commentId, postId) {
if (!confirm(ZS.t("Supprimer ?", "Delete?"))) return;
await ZS.api("DELETE", "/api/social/comments/" + commentId);
toggleComments(postId);
setTimeout(function() { toggleComments(postId); }, 50);
}
async function toggleNotes(postId) {
var c = document.getElementById("notes-" + postId);
if (!c) return;
if (c.innerHTML) { c.innerHTML = ""; return; }
c.innerHTML = _spinner();
var data = await ZS.api("GET", "/api/social/posts/" + postId + "/notes");
if (!data.notes || data.notes.length === 0) {
c.innerHTML = '<div class="note-empty">' + ZS.t("Aucune note", "No notes") + '</div>';
return;
}
c.innerHTML = data.notes.map(function(n) {
return '<div class="note-item"><div class="note-author">' + ZS.esc(n.username) + '</div><div class="note-text">' + ZS.esc(n.content) + '</div></div>';
}).join("");
}
function reportItem(type, id) {
var reason = prompt(ZS.t("Raison du signalement :", "Report reason:"));
if (!reason) return;
ZS.api("POST", "/api/social/report", { targetType: type, targetId: id, reason: reason }).then(function(d) {
alert(d.error || ZS.t("Signalement envoyé.", "Report sent."));
});
}
// ═══════════════════════════════════════
// AUTH
// ═══════════════════════════════════════
function renderLogin() {
var L = ZS.lang;
var html = '<div class="auth-card">' +
'<h2>' + ZS.t("Connexion", "Log in") + '</h2>' +
'<form data-form="loginForm">' +
'<div class="form-group"><label>' + ZS.t("Nom d'utilisateur ou email", "Username or email") + '</label>' +
'<input type="text" name="login" required autocomplete="username" autofocus></div>' +
'<div class="form-group"><label>' + ZS.t("Mot de passe", "Password") + '</label>' +
'<input type="password" name="password" required autocomplete="current-password"></div>' +
'<div id="loginError" class="form-error hidden"></div>' +
'<button type="submit" class="btn-primary mt-16">' + ZS.t("Se connecter", "Log in") + '</button>' +
'</form>' +
'<div class="auth-footer">' + ZS.t("Pas encore de compte ? ", "No account? ") +
'<a href="#" data-action="nav" data-arg1="signup">' + ZS.t("S'inscrire", "Sign up") + '</a></div></div>';
ZS.renderShell(null, html, null);
}
function renderSignup() {
var L = ZS.lang;
var html = '<div class="auth-card">' +
'<h2>' + ZS.t("Inscription", "Sign up") + '</h2>' +
'<form data-form="signupForm">' +
'<div class="form-group"><label>' + ZS.t("Nom d'utilisateur", "Username") + '</label>' +
'<input type="text" name="username" required minlength="3" maxlength="20" pattern="[a-zA-Z0-9_]+" autocomplete="username">' +
'<div class="form-hint">3-20 ' + ZS.t("caractères", "characters") + ', a-z 0-9 _</div></div>' +
'<div class="form-group"><label>Email</label>' +
'<input type="email" name="email" autocomplete="email"></div>' +
'<div class="form-group"><label>' + ZS.t("Date de naissance", "Date of birth") + '</label>' +
'<input type="date" name="birthdate" id="suBirth" required>' +
'<div class="form-hint">' + ZS.t("Uniquement pour vérifier l'âge.", "Used only to verify age.") + '</div></div>' +
'<div class="form-group"><label>' + ZS.t("Mot de passe", "Password") + '</label>' +
'<input type="password" name="password" id="suPass" required autocomplete="new-password">' +
'<div class="strength-bar"><div class="strength-fill" id="strFill"></div></div>' +
'<div class="strength-label" id="strText"></div>' +
'<div class="form-hint">' + ZS.t("12+ caractères, majuscule, minuscule, chiffre, spécial", "12+ chars, upper, lower, digit, special") + '</div></div>' +
'<label class="tos-check"><input type="checkbox" id="suTos" required>' +
'<span>' + ZS.t("J'accepte les ", "I accept the ") + '<a href="/tos" target="_blank">' + ZS.t("CGU", "ToS") + '</a> ' + ZS.t("et la ", "and the ") + '<a href="/privacy-policy" target="_blank">' + ZS.t("politique de confidentialité", "privacy policy") + '</a></span></label>' +
'<div id="suError" class="form-error hidden"></div>' +
'<button type="submit" class="btn-primary mt-12">' + ZS.t("Créer mon compte", "Create account") + '</button>' +
'</form>' +
'<div class="auth-footer">' + ZS.t("Déjà un compte ? ", "Already have an account? ") +
'<a href="#" data-action="nav" data-arg1="login">' + ZS.t("Se connecter", "Log in") + '</a></div></div>';
ZS.renderShell(null, html, null);
}
// ═══════════════════════════════════════
// PRIVACY CHOICE
// ═══════════════════════════════════════
var _selectedPrivacy = null;
function renderPrivacyChoice() {
var L = ZS.lang;
var html = '<div class="auth-card mw-520">' +
'<h2>' + ZS.icon("shield") + ' ' + ZS.t("Confidentialité", "Privacy") + '</h2>' +
'<p class="text-center">' + ZS.t("Choisissez un niveau de confidentialité.", "Choose a privacy level.") + '</p>' +
'<div class="warning-box"><strong>' + ZS.icon("shield") + ' ' + ZS.t("Attention", "Important") + '</strong>' +
ZS.t("Votre choix est appliqué immédiatement.", "Applied immediately.") + '</div>' +
'<div class="privacy-grid">' +
_privOption("public", "globe", ZS.t("Public", "Public"), ZS.t("Tout le monde peut voir vos publications.", "Everyone can see your posts."),
[ZS.t("Visible par tous", "Visible to all"), ZS.t("Peut apparaître dans les moteurs de recherche", "May appear in search engines")]) +
_privOption("connections_only", "users", ZS.t("Connexions", "Connections"), ZS.t("Seules vos connexions approuvées.", "Only approved connections."),
[ZS.t("Seuls vos contacts voient vos posts", "Only your contacts see your posts"), ZS.t("Demandes de connexion", "Connection requests")]) +
_privOption("private", "lock", ZS.t("Privé", "Private"), ZS.t("Personne sauf vous.", "Nobody but you."),
[ZS.t("Seul vous voyez vos posts", "Only you see your posts"), ZS.t("Modération légale possible (DSA)", "Legal moderation possible (DSA)")]) +
'</div>' +
'<div id="privConfirm" class="hidden mt-16">' +
'<div class="warning-box" id="privSummary"></div>' +
'<label class="tos-check"><input type="checkbox" id="privAck"><span id="privAckText"></span></label>' +
'<div id="privError" class="form-error hidden"></div>' +
'<button class="btn-primary" id="privBtn" disabled data-action="confirmPrivacy">' + ZS.t("Confirmer", "Confirm") + '</button>' +
'</div></div>';
ZS.renderShell(null, html, null);
}
function _privOption(level, iconName, title, desc, consequences) {
var items = consequences.map(function(c) { return '<li>' + c + '</li>'; }).join("");
return '<div class="privacy-card" data-level="' + level + '" data-action="selectPrivacy" data-arg1="' + level + '">' +
'<div class="privacy-card-check">' + ZS.icon("check") + '</div>' +
'<div class="privacy-card-icon">' + ZS.icon(iconName) + '</div>' +
'<h3>' + title + '</h3>' +
'<p>' + desc + '</p>' +
(items ? '<ul>' + items + '</ul>' : '') + '</div>';
}
function selectPrivacy(level) {
_selectedPrivacy = level;
document.querySelectorAll(".privacy-card").forEach(function(el) { el.classList.remove("selected"); });
document.querySelector('.privacy-card[data-level="' + level + '"]').classList.add("selected");
var sums = {
public: ZS.t("Mode <strong>PUBLIC</strong> : tous les utilisateurs et les moteurs de recherche pourront voir vos publications.", "Mode <strong>PUBLIC</strong>: all users and search engines will see your posts."),
connections_only: ZS.t("Mode <strong>CONNECTIONS</strong> : seuls les contacts que vous acceptez pourront voir vos publications.", "Mode <strong>CONNECTIONS</strong>: only contacts you accept will see your posts."),
private: ZS.t("Mode <strong>PRIVÉ</strong> : personne d'autre que vous ne pourra voir vos publications.", "Mode <strong>PRIVATE</strong>: nobody but you will see your posts."),
};
var acks = {
public: ZS.t("Je comprends que mes publications seront visibles par tout le monde.", "I understand my posts will be visible to everyone."),
connections_only: ZS.t("Je comprends que seuls mes contacts approuvés verront mes publications.", "I understand only approved contacts will see my posts."),
private: ZS.t("Je comprends que mes publications ne seront visibles que par moi-même.", "I understand my posts will only be visible to me."),
};
document.getElementById("privSummary").innerHTML = sums[level];
document.getElementById("privAckText").textContent = acks[level];
document.getElementById("privConfirm").style.display = "block";
document.getElementById("privAck").checked = false;
document.getElementById("privBtn").disabled = true;
document.getElementById("privAck").onchange = function() {
document.getElementById("privBtn").disabled = !this.checked;
};
document.getElementById("privConfirm").scrollIntoView({ behavior: "smooth", block: "nearest" });
}
async function confirmPrivacy() {
if (!_selectedPrivacy) return;
var errEl = document.getElementById("privError");
errEl.style.display = "none";
var data = await ZS.api("POST", "/api/social/user/privacy-choice", { level: _selectedPrivacy, acknowledged: true });
if (data.error) { errEl.textContent = data.error; errEl.style.display = "block"; return; }
ZS.user.privacyLevel = data.privacyLevel;
ZS.user.privacyChoiceMade = true;
ZS.navigate("feed");
}
// ═══════════════════════════════════════
// PROFILE
// ═══════════════════════════════════════
async function renderProfile() {
if (!ZS.user) { ZS.navigate("login"); return; }
await _renderProfilePage(ZS.user.username);
}
async function renderProfileView(username) {
await _renderProfilePage(username);
}
async function _renderProfilePage(username) {
var L = ZS.lang;
ZS.renderShell(null, _spinner(), null);
var data = await ZS.api("GET", "/api/social/user/" + encodeURIComponent(username));
if (data.error) {
ZS.renderShell(null, '<div class="empty-state"><div class="empty-state-icon">' + ZS.icon("close") + '</div><h3>' + ZS.esc(data.error) + '</h3></div>', null);
return;
}
if (data.isPrivate) {
ZS.renderShell(null, '<div class="empty-state"><div class="empty-state-icon">' + ZS.icon("lock") + '</div><h3>' + ZS.t("Ce profil est privé.", "This profile is private.") + '</h3></div>', null);
return;
}
var user = data.user;
var badges = data.badges || [];
var posts = data.posts || [];
var isOwn = ZS.user && user.username === ZS.user.username;
var roleLabel = { user: ZS.t("Membre", "Member"), moderator: ZS.t("Modérateur", "Moderator"), admin: ZS.t("Administrateur", "Admin") };
var joinDate = new Date(user.created_at + "Z").toLocaleDateString(L ? "fr-FR" : "en-US", { day: "numeric", month: "long", year: "numeric" });
var badgesHtml = "";
if (badges.length > 0) {
badgesHtml = '<div class="profile-badges-grid">' + badges.map(function(b) {
return '<div class="profile-badge">' + b.icon + " " + ZS.esc(b.name) + '</div>';
}).join("") + '</div>';
}
var actionsHtml = "";
if (isOwn) {
actionsHtml = '<div class="profile-actions-row">' +
'<button class="btn-primary btn-primary-sm" data-action="navigateSettings">' + ZS.icon("settings") + ' ' + ZS.t("Paramètres", "Settings") + '</button>' +
'<button class="btn-primary btn-primary-sm btn-outline" data-action="navigatePost">' + ZS.icon("pen") + ' ' + ZS.t("Publier", "Post") + '</button>' +
'</div>';
} else if (ZS.user) {
var followBtn = user.isFollowing
? '<button class="btn-primary btn-primary-sm btn-outline" data-action="unfollow" data-arg1="' + user.id + '">' + ZS.t("Ne plus suivre", "Unfollow") + '</button>'
: '<button class="btn-primary btn-primary-sm" data-action="follow" data-arg1="' + user.id + '">' + ZS.icon("plus") + ' ' + ZS.t("Suivre", "Follow") + '</button>';
var verifyBtn = '';
if (ZS.user.verifiedLevel === 2 && !user.verified_level) {
verifyBtn = '<button class="btn-primary btn-primary-sm btn-gold" data-action="proposeVerify" data-arg1="' + user.id + '">' + ZS.icon("verifiedGold") + ' ' + ZS.t("Vérifier", "Verify") + '</button>';
}
actionsHtml = '<div class="profile-actions-row">' + followBtn +
'<button class="btn-primary btn-primary-sm btn-outline" data-action="chatWith" data-arg1="' + ZS.esc(user.username) + '">' + ZS.icon("mail") + ' ' + ZS.t("Message", "Message") + '</button>' +
verifyBtn + '</div>';
}
var profileCard = '<div class="profile-card">' +
'<div class="profile-banner"></div>' +
'<div class="profile-main">' +
'<div class="profile-avatar-lg">' +
(user.avatar_url ? '<img src="' + ZS.esc(user.avatar_url) + '" alt="' + ZS.esc(user.username) + '" loading="lazy">' : ZS.esc(user.username).charAt(0).toUpperCase()) +
'</div>' +
'<h2 class="profile-name">' + ZS.esc(user.display_name || user.username) +
_verifiedBadge(user.verified_level, user.username, user.verified_by_username, user.verified_by_display_name, user.verified_by_avatar) +
(isOwn ? ' <span class="profile-role-tag">' + (roleLabel[user.role] || user.role) + '</span>' : '') + '</h2>' +
'<div class="profile-handle">@' + ZS.esc(user.username) + '</div>' +
(user.bio ? '<div class="profile-bio">' + ZS.esc(user.bio) + '</div>' : '') +
'<div class="profile-stats-row">' +
'<div class="profile-stat"><span class="profile-stat-value">' + (user.postCount || 0) + '</span><span class="profile-stat-label">' + ZS.t("posts", "posts") + '</span></div>' +
'<div class="profile-stat"><span class="profile-stat-value">' + (user.followerCount || 0) + '</span><span class="profile-stat-label">' + ZS.t("abonnés", "followers") + '</span></div>' +
'<div class="profile-stat"><span class="profile-stat-value">' + (user.followingCount || 0) + '</span><span class="profile-stat-label">' + ZS.t("abonnements", "following") + '</span></div>' +
'<div class="profile-stat"><span class="profile-stat-value">' + joinDate + '</span><span class="profile-stat-label">' + ZS.t("Inscrit", "Joined") + '</span></div>' +
'</div>' + actionsHtml +
'</div></div>';
var postsHtml = "";
if (posts.length > 0) {
postsHtml = '<h3 class="mt-20 mb-12">' + ZS.t("Publications", "Posts") + '</h3>';
for (var i = 0; i < posts.length; i++) postsHtml += _postCard(posts[i]);
} else {
postsHtml = '<div class="empty-state p-30"><div class="empty-state-icon">' + ZS.icon("note") + '</div><p>' + ZS.t("Aucune publication.", "No posts.") + '</p></div>';
}
ZS.renderShell(null, profileCard + postsHtml, null);
if (ZS._updateSEO) ZS._updateSEO(
(user.display_name || user.username) + ' — Zektyc Social',
user.bio || ZS.t('Profil de', 'Profile of') + ' ' + user.username,
location.origin + location.pathname + '#/profile/' + user.username
);
}
// ═══════════════════════════════════════
// SETTINGS
// ═══════════════════════════════════════
async function renderSettings() {
if (!ZS.user) { ZS.navigate("login"); return; }
var html = '<h2 class="mb-16">' + ZS.icon("settings") + ' ' + ZS.t("Paramètres", "Settings") + '</h2>' +
'<div class="tabs">' +
'<button class="tab-btn active" data-action="showSettingsTab" data-arg1="bio">' + ZS.t("Profil", "Profile") + '</button>' +
'<button class="tab-btn" data-action="showSettingsTab" data-arg1="password">' + ZS.t("Mot de passe", "Password") + '</button>' +
'<button class="tab-btn" data-action="showSettingsTab" data-arg1="privacy">' + ZS.t("Confidentialité", "Privacy") + '</button>' +
'<button class="tab-btn" data-action="showSettingsTab" data-arg1="danger">' + ZS.t("Danger", "Danger") + '</button>' +
'</div><div id="settingsContent"></div>';
ZS.renderShell(null, html, null);
showSettingsTab("bio", document.querySelector(".tab-btn"));
}
function showSettingsTab(tab, btn) {
document.querySelectorAll(".tab-btn").forEach(function(t) { t.classList.remove("active"); });
if (btn) btn.classList.add("active");
var el = document.getElementById("settingsContent");
if (!el) return;
if (tab === "bio") {
el.innerHTML = '<div class="settings-card"><h3>' + ZS.t("Photo de profil", "Profile picture") + '</h3>' +
'<div class="avatar-upload-area">' +
'<div id="avatarPreview">' + _avatar(ZS.user.username, 80, ZS.user.avatarUrl) + '</div>' +
'<div class="avatar-upload-actions">' +
'<label class="btn-primary btn-primary-sm avatar-upload-label">' + ZS.t("Choisir une image", "Choose image") +
'<input type="file" id="avatarInput" accept="image/jpeg,image/png,image/gif,image/webp" class="avatar-upload-input"></label>' +
(ZS.user.avatarUrl ? '<button class="btn-secondary btn-secondary-sm mt-8" data-action="deleteAvatar">' + ZS.t("Supprimer", "Remove") + '</button>' : '') +
'</div></div>' +
'<div id="avatarStatus" class="form-hint hidden"></div>' +
'</div>' +
'<div class="settings-card mt-16"><h3>' + ZS.t("Nom d'affichage", "Display name") + '</h3>' +
'<input class="settings-input" type="text" id="settingsDisplayName" maxlength="50" placeholder="' + ZS.t("Votre nom affiché", "Your display name") + '" value="' + ZS.esc(ZS.user.displayName || '') + '">' +
'<div class="mt-8"><button class="btn-primary btn-primary-sm" data-action="saveDisplayName">' + ZS.t("Enregistrer", "Save") + '</button></div></div>' +
'<div class="settings-card mt-16"><h3>' + ZS.t("Username (@)", "Username (@)") + '</h3>' +
'<input class="settings-input" type="text" id="settingsUsername" maxlength="30" pattern="[a-zA-Z0-9_]+" placeholder="@username" value="' + ZS.esc(ZS.user.username) + '">' +
'<div class="form-hint">' + ZS.t("3-30 caractères, lettres, chiffres et underscores. Changement possible une fois par mois.", "3-30 characters, letters, numbers and underscores. Changeable once per month.") + '</div>' +
'<div class="mt-8"><button class="btn-primary btn-primary-sm" data-action="saveUsername">' + ZS.t("Enregistrer", "Save") + '</button></div></div>' +
'<div class="settings-card mt-16"><h3>' + ZS.t("Bio", "Bio") + '</h3>' +
'<textarea class="settings-textarea" id="settingsBio" rows="3" maxlength="500">' + ZS.esc(ZS.user.bio || '') + '</textarea>' +
'<div class="mt-8"><button class="btn-primary btn-primary-sm" data-action="saveBio">' + ZS.t("Enregistrer", "Save") + '</button></div></div>';
} else if (tab === "password") {
el.innerHTML = '<div class="settings-card"><h3>' + ZS.t("Changer le mot de passe", "Change password") + '</h3>' +
'<div class="form-group"><label>' + ZS.t("Mot de passe actuel", "Current password") + '</label>' +
'<input class="settings-input" type="password" id="settingsCurrentPass" required></div>' +
'<div class="form-group"><label>' + ZS.t("Nouveau mot de passe", "New password") + '</label>' +
'<input class="settings-input" type="password" id="settingsNewPass" required></div>' +
'<button class="btn-primary btn-primary-sm" data-action="savePassword">' + ZS.t("Changer", "Change") + '</button></div>';
} else if (tab === "privacy") {
var level = ZS.user.privacyLevel || "connections_only";
el.innerHTML = '<div class="settings-card"><h3>' + ZS.t("Confidentialité", "Privacy") + '</h3>' +
'<div class="privacy-grid mt-12">' +
_privOption("public", "globe", ZS.t("Public", "Public"), ZS.t("Tout le monde", "Everyone"), []) +
_privOption("connections_only", "users", ZS.t("Connexions", "Connections"), ZS.t("Vos contacts", "Your contacts"), []) +
_privOption("private", "lock", ZS.t("Privé", "Private"), ZS.t("Personne sauf vous", "Nobody but you"), []) +
'</div>' +
'<button class="btn-primary btn-primary-sm mt-16" data-action="savePrivacy">' + ZS.t("Enregistrer", "Save") + '</button></div>';
_selectedPrivacy = level;
setTimeout(function() { selectPrivacy(level); }, 0);
} else if (tab === "danger") {
el.innerHTML = '<div class="settings-card"><h3>' + ZS.icon("trash") + ' ' + ZS.t("Supprimer mon compte", "Delete account") + '</h3>' +
'<div class="warning-box"><strong>' + ZS.t("Action irréversible", "Irreversible action") + '</strong>' +
ZS.t("Toutes vos données seront supprimées.", "All your data will be deleted.") + '</div>' +
'<div class="form-group"><label>' + ZS.t("Mot de passe pour confirmer", "Password to confirm") + '</label>' +
'<input class="settings-input" type="password" id="settingsDeletePass" required></div>' +
'<button class="btn-primary btn-primary-sm btn-danger" data-action="deleteAccount">' + ZS.t("Supprimer", "Delete") + '</button></div>';
}
}
async function saveBio() {
var data = await ZS.api("POST", "/api/social/user/update-bio", { bio: document.getElementById("settingsBio").value });
alert(data.error || data.message);
}
async function saveDisplayName() {
var data = await ZS.api("POST", "/api/social/user/update-display-name", { displayName: document.getElementById("settingsDisplayName").value });
if (data.error) { alert(data.error); return; }
ZS.user.displayName = data.displayName;
alert(ZS.t("Nom d'affichage mis à jour", "Display name updated"));
}
async function savePassword() {
var data = await ZS.api("POST", "/api/social/user/update-password", { currentPassword: document.getElementById("settingsCurrentPass").value, newPassword: document.getElementById("settingsNewPass").value });
alert(data.error || data.message);
}
async function savePrivacy() {
if (!_selectedPrivacy) return;
var data = await ZS.api("POST", "/api/social/user/privacy-choice", { level: _selectedPrivacy, acknowledged: true });
if (data.error) { alert(data.error); return; }
ZS.user.privacyLevel = data.privacyLevel;
alert(ZS.t("Confidentialité mise à jour", "Privacy updated"));
}
async function deleteAccount() {
if (!confirm(ZS.t("Êtes-vous sûr ?", "Are you sure?"))) return;
var data = await ZS.api("POST", "/api/social/user/delete-account", { password: document.getElementById("settingsDeletePass").value });
if (data.error) { alert(data.error); return; }
ZS.user = null;
ZS.navigate("feed");
}
// ═══════════════════════════════════════
// COMMUNITY
// ═══════════════════════════════════════
async function renderCommunity() {
var L = ZS.lang;
var html = '<h2 class="mb-16">' + ZS.icon("users") + ' ' + ZS.t("Communauté", "Community") + '</h2>' +
'<div class="compose-box p-16">' +
'<input type="text" class="settings-input w-full" id="searchInput" placeholder="' + ZS.t("Rechercher un utilisateur...", "Search users...") + '" data-key="Enter" data-key-action="searchUsers">' +
'</div>' +
'<div id="searchResults"></div>' +
(ZS.user ? '<div id="connectionsSection" class="mt-16"></div>' : '');
ZS.renderShell(null, html, null);
if (ZS._updateSEO) ZS._updateSEO(
ZS.t("Communauté", "Community") + ' — Zektyc Social',
ZS.t("Recherchez et connectez-vous avec d'autres utilisateurs.", "Search and connect with other users."),
location.origin + location.pathname + '#/community'
);
if (ZS.user) loadConnections();
}
async function searchUsers() {
var q = document.getElementById("searchInput").value.trim();
var el = document.getElementById("searchResults");
if (q.length < 2) { el.innerHTML = ""; return; }
el.innerHTML = _spinner();
var data = await ZS.api("GET", "/api/social/search?q=" + encodeURIComponent(q));
if (!data.users || data.users.length === 0) {
el.innerHTML = '<div class="note-empty p-16">' + ZS.t("Aucun résultat", "No results") + '</div>';
return;
}
el.innerHTML = data.users.map(function(u) {
return '<div class="search-result-item" data-action="nav" data-arg1="profile-view" data-arg2="' + ZS.esc(u.username) + '">' +
_avatar(u.username, 40, u.avatar_url) +
'<div><div class="font-600 text-base">' + ZS.esc(u.username) + '</div>' +
(u.bio ? '<div class="text-xs text-muted">' + ZS.esc(u.bio) + '</div>' : '') +
'</div></div>';
}).join("");
}
async function loadConnections() {
var el = document.getElementById("connectionsSection");
if (!el) return;
var data = await ZS.api("GET", "/api/social/connections");
var html = "";
if (data.incoming && data.incoming.length > 0) {
html += '<h3 class="mb-8">' + ZS.t("Demandes reçues", "Incoming") + '</h3>';
html += data.incoming.map(function(c) {
return '<div class="conn-item">' + _avatar(c.username, 40, c.avatar_url) +
'<div class="conn-info"><div class="conn-name">' + ZS.esc(c.username) + '</div></div>' +
'<div class="conn-actions">' +
'<button class="btn-primary btn-primary-sm" data-action="acceptConnection" data-arg1="' + ZS.esc(c.username) + '">' + ZS.icon("check") + '</button>' +
'<button class="btn-primary btn-primary-sm btn-outline" data-action="rejectConnection" data-arg1="' + ZS.esc(c.username) + '">' + ZS.icon("close") + '</button>' +
'</div></div>';
}).join("");
}
if (data.accepted && data.accepted.length > 0) {
html += '<h3 class="mv-16 mb-8">' + ZS.t("Mes contacts", "My contacts") + ' (' + data.accepted.length + ')</h3>';
html += data.accepted.map(function(c) {
return '<div class="conn-item">' + _avatar(c.username, 40, c.avatar_url) +
'<div class="conn-info"><div class="conn-name">' + ZS.esc(c.username) + '</div></div>' +
'<button class="btn-primary btn-primary-sm btn-outline" data-action="chatWith" data-arg1="' + ZS.esc(c.username) + '">' + ZS.icon("mail") + '</button>' +
'</div>';
}).join("");
}
if (!html) html = '<div class="note-empty p-16">' + ZS.t("Aucune connexion pour le moment.", "No connections yet.") + '</div>';
el.innerHTML = html;
}
async function acceptConnection(username) {
await ZS.api("POST", "/api/social/connections/" + encodeURIComponent(username) + "/accept");
loadConnections();
}
async function rejectConnection(username) {
await ZS.api("POST", "/api/social/connections/" + encodeURIComponent(username) + "/reject");
loadConnections();
}
async function sendConnection(username) {
var data = await ZS.api("POST", "/api/social/connections/" + encodeURIComponent(username));
alert(data.error || data.message);
}
async function removeConnection(username) {
if (!confirm(ZS.t("Retirer ?", "Remove?"))) return;
var data = await ZS.api("DELETE", "/api/social/connections/" + encodeURIComponent(username));
alert(data.error || data.message);
_renderProfilePage(username);
}
// ═══════════════════════════════════════
// MESSAGES
// ═══════════════════════════════════════
async function renderMessages() {
if (!ZS.user) { ZS.navigate("login"); return; }
ZS.renderShell(null, '<h2 class="mb-16">' + ZS.icon("mail") + ' ' + ZS.t("Messages", "Messages") + '</h2>' +
'<div id="convList">' + _spinner() + '</div>', null);
var data = await ZS.api("GET", "/api/social/messages");
var el = document.getElementById("convList");
if (!data.conversations || data.conversations.length === 0) {
el.innerHTML = '<div class="empty-state p-40"><div class="empty-state-icon">' + ZS.icon("mail") + '</div>' +
'<h3>' + ZS.t("Aucune conversation", "No conversations") + '</h3></div>';
return;
}
el.innerHTML = data.conversations.map(function(c) {
return '<div class="msg-list-item" data-action="chatWithUser" data-arg1="' + ZS.esc(c.other_username) + '">' +
'<div class="msg-list-avatar">' + ZS.esc(c.other_username).charAt(0).toUpperCase() + '</div>' +
'<div class="msg-list-info"><div class="msg-list-name">' + ZS.esc(c.other_username) + '</div>' +
'<div class="msg-list-preview">' + ZS.esc(c.content) + '</div></div>' +
'<div class="msg-list-meta"><div class="msg-list-time">' + _timeAgo(c.created_at) + '</div>' +
(c.unread_count > 0 ? '<div class="msg-list-unread">' + c.unread_count + '</div>' : '') +
'</div></div>';
}).join("");
}
async function renderChat(username) {
if (!ZS.user) { ZS.navigate("login"); return; }
var html = '<div class="chat-window">' +
'<div class="chat-header-row">' +
'<button class="btn-ghost" data-action="nav" data-arg1="messages">' + ZS.icon("arrowLeft") + '</button>' +
'<h2 class="chat-header-name">' + ZS.esc(username) + '</h2></div>' +
'<div class="chat-messages" id="chatMessages">' + _spinner() + '</div>' +
'<div class="chat-input-bar">' +
'<input type="text" id="chatInput" placeholder="' + ZS.t("Écrire...", "Type...") + '" data-key="Enter" data-key-action="sendMsg" data-arg1="' + ZS.esc(username) + '">' +
'<button class="btn-primary btn-primary-sm" data-action="sendMsg" data-arg1="' + ZS.esc(username) + '">' + ZS.icon("send") + '</button></div></div>';
ZS.renderShell(null, html, null);
await loadChatMessages(username);
}
async function loadChatMessages(username) {
var el = document.getElementById("chatMessages");
var data = await ZS.api("GET", "/api/social/messages/" + encodeURIComponent(username));
if (!data.messages || data.messages.length === 0) {
el.innerHTML = '<div class="note-empty p-24">' + ZS.t("Pas encore de messages.", "No messages yet.") + '</div>';
return;
}
el.innerHTML = data.messages.map(function(m) {
var isMine = m.sender_id === ZS.user.id;
return '<div class="chat-bubble ' + (isMine ? 'chat-sent' : 'chat-received') + '">' +
'<div>' + ZS.esc(m.content) + '</div>' +
'<div class="chat-time">' + _timeAgo(m.created_at) + '</div></div>';
}).join("");
el.scrollTop = el.scrollHeight;
}
async function sendMsg(username) {
var input = document.getElementById("chatInput");
if (!input || !input.value.trim()) return;
var content = input.value.trim();
input.value = "";
var data = await ZS.api("POST", "/api/social/messages/" + encodeURIComponent(username), { content: content });
if (data.error) { alert(data.error); return; }
loadChatMessages(username);
}
// ═══════════════════════════════════════
// NOTIFICATIONS
// ═══════════════════════════════════════
async function renderNotifications() {
if (!ZS.user) { ZS.navigate("login"); return; }
ZS.renderShell(null, '<div class="notif-header">' +
'<h2>' + ZS.icon("bell") + ' ' + ZS.t("Alertes", "Alerts") + '</h2>' +
'<button class="btn-primary btn-primary-sm btn-outline" data-action="markAllRead">' + ZS.t("Tout marquer lu", "Mark all read") + '</button></div>' +
'<div id="verifyRequests"></div>' +
'<div id="notifList">' + _spinner() + '</div>', null);
// Load verification requests
var vrData = await ZS.api("GET", "/api/social/verification/pending");
var vrEl = document.getElementById("verifyRequests");
if (vrEl && vrData.requests && vrData.requests.length > 0) {
vrEl.innerHTML = '<div class="verify-requests-section">' +
'<h3 class="mb-8">' + ZS.icon("verifiedGold") + ' ' + ZS.t("Propositions de vérification", "Verification proposals") + '</h3>' +
vrData.requests.map(function(r) {
return '<div class="verify-request-item">' +
_avatar(r.username, 36, r.avatar_url) +
'<div class="verify-request-info"><strong>' + ZS.esc(r.username) + '</strong> ' +
ZS.t("souhaite vous attribuer un badge Vérifié", "wants to give you a Verified badge") + '</div>' +
'<div class="verify-request-actions">' +
'<button class="btn-primary btn-primary-sm" data-action="acceptVerify" data-arg1="' + r.id + '">' + ZS.icon("check") + ' ' + ZS.t("Accepter", "Accept") + '</button>' +
'<button class="btn-primary btn-primary-sm btn-outline" data-action="rejectVerify" data-arg1="' + r.id + '">' + ZS.icon("close") + '</button>' +
'</div></div>';
}).join("") + '</div>';
}
var data = await ZS.api("GET", "/api/social/notifications");
var el = document.getElementById("notifList");
if (!data.notifications || data.notifications.length === 0) {
el.innerHTML = '<div class="empty-state p-40"><div class="empty-state-icon">' + ZS.icon("bell") + '</div>' +
'<h3>' + ZS.t("Aucune alerte", "No alerts") + '</h3></div>';
return;
}
var icons = { like: "heart", comment: "comment", message: "mail", connection_request: "user", connection_accepted: "users", follow: "user", post_approved: "check", post_rejected: "close", verification_request: "verifiedGold", verification_accepted: "check" };
el.innerHTML = data.notifications.map(function(n) {
var text = _notifText(n);
var iconName = icons[n.type] || "bell";
var actions = _notifActions(n);
var usernameLink = n.from_username ? ' data-action="navProfile" data-arg1="' + ZS.esc(n.from_username) + '"' : '';
return '<div class="notif-item' + (n.is_read ? '' : ' unread') + '">' +
'<div class="notif-icon"' + usernameLink + '>' + ZS.icon(iconName) + '</div>' +
'<div class="notif-body"' + usernameLink + '><div class="notif-text">' + text + '</div>' +
'<div class="notif-time">' + _timeAgo(n.created_at) + '</div></div>' +
(actions ? '<div class="notif-actions">' + actions + '</div>' : '') + '</div>';
}).join("");
ZS.refreshCounts();
}
function _notifText(n) {
var u = n.from_username ? ZS.esc(n.from_display_name || n.from_username) : "";
switch (n.type) {
case "like": return '<strong>' + u + '</strong> ' + ZS.t("a aimé votre publication", "liked your post");
case "comment": return '<strong>' + u + '</strong> ' + ZS.t("a commenté votre publication", "commented on your post");
case "message": return '<strong>' + u + '</strong> ' + ZS.t("vous a envoyé un message", "sent you a message");
case "connection_request": return '<strong>' + u + '</strong> ' + ZS.t("souhaite se connecter", "wants to connect");
case "connection_accepted": return '<strong>' + u + '</strong> ' + ZS.t("a accepté votre demande", "accepted your request");
case "follow": return '<strong>' + u + '</strong> ' + ZS.t("commence à vous suivre", "started following you");
case "post_approved": return '<strong>' + u + '</strong> ' + ZS.t("a vérifié votre publication", "verified your post");
case "post_rejected": return '<strong>' + u + '</strong> ' + ZS.t("a rejeté votre publication", "rejected your post");
case "verification_request": return '<strong>' + u + '</strong> ' + ZS.t("vous propose une vérification Blue ✓", "proposes a Blue ✓ verification");
case "verification_accepted": return '<strong>' + u + '</strong> ' + ZS.t("a accepté votre vérification", "accepted your verification");
default: return n.type;
}
}
function _notifActions(n) {
if (!n.from_username) return "";
switch (n.type) {
case "connection_request":
return '<button class="btn-primary btn-primary-sm" data-action="acceptConnection" data-arg1="' + ZS.esc(n.from_username) + '">' + ZS.icon("check") + ' ' + ZS.t("Accepter", "Accept") + '</button>' +
'<button class="btn-primary btn-primary-sm btn-outline" data-action="rejectConnection" data-arg1="' + ZS.esc(n.from_username) + '">' + ZS.icon("close") + '</button>';
case "message":
return '<button class="btn-primary btn-primary-sm btn-outline" data-action="chatWith" data-arg1="' + ZS.esc(n.from_username) + '">' + ZS.icon("reply") + ' ' + ZS.t("Répondre", "Reply") + '</button>';
case "follow":
return '<button class="btn-primary btn-primary-sm" data-action="follow" data-arg1="' + n.from_user_id + '">' + ZS.icon("plus") + ' ' + ZS.t("Suivre", "Follow") + '</button>';
case "comment":
return '<button class="btn-primary btn-primary-sm btn-outline" data-action="navProfile" data-arg1="' + ZS.esc(n.from_username) + '">' + ZS.icon("user") + ' ' + ZS.t("Voir profil", "View profile") + '</button>';
default:
return "";
}
}
async function markAllRead() {
await ZS.api("POST", "/api/social/notifications/read");
ZS._unreadNotifs = 0;
ZS.refreshCounts();
renderNotifications();
}
// ═══════════════════════════════════════
// MODERATION
// ═══════════════════════════════════════
var _modTab = "posts";
async function renderMod() {
if (!ZS.user || (ZS.user.role !== "admin" && ZS.user.role !== "moderator")) {
ZS.renderShell(null, '<div class="empty-state"><div class="empty-state-icon">' + ZS.icon("shield") + '</div><h3>' + ZS.t("Accès refusé", "Access denied") + '</h3></div>', null);
return;
}
var html = '<h2 class="mb-16">' + ZS.icon("shield") + ' ' + ZS.t("Modération", "Moderation") + '</h2>' +
'<div class="mod-tabs">' +
'<button class="mod-tab active" data-action="switchModTab" data-arg1="posts">' + ZS.t("Posts", "Posts") + '</button>' +
'<button class="mod-tab" data-action="switchModTab" data-arg1="notes">' + ZS.t("Notes", "Notes") + '</button>' +
'<button class="mod-tab" data-action="switchModTab" data-arg1="reports">' + ZS.t("Signalements", "Reports") + '</button>' +
'</div><div id="modContent">' + _spinner() + '</div>';
ZS.renderShell(null, html, null);
_loadModData();
}
async function _loadModData() {
var data = await ZS.api("GET", "/api/social/mod/pending");
window._modData = data;
_renderModTab();
}
function switchModTab(tab, btn) {
_modTab = tab;
document.querySelectorAll(".mod-tab").forEach(function(t) { t.classList.remove("active"); });
btn.classList.add("active");
_renderModTab();
}
function _renderModTab() {
var data = window._modData || {};
var el = document.getElementById("modContent");
if (!el) return;
if (_modTab === "posts") {
var posts = data.posts || [];
if (posts.length === 0) {
el.innerHTML = '<div class="empty-state p-30"><div class="empty-state-icon">' + ZS.icon("check") + '</div><h3>' + ZS.t("Tout est vérifié", "All clear") + '</h3></div>';
return;
}
el.innerHTML = posts.map(function(p) {
return '<div class="mod-card"><div class="mod-card-header"><span class="mod-card-author">' + ZS.esc(p.username) + '</span><span class="mod-card-time">' + _timeAgo(p.created_at) + '</span></div>' +
'<div class="mod-card-body">' + ZS.esc(p.content) + '</div>' +
'<div class="mod-card-actions">' +
'<button class="btn-primary btn-primary-sm" data-action="modAction" data-arg1="posts" data-arg2="' + p.id + '" data-action-type="approve">' + ZS.icon("check") + ' ' + ZS.t("Approuver", "Approve") + '</button>' +
'<button class="btn-primary btn-primary-sm btn-danger" data-action="modAction" data-arg1="posts" data-arg2="' + p.id + '" data-action-type="reject">' + ZS.icon("close") + ' ' + ZS.t("Rejeter", "Reject") + '</button>' +
'</div></div>';
}).join("");
} else if (_modTab === "notes") {
var notes = data.notes || [];
if (notes.length === 0) {
el.innerHTML = '<div class="empty-state p-30"><div class="empty-state-icon">' + ZS.icon("check") + '</div><h3>' + ZS.t("Aucune note en attente", "No pending notes") + '</h3></div>';
return;
}
el.innerHTML = notes.map(function(n) {
return '<div class="mod-card"><div class="mod-card-header"><span class="mod-card-author">' + ZS.esc(n.username) + '</span><span class="mod-card-time">' + _timeAgo(n.created_at) + '</span></div>' +
'<div class="mod-card-body">' + ZS.esc(n.content) + '</div>' +
'<div class="mod-card-actions">' +
'<button class="btn-primary btn-primary-sm" data-action="modNoteAction" data-arg2="' + n.id + '" data-action-type="approve">' + ZS.icon("check") + '</button>' +
'<button class="btn-primary btn-primary-sm btn-danger" data-action="modNoteAction" data-arg2="' + n.id + '" data-action-type="reject">' + ZS.icon("close") + '</button>' +
'</div></div>';
}).join("");
} else if (_modTab === "reports") {
var reports = data.reports || [];
if (reports.length === 0) {
el.innerHTML = '<div class="empty-state p-30"><div class="empty-state-icon">' + ZS.icon("check") + '</div><h3>' + ZS.t("Aucun signalement", "No reports") + '</h3></div>';
return;
}
el.innerHTML = reports.map(function(r) {
return '<div class="mod-card"><div class="mod-card-meta">' + ZS.t("Par", "By") + ' <strong>' + ZS.esc(r.reporter_username) + '</strong> · ' + _timeAgo(r.created_at) + '</div>' +
'<div class="mod-card-body">' + r.target_type + '#' + r.target_id + ' — ' + ZS.esc(r.reason) + '</div>' +
'<div class="mod-card-actions">' +
'<button class="btn-primary btn-primary-sm" data-action="resolveReport" data-arg1="' + r.id + '">' + ZS.icon("check") + ' ' + ZS.t("Résolu", "Resolved") + '</button>' +
'</div></div>';
}).join("");
}
}
function modAction(type, id, actionType) {
ZS.api("POST", "/api/social/mod/verify/" + id, { action: actionType }).then(function(d) {
if (d.error) alert(d.error); _loadModData();
});
}
function modNoteAction(id, actionType) {
ZS.api("POST", "/api/social/mod/note/" + id, { action: actionType }).then(function(d) {
if (d.error) alert(d.error); _loadModData();
});
}
function resolveReport(id) {
ZS.api("POST", "/api/social/mod/report/" + id, { action: "resolve" }).then(function(d) {
if (d.error) alert(d.error); _loadModData();
});
}