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

𝑪𝑨𝑷𝑰𝑻𝑨𝑰𝑵𝑬 𝑱𝑨𝑪𝑲
Divin
Donateur 🤲
🥇 Top contributeur
Messages
54 697
Fofocoins
254 280
Don à Foforum
Virement de 100€ 💸
Virement de 100€ 💸
Virement de 50€
Genre
Homme
import { useState } from "react";

const CATEGORIES = [
{ id: "all", label: "Tout", color: "#7F77DD", bg: "#EEEDFE" },
{ id: "actu", label: "Actualité", 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 INITIAL_TOPICS = [
{ id: 1, title: "Quels sont vos films préférés de 2025 ?", category: "culture", author: "Cinéphile42", avatar: "C", avatarColor: "#D4537E", avatarBg: "#FBEAF0", replies: 24, likes: 47, time: "il y a 2h", content: "Je cherche des recommandations pour mes soirées du week-end !", pinned: true },
{ id: 2, title: "L'IA va-t-elle remplacer les développeurs ?", category: "tech", author: "DevSenior", avatar: "D", avatarColor: "#0F6E56", avatarBg: "#E1F5EE", replies: 58, likes: 102, time: "il y a 4h", content: "Un débat qui revient souvent… vos avis ?", pinned: false },
{ id: 3, title: "Blague du jour — partagez les vôtres !", category: "humour", author: "Rigolo77", avatar: "R", avatarColor: "#BA7517", avatarBg: "#FAEEDA", replies: 89, likes: 213, time: "il y a 6h", content: "Pourquoi les plongeurs plongent-ils toujours en arrière ? Parce que sinon ils tomberaient dans le bateau.", pinned: false },
{ id: 4, title: "Résultats de la Ligue des Champions", category: "sport", author: "FootFan", avatar: "F", avatarColor: "#A32D2D", avatarBg: "#FCEBEB", replies: 132, likes: 88, time: "il y a 1h", content: "Quelle soirée ! Vos réactions ?", pinned: false },
{ id: 5, title: "Élections européennes — analyse et perspectives", category: "actu", author: "Politologue", avatar: "P", avatarColor: "#185FA5", avatarBg: "#E6F1FB", replies: 76, likes: 54, time: "il y a 3h", content: "Un décryptage des résultats et de leurs implications.", pinned: false },
{ id: 6, title: "Vos jeux vidéo du moment ?", category: "tech", author: "GamerPro", avatar: "G", avatarColor: "#0F6E56", avatarBg: "#E1F5EE", replies: 41, likes: 67, time: "il y a 5h", content: "Partagez vos découvertes récentes !", pinned: false },
];

const COLORS = ["#7F77DD","#D4537E","#1D9E75","#BA7517","#378ADD","#D85A30"];
const BGCOLORS = ["#EEEDFE","#FBEAF0","#E1F5EE","#FAEEDA","#E6F1FB","#FAECE7"];

let nextId = 7;

export default function Forum() {
const [topics, setTopics] = useState(INITIAL_TOPICS);
const [activeCat, setActiveCat] = useState("all");
const [view, setView] = useState("list"); // list | topic | profile | newTopic
const [activeTopic, setActiveTopic] = useState(null);
const [activeUser, setActiveUser] = useState(null);
const [liked, setLiked] = useState({});
const [replyText, setReplyText] = useState("");
const [replies, setReplies] = useState({});
const [showForm, setShowForm] = useState(false);
const [newTitle, setNewTitle] = useState("");
const [newContent, setNewContent] = useState("");
const [newCat, setNewCat] = useState("actu");
const [search, setSearch] = useState("");
const [currentUser] = useState({ name: "Moi", avatar: "M", avatarColor: "#7F77DD", avatarBg: "#EEEDFE", posts: 3, joined: "Mai 2026" });

const filtered = topics.filter(t =>
(activeCat === "all" || t.category === activeCat) &&
(search === "" || t.title.toLowerCase().includes(search.toLowerCase()))
);

const getCatLabel = (id) => CATEGORIES.find(c => c.id === id)?.label || id;
const getCatColor = (id) => CATEGORIES.find(c => c.id === id)?.color || "#888";
const getCatBg = (id) => CATEGORIES.find(c => c.id === id)?.bg || "#eee";

const handleLike = (id, e) => {
e?.stopPropagation();
setLiked(prev => ({ ...prev, [id]: !prev[id] }));
setTopics(prev => prev.map(t => t.id === id ? { ...t, likes: t.likes + (liked[id] ? -1 : 1) } : t));
};

const handleReply = (topicId) => {
if (!replyText.trim()) return;
const newReply = { id: Date.now(), author: currentUser.name, avatar: currentUser.avatar, avatarColor: currentUser.avatarColor, avatarBg: currentUser.avatarBg, content: replyText, time: "à l'instant", likes: 0 };
setReplies(prev => ({ ...prev, [topicId]: [...(prev[topicId] || []), newReply] }));
setTopics(prev => prev.map(t => t.id === topicId ? { ...t, replies: t.replies + 1 } : t));
setReplyText("");
};

const handleNewTopic = () => {
if (!newTitle.trim() || !newContent.trim()) return;
const idx = nextId % COLORS.length;
const t = { id: nextId++, title: newTitle, category: newCat, author: currentUser.name, avatar: currentUser.avatar, avatarColor: currentUser.avatarColor, avatarBg: currentUser.avatarBg, replies: 0, likes: 0, time: "à l'instant", content: newContent, pinned: false };
setTopics(prev => [t, ...prev]);
setNewTitle(""); setNewContent(""); setShowForm(false); setView("list");
};

const openTopic = (t) => { setActiveTopic(t); setView("topic"); };
const openProfile = (e, author) => { e.stopPropagation(); setActiveUser(author); setView("profile"); };

const styles = {
wrap: { fontFamily: "system-ui, sans-serif", maxWidth: 720, margin: "0 auto", padding: "0 0 2rem" },
header: { background: "linear-gradient(135deg, #7F77DD 0%, #D4537E 50%, #BA7517 100%)", borderRadius: 16, padding: "1.5rem 1.5rem 1.2rem", marginBottom: 16, color: "#fff" },
headerTitle: { fontSize: 26, fontWeight: 700, margin: 0, letterSpacing: -0.5 },
headerSub: { fontSize: 13, opacity: 0.85, marginTop: 4 },
topBar: { display: "flex", gap: 8, marginBottom: 12, alignItems: "center" },
searchInput: { flex: 1, padding: "8px 12px", borderRadius: 10, border: "1.5px solid #e0e0e0", fontSize: 14, outline: "none", background: "#fafafa" },
btn: { padding: "8px 16px", borderRadius: 10, border: "none", background: "linear-gradient(90deg,#7F77DD,#D4537E)", color: "#fff", fontWeight: 600, fontSize: 14, cursor: "pointer", whiteSpace: "nowrap" },
cats: { display: "flex", gap: 8, flexWrap: "wrap", marginBottom: 14 },
catBtn: (active, c) => ({ padding: "5px 14px", borderRadius: 20, border: `1.5px solid ${active ? c.color : "#ddd"}`, background: active ? c.bg : "#fff", color: active ? c.color : "#888", fontWeight: active ? 700 : 400, fontSize: 13, cursor: "pointer", transition: "all .15s" }),
card: { background: "#fff", borderRadius: 14, border: "1px solid #f0f0f0", padding: "14px 16px", marginBottom: 10, cursor: "pointer", transition: "box-shadow .15s", boxShadow: "0 1px 4px rgba(0,0,0,0.04)" },
cardTop: { display: "flex", alignItems: "flex-start", gap: 10 },
avatar: (color, bg, size=36) => ({ width: size, height: size, borderRadius: "50%", background: bg, color, fontWeight: 700, fontSize: size === 36 ? 15 : 13, display: "flex", alignItems: "center", justifyContent: "center", flexShrink: 0 }),
catTag: (id) => ({ display: "inline-block", padding: "2px 10px", borderRadius: 20, background: getCatBg(id), color: getCatColor(id), fontSize: 11, fontWeight: 600, marginRight: 6 }),
meta: { fontSize: 12, color: "#aaa", marginTop: 3 },
cardTitle: { fontSize: 15, fontWeight: 600, color: "#222", margin: "4px 0 2px", lineHeight: 1.3 },
cardFoot: { display: "flex", alignItems: "center", gap: 14, marginTop: 10 },
likeBtn: (liked) => ({ display: "flex", alignItems: "center", gap: 4, padding: "4px 10px", borderRadius: 20, border: `1.5px solid ${liked ? "#D4537E" : "#eee"}`, background: liked ? "#FBEAF0" : "#fafafa", color: liked ? "#D4537E" : "#aaa", fontSize: 13, fontWeight: 600, cursor: "pointer" }),
replyBadge: { display: "flex", alignItems: "center", gap: 4, fontSize: 13, color: "#aaa" },
pinBadge: { display: "inline-flex", alignItems: "center", gap: 4, background: "#FAEEDA", color: "#BA7517", fontSize: 11, fontWeight: 700, padding: "2px 8px", borderRadius: 20, marginRight: 6 },
backBtn: { background: "none", border: "none", color: "#7F77DD", fontWeight: 700, fontSize: 14, cursor: "pointer", padding: "8px 0", display: "flex", alignItems: "center", gap: 4 },
topicHeader: { background: "#fff", borderRadius: 14, border: "1px solid #f0f0f0", padding: "16px", marginBottom: 12 },
topicTitle: { fontSize: 20, fontWeight: 700, color: "#222", margin: "8px 0 6px" },
topicBody: { fontSize: 15, color: "#444", lineHeight: 1.6, marginBottom: 12 },
replyCard: { background: "#fafafa", borderRadius: 12, border: "1px solid #f0f0f0", padding: "12px 14px", marginBottom: 8 },
replyInput: { width: "100%", padding: "10px 12px", borderRadius: 10, border: "1.5px solid #e0e0e0", fontSize: 14, outline: "none", resize: "none", boxSizing: "border-box", fontFamily: "inherit" },
sendBtn: { padding: "8px 18px", borderRadius: 10, border: "none", background: "linear-gradient(90deg,#7F77DD,#D4537E)", color: "#fff", fontWeight: 700, fontSize: 14, cursor: "pointer", marginTop: 8 },
formCard: { background: "#fff", borderRadius: 14, border: "1.5px solid #e0e0e0", padding: "18px", marginBottom: 16 },
formLabel: { fontSize: 13, fontWeight: 600, color: "#555", display: "block", marginBottom: 5 },
formInput: { width: "100%", padding: "9px 12px", borderRadius: 10, border: "1.5px solid #e0e0e0", fontSize: 14, outline: "none", boxSizing: "border-box", marginBottom: 12 },
select: { width: "100%", padding: "9px 12px", borderRadius: 10, border: "1.5px solid #e0e0e0", fontSize: 14, outline: "none", marginBottom: 12, background: "#fff" },
profileCard: { background: "#fff", borderRadius: 14, border: "1px solid #f0f0f0", padding: "24px", textAlign: "center" },
statGrid: { display: "grid", gridTemplateColumns: "1fr 1fr", gap: 10, marginTop: 16 },
statBox: { background: "#fafafa", borderRadius: 10, padding: "12px", textAlign: "center" },
statNum: { fontSize: 22, fontWeight: 700, color: "#7F77DD" },
statLbl: { fontSize: 12, color: "#aaa", marginTop: 2 },
};

if (view === "profile") {
const isMe = activeUser === currentUser.name;
const userTopics = topics.filter(t => t.author === activeUser);
const u = userTopics[0] || currentUser;
return (
<div style={styles.wrap}>
<button style={styles.backBtn} onClick={() => setView("list")}>← Retour</button>
<div style={styles.profileCard}>
<div style={{...styles.avatar(isMe ? currentUser.avatarColor : getCatColor("all"), isMe ? currentUser.avatarBg : "#EEEDFE", 64), margin: "0 auto 12px"}}>{isMe ? currentUser.avatar : activeUser[0].toUpperCase()}</div>
<div style={{fontSize: 20, fontWeight: 700, color: "#222"}}>{activeUser}</div>
<div style={{fontSize: 13, color: "#aaa", marginTop: 3}}>Membre depuis {currentUser.joined}</div>
<div style={styles.statGrid}>
<div style={styles.statBox}><div style={styles.statNum}>{userTopics.length}</div><div style={styles.statLbl}>Sujets créés</div></div>
<div style={styles.statBox}><div style={styles.statNum}>{userTopics.reduce((a, t) => a + t.likes, 0)}</div><div style={styles.statLbl}>Likes reçus</div></div>
</div>
<div style={{marginTop: 18, textAlign: "left"}}>
<div style={{fontSize: 14, fontWeight: 700, color: "#555", marginBottom: 8}}>Sujets de {activeUser}</div>
{userTopics.length === 0 && <div style={{color: "#aaa", fontSize: 13}}>Aucun sujet pour l'instant.</div>}
{userTopics.map(t => (
<div key={t.id} style={{...styles.card, cursor: "pointer"}} onClick={() => { setActiveTopic(t); setView("topic"); }}>
<span style={styles.catTag(t.category)}>{getCatLabel(t.category)}</span>
<div style={styles.cardTitle}>{t.title}</div>
</div>
))}
</div>
</div>
</div>
);
}

if (view === "topic" && activeTopic) {
const topicReplies = replies[activeTopic.id] || [];
const t = topics.find(x => x.id === activeTopic.id) || activeTopic;
return (
<div style={styles.wrap}>
<button style={styles.backBtn} onClick={() => setView("list")}>← Retour au forum</button>
<div style={styles.topicHeader}>
<span style={styles.catTag(t.category)}>{getCatLabel(t.category)}</span>
{t.pinned && <span style={styles.pinBadge}>📌 Épinglé</span>}
<div style={styles.topicTitle}>{t.title}</div>
<div style={{display:"flex",alignItems:"center",gap:8,marginBottom:10}}>
<div style={styles.avatar(t.avatarColor, t.avatarBg, 28)}>{t.avatar}</div>
<span style={{fontSize:13,color:"#888"}}>par <b style={{color:"#555",cursor:"pointer"}} onClick={e=>openProfile(e,t.author)}>{t.author}</b> · {t.time}</span>
</div>
<div style={styles.topicBody}>{t.content}</div>
<div style={styles.cardFoot}>
<button style={styles.likeBtn(liked[t.id])} onClick={e=>handleLike(t.id,e)}>❤ {t.likes}</button>
<span style={styles.replyBadge}>💬 {t.replies} réponses</span>
</div>
</div>
<div style={{fontSize:14,fontWeight:700,color:"#555",marginBottom:8}}>Réponses</div>
{topicReplies.length === 0 && <div style={{color:"#bbb",fontSize:13,marginBottom:12}}>Aucune réponse encore. Soyez le premier !</div>}
{topicReplies.map(r => (
<div key={r.id} style={styles.replyCard}>
<div style={{display:"flex",alignItems:"center",gap:8,marginBottom:6}}>
<div style={styles.avatar(r.avatarColor, r.avatarBg, 28)}>{r.avatar}</div>
<span style={{fontSize:13,fontWeight:600,color:"#555"}}>{r.author}</span>
<span style={{fontSize:12,color:"#bbb"}}>· {r.time}</span>
</div>
<div style={{fontSize:14,color:"#333",lineHeight:1.5}}>{r.content}</div>
</div>
))}
<div style={{marginTop:14}}>
<textarea style={styles.replyInput} rows={3} placeholder="Écrire une réponse..." value={replyText} onChange={e=>setReplyText(e.target.value)} />
<button style={styles.sendBtn} onClick={()=>handleReply(t.id)}>Envoyer</button>
</div>
</div>
);
}

return (
<div style={styles.wrap}>
<div style={styles.header}>
<div style={styles.headerTitle}>💬 Forum Général</div>
<div style={styles.headerSub}>{topics.length} sujets · Rejoignez la discussion !</div>
</div>

<div style={styles.topBar}>
<input style={styles.searchInput} placeholder="🔍 Rechercher un sujet..." value={search} onChange={e=>setSearch(e.target.value)} />
<button style={styles.btn} onClick={()=>setShowForm(!showForm)}>+ Nouveau sujet</button>
<div style={{...styles.avatar(currentUser.avatarColor, currentUser.avatarBg, 36), cursor:"pointer"}} onClick={e=>openProfile(e, currentUser.name)}>{currentUser.avatar}</div>
</div>

{showForm && (
<div style={styles.formCard}>
<div style={{fontSize:16,fontWeight:700,color:"#333",marginBottom:14}}>Créer un nouveau sujet</div>
<label style={styles.formLabel}>Titre</label>
<input style={styles.formInput} placeholder="Titre du sujet..." value={newTitle} onChange={e=>setNewTitle(e.target.value)} />
<label style={styles.formLabel}>Catégorie</label>
<select style={styles.select} value={newCat} onChange={e=>setNewCat(e.target.value)}>
{CATEGORIES.filter(c=>c.id!=="all").map(c=><option key={c.id} value={c.id}>{c.label}</option>)}
</select>
<label style={styles.formLabel}>Contenu</label>
<textarea style={{...styles.replyInput, marginBottom:12}} rows={4} placeholder="Décrivez votre sujet..." value={newContent} onChange={e=>setNewContent(e.target.value)} />
<div style={{display:"flex",gap:8}}>
<button style={styles.sendBtn} onClick={handleNewTopic}>Publier</button>
<button style={{...styles.sendBtn, background:"#eee",color:"#888"}} onClick={()=>setShowForm(false)}>Annuler</button>
</div>
</div>
)}

<div style={styles.cats}>
{CATEGORIES.map(c => (
<button key={c.id} style={styles.catBtn(activeCat===c.id, c)} onClick={()=>setActiveCat(c.id)}>{c.label}</button>
))}
</div>

{filtered.length === 0 && <div style={{color:"#bbb",fontSize:14,textAlign:"center",padding:"2rem 0"}}>Aucun sujet trouvé.</div>}

{filtered.sort((a,b)=>(b.pinned?1:0)-(a.pinned?1:0)).map(t => (
<div key={t.id} style={styles.card} onClick={()=>openTopic(t)}>
<div style={styles.cardTop}>
<div style={styles.avatar(t.avatarColor, t.avatarBg)} onClick={e=>openProfile(e,t.author)}>{t.avatar}</div>
<div style={{flex:1,minWidth:0}}>
<div>
{t.pinned && <span style={styles.pinBadge}>📌</span>}
<span style={styles.catTag(t.category)}>{getCatLabel(t.category)}</span>
</div>
<div style={styles.cardTitle}>{t.title}</div>
<div style={styles.meta}>par <b style={{color:"#888",cursor:"pointer"}} onClick={e=>openProfile(e,t.author)}>{t.author}</b> · {t.time}</div>
</div>
</div>
<div style={styles.cardFoot}>
<button style={styles.likeBtn(liked[t.id])} onClick={e=>handleLike(t.id,e)}>❤ {t.likes}</button>
<span style={styles.replyBadge}>💬 {t.replies}</span>
</div>
</div>
))}
</div>
);
}
 
𝑪𝑨𝑷𝑰𝑻𝑨𝑰𝑵𝑬 𝑱𝑨𝑪𝑲
Divin
Donateur 🤲
🥇 Top contributeur
Messages
54 697
Fofocoins
254 280
Don à Foforum
Virement de 100€ 💸
Virement de 100€ 💸
Virement de 50€
Genre
Homme
Vu la longueur du code il va falloir s'attendre à un truc très basique
Pour bien faire il faudrait créer un plan
Donc des pages et un code pour chaque page

Par exemple ...
Connexion
Profil
Forum

Et une réservée pour l'administration
Sans oublier le RGPD obligatoire vu les lois européennes
 
𝑪𝑨𝑷𝑰𝑻𝑨𝑰𝑵𝑬 𝑱𝑨𝑪𝑲
Divin
Donateur 🤲
🥇 Top contributeur
Messages
54 697
Fofocoins
254 280
Don à Foforum
Virement de 100€ 💸
Virement de 100€ 💸
Virement de 50€
Genre
Homme
Voici une version basique de chez basique
Celle du code publié tout en haut !


Je tente de créer un forum de discussions (à titre éducatif et pas en concurrence)
 
𝑪𝑨𝑷𝑰𝑻𝑨𝑰𝑵𝑬 𝑱𝑨𝑪𝑲
Divin
Donateur 🤲
🥇 Top contributeur
Messages
54 697
Fofocoins
254 280
Don à Foforum
Virement de 100€ 💸
Virement de 100€ 💸
Virement de 50€
Genre
Homme
Ok on va modifier le code pour pouvoir s'inscrire
Et empêcher les inscriptions de masse (fakes)
import { useState, useEffect, useRef } from "react";

// ─── Constants ───────────────────────────────────────────────────────────────
const CATEGORIES = [
{ id: "all", label: "Tout", color: "#7F77DD", bg: "#EEEDFE" },
{ id: "actu", label: "Actualité", 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 AVATAR_COLORS = [
{ color:"#7F77DD", bg:"#EEEDFE" }, { color:"#D4537E", bg:"#FBEAF0" },
{ color:"#0F6E56", bg:"#E1F5EE" }, { color:"#BA7517", bg:"#FAEEDA" },
{ color:"#185FA5", bg:"#E6F1FB" }, { color:"#A32D2D", bg:"#FCEBEB" },
];

const INITIAL_TOPICS = [
{ id:1, title:"Quels sont vos films préférés de 2025 ?", category:"culture", author:"Cinéphile42", avatarColor:"#D4537E", avatarBg:"#FBEAF0", replies:24, likes:47, time:"il y a 2h", content:"Je cherche des recommandations pour mes soirées du week-end !", pinned:true },
{ id:2, title:"L'IA va-t-elle remplacer les développeurs ?", category:"tech", author:"DevSenior", avatarColor:"#0F6E56", avatarBg:"#E1F5EE", replies:58, likes:102, time:"il y a 4h", content:"Un débat qui revient souvent… vos avis ?", pinned:false },
{ id:3, title:"Blague du jour — partagez les vôtres !", category:"humour", author:"Rigolo77", avatarColor:"#BA7517", avatarBg:"#FAEEDA", replies:89, likes:213, time:"il y a 6h", content:"Pourquoi les plongeurs plongent-ils toujours en arrière ? Parce que sinon ils tomberaient dans le bateau.", pinned:false },
{ id:4, title:"Résultats de la Ligue des Champions", category:"sport", author:"FootFan", avatarColor:"#A32D2D", avatarBg:"#FCEBEB", replies:132, likes:88, time:"il y a 1h", content:"Quelle soirée ! Vos réactions ?", pinned:false },
{ id:5, title:"Élections européennes — analyse et perspectives", category:"actu", author:"Politologue", avatarColor:"#185FA5", avatarBg:"#E6F1FB", replies:76, likes:54, time:"il y a 3h", content:"Un décryptage des résultats et de leurs implications.", pinned:false },
];

const MAX_LOGIN_ATTEMPTS = 5;
const LOCKOUT_MS = 2 * 60 * 1000; // 2 min
const MIN_REGISTER_INTERVAL_MS = 10 * 1000; // 10s entre deux inscriptions depuis le même "navigateur"
const BAD_WORDS = ["admin","moderateur","modérateur","root","system"];

let nextTopicId = 6;

// ─── Tiny hash (not crypto, just anti-plaintext) ──────────────────────────
function simpleHash(str) {
let h = 0x811c9dc5;
for (let i = 0; i < str.length; i++) {
h ^= str.charCodeAt(i);
h = (h * 0x01000193) >>> 0;
}
return h.toString(16);
}

// ─── Storage helpers ──────────────────────────────────────────────────────
async function storageGet(key) {
try { const r = await window.storage.get(key, true); return r ? JSON.parse(r.value) : null; }
catch { return null; }
}
async function storageSet(key, val) {
try { await window.storage.set(key, JSON.stringify(val), true); } catch {}
}

// ─── Styles ───────────────────────────────────────────────────────────────
const S = {
wrap: { fontFamily:"system-ui,sans-serif", maxWidth:720, margin:"0 auto", padding:"0 0 2rem" },
header: { background:"linear-gradient(135deg,#7F77DD 0%,#D4537E 50%,#BA7517 100%)", borderRadius:16, padding:"1.5rem 1.5rem 1.2rem", marginBottom:16, color:"#fff" },
hTitle: { fontSize:26, fontWeight:700, margin:0, letterSpacing:-0.5 },
hSub: { fontSize:13, opacity:.85, marginTop:4 },
card: { background:"#fff", borderRadius:14, border:"1px solid #f0f0f0", padding:"14px 16px", marginBottom:10, boxShadow:"0 1px 4px rgba(0,0,0,0.04)" },
input: { width:"100%", padding:"9px 12px", borderRadius:10, border:"1.5px solid #e0e0e0", fontSize:14, outline:"none", boxSizing:"border-box", fontFamily:"inherit" },
textarea: { width:"100%", padding:"10px 12px", borderRadius:10, border:"1.5px solid #e0e0e0", fontSize:14, outline:"none", resize:"none", boxSizing:"border-box", fontFamily:"inherit" },
btn: (bg="#7F77DD",color="#fff") => ({ padding:"9px 20px", borderRadius:10, border:"none", background:bg, color, fontWeight:700, fontSize:14, cursor:"pointer" }),
gradBtn: { padding:"9px 20px", borderRadius:10, border:"none", background:"linear-gradient(90deg,#7F77DD,#D4537E)", color:"#fff", fontWeight:700, fontSize:14, cursor:"pointer" },
backBtn: { background:"none", border:"none", color:"#7F77DD", fontWeight:700, fontSize:14, cursor:"pointer", padding:"8px 0" },
avatar: (color,bg,size=36) => ({ width:size, height:size, borderRadius:"50%", background:bg, color, fontWeight:700, fontSize:size>40?22:15, display:"flex", alignItems:"center", justifyContent:"center", flexShrink:0 }),
catTag: (id) => { const c=CATEGORIES.find(x=>x.id===id); return { display:"inline-block", padding:"2px 10px", borderRadius:20, background:c?.bg||"#eee", color:c?.color||"#888", fontSize:11, fontWeight:600, marginRight:6 }; },
pinBadge: { display:"inline-flex", alignItems:"center", gap:4, background:"#FAEEDA", color:"#BA7517", fontSize:11, fontWeight:700, padding:"2px 8px", borderRadius:20, marginRight:6 },
likeBtn: (on) => ({ display:"flex", alignItems:"center", gap:4, padding:"4px 10px", borderRadius:20, border:`1.5px solid ${on?"#D4537E":"#eee"}`, background:eek:n?"#FBEAF0":"#fafafa", color:eek:n?"#D4537E":"#aaa", fontSize:13, fontWeight:600, cursor:"pointer" }),
label: { fontSize:13, fontWeight:600, color:"#555", display:"block", marginBottom:5 },
error: { color:"#c0392b", fontSize:13, marginTop:6 },
success: { color:"#0F6E56", fontSize:13, marginTop:6 },
badge: (color,bg) => ({ display:"inline-block", padding:"2px 10px", borderRadius:20, background:bg, color, fontSize:12, fontWeight:700 }),
divider: { border:"none", borderTop:"1px solid #f0f0f0", margin:"12px 0" },
tab: (on) => ({ padding:"8px 20px", borderRadius:10, border:"none", background:eek:n?"#7F77DD":"#f0f0f0", color:eek:n?"#fff":"#888", fontWeight:700, fontSize:14, cursor:"pointer" }),
};

// ═══════════════════════════════════════════════════════════════════════════
export default function App() {
const [screen, setScreen] = useState("loading"); // loading|auth|forum
const [authTab, setAuthTab] = useState("login");
const [currentUser, setCurrentUser] = useState(null);
const [topics, setTopics] = useState(INITIAL_TOPICS);
const [view, setView] = useState("list"); // list|topic|profile|newTopic
const [activeTopic, setActiveTopic] = useState(null);
const [activeAuthor, setActiveAuthor] = useState(null);
const [liked, setLiked] = useState({});
const [replyText, setReplyText] = useState("");
const [replies, setReplies] = useState({});
const [activeCat, setActiveCat] = useState("all");
const [search, setSearch] = useState("");
const [showForm, setShowForm] = useState(false);
const [newTitle, setNewTitle] = useState("");
const [newContent, setNewContent] = useState("");
const [newCat, setNewCat] = useState("actu");

// Auth form state
const [loginUser, setLoginUser] = useState("");
const [loginPass, setLoginPass] = useState("");
const [regUser, setRegUser] = useState("");
const [regPass, setRegPass] = useState("");
const [regPass2, setRegPass2] = useState("");
const [authError, setAuthError] = useState("");
const [authInfo, setAuthInfo] = useState("");
const [loading, setLoading] = useState(false);

// Anti-abuse (in-memory for this session)
const loginAttempts = useRef({}); // { username: { count, lockedUntil } }
const lastRegTime = useRef(0);

// ── Load session on mount ──────────────────────────────────────────────
useEffect(() => {
(async () => {
const session = await storageGet("session");
if (session?.username) {
const users = (await storageGet("users")) || {};
if (users[session.username]) {
setCurrentUser({ username: session.username, ...users[session.username] });
setScreen("forum");
return;
}
}
setScreen("auth");
})();
}, []);

// ── Auth helpers ───────────────────────────────────────────────────────
const getCatLabel = id => CATEGORIES.find(c=>c.id===id)?.label || id;

function validateUsername(u) {
if (u.length < 3 || u.length > 20) return "Pseudo : 3 à 20 caractères.";
if (!/^[a-zA-Z0-9_\-À-ÿ]+$/.test(u)) return "Caractères autorisés : lettres, chiffres, _ et -.";
if (BAD_WORDS.some(w => u.toLowerCase().includes(w))) return "Ce pseudo est réservé.";
return null;
}
function validatePassword(p) {
if (p.length < 6) return "Mot de passe : 6 caractères minimum.";
return null;
}
function isLockedOut(username) {
const a = loginAttempts.current[username];
if (!a) return false;
if (a.lockedUntil && Date.now() < a.lockedUntil) return a.lockedUntil;
return false;
}
function recordFailedAttempt(username) {
const a = loginAttempts.current[username] || { count:0, lockedUntil:0 };
a.count++;
if (a.count >= MAX_LOGIN_ATTEMPTS) { a.lockedUntil = Date.now() + LOCKOUT_MS; a.count = 0; }
loginAttempts.current[username] = a;
}
function resetAttempts(username) { delete loginAttempts.current[username]; }

async function handleLogin() {
setAuthError(""); setAuthInfo("");
const u = loginUser.trim();
if (!u || !loginPass) { setAuthError("Remplissez tous les champs."); return; }
const locked = isLockedOut(u);
if (locked) {
const secs = Math.ceil((locked - Date.now()) / 1000);
setAuthError(`Trop de tentatives. Réessayez dans ${secs}s.`);
return;
}
setLoading(true);
const users = (await storageGet("users")) || {};
const user = users;
if (!user || user.passwordHash !== simpleHash(loginPass)) {
recordFailedAttempt(u);
const a = loginAttempts.current || {};
const remaining = MAX_LOGIN_ATTEMPTS - (a.count || 0);
setAuthError(`Identifiants incorrects. ${remaining} tentative(s) restante(s).`);
setLoading(false); return;
}
resetAttempts(u);
await storageSet("session", { username: u });
setCurrentUser({ username: u, ...user });
setScreen("forum");
setLoading(false);
}

async function handleRegister() {
setAuthError(""); setAuthInfo("");
const u = regUser.trim();
const errU = validateUsername(u);
if (errU) { setAuthError(errU); return; }
const errP = validatePassword(regPass);
if (errP) { setAuthError(errP); return; }
if (regPass !== regPass2) { setAuthError("Les mots de passe ne correspondent pas."); return; }

// Anti-spam : délai entre inscriptions depuis ce navigateur
const elapsed = Date.now() - lastRegTime.current;
if (elapsed < MIN_REGISTER_INTERVAL_MS) {
setAuthError(`Patientez encore ${Math.ceil((MIN_REGISTER_INTERVAL_MS - elapsed)/1000)}s avant une nouvelle inscription.`);
return;
}

setLoading(true);
const users = (await storageGet("users")) || {};
if (users) { setAuthError("Ce pseudo est déjà pris."); setLoading(false); return; }

const colorIdx = Object.keys(users).length % AVATAR_COLORS.length;
const newUser = {
passwordHash: simpleHash(regPass),
avatarColor: AVATAR_COLORS[colorIdx].color,
avatarBg: AVATAR_COLORS[colorIdx].bg,
joined: new Date().toLocaleDateString("fr-FR", { month:"long", year:"numeric" }),
posts: 0,
};
users = newUser;
await storageSet("users", users);
lastRegTime.current = Date.now();

await storageSet("session", { username: u });
setCurrentUser({ username: u, ...newUser });
setScreen("forum");
setLoading(false);
}

async function handleLogout() {
await storageSet("session", null);
setCurrentUser(null); setScreen("auth"); setAuthTab("login");
setLoginUser(""); setLoginPass(""); setAuthError(""); setAuthInfo("");
}

// ── Forum actions ──────────────────────────────────────────────────────
const handleLike = (id, e) => {
e?.stopPropagation();
setLiked(prev => { const on=!prev[id]; return {...prev,[id]:eek:n}; });
setTopics(prev => prev.map(t => t.id===id ? {...t, likes: t.likes + (liked[id]?-1:1)} : t));
};

const handleReply = (topicId) => {
if (!replyText.trim()) return;
const r = { id: Date.now(), author: currentUser.username, avatarColor: currentUser.avatarColor, avatarBg: currentUser.avatarBg, content: replyText, time:"à l'instant" };
setReplies(prev => ({...prev, [topicId]: [...(prev[topicId]||[]), r]}));
setTopics(prev => prev.map(t => t.id===topicId ? {...t, replies:t.replies+1} : t));
setReplyText("");
};

const handleNewTopic = () => {
if (!newTitle.trim() || !newContent.trim()) return;
const t = { id: nextTopicId++, title: newTitle, category: newCat, author: currentUser.username, avatarColor: currentUser.avatarColor, avatarBg: currentUser.avatarBg, replies:0, likes:0, time:"à l'instant", content: newContent, pinned:false };
setTopics(prev => [t, ...prev]);
setNewTitle(""); setNewContent(""); setShowForm(false);
};

const openTopic = t => { setActiveTopic(t); setView("topic"); };
const openProfile = (e, author) => { e?.stopPropagation(); setActiveAuthor(author); setView("profile"); };

// ══════════════════════════════════════════════════════════════════════
// ── Loading ────────────────────────────────────────────────────────────
if (screen === "loading") return (
<div style={{...S.wrap, textAlign:"center", paddingTop:60, color:"#aaa"}}>
<div style={{fontSize:32, marginBottom:12}}>💬</div>
<div>Chargement du forum…</div>
</div>
);

// ══════════════════════════════════════════════════════════════════════
// ── Auth screen ────────────────────────────────────────────────────────
if (screen === "auth") return (
<div style={S.wrap}>
<div style={S.header}>
<div style={S.hTitle}>💬 Forum Général</div>
<div style={S.hSub}>Rejoignez la communauté pour participer aux discussions</div>
</div>

<div style={{...S.card, maxWidth:420, margin:"0 auto"}}>
{/* Tabs */}
<div style={{display:"flex", gap:8, marginBottom:20}}>
<button style={S.tab(authTab==="login")} onClick={()=>{setAuthTab("login");setAuthError("");setAuthInfo("");}}>Connexion</button>
<button style={S.tab(authTab==="register")} onClick={()=>{setAuthTab("register");setAuthError("");setAuthInfo("");}}>Inscription</button>
</div>

{authTab === "login" ? (
<div>
<label style={S.label}>Pseudo</label>
<input style={{...S.input, marginBottom:12}} value={loginUser} onChange={e=>setLoginUser(e.target.value)} placeholder="Votre pseudo" onKeyDown={e=>e.key==="Enter"&&handleLogin()} />
<label style={S.label}>Mot de passe</label>
<input style={{...S.input, marginBottom:16}} type="password" value={loginPass} onChange={e=>setLoginPass(e.target.value)} placeholder="••••••••" onKeyDown={e=>e.key==="Enter"&&handleLogin()} />
{authError && <div style={S.error}>⚠ {authError}</div>}
{authInfo && <div style={S.success}>✓ {authInfo}</div>}
<button style={{...S.gradBtn, marginTop:14, width:"100%"}} onClick={handleLogin} disabled={loading}>
{loading ? "Connexion…" : "Se connecter"}
</button>
<div style={{textAlign:"center", marginTop:12, fontSize:13, color:"#aaa"}}>
Pas encore de compte ?{" "}
<span style={{color:"#7F77DD", cursor:"pointer", fontWeight:600}} onClick={()=>setAuthTab("register")}>S'inscrire</span>
</div>
</div>
) : (
<div>
<label style={S.label}>Pseudo <span style={{color:"#aaa",fontWeight:400}}>(3–20 caractères)</span></label>
<input style={{...S.input, marginBottom:12}} value={regUser} onChange={e=>setRegUser(e.target.value)} placeholder="Choisissez un pseudo" />
<label style={S.label}>Mot de passe <span style={{color:"#aaa",fontWeight:400}}>(6 car. min.)</span></label>
<input style={{...S.input, marginBottom:12}} type="password" value={regPass} onChange={e=>setRegPass(e.target.value)} placeholder="••••••••" />
<label style={S.label}>Confirmer le mot de passe</label>
<input style={{...S.input, marginBottom:16}} type="password" value={regPass2} onChange={e=>setRegPass2(e.target.value)} placeholder="••••••••" onKeyDown={e=>e.key==="Enter"&&handleRegister()} />

{/* Anti-fake notice */}
<div style={{background:"#EEEDFE", borderRadius:10, padding:"10px 12px", fontSize:12, color:"#7F77DD", marginBottom:12}}>
🛡 Protection anti-abus activée : une seule inscription possible toutes les 10 secondes depuis votre appareil. Les comptes suspects peuvent être bannis.
</div>

{authError && <div style={S.error}>⚠ {authError}</div>}
{authInfo && <div style={S.success}>✓ {authInfo}</div>}
<button style={{...S.gradBtn, marginTop:10, width:"100%"}} onClick={handleRegister} disabled={loading}>
{loading ? "Inscription…" : "Créer mon compte"}
</button>
<div style={{textAlign:"center", marginTop:12, fontSize:13, color:"#aaa"}}>
Déjà membre ?{" "}
<span style={{color:"#7F77DD", cursor:"pointer", fontWeight:600}} onClick={()=>setAuthTab("login")}>Se connecter</span>
</div>
</div>
)}
</div>
</div>
);

// ══════════════════════════════════════════════════════════════════════
// ── Forum : profile view ───────────────────────────────────────────────
if (view === "profile") {
const isMe = activeAuthor === currentUser.username;
const userTopics = topics.filter(t => t.author === activeAuthor);
const ac = isMe ? currentUser.avatarColor : (userTopics[0]?.avatarColor || "#7F77DD");
const ab = isMe ? currentUser.avatarBg : (userTopics[0]?.avatarBg || "#EEEDFE");
return (
<div style={S.wrap}>
<button style={S.backBtn} onClick={()=>setView("list")}>← Retour</button>
<div style={{...S.card, textAlign:"center", padding:"24px"}}>
<div style={{...S.avatar(ac, ab, 64), margin:"0 auto 12px"}}>{activeAuthor[0].toUpperCase()}</div>
<div style={{fontSize:20, fontWeight:700, color:"#222"}}>{activeAuthor}</div>
{isMe && <div style={{...S.badge("#7F77DD","#EEEDFE"), marginTop:6}}>👤 Vous</div>}
<div style={{fontSize:13, color:"#aaa", marginTop:6}}>Membre depuis {isMe ? currentUser.joined : (userTopics[0]?.joined || "?")}</div>
<div style={{display:"grid", gridTemplateColumns:"1fr 1fr", gap:10, marginTop:16}}>
<div style={{background:"#fafafa", borderRadius:10, padding:12}}>
<div style={{fontSize:22, fontWeight:700, color:"#7F77DD"}}>{userTopics.length}</div>
<div style={{fontSize:12, color:"#aaa", marginTop:2}}>Sujets créés</div>
</div>
<div style={{background:"#fafafa", borderRadius:10, padding:12}}>
<div style={{fontSize:22, fontWeight:700, color:"#D4537E"}}>{userTopics.reduce((a,t)=>a+t.likes,0)}</div>
<div style={{fontSize:12, color:"#aaa", marginTop:2}}>Likes reçus</div>
</div>
</div>
{isMe && <button style={{...S.btn("#eee","#888"), marginTop:16, width:"100%"}} onClick={handleLogout}>Se déconnecter</button>}
{userTopics.length > 0 && (
<div style={{marginTop:18, textAlign:"left"}}>
<div style={{fontSize:14, fontWeight:700, color:"#555", marginBottom:8}}>Sujets de {activeAuthor}</div>
{userTopics.map(t => (
<div key={t.id} style={{...S.card, cursor:"pointer"}} onClick={()=>openTopic(t)}>
<span style={S.catTag(t.category)}>{getCatLabel(t.category)}</span>
<div style={{fontSize:15, fontWeight:600, color:"#222", margin:"4px 0 0"}}>{t.title}</div>
</div>
))}
</div>
)}
</div>
</div>
);
}

// ── Forum : topic detail ───────────────────────────────────────────────
if (view === "topic" && activeTopic) {
const t = topics.find(x=>x.id===activeTopic.id) || activeTopic;
const topicReplies = replies[t.id] || [];
return (
<div style={S.wrap}>
<button style={S.backBtn} onClick={()=>setView("list")}>← Retour au forum</button>
<div style={S.card}>
<span style={S.catTag(t.category)}>{getCatLabel(t.category)}</span>
{t.pinned && <span style={S.pinBadge}>📌 Épinglé</span>}
<div style={{fontSize:20, fontWeight:700, color:"#222", margin:"8px 0 6px"}}>{t.title}</div>
<div style={{display:"flex", alignItems:"center", gap:8, marginBottom:10}}>
<div style={S.avatar(t.avatarColor, t.avatarBg, 28)}>{t.author[0].toUpperCase()}</div>
<span style={{fontSize:13, color:"#888"}}>par <b style={{color:"#555", cursor:"pointer"}} onClick={e=>openProfile(e,t.author)}>{t.author}</b> · {t.time}</span>
</div>
<div style={{fontSize:15, color:"#444", lineHeight:1.6, marginBottom:12}}>{t.content}</div>
<div style={{display:"flex", alignItems:"center", gap:14}}>
<button style={S.likeBtn(liked[t.id])} onClick={e=>handleLike(t.id,e)}>❤ {t.likes}</button>
<span style={{fontSize:13, color:"#aaa"}}>💬 {t.replies} réponses</span>
</div>
</div>

<div style={{fontSize:14, fontWeight:700, color:"#555", margin:"12px 0 8px"}}>Réponses ({topicReplies.length})</div>
{topicReplies.length === 0 && <div style={{color:"#bbb", fontSize:13, marginBottom:12}}>Aucune réponse encore. Soyez le premier !</div>}
{topicReplies.map(r => (
<div key={r.id} style={{...S.card, background:"#fafafa"}}>
<div style={{display:"flex", alignItems:"center", gap:8, marginBottom:6}}>
<div style={S.avatar(r.avatarColor, r.avatarBg, 28)}>{r.author[0].toUpperCase()}</div>
<span style={{fontSize:13, fontWeight:600, color:"#555", cursor:"pointer"}} onClick={e=>openProfile(e,r.author)}>{r.author}</span>
<span style={{fontSize:12, color:"#bbb"}}>· {r.time}</span>
</div>
<div style={{fontSize:14, color:"#333", lineHeight:1.5}}>{r.content}</div>
</div>
))}
<div style={{marginTop:14}}>
<textarea style={S.textarea} rows={3} placeholder="Écrire une réponse…" value={replyText} onChange={e=>setReplyText(e.target.value)} />
<button style={{...S.gradBtn, marginTop:8}} onClick={()=>handleReply(t.id)}>Envoyer</button>
</div>
</div>
);
}

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

return (
<div style={S.wrap}>
<div style={S.header}>
<div style={{display:"flex", justifyContent:"space-between", alignItems:"flex-start"}}>
<div>
<div style={S.hTitle}>💬 Forum Général</div>
<div style={S.hSub}>{topics.length} sujets · Bienvenue, <b>{currentUser.username}</b> !</div>
</div>
<div style={{display:"flex", alignItems:"center", gap:8}}>
<div style={{...S.avatar(currentUser.avatarColor, currentUser.avatarBg, 38), cursor:"pointer", border:"2px solid rgba(255,255,255,.4)"}} onClick={e=>openProfile(e,currentUser.username)}>
{currentUser.username[0].toUpperCase()}
</div>
</div>
</div>
</div>

{/* Top bar */}
<div style={{display:"flex", gap:8, marginBottom:12, alignItems:"center"}}>
<input style={{...S.input, flex:1}} placeholder="🔍 Rechercher…" value={search} onChange={e=>setSearch(e.target.value)} />
<button style={S.gradBtn} onClick={()=>setShowForm(!showForm)}>+ Nouveau sujet</button>
</div>

{/* New topic form */}
{showForm && (
<div style={{...S.card, border:"1.5px solid #e0e0e0", marginBottom:16}}>
<div style={{fontSize:16, fontWeight:700, color:"#333", marginBottom:14}}>Créer un sujet</div>
<label style={S.label}>Titre</label>
<input style={{...S.input, marginBottom:12}} placeholder="Titre du sujet…" value={newTitle} onChange={e=>setNewTitle(e.target.value)} />
<label style={S.label}>Catégorie</label>
<select style={{...S.input, marginBottom:12}} value={newCat} onChange={e=>setNewCat(e.target.value)}>
{CATEGORIES.filter(c=>c.id!=="all").map(c=><option key={c.id} value={c.id}>{c.label}</option>)}
</select>
<label style={S.label}>Contenu</label>
<textarea style={{...S.textarea, marginBottom:12}} rows={4} placeholder="Décrivez votre sujet…" value={newContent} onChange={e=>setNewContent(e.target.value)} />
<div style={{display:"flex", gap:8}}>
<button style={S.gradBtn} onClick={handleNewTopic}>Publier</button>
<button style={S.btn("#eee","#888")} onClick={()=>setShowForm(false)}>Annuler</button>
</div>
</div>
)}

{/* Categories */}
<div style={{display:"flex", gap:8, flexWrap:"wrap", marginBottom:14}}>
{CATEGORIES.map(c => {
const on = activeCat===c.id;
return <button key={c.id} style={{padding:"5px 14px", borderRadius:20, border:`1.5px solid ${on?c.color:"#ddd"}`, background:eek:n?c.bg:"#fff", color:eek:n?c.color:"#888", fontWeight:eek:n?700:400, fontSize:13, cursor:"pointer"}} onClick={()=>setActiveCat(c.id)}>{c.label}</button>;
})}
</div>

{filtered.length === 0 && <div style={{color:"#bbb", fontSize:14, textAlign:"center", padding:"2rem 0"}}>Aucun sujet trouvé.</div>}

{filtered.map(t => (
<div key={t.id} style={{...S.card, cursor:"pointer"}} onClick={()=>openTopic(t)}>
<div style={{display:"flex", alignItems:"flex-start", gap:10}}>
<div style={{...S.avatar(t.avatarColor, t.avatarBg), cursor:"pointer"}} onClick={e=>openProfile(e,t.author)}>{t.author[0].toUpperCase()}</div>
<div style={{flex:1, minWidth:0}}>
<div>
{t.pinned && <span style={S.pinBadge}>📌</span>}
<span style={S.catTag(t.category)}>{getCatLabel(t.category)}</span>
</div>
<div style={{fontSize:15, fontWeight:600, color:"#222", margin:"4px 0 2px", lineHeight:1.3}}>{t.title}</div>
<div style={{fontSize:12, color:"#aaa"}}>par <b style={{color:"#888", cursor:"pointer"}} onClick={e=>openProfile(e,t.author)}>{t.author}</b> · {t.time}</div>
</div>
</div>
<div style={{display:"flex", alignItems:"center", gap:14, marginTop:10}}>
<button style={S.likeBtn(liked[t.id])} onClick={e=>handleLike(t.id,e)}>❤ {t.likes}</button>
<span style={{fontSize:13, color:"#aaa"}}>💬 {t.replies}</span>
</div>
</div>
))}
</div>
);
}
 
𝑪𝑨𝑷𝑰𝑻𝑨𝑰𝑵𝑬 𝑱𝑨𝑪𝑲
Divin
Donateur 🤲
🥇 Top contributeur
Messages
54 697
Fofocoins
254 280
Don à Foforum
Virement de 100€ 💸
Virement de 100€ 💸
Virement de 50€
Genre
Homme
import { useState, useEffect, useRef } from "react";

// ─── Constants ───────────────────────────────────────────────────────────────
const CATEGORIES = [
{ id: "all", label: "Tout", color: "#7F77DD", bg: "#EEEDFE" },
{ id: "actu", label: "Actualité", 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 AVATAR_COLORS = [
{ color:"#7F77DD", bg:"#EEEDFE" }, { color:"#D4537E", bg:"#FBEAF0" },
{ color:"#0F6E56", bg:"#E1F5EE" }, { color:"#BA7517", bg:"#FAEEDA" },
{ color:"#185FA5", bg:"#E6F1FB" }, { color:"#A32D2D", bg:"#FCEBEB" },
];

const INITIAL_TOPICS = [
{ id:1, title:"Quels sont vos films préférés de 2025 ?", category:"culture", author:"Cinéphile42", avatarColor:"#D4537E", avatarBg:"#FBEAF0", replies:24, likes:47, time:"il y a 2h", content:"Je cherche des recommandations pour mes soirées du week-end !", pinned:true },
{ id:2, title:"L'IA va-t-elle remplacer les développeurs ?", category:"tech", author:"DevSenior", avatarColor:"#0F6E56", avatarBg:"#E1F5EE", replies:58, likes:102, time:"il y a 4h", content:"Un débat qui revient souvent… vos avis ?", pinned:false },
{ id:3, title:"Blague du jour — partagez les vôtres !", category:"humour", author:"Rigolo77", avatarColor:"#BA7517", avatarBg:"#FAEEDA", replies:89, likes:213, time:"il y a 6h", content:"Pourquoi les plongeurs plongent-ils toujours en arrière ? Parce que sinon ils tomberaient dans le bateau.", pinned:false },
{ id:4, title:"Résultats de la Ligue des Champions", category:"sport", author:"FootFan", avatarColor:"#A32D2D", avatarBg:"#FCEBEB", replies:132, likes:88, time:"il y a 1h", content:"Quelle soirée ! Vos réactions ?", pinned:false },
{ id:5, title:"Élections européennes — analyse et perspectives", category:"actu", author:"Politologue", avatarColor:"#185FA5", avatarBg:"#E6F1FB", replies:76, likes:54, time:"il y a 3h", content:"Un décryptage des résultats et de leurs implications.", pinned:false },
];

const MAX_LOGIN_ATTEMPTS = 5;
const LOCKOUT_MS = 2 * 60 * 1000; // 2 min
const MIN_REGISTER_INTERVAL_MS = 10 * 1000; // 10s entre deux inscriptions depuis le même "navigateur"
const BAD_WORDS = ["admin","moderateur","modérateur","root","system"];

let nextTopicId = 6;

// ─── Tiny hash (not crypto, just anti-plaintext) ──────────────────────────
function simpleHash(str) {
let h = 0x811c9dc5;
for (let i = 0; i < str.length; i++) {
h ^= str.charCodeAt(i);
h = (h * 0x01000193) >>> 0;
}
return h.toString(16);
}

// ─── Storage helpers ──────────────────────────────────────────────────────
async function storageGet(key) {
try { const r = await window.storage.get(key, true); return r ? JSON.parse(r.value) : null; }
catch { return null; }
}
async function storageSet(key, val) {
try { await window.storage.set(key, JSON.stringify(val), true); } catch {}
}

// ─── Styles ───────────────────────────────────────────────────────────────
const S = {
wrap: { fontFamily:"system-ui,sans-serif", maxWidth:720, margin:"0 auto", padding:"0 0 2rem" },
header: { background:"linear-gradient(135deg,#7F77DD 0%,#D4537E 50%,#BA7517 100%)", borderRadius:16, padding:"1.5rem 1.5rem 1.2rem", marginBottom:16, color:"#fff" },
hTitle: { fontSize:26, fontWeight:700, margin:0, letterSpacing:-0.5 },
hSub: { fontSize:13, opacity:.85, marginTop:4 },
card: { background:"#fff", borderRadius:14, border:"1px solid #f0f0f0", padding:"14px 16px", marginBottom:10, boxShadow:"0 1px 4px rgba(0,0,0,0.04)" },
input: { width:"100%", padding:"9px 12px", borderRadius:10, border:"1.5px solid #e0e0e0", fontSize:14, outline:"none", boxSizing:"border-box", fontFamily:"inherit" },
textarea: { width:"100%", padding:"10px 12px", borderRadius:10, border:"1.5px solid #e0e0e0", fontSize:14, outline:"none", resize:"none", boxSizing:"border-box", fontFamily:"inherit" },
btn: (bg="#7F77DD",color="#fff") => ({ padding:"9px 20px", borderRadius:10, border:"none", background:bg, color, fontWeight:700, fontSize:14, cursor:"pointer" }),
gradBtn: { padding:"9px 20px", borderRadius:10, border:"none", background:"linear-gradient(90deg,#7F77DD,#D4537E)", color:"#fff", fontWeight:700, fontSize:14, cursor:"pointer" },
backBtn: { background:"none", border:"none", color:"#7F77DD", fontWeight:700, fontSize:14, cursor:"pointer", padding:"8px 0" },
avatar: (color,bg,size=36) => ({ width:size, height:size, borderRadius:"50%", background:bg, color, fontWeight:700, fontSize:size>40?22:15, display:"flex", alignItems:"center", justifyContent:"center", flexShrink:0 }),
catTag: (id) => { const c=CATEGORIES.find(x=>x.id===id); return { display:"inline-block", padding:"2px 10px", borderRadius:20, background:c?.bg||"#eee", color:c?.color||"#888", fontSize:11, fontWeight:600, marginRight:6 }; },
pinBadge: { display:"inline-flex", alignItems:"center", gap:4, background:"#FAEEDA", color:"#BA7517", fontSize:11, fontWeight:700, padding:"2px 8px", borderRadius:20, marginRight:6 },
likeBtn: (on) => ({ display:"flex", alignItems:"center", gap:4, padding:"4px 10px", borderRadius:20, border:`1.5px solid ${on?"#D4537E":"#eee"}`, background:eek:n?"#FBEAF0":"#fafafa", color:eek:n?"#D4537E":"#aaa", fontSize:13, fontWeight:600, cursor:"pointer" }),
label: { fontSize:13, fontWeight:600, color:"#555", display:"block", marginBottom:5 },
error: { color:"#c0392b", fontSize:13, marginTop:6 },
success: { color:"#0F6E56", fontSize:13, marginTop:6 },
badge: (color,bg) => ({ display:"inline-block", padding:"2px 10px", borderRadius:20, background:bg, color, fontSize:12, fontWeight:700 }),
divider: { border:"none", borderTop:"1px solid #f0f0f0", margin:"12px 0" },
tab: (on) => ({ padding:"8px 20px", borderRadius:10, border:"none", background:eek:n?"#7F77DD":"#f0f0f0", color:eek:n?"#fff":"#888", fontWeight:700, fontSize:14, cursor:"pointer" }),
};

// ═══════════════════════════════════════════════════════════════════════════
export default function App() {
const [screen, setScreen] = useState("loading"); // loading|auth|forum
const [authTab, setAuthTab] = useState("login");
const [currentUser, setCurrentUser] = useState(null);
const [topics, setTopics] = useState(INITIAL_TOPICS);
const [view, setView] = useState("list"); // list|topic|profile|newTopic
const [activeTopic, setActiveTopic] = useState(null);
const [activeAuthor, setActiveAuthor] = useState(null);
const [liked, setLiked] = useState({});
const [replyText, setReplyText] = useState("");
const [replies, setReplies] = useState({});
const [activeCat, setActiveCat] = useState("all");
const [search, setSearch] = useState("");
const [showForm, setShowForm] = useState(false);
const [newTitle, setNewTitle] = useState("");
const [newContent, setNewContent] = useState("");
const [newCat, setNewCat] = useState("actu");

// Auth form state
const [loginUser, setLoginUser] = useState("");
const [loginPass, setLoginPass] = useState("");
const [regUser, setRegUser] = useState("");
const [regPass, setRegPass] = useState("");
const [regPass2, setRegPass2] = useState("");
const [authError, setAuthError] = useState("");
const [authInfo, setAuthInfo] = useState("");
const [loading, setLoading] = useState(false);

// Anti-abuse (in-memory for this session)
const loginAttempts = useRef({}); // { username: { count, lockedUntil } }
const lastRegTime = useRef(0);

// ── Load session on mount ──────────────────────────────────────────────
useEffect(() => {
(async () => {
const session = await storageGet("session");
if (session?.username) {
const users = (await storageGet("users")) || {};
if (users[session.username]) {
setCurrentUser({ username: session.username, ...users[session.username] });
setScreen("forum");
return;
}
}
setScreen("auth");
})();
}, []);

// ── Auth helpers ───────────────────────────────────────────────────────
const getCatLabel = id => CATEGORIES.find(c=>c.id===id)?.label || id;

function validateUsername(u) {
if (u.length < 3 || u.length > 20) return "Pseudo : 3 à 20 caractères.";
if (!/^[a-zA-Z0-9_\-À-ÿ]+$/.test(u)) return "Caractères autorisés : lettres, chiffres, _ et -.";
if (BAD_WORDS.some(w => u.toLowerCase().includes(w))) return "Ce pseudo est réservé.";
return null;
}
function validatePassword(p) {
if (p.length < 6) return "Mot de passe : 6 caractères minimum.";
return null;
}
function isLockedOut(username) {
const a = loginAttempts.current[username];
if (!a) return false;
if (a.lockedUntil && Date.now() < a.lockedUntil) return a.lockedUntil;
return false;
}
function recordFailedAttempt(username) {
const a = loginAttempts.current[username] || { count:0, lockedUntil:0 };
a.count++;
if (a.count >= MAX_LOGIN_ATTEMPTS) { a.lockedUntil = Date.now() + LOCKOUT_MS; a.count = 0; }
loginAttempts.current[username] = a;
}
function resetAttempts(username) { delete loginAttempts.current[username]; }

async function handleLogin() {
setAuthError(""); setAuthInfo("");
const u = loginUser.trim();
if (!u || !loginPass) { setAuthError("Remplissez tous les champs."); return; }
const locked = isLockedOut(u);
if (locked) {
const secs = Math.ceil((locked - Date.now()) / 1000);
setAuthError(`Trop de tentatives. Réessayez dans ${secs}s.`);
return;
}
setLoading(true);
const users = (await storageGet("users")) || {};
const user = users;
if (!user || user.passwordHash !== simpleHash(loginPass)) {
recordFailedAttempt(u);
const a = loginAttempts.current || {};
const remaining = MAX_LOGIN_ATTEMPTS - (a.count || 0);
setAuthError(`Identifiants incorrects. ${remaining} tentative(s) restante(s).`);
setLoading(false); return;
}
resetAttempts(u);
await storageSet("session", { username: u });
setCurrentUser({ username: u, ...user });
setScreen("forum");
setLoading(false);
}

async function handleRegister() {
setAuthError(""); setAuthInfo("");
const u = regUser.trim();
const errU = validateUsername(u);
if (errU) { setAuthError(errU); return; }
const errP = validatePassword(regPass);
if (errP) { setAuthError(errP); return; }
if (regPass !== regPass2) { setAuthError("Les mots de passe ne correspondent pas."); return; }

// Anti-spam : délai entre inscriptions depuis ce navigateur
const elapsed = Date.now() - lastRegTime.current;
if (elapsed < MIN_REGISTER_INTERVAL_MS) {
setAuthError(`Patientez encore ${Math.ceil((MIN_REGISTER_INTERVAL_MS - elapsed)/1000)}s avant une nouvelle inscription.`);
return;
}

setLoading(true);
const users = (await storageGet("users")) || {};
if (users) { setAuthError("Ce pseudo est déjà pris."); setLoading(false); return; }

const colorIdx = Object.keys(users).length % AVATAR_COLORS.length;
const newUser = {
passwordHash: simpleHash(regPass),
avatarColor: AVATAR_COLORS[colorIdx].color,
avatarBg: AVATAR_COLORS[colorIdx].bg,
joined: new Date().toLocaleDateString("fr-FR", { month:"long", year:"numeric" }),
posts: 0,
};
users = newUser;
await storageSet("users", users);
lastRegTime.current = Date.now();

await storageSet("session", { username: u });
setCurrentUser({ username: u, ...newUser });
setScreen("forum");
setLoading(false);
}

async function handleLogout() {
await storageSet("session", null);
setCurrentUser(null); setScreen("auth"); setAuthTab("login");
setLoginUser(""); setLoginPass(""); setAuthError(""); setAuthInfo("");
}

// ── Forum actions ──────────────────────────────────────────────────────
const handleLike = (id, e) => {
e?.stopPropagation();
setLiked(prev => { const on=!prev[id]; return {...prev,[id]:eek:n}; });
setTopics(prev => prev.map(t => t.id===id ? {...t, likes: t.likes + (liked[id]?-1:1)} : t));
};

const handleReply = (topicId) => {
if (!replyText.trim()) return;
const r = { id: Date.now(), author: currentUser.username, avatarColor: currentUser.avatarColor, avatarBg: currentUser.avatarBg, content: replyText, time:"à l'instant" };
setReplies(prev => ({...prev, [topicId]: [...(prev[topicId]||[]), r]}));
setTopics(prev => prev.map(t => t.id===topicId ? {...t, replies:t.replies+1} : t));
setReplyText("");
};

const handleNewTopic = () => {
if (!newTitle.trim() || !newContent.trim()) return;
const t = { id: nextTopicId++, title: newTitle, category: newCat, author: currentUser.username, avatarColor: currentUser.avatarColor, avatarBg: currentUser.avatarBg, replies:0, likes:0, time:"à l'instant", content: newContent, pinned:false };
setTopics(prev => [t, ...prev]);
setNewTitle(""); setNewContent(""); setShowForm(false);
};

const openTopic = t => { setActiveTopic(t); setView("topic"); };
const openProfile = (e, author) => { e?.stopPropagation(); setActiveAuthor(author); setView("profile"); };

// ══════════════════════════════════════════════════════════════════════
// ── Loading ────────────────────────────────────────────────────────────
if (screen === "loading") return (
<div style={{...S.wrap, textAlign:"center", paddingTop:60, color:"#aaa"}}>
<div style={{fontSize:32, marginBottom:12}}>💬</div>
<div>Chargement du forum…</div>
</div>
);

// ══════════════════════════════════════════════════════════════════════
// ── Auth screen ────────────────────────────────────────────────────────
if (screen === "auth") return (
<div style={S.wrap}>
<div style={S.header}>
<div style={S.hTitle}>💬 Forum Général</div>
<div style={S.hSub}>Rejoignez la communauté pour participer aux discussions</div>
</div>

<div style={{...S.card, maxWidth:420, margin:"0 auto"}}>
{/* Tabs */}
<div style={{display:"flex", gap:8, marginBottom:20}}>
<button style={S.tab(authTab==="login")} onClick={()=>{setAuthTab("login");setAuthError("");setAuthInfo("");}}>Connexion</button>
<button style={S.tab(authTab==="register")} onClick={()=>{setAuthTab("register");setAuthError("");setAuthInfo("");}}>Inscription</button>
</div>

{authTab === "login" ? (
<div>
<label style={S.label}>Pseudo</label>
<input style={{...S.input, marginBottom:12}} value={loginUser} onChange={e=>setLoginUser(e.target.value)} placeholder="Votre pseudo" onKeyDown={e=>e.key==="Enter"&&handleLogin()} />
<label style={S.label}>Mot de passe</label>
<input style={{...S.input, marginBottom:16}} type="password" value={loginPass} onChange={e=>setLoginPass(e.target.value)} placeholder="••••••••" onKeyDown={e=>e.key==="Enter"&&handleLogin()} />
{authError && <div style={S.error}>⚠ {authError}</div>}
{authInfo && <div style={S.success}>✓ {authInfo}</div>}
<button style={{...S.gradBtn, marginTop:14, width:"100%"}} onClick={handleLogin} disabled={loading}>
{loading ? "Connexion…" : "Se connecter"}
</button>
<div style={{textAlign:"center", marginTop:12, fontSize:13, color:"#aaa"}}>
Pas encore de compte ?{" "}
<span style={{color:"#7F77DD", cursor:"pointer", fontWeight:600}} onClick={()=>setAuthTab("register")}>S'inscrire</span>
</div>
</div>
) : (
<div>
<label style={S.label}>Pseudo <span style={{color:"#aaa",fontWeight:400}}>(3–20 caractères)</span></label>
<input style={{...S.input, marginBottom:12}} value={regUser} onChange={e=>setRegUser(e.target.value)} placeholder="Choisissez un pseudo" />
<label style={S.label}>Mot de passe <span style={{color:"#aaa",fontWeight:400}}>(6 car. min.)</span></label>
<input style={{...S.input, marginBottom:12}} type="password" value={regPass} onChange={e=>setRegPass(e.target.value)} placeholder="••••••••" />
<label style={S.label}>Confirmer le mot de passe</label>
<input style={{...S.input, marginBottom:16}} type="password" value={regPass2} onChange={e=>setRegPass2(e.target.value)} placeholder="••••••••" onKeyDown={e=>e.key==="Enter"&&handleRegister()} />

{/* Anti-fake notice */}
<div style={{background:"#EEEDFE", borderRadius:10, padding:"10px 12px", fontSize:12, color:"#7F77DD", marginBottom:12}}>
🛡 Protection anti-abus activée : une seule inscription possible toutes les 10 secondes depuis votre appareil. Les comptes suspects peuvent être bannis.
</div>

{authError && <div style={S.error}>⚠ {authError}</div>}
{authInfo && <div style={S.success}>✓ {authInfo}</div>}
<button style={{...S.gradBtn, marginTop:10, width:"100%"}} onClick={handleRegister} disabled={loading}>
{loading ? "Inscription…" : "Créer mon compte"}
</button>
<div style={{textAlign:"center", marginTop:12, fontSize:13, color:"#aaa"}}>
Déjà membre ?{" "}
<span style={{color:"#7F77DD", cursor:"pointer", fontWeight:600}} onClick={()=>setAuthTab("login")}>Se connecter</span>
</div>
</div>
)}
</div>
</div>
);

// ══════════════════════════════════════════════════════════════════════
// ── Forum : profile view ───────────────────────────────────────────────
if (view === "profile") {
const isMe = activeAuthor === currentUser.username;
const userTopics = topics.filter(t => t.author === activeAuthor);
const ac = isMe ? currentUser.avatarColor : (userTopics[0]?.avatarColor || "#7F77DD");
const ab = isMe ? currentUser.avatarBg : (userTopics[0]?.avatarBg || "#EEEDFE");
return (
<div style={S.wrap}>
<button style={S.backBtn} onClick={()=>setView("list")}>← Retour</button>
<div style={{...S.card, textAlign:"center", padding:"24px"}}>
<div style={{...S.avatar(ac, ab, 64), margin:"0 auto 12px"}}>{activeAuthor[0].toUpperCase()}</div>
<div style={{fontSize:20, fontWeight:700, color:"#222"}}>{activeAuthor}</div>
{isMe && <div style={{...S.badge("#7F77DD","#EEEDFE"), marginTop:6}}>👤 Vous</div>}
<div style={{fontSize:13, color:"#aaa", marginTop:6}}>Membre depuis {isMe ? currentUser.joined : (userTopics[0]?.joined || "?")}</div>
<div style={{display:"grid", gridTemplateColumns:"1fr 1fr", gap:10, marginTop:16}}>
<div style={{background:"#fafafa", borderRadius:10, padding:12}}>
<div style={{fontSize:22, fontWeight:700, color:"#7F77DD"}}>{userTopics.length}</div>
<div style={{fontSize:12, color:"#aaa", marginTop:2}}>Sujets créés</div>
</div>
<div style={{background:"#fafafa", borderRadius:10, padding:12}}>
<div style={{fontSize:22, fontWeight:700, color:"#D4537E"}}>{userTopics.reduce((a,t)=>a+t.likes,0)}</div>
<div style={{fontSize:12, color:"#aaa", marginTop:2}}>Likes reçus</div>
</div>
</div>
{isMe && <button style={{...S.btn("#eee","#888"), marginTop:16, width:"100%"}} onClick={handleLogout}>Se déconnecter</button>}
{userTopics.length > 0 && (
<div style={{marginTop:18, textAlign:"left"}}>
<div style={{fontSize:14, fontWeight:700, color:"#555", marginBottom:8}}>Sujets de {activeAuthor}</div>
{userTopics.map(t => (
<div key={t.id} style={{...S.card, cursor:"pointer"}} onClick={()=>openTopic(t)}>
<span style={S.catTag(t.category)}>{getCatLabel(t.category)}</span>
<div style={{fontSize:15, fontWeight:600, color:"#222", margin:"4px 0 0"}}>{t.title}</div>
</div>
))}
</div>
)}
</div>
</div>
);
}

// ── Forum : topic detail ───────────────────────────────────────────────
if (view === "topic" && activeTopic) {
const t = topics.find(x=>x.id===activeTopic.id) || activeTopic;
const topicReplies = replies[t.id] || [];
return (
<div style={S.wrap}>
<button style={S.backBtn} onClick={()=>setView("list")}>← Retour au forum</button>
<div style={S.card}>
<span style={S.catTag(t.category)}>{getCatLabel(t.category)}</span>
{t.pinned && <span style={S.pinBadge}>📌 Épinglé</span>}
<div style={{fontSize:20, fontWeight:700, color:"#222", margin:"8px 0 6px"}}>{t.title}</div>
<div style={{display:"flex", alignItems:"center", gap:8, marginBottom:10}}>
<div style={S.avatar(t.avatarColor, t.avatarBg, 28)}>{t.author[0].toUpperCase()}</div>
<span style={{fontSize:13, color:"#888"}}>par <b style={{color:"#555", cursor:"pointer"}} onClick={e=>openProfile(e,t.author)}>{t.author}</b> · {t.time}</span>
</div>
<div style={{fontSize:15, color:"#444", lineHeight:1.6, marginBottom:12}}>{t.content}</div>
<div style={{display:"flex", alignItems:"center", gap:14}}>
<button style={S.likeBtn(liked[t.id])} onClick={e=>handleLike(t.id,e)}>❤ {t.likes}</button>
<span style={{fontSize:13, color:"#aaa"}}>💬 {t.replies} réponses</span>
</div>
</div>

<div style={{fontSize:14, fontWeight:700, color:"#555", margin:"12px 0 8px"}}>Réponses ({topicReplies.length})</div>
{topicReplies.length === 0 && <div style={{color:"#bbb", fontSize:13, marginBottom:12}}>Aucune réponse encore. Soyez le premier !</div>}
{topicReplies.map(r => (
<div key={r.id} style={{...S.card, background:"#fafafa"}}>
<div style={{display:"flex", alignItems:"center", gap:8, marginBottom:6}}>
<div style={S.avatar(r.avatarColor, r.avatarBg, 28)}>{r.author[0].toUpperCase()}</div>
<span style={{fontSize:13, fontWeight:600, color:"#555", cursor:"pointer"}} onClick={e=>openProfile(e,r.author)}>{r.author}</span>
<span style={{fontSize:12, color:"#bbb"}}>· {r.time}</span>
</div>
<div style={{fontSize:14, color:"#333", lineHeight:1.5}}>{r.content}</div>
</div>
))}
<div style={{marginTop:14}}>
<textarea style={S.textarea} rows={3} placeholder="Écrire une réponse…" value={replyText} onChange={e=>setReplyText(e.target.value)} />
<button style={{...S.gradBtn, marginTop:8}} onClick={()=>handleReply(t.id)}>Envoyer</button>
</div>
</div>
);
}

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

return (
<div style={S.wrap}>
<div style={S.header}>
<div style={{display:"flex", justifyContent:"space-between", alignItems:"flex-start"}}>
<div>
<div style={S.hTitle}>💬 Forum Général</div>
<div style={S.hSub}>{topics.length} sujets · Bienvenue, <b>{currentUser.username}</b> !</div>
</div>
<div style={{display:"flex", alignItems:"center", gap:8}}>
<div style={{...S.avatar(currentUser.avatarColor, currentUser.avatarBg, 38), cursor:"pointer", border:"2px solid rgba(255,255,255,.4)"}} onClick={e=>openProfile(e,currentUser.username)}>
{currentUser.username[0].toUpperCase()}
</div>
</div>
</div>
</div>

{/* Top bar */}
<div style={{display:"flex", gap:8, marginBottom:12, alignItems:"center"}}>
<input style={{...S.input, flex:1}} placeholder="🔍 Rechercher…" value={search} onChange={e=>setSearch(e.target.value)} />
<button style={S.gradBtn} onClick={()=>setShowForm(!showForm)}>+ Nouveau sujet</button>
</div>

{/* New topic form */}
{showForm && (
<div style={{...S.card, border:"1.5px solid #e0e0e0", marginBottom:16}}>
<div style={{fontSize:16, fontWeight:700, color:"#333", marginBottom:14}}>Créer un sujet</div>
<label style={S.label}>Titre</label>
<input style={{...S.input, marginBottom:12}} placeholder="Titre du sujet…" value={newTitle} onChange={e=>setNewTitle(e.target.value)} />
<label style={S.label}>Catégorie</label>
<select style={{...S.input, marginBottom:12}} value={newCat} onChange={e=>setNewCat(e.target.value)}>
{CATEGORIES.filter(c=>c.id!=="all").map(c=><option key={c.id} value={c.id}>{c.label}</option>)}
</select>
<label style={S.label}>Contenu</label>
<textarea style={{...S.textarea, marginBottom:12}} rows={4} placeholder="Décrivez votre sujet…" value={newContent} onChange={e=>setNewContent(e.target.value)} />
<div style={{display:"flex", gap:8}}>
<button style={S.gradBtn} onClick={handleNewTopic}>Publier</button>
<button style={S.btn("#eee","#888")} onClick={()=>setShowForm(false)}>Annuler</button>
</div>
</div>
)}

{/* Categories */}
<div style={{display:"flex", gap:8, flexWrap:"wrap", marginBottom:14}}>
{CATEGORIES.map(c => {
const on = activeCat===c.id;
return <button key={c.id} style={{padding:"5px 14px", borderRadius:20, border:`1.5px solid ${on?c.color:"#ddd"}`, background:eek:n?c.bg:"#fff", color:eek:n?c.color:"#888", fontWeight:eek:n?700:400, fontSize:13, cursor:"pointer"}} onClick={()=>setActiveCat(c.id)}>{c.label}</button>;
})}
</div>

{filtered.length === 0 && <div style={{color:"#bbb", fontSize:14, textAlign:"center", padding:"2rem 0"}}>Aucun sujet trouvé.</div>}

{filtered.map(t => (
<div key={t.id} style={{...S.card, cursor:"pointer"}} onClick={()=>openTopic(t)}>
<div style={{display:"flex", alignItems:"flex-start", gap:10}}>
<div style={{...S.avatar(t.avatarColor, t.avatarBg), cursor:"pointer"}} onClick={e=>openProfile(e,t.author)}>{t.author[0].toUpperCase()}</div>
<div style={{flex:1, minWidth:0}}>
<div>
{t.pinned && <span style={S.pinBadge}>📌</span>}
<span style={S.catTag(t.category)}>{getCatLabel(t.category)}</span>
</div>
<div style={{fontSize:15, fontWeight:600, color:"#222", margin:"4px 0 2px", lineHeight:1.3}}>{t.title}</div>
<div style={{fontSize:12, color:"#aaa"}}>par <b style={{color:"#888", cursor:"pointer"}} onClick={e=>openProfile(e,t.author)}>{t.author}</b> · {t.time}</div>
</div>
</div>
<div style={{display:"flex", alignItems:"center", gap:14, marginTop:10}}>
<button style={S.likeBtn(liked[t.id])} onClick={e=>handleLike(t.id,e)}>❤ {t.likes}</button>
<span style={{fontSize:13, color:"#aaa"}}>💬 {t.replies}</span>
</div>
</div>
))}
</div>
);
}
Je tente de créer un forum de discussions (à titre éducatif et pas en concurrence)
 
𝑪𝑨𝑷𝑰𝑻𝑨𝑰𝑵𝑬 𝑱𝑨𝑪𝑲
Divin
Donateur 🤲
🥇 Top contributeur
Messages
54 697
Fofocoins
254 280
Don à Foforum
Virement de 100€ 💸
Virement de 100€ 💸
Virement de 50€
Genre
Homme
On va recommencer
Mais avec la possibilité d'intégrer le forum à la plateforme Blogger
Normalement c'est du jamais vu
Mais on va essayer pour voir
<!--
╔══════════════════════════════════════════════════════════════════╗
║ FORUM BLOGGER + FIREBASE ║
║ À coller dans : Blogger > Mise en page > Ajouter un widget ║
║ > HTML/JavaScript ║
║ ║
║ AVANT DE COLLER : remplace les valeurs dans firebaseConfig ║
║ par celles de TON projet Firebase (voir guide ci-dessous) ║
╚══════════════════════════════════════════════════════════════════╝
-->

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

<!-- Firebase SDK -->
<script type="module">
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, getDocs, doc, updateDoc,
increment, query, orderBy, onSnapshot, serverTimestamp,
getDoc, setDoc } from "https://www.gstatic.com/firebasejs/10.12.0/firebase-firestore.js";

// ─────────────────────────────────────────────────────────────────
// 🔧 REMPLACE ICI PAR TA CONFIG FIREBASE
// ─────────────────────────────────────────────────────────────────
const firebaseConfig = {
apiKey: "REMPLACE_PAR_TON_API_KEY",
authDomain: "REMPLACE.firebaseapp.com",
projectId: "REMPLACE_PAR_TON_PROJECT_ID",
storageBucket: "REMPLACE.appspot.com",
messagingSenderId: "REMPLACE_PAR_TON_SENDER_ID",
appId: "REMPLACE_PAR_TON_APP_ID"
};
// ─────────────────────────────────────────────────────────────────

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

// ── Helpers ────────────────────────────────────────────────────────
const CATS = [
{ id:"all", label:"Tout", color:"#7F77DD", bg:"#EEEDFE" },
{ id:"actu", label:"Actualité", 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" });
};

const css = `
#forum-root * { box-sizing: border-box; font-family: system-ui, sans-serif; margin: 0; padding: 0; }
#forum-wrap { max-width: 720px; margin: 0 auto; padding: 0 0 2rem; }
.f-header { background: linear-gradient(135deg,#7F77DD,#D4537E 50%,#BA7517); border-radius: 16px; padding: 1.4rem 1.5rem; margin-bottom: 16px; color: #fff; }
.f-header h1 { font-size: 24px; font-weight: 700; }
.f-header p { font-size: 13px; opacity: .85; margin-top: 4px; }
.f-card { background: #fff; border-radius: 14px; border: 1px solid #f0f0f0; padding: 14px 16px; margin-bottom: 10px; box-shadow: 0 1px 4px rgba(0,0,0,.04); }
.f-input { width: 100%; padding: 9px 12px; border-radius: 10px; border: 1.5px solid #e0e0e0; font-size: 14px; outline: none; }
.f-textarea { width: 100%; padding: 9px 12px; border-radius: 10px; border: 1.5px solid #e0e0e0; font-size: 14px; outline: none; resize: vertical; }
.f-btn { padding: 9px 20px; border-radius: 10px; border: none; font-weight: 700; font-size: 14px; cursor: pointer; }
.f-btn-grad { background: linear-gradient(90deg,#7F77DD,#D4537E); color: #fff; }
.f-btn-google { background: #fff; color: #333; border: 1.5px solid #ddd; display: flex; align-items: center; gap: 8px; justify-content: center; width: 100%; margin-top: 10px; }
.f-btn-ghost { background: #f0f0f0; color: #666; }
.f-btn-sm { padding: 5px 14px; font-size: 13px; }
.f-tab { padding: 8px 20px; border-radius: 10px; border: none; font-weight: 700; font-size: 14px; cursor: pointer; }
.f-tab.on { background: #7F77DD; color: #fff; }
.f-tab.off { background: #f0f0f0; color: #888; }
.f-cat-btn { padding: 5px 14px; border-radius: 20px; font-size: 13px; cursor: pointer; border: 1.5px solid #ddd; background: #fff; color: #888; font-weight: 400; }
.f-cat-btn.on { font-weight: 700; }
.f-tag { display: inline-block; padding: 2px 10px; border-radius: 20px; font-size: 11px; font-weight: 600; margin-right: 6px; }
.f-pin { 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; }
.f-avatar { border-radius: 50%; display: flex; align-items: center; justify-content: center; font-weight: 700; flex-shrink: 0; overflow: hidden; }
.f-avatar img { width: 100%; height: 100%; object-fit: cover; }
.f-like { 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; }
.f-like.on { border-color: #D4537E; background: #FBEAF0; color: #D4537E; }
.f-error { color: #c0392b; font-size: 13px; margin-top: 6px; }
.f-info { color: #0F6E56; font-size: 13px; margin-top: 6px; }
.f-shield { background: #EEEDFE; border-radius: 10px; padding: 10px 12px; font-size: 12px; color: #7F77DD; margin-bottom: 12px; }
.f-topbar { display: flex; gap: 8px; align-items: center; margin-bottom: 12px; }
.f-cats { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 14px; }
.f-label { font-size: 13px; font-weight: 600; color: #555; display: block; margin-bottom: 5px; margin-top: 10px; }
.f-stat { background: #fafafa; border-radius: 10px; padding: 12px; text-align: center; }
.f-stat b { display: block; font-size: 22px; font-weight: 700; }
.f-back { background: none; border: none; color: #7F77DD; font-weight: 700; font-size: 14px; cursor: pointer; padding: 8px 0; display: flex; align-items: center; gap: 4px; }
.f-row { display: flex; align-items: center; gap: 8px; }
.f-muted { color: #aaa; font-size: 12px; }
.f-select { width: 100%; padding: 9px 12px; border-radius: 10px; border: 1.5px solid #e0e0e0; font-size: 14px; outline: none; background: #fff; margin-top: 10px; }
hr.f-hr { border: none; border-top: 1px solid #f0f0f0; margin: 12px 0; }
`;

// ── State ─────────────────────────────────────────────────────────
let state = {
screen: "loading", // loading | auth | forum
authTab: "login",
user: null,
topics: [],
replies: {},
liked: {},
activeCat: "all",
search: "",
view: "list", // list | topic | profile | newTopic
activeTopic: null,
activeAuthor: null,
showForm: false,
// form fields
lEmail:"", lPass:"",
rPseudo:"", rEmail:"", rPass:"", rPass2:"",
nTitle:"", nContent:"", nCat:"actu",
replyText:"",
error:"", info:"", loading:false,
};

// ── Render engine ─────────────────────────────────────────────────
const root = document.getElementById("forum-root");
root.innerHTML = `<style>${css}</style><div id="forum-wrap"></div>`;
const wrap = document.getElementById("forum-wrap");

function render() {
const s = state;
if (s.screen === "loading") { wrap.innerHTML = `<div style="text-align:center;padding:60px 0;color:#aaa"><div style="font-size:32px">💬</div><p style="margin-top:12px">Chargement…</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 setState(patch, skipRender) {
state = { ...state, ...patch };
if (!skipRender) render();
}

// ── Avatar helper ──────────────────────────────────────────────────
function avatarHTML(user, size=36) {
if (user?.photoURL) return `<div class="f-avatar" 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?.displayName || user?.pseudo || "?";
const idx = name.charCodeAt(0) % colors.length;
return `<div class="f-avatar" style="width:${size}px;height:${size}px;background:${bgs[idx]};color:${colors[idx]};font-size:${size>40?20:14}px">${name[0].toUpperCase()}</div>`;
}

// ── AUTH SCREEN ────────────────────────────────────────────────────
function renderAuth() {
const s = state;
wrap.innerHTML = `
<div class="f-header"><h1>💬 Forum Général</h1><p>Rejoignez la communauté pour participer</p></div>
<div class="f-card" style="max-width:420px;margin:0 auto">
<div style="display:flex;gap:8px;margin-bottom:20px">
<button class="f-tab ${s.authTab==="login"?"on":"off"}" id="tab-login">Connexion</button>
<button class="f-tab ${s.authTab==="register"?"on":"off"}" id="tab-reg">Inscription</button>
</div>
${s.authTab==="login" ? `
<label class="f-label">Email</label>
<input class="f-input" id="l-email" type="email" placeholder="vous@exemple.com" value="${s.lEmail}">
<label class="f-label">Mot de passe</label>
<input class="f-input" id="l-pass" type="password" placeholder="••••••••" value="${s.lPass}" style="margin-top:0">
${s.error?`<div class="f-error">⚠ ${s.error}</div>`:""}
${s.info ?`<div class="f-info">✓ ${s.info}</div>`:""}
<button class="f-btn f-btn-grad" id="btn-login" style="width:100%;margin-top:14px">${s.loading?"Connexion…":"Se connecter"}</button>
<button class="f-btn f-btn-google" id="btn-google-login">
<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>
Continuer avec Google
</button>
<p style="text-align:center;margin-top:12px;font-size:13px;color:#aaa">Pas de compte ? <a href="#" id="go-reg" style="color:#7F77DD;font-weight:600">S'inscrire</a></p>
` : `
<label class="f-label">Pseudo <span style="color:#aaa;font-weight:400">(3–20 car.)</span></label>
<input class="f-input" id="r-pseudo" placeholder="MonPseudo" value="${s.rPseudo}">
<label class="f-label">Email</label>
<input class="f-input" id="r-email" type="email" placeholder="vous@exemple.com" value="${s.rEmail}">
<label class="f-label">Mot de passe <span style="color:#aaa;font-weight:400">(6 car. min.)</span></label>
<input class="f-input" id="r-pass" type="password" placeholder="••••••••" value="${s.rPass}">
<label class="f-label">Confirmer le mot de passe</label>
<input class="f-input" id="r-pass2" type="password" placeholder="••••••••" value="${s.rPass2}">
<div class="f-shield" style="margin-top:12px">🛡 Protection anti-abus : validation email requise, comptes suspects bannis automatiquement.</div>
${s.error?`<div class="f-error">⚠ ${s.error}</div>`:""}
${s.info ?`<div class="f-info">✓ ${s.info}</div>`:""}
<button class="f-btn f-btn-grad" id="btn-register" style="width:100%;margin-top:10px">${s.loading?"Inscription…":"Créer mon compte"}</button>
<button class="f-btn f-btn-google" id="btn-google-reg">
<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>
Continuer avec Google
</button>
<p style="text-align:center;margin-top:12px;font-size:13px;color:#aaa">Déjà membre ? <a href="#" id="go-login" style="color:#7F77DD;font-weight:600">Se connecter</a></p>
`}
</div>`;

// events
document.getElementById("tab-login")?.addEventListener("click", () => setState({ authTab:"login", error:"", info:"" }));
document.getElementById("tab-reg")?.addEventListener("click", () => setState({ authTab:"register", error:"", info:"" }));
document.getElementById("go-login")?.addEventListener("click", e => { e.preventDefault(); setState({ authTab:"login", error:"", info:"" }); });
document.getElementById("go-reg")?.addEventListener("click", e => { e.preventDefault(); setState({ authTab:"register", error:"", info:"" }); });
document.getElementById("btn-login")?.addEventListener("click", doLogin);
document.getElementById("btn-register")?.addEventListener("click", doRegister);
document.getElementById("btn-google-login")?.addEventListener("click", doGoogle);
document.getElementById("btn-google-reg")?.addEventListener("click", doGoogle);

// sync inputs to state live (so values survive re-renders)
const bind = (id, key) => document.getElementById(id)?.addEventListener("input", e => setState({ [key]: e.target.value }, true));
bind("l-email","lEmail"); bind("l-pass","lPass");
bind("r-pseudo","rPseudo"); bind("r-email","rEmail"); bind("r-pass","rPass"); bind("r-pass2","rPass2");
}

// ── Auth actions ───────────────────────────────────────────────────
function validatePseudo(p) {
if (!p || p.length < 3 || p.length > 20) return "Pseudo : 3 à 20 caractères.";
if (!/^[a-zA-Z0-9_\-À-ÿ]+$/.test(p)) return "Caractères invalides dans le pseudo.";
if (["admin","moderateur","root","system"].some(w => p.toLowerCase().includes(w))) return "Ce pseudo est réservé.";
return null;
}

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

async function doRegister() {
const pseudo = document.getElementById("r-pseudo")?.value.trim();
const email = document.getElementById("r-email")?.value.trim();
const pass = document.getElementById("r-pass")?.value;
const pass2 = document.getElementById("r-pass2")?.value;
const errP = validatePseudo(pseudo);
if (errP) { setState({ error:errP }); return; }
if (!email) { setState({ error:"Email requis." }); return; }
if (pass.length < 6) { setState({ error:"Mot de passe : 6 caractères minimum." }); return; }
if (pass !== pass2) { setState({ error:"Les mots de passe ne correspondent pas." }); return; }
setState({ loading:true, error:"", info:"" });
try {
// Vérifie unicité du pseudo dans Firestore
const snap = await getDocs(query(collection(db, "users")));
const taken = snap.docs.some(d => d.data().pseudo?.toLowerCase() === pseudo.toLowerCase());
if (taken) { setState({ loading:false, error:"Ce pseudo est déjà pris." }); return; }

const cred = await createUserWithEmailAndPassword(auth, email, pass);
await updateProfile(cred.user, { displayName: pseudo });
await setDoc(doc(db, "users", cred.user.uid), {
pseudo, email,
joined: new Date().toLocaleDateString("fr-FR", { month:"long", year:"numeric" }),
posts: 0, role: "member",
});
// onAuthStateChanged prendra le relais
} catch(e) {
const msg = e.code === "auth/email-already-in-use" ? "Cet email est déjà utilisé."
: 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 annulée ou bloquée." });
}
}

// ── FORUM LIST ─────────────────────────────────────────────────────
function renderList() {
const s = state;
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));

const topicsHTML = filtered.length === 0
? `<p style="text-align:center;color:#bbb;padding:2rem 0">Aucun sujet trouvé.</p>`
: filtered.map(t => {
const cat = catOf(t.category);
return `
<div class="f-card topic-card" data-id="${t.id}" style="cursor:pointer">
<div class="f-row" style="align-items:flex-start;gap:10px">
<div class="author-link" data-uid="${t.authorId}" style="cursor:pointer">${avatarHTML({displayName:t.author, photoURL:t.photoURL})}</div>
<div style="flex:1;min-width:0">
<div>${t.pinned?`<span class="f-pin">📌</span>`:""}<span class="f-tag" style="background:${cat.bg};color:${cat.color}">${cat.label}</span></div>
<div style="font-size:15px;font-weight:600;color:#222;margin:4px 0 2px;line-height:1.3">${t.title}</div>
<div class="f-muted">par <b style="color:#888;cursor:pointer" class="author-link" data-uid="${t.authorId}">${t.author}</b> · ${ts2str(t.createdAt)}</div>
</div>
</div>
<div class="f-row" style="margin-top:10px">
<button class="f-like${s.liked[t.id]?" on":""} like-btn" data-id="${t.id}">❤ ${t.likes||0}</button>
<span class="f-muted" style="margin-left:8px">💬 ${t.replyCount||0}</span>
</div>
</div>`;
}).join("");

const newForm = s.showForm ? `
<div class="f-card" style="border:1.5px solid #e0e0e0;margin-bottom:16px">
<div style="font-size:16px;font-weight:700;color:#333;margin-bottom:10px">Créer un sujet</div>
<label class="f-label">Titre</label>
<input class="f-input" id="n-title" placeholder="Titre du sujet…" value="${s.nTitle}">
<label class="f-label">Catégorie</label>
<select class="f-select" id="n-cat">
${CATS.filter(c=>c.id!=="all").map(c=>`<option value="${c.id}"${c.id===s.nCat?" selected":""}>${c.label}</option>`).join("")}
</select>
<label class="f-label">Contenu</label>
<textarea class="f-textarea" id="n-content" rows="4" placeholder="Décrivez votre sujet…" style="margin-top:0">${s.nContent}</textarea>
<div class="f-row" style="margin-top:10px;gap:8px">
<button class="f-btn f-btn-grad" id="btn-post">Publier</button>
<button class="f-btn f-btn-ghost" id="btn-cancel">Annuler</button>
</div>
</div>` : "";

wrap.innerHTML = `
<div class="f-header" style="display:flex;justify-content:space-between;align-items:flex-start">
<div>
<h1>💬 Forum Général</h1>
<p>${s.topics.length} sujets · Bienvenue, <b>${s.user?.displayName||"?"}</b> !</p>
</div>
<div style="cursor:pointer" id="my-profile">${avatarHTML(s.user, 40)}</div>
</div>
<div class="f-topbar">
<input class="f-input" id="search" placeholder="🔍 Rechercher…" value="${s.search}" style="flex:1">
<button class="f-btn f-btn-grad" id="btn-new-topic">+ Nouveau sujet</button>
</div>
${newForm}
<div class="f-cats">
${CATS.map(c=>`<button class="f-cat-btn${s.activeCat===c.id?" on":""}" data-cat="${c.id}" style="${s.activeCat===c.id?`border-color:${c.color};background:${c.bg};color:${c.color}`:""}">${c.label}</button>`).join("")}
</div>
${topicsHTML}`;

// Events
document.getElementById("my-profile")?.addEventListener("click", () => setState({ view:"profile", activeAuthor:s.user.uid }));
document.getElementById("search")?.addEventListener("input", e => setState({ search: e.target.value }));
document.getElementById("btn-new-topic")?.addEventListener("click", () => setState({ showForm:!s.showForm }));
document.querySelectorAll(".f-cat-btn").forEach(b => b.addEventListener("click", () => setState({ activeCat: b.dataset.cat })));
document.querySelectorAll(".topic-card").forEach(el => el.addEventListener("click", () => {
const t = s.topics.find(x => x.id === el.dataset.id);
if (t) setState({ view:"topic", activeTopic:t });
}));
document.querySelectorAll(".author-link").forEach(el => el.addEventListener("click", e => { e.stopPropagation(); setState({ view:"profile", activeAuthor:el.dataset.uid }); }));
document.querySelectorAll(".like-btn").forEach(btn => btn.addEventListener("click", async e => {
e.stopPropagation();
const id = btn.dataset.id;
const on = !s.liked[id];
setState({ liked:{ ...s.liked, [id]:eek:n } });
await updateDoc(doc(db,"topics",id), { likes: increment(on?1:-1) });
}));

if (s.showForm) {
document.getElementById("n-title")?.addEventListener("input", e => setState({ nTitle:e.target.value }, true));
document.getElementById("n-content")?.addEventListener("input", e => setState({ nContent:e.target.value }, true));
document.getElementById("n-cat")?.addEventListener("change", e => setState({ nCat:e.target.value }, true));
document.getElementById("btn-cancel")?.addEventListener("click", () => setState({ showForm:false }));
document.getElementById("btn-post")?.addEventListener("click", doPostTopic);
}
}

// ── Seed données demo (s'exécute une seule fois) ───────────────────
async function seedDemoTopics() {
const snap = await getDocs(collection(db, "topics"));
if (!snap.empty) return; // déjà des données, on ne re-seed pas

const DEMO = [
{ title:"Quels sont vos films préférés de 2025 ?", category:"culture", content:"Je cherche des recommandations pour mes soirées du week-end !", pinned:true, likes:47, replyCount:24 },
{ title:"L'IA va-t-elle remplacer les développeurs ?", category:"tech", content:"Un débat qui revient souvent… vos avis ?", pinned:false, likes:102, replyCount:58 },
{ title:"Blague du jour — partagez les vôtres !", category:"humour", content:"Pourquoi les plongeurs plongent-ils toujours en arrière ? Parce que sinon ils tomberaient dans le bateau.", pinned:false, likes:213, replyCount:89 },
{ title:"Résultats de la Ligue des Champions", category:"sport", content:"Quelle soirée ! Vos réactions ?", pinned:false, likes:88, replyCount:132 },
{ title:"Élections européennes — analyse et perspectives", category:"actu", content:"Un décryptage des résultats et de leurs implications.", pinned:false, likes:54, replyCount:76 },
];

for (const t of DEMO) {
await addDoc(collection(db, "topics"), {
...t,
author: "Équipe Forum",
authorId: "demo",
photoURL: null,
createdAt: serverTimestamp(),
});
}
}

async function doPostTopic() {
const title = document.getElementById("n-title")?.value.trim();
const content = document.getElementById("n-content")?.value.trim();
const cat = document.getElementById("n-cat")?.value;
if (!title || !content) return;
const u = state.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 DETAIL ───────────────────────────────────────────────────
function renderTopic() {
const s = state;
const t = s.activeTopic;
if (!t) { setState({ view:"list" }); return; }
const cat = catOf(t.category);
const reps = s.replies[t.id] || [];

wrap.innerHTML = `
<button class="f-back" id="back">← Retour</button>
<div class="f-card">
<span class="f-tag" style="background:${cat.bg};color:${cat.color}">${cat.label}</span>
${t.pinned?`<span class="f-pin">📌 Épinglé</span>`:""}
<div style="font-size:20px;font-weight:700;color:#222;margin:8px 0 6px">${t.title}</div>
<div class="f-row" style="margin-bottom:10px">
<div style="cursor:pointer" class="author-link" data-uid="${t.authorId}">${avatarHTML({displayName:t.author, photoURL:t.photoURL}, 28)}</div>
<span class="f-muted">par <b style="color:#555;cursor:pointer" class="author-link" data-uid="${t.authorId}">${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="f-row">
<button class="f-like${s.liked[t.id]?" on":""}" id="topic-like">❤ ${t.likes||0}</button>
<span class="f-muted" style="margin-left:8px">💬 ${reps.length} réponse(s)</span>
</div>
</div>
<div style="font-size:14px;font-weight:700;color:#555;margin:12px 0 8px">Réponses (${reps.length})</div>
${reps.length===0?`<p style="color:#bbb;font-size:13px;margin-bottom:12px">Aucune réponse. Soyez le premier !</p>`:""}
${reps.map(r=>`
<div class="f-card" style="background:#fafafa">
<div class="f-row" style="margin-bottom:6px">
${avatarHTML({displayName:r.author, photoURL:r.photoURL}, 28)}
<b style="font-size:13px;color:#555">${r.author}</b>
<span class="f-muted">· ${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="f-textarea" id="reply-text" rows="3" placeholder="Écrire une réponse…">${s.replyText}</textarea>
<button class="f-btn f-btn-grad" id="btn-reply" style="margin-top:8px">Envoyer</button>
</div>`;

document.getElementById("back")?.addEventListener("click", () => setState({ view:"list", activeTopic:null }));
document.getElementById("topic-like")?.addEventListener("click", async () => {
const on = !s.liked[t.id];
setState({ liked:{ ...s.liked, [t.id]:eek:n } });
await updateDoc(doc(db,"topics",t.id), { likes: increment(on?1:-1) });
});
document.getElementById("reply-text")?.addEventListener("input", e => setState({ replyText:e.target.value }, true));
document.getElementById("btn-reply")?.addEventListener("click", doReply);
document.querySelectorAll(".author-link").forEach(el => el.addEventListener("click", () => setState({ view:"profile", activeAuthor:el.dataset.uid })));
}

async function doReply() {
const content = document.getElementById("reply-text")?.value.trim();
if (!content) return;
const u = state.user;
const t = state.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) });
// Reload replies
const snap = await getDocs(query(collection(db,"topics",t.id,"replies"), orderBy("createdAt")));
const reps = snap.docs.map(d=>({...d.data(), id:d.id}));
setState({ replies:{ ...state.replies, [t.id]:reps }, replyText:"" });
}

// ── PROFILE ────────────────────────────────────────────────────────
async function renderProfile() {
const s = state;
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 {}
const userTopics = s.topics.filter(t => t.authorId === uid);

wrap.innerHTML = `
<button class="f-back" id="back">← Retour</button>
<div class="f-card" style="text-align:center;padding:24px">
<div style="margin:0 auto 12px;width:64px">${avatarHTML({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">👤 Vous</div>`:""}
<div class="f-muted" style="margin-top:6px">Membre depuis ${uData.joined||"?"}</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-top:16px">
<div class="f-stat"><b style="color:#7F77DD">${userTopics.length}</b><span class="f-muted">Sujets</span></div>
<div class="f-stat"><b style="color:#D4537E">${userTopics.reduce((a,t)=>a+(t.likes||0),0)}</b><span class="f-muted">Likes reçus</span></div>
</div>
${isMe?`<button class="f-btn f-btn-ghost" id="btn-logout" style="width:100%;margin-top:16px">Se déconnecter</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(t=>{const cat=catOf(t.category);return `<div class="f-card topic-card" data-id="${t.id}" style="cursor:pointer"><span class="f-tag" 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("back")?.addEventListener("click", () => setState({ view:"list", activeAuthor:null }));
document.getElementById("btn-logout")?.addEventListener("click", () => signOut(auth));
document.querySelectorAll(".topic-card").forEach(el => el.addEventListener("click", () => {
const t = s.topics.find(x=>x.id===el.dataset.id);
if (t) setState({ view:"topic", activeTopic:t });
}));
}

// ── Firebase listeners ─────────────────────────────────────────────
onAuthStateChanged(auth, async user => {
if (user) {
setState({ user, screen:"forum" }, true);
await seedDemoTopics(); // injecte les sujets de départ si la base est vide
// Écoute les topics en temps réel
onSnapshot(query(collection(db,"topics"), orderBy("createdAt","desc")), snap => {
const topics = snap.docs.map(d=>({...d.data(), id:d.id}));
setState({ topics });
});
} else {
setState({ user:null, screen:"auth", view:"list", topics:[] });
}
});

</script>
 

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 !