1/* âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 2 * L'ESPACE COMMUNAUTà â TREIZIÃME MORCEAU DÃTACHà DE src/app.js 3 * âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 4 * CREA COMMUNAUTE â le lien de personne à groupe : cercles, événements, 5 * entraide, ressources partagées 6 * 7 * 8 * « Le manque, dans ce que je viens de te définir, c'est l'espace 9 * communautaire. Il faut créer un vrai espace communautaire, qui vient se 10 * lier à l'espace réseau â mais un espace communautaire, c'est beaucoup, 11 * beaucoup de choses. » 12 * 13 * ââ LA LIGNE QUI SÃPARE RÃSEAU ET COMMUNAUTà ââââââââââââââââââââââââââââââââ 14 * 15 * Elle n'est pas décorative, et sans elle les deux espaces se recopient : 16 * 17 * RÃSEAU des liens de personne à personne. Ce que JE publie atteint 18 * les gens que je connais. La relation est le sujet. 19 * COMMUNAUTà des liens de personne à GROUPE. Ce qu'on y dépose reste 20 * après nous, appartient au groupe, et sert à quelqu'un qu'on 21 * ne connaît pas. Le sujet est le sujet. 22 * 23 * D'où quatre choses qu'un fil ne saura jamais faire, et qui sont ici : 24 * 25 * Cercles un endroit qui a des membres, une raison d'être et une durée. 26 * Ãvénements une date, un lieu, des inscrits â donc quelque chose qui 27 * ARRIVE, et pas seulement quelque chose qui se lit. 28 * Entraide une QUESTION qui reste ouverte tant que personne n'a répondu, 29 * et qui se referme sur une réponse retenue. Un fil enterre une 30 * question en trois heures ; ici elle remonte tant qu'elle vaut. 31 * Ressources ce que le groupe a trouvé et gardé â la mémoire commune. 32 * 33 * ââ LA PLACE PUBLIQUE âââââââââââââââââââââââââââââââââââââââââââââââââââââââ 34 * 35 * Le cinquième onglet est le fil OUVERT â tout le monde, sans filtre de 36 * relations. Il est rendu par `renderCommunityFeed` du tronc, celui-là même 37 * qui existait : c'est la même publication, le même moteur, le même serveur. 38 * L'Accueil du Réseau montre ce fil TRIà POUR VOUS ; la place publique le 39 * montre tel qu'il est. Deux lectures, une seule source â jamais deux fils à 40 * tenir d'accord. 41 * 42 * ââ CE QUE CE MODULE NE TIENT PAS âââââââââââââââââââââââââââââââââââââââââââ 43 * 44 * Cercles, événements, entraide et ressources vivent SUR L'APPAREIL 45 * (`STATE.communaute`). Le fil et les membres, eux, passent déjà par le 46 * serveur. Tant qu'il n'y a pas de route `/api/communaute/*`, un cercle créé 47 * ici n'est visible que par vous â et le dire fait partie du travail. 48 * 49 * Emprunts : AUCUN nom lu du tronc autrement que par `window`. 50 * 51 * Expose window.CREA_COMMUNAUTE. 52 * âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ */ 53;(function () { 54 'use strict' 55 if (window.CREA_COMMUNAUTE) return 56 57 function S() { return window.STATE || (window.STATE = {}) } 58 function esc(s) { 59 if (typeof window.escHtml === 'function') return window.escHtml(s) 60 return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) { 61 return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c] 62 }) 63 } 64 function redessiner() { if (typeof window.render === 'function') window.render() } 65 function garder() { if (typeof window.save === 'function') window.save() } 66 function idNeuf() { 67 if (typeof window.uid === 'function') return window.uid() 68 return 'cm-' + Math.random().toString(36).slice(2) + Date.now().toString(36) 69 } 70 function dire(t, g, s) { if (typeof window.logNotice === 'function') window.logNotice(t, g || 'info', s || 'â¹ï¸') } 71 function ilYA(t) {
72 if (typeof window.timeAgo === 'function' && t) { try { return window.timeAgo(t) } catch (e) { /* repli */ } } 73 var ms = typeof t === 'number' ? t : Date.parse(t || '') 74 if (!Number.isFinite(ms)) return '' 75 var j = Math.round((Date.now() - ms) / 86400000) 76 return j <= 0 ? "aujourd'hui" : 'il y a ' + j + ' j' 77 } 78 function partage() { return typeof window.sessionPresumee === 'function' ? !!window.sessionPresumee() : false } 79 80 var IC = { 81 cercle: '<circle cx="12" cy="12" r="8"/><circle cx="12" cy="12" r="3"/>', 82 agenda: '<rect x="4" y="5.5" width="16" height="14" rx="2"/><path d="M4 10h16M9 3.5v4M15 3.5v4"/>', 83 entraide: '<path d="M9.2 9.4a2.9 2.9 0 1 1 3.6 2.8V14"/><circle cx="12.4" cy="17.6" r=".6" fill="currentColor"/><circle cx="12" cy="12" r="9"/>', 84 ressource: '<path d="M5 4.5h9l5 5V19.5H5Z"/><path d="M14 4.5v5h5"/><path d="M8.5 13h7M8.5 16h4.5"/>', 85 place: '<path d="M4 6h16M4 12h16M4 18h10"/>', 86 membres: '<circle cx="9" cy="8" r="3.2"/><path d="M3 19.5a6 6 0 0 1 12 0"/><path d="M16.5 6.2a3.2 3.2 0 0 1 0 6"/><path d="M17.6 14.2a6 6 0 0 1 3.4 5.3"/>', 87 plus: '<path d="M12 5v14M5 12h14"/>', 88 coche: '<path d="m5 12.5 4.5 4.5L19 7.5"/>', 89 lieu: '<path d="M12 21s6.5-6 6.5-10.5a6.5 6.5 0 0 0-13 0C5.5 15 12 21 12 21Z"/><circle cx="12" cy="10.4" r="2.4"/>', 90 poubelle: '<path d="M4 7h16M9 7V4.5h6V7M6.5 7l1 12.5h9L17.5 7"/>', 91 lien: '<path d="M10.5 13.5a3.5 3.5 0 0 0 5 0l3-3a3.5 3.5 0 1 0-5-5l-1.5 1.5"/><path d="M13.5 10.5a3.5 3.5 0 0 0-5 0l-3 3a3.5 3.5 0 1 0 5 5L12 17"/>', 92 } 93 function ic(n, t, w) { 94 return '<svg viewBox="0 0 24 24" width="' + (t || 16) + '" height="' + (t || 16) + '" fill="none" stroke="currentColor" ' + 95 'stroke-width="' + (w || 1.7) + '" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">' + (IC[n] || IC.cercle) + '</svg>' 96 } 97 98 var ONGLETS = [ 99 { id: 'cercles', nom: 'Cercles', ic: 'cercle' }, 100 { id: 'evenements', nom: 'Ãvénements', ic: 'agenda' }, 101 { id: 'entraide', nom: 'Entraide', ic: 'entraide' }, 102 { id: 'ressources', nom: 'Ressources', ic: 'ressource' }, 103 { id: 'place', nom: 'Place publique', ic: 'place' }, 104 { id: 'membres', nom: 'Membres', ic: 'membres' }, 105 ] 106 107 function etat() { 108 var st = S() 109 if (!st.communaute || typeof st.communaute !== 'object') st.communaute = {} 110 var c = st.communaute 111 if (!Array.isArray(c.cercles)) c.cercles = [] 112 if (!Array.isArray(c.evenements)) c.evenements = [] 113 if (!Array.isArray(c.entraides)) c.entraides = [] 114 if (!Array.isArray(c.ressources)) c.ressources = [] 115 return c 116 } 117 function moi() { 118 var st = S() 119 return (st.communityProfile && st.communityProfile.name) || st.userName || 'Vous' 120 } 121 function onglet() { 122 var o = etat().onglet 123 return ONGLETS.some(function (x) { return x.id === o }) ? o : 'cercles' 124 } 125 126 /* âââ CERCLES âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ */ 127 function vueCercles() { 128 var c = etat() 129 return `<div class="cm-page"> 130 <div class="cm-barre"> 131 <p class="cm-page-titre">Cercles (${c.cercles.length})</p> 132 <button class="cm-primaire" data-cm="cercle-creer">${ic('plus', 14)} Ouvrir un cercle</button> 133 </div> 134 <p class="cm-note">Un cercle a des membres, une raison dâêtre et une durée. Câest ce qui le distingue dâun sujet de conversation : on peut y entrer, en sortir, et il reste quand vous partez.</p> 135 ${c.cercles.length ? `<div class="cm-grille">${c.cercles.map(function (x) { 136 var dedans = (x.membres || []).indexOf(moi()) >= 0 137 return `<article class="cm-carte cm-cercle"> 138 <span class="cm-pastille">${ic('cercle', 18)}</span> 139 <div class="cm-cercle-corps"> 140 <b>${esc(x.nom)}</b> 141 <span>${esc(x.raison || 'Sans description')}</span> 142 <em>${(x.membres || []).length} membre${(x.membres || []).length > 1 ? 's' : ''} · ouvert ${esc(ilYA(x.createdAt))}</em> 143 </div> 144 <div class="cm-cercle-verbes"> 145 <button class="${dedans ? 'cm-plat' : 'cm-primaire'} cm-mini" data-cm="${dedans ? 'cercle-quitter' : 'cercle-rejoindre'}" data-cm-val="${esc(x.id)}">${dedans ? 'Quitter' : 'Rejoindre'}</button> 146 ${x.auteur === moi() ? `<button class="cm-icone" data-cm="cercle-fermer" data-cm-val="${esc(x.id)}" title="Fermer le cercle">${ic('poubelle', 14)}</button>` : ''} 147 </div> 148 </article>` 149 }).join('')}</div>` : vide('Aucun cercle.', 'Le premier cercle donne le ton : nommez-le par ce quâon y fait, pas par ce dont on y parle.', 'cercle-creer', 'Ouvrir un cercle')} 150 </div>` 151 } 152 153 /* âââ ÃVÃNEMENTS ââââââââââââââââââââââââââââââââââââââââââââââââââââââââ */ 154 function vueEvenements() { 155 var c = etat() 156 var maintenant = Date.now() 157 var tries = c.evenements.slice().sort(function (a, b) { return (a.quand || 0) - (b.quand || 0) }) 158 var aVenir = tries.filter(function (e) { return !e.quand || e.quand >= maintenant }) 159 var passes = tries.filter(function (e) { return e.quand && e.quand < maintenant }).reverse() 160 return `<div class="cm-page"> 161 <div class="cm-barre"> 162 <p class="cm-page-titre">à venir (${aVenir.length})</p> 163 <button class="cm-primaire" data-cm="evenement-creer">${ic('plus', 14)} Proposer</button> 164 </div> 165 ${aVenir.length ? aVenir.map(carteEvenement).join('') 166 : vide('Rien de prévu.', 'Un événement, câest une date que plusieurs personnes tiennent en même temps â la seule chose quâun fil ne peut pas produire.', 'evenement-creer', 'Proposer une date')} 167 ${passes.length ? `<p class="cm-page-titre cm-page-titre-second">Passés</p>${passes.slice(0, 6).map(carteEvenement).join('')}` : ''} 168 </div>` 169 } 170 function carteEvenement(e) { 171 var inscrit = (e.inscrits || []).indexOf(moi()) >= 0 172 var quand = e.quand ? new Date(e.quand) : null 173 var passe = e.quand && e.quand < Date.now() 174 return `<article class="cm-carte cm-evenement${passe ? ' cm-passe' : ''}"> 175 <div class="cm-date"> 176 <b>${quand ? quand.toLocaleDateString('fr-FR', { day: '2-digit' }) : 'â'}</b>
177 <span>${quand ? quand.toLocaleDateString('fr-FR', { month: 'short' }) : ''}</span> 178 </div> 179 <div class="cm-evenement-corps"> 180 <b>${esc(e.titre)}</b> 181 <span>${quand ? esc(quand.toLocaleString('fr-FR', { weekday: 'long', hour: '2-digit', minute: '2-digit' })) : 'Date à fixer'}</span> 182 <em>${e.distanciel ? 'En ligne' : ic('lieu', 12) + ' ' + esc(e.lieu || 'Lieu à préciser')} · ${(e.inscrits || []).length} inscrit${(e.inscrits || []).length > 1 ? 's' : ''}</em> 183 ${e.detail ? `<p>${esc(e.detail)}</p>` : ''} 184 </div> 185 <div class="cm-evenement-verbes"> 186 ${passe ? `<span class="cm-etat">Terminé</span>` : `<button class="${inscrit ? 'cm-plat' : 'cm-primaire'} cm-mini" data-cm="${inscrit ? 'evenement-desinscrire' : 'evenement-inscrire'}" data-cm-val="${esc(e.id)}">${inscrit ? 'Je me retire' : 'Je viens'}</button>`} 187 ${e.auteur === moi() ? `<button class="cm-icone" data-cm="evenement-annuler" data-cm-val="${esc(e.id)}" title="Annuler">${ic('poubelle', 14)}</button>` : ''} 188 </div> 189 </article>` 190 } 191 192 /* âââ ENTRAIDE ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 193 * UNE QUESTION N'EST PAS UNE PUBLICATION. Elle a un état â ouverte ou 194 * résolue â et une réponse RETENUE. C'est ce qui la rend utile à quelqu'un 195 * qui arrivera dans six mois, alors qu'une publication ne sert qu'à ceux qui 196 * passaient là ce jour-là . */ 197 function vueEntraide() { 198 var c = etat() 199 var filtre = c.entraideFiltre || 'ouvertes' 200 var liste = c.entraides.slice().sort(function (a, b) { return (b.createdAt || 0) - (a.createdAt || 0) }) 201 if (filtre === 'ouvertes') liste = liste.filter(function (q) { return !q.resolue }) 202 else if (filtre === 'resolues') liste = liste.filter(function (q) { return q.resolue }) 203 else if (filtre === 'miennes') liste = liste.filter(function (q) { return q.auteur === moi() }) 204 return `<div class="cm-page"> 205 <div class="cm-barre"> 206 <p class="cm-page-titre">Entraide</p> 207 <button class="cm-primaire" data-cm="question-poser">${ic('plus', 14)} Poser une question</button> 208 </div> 209 <div class="cm-filtres"> 210 ${[['ouvertes', 'Sans réponse'], ['resolues', 'Résolues'], ['miennes', 'Les miennes'], ['toutes', 'Toutes']].map(function (f) { 211 return `<button class="cm-puce${filtre === f[0] ? ' on' : ''}" data-cm="entraide-filtre" data-cm-val="${f[0]}">${f[1]}</button>` 212 }).join('')} 213 </div> 214 ${liste.length ? liste.map(function (q) { 215 var ouverte = c.questionOuverte === q.id 216 var reponses = q.reponses || [] 217 return `<article class="cm-carte cm-question${q.resolue ? ' cm-resolue' : ''}"> 218 <header> 219 <b>${esc(q.titre)}</b> 220 <span class="cm-etat">${q.resolue ? ic('coche', 12) + ' Résolue' : reponses.length + ' réponse' + (reponses.length > 1 ? 's' : '')}</span> 221 </header> 222 ${q.detail ? `<p class="cm-question-detail">${esc(q.detail)}</p>` : ''} 223 <p class="cm-meta">${esc(q.auteur)} · ${esc(ilYA(q.createdAt))}</p> 224 <footer> 225 <button class="cm-plat cm-mini" data-cm="question-ouvrir" data-cm-val="${esc(q.id)}">${ouverte ? 'Replier' : 'Répondre / lire'}</button> 226 ${q.auteur === moi() ? `<button class="cm-icone" data-cm="question-supprimer" data-cm-val="${esc(q.id)}" title="Supprimer">${ic('poubelle', 14)}</button>` : ''} 227 </footer> 228 ${ouverte ? `<div class="cm-reponses"> 229 ${reponses.length ? reponses.map(function (r) { 230 return `<div class="cm-reponse${q.retenue === r.id ? ' cm-retenue' : ''}"> 231 <b>${esc(r.qui)}</b><span>${esc(r.texte)}</span><em>${esc(ilYA(r.createdAt))}</em> 232 ${q.auteur === moi() && !q.resolue ? `<button class="cm-plat cm-mini" data-cm="reponse-retenir" data-cm-val="${esc(q.id)}" data-cm-val2="${esc(r.id)}">Retenir</button>` : ''}
233 ${q.retenue === r.id ? `<span class="cm-etat">${ic('coche', 12)} Retenue</span>` : ''} 234 </div>` 235 }).join('') : `<p class="cm-vide-mini">Personne nâa encore répondu.</p>`} 236 <div class="cm-reponse-saisie"> 237 <input id="cm-reponse-${esc(q.id)}" placeholder="Votre réponseâ¦" /> 238 <button class="cm-primaire cm-mini" data-cm="repondre" data-cm-val="${esc(q.id)}">Répondre</button> 239 </div> 240 </div>` : ''} 241 </article>` 242 }).join('') : vide(filtre === 'ouvertes' ? 'Aucune question en attente.' : 'Rien ici.', 243 'Une question posée ici reste visible tant que personne nây a répondu. Câest exactement lâinverse dâun fil.', 244 'question-poser', 'Poser une question')} 245 </div>` 246 } 247 248 /* âââ RESSOURCES ââââââââââââââââââââââââââââââââââââââââââââââââââââââââ */ 249 function vueRessources() { 250 var c = etat() 251 var liste = c.ressources.slice().sort(function (a, b) { return (b.utiles || 0) - (a.utiles || 0) }) 252 return `<div class="cm-page"> 253 <div class="cm-barre"> 254 <p class="cm-page-titre">Ressources (${liste.length})</p> 255 <button class="cm-primaire" data-cm="ressource-ajouter">${ic('plus', 14)} Partager</button> 256 </div> 257 <p class="cm-note">Ce que le groupe a trouvé et gardé. Classé par ce qui a servi, pas par ce qui est récent â une bonne ressource ne vieillit pas en trois jours.</p> 258 ${liste.length ? liste.map(function (r) { 259 var vote = (r.votants || []).indexOf(moi()) >= 0 260 var url = /^https?:\/\//i.test(String(r.lien || '')) ? String(r.lien) : '' 261 return `<article class="cm-carte cm-ressource"> 262 <span class="cm-pastille">${ic('ressource', 16)}</span> 263 <div class="cm-ressource-corps"> 264 <b>${esc(r.titre)}</b> 265 ${url ? `<a href="${esc(url)}" target="_blank" rel="noopener noreferrer">${ic('lien', 12)} ${esc(r.lien)}</a>` : ''} 266 ${r.note ? `<span>${esc(r.note)}</span>` : ''} 267 <em>partagé par ${esc(r.qui)} · ${esc(ilYA(r.createdAt))}</em> 268 </div> 269 <div class="cm-ressource-verbes"> 270 <button class="cm-plat cm-mini${vote ? ' on' : ''}" data-cm="ressource-utile" data-cm-val="${esc(r.id)}">${ic('coche', 12)} Utile ${r.utiles || 0}</button> 271 ${r.qui === moi() ? `<button class="cm-icone" data-cm="ressource-retirer" data-cm-val="${esc(r.id)}" title="Retirer">${ic('poubelle', 14)}</button>` : ''} 272 </div> 273 </article>` 274 }).join('') : vide('Aucune ressource.', 'La première ressource partagée est celle quâon aurait aimé trouver soi-même le premier jour.', 'ressource-ajouter', 'Partager une ressource')} 275 </div>` 276 } 277 278 function vide(titre, sous, geste, libelle) { 279 return `<div class="cm-vide"> 280 <p class="cm-vide-titre">${esc(titre)}</p> 281 <p class="cm-vide-sous">${esc(sous)}</p> 282 ${geste ? `<button class="cm-primaire" data-cm="${geste}">${esc(libelle || 'Commencer')}</button>` : ''} 283 </div>` 284 } 285 286 /* âââ LA COQUILLE âââââââââââââââââââââââââââââââââââââââââââââââââââââââ 287 * `place` et `membres` sont rendus PAR LE TRONC. Ce sont eux qui parlent au 288 * serveur (publication, médias signés, messages privés, appels) : en 289 * réécrire une version ici aurait fait deux fils à tenir d'accord, et c'est 290 * toujours celui qu'on ne regarde pas qui devient faux. */ 291 function rendre() { 292 var st = S() 293 if (partage() && !st.communityRemoteLoaded && !st.communityRemoteLoading && typeof window.loadCommunityRemote === 'function') { 294 setTimeout(function () { window.loadCommunityRemote({ toast: false }) }, 0) 295 } 296 var o = onglet() 297 var corps 298 if (o === 'evenements') corps = vueEvenements() 299 else if (o === 'entraide') corps = vueEntraide() 300 else if (o === 'ressources') corps = vueRessources() 301 else if (o === 'place') { 302 corps = typeof window.renderCommunityFeed === 'function' 303 ? window.renderCommunityFeed(moi(), st.communityProfile || {}) 304 : `<div class="cm-vide"><p class="cm-vide-titre">La place publique est indisponible.</p></div>` 305 } else if (o === 'membres') { 306 corps = typeof window.renderCommunityMembers === 'function' 307 ? window.renderCommunityMembers() 308 : `<div class="cm-vide"><p class="cm-vide-titre">La liste des membres est indisponible.</p></div>` 309 } else corps = vueCercles() 310 311 var entete = typeof window._espaceMiniHeader === 'function' 312 ? window._espaceMiniHeader('Communauté', '', '') 313 : `<div class="cm-entete-repli">Communauté</div>` 314 /* PAS DE BARRE DE QUESTIONS ICI NON PLUS â 188 px, 26 % de l'écran, posée 315 * par-dessus ce qu'on est venu lire. Même retrait, même raison mesurée que 316 * Découvrir, Finance, le Directeur financier, le Marché et le Réseau. */ 317 var modale = (st.communityChatWith && typeof window.renderCommunityChatModal === 'function') 318 ? window.renderCommunityChatModal() : '' 319 return `<main class="cm-espace flex flex-1 flex-col overflow-hidden min-w-0" style="background:var(--bg-primary)"> 320 ${entete} 321 <nav class="cm-onglets"> 322 ${ONGLETS.map(function (x) { 323 var n = x.id === 'cercles' ? etat().cercles.length 324 : x.id === 'evenements' ? etat().evenements.filter(function (e) { return !e.quand || e.quand >= Date.now() }).length 325 : x.id === 'entraide' ? etat().entraides.filter(function (q) { return !q.resolue }).length 326 : x.id === 'ressources' ? etat().ressources.length 327 : 0 328 return `<button class="cm-onglet${x.id === o ? ' on' : ''}" data-cm="onglet" data-cm-val="${x.id}">${ic(x.ic, 17)}<span>${x.nom}</span>${n ? `<b>${n}</b>` : ''}</button>` 329 }).join('')} 330 <button class="cm-onglet cm-onglet-sortie" data-cm="ouvrir-mode" data-cm-val="network" title="Retour au Réseau">Réseau â</button> 331 </nav> 332 ${!partage() && (o === 'place' || o === 'membres') ? `<p class="cm-avis">Espace local : connectez-vous pour publier et voir les autres membres.</p>` : ''} 333 <div class="cm-defilement flex-1 overflow-y-auto min-h-0">${corps}</div> 334 ${modale} 335 </main>` 336 } 337 338 /* âââ LES GESTES ââââââââââââââââââââââââââââââââââââââââââââââââââââââââ */ 339 function cible(e) { 340 var n = e.target 341 while (n && n !== document) { 342 if (n.getAttribute && n.getAttribute('data-cm')) return n 343 n = n.parentNode 344 } 345 return null 346 } 347 async function demander(titre, exemple, multiligne) { 348 if (typeof window.promptModal !== 'function') return null 349 return window.promptModal(titre, exemple, '', 'Valider', { multiline: !!multiligne }) 350 } 351 352 async function agir(geste, val, val2) { 353 var c = etat() 354 switch (geste) { 355 case 'onglet': c.onglet = val; garder(); redessiner(); return
356 case 'ouvrir-mode': if (typeof window.openModeTab === 'function') window.openModeTab(val); return 357 358 /* ââ CERCLES ââââââââââââââââââââââââââââââââââââââââââââââââââââââ */ 359 case 'cercle-creer': { 360 var nom = await demander('Nom du cercle', 'ex. Comptables dâÃle-de-France') 361 if (!nom || !String(nom).trim()) return 362 var raison = await demander('à quoi sert ce cercle ?', 'ce quâon y fait, en une phrase') 363 c.cercles.unshift({ 364 id: idNeuf(), nom: String(nom).trim(), raison: String(raison || '').trim(), 365 auteur: moi(), membres: [moi()], createdAt: Date.now(), 366 }) 367 garder(); redessiner(); return 368 } 369 case 'cercle-rejoindre': { 370 var x1 = c.cercles.find(function (x) { return x.id === val }); if (!x1) return 371 x1.membres = x1.membres || [] 372 if (x1.membres.indexOf(moi()) < 0) x1.membres.push(moi()) 373 garder(); redessiner(); return 374 } 375 case 'cercle-quitter': { 376 var x2 = c.cercles.find(function (x) { return x.id === val }); if (!x2) return 377 x2.membres = (x2.membres || []).filter(function (m) { return m !== moi() }) 378 garder(); redessiner(); return 379 } 380 case 'cercle-fermer': { 381 var x3 = c.cercles.find(function (x) { return x.id === val }); if (!x3) return 382 var ok = typeof window.confirmModal === 'function' 383 ? await window.confirmModal('Fermer le cercle « ' + esc(x3.nom) + ' » ?', { confirmLabel: 'Fermer', danger: true }) 384 : true 385 if (!ok) return 386 c.cercles = c.cercles.filter(function (x) { return x.id !== val }) 387 garder(); redessiner(); return 388 } 389 390 /* ââ ÃVÃNEMENTS âââââââââââââââââââââââââââââââââââââââââââââââââââ */ 391 case 'evenement-creer': { 392 var titre = await demander('Titre de lâévénement', 'ex. Permanence entraide â jeudi soir') 393 if (!titre || !String(titre).trim()) return 394 var date = await demander('Quand ? (JJ/MM/AAAA HH:MM)', 'ex. 12/09/2026 18:30') 395 var lieu = await demander('Où ? (vide = en ligne)', 'ex. Cergy, salle 2') 396 c.evenements.unshift({ 397 id: idNeuf(), titre: String(titre).trim(), quand: lireDate(date), 398 lieu: String(lieu || '').trim(), distanciel: !String(lieu || '').trim(), 399 auteur: moi(), inscrits: [moi()], createdAt: Date.now(), 400 }) 401 garder(); redessiner(); return 402 } 403 case 'evenement-inscrire': { 404 var e1 = c.evenements.find(function (x) { return x.id === val }); if (!e1) return 405 e1.inscrits = e1.inscrits || [] 406 if (e1.inscrits.indexOf(moi()) < 0) e1.inscrits.push(moi()) 407 garder(); redessiner(); return 408 } 409 case 'evenement-desinscrire': { 410 var e2 = c.evenements.find(function (x) { return x.id === val }); if (!e2) return 411 e2.inscrits = (e2.inscrits || []).filter(function (m) { return m !== moi() }) 412 garder(); redessiner(); return 413 } 414 case 'evenement-annuler': 415 c.evenements = c.evenements.filter(function (x) { return x.id !== val }) 416 garder(); redessiner(); return 417 418 /* ââ ENTRAIDE âââââââââââââââââââââââââââââââââââââââââââââââââââââ */ 419 case 'entraide-filtre': c.entraideFiltre = val; redessiner(); return 420 case 'question-poser': { 421 var qt = await demander('Votre question', 'ex. Comment justifier un écart de caisse ?') 422 if (!qt || !String(qt).trim()) return 423 var qd = await demander('Le contexte (facultatif)', 'ce que vous avez déjà essayé', true) 424 c.entraides.unshift({ 425 id: idNeuf(), titre: String(qt).trim(), detail: String(qd || '').trim(), 426 auteur: moi(), reponses: [], resolue: false, createdAt: Date.now(), 427 }) 428 c.questionOuverte = c.entraides[0].id 429 garder(); redessiner(); return 430 } 431 case 'question-ouvrir': 432 c.questionOuverte = c.questionOuverte === val ? '' : val 433 redessiner() 434 setTimeout(function () { var el = document.getElementById('cm-reponse-' + val); if (el) el.focus() }, 0) 435 return 436 case 'question-supprimer': 437 c.entraides = c.entraides.filter(function (x) { return x.id !== val }) 438 garder(); redessiner(); return 439 case 'repondre': { 440 var champ = document.getElementById('cm-reponse-' + val) 441 var texte = champ && champ.value ? champ.value.trim() : '' 442 if (!texte) return 443 var q1 = c.entraides.find(function (x) { return x.id === val }); if (!q1) return 444 q1.reponses = q1.reponses || [] 445 q1.reponses.push({ id: idNeuf(), qui: moi(), texte: texte, createdAt: Date.now() }) 446 if (champ) champ.value = '' 447 garder(); redessiner(); return 448 } 449 case 'reponse-retenir': { 450 var q2 = c.entraides.find(function (x) { return x.id === val }); if (!q2) return 451 q2.retenue = val2 452 q2.resolue = true 453 dire('Question refermée sur une réponse. Elle restera lisible.', 'success', 'â ') 454 garder(); redessiner(); return 455 } 456 457 /* ââ RESSOURCES âââââââââââââââââââââââââââââââââââââââââââââââââââ */ 458 case 'ressource-ajouter': { 459 var rt = await demander('Titre de la ressource', 'ex. Le guide du plan comptable, en clair') 460 if (!rt || !String(rt).trim()) return 461 var rl = await demander('Lien (facultatif)', 'https://â¦') 462 var rn = await demander('Pourquoi câest utile ? (facultatif)', 'en une phrase') 463 c.ressources.unshift({ 464 id: idNeuf(), titre: String(rt).trim(), lien: String(rl || '').trim(), 465 note: String(rn || '').trim(), qui: moi(), utiles: 0, votants: [], createdAt: Date.now(), 466 }) 467 garder(); redessiner(); return 468 } 469 case 'ressource-utile': { 470 var r1 = c.ressources.find(function (x) { return x.id === val }); if (!r1) return 471 r1.votants = r1.votants || [] 472 var i = r1.votants.indexOf(moi()) 473 if (i >= 0) { r1.votants.splice(i, 1); r1.utiles = Math.max(0, (r1.utiles || 1) - 1) } 474 else { r1.votants.push(moi()); r1.utiles = (r1.utiles || 0) + 1 } 475 garder(); redessiner(); return 476 } 477 case 'ressource-retirer': 478 c.ressources = c.ressources.filter(function (x) { return x.id !== val }) 479 garder(); redessiner(); return 480 } 481 } 482 483 /* « 12/09/2026 18:30 » â LU, PAS DEVINÃ. `new Date('12/09/2026')` lit un mois
484 * de décembre en anglais : la même chaîne donnerait septembre ici et décembre 485 * ailleurs, et personne ne le verrait avant le jour de l'événement. */ 486 function lireDate(texte) { 487 var s = String(texte || '').trim() 488 if (!s) return 0 489 var m = s.match(/^(\d{1,2})[/.\-](\d{1,2})[/.\-](\d{4})(?:\s+(\d{1,2})[:h](\d{2}))?$/) 490 if (!m) { var t = Date.parse(s); return Number.isFinite(t) ? t : 0 } 491 var d = new Date(Number(m[3]), Number(m[2]) - 1, Number(m[1]), Number(m[4] || 9), Number(m[5] || 0)) 492 return d.getTime() 493 } 494 495 function brancher() { 496 if (brancher.fait) return 497 brancher.fait = true 498 document.addEventListener('click', function (e) { 499 var el = cible(e) 500 if (!el) return 501 e.preventDefault() 502 Promise.resolve(agir(el.getAttribute('data-cm'), el.getAttribute('data-cm-val') || '', el.getAttribute('data-cm-val2') || '')) 503 .catch(function (err) { dire('Geste impossible : ' + esc(err && err.message), 'error', 'â ï¸') }) 504 }) 505 document.addEventListener('keydown', function (e) { 506 if (e.key !== 'Enter' || e.shiftKey) return 507 var t = e.target 508 if (!t || !t.id || t.id.indexOf('cm-reponse-') !== 0) return 509 e.preventDefault() 510 agir('repondre', t.id.slice('cm-reponse-'.length), '') 511 }) 512 } 513 514 if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', brancher) 515 else brancher() 516 517 window.CREA_COMMUNAUTE = { 518 rendre: rendre, 519 onglets: ONGLETS, 520 ouvrirOnglet: function (id) { etat().onglet = id; garder(); redessiner() }, 521 /* Ce que le Discover des Espaces vient chercher : les prochaines dates et 522 * les questions qui attendent encore quelqu'un. */ 523 prochains: function (n) { 524 return etat().evenements 525 .filter(function (e) { return !e.quand || e.quand >= Date.now() }) 526 .sort(function (a, b) { return (a.quand || 0) - (b.quand || 0) }) 527 .slice(0, n || 3) 528 }, 529 questionsOuvertes: function (n) { 530 return etat().entraides.filter(function (q) { return !q.resolue }).slice(0, n || 3) 531 }, 532 } 533})()
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.