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

𝑪𝑨𝑷𝑰𝑻𝑨𝑰𝑵𝑬 𝑱𝑨𝑪𝑲
Divin
Donateur 🤲
🥇 Top contributeur
Messages
54 691
Fofocoins
254 240
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 691
Fofocoins
254 240
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 691
Fofocoins
254 240
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)
 

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 !