- site vitrine + fingerprint test + zmap + z-panel - remplacement duckdns -> riricdev.tail5ea5cd.ts.net (canonical/og/alternates/docs) - suppression du token DuckDNS (plus utilisé)
1007 lines
55 KiB
JavaScript
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();
|
|
});
|
|
}
|