Je tente de créer un forum de discussions (à titre éducatif et pas en concurrence)

𝑪𝑨𝑷𝑰𝑻𝑨𝑰𝑵𝑬 𝑱𝑨𝑪𝑲
Divin
Donateur 🤲
🥇 Top contributeur
Messages
55 153
Fofocoins
257 984
Don à Foforum
Virement de 100€ 💸
Virement de 100€ 💸
Virement de 50€
Genre
Homme

On passe au html du thème​

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE html>
<html b:version='2' class='v2' expr:dir='data:blog.languageDirection' xmlns='http://www.w3.org/1999/xhtml' xmlns:b='http://www.google.com/2005/gml/b' xmlns:data='http://www.google.com/2005/gml/data' xmlns:expr='http://www.google.com/2005/gml/expr'>
<head>
<meta charset='UTF-8'/>
<meta content='width=device-width, initial-scale=1' name='viewport'/>
<title><data:blog.pageTitle/></title>
<b:skin><![CDATA[ ]]></b:skin>
</head>
<body>

<div id='site-header'>
<div>
<h1 id='site-title'>&#128172; ForumFrance</h1>
<p id='header-sub'>Chargement&#8230;</p>
</div>
<div id='header-user'></div>
</div>

<div id='forum-root'></div>

<div id='site-footer'>ForumFrance &#169; 2025</div>

<script type='module'>
//<![CDATA[

// ── CSS injecte via JS ──────────────────────────────────────────
const st = document.createElement('style');
st.textContent = `
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,sans-serif;background:#f4f4f8;min-height:100vh}
#site-header{background:linear-gradient(135deg,#7F77DD,#D4537E 50%,#BA7517);padding:1.2rem 1.5rem;color:#fff;display:flex;justify-content:space-between;align-items:center;position:sticky;top:0;z-index:100;box-shadow:0 2px 12px rgba(0,0,0,.15)}
#site-title{font-size:22px;font-weight:700}
#header-sub{font-size:12px;opacity:.8;margin-top:2px}
#header-user{display:flex;align-items:center;gap:10px}
#forum-root{max-width:780px;margin:0 auto;padding:1.5rem 1rem 3rem}
#site-footer{text-align:center;color:#bbb;font-size:12px;padding:2rem 0 1rem}
.fc{background:#fff;border-radius:14px;border:1px solid #ececec;padding:16px;margin-bottom:12px;box-shadow:0 1px 4px rgba(0,0,0,.04)}
.fi,.fta,.fse{width:100%;padding:9px 12px;border-radius:10px;border:1.5px solid #e0e0e0;font-size:14px;outline:none;font-family:inherit;background:#fff}
.fta{resize:vertical}
.fb{padding:9px 20px;border-radius:10px;border:none;font-weight:700;font-size:14px;cursor:pointer}
.fbg{background:linear-gradient(90deg,#7F77DD,#D4537E);color:#fff}
.fbo{background:#f0f0f0;color:#666}
.fbsm{padding:5px 14px;font-size:13px}
.fbgl{background:#fff;color:#333;border:1.5px solid #ddd;display:flex;align-items:center;gap:8px;justify-content:center;width:100%;margin-top:10px;padding:9px 20px;border-radius:10px;font-weight:700;font-size:14px;cursor:pointer}
.ft{padding:8px 20px;border-radius:10px;border:none;font-weight:700;font-size:14px;cursor:pointer}
.ft.on{background:#7F77DD;color:#fff}
.ft.off{background:#f0f0f0;color:#888}
.fcb{padding:5px 14px;border-radius:20px;font-size:13px;cursor:pointer;border:1.5px solid #ddd;background:#fff;color:#888}
.fcb.on{font-weight:700}
.ftag{display:inline-block;padding:2px 10px;border-radius:20px;font-size:11px;font-weight:600;margin-right:6px}
.fpin{display:inline-flex;align-items:center;gap:4px;background:#FAEEDA;color:#BA7517;font-size:11px;font-weight:700;padding:2px 8px;border-radius:20px;margin-right:6px}
.fav{border-radius:50%;display:flex;align-items:center;justify-content:center;font-weight:700;flex-shrink:0;overflow:hidden}
.fav img{width:100%;height:100%;object-fit:cover}
.flk{display:flex;align-items:center;gap:4px;padding:4px 12px;border-radius:20px;border:1.5px solid #eee;background:#fafafa;color:#aaa;font-size:13px;font-weight:600;cursor:pointer}
.flk.on{border-color:#D4537E;background:#FBEAF0;color:#D4537E}
.ferr{color:#c0392b;font-size:13px;margin-top:6px}
.fsh{background:#EEEDFE;border-radius:10px;padding:10px 12px;font-size:12px;color:#7F77DD;margin-bottom:12px}
.flbl{font-size:13px;font-weight:600;color:#555;display:block;margin-bottom:5px;margin-top:10px}
.fmu{color:#aaa;font-size:12px}
.frw{display:flex;align-items:center;gap:8px}
.fbk{background:none;border:none;color:#7F77DD;font-weight:700;font-size:14px;cursor:pointer;padding:8px 0}
.ftb{display:flex;gap:8px;align-items:center;margin-bottom:12px}
.fcts{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:14px}
.fst{background:#fafafa;border-radius:10px;padding:12px;text-align:center}
.fg2{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-top:16px}
.faw{max-width:420px;margin:2rem auto}
.tpc:hover{box-shadow:0 4px 16px rgba(127,119,221,.12);border-color:#d0cef8;transition:all .15s}
`;
document.head.appendChild(st);

// ── Firebase ────────────────────────────────────────────────────
import { initializeApp }
from "https://www.gstatic.com/firebasejs/10.12.0/firebase-app.js";
import { getAuth, createUserWithEmailAndPassword, signInWithEmailAndPassword,
signInWithPopup, GoogleAuthProvider, signOut, onAuthStateChanged,
updateProfile }
from "https://www.gstatic.com/firebasejs/10.12.0/firebase-auth.js";
import { getFirestore, collection, addDoc, doc, updateDoc, increment,
query, orderBy, onSnapshot, serverTimestamp, getDoc, setDoc, getDocs }
from "https://www.gstatic.com/firebasejs/10.12.0/firebase-firestore.js";

const firebaseConfig = {
apiKey: "AIzaSyAGAlQFAO2z1K5iQA5h6JHPeX_zaMOkb5Q",
authDomain: "forumfrance-6de67.firebaseapp.com",
projectId: "forumfrance-6de67",
storageBucket: "forumfrance-6de67.firebasestorage.app",
messagingSenderId: "246371695638",
appId: "1:246371695638:web:f48296c3cb65953cf88937"
};

const app = initializeApp(firebaseConfig);
const auth = getAuth(app);
const db = getFirestore(app);
const gProvider = new GoogleAuthProvider();

// ── Categories ──────────────────────────────────────────────────
const CATS = [
{ id:"all", label:"Tout", color:"#7F77DD", bg:"#EEEDFE" },
{ id:"actu", label:"Actualite", color:"#185FA5", bg:"#E6F1FB" },
{ id:"culture", label:"Culture", color:"#D4537E", bg:"#FBEAF0" },
{ id:"tech", label:"Tech", color:"#0F6E56", bg:"#E1F5EE" },
{ id:"humour", label:"Humour", color:"#BA7517", bg:"#FAEEDA" },
{ id:"sport", label:"Sport", color:"#A32D2D", bg:"#FCEBEB" },
];
const catOf = id => CATS.find(c => c.id === id) || CATS[0];
const ts2str = ts => {
if (!ts) return "";
const d = ts.toDate ? ts.toDate() : new Date(ts);
return d.toLocaleDateString("fr-FR", { day:"2-digit", month:"short", hour:"2-digit", minute:"2-digit" });
};

// ── State ───────────────────────────────────────────────────────
let S = {
screen:"loading", authTab:"login", user:null,
topics:[], replies:{}, liked:{},
activeCat:"all", search:"", view:"list",
activeTopic:null, activeAuthor:null, showForm:false,
lEmail:"", lPass:"",
rPseudo:"", rEmail:"", rPass:"", rPass2:"",
nTitle:"", nContent:"", nCat:"actu", replyText:"",
error:"", loading:false,
};

const root = document.getElementById("forum-root");
const hSub = document.getElementById("header-sub");
const hUser = document.getElementById("header-user");

function setState(patch, skip) {
S = Object.assign({}, S, patch);
if (!skip) render();
}

// ── Avatar ──────────────────────────────────────────────────────
function av(user, size) {
size = size || 36;
if (user && user.photoURL)
return '<div class="fav" style="width:' + size + 'px;height:' + size + 'px"><img src="' + user.photoURL + '" referrerpolicy="no-referrer"></div>';
const colors = ["#7F77DD","#D4537E","#0F6E56","#BA7517","#185FA5","#A32D2D"];
const bgs = ["#EEEDFE","#FBEAF0","#E1F5EE","#FAEEDA","#E6F1FB","#FCEBEB"];
const name = (user && (user.displayName || user.pseudo)) || "?";
const idx = name.charCodeAt(0) % colors.length;
return '<div class="fav" style="width:' + size + 'px;height:' + size + 'px;background:' + bgs[idx] + ';color:' + colors[idx] + ';font-size:' + (size > 40 ? 20 : 14) + 'px">' + name[0].toUpperCase() + '</div>';
}

// ── Render ──────────────────────────────────────────────────────
function render() {
updateHeader();
if (S.screen === "loading") { root.innerHTML = '<div style="text-align:center;padding:60px 0;color:#aaa"><div style="font-size:40px">&#128172;</div><p style="margin-top:12px">Chargement&#8230;</p></div>'; return; }
if (S.screen === "auth") { renderAuth(); return; }
if (S.view === "list") { renderList(); return; }
if (S.view === "topic") { renderTopic(); return; }
if (S.view === "profile") { renderProfile(); return; }
}

function updateHeader() {
if (!S.user) { hSub.textContent = "Connectez-vous pour participer"; hUser.innerHTML = ""; return; }
hSub.textContent = S.topics.length + " sujets · Bonjour, " + (S.user.displayName || "?") + " !";
hUser.innerHTML = '<div id="hav" style="cursor:pointer">' + av(S.user, 38) + '</div><button class="fb fbo fbsm" id="hlo" style="font-size:12px">Deconnexion</button>';
document.getElementById("hav")?.addEventListener("click", () => setState({ view:"profile", activeAuthor:S.user.uid }));
document.getElementById("hlo")?.addEventListener("click", () => signOut(auth));
}

// ── Google icon ─────────────────────────────────────────────────
function gico() {
return '<svg width="18" height="18" viewBox="0 0 48 48"><path fill="#EA4335" d="M24 9.5c3.54 0 6.71 1.22 9.21 3.6l6.85-6.85C35.9 2.38 30.47 0 24 0 14.62 0 6.51 5.38 2.56 13.22l7.98 6.19C12.43 13.72 17.74 9.5 24 9.5z"/><path fill="#4285F4" d="M46.98 24.55c0-1.57-.15-3.09-.38-4.55H24v9.02h12.94c-.58 2.96-2.26 5.48-4.78 7.18l7.73 6c4.51-4.18 7.09-10.36 7.09-17.65z"/><path fill="#FBBC05" d="M10.53 28.59c-.48-1.45-.76-2.99-.76-4.59s.27-3.14.76-4.59l-7.98-6.19C.92 16.46 0 20.12 0 24c0 3.88.92 7.54 2.56 10.78l7.97-6.19z"/><path fill="#34A853" d="M24 48c6.48 0 11.93-2.13 15.89-5.81l-7.73-6c-2.15 1.45-4.92 2.3-8.16 2.3-6.26 0-11.57-4.22-13.47-9.91l-7.98 6.19C6.51 42.62 14.62 48 24 48z"/></svg>';
}

// ── AUTH ────────────────────────────────────────────────────────
function renderAuth() {
const login = S.authTab === "login";
root.innerHTML =
'<div class="faw"><div class="fc">' +
'<div class="frw" style="margin-bottom:20px;gap:8px">' +
'<button class="ft ' + (login?"on":"off") + '" id="tl">Connexion</button>' +
'<button class="ft ' + (!login?"on":"off") + '" id="tr">Inscription</button>' +
'</div>' +
(login ?
'<label class="flbl">Email</label>' +
'<input class="fi" id="le" type="email" placeholder="vous@exemple.com" value="' + S.lEmail + '">' +
'<label class="flbl">Mot de passe</label>' +
'<input class="fi" id="lp" type="password" placeholder="&#8226;&#8226;&#8226;&#8226;&#8226;&#8226;" value="' + S.lPass + '">' +
(S.error ? '<div class="ferr">&#9888; ' + S.error + '</div>' : '') +
'<button class="fb fbg" id="bli" style="width:100%;margin-top:14px">' + (S.loading?"Connexion&#8230;":"Se connecter") + '</button>' +
'<button class="fbgl" id="bgl">' + gico() + ' Continuer avec Google</button>' +
'<p style="text-align:center;margin-top:12px;font-size:13px;color:#aaa">Pas de compte ? <a href="#" id="gor" style="color:#7F77DD;font-weight:600">S\'inscrire</a></p>'
:
'<label class="flbl">Pseudo (3-20 car.)</label>' +
'<input class="fi" id="rp" placeholder="MonPseudo" value="' + S.rPseudo + '">' +
'<label class="flbl">Email</label>' +
'<input class="fi" id="re" type="email" placeholder="vous@exemple.com" value="' + S.rEmail + '">' +
'<label class="flbl">Mot de passe (6 car. min.)</label>' +
'<input class="fi" id="rpa" type="password" placeholder="&#8226;&#8226;&#8226;&#8226;&#8226;&#8226;" value="' + S.rPass + '">' +
'<label class="flbl">Confirmer</label>' +
'<input class="fi" id="rp2" type="password" placeholder="&#8226;&#8226;&#8226;&#8226;&#8226;&#8226;" value="' + S.rPass2 + '">' +
'<div class="fsh" style="margin-top:12px">&#128737; Protection anti-abus activee.</div>' +
(S.error ? '<div class="ferr">&#9888; ' + S.error + '</div>' : '') +
'<button class="fb fbg" id="brg" style="width:100%;margin-top:10px">' + (S.loading?"Inscription&#8230;":"Creer mon compte") + '</button>' +
'<button class="fbgl" id="bgr">' + gico() + ' Continuer avec Google</button>' +
'<p style="text-align:center;margin-top:12px;font-size:13px;color:#aaa">Deja membre ? <a href="#" id="gol" style="color:#7F77DD;font-weight:600">Se connecter</a></p>'
) +
'</div></div>';

document.getElementById("tl")?.addEventListener("click", () => setState({ authTab:"login", error:"" }));
document.getElementById("tr")?.addEventListener("click", () => setState({ authTab:"register", error:"" }));
document.getElementById("gol")?.addEventListener("click", e => { e.preventDefault(); setState({ authTab:"login", error:"" }); });
document.getElementById("gor")?.addEventListener("click", e => { e.preventDefault(); setState({ authTab:"register", error:"" }); });
document.getElementById("bli")?.addEventListener("click", doLogin);
document.getElementById("brg")?.addEventListener("click", doRegister);
document.getElementById("bgl")?.addEventListener("click", doGoogle);
document.getElementById("bgr")?.addEventListener("click", doGoogle);

const bind = (id, key) => document.getElementById(id)?.addEventListener("input", e => setState({ [key]: e.target.value }, true));
bind("le","lEmail"); bind("lp","lPass");
bind("rp","rPseudo"); bind("re","rEmail"); bind("rpa","rPass"); bind("rp2","rPass2");
}

// ── Auth actions ────────────────────────────────────────────────
function valPseudo(p) {
if (!p || p.length < 3 || p.length > 20) return "Pseudo : 3 a 20 caracteres.";
if (!/^[a-zA-Z0-9_\-]+$/.test(p)) return "Caracteres invalides.";
if (["admin","moderateur","root","system"].some(w => p.toLowerCase().includes(w))) return "Pseudo reserve.";
return null;
}

async function doLogin() {
const email = document.getElementById("le")?.value.trim();
const pass = document.getElementById("lp")?.value;
if (!email || !pass) { setState({ error:"Remplissez tous les champs." }); return; }
setState({ loading:true, error:"" });
try {
await signInWithEmailAndPassword(auth, email, pass);
} catch(e) {
const msg = e.code === "auth/invalid-credential" ? "Email ou mot de passe incorrect."
: e.code === "auth/too-many-requests" ? "Trop de tentatives. Reessayez plus tard."
: e.message;
setState({ loading:false, error:msg });
}
}

async function doRegister() {
const pseudo = document.getElementById("rp")?.value.trim();
const email = document.getElementById("re")?.value.trim();
const pass = document.getElementById("rpa")?.value;
const pass2 = document.getElementById("rp2")?.value;
const err = valPseudo(pseudo);
if (err) { setState({ error:err }); return; }
if (!email) { setState({ error:"Email requis." }); return; }
if (pass.length < 6){ setState({ error:"Mot de passe : 6 caracteres minimum." }); return; }
if (pass !== pass2) { setState({ error:"Les mots de passe ne correspondent pas." }); return; }
setState({ loading:true, error:"" });
try {
const cred = await createUserWithEmailAndPassword(auth, email, pass);
await updateProfile(cred.user, { displayName: pseudo });
await cred.user.getIdToken(true);
await setDoc(doc(db, "users", cred.user.uid), {
pseudo, email,
joined: new Date().toLocaleDateString("fr-FR", { month:"long", year:"numeric" }),
posts:0, role:"member",
});
} catch(e) {
const msg = e.code === "auth/email-already-in-use" ? "Cet email est deja utilise." : e.message;
setState({ loading:false, error:msg });
}
}

async function doGoogle() {
try {
const result = await signInWithPopup(auth, gProvider);
const u = result.user;
const ref = doc(db, "users", u.uid);
const snap = await getDoc(ref);
if (!snap.exists()) {
await setDoc(ref, {
pseudo: u.displayName || "Utilisateur", email: u.email,
joined: new Date().toLocaleDateString("fr-FR", { month:"long", year:"numeric" }),
posts:0, role:"member",
});
}
} catch(e) {
setState({ error:"Connexion Google annulee ou bloquee." });
}
}

// ── LIST ────────────────────────────────────────────────────────
function renderList() {
const filtered = S.topics
.filter(t => (S.activeCat === "all" || t.category === S.activeCat) && (!S.search || t.title.toLowerCase().includes(S.search.toLowerCase())))
.sort((a,b) => (b.pinned?1:0) - (a.pinned?1:0));

let thtml = filtered.length === 0
? '<p style="text-align:center;color:#bbb;padding:2rem 0">Aucun sujet trouve.</p>'
: filtered.map(function(t) {
const cat = catOf(t.category);
return '<div class="fc tpc" data-id="' + t.id + '" style="cursor:pointer">' +
'<div class="frw" style="align-items:flex-start;gap:10px">' +
'<div class="alnk" data-uid="' + t.authorId + '" style="cursor:pointer">' + av({displayName:t.author, photoURL:t.photoURL}) + '</div>' +
'<div style="flex:1;min-width:0">' +
(t.pinned ? '<span class="fpin">&#128204;</span>' : '') +
'<span class="ftag" style="background:' + cat.bg + ';color:' + cat.color + '">' + cat.label + '</span>' +
'<div style="font-size:15px;font-weight:600;color:#222;margin:4px 0 2px;line-height:1.3">' + t.title + '</div>' +
'<div class="fmu">par <b style="color:#888;cursor:pointer">' + t.author + '</b> · ' + ts2str(t.createdAt) + '</div>' +
'</div></div>' +
'<div class="frw" style="margin-top:10px">' +
'<button class="flk' + (S.liked[t.id]?" on":"") + ' lkb" data-id="' + t.id + '">&#10084; ' + (t.likes||0) + '</button>' +
'<span class="fmu" style="margin-left:8px">&#128172; ' + (t.replyCount||0) + '</span>' +
'</div></div>';
}).join("");

let form = "";
if (S.showForm) {
form = '<div class="fc" style="border:1.5px solid #e0e0e0;margin-bottom:16px">' +
'<div style="font-size:16px;font-weight:700;color:#333;margin-bottom:10px">Creer un sujet</div>' +
'<label class="flbl">Titre</label><input class="fi" id="nt" placeholder="Titre du sujet" value="' + S.nTitle + '">' +
'<label class="flbl">Categorie</label>' +
'<select class="fse" id="nc" style="margin-top:5px">' +
CATS.filter(c => c.id !== "all").map(c => '<option value="' + c.id + '"' + (c.id === S.nCat?" selected":"") + '>' + c.label + '</option>').join("") +
'</select>' +
'<label class="flbl">Contenu</label><textarea class="fta" id="nco" rows="4" placeholder="Decrivez votre sujet">' + S.nContent + '</textarea>' +
'<div class="frw" style="margin-top:10px;gap:8px">' +
'<button class="fb fbg" id="bpt">Publier</button>' +
'<button class="fb fbo" id="bcl">Annuler</button>' +
'</div></div>';
}

root.innerHTML =
'<div class="ftb">' +
'<input class="fi" id="sr" placeholder="&#128269; Rechercher" value="' + S.search + '" style="flex:1">' +
'<button class="fb fbg" id="bnt">+ Nouveau sujet</button>' +
'</div>' +
form +
'<div class="fcts">' +
CATS.map(function(c) {
const on = S.activeCat === c.id;
return '<button class="fcb' + (on?" on":"") + '" data-cat="' + c.id + '" style="' + (on?"border-color:"+c.color+";background:"+c.bg+";color:"+c.color+";font-weight:700":"") + '">' + c.label + '</button>';
}).join("") +
'</div>' + thtml;

document.getElementById("sr")?.addEventListener("input", e => setState({ search:e.target.value }));
document.getElementById("bnt")?.addEventListener("click", () => setState({ showForm:!S.showForm }));
document.querySelectorAll(".fcb").forEach(b => b.addEventListener("click", () => setState({ activeCat:b.dataset.cat })));
document.querySelectorAll(".tpc").forEach(el => el.addEventListener("click", function() {
const t = S.topics.find(x => x.id === el.dataset.id);
if (t) setState({ view:"topic", activeTopic:t });
}));
document.querySelectorAll(".alnk").forEach(el => el.addEventListener("click", function(e) {
e.stopPropagation();
setState({ view:"profile", activeAuthor:el.dataset.uid });
}));
document.querySelectorAll(".lkb").forEach(btn => btn.addEventListener("click", async function(e) {
e.stopPropagation();
const id = btn.dataset.id;
const on = !S.liked[id];
const liked = Object.assign({}, S.liked);
liked[id] = on;
setState({ liked:liked });
await updateDoc(doc(db,"topics",id), { likes:increment(on?1:-1) });
}));
if (S.showForm) {
document.getElementById("nt")?.addEventListener("input", e => setState({ nTitle:e.target.value }, true));
document.getElementById("nco")?.addEventListener("input", e => setState({ nContent:e.target.value }, true));
document.getElementById("nc")?.addEventListener("change", e => setState({ nCat:e.target.value }, true));
document.getElementById("bcl")?.addEventListener("click", () => setState({ showForm:false }));
document.getElementById("bpt")?.addEventListener("click", doPost);
}
}

async function doPost() {
const title = document.getElementById("nt")?.value.trim();
const content = document.getElementById("nco")?.value.trim();
const cat = document.getElementById("nc")?.value;
if (!title || !content) return;
const u = S.user;
await addDoc(collection(db,"topics"), {
title, content, category:cat,
author:u.displayName||"Anonyme", authorId:u.uid,
photoURL:u.photoURL||null,
likes:0, replyCount:0, pinned:false,
createdAt:serverTimestamp(),
});
setState({ showForm:false, nTitle:"", nContent:"", nCat:"actu" });
}

// ── TOPIC ───────────────────────────────────────────────────────
function renderTopic() {
const t = S.activeTopic;
if (!t) { setState({ view:"list" }); return; }
const cat = catOf(t.category);
const reps = S.replies[t.id] || [];

root.innerHTML =
'<button class="fbk" id="bk">&#8592; Retour</button>' +
'<div class="fc">' +
'<span class="ftag" style="background:' + cat.bg + ';color:' + cat.color + '">' + cat.label + '</span>' +
(t.pinned ? '<span class="fpin">&#128204; Epingle</span>' : '') +
'<div style="font-size:20px;font-weight:700;color:#222;margin:8px 0 6px">' + t.title + '</div>' +
'<div class="frw" style="margin-bottom:10px">' +
av({displayName:t.author, photoURL:t.photoURL}, 28) +
'<span class="fmu">par <b style="color:#555">' + t.author + '</b> · ' + ts2str(t.createdAt) + '</span>' +
'</div>' +
'<div style="font-size:15px;color:#444;line-height:1.6;margin-bottom:12px">' + t.content + '</div>' +
'<div class="frw">' +
'<button class="flk' + (S.liked[t.id]?" on":"") + '" id="tlk">&#10084; ' + (t.likes||0) + '</button>' +
'<span class="fmu" style="margin-left:8px">&#128172; ' + reps.length + ' reponse(s)</span>' +
'</div></div>' +
'<div style="font-size:14px;font-weight:700;color:#555;margin:12px 0 8px">Reponses (' + reps.length + ')</div>' +
(reps.length === 0 ? '<p style="color:#bbb;font-size:13px;margin-bottom:12px">Aucune reponse. Soyez le premier !</p>' : '') +
reps.map(function(r) {
return '<div class="fc" style="background:#fafafa">' +
'<div class="frw" style="margin-bottom:6px">' +
av({displayName:r.author, photoURL:r.photoURL}, 28) +
'<b style="font-size:13px;color:#555">' + r.author + '</b>' +
'<span class="fmu">· ' + ts2str(r.createdAt) + '</span>' +
'</div><div style="font-size:14px;color:#333;line-height:1.5">' + r.content + '</div></div>';
}).join("") +
'<div style="margin-top:14px">' +
'<textarea class="fta" id="rt" rows="3" placeholder="Ecrire une reponse">' + S.replyText + '</textarea>' +
'<button class="fb fbg" id="bry" style="margin-top:8px">Envoyer</button>' +
'</div>';

document.getElementById("bk")?.addEventListener("click", () => setState({ view:"list", activeTopic:null }));
document.getElementById("tlk")?.addEventListener("click", async function() {
const on = !S.liked[t.id];
const liked = Object.assign({}, S.liked);
liked[t.id] = on;
setState({ liked:liked });
await updateDoc(doc(db,"topics",t.id), { likes:increment(on?1:-1) });
});
document.getElementById("rt")?.addEventListener("input", e => setState({ replyText:e.target.value }, true));
document.getElementById("bry")?.addEventListener("click", doReply);
}

async function doReply() {
const content = document.getElementById("rt")?.value.trim();
if (!content) return;
const u = S.user;
const t = S.activeTopic;
await addDoc(collection(db,"topics",t.id,"replies"), {
content, author:u.displayName||"Anonyme", authorId:u.uid,
photoURL:u.photoURL||null, createdAt:serverTimestamp(),
});
await updateDoc(doc(db,"topics",t.id), { replyCount:increment(1) });
const snap = await getDocs(query(collection(db,"topics",t.id,"replies"), orderBy("createdAt")));
const reps = snap.docs.map(d => Object.assign({}, d.data(), { id:d.id }));
const replies = Object.assign({}, S.replies);
replies[t.id] = reps;
setState({ replies:replies, replyText:"" });
}

// ── PROFILE ─────────────────────────────────────────────────────
async function renderProfile() {
const uid = S.activeAuthor;
const isMe = uid === S.user?.uid;
let uData = { pseudo:S.user?.displayName||"?", joined:"?" };
try { const d = await getDoc(doc(db,"users",uid)); if (d.exists()) uData = d.data(); } catch(e) {}
const userTopics = S.topics.filter(t => t.authorId === uid);

root.innerHTML =
'<button class="fbk" id="bk">&#8592; Retour</button>' +
'<div class="fc" style="text-align:center;padding:24px">' +
'<div style="margin:0 auto 12px;width:64px">' + av({displayName:uData.pseudo, photoURL:isMe?S.user.photoURL:null}, 64) + '</div>' +
'<div style="font-size:20px;font-weight:700;color:#222">' + uData.pseudo + '</div>' +
(isMe ? '<div style="display:inline-block;padding:2px 10px;border-radius:20px;background:#EEEDFE;color:#7F77DD;font-size:12px;font-weight:700;margin-top:6px">&#128100; Vous</div>' : '') +
'<div class="fmu" style="margin-top:6px">Membre depuis ' + (uData.joined||"?") + '</div>' +
'<div class="fg2">' +
'<div class="fst"><b style="display:block;font-size:22px;font-weight:700;color:#7F77DD">' + userTopics.length + '</b><span class="fmu">Sujets</span></div>' +
'<div class="fst"><b style="display:block;font-size:22px;font-weight:700;color:#D4537E">' + userTopics.reduce(function(a,t){return a+(t.likes||0);},0) + '</b><span class="fmu">Likes recus</span></div>' +
'</div>' +
(isMe ? '<button class="fb fbo" id="blo" style="width:100%;margin-top:16px">Se deconnecter</button>' : '') +
(userTopics.length > 0 ?
'<div style="text-align:left;margin-top:18px">' +
'<div style="font-size:14px;font-weight:700;color:#555;margin-bottom:8px">Sujets de ' + uData.pseudo + '</div>' +
userTopics.map(function(t){
const cat = catOf(t.category);
return '<div class="fc tpc" data-id="' + t.id + '" style="cursor:pointer">' +
'<span class="ftag" style="background:' + cat.bg + ';color:' + cat.color + '">' + cat.label + '</span>' +
'<div style="font-size:15px;font-weight:600;color:#222;margin-top:4px">' + t.title + '</div></div>';
}).join("") +
'</div>' : '') +
'</div>';

document.getElementById("bk")?.addEventListener("click", () => setState({ view:"list", activeAuthor:null }));
document.getElementById("blo")?.addEventListener("click", () => signOut(auth));
document.querySelectorAll(".tpc").forEach(el => el.addEventListener("click", function() {
const t = S.topics.find(x => x.id === el.dataset.id);
if (t) setState({ view:"topic", activeTopic:t });
}));
}

// ── Seed demo ───────────────────────────────────────────────────
async function seedDemo() {
const snap = await getDocs(collection(db,"topics"));
if (!snap.empty) return;
const DEMO = [
{ title:"Quels sont vos films preferes de 2025 ?", category:"culture", content:"Je cherche des recommandations pour mes soirees du week-end !", pinned:true, likes:47, replyCount:24 },
{ title:"L'IA va-t-elle remplacer les developpeurs ?", category:"tech", content:"Un debat qui revient souvent. Vos avis ?", pinned:false, likes:102, replyCount:58 },
{ title:"Blague du jour - partagez les votres !", category:"humour", content:"Pourquoi les plongeurs plongent-ils toujours en arriere ?", pinned:false, likes:213, replyCount:89 },
{ title:"Resultats de la Ligue des Champions", category:"sport", content:"Quelle soiree ! Vos reactions ?", pinned:false, likes:88, replyCount:132 },
{ title:"Elections europeennes - analyse et perspectives", category:"actu", content:"Un decryptage des resultats et de leurs implications.", pinned:false, likes:54, replyCount:76 },
];
for (let i = 0; i < DEMO.length; i++) {
await addDoc(collection(db,"topics"), Object.assign({}, DEMO, { author:"Equipe Forum", authorId:"demo", photoURL:null, createdAt:serverTimestamp() }));
}
}

// ── Firebase listener ───────────────────────────────────────────
onAuthStateChanged(auth, async function(user) {
if (user) {
setState({ user:user, screen:"forum" }, true);
await seedDemo();
onSnapshot(query(collection(db,"topics"), orderBy("createdAt","desc")), function(snap) {
const topics = snap.docs.map(d => Object.assign({}, d.data(), { id:d.id }));
setState({ topics:topics });
});
} else {
setState({ user:null, screen:"auth", view:"list", topics:[] });
}
});

//]]>
</script>

</body>
</html>
 
𝑪𝑨𝑷𝑰𝑻𝑨𝑰𝑵𝑬 𝑱𝑨𝑪𝑲
Divin
Donateur 🤲
🥇 Top contributeur
Messages
55 153
Fofocoins
257 984
Don à Foforum
Virement de 100€ 💸
Virement de 100€ 💸
Virement de 50€
Genre
Homme

RECTIFIE AVEC ZERO ERREUR EN ENREGISTREMENT​


<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE html>
<html b:version='2' class='v2' expr:dir='data:blog.languageDirection' xmlns='http://www.w3.org/1999/xhtml' xmlns:b='http://www.google.com/2005/gml/b' xmlns:data='http://www.google.com/2005/gml/data' xmlns:expr='http://www.google.com/2005/gml/expr'>
<head>
<meta charset='UTF-8'/>
<meta content='width=device-width, initial-scale=1' name='viewport'/>
<title><data:blog.pageTitle/></title>
<b:skin><![CDATA[ ]]></b:skin>
</head>
<body>

<div id='site-header'>
<div>
<h1 id='site-title'>&#128172; ForumFrance</h1>
<p id='header-sub'>Chargement&#8230;</p>
</div>
<div id='header-user'></div>
</div>

<div id='forum-root'>
<b:section id='main' name='Main' showaddelement='no'/>
</div>

<div id='site-footer'>ForumFrance &#169; 2025</div>

<script type='module'>
//<![CDATA[

// ── CSS injecte via JS ──────────────────────────────────────────
const st = document.createElement('style');
st.textContent = `
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,sans-serif;background:#f4f4f8;min-height:100vh}
#site-header{background:linear-gradient(135deg,#7F77DD,#D4537E 50%,#BA7517);padding:1.2rem 1.5rem;color:#fff;display:flex;justify-content:space-between;align-items:center;position:sticky;top:0;z-index:100;box-shadow:0 2px 12px rgba(0,0,0,.15)}
#site-title{font-size:22px;font-weight:700}
#header-sub{font-size:12px;opacity:.8;margin-top:2px}
#header-user{display:flex;align-items:center;gap:10px}
#forum-root{max-width:780px;margin:0 auto;padding:1.5rem 1rem 3rem}
#site-footer{text-align:center;color:#bbb;font-size:12px;padding:2rem 0 1rem}
.fc{background:#fff;border-radius:14px;border:1px solid #ececec;padding:16px;margin-bottom:12px;box-shadow:0 1px 4px rgba(0,0,0,.04)}
.fi,.fta,.fse{width:100%;padding:9px 12px;border-radius:10px;border:1.5px solid #e0e0e0;font-size:14px;outline:none;font-family:inherit;background:#fff}
.fta{resize:vertical}
.fb{padding:9px 20px;border-radius:10px;border:none;font-weight:700;font-size:14px;cursor:pointer}
.fbg{background:linear-gradient(90deg,#7F77DD,#D4537E);color:#fff}
.fbo{background:#f0f0f0;color:#666}
.fbsm{padding:5px 14px;font-size:13px}
.fbgl{background:#fff;color:#333;border:1.5px solid #ddd;display:flex;align-items:center;gap:8px;justify-content:center;width:100%;margin-top:10px;padding:9px 20px;border-radius:10px;font-weight:700;font-size:14px;cursor:pointer}
.ft{padding:8px 20px;border-radius:10px;border:none;font-weight:700;font-size:14px;cursor:pointer}
.ft.on{background:#7F77DD;color:#fff}
.ft.off{background:#f0f0f0;color:#888}
.fcb{padding:5px 14px;border-radius:20px;font-size:13px;cursor:pointer;border:1.5px solid #ddd;background:#fff;color:#888}
.fcb.on{font-weight:700}
.ftag{display:inline-block;padding:2px 10px;border-radius:20px;font-size:11px;font-weight:600;margin-right:6px}
.fpin{display:inline-flex;align-items:center;gap:4px;background:#FAEEDA;color:#BA7517;font-size:11px;font-weight:700;padding:2px 8px;border-radius:20px;margin-right:6px}
.fav{border-radius:50%;display:flex;align-items:center;justify-content:center;font-weight:700;flex-shrink:0;overflow:hidden}
.fav img{width:100%;height:100%;object-fit:cover}
.flk{display:flex;align-items:center;gap:4px;padding:4px 12px;border-radius:20px;border:1.5px solid #eee;background:#fafafa;color:#aaa;font-size:13px;font-weight:600;cursor:pointer}
.flk.on{border-color:#D4537E;background:#FBEAF0;color:#D4537E}
.ferr{color:#c0392b;font-size:13px;margin-top:6px}
.fsh{background:#EEEDFE;border-radius:10px;padding:10px 12px;font-size:12px;color:#7F77DD;margin-bottom:12px}
.flbl{font-size:13px;font-weight:600;color:#555;display:block;margin-bottom:5px;margin-top:10px}
.fmu{color:#aaa;font-size:12px}
.frw{display:flex;align-items:center;gap:8px}
.fbk{background:none;border:none;color:#7F77DD;font-weight:700;font-size:14px;cursor:pointer;padding:8px 0}
.ftb{display:flex;gap:8px;align-items:center;margin-bottom:12px}
.fcts{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:14px}
.fst{background:#fafafa;border-radius:10px;padding:12px;text-align:center}
.fg2{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-top:16px}
.faw{max-width:420px;margin:2rem auto}
.tpc:hover{box-shadow:0 4px 16px rgba(127,119,221,.12);border-color:#d0cef8;transition:all .15s}
`;
document.head.appendChild(st);

// ── Firebase ────────────────────────────────────────────────────
import { initializeApp }
from "https://www.gstatic.com/firebasejs/10.12.0/firebase-app.js";
import { getAuth, createUserWithEmailAndPassword, signInWithEmailAndPassword,
signInWithPopup, GoogleAuthProvider, signOut, onAuthStateChanged,
updateProfile }
from "https://www.gstatic.com/firebasejs/10.12.0/firebase-auth.js";
import { getFirestore, collection, addDoc, doc, updateDoc, increment,
query, orderBy, onSnapshot, serverTimestamp, getDoc, setDoc, getDocs }
from "https://www.gstatic.com/firebasejs/10.12.0/firebase-firestore.js";

const firebaseConfig = {
apiKey: "AIzaSyAGAlQFAO2z1K5iQA5h6JHPeX_zaMOkb5Q",
authDomain: "forumfrance-6de67.firebaseapp.com",
projectId: "forumfrance-6de67",
storageBucket: "forumfrance-6de67.firebasestorage.app",
messagingSenderId: "246371695638",
appId: "1:246371695638:web:f48296c3cb65953cf88937"
};

const app = initializeApp(firebaseConfig);
const auth = getAuth(app);
const db = getFirestore(app);
const gProvider = new GoogleAuthProvider();

// ── Categories ──────────────────────────────────────────────────
const CATS = [
{ id:"all", label:"Tout", color:"#7F77DD", bg:"#EEEDFE" },
{ id:"actu", label:"Actualite", color:"#185FA5", bg:"#E6F1FB" },
{ id:"culture", label:"Culture", color:"#D4537E", bg:"#FBEAF0" },
{ id:"tech", label:"Tech", color:"#0F6E56", bg:"#E1F5EE" },
{ id:"humour", label:"Humour", color:"#BA7517", bg:"#FAEEDA" },
{ id:"sport", label:"Sport", color:"#A32D2D", bg:"#FCEBEB" },
];
const catOf = id => CATS.find(c => c.id === id) || CATS[0];
const ts2str = ts => {
if (!ts) return "";
const d = ts.toDate ? ts.toDate() : new Date(ts);
return d.toLocaleDateString("fr-FR", { day:"2-digit", month:"short", hour:"2-digit", minute:"2-digit" });
};

// ── State ───────────────────────────────────────────────────────
let S = {
screen:"loading", authTab:"login", user:null,
topics:[], replies:{}, liked:{},
activeCat:"all", search:"", view:"list",
activeTopic:null, activeAuthor:null, showForm:false,
lEmail:"", lPass:"",
rPseudo:"", rEmail:"", rPass:"", rPass2:"",
nTitle:"", nContent:"", nCat:"actu", replyText:"",
error:"", loading:false,
};

const root = document.getElementById("forum-root");
const hSub = document.getElementById("header-sub");
const hUser = document.getElementById("header-user");

function setState(patch, skip) {
S = Object.assign({}, S, patch);
if (!skip) render();
}

// ── Avatar ──────────────────────────────────────────────────────
function av(user, size) {
size = size || 36;
if (user && user.photoURL)
return '<div class="fav" style="width:' + size + 'px;height:' + size + 'px"><img src="' + user.photoURL + '" referrerpolicy="no-referrer"></div>';
const colors = ["#7F77DD","#D4537E","#0F6E56","#BA7517","#185FA5","#A32D2D"];
const bgs = ["#EEEDFE","#FBEAF0","#E1F5EE","#FAEEDA","#E6F1FB","#FCEBEB"];
const name = (user && (user.displayName || user.pseudo)) || "?";
const idx = name.charCodeAt(0) % colors.length;
return '<div class="fav" style="width:' + size + 'px;height:' + size + 'px;background:' + bgs[idx] + ';color:' + colors[idx] + ';font-size:' + (size > 40 ? 20 : 14) + 'px">' + name[0].toUpperCase() + '</div>';
}

// ── Render ──────────────────────────────────────────────────────
function render() {
updateHeader();
if (S.screen === "loading") { root.innerHTML = '<div style="text-align:center;padding:60px 0;color:#aaa"><div style="font-size:40px">&#128172;</div><p style="margin-top:12px">Chargement&#8230;</p></div>'; return; }
if (S.screen === "auth") { renderAuth(); return; }
if (S.view === "list") { renderList(); return; }
if (S.view === "topic") { renderTopic(); return; }
if (S.view === "profile") { renderProfile(); return; }
}

function updateHeader() {
if (!S.user) { hSub.textContent = "Connectez-vous pour participer"; hUser.innerHTML = ""; return; }
hSub.textContent = S.topics.length + " sujets · Bonjour, " + (S.user.displayName || "?") + " !";
hUser.innerHTML = '<div id="hav" style="cursor:pointer">' + av(S.user, 38) + '</div><button class="fb fbo fbsm" id="hlo" style="font-size:12px">Deconnexion</button>';
document.getElementById("hav")?.addEventListener("click", () => setState({ view:"profile", activeAuthor:S.user.uid }));
document.getElementById("hlo")?.addEventListener("click", () => signOut(auth));
}

// ── Google icon ─────────────────────────────────────────────────
function gico() {
return '<svg width="18" height="18" viewBox="0 0 48 48"><path fill="#EA4335" d="M24 9.5c3.54 0 6.71 1.22 9.21 3.6l6.85-6.85C35.9 2.38 30.47 0 24 0 14.62 0 6.51 5.38 2.56 13.22l7.98 6.19C12.43 13.72 17.74 9.5 24 9.5z"/><path fill="#4285F4" d="M46.98 24.55c0-1.57-.15-3.09-.38-4.55H24v9.02h12.94c-.58 2.96-2.26 5.48-4.78 7.18l7.73 6c4.51-4.18 7.09-10.36 7.09-17.65z"/><path fill="#FBBC05" d="M10.53 28.59c-.48-1.45-.76-2.99-.76-4.59s.27-3.14.76-4.59l-7.98-6.19C.92 16.46 0 20.12 0 24c0 3.88.92 7.54 2.56 10.78l7.97-6.19z"/><path fill="#34A853" d="M24 48c6.48 0 11.93-2.13 15.89-5.81l-7.73-6c-2.15 1.45-4.92 2.3-8.16 2.3-6.26 0-11.57-4.22-13.47-9.91l-7.98 6.19C6.51 42.62 14.62 48 24 48z"/></svg>';
}

// ── AUTH ────────────────────────────────────────────────────────
function renderAuth() {
const login = S.authTab === "login";
root.innerHTML =
'<div class="faw"><div class="fc">' +
'<div class="frw" style="margin-bottom:20px;gap:8px">' +
'<button class="ft ' + (login?"on":"off") + '" id="tl">Connexion</button>' +
'<button class="ft ' + (!login?"on":"off") + '" id="tr">Inscription</button>' +
'</div>' +
(login ?
'<label class="flbl">Email</label>' +
'<input class="fi" id="le" type="email" placeholder="vous@exemple.com" value="' + S.lEmail + '">' +
'<label class="flbl">Mot de passe</label>' +
'<input class="fi" id="lp" type="password" placeholder="&#8226;&#8226;&#8226;&#8226;&#8226;&#8226;" value="' + S.lPass + '">' +
(S.error ? '<div class="ferr">&#9888; ' + S.error + '</div>' : '') +
'<button class="fb fbg" id="bli" style="width:100%;margin-top:14px">' + (S.loading?"Connexion&#8230;":"Se connecter") + '</button>' +
'<button class="fbgl" id="bgl">' + gico() + ' Continuer avec Google</button>' +
'<p style="text-align:center;margin-top:12px;font-size:13px;color:#aaa">Pas de compte ? <a href="#" id="gor" style="color:#7F77DD;font-weight:600">S\'inscrire</a></p>'
:
'<label class="flbl">Pseudo (3-20 car.)</label>' +
'<input class="fi" id="rp" placeholder="MonPseudo" value="' + S.rPseudo + '">' +
'<label class="flbl">Email</label>' +
'<input class="fi" id="re" type="email" placeholder="vous@exemple.com" value="' + S.rEmail + '">' +
'<label class="flbl">Mot de passe (6 car. min.)</label>' +
'<input class="fi" id="rpa" type="password" placeholder="&#8226;&#8226;&#8226;&#8226;&#8226;&#8226;" value="' + S.rPass + '">' +
'<label class="flbl">Confirmer</label>' +
'<input class="fi" id="rp2" type="password" placeholder="&#8226;&#8226;&#8226;&#8226;&#8226;&#8226;" value="' + S.rPass2 + '">' +
'<div class="fsh" style="margin-top:12px">&#128737; Protection anti-abus activee.</div>' +
(S.error ? '<div class="ferr">&#9888; ' + S.error + '</div>' : '') +
'<button class="fb fbg" id="brg" style="width:100%;margin-top:10px">' + (S.loading?"Inscription&#8230;":"Creer mon compte") + '</button>' +
'<button class="fbgl" id="bgr">' + gico() + ' Continuer avec Google</button>' +
'<p style="text-align:center;margin-top:12px;font-size:13px;color:#aaa">Deja membre ? <a href="#" id="gol" style="color:#7F77DD;font-weight:600">Se connecter</a></p>'
) +
'</div></div>';

document.getElementById("tl")?.addEventListener("click", () => setState({ authTab:"login", error:"" }));
document.getElementById("tr")?.addEventListener("click", () => setState({ authTab:"register", error:"" }));
document.getElementById("gol")?.addEventListener("click", e => { e.preventDefault(); setState({ authTab:"login", error:"" }); });
document.getElementById("gor")?.addEventListener("click", e => { e.preventDefault(); setState({ authTab:"register", error:"" }); });
document.getElementById("bli")?.addEventListener("click", doLogin);
document.getElementById("brg")?.addEventListener("click", doRegister);
document.getElementById("bgl")?.addEventListener("click", doGoogle);
document.getElementById("bgr")?.addEventListener("click", doGoogle);

const bind = (id, key) => document.getElementById(id)?.addEventListener("input", e => setState({ [key]: e.target.value }, true));
bind("le","lEmail"); bind("lp","lPass");
bind("rp","rPseudo"); bind("re","rEmail"); bind("rpa","rPass"); bind("rp2","rPass2");
}

// ── Auth actions ────────────────────────────────────────────────
function valPseudo(p) {
if (!p || p.length < 3 || p.length > 20) return "Pseudo : 3 a 20 caracteres.";
if (!/^[a-zA-Z0-9_\-]+$/.test(p)) return "Caracteres invalides.";
if (["admin","moderateur","root","system"].some(w => p.toLowerCase().includes(w))) return "Pseudo reserve.";
return null;
}

async function doLogin() {
const email = document.getElementById("le")?.value.trim();
const pass = document.getElementById("lp")?.value;
if (!email || !pass) { setState({ error:"Remplissez tous les champs." }); return; }
setState({ loading:true, error:"" });
try {
await signInWithEmailAndPassword(auth, email, pass);
} catch(e) {
const msg = e.code === "auth/invalid-credential" ? "Email ou mot de passe incorrect."
: e.code === "auth/too-many-requests" ? "Trop de tentatives. Reessayez plus tard."
: e.message;
setState({ loading:false, error:msg });
}
}

async function doRegister() {
const pseudo = document.getElementById("rp")?.value.trim();
const email = document.getElementById("re")?.value.trim();
const pass = document.getElementById("rpa")?.value;
const pass2 = document.getElementById("rp2")?.value;
const err = valPseudo(pseudo);
if (err) { setState({ error:err }); return; }
if (!email) { setState({ error:"Email requis." }); return; }
if (pass.length < 6){ setState({ error:"Mot de passe : 6 caracteres minimum." }); return; }
if (pass !== pass2) { setState({ error:"Les mots de passe ne correspondent pas." }); return; }
setState({ loading:true, error:"" });
try {
const cred = await createUserWithEmailAndPassword(auth, email, pass);
await updateProfile(cred.user, { displayName: pseudo });
await cred.user.getIdToken(true);
await setDoc(doc(db, "users", cred.user.uid), {
pseudo, email,
joined: new Date().toLocaleDateString("fr-FR", { month:"long", year:"numeric" }),
posts:0, role:"member",
});
} catch(e) {
const msg = e.code === "auth/email-already-in-use" ? "Cet email est deja utilise." : e.message;
setState({ loading:false, error:msg });
}
}

async function doGoogle() {
try {
const result = await signInWithPopup(auth, gProvider);
const u = result.user;
const ref = doc(db, "users", u.uid);
const snap = await getDoc(ref);
if (!snap.exists()) {
await setDoc(ref, {
pseudo: u.displayName || "Utilisateur", email: u.email,
joined: new Date().toLocaleDateString("fr-FR", { month:"long", year:"numeric" }),
posts:0, role:"member",
});
}
} catch(e) {
setState({ error:"Connexion Google annulee ou bloquee." });
}
}

// ── LIST ────────────────────────────────────────────────────────
function renderList() {
const filtered = S.topics
.filter(t => (S.activeCat === "all" || t.category === S.activeCat) && (!S.search || t.title.toLowerCase().includes(S.search.toLowerCase())))
.sort((a,b) => (b.pinned?1:0) - (a.pinned?1:0));

let thtml = filtered.length === 0
? '<p style="text-align:center;color:#bbb;padding:2rem 0">Aucun sujet trouve.</p>'
: filtered.map(function(t) {
const cat = catOf(t.category);
return '<div class="fc tpc" data-id="' + t.id + '" style="cursor:pointer">' +
'<div class="frw" style="align-items:flex-start;gap:10px">' +
'<div class="alnk" data-uid="' + t.authorId + '" style="cursor:pointer">' + av({displayName:t.author, photoURL:t.photoURL}) + '</div>' +
'<div style="flex:1;min-width:0">' +
(t.pinned ? '<span class="fpin">&#128204;</span>' : '') +
'<span class="ftag" style="background:' + cat.bg + ';color:' + cat.color + '">' + cat.label + '</span>' +
'<div style="font-size:15px;font-weight:600;color:#222;margin:4px 0 2px;line-height:1.3">' + t.title + '</div>' +
'<div class="fmu">par <b style="color:#888;cursor:pointer">' + t.author + '</b> · ' + ts2str(t.createdAt) + '</div>' +
'</div></div>' +
'<div class="frw" style="margin-top:10px">' +
'<button class="flk' + (S.liked[t.id]?" on":"") + ' lkb" data-id="' + t.id + '">&#10084; ' + (t.likes||0) + '</button>' +
'<span class="fmu" style="margin-left:8px">&#128172; ' + (t.replyCount||0) + '</span>' +
'</div></div>';
}).join("");

let form = "";
if (S.showForm) {
form = '<div class="fc" style="border:1.5px solid #e0e0e0;margin-bottom:16px">' +
'<div style="font-size:16px;font-weight:700;color:#333;margin-bottom:10px">Creer un sujet</div>' +
'<label class="flbl">Titre</label><input class="fi" id="nt" placeholder="Titre du sujet" value="' + S.nTitle + '">' +
'<label class="flbl">Categorie</label>' +
'<select class="fse" id="nc" style="margin-top:5px">' +
CATS.filter(c => c.id !== "all").map(c => '<option value="' + c.id + '"' + (c.id === S.nCat?" selected":"") + '>' + c.label + '</option>').join("") +
'</select>' +
'<label class="flbl">Contenu</label><textarea class="fta" id="nco" rows="4" placeholder="Decrivez votre sujet">' + S.nContent + '</textarea>' +
'<div class="frw" style="margin-top:10px;gap:8px">' +
'<button class="fb fbg" id="bpt">Publier</button>' +
'<button class="fb fbo" id="bcl">Annuler</button>' +
'</div></div>';
}

root.innerHTML =
'<div class="ftb">' +
'<input class="fi" id="sr" placeholder="&#128269; Rechercher" value="' + S.search + '" style="flex:1">' +
'<button class="fb fbg" id="bnt">+ Nouveau sujet</button>' +
'</div>' +
form +
'<div class="fcts">' +
CATS.map(function(c) {
const on = S.activeCat === c.id;
return '<button class="fcb' + (on?" on":"") + '" data-cat="' + c.id + '" style="' + (on?"border-color:"+c.color+";background:"+c.bg+";color:"+c.color+";font-weight:700":"") + '">' + c.label + '</button>';
}).join("") +
'</div>' + thtml;

document.getElementById("sr")?.addEventListener("input", e => setState({ search:e.target.value }));
document.getElementById("bnt")?.addEventListener("click", () => setState({ showForm:!S.showForm }));
document.querySelectorAll(".fcb").forEach(b => b.addEventListener("click", () => setState({ activeCat:b.dataset.cat })));
document.querySelectorAll(".tpc").forEach(el => el.addEventListener("click", function() {
const t = S.topics.find(x => x.id === el.dataset.id);
if (t) setState({ view:"topic", activeTopic:t });
}));
document.querySelectorAll(".alnk").forEach(el => el.addEventListener("click", function(e) {
e.stopPropagation();
setState({ view:"profile", activeAuthor:el.dataset.uid });
}));
document.querySelectorAll(".lkb").forEach(btn => btn.addEventListener("click", async function(e) {
e.stopPropagation();
const id = btn.dataset.id;
const on = !S.liked[id];
const liked = Object.assign({}, S.liked);
liked[id] = on;
setState({ liked:liked });
await updateDoc(doc(db,"topics",id), { likes:increment(on?1:-1) });
}));
if (S.showForm) {
document.getElementById("nt")?.addEventListener("input", e => setState({ nTitle:e.target.value }, true));
document.getElementById("nco")?.addEventListener("input", e => setState({ nContent:e.target.value }, true));
document.getElementById("nc")?.addEventListener("change", e => setState({ nCat:e.target.value }, true));
document.getElementById("bcl")?.addEventListener("click", () => setState({ showForm:false }));
document.getElementById("bpt")?.addEventListener("click", doPost);
}
}

async function doPost() {
const title = document.getElementById("nt")?.value.trim();
const content = document.getElementById("nco")?.value.trim();
const cat = document.getElementById("nc")?.value;
if (!title || !content) return;
const u = S.user;
await addDoc(collection(db,"topics"), {
title, content, category:cat,
author:u.displayName||"Anonyme", authorId:u.uid,
photoURL:u.photoURL||null,
likes:0, replyCount:0, pinned:false,
createdAt:serverTimestamp(),
});
setState({ showForm:false, nTitle:"", nContent:"", nCat:"actu" });
}

// ── TOPIC ───────────────────────────────────────────────────────
function renderTopic() {
const t = S.activeTopic;
if (!t) { setState({ view:"list" }); return; }
const cat = catOf(t.category);
const reps = S.replies[t.id] || [];

root.innerHTML =
'<button class="fbk" id="bk">&#8592; Retour</button>' +
'<div class="fc">' +
'<span class="ftag" style="background:' + cat.bg + ';color:' + cat.color + '">' + cat.label + '</span>' +
(t.pinned ? '<span class="fpin">&#128204; Epingle</span>' : '') +
'<div style="font-size:20px;font-weight:700;color:#222;margin:8px 0 6px">' + t.title + '</div>' +
'<div class="frw" style="margin-bottom:10px">' +
av({displayName:t.author, photoURL:t.photoURL}, 28) +
'<span class="fmu">par <b style="color:#555">' + t.author + '</b> · ' + ts2str(t.createdAt) + '</span>' +
'</div>' +
'<div style="font-size:15px;color:#444;line-height:1.6;margin-bottom:12px">' + t.content + '</div>' +
'<div class="frw">' +
'<button class="flk' + (S.liked[t.id]?" on":"") + '" id="tlk">&#10084; ' + (t.likes||0) + '</button>' +
'<span class="fmu" style="margin-left:8px">&#128172; ' + reps.length + ' reponse(s)</span>' +
'</div></div>' +
'<div style="font-size:14px;font-weight:700;color:#555;margin:12px 0 8px">Reponses (' + reps.length + ')</div>' +
(reps.length === 0 ? '<p style="color:#bbb;font-size:13px;margin-bottom:12px">Aucune reponse. Soyez le premier !</p>' : '') +
reps.map(function(r) {
return '<div class="fc" style="background:#fafafa">' +
'<div class="frw" style="margin-bottom:6px">' +
av({displayName:r.author, photoURL:r.photoURL}, 28) +
'<b style="font-size:13px;color:#555">' + r.author + '</b>' +
'<span class="fmu">· ' + ts2str(r.createdAt) + '</span>' +
'</div><div style="font-size:14px;color:#333;line-height:1.5">' + r.content + '</div></div>';
}).join("") +
'<div style="margin-top:14px">' +
'<textarea class="fta" id="rt" rows="3" placeholder="Ecrire une reponse">' + S.replyText + '</textarea>' +
'<button class="fb fbg" id="bry" style="margin-top:8px">Envoyer</button>' +
'</div>';

document.getElementById("bk")?.addEventListener("click", () => setState({ view:"list", activeTopic:null }));
document.getElementById("tlk")?.addEventListener("click", async function() {
const on = !S.liked[t.id];
const liked = Object.assign({}, S.liked);
liked[t.id] = on;
setState({ liked:liked });
await updateDoc(doc(db,"topics",t.id), { likes:increment(on?1:-1) });
});
document.getElementById("rt")?.addEventListener("input", e => setState({ replyText:e.target.value }, true));
document.getElementById("bry")?.addEventListener("click", doReply);
}

async function doReply() {
const content = document.getElementById("rt")?.value.trim();
if (!content) return;
const u = S.user;
const t = S.activeTopic;
await addDoc(collection(db,"topics",t.id,"replies"), {
content, author:u.displayName||"Anonyme", authorId:u.uid,
photoURL:u.photoURL||null, createdAt:serverTimestamp(),
});
await updateDoc(doc(db,"topics",t.id), { replyCount:increment(1) });
const snap = await getDocs(query(collection(db,"topics",t.id,"replies"), orderBy("createdAt")));
const reps = snap.docs.map(d => Object.assign({}, d.data(), { id:d.id }));
const replies = Object.assign({}, S.replies);
replies[t.id] = reps;
setState({ replies:replies, replyText:"" });
}

// ── PROFILE ─────────────────────────────────────────────────────
async function renderProfile() {
const uid = S.activeAuthor;
const isMe = uid === S.user?.uid;
let uData = { pseudo:S.user?.displayName||"?", joined:"?" };
try { const d = await getDoc(doc(db,"users",uid)); if (d.exists()) uData = d.data(); } catch(e) {}
const userTopics = S.topics.filter(t => t.authorId === uid);

root.innerHTML =
'<button class="fbk" id="bk">&#8592; Retour</button>' +
'<div class="fc" style="text-align:center;padding:24px">' +
'<div style="margin:0 auto 12px;width:64px">' + av({displayName:uData.pseudo, photoURL:isMe?S.user.photoURL:null}, 64) + '</div>' +
'<div style="font-size:20px;font-weight:700;color:#222">' + uData.pseudo + '</div>' +
(isMe ? '<div style="display:inline-block;padding:2px 10px;border-radius:20px;background:#EEEDFE;color:#7F77DD;font-size:12px;font-weight:700;margin-top:6px">&#128100; Vous</div>' : '') +
'<div class="fmu" style="margin-top:6px">Membre depuis ' + (uData.joined||"?") + '</div>' +
'<div class="fg2">' +
'<div class="fst"><b style="display:block;font-size:22px;font-weight:700;color:#7F77DD">' + userTopics.length + '</b><span class="fmu">Sujets</span></div>' +
'<div class="fst"><b style="display:block;font-size:22px;font-weight:700;color:#D4537E">' + userTopics.reduce(function(a,t){return a+(t.likes||0);},0) + '</b><span class="fmu">Likes recus</span></div>' +
'</div>' +
(isMe ? '<button class="fb fbo" id="blo" style="width:100%;margin-top:16px">Se deconnecter</button>' : '') +
(userTopics.length > 0 ?
'<div style="text-align:left;margin-top:18px">' +
'<div style="font-size:14px;font-weight:700;color:#555;margin-bottom:8px">Sujets de ' + uData.pseudo + '</div>' +
userTopics.map(function(t){
const cat = catOf(t.category);
return '<div class="fc tpc" data-id="' + t.id + '" style="cursor:pointer">' +
'<span class="ftag" style="background:' + cat.bg + ';color:' + cat.color + '">' + cat.label + '</span>' +
'<div style="font-size:15px;font-weight:600;color:#222;margin-top:4px">' + t.title + '</div></div>';
}).join("") +
'</div>' : '') +
'</div>';

document.getElementById("bk")?.addEventListener("click", () => setState({ view:"list", activeAuthor:null }));
document.getElementById("blo")?.addEventListener("click", () => signOut(auth));
document.querySelectorAll(".tpc").forEach(el => el.addEventListener("click", function() {
const t = S.topics.find(x => x.id === el.dataset.id);
if (t) setState({ view:"topic", activeTopic:t });
}));
}

// ── Seed demo ───────────────────────────────────────────────────
async function seedDemo() {
const snap = await getDocs(collection(db,"topics"));
if (!snap.empty) return;
const DEMO = [
{ title:"Quels sont vos films preferes de 2025 ?", category:"culture", content:"Je cherche des recommandations pour mes soirees du week-end !", pinned:true, likes:47, replyCount:24 },
{ title:"L'IA va-t-elle remplacer les developpeurs ?", category:"tech", content:"Un debat qui revient souvent. Vos avis ?", pinned:false, likes:102, replyCount:58 },
{ title:"Blague du jour - partagez les votres !", category:"humour", content:"Pourquoi les plongeurs plongent-ils toujours en arriere ?", pinned:false, likes:213, replyCount:89 },
{ title:"Resultats de la Ligue des Champions", category:"sport", content:"Quelle soiree ! Vos reactions ?", pinned:false, likes:88, replyCount:132 },
{ title:"Elections europeennes - analyse et perspectives", category:"actu", content:"Un decryptage des resultats et de leurs implications.", pinned:false, likes:54, replyCount:76 },
];
for (let i = 0; i < DEMO.length; i++) {
await addDoc(collection(db,"topics"), Object.assign({}, DEMO, { author:"Equipe Forum", authorId:"demo", photoURL:null, createdAt:serverTimestamp() }));
}
}

// ── Firebase listener ───────────────────────────────────────────
onAuthStateChanged(auth, async function(user) {
if (user) {
setState({ user:user, screen:"forum" }, true);
await seedDemo();
onSnapshot(query(collection(db,"topics"), orderBy("createdAt","desc")), function(snap) {
const topics = snap.docs.map(d => Object.assign({}, d.data(), { id:d.id }));
setState({ topics:topics });
});
} else {
setState({ user:null, screen:"auth", view:"list", topics:[] });
}
});

//]]>
</script>

</body>
</html>
 
𝑪𝑨𝑷𝑰𝑻𝑨𝑰𝑵𝑬 𝑱𝑨𝑪𝑲
Divin
Donateur 🤲
🥇 Top contributeur
Messages
55 153
Fofocoins
257 984
Don à Foforum
Virement de 100€ 💸
Virement de 100€ 💸
Virement de 50€
Genre
Homme

Le forum est prêt​

MAIS c'est juste un amusement pour moi
J'ai passé 6 heures à le créer et ne compte pas l'améliorer encore moins inviter des futurs membres

Je tente de créer un forum de discussions (à titre éducatif et pas en concurrence)
 

Sujets populaires

Derniers messages

🚫 Alerte AdBlock !

Vous avez activé le mode Ninja, et il cache toutes les pubs ! 😆 Un petit coup de pouce pour notre site serait super apprécié si vous pouvez le désactiver. 🙏

🦸‍♂️ J'ai Désactivé AdBlock !