1/** 2 * nmc-reels-player.js â NMC Reels Player (Web Component Shadow DOM) 3 * VAP 2.8 â Mobile fullscreen scroll-snap reels · autoplay · share · video responses 4 * 5 * Usage: 6 * <nmc-reels-player id="nmcReelsPlayer"></nmc-reels-player> 7 * 8 * // Depuis renderPage (après initReelsMode exposé via window) : 9 * if (filteredMedia.length > 0) initReelsMode(filteredMedia); 10 * 11 * Ãvénements émis (bubbles + composed) : 12 * nmc-reels-enter â { mediaCount } â mode Reels activé 13 * nmc-reels-exit â {} â mode Reels quitté 14 * nmc-track â { type: 'reel_view'|'reel_share', data: { mediaId } } 15 * 16 * API publique (élément) : 17 * player.initReelsMode(mediaList) â init + autoenter mobile 18 * player.enterReelsMode() â passe en plein écran 19 * player.exitReelsMode() â quitte le plein écran 20 * player.renderReels() â re-render les items 21 * player.reelsWrapper â getter â #reelsWrapper (shadow) 22 * player.reelsContainer â getter â #reelsContainer (shadow) 23 * 24 * Globals exposés (backward compat onclick handlers + scripts externes) : 25 * window.initReelsMode, enterReelsMode, exitReelsMode, renderReels 26 * window.toggleReelVideo, autoplayVisibleReelVideo, updateGalleryCounter 27 * window.onReelsScroll, copyReelLink, navigateToTag, filterByPlatform 28 * window.scrollToMedia, showResponsesFeed, exitResponsesFeed, goToParentVideo 29 * window.openSubmitModal, closeSubmitModal, handleVideoSelect, removeSelectedVideo 30 * window.openResponseTerms, closeResponseTerms, acceptResponseTerms 31 * window.submitResponse, shareReel 32 * 33 * Dépendances : 34 * window.escapeHtml, getPlatformEmoji, isMobile (nmc-page-utils.js) 35 * window.trackAd, currentAds, playWithPreroll, stopAllPrerolls, 36 * refreshPrerollListeners (nmc-ads-engine.js) 37 * window.enqueueEnrichment (nmc-ai-enrichment.js) 38 * window.username, API_URL (page.html inline script) 39 * 40 * CSS global conservé dans page.css (affecte le light DOM) : 41 * .reels-mode .container, .reels-mode .site-header, etc. 42 */ 43 44(function () { 45 'use strict'; 46 47 // ============================================================ 48 // CSS Shadow DOM â tout le CSS reels/responses/modals 49 // ============================================================ 50 const REELS_CSS = /* css */` 51 :host { 52 display: block; 53 pointer-events: none; 54 } 55 56 /* ââ Reels container (caché par défaut, affiché en reels-mode) ââ */ 57 .reels-container { 58 display: none; 59 pointer-events: auto; 60 } 61 62 :host-context(.reels-mode) .reels-container { 63 display: block; 64 position: fixed; 65 top: 0; 66 left: 0; 67 right: 0; 68 bottom: 0; 69 overflow: hidden; 70 background: #000; 71 z-index: 9999; 72 } 73 74 /* ââ Responses header âââââââââââââââââââââââââââââââââââââââ */ 75 .responses-header { 76 display: none; 77 position: fixed; 78 top: 0; 79 left: 0; 80 right: 0; 81 z-index: 110; 82 padding: 15px 20px; 83 padding-top: max(15px, env(safe-area-inset-top)); 84 background: rgba(0,0,0,0.8); 85 backdrop-filter: blur(10px); 86 align-items: center; 87 justify-content: space-between; 88 pointer-events: auto; 89 } 90 .responses-header.active { 91 display: flex; 92 } 93 .responses-back-btn { 94 display: flex; 95 align-items: center; 96 gap: 8px; 97 color: white; 98 background: rgba(255,255,255,0.15); 99 border: none; 100 border-radius: 20px; 101 padding: 8px 16px; 102 font-size: 0.9rem; 103 font-weight: 600; 104 cursor: pointer; 105 font-family: inherit; 106 } 107 108 /* ââ Reels wrapper (scroll-snap vertical) âââââââââââââââââââ */ 109 .reels-wrapper { 110 height: 100%; 111 overflow-y: scroll; 112 scroll-snap-type: y mandatory; 113 -webkit-overflow-scrolling: touch; 114 scrollbar-width: none; 115 -ms-overflow-style: none; 116 } 117 .reels-wrapper::-webkit-scrollbar {
118 display: none; 119 } 120 121 /* ââ Reel item âââââââââââââââââââââââââââââââââââââââââââââââ */ 122 .reel-item { 123 height: 100vh; 124 height: 100dvh; 125 width: 100%; 126 scroll-snap-align: start; 127 scroll-snap-stop: always; 128 position: relative; 129 display: flex; 130 align-items: center; 131 justify-content: center; 132 background: #000; 133 } 134 .reel-item .reel-media { 135 position: absolute; 136 top: 0; left: 0; right: 0; bottom: 0; 137 display: flex; 138 align-items: center; 139 justify-content: center; 140 z-index: 1; 141 cursor: pointer; 142 } 143 .reel-item .reel-media.reel-upload { 144 display: flex; 145 align-items: center; 146 justify-content: center; 147 z-index: 5; 148 } 149 .reel-item .reel-media.reel-upload video { 150 width: 100%; height: 100%; 151 max-width: 100vw; 152 max-height: 100vh; max-height: 100dvh; 153 object-fit: contain; 154 position: relative; z-index: 5; 155 } 156 .reel-play-indicator { 157 position: absolute; 158 top: 50%; left: 50%; 159 transform: translate(-50%, -50%); 160 width: 80px; height: 80px; 161 background: rgba(0,0,0,0.6); 162 border-radius: 50%; 163 display: flex; align-items: center; justify-content: center; 164 font-size: 2rem; color: white; 165 opacity: 0; transition: opacity 0.3s; 166 pointer-events: none; z-index: 20; 167 } 168 .reel-play-indicator.visible { opacity: 1; } 169 .reel-item video { pointer-events: none; } 170 .reel-item iframe { 171 width: 100%; height: 100%; 172 max-height: 100vh; max-height: 100dvh; 173 border: none; 174 } 175 .reel-item.vertical iframe { width: 100%; height: 100%; object-fit: contain; } 176 .reel-item.horizontal iframe { 177 width: 100%; height: auto; 178 aspect-ratio: 16/9; max-height: 50vh; 179 } 180 181 /* ââ Reel info overlay (bas) âââââââââââââââââââââââââââââââââ */ 182 .reel-info { 183 position: absolute; 184 bottom: 0; left: 0; right: 0; 185 padding: 80px 15px 30px; 186 background: linear-gradient(to top, rgba(0,0,0,0.9) 0%, rgba(0,0,0,0.6) 50%, transparent 100%); 187 color: white; 188 z-index: 10; 189 pointer-events: none; 190 } 191 .reel-info * { pointer-events: auto; } 192 .reel-title { 193 font-size: 1rem; font-weight: 700; margin-bottom: 8px; line-height: 1.3; 194 display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; 195 } 196 .reel-description { 197 font-size: 0.85rem; color: rgba(255,255,255,0.8); margin-bottom: 10px; 198 display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; 199 } 200 .reel-tags { display: flex; flex-wrap: wrap; gap: 6px; } 201 .reel-tag { 202 padding: 4px 10px; 203 background: rgba(255,255,255,0.2); 204 border-radius: 12px; font-size: 0.75rem; color: white; text-decoration: none; 205 } 206 .reel-platform { 207 display: inline-block; padding: 6px 14px; 208 background: rgba(255,255,255,0.2); border-radius: 20px; 209 color: white; font-size: 0.8rem; font-weight: 600; margin-bottom: 10px; 210 text-decoration: none; cursor: pointer; 211 } 212 .reel-platform:hover { background: rgba(255,255,255,0.3); } 213 .reel-platform-badge { 214 display: inline-block; 215 background: rgba(255,255,255,0.15); padding: 6px 14px; border-radius: 20px; 216 font-size: 0.8rem; color: white; text-decoration: none; margin-bottom: 10px; 217 backdrop-filter: blur(5px); cursor: pointer; 218 } 219 .reel-platform-badge:hover { background: rgba(255,255,255,0.25); } 220 221 /* ââ Reels header (logo + créateur + bouton fermer) âââââââââââ */ 222 .reels-header { 223 position: fixed; 224 top: 0; left: 0; right: 0; 225 padding: 15px; 226 display: flex; justify-content: space-between; align-items: center; 227 z-index: 100; 228 background: linear-gradient(to bottom, rgba(0,0,0,0.6) 0%, transparent 100%); 229 pointer-events: none; 230 } 231 .reels-header > * { pointer-events: auto; } 232 .reels-back { 233 width: 40px; height: 40px; 234 background: rgba(255,255,255,0.2); 235 border: none; border-radius: 50%; 236 color: white; font-size: 1.3rem; cursor: pointer; 237 backdrop-filter: blur(10px); 238 display: flex; align-items: center; justify-content: center; 239 } 240 .reels-logo { 241 width: 40px; height: 40px; 242 display: flex; align-items: center; justify-content: center; 243 }
244 .reels-logo img { width: 35px; height: 35px; border-radius: 8px; object-fit: cover; } 245 .reels-creator { 246 display: flex; align-items: center; gap: 10px; 247 color: white; text-decoration: none; 248 } 249 .reels-creator img { width: 35px; height: 35px; border-radius: 50%; object-fit: cover; } 250 .reels-creator span { font-weight: 600; font-size: 0.9rem; } 251 252 /* ââ Bouton share ââââââââââââââââââââââââââââââââââââââââââââââ */ 253 .reels-share-btn { 254 position: fixed; 255 bottom: 30px; right: 20px; 256 width: 50px; height: 50px; 257 background: rgba(255,255,255,0.2); 258 border-radius: 50%; 259 display: flex; align-items: center; justify-content: center; 260 cursor: pointer; z-index: 100; 261 backdrop-filter: blur(10px); 262 border: 2px solid rgba(255,255,255,0.3); 263 transition: all 0.3s; 264 pointer-events: auto; 265 } 266 .reels-share-btn:hover { background: rgba(255,255,255,0.3); transform: scale(1.1); } 267 .reels-share-btn.copied { background: #10B981; border-color: #10B981; } 268 .reels-share-btn img { width: 30px; height: 30px; border-radius: 50%; object-fit: cover; } 269 270 /* ââ Boutons réponses fixes ââââââââââââââââââââââââââââââââââââ */ 271 .fixed-response-actions { 272 position: fixed; 273 bottom: 140px; right: 15px; 274 z-index: 150; 275 display: flex; flex-direction: column; gap: 6px; 276 pointer-events: auto; 277 } 278 .fixed-action-btn { 279 touch-action: manipulation; 280 -webkit-tap-highlight-color: transparent; 281 user-select: none; -webkit-user-select: none; 282 display: flex; flex-direction: row; align-items: center; gap: 4px; 283 background: rgba(255,255,255,0.15); 284 backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px); 285 border: none; border-radius: 15px; padding: 6px 12px; 286 cursor: pointer; color: white; font-size: 0.75rem; 287 } 288 .fixed-action-btn:active { 289 transform: scale(0.95); 290 background: rgba(139,92,246,0.8); 291 } 292 .fixed-action-btn .action-icon { font-size: 0.9rem; } 293 .fixed-action-btn .action-label { font-size: 0.7rem; font-weight: 600; } 294 295 /* ââ Compteur reel âââââââââââââââââââââââââââââââââââââââââââââ */ 296 .reel-counter { 297 position: fixed; 298 top: 70px; left: 50%; 299 transform: translateX(-50%); 300 background: rgba(0,0,0,0.5); 301 padding: 5px 12px; border-radius: 15px; 302 font-size: 0.8rem; color: white; 303 z-index: 100; backdrop-filter: blur(10px); 304 pointer-events: none; 305 } 306 307 /* ââ Pub in-feed âââââââââââââââââââââââââââââââââââââââââââââââ */ 308 .reel-item.reel-ad { 309 background: #000; 310 display: flex; flex-direction: column; align-items: center; justify-content: center; 311 position: relative; 312 } 313 .reel-item.reel-ad .reel-info { 314 position: absolute; bottom: 20px; left: 0; right: 0; 315 text-align: center; padding: 0; background: none; 316 } 317 .reel-ad-content { 318 position: absolute; top: 0; left: 0; right: 0; bottom: 0; 319 display: flex; align-items: center; justify-content: center; 320 width: 100%; height: 100%; padding: 20px; 321 } 322 .reel-ad-content a { display: block; width: 100%; max-width: 100%; } 323 .reel-ad-content img, .reel-ad-content video { 324 width: 100%; height: 100%; object-fit: cover; border-radius: 0; 325 } 326 327 /* ââ Overlay pub dans reel âââââââââââââââââââââââââââââââââââââ */ 328 .reel-overlay-ad { 329 position: absolute; bottom: 200px; right: 10px; z-index: 50; display: block; 330 } 331 .reel-overlay-ad.top-left { top: 10px; left: 10px; right: auto; bottom: auto; } 332 .reel-overlay-ad.top-right { top: 10px; right: 10px; left: auto; bottom: auto; } 333 .reel-overlay-ad.bottom-left { bottom: 200px; left: 10px; top: auto; right: auto; } 334 .reel-overlay-ad.bottom-right{ bottom: 200px; right: 10px; top: auto; left: auto; } 335 .reel-overlay-ad img { 336 max-width: 100px; border-radius: 8px; box-shadow: 0 4px 15px rgba(0,0,0,0.4); 337 } 338 .reel-overlay-close { 339 position: absolute; top: -8px; right: -8px; 340 width: 22px; height: 22px; 341 background: rgba(0,0,0,0.7); border: none; border-radius: 50%; 342 color: white; font-size: 0.7rem; cursor: pointer; 343 display: flex; align-items: center; justify-content: center; 344 } 345 346 /* ââ Placeholder reel ââââââââââââââââââââââââââââââââââââââââââ */ 347 .reel-placeholder { 348 width: 100%; height: 100%; 349 display: flex; flex-direction: column; align-items: center; justify-content: center; 350 background: linear-gradient(135deg, var(--bg-elevated, #252542), var(--bg-card, #1A1A2E)); 351 cursor: pointer; 352 } 353 .reel-placeholder .play-icon { 354 width: 80px; height: 80px; 355 background: rgba(255,255,255,0.9); border-radius: 50%; 356 display: flex; align-items: center; justify-content: center; 357 margin-bottom: 15px; 358 } 359 .reel-placeholder .play-icon svg { width: 35px; height: 35px; fill: var(--primary, #8B5CF6); margin-left: 5px; } 360 361 /* ââ Side actions (désactivé, boutons fixes utilisés) âââââââââââ */ 362 .reel-actions { 363 position: absolute; right: 10px; bottom: 180px; 364 z-index: 100; display: flex; flex-direction: column; gap: 18px; align-items: center; 365 pointer-events: auto; 366 } 367 .reel-action-btn { 368 display: flex; flex-direction: column; align-items: center; gap: 4px; 369 background: none; border: none; cursor: pointer; color: white; 370 pointer-events: auto; text-decoration: none; touch-action: manipulation; 371 padding: 10px; margin: -10px; position: relative; z-index: 101; 372 } 373 .reel-action-btn .action-icon { 374 width: 46px; height: 46px; 375 background: rgba(255,255,255,0.15); backdrop-filter: blur(10px); 376 border-radius: 50%; display: flex; align-items: center; justify-content: center; 377 font-size: 1.3rem; transition: all 0.3s; 378 } 379 .reel-action-btn:hover .action-icon, .reel-action-btn:active .action-icon { 380 background: rgba(255,255,255,0.25); transform: scale(1.1); 381 } 382 .reel-action-btn .action-count { font-size: 0.7rem; font-weight: 600; }
383 @keyframes pulse-responses { 384 0%, 100% { box-shadow: 0 0 0 0 rgba(139,92,246,0.5); } 385 50% { box-shadow: 0 0 0 8px rgba(139,92,246,0); } 386 } 387 388 /* ââ Response badge sur vidéo ââââââââââââââââââââââââââââââââââ */ 389 .response-badge { 390 display: flex; align-items: center; gap: 6px; 391 background: rgba(139,92,246,0.3); padding: 6px 12px; border-radius: 15px; 392 font-size: 0.8rem; margin-bottom: 10px; width: fit-content; cursor: pointer; 393 } 394 .response-badge:hover { background: rgba(139,92,246,0.5); } 395 396 /* ââ Articles en mode Reels ââââââââââââââââââââââââââââââââââââ */ 397 .reel-article { 398 display: flex; flex-direction: column; height: 100%; 399 background: linear-gradient(180deg, #1A1A1A 0%, #252525 100%); 400 position: relative; overflow: hidden; 401 } 402 .reel-article-scroll { flex: 1; padding: 60px 20px 20px; overflow-y: auto; } 403 .reel-article-footer { 404 position: absolute; bottom: 0; left: 0; right: 0; 405 padding: 60px 20px 25px; 406 background: linear-gradient(to top, rgba(26,26,26,0.95) 0%, rgba(26,26,26,0.7) 50%, transparent 100%); 407 z-index: 5; 408 } 409 .reel-article-thumbnail { width: 100%; max-height: 300px; object-fit: cover; border-radius: 12px; margin-bottom: 20px; } 410 .reel-article-title { font-size: 1.8rem; font-weight: 700; color: var(--primary-light, #A78BFA); margin-bottom: 20px; line-height: 1.2; word-wrap: break-word; } 411 .reel-article-content { color: white; font-size: 1.1rem; line-height: 1.8; word-wrap: break-word; overflow-wrap: break-word; padding-bottom: 100px; } 412 .reel-article-content h2 { font-size: 1.4rem; color: var(--primary-light, #A78BFA); margin: 20px 0 10px; } 413 .reel-article-content h3 { font-size: 1.2rem; color: #ccc; margin: 15px 0 8px; } 414 .reel-article-content p { margin-bottom: 15px; word-wrap: break-word; } 415 .reel-article-content ul, .reel-article-content ol { margin: 15px 0; padding-left: 25px; } 416 .reel-article-content li { margin-bottom: 8px; } 417 .reel-article-content a { color: var(--primary-light, #A78BFA); text-decoration: underline; } 418 419 /* ââ Photos en mode Reels ââââââââââââââââââââââââââââââââââââââ */ 420 .reel-photo { 421 display: flex; flex-direction: column; height: 100%; max-height: 100vh; 422 background: #000; position: relative; overflow: hidden; align-items: center; justify-content: center; 423 } 424 .reel-photo-img { max-width: 100%; max-height: 70vh; object-fit: contain; padding: 10px; box-sizing: border-box; } 425 .reel-photo-info { 426 position: absolute; bottom: 0; left: 0; right: 0; 427 padding: 60px 15px 25px; 428 background: linear-gradient(to top, rgba(0,0,0,0.9) 0%, rgba(0,0,0,0.6) 50%, transparent 100%); 429 color: white; z-index: 5; 430 } 431 432 /* ââ Galeries en mode Reels ââââââââââââââââââââââââââââââââââââ */ 433 .reel-gallery { 434 display: flex; flex-direction: column; height: 100%; max-height: 100vh; 435 background: #000; position: relative; overflow: hidden; 436 } 437 .reel-gallery-header { 438 position: absolute; top: 0; left: 0; right: 0; 439 display: flex; align-items: center; gap: 10px; padding: 60px 15px 15px; 440 background: linear-gradient(to bottom, rgba(0,0,0,0.8) 0%, transparent 100%); z-index: 5; 441 } 442 .reel-gallery-counter { 443 margin-left: auto; background: rgba(255,255,255,0.2); 444 padding: 4px 10px; border-radius: 12px; font-size: 0.8rem; color: white; 445 } 446 .reel-gallery-slides { 447 flex: 1; display: flex; overflow-x: auto; 448 scroll-snap-type: x mandatory; scrollbar-width: none; -webkit-overflow-scrolling: touch; 449 } 450 .reel-gallery-slides::-webkit-scrollbar { display: none; } 451 .reel-gallery-slide { 452 flex: 0 0 100%; scroll-snap-align: start; 453 display: flex; align-items: center; justify-content: center; 454 padding: 60px 10px 100px; box-sizing: border-box; 455 } 456 .reel-gallery-slide img { max-width: 100%; max-height: calc(100vh - 180px); object-fit: contain; } 457 .reel-gallery-info { 458 position: absolute; bottom: 0; left: 0; right: 0; 459 padding: 80px 15px 30px; 460 background: linear-gradient(to top, rgba(0,0,0,0.9) 0%, rgba(0,0,0,0.6) 50%, transparent 100%); 461 color: white; z-index: 5; 462 } 463 464 /* ââ Submit Response Modal âââââââââââââââââââââââââââââââââââââ */ 465 .submit-modal-overlay { 466 position: fixed; top: 0; left: 0; right: 0; bottom: 0; 467 background: rgba(0,0,0,0.95); 468 z-index: 10010; 469 display: none; align-items: flex-end; justify-content: center; 470 pointer-events: auto; 471 } 472 .submit-modal-overlay.active { display: flex; } 473 .submit-modal { 474 width: 100%; max-width: 500px; max-height: 90vh; 475 background: var(--bg-card, #1A1A2E); 476 border-radius: 24px 24px 0 0; overflow: hidden; 477 transform: translateY(100%); transition: transform 0.3s ease; 478 } 479 .submit-modal-overlay.active .submit-modal { transform: translateY(0); } 480 .submit-modal-header { 481 padding: 20px; border-bottom: 1px solid var(--border, rgba(139,92,246,0.2)); 482 display: flex; justify-content: space-between; align-items: center; 483 } 484 .submit-modal-header h3 { font-size: 1.1rem; margin: 0; color: white; } 485 .submit-modal-close { 486 background: none; border: none; color: var(--text-dim, #A1A1AA); font-size: 1.5rem; cursor: pointer; padding: 5px; 487 } 488 .submit-modal-body { padding: 20px; overflow-y: auto; max-height: calc(90vh - 80px); } 489 .submit-parent-info { 490 background: rgba(139,92,246,0.15); border: 1px solid rgba(139,92,246,0.3); 491 border-radius: 12px; padding: 12px; margin-bottom: 20px; 492 display: flex; gap: 12px; align-items: center; 493 } 494 .submit-parent-info .icon { font-size: 1.5rem; } 495 .submit-parent-info .text { flex: 1; } 496 .submit-parent-info .label { font-size: 0.75rem; color: var(--text-dim, #A1A1AA); } 497 .submit-parent-info .title { font-weight: 600; font-size: 0.9rem; color: white; } 498 .submit-form-group { margin-bottom: 18px; } 499 .submit-form-group label { display: block; font-weight: 600; margin-bottom: 8px; font-size: 0.9rem; color: white; } 500 .submit-form-group input { 501 width: 100%; padding: 14px; 502 background: var(--bg-elevated, #252542); 503 border: 2px solid var(--border, rgba(139,92,246,0.2)); 504 border-radius: 12px; color: white; font-family: inherit; font-size: 1rem; 505 box-sizing: border-box; 506 } 507 .submit-form-group input:focus { outline: none; border-color: var(--primary, #8B5CF6); } 508 .submit-upload-zone { 509 border: 2px dashed var(--border, rgba(139,92,246,0.2)); 510 border-radius: 16px; padding: 30px 20px; text-align: center; cursor: pointer; transition: all 0.3s; 511 } 512 .submit-upload-zone:hover { border-color: var(--primary, #8B5CF6); background: rgba(139,92,246,0.1); } 513 .submit-upload-zone.has-file { border-color: #10B981; background: rgba(16,185,129,0.1); } 514 .submit-upload-zone .upload-icon { font-size: 2.5rem; margin-bottom: 10px; } 515 .submit-upload-zone .upload-text { font-weight: 600; color: white; } 516 .submit-upload-zone .upload-hint { font-size: 0.8rem; color: var(--text-dim, #A1A1AA); margin-top: 5px; } 517 .submit-video-preview { display: none; margin-top: 15px; border-radius: 12px; overflow: hidden; background: #000; } 518 .submit-video-preview.active { display: block; } 519 .submit-video-preview video { width: 100%; max-height: 200px; object-fit: contain; } 520 .submit-video-preview .preview-info { 521 padding: 10px; background: var(--bg-elevated, #252542); 522 display: flex; justify-content: space-between; align-items: center; 523 } 524 .submit-video-preview .preview-info span { color: white; font-size: 0.85rem; } 525 .submit-video-preview .remove-btn { 526 background: #EC4899; color: white; border: none; 527 padding: 6px 12px; border-radius: 8px; font-size: 0.8rem; cursor: pointer; 528 } 529 .submit-btn { 530 width: 100%; padding: 16px; 531 background: linear-gradient(135deg, var(--primary, #8B5CF6), var(--secondary, #EC4899)); 532 color: white; border: none; border-radius: 14px; 533 font-size: 1rem; font-weight: 700; cursor: pointer; font-family: inherit; margin-top: 10px; 534 } 535 .submit-btn:disabled { opacity: 0.5;
535 cursor: not-allowed; } 536 .submit-progress-bar { 537 display: none; height: 6px; 538 background: var(--bg-elevated, #252542); 539 border-radius: 3px; margin-top: 15px; overflow: hidden; 540 } 541 .submit-progress-bar.active { display: block; } 542 .submit-progress-fill { 543 height: 100%; 544 background: linear-gradient(90deg, var(--primary, #8B5CF6), var(--secondary, #EC4899)); 545 width: 0%; transition: width 0.3s; 546 } 547 548 /* ââ Terms Modal âââââââââââââââââââââââââââââââââââââââââââââââ */ 549 .resp-terms-overlay { 550 display: none; position: fixed; inset: 0; z-index: 10020; 551 background: rgba(0,0,0,0.85); backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px); 552 align-items: center; justify-content: center; padding: 16px; 553 pointer-events: auto; 554 } 555 .resp-terms-overlay.active { display: flex; } 556 .resp-terms-modal { 557 width: 100%; max-width: 540px; max-height: 85vh; max-height: 85dvh; 558 background: var(--bg-card, #1A1A2E); border-radius: 24px; 559 display: flex; flex-direction: column; overflow: hidden; 560 box-shadow: 0 25px 60px rgba(0,0,0,0.6); 561 } 562 .resp-terms-modal-header { 563 padding: 20px 24px; border-bottom: 1px solid rgba(255,255,255,0.08); 564 display: flex; align-items: center; justify-content: space-between; flex-shrink: 0; 565 } 566 .resp-terms-modal-header h3 { font-size: 1.15rem; font-weight: 600; color: #fff; margin: 0; } 567 .resp-terms-body { 568 flex: 1; overflow-y: auto; padding: 24px; -webkit-overflow-scrolling: touch; 569 overscroll-behavior: contain; font-size: 0.85rem; line-height: 1.6; min-height: 0; color: rgba(255,255,255,0.7); 570 } 571 .resp-terms-body h4 { margin: 18px 0 6px; font-size: 0.95rem; color: #fff; } 572 .resp-terms-body p { color: rgba(255,255,255,0.7); margin: 0 0 10px; } 573 .resp-terms-body ul { color: rgba(255,255,255,0.7); padding-left: 20px; margin: 0 0 10px; } 574 .resp-terms-body li { margin-bottom: 4px; } 575 .resp-terms-footer { padding: 12px 24px; flex-shrink: 0; } 576 577 /* ââ Inline style resets âââââââââââââââââââââââââââââââââââââââ */ 578 * { box-sizing: border-box; } 579 a { text-decoration: none; } 580 button { font-family: inherit; } 581 `; 582 583 // ============================================================ 584 // HTML Shadow DOM 585 // ============================================================ 586 const REELS_HTML = /* html */` 587 <!-- Reels Mode Container (mobile) --> 588 <div class="reels-container" id="reelsContainer"> 589 <div class="reels-header"> 590 <a href="/" class="reels-logo" title="partage.ai"> 591 <img src="/share.png" alt="partage.ai"> 592 </a> 593 <div class="reels-creator" id="reelsCreator"> 594 <img src="/share.png" alt="" id="reelsCreatorAvatar"> 595 <span id="reelsCreatorName">Créateur</span> 596 </div> 597 <button class="reels-back" id="reelsBackBtn" title="Fermer">â</button> 598 </div> 599 <div class="reel-counter" id="reelCounter">1 / 1</div> 600 <div class="reels-wrapper" id="reelsWrapper"></div> 601 <!-- Bouton share en bas à droite --> 602 <div class="reels-share-btn" id="reelsShareBtn"> 603 <img src="/share.png" alt="Partager" id="reelsShareImg"> 604 </div> 605 <!-- Boutons réponses fixes --> 606 <div class="fixed-response-actions" id="fixedResponseActions" style="display:none;"> 607 <button type="button" class="fixed-action-btn" id="fixedResponsesBtn"> 608 <span class="action-icon">ð¬</span> 609 <span class="action-label">Réponses</span> 610 </button> 611 <button type="button" class="fixed-action-btn" id="fixedSubmitBtn"> 612 <span class="action-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" width="24" height="24"><rect x="2" y="4" width="15" height="14" rx="2"/><path d="M17 9l5-3v12l-5-3"/></svg></span>
613 <span class="action-label">Répondre</span> 614 </button> 615 </div> 616 </div> 617 618 <!-- Responses Feed Header --> 619 <div class="responses-header" id="responsesHeader"> 620 <button class="responses-back-btn" id="responsesBackBtn">â Retour</button> 621 <div style="flex:1;"></div> 622 </div> 623 624 <!-- Submit Response Modal --> 625 <div class="submit-modal-overlay" id="submitModalOverlay"> 626 <div class="submit-modal"> 627 <div class="submit-modal-header"> 628 <h3>ð¤ Répondre en vidéo</h3> 629 <button class="submit-modal-close" id="submitModalClose">Ã</button> 630 </div> 631 <div class="submit-modal-body"> 632 <div class="submit-parent-info"> 633 <div class="icon">ð¬</div> 634 <div class="text"> 635 <div class="label">Tu réponds à :</div> 636 <div class="title" id="submitParentTitle">-</div> 637 </div> 638 </div> 639 <form id="submitResponseForm"> 640 <div class="submit-form-group"> 641 <label>ð¹ Ta vidéo</label> 642 <div class="submit-upload-zone" id="submitUploadZone"> 643 <div class="upload-icon">ð¬</div> 644 <div class="upload-text">Choisir ou filmer</div> 645 <div class="upload-buttons" style="display:flex;gap:10px;margin-top:10px;"> 646 <button type="button" id="chooseVideoBtn" style="flex:1;padding:12px;border-radius:8px;border:none;background:linear-gradient(135deg,var(--primary,#8B5CF6),var(--secondary,#EC4899));color:white;font-weight:600;cursor:pointer;">ð Choisir</button> 647 <button type="button" id="cameraVideoBtn" style="flex:1;padding:12px;border-radius:8px;border:none;background:linear-gradient(135deg,#10B981,#059669);color:white;font-weight:600;cursor:pointer;">ð· Filmer</button> 648 </div> 649 <div class="upload-hint" style="margin-top:10px;">MP4, MOV ⢠Max 100MB</div> 650 </div> 651 <input type="file" id="submitVideoInput" accept="video/*" style="display:none"> 652 <input type="file" id="submitCameraInput" accept="video/*" capture="environment" style="display:none"> 653 <div class="submit-video-preview" id="submitVideoPreview"> 654 <video id="submitPreviewVideo" controls playsinline></video> 655 <div class="preview-info"> 656 <span id="submitFileName">video.mp4</span> 657 <button type="button" class="remove-btn" id="removeVideoBtn">â</button> 658 </div> 659 </div> 660 </div> 661 <div class="submit-form-group"> 662 <label>ð¤ Ton pseudo</label> 663 <input type="text" id="submitUserName" placeholder="@ton_nom" required> 664 </div> 665 <div class="submit-form-group"> 666 <label>⨠Titre (optionnel)</label> 667 <input type="text" id="submitVideoTitle" placeholder="Ma réponse..."> 668 </div> 669 <div class="submit-progress-bar" id="submitProgressBar"> 670 <div class="submit-progress-fill" id="submitProgressFill"></div> 671 </div> 672 <button type="submit" class="submit-btn" id="submitBtn">ð¤ Envoyer ma réponse</button> 673 <p style="text-align:center;font-size:0.75rem;color:var(--text-dim,#A1A1AA);margin-top:10px;"> 674 En envoyant, tu acceptes nos <a href="#" id="openTermsLink" style="color:var(--primary,#8B5CF6);">conditions d'utilisation</a> 675 </p> 676 </form> 677 </div> 678 </div> 679 </div> 680 681 <!-- Terms Modal --> 682 <div class="resp-terms-overlay" id="responseTermsOverlay"> 683 <div class="resp-terms-modal"> 684 <div class="resp-terms-modal-header"> 685 <h3>ð Conditions d'utilisation</h3> 686 <button id="closeTermsBtn" style="background:rgba(255,255,255,0.08);border:none;color:rgba(255,255,255,0.6);width:36px;height:36px;border-radius:10px;font-size:1.2rem;cursor:pointer;display:flex;align-items:center;justify-content:center;">Ã</button> 687 </div> 688 <div class="resp-terms-body" id="responseTermsBody"> 689 <h3 style="font-size:1.3rem;color:#fff;margin:0 0 6px;">Conditions d'utilisation</h3> 690 <p style="font-size:0.8rem;color:rgba(255,255,255,0.4);margin-bottom:20px;">Dernière mise à jour : 9 février 2026</p> 691 <h4>1. Acceptation des conditions</h4> 692 <p>En accédant à la plateforme partage.ai (faitsdivers.ca) (« la Plateforme ») ou en y soumettant du contenu, vous acceptez d'être lié par les présentes conditions d'utilisation (« Conditions »). Si vous n'acceptez pas ces Conditions, vous ne devez pas utiliser la Plateforme.</p> 693 <h4>2. Description du service</h4> 694 <p>La Plateforme permet aux utilisateurs de consulter, partager et soumettre du contenu généré par les utilisateurs (« Contenu »), incluant notamment des textes, images, vidéos, fichiers audio et tout autre média numérique.</p> 695 <p>La Plateforme agit comme intermédiaire technique permettant la diffusion de Contenu soumis par les utilisateurs.</p> 696 <h4>3. Admissibilité</h4> 697 <p>Vous déclarez avoir l'âge légal requis dans votre juridiction pour utiliser la Plateforme. Si vous utilisez la Plateforme au nom d'une organisation, vous déclarez avoir l'autorité nécessaire pour lier cette organisation aux présentes Conditions.</p> 698 <h4>4. Soumission de contenu</h4> 699 <p>En soumettant du Contenu sur la Plateforme :</p> 700 <ul> 701 <li>Vous déclarez détenir tous les droits nécessaires pour publier ce Contenu.</li> 702 <li>Vous garantissez que le Contenu respecte les lois applicables, notamment en matière de droit d'auteur, de vie privée et de droit à l'image.</li> 703 <li>Vous êtes responsable de vous assurer que toute personne identifiable apparaissant dans votre Contenu est représentée conformément aux lois applicables.</li> 704 </ul> 705 <h4>5. Licence accordée à la Plateforme</h4> 706 <p>En soumettant du Contenu, vous accordez à partage.ai une licence mondiale, non exclusive, gratuite permettant d'utiliser, reproduire, adapter, distribuer et afficher le Contenu dans le cadre de la Plateforme.</p> 707 <h4>6. Utilisation de technologies automatisées</h4> 708 <p>Le Contenu soumis peut être traité à l'aide de systèmes automatisés, incluant des outils d'analyse et de modération assistés par intelligence artificielle.</p> 709 <h4>7. Contenu interdit</h4> 710 <p>Il est interdit de soumettre du Contenu illégal, diffamatoire, haineux ou portant atteinte à la vie privée d'autrui.</p> 711 <h4>8. Responsabilité</h4> 712 <p>Vous êtes seul responsable du Contenu que vous soumettez. La Plateforme est fournie « telle quelle », sans garantie d'aucune sorte.</p> 713 <h4>9. Droit applicable</h4> 714 <p>Les présentes Conditions sont régies par les lois de la province de Québec et les lois fédérales du Canada applicables.</p> 715 </div> 716 <div id="responseTermsHint" style="padding:8px 24px;text-align:center;font-size:0.75rem;color:rgba(255,255,255,0.4);flex-shrink:0;">â Défile jusqu'en bas pour accepter â</div> 717 <div class="resp-terms-footer"> 718 <button id="responseTermsAcceptBtn" disabled style="width:100%;padding:14px;border-radius:12px;border:none;background:var(--primary,#8b5cf6);color:white;font-weight:700;font-size:1rem;cursor:pointer;opacity:0.5;">
719 ð Défile pour débloquer 720 </button> 721 </div> 722 </div> 723 </div> 724 `; 725 726 // ============================================================ 727 // Web Component 728 // ============================================================ 729 class NmcReelsPlayer extends HTMLElement { 730 constructor() { 731 super(); 732 this._shadow = this.attachShadow({ mode: 'open' }); 733 734 // ââ State âââââââââââââââââââââââââââââââââââââââââââââââââ 735 this._reelsMedia = []; 736 this._currentReelIndex = 0; 737 this._isReelsMode = false; 738 this._isResponsesFeed = false; 739 this._responsesFeedFromGrid = false; 740 this._currentParentVideoId = null; 741 this._currentParentVideoTitle = ''; 742 this._responsesCache = {}; 743 this._selectedVideoFile = null; 744 this._allMediaWithResponses = []; 745 this._responseTermsFromSubmit = false; 746 747 // ââ Bound handlers (for removeEventListener) âââââââââââââââ 748 this._onPopState = this._handlePopState.bind(this); 749 this._onKeyDown = this._handleKeyDown.bind(this); 750 this._onReelsScroll = this.onReelsScroll.bind(this); 751 } 752 753 connectedCallback() { 754 this._shadow.innerHTML = `<style>${REELS_CSS}</style>${REELS_HTML}`; 755 this._bindEvents(); 756 this._exposeGlobals(); 757 window.addEventListener('popstate', this._onPopState); 758 document.addEventListener('keydown', this._onKeyDown); 759 } 760 761 disconnectedCallback() { 762 window.removeEventListener('popstate', this._onPopState); 763 document.removeEventListener('keydown', this._onKeyDown); 764 this.reelsWrapper?.removeEventListener('scroll', this._onReelsScroll); 765 } 766 767 // ââ DOM shortcuts ââââââââââââââââââââââââââââââââââââââââââ 768 get reelsWrapper() { return this._shadow.getElementById('reelsWrapper'); } 769 get reelsContainer() { return this._shadow.getElementById('reelsContainer'); } 770 _$ (id) { return this._shadow.getElementById(id); } 771 772 // ââ Bind events ââââââââââââââââââââââââââââââââââââââââââââ 773 _bindEvents() { 774 const s = this._shadow; 775 776 // Header 777 s.getElementById('reelsBackBtn').addEventListener('click', () => this.exitReelsMode()); 778 s.getElementById('reelsShareBtn').addEventListener('click', () => this.copyReelLink()); 779 s.getElementById('responsesBackBtn').addEventListener('click', () => this.exitResponsesFeed()); 780 781 // Fixed response buttons 782 s.getElementById('fixedResponsesBtn').addEventListener('click', () => this._handleFixedResponsesClick()); 783 s.getElementById('fixedSubmitBtn').addEventListener('click', () => this._handleFixedSubmitClick()); 784 785 // Submit modal 786 s.getElementById('submitModalOverlay').addEventListener('click', (e) => { 787 if (e.target === s.getElementById('submitModalOverlay')) this.closeSubmitModal(); 788 }); 789 s.getElementById('submitModalClose').addEventListener('click', () => this.closeSubmitModal()); 790 s.getElementById('submitResponseForm').addEventListener('submit', (e) => this._submitResponse(e)); 791 s.getElementById('chooseVideoBtn').addEventListener('click', () => s.getElementById('submitVideoInput').click()); 792 s.getElementById('cameraVideoBtn').addEventListener('click', () => s.getElementById('submitCameraInput').click()); 793 s.getElementById('submitVideoInput').addEventListener('change', () => this.handleVideoSelect(s.getElementById('submitVideoInput'))); 794 s.getElementById('submitCameraInput').addEventListener('change', () => this.handleVideoSelect(s.getElementById('submitCameraInput'))); 795 s.getElementById('removeVideoBtn').addEventListener('click', () => this.removeSelectedVideo()); 796 s.getElementById('openTermsLink').addEventListener('click', (e) => { 797 e.preventDefault(); 798 this.openResponseTerms(true); 799 }); 800 801 // Terms modal 802 s.getElementById('responseTermsOverlay').addEventListener('click', (e) => { 803 if (e.target === s.getElementById('responseTermsOverlay')) this.closeResponseTerms(); 804 }); 805 s.getElementById('closeTermsBtn').addEventListener('click', () => this.closeResponseTerms()); 806 s.getElementById('responseTermsAcceptBtn').addEventListener('click', () => { 807 if (this._responseTermsFromSubmit) this._acceptResponseTerms(); 808 else this.closeResponseTerms(); 809 }); 810 s.getElementById('responseTermsBody').addEventListener('scroll', () => { 811 this._onTermsScroll(); 812 }, { passive: true }); 813 } 814 815 _handlePopState() { 816 if (this._isResponsesFeed) this.exitResponsesFeed(); 817 else if (this._isReelsMode) this.exitReelsMode(); 818 } 819 820 _handleKeyDown(e) { 821 if (e.key === 'Escape') { 822 if (this._$('submitModalOverlay').classList.contains('active')) this.closeSubmitModal(); 823 else if (this._isResponsesFeed) this.exitResponsesFeed(); 824 } 825 } 826 827 // ââ PUBLIC: initReelsMode ââââââââââââââââââââââââââââââââââ 828 initReelsMode(mediaList) { 829 if (!window.isMobile()) return; 830 this._allMediaWithResponses = mediaList; 831 this._reelsMedia = mediaList.filter(m => !m.parent_id); 832 833 // Only auto-enter Reels if enough visual media (video, photo, gallery). 834 // A single YouTube among 55 articles should NOT trigger reels mode. 835 const visualPlatforms = ['upload', 'ugc', 'youtube', 'tiktok', 'twitch', 'facebook', 'vimeo', 'photo', 'gallery', 'suno']; 836 const visualMedia = this._reelsMedia.filter(m => visualPlatforms.includes(m.platform)); 837 const visualRatio = this._reelsMedia.length > 0 ? visualMedia.length / this._reelsMedia.length : 0; 838 if (visualRatio < 0.3) return; // Need at least 30% visual content for reels mode 839 840 this.enterReelsMode(); 841 } 842 843 // ââ PUBLIC: enterReelsMode âââââââââââââââââââââââââââââââââ 844 enterReelsMode() { 845 if (!this._reelsMedia.length) return; 846 this._isReelsMode = true; 847 document.documentElement.classList.add('reels-mode'); 848 849 // Forcer le container visible (fiabilité â :host-context pas toujours fiable) 850 const container = this._$('reelsContainer'); 851 if (container) { 852 container.style.display = 'block'; 853 container.style.position = 'fixed'; 854 container.style.inset = '0'; 855 container.style.overflow = 'hidden'; 856 container.style.background = '#000'; 857 container.style.zIndex = '9999'; 858 } 859 860 // Ajuster si header pub présent 861 const adsEngine = document.getElementById('nmcAdsEngine'); 862 const adHeader = adsEngine?.headerEl; 863 if (adHeader && adHeader.classList.contains('visible') && container) { 864 container.style.top = (adHeader.offsetHeight || 80) + 'px'; 865 } 866 867 // Mettre à jour le header créateur 868 const pageObj = window.currentPageObj; 869 const avatarEl = this._$('reelsCreatorAvatar'); 870 const nameEl = this._$('reelsCreatorName'); 871 const shareImg = this._$('reelsShareImg'); 872 if (pageObj?.avatar) { 873 if (avatarEl) avatarEl.src = pageObj.avatar; 874 if (shareImg) shareImg.src = pageObj.avatar; 875 } 876 if (nameEl) nameEl.textContent = pageObj?.creator_name || pageObj?.username || 'Créateur'; 877 878 this.renderReels(); 879 880 const wrapper = this.reelsWrapper; 881 wrapper.removeEventListener('scroll', this._onReelsScroll); 882 wrapper.addEventListener('scroll', this._onReelsScroll, { passive: true }); 883 884 document.body.style.overflow = 'hidden'; 885 886 // Autoplay la première vidéo 887 setTimeout(() => { 888 const firstVideo = this._shadow.querySelector('.reel-item video'); 889 if (firstVideo) { 890 firstVideo.muted = false;
891 if (window.playWithPreroll) window.playWithPreroll(firstVideo); 892 } 893 }, 100); 894 895 // Scroller vers média spécifique si ?m=ID 896 const mediaIdParam = window.mediaIdParam; 897 if (mediaIdParam) { 898 setTimeout(async () => { 899 if (mediaIdParam.startsWith('resp_')) { 900 await this._loadResponseFeedForMedia(mediaIdParam); 901 } else { 902 this.scrollToMedia(mediaIdParam); 903 } 904 }, 100); 905 } 906 907 this.dispatchEvent(new CustomEvent('nmc-reels-enter', { 908 bubbles: true, composed: true, 909 detail: { mediaCount: this._reelsMedia.length } 910 })); 911 } 912 913 // ââ PUBLIC: exitReelsMode ââââââââââââââââââââââââââââââââââ 914 exitReelsMode() { 915 this._isReelsMode = false; 916 document.documentElement.classList.remove('reels-mode'); 917 document.body.style.overflow = ''; 918 919 const container = this._$('reelsContainer'); 920 if (container) { 921 container.style.display = ''; 922 container.style.position = ''; 923 container.style.inset = ''; 924 container.style.overflow = ''; 925 container.style.background = ''; 926 container.style.zIndex = ''; 927 container.style.top = ''; 928 } 929 930 // Pause iframes 931 this._shadow.querySelectorAll('.reel-item iframe').forEach(iframe => { 932 iframe.src = 'about:blank'; 933 }); 934 // Pause vidéos 935 this._shadow.querySelectorAll('.reel-item video').forEach(v => v.pause()); 936 // Pause audios Suno 937 this._shadow.querySelectorAll('.reel-item audio').forEach(a => a.pause()); 938 939 this.dispatchEvent(new CustomEvent('nmc-reels-exit', { bubbles: true, composed: true })); 940 } 941 942 // ââ PUBLIC: renderReels ââââââââââââââââââââââââââââââââââââ 943 renderReels() { 944 const wrapper = this.reelsWrapper; 945 const currentAds = window.currentAds || {}; 946 const infeedAd = currentAds.infeed?.[0]; 947 const infeedPosition = infeedAd ? (parseInt(infeedAd.position) || 3) : -1; 948 949 let reelsItems = []; 950 this._reelsMedia.forEach((media, index) => { 951 if (infeedAd && index === infeedPosition) {
952 reelsItems.push({ type: 'ad', ad: infeedAd }); 953 } 954 reelsItems.push({ type: 'media', media, originalIndex: index }); 955 }); 956 957 const overlayAd = currentAds['video-overlay']?.[0]; 958 const _esc = window.escapeHtml || (t => t); 959 const _emoji = window.getPlatformEmoji || (() => 'ðº'); 960 const un = window.username || ''; 961 962 wrapper.innerHTML = reelsItems.map((item, index) => { 963 if (item.type === 'ad') { 964 if (window.trackAd) window.trackAd(item.ad.id, 'impression'); 965 return ` 966 <div class="reel-item reel-ad" data-index="${index}" data-is-ad="true"> 967 <div class="reel-media reel-ad-content"> 968 <a href="${_esc(item.ad.link)}" target="_blank" onclick="window.trackAd?.('${item.ad.id}','click')"> 969 <img src="${_esc(item.ad.content)}" alt="Publicité"> 970 </a> 971 </div> 972 <div class="reel-info"> 973 <span style="background:rgba(255,255,255,0.2);padding:4px 12px;border-radius:12px;font-size:0.75rem;">Sponsorisé</span> 974 </div> 975 </div>`; 976 } 977 978 const media = item.media; 979 const isVertical = media.vertical == 1; 980 const tagsHtml = (media.tags || []).map(tag => 981 `<a href="/${un}/${_esc(tag.slug)}" class="reel-tag" onclick="window.navigateToTag?.('${_esc(tag.slug)}')">${tag.emoji || ''} ${_esc(tag.name)}</a>` 982 ).join(''); 983 984 const isProprietaryContent = media.platform === 'upload' || media.platform === 'ugc'; 985 let overlayHtml = ''; 986 if (overlayAd && isProprietaryContent) { 987 const pos = overlayAd.position || 'top-right'; 988 overlayHtml = ` 989 <div class="reel-overlay-ad ${pos}"> 990 <a href="${_esc(overlayAd.link)}" target="_blank" onclick="window.trackAd?.('${overlayAd.id}','click')"> 991 <img src="${_esc(overlayAd.content)}" alt="Pub"> 992 </a> 993 <button class="reel-overlay-close" onclick="event.preventDefault();event.stopPropagation();this.parentElement.style.display='none';">â</button> 994 </div>`; 995 if (item.originalIndex === this._reelsMedia.findIndex(m => m.platform === 'upload' || m.platform === 'ugc')) { 996 if (window.trackAd) window.trackAd(overlayAd.id, 'impression'); 997 } 998 } 999 1000 // Upload / UGC 1001 if (media.platform === 'upload' || media.platform === 'ugc') { 1002 let videoUrl = media.url || ''; 1003 if (videoUrl.startsWith('/')) videoUrl = window.location.origin + videoUrl; 1004 let thumbUrl = media.thumbnail || ''; 1005 if (thumbUrl.startsWith('/')) thumbUrl = window.location.origin + thumbUrl; 1006 return ` 1007 <div class="reel-item ${isVertical ? 'vertical' : 'horizontal'}" data-index="${index}" data-original-index="${item.originalIndex}" data-media-id="${media.id}" data-platform="${media.platform}" data-allows-responses="${media.allows_responses || 0}"> 1008 <div class="reel-media reel-${media.platform}" onclick="window.toggleReelVideo?.(this)"> 1009 <video src="${videoUrl}" poster="${thumbUrl}" playsinline webkit-playsinline muted loop preload="auto" style="width:100%;height:100%;max-height:100vh;object-fit:contain;background:#000;pointer-events:none;"></video> 1010 <div class="reel-play-indicator">â¶</div> 1011 </div> 1012 ${overlayHtml} 1013 <div class="reel-info"> 1014 ${media.parent_id ? `<div class="response-badge" onclick="window.goToParentVideo?.('${media.parent_id}')">â Retour</div>` : ''} 1015 <a href="/${un}?platform=${media.platform}" class="reel-platform" onclick="event.stopPropagation();window.filterByPlatform?.('${media.platform}')">${_emoji(media.platform)} ${media.platform === 'ugc' ? 'UGC' : media.platform}</a> 1016 <h3 class="reel-title">${_esc(media.title)}</h3> 1017 ${media.description ? `<p class="reel-description">${_esc(media.description)}</p>` : ''} 1018 ${tagsHtml ? `<div class="reel-tags">${tagsHtml}</div>` : ''} 1019 </div> 1020 </div>`; 1021 } 1022 1023 // Suno â lecteur embarquable officiel (le CDN mp3 exige des URLs signées depuis 2026-09, voir nmc-media-card.js) 1024 if (media.platform === 'suno') { 1025 const uuidM = [media.embed_url, media.url, media.thumbnail].map(v => typeof v === 'string' && v.match(/^https:\/\/(?:cdn[12]\.suno\.ai\/(?:image_(?:large_)?)?|suno\.com\/(?:embed|song)\/)([a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})/i)).find(Boolean); 1026 const embedSrc = uuidM ? `https://suno.com/embed/${uuidM[1].toLowerCase()}` : ''; 1027 return ` 1028 <div class="reel-item vertical" data-index="${index}" data-original-index="${item.originalIndex}" data-media-id="${media.id}" data-platform="suno" data-allows-responses="${media.allows_responses || 0}"> 1029 <div class="reel-media reel-suno" style="background:linear-gradient(135deg,#1a1a2e 0%,#16213e 50%,#0f3460 100%);display:flex;flex-direction:column;align-items:center;justify-content:center;padding:30px 20px;"> 1030 ${uuidM ? `<img src="https://cdn2.suno.ai/image_${uuidM[1].toLowerCase()}.jpeg" alt="Cover" loading="lazy" style="width:160px;height:160px;border-radius:16px;object-fit:cover;box-shadow:0 20px 60px rgba(0,0,0,0.5);margin-bottom:20px;" onerror="this.style.display='none'">` : ''} 1031 <h2 style="color:white;font-size:1.5rem;text-align:center;margin-bottom:15px;max-width:80%;">${_esc(media.title)}</h2> 1032 <div style="display:inline-flex;align-items:center;gap:8px;background:rgba(255,255,255,0.1);padding:8px 16px;border-radius:20px;margin-bottom:20px;">
1033 <span>ð¤</span><span style="color:#a78bfa;font-weight:600;">Créé avec Suno AI</span> 1034 </div> 1035 ${embedSrc 1036 ? `<iframe src="${embedSrc}" data-platform="suno" data-base-url="${embedSrc}" title="${_esc(media.title)}" loading="lazy" allow="autoplay; clipboard-write; encrypted-media" referrerpolicy="strict-origin-when-cross-origin" style="width:100%;max-width:480px;height:240px;border:0;border-radius:12px;background:#0f0f1a;" data-suno-embed="true"></iframe>` 1037 : ''} 1038 ${/^https:\/\/suno\.com\//i.test(media.url || '') ? `<a href="${_esc(media.url)}" target="_blank" rel="noopener" style="margin-top:25px;color:#a78bfa;text-decoration:none;font-weight:600;display:flex;align-items:center;gap:8px;">ð Ãcouter sur Suno</a>` : ''} 1039 </div> 1040 ${overlayHtml} 1041 <div class="reel-info"> 1042 ${media.parent_id ? `<div class="response-badge" onclick="window.goToParentVideo?.('${media.parent_id}')">â Retour</div>` : ''} 1043 <a href="/${un}?platform=suno" class="reel-platform" onclick="window.filterByPlatform?.('suno')">ð¤ suno</a> 1044 ${media.description ? `<p class="reel-description">${_esc(media.description)}</p>` : ''} 1045 ${tagsHtml ? `<div class="reel-tags">${tagsHtml}</div>` : ''} 1046 </div> 1047 </div>`; 1048 } 1049 1050 // Article 1051 if (media.platform === 'article') { 1052 let thumbUrl = media.thumbnail || ''; 1053 if (thumbUrl.startsWith('/')) thumbUrl = window.location.origin + thumbUrl; 1054 return ` 1055 <div class="reel-item" data-index="${index}" data-original-index="${item.originalIndex}" data-media-id="${media.id}" data-platform="article" data-has-audio="0"> 1056 <div class="reel-article"> 1057 <div class="reel-article-scroll"> 1058 ${thumbUrl ? `<img src="${thumbUrl}" class="reel-article-thumbnail" alt="">` : ''} 1059 <h2 class="reel-article-title">${_esc(media.title)}</h2> 1060 <div class="reel-article-content">${_esc(media.content || media.description || '')}</div> 1061 </div> 1062 <div class="reel-article-footer"> 1063 <a href="/${un}?platform=article" class="reel-platform-badge" onclick="event.stopPropagation();window.filterByPlatform?.('article')">ð° Article</a> 1064 ${tagsHtml ? `<div class="reel-tags">${tagsHtml}</div>` : ''} 1065 </div> 1066 </div> 1067 </div>`; 1068 } 1069 1070 // Photo 1071 if (media.platform === 'photo') { 1072 let photoUrl = media.url || media.thumbnail || ''; 1073 if (photoUrl.startsWith('/')) photoUrl = window.location.origin + photoUrl; 1074 return ` 1075 <div class="reel-item" data-index="${index}" data-original-index="${item.originalIndex}" data-media-id="${media.id}" data-platform="photo" data-has-audio="0"> 1076 <div class="reel-photo"> 1077 <img src="${photoUrl}" class="reel-photo-img" alt="${_esc(media.title)}"> 1078 <div class="reel-photo-info"> 1079 <a href="/${un}?platform=photo" class="reel-platform-badge" onclick="event.stopPropagation();window.filterByPlatform?.('photo')">ð· Photo</a> 1080 <h3 class="reel-title">${_esc(media.title)}</h3> 1081 ${media.description ? `<p class="reel-description">${_esc(media.description)}</p>` : ''} 1082 ${tagsHtml ? `<div class="reel-tags">${tagsHtml}</div>` : ''} 1083 </div> 1084 </div> 1085 </div>`; 1086 } 1087 1088 // Gallery 1089 if (media.platform === 'gallery') { 1090 let images = []; 1091 try { images = JSON.parse(media.images || '[]'); } catch(e) {} 1092 if (!images.length && media.thumbnail) images = [media.thumbnail]; 1093 const galleryHtml = images.map((img, i) => { 1094 let url = img; 1095 if (url.startsWith('/')) url = window.location.origin + url; 1096 return `<div class="reel-gallery-slide"><img src="${url}" alt="Image ${i+1}"></div>`; 1097 }).join(''); 1098 return ` 1099 <div class="reel-item" data-index="${index}" data-original-index="${item.originalIndex}" data-media-id="${media.id}" data-platform="gallery" data-has-audio="0"> 1100 <div class="reel-gallery"> 1101 <div class="reel-gallery-header">
1102 <span class="reel-gallery-counter">1/${images.length}</span> 1103 </div> 1104 <div class="reel-gallery-slides" onscroll="window.updateGalleryCounter?.(this)">${galleryHtml}</div> 1105 <div class="reel-gallery-info"> 1106 <a href="/${un}?platform=gallery" class="reel-platform-badge" onclick="event.stopPropagation();window.filterByPlatform?.('gallery')">ð¼ï¸ Galerie</a> 1107 <h3 class="reel-title">${_esc(media.title)}</h3> 1108 ${media.description ? `<p class="reel-description">${_esc(media.description)}</p>` : ''} 1109 ${tagsHtml ? `<div class="reel-tags">${tagsHtml}</div>` : ''} 1110 </div> 1111 </div> 1112 </div>`; 1113 } 1114 1115 // Iframe (YouTube, TikTok, Spotify, etc.) 1116 return ` 1117 <div class="reel-item ${isVertical ? 'vertical' : 'horizontal'}" data-index="${index}" data-original-index="${item.originalIndex}" data-media-id="${media.id}" data-platform="${media.platform}" data-allows-responses="${media.allows_responses || 0}"> 1118 <div class="reel-media"> 1119 <iframe 1120 src="${this._getReelSrc(media.embed_url, media.platform, index === 0)}" 1121 allowfullscreen 1122 allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; fullscreen" 1123 playsinline 1124 data-platform="${media.platform}" 1125 data-base-url="${media.embed_url}" 1126 data-index="${index}" 1127 ></iframe> 1128 </div> 1129 ${overlayHtml} 1130 <div class="reel-info"> 1131 ${media.parent_id ? `<div class="response-badge" onclick="window.goToParentVideo?.('${media.parent_id}')">â Retour</div>` : ''} 1132 <a href="/${un}?platform=${media.platform}" class="reel-platform" onclick="window.filterByPlatform?.('${media.platform}')">${_emoji(media.platform)} ${media.platform}</a> 1133 <h3 class="reel-title">${_esc(media.title)}</h3> 1134 ${media.description ? `<p class="reel-description">${_esc(media.description)}</p>` : ''} 1135 ${tagsHtml ? `<div class="reel-tags">${tagsHtml}</div>` : ''} 1136 </div> 1137 </div>`; 1138 }).join(''); 1139 1140 this._updateReelCounter(); 1141 if (window.refreshPrerollListeners) setTimeout(() => window.refreshPrerollListeners(), 100); 1142 1143 // Trigger enrichment for first reel 1144 const firstReel = this._shadow.querySelector('.reel-item[data-media-id]'); 1145 if (firstReel && window.enqueueEnrichment) window.enqueueEnrichment(firstReel.dataset.mediaId); 1146 1147 // Dispatch event for nmc-track 1148 this.dispatchEvent(new CustomEvent('nmc-track', { 1149 bubbles: true, composed: true, 1150 detail: { type: 'reel_view', data: { count: this._reelsMedia.length } } 1151 })); 1152 } 1153 1154 // ââ onReelsScroll ââââââââââââââââââââââââââââââââââââââââââ 1155 onReelsScroll() { 1156 const wrapper = this.reelsWrapper; 1157 const items = wrapper.querySelectorAll('.reel-item'); 1158 const scrollTop = wrapper.scrollTop; 1159 const itemHeight = window.innerHeight; 1160 const newIndex = Math.round(scrollTop / itemHeight); 1161 1162 if (newIndex !== this._currentReelIndex && newIndex >= 0 && newIndex < items.length) { 1163 if (window.stopAllPrerolls) window.stopAllPrerolls(); 1164 1165 const prevItem = items[this._currentReelIndex]; 1166 if (prevItem) { 1167 const prevIframe = prevItem.querySelector('iframe'); 1168 if (prevIframe) this._stopVideo(prevIframe); 1169 const prevVideo = prevItem.querySelector('video'); 1170 if (prevVideo) { prevVideo.pause(); prevVideo.currentTime = 0; } 1171 const prevAudio = prevItem.querySelector('audio'); 1172 if (prevAudio) { prevAudio.pause(); prevAudio.currentTime = 0; } 1173 } 1174 1175 const currentItem = items[newIndex]; 1176 if (currentItem && !currentItem.classList.contains('reel-ad')) { 1177 const iframe = currentItem.querySelector('iframe'); 1178 if (iframe) this._restoreVideo(iframe); 1179 const video = currentItem.querySelector('video'); 1180 if (video && window.playWithPreroll) window.playWithPreroll(video); 1181 } 1182 1183 this._currentReelIndex = newIndex; 1184 this._updateReelCounter(); 1185 1186 const reelMediaId = currentItem?.dataset?.mediaId; 1187 if (reelMediaId && window.enqueueEnrichment) window.enqueueEnrichment(reelMediaId); 1188 } 1189 } 1190 1191 // ââ scrollToMedia ââââââââââââââââââââââââââââââââââââââââââ 1192 scrollToMedia(mediaId) { 1193 const wrapper = this.reelsWrapper; 1194 const target = wrapper.querySelector(`.reel-item[data-media-id="${mediaId}"]`); 1195 if (target) { 1196 const items = wrapper.querySelectorAll('.reel-item'); 1197 this._currentReelIndex = Array.from(items).indexOf(target); 1198 target.scrollIntoView({ behavior: 'instant' }); 1199 this._updateReelCounter(); 1200 } 1201 } 1202 1203 // ââ copyReelLink âââââââââââââââââââââââââââââââââââââââââââ 1204 copyReelLink() { 1205 const wrapper = this.reelsWrapper; 1206 const items = wrapper.querySelectorAll('.reel-item'); 1207 const current = items[this._currentReelIndex]; 1208 if (!current || current.classList.contains('reel-ad')) return; 1209 1210 const mediaId = current.dataset.mediaId; 1211 const media = this._reelsMedia.find(m => m.id == mediaId); 1212 if (!media) return; 1213 1214 const url = window.location.origin + '/' + (window.username || '') + '?m=' + media.id; 1215 if (navigator.share) { 1216 navigator.share({ 1217 title: media.title, 1218 text: this._isResponsesFeed ? 'Regarde cette réponse vidéo sur partage.ai' : `Regarde "${media.title}" sur partage.ai`, 1219 url 1220 }).catch(() => {}); 1221 } else { 1222 navigator.clipboard.writeText(url).then(() => { 1223 const btn = this._$('reelsShareBtn'); 1224 if (btn) { 1225 btn.classList.add('copied'); 1226 setTimeout(() => btn.classList.remove('copied'), 2000); 1227 } 1228 }).catch(() => {}); 1229 } 1230 1231 this.dispatchEvent(new CustomEvent('nmc-track', { 1232 bubbles: true, composed: true, 1233 detail: { type: 'reel_share', data: { mediaId: media.id } } 1234 })); 1235 } 1236 1237 // ââ shareReel (par index) ââââââââââââââââââââââââââââââââââ 1238 shareReel(index) { 1239 const media = this._reelsMedia[index]; 1240 if (!media) return; 1241 const url = window.location.origin + '/' + (window.username || '') + '?m=' + media.id; 1242 if (navigator.share) { 1243 navigator.share({ title: media.title, url }).catch(() => {}); 1244 } else { 1245 navigator.clipboard.writeText(url).catch(() => {}); 1246 } 1247 } 1248 1249 // ââ navigateToTag ââââââââââââââââââââââââââââââââââââââââââ 1250 navigateToTag(tagSlug) { 1251 this.exitReelsMode(); 1252 window.location.href = '/' + (window.username || '') + '/' + tagSlug; 1253 } 1254 1255 // ââ filterByPlatform âââââââââââââââââââââââââââââââââââââââ 1256 filterByPlatform(platform) { 1257 this.exitReelsMode(); 1258 window.location.href = '/' + (window.username || '') + '?platform=' + platform; 1259 } 1260 1261 // ââ autoplayVisibleReelVideo âââââââââââââââââââââââââââââââ 1262 autoplayVisibleReelVideo() { 1263 const wrapper = this.reelsWrapper; 1264 if (!wrapper) return; 1265 const items = wrapper.querySelectorAll('.reel-item'); 1266 const scrollTop = wrapper.scrollTop; 1267 const currentIndex = Math.round(scrollTop / window.innerHeight);
1268 items.forEach((item, i) => { 1269 const video = item.querySelector('.reel-upload video'); 1270 if (!video) return; 1271 if (i === currentIndex) video.play().catch(() => {}); 1272 else { video.pause(); video.currentTime = 0; } 1273 }); 1274 } 1275 1276 // ââ toggleReelVideo ââââââââââââââââââââââââââââââââââââââââ 1277 toggleReelVideo(container) { 1278 const video = container.querySelector('video'); 1279 if (!video) return; 1280 if (video.paused) { 1281 if (window.playWithPreroll) window.playWithPreroll(video); 1282 else video.play(); 1283 } else { 1284 video.pause(); 1285 } 1286 } 1287 1288 // ââ updateGalleryCounter âââââââââââââââââââââââââââââââââââ 1289 updateGalleryCounter(slidesEl) { 1290 const slideWidth = slidesEl.offsetWidth; 1291 const currentSlide = Math.round(slidesEl.scrollLeft / slideWidth) + 1; 1292 const totalSlides = slidesEl.querySelectorAll('.reel-gallery-slide').length; 1293 const counter = slidesEl.closest('.reel-gallery')?.querySelector('.reel-gallery-counter'); 1294 if (counter) counter.textContent = `${currentSlide}/${totalSlides}`; 1295 } 1296 1297 // ââ VIDEO RESPONSES ââââââââââââââââââââââââââââââââââââââââ 1298 async showResponsesFeed(parentId, parentTitle) { 1299 this._currentParentVideoId = parentId; 1300 this._currentParentVideoTitle = parentTitle; 1301 this._isResponsesFeed = true; 1302 this._responsesFeedFromGrid = !this._isReelsMode; 1303 1304 this._$('responsesHeader').classList.add('active'); 1305 this._$('fixedResponseActions').style.display = 'none'; 1306 1307 try { 1308 let responses = this._responsesCache[parentId]; 1309 if (!responses) { 1310 const res = await fetch(`${window.API_URL || '/api/v2'}/get-responses?parent_id=${encodeURIComponent(parentId)}`); 1311 const data = await res.json(); 1312 responses = data.responses || []; 1313 this._responsesCache[parentId] = responses; 1314 } 1315 1316 if (!responses.length) { 1317 alert('Aucune réponse encore. Sois le premier à répondre !'); 1318 this.openSubmitModal(parentId, parentTitle); 1319 this._isResponsesFeed = false; 1320 this._$('responsesHeader').classList.remove('active'); 1321 this._updateFixedResponseButtons(); 1322 return; 1323 } 1324 1325 this._reelsMedia = responses; 1326 this._currentReelIndex = 0; 1327 this.enterReelsMode(); 1328 this.renderReels(); 1329 1330 this.reelsWrapper.scrollTo(0, 0); 1331 setTimeout(() => { 1332 const firstVideo = this._shadow.querySelector('.reel-item video'); 1333 if (firstVideo) { 1334 firstVideo.muted = false; 1335 if (window.playWithPreroll) window.playWithPreroll(firstVideo); 1336 } 1337 }, 100); 1338 1339 history.pushState({ responsesFeed: true, parentId }, ''); 1340 } catch (err) { 1341 console.error('Erreur chargement réponses:', err); 1342 alert('Erreur lors du chargement des réponses'); 1343 this._isResponsesFeed = false; 1344 this._$('responsesHeader').classList.remove('active'); 1345 } 1346 } 1347 1348 exitResponsesFeed() { 1349 this._isResponsesFeed = false; 1350 this._currentParentVideoId = null; 1351 this._$('responsesHeader').classList.remove('active'); 1352 1353 this._shadow.querySelectorAll('.reel-item video').forEach(v => v.pause()); 1354 1355 if (this._responsesFeedFromGrid) { 1356 this._responsesFeedFromGrid = false; 1357 this.exitReelsMode(); 1358 } else { 1359 this._reelsMedia = this._allMediaWithResponses.filter(m => !m.parent_id); 1360 this.renderReels(); 1361 } 1362 } 1363 1364 goToParentVideo(parentId) { 1365 this.exitResponsesFeed(); 1366 setTimeout(() => { 1367 const parent = this.reelsWrapper.querySelector(`[data-media-id="${parentId}"]`); 1368 if (parent) parent.scrollIntoView({ behavior: 'smooth' }); 1369 }, 100); 1370 } 1371 1372 // ââ SUBMIT MODAL âââââââââââââââââââââââââââââââââââââââââââ 1373 openSubmitModal(parentId, parentTitle) {
1374 this._currentParentVideoId = parentId; 1375 this._currentParentVideoTitle = parentTitle; 1376 this._$('submitParentTitle').textContent = parentTitle || 'Vidéo'; 1377 this._$('submitModalOverlay').classList.add('active'); 1378 this._resetSubmitForm(); 1379 } 1380 1381 closeSubmitModal() { 1382 this._$('submitModalOverlay').classList.remove('active'); 1383 this._resetSubmitForm(); 1384 } 1385 1386 _resetSubmitForm() { 1387 this._selectedVideoFile = null; 1388 this._$('submitVideoInput').value = ''; 1389 this._$('submitCameraInput').value = ''; 1390 this._$('submitUserName').value = ''; 1391 this._$('submitVideoTitle').value = ''; 1392 this._$('submitUploadZone').classList.remove('has-file'); 1393 this._$('submitVideoPreview').classList.remove('active'); 1394 this._$('submitProgressBar').classList.remove('active'); 1395 this._$('submitBtn').disabled = false; 1396 1397 const uploadText = this._$('submitUploadZone').querySelector('.upload-text'); 1398 const uploadIcon = this._$('submitUploadZone').querySelector('.upload-icon'); 1399 const uploadButtons = this._$('submitUploadZone').querySelector('.upload-buttons'); 1400 const uploadHint = this._$('submitUploadZone').querySelector('.upload-hint'); 1401 if (uploadText) uploadText.textContent = 'Choisir ou filmer'; 1402 if (uploadIcon) uploadIcon.textContent = 'ð¬'; 1403 if (uploadButtons) uploadButtons.style.display = 'flex'; 1404 if (uploadHint) uploadHint.style.display = 'block'; 1405 1406 const previewVideo = this._$('submitPreviewVideo'); 1407 if (previewVideo?.src) { URL.revokeObjectURL(previewVideo.src); previewVideo.src = ''; } 1408 this._$('submitVideoPreview').style.display = 'none'; 1409 } 1410 1411 handleVideoSelect(input) { 1412 const file = input.files[0]; 1413 if (!file) return; 1414 if (!file.type.startsWith('video/')) { alert('Sélectionne une vidéo !'); return; } 1415 if (file.size > 100 * 1024 * 1024) { alert('Fichier trop volumineux (max 100MB)'); return; } 1416 1417 this._selectedVideoFile = file; 1418 const zone = this._$('submitUploadZone'); 1419 zone.classList.add('has-file'); 1420 const ut = zone.querySelector('.upload-text'); 1421 const ui = zone.querySelector('.upload-icon'); 1422 const ub = zone.querySelector('.upload-buttons'); 1423 const uh = zone.querySelector('.upload-hint'); 1424 if (ut) ut.textContent = 'â Vidéo sélectionnée'; 1425 if (ui) ui.textContent = 'â '; 1426 if (ub) ub.style.display = 'none'; 1427 if (uh) uh.style.display = 'none'; 1428 1429 const previewVideo = this._$('submitPreviewVideo'); 1430 const fileName = this._$('submitFileName'); 1431 const preview = this._$('submitVideoPreview'); 1432 if (previewVideo) { 1433 const videoURL = URL.createObjectURL(file); 1434 previewVideo.src = videoURL; 1435 previewVideo.setAttribute('playsinline', ''); 1436 previewVideo.muted = true; 1437 previewVideo.preload = 'metadata'; 1438 previewVideo.onloadeddata = () => { 1439 try { 1440 const canvas = document.createElement('canvas'); 1441 canvas.width = previewVideo.videoWidth; 1442 canvas.height = previewVideo.videoHeight; 1443 canvas.getContext('2d').drawImage(previewVideo, 0, 0); 1444 previewVideo.poster = canvas.toDataURL('image/jpeg'); 1445 } catch(e) {} 1446 }; 1447 previewVideo.load(); 1448 previewVideo.play().catch(() => {}); 1449 } 1450 if (fileName) fileName.textContent = file.name; 1451 if (preview) { preview.style.display = 'block'; preview.classList.add('active'); } 1452 } 1453 1454 removeSelectedVideo() { 1455 this._selectedVideoFile = null; 1456 this._$('submitVideoInput').value = ''; 1457 this._$('submitCameraInput').value = ''; 1458 const zone = this._$('submitUploadZone'); 1459 zone.classList.remove('has-file'); 1460 const ut = zone.querySelector('.upload-text'); 1461 const ui = zone.querySelector('.upload-icon'); 1462 const ub = zone.querySelector('.upload-buttons'); 1463 const uh = zone.querySelector('.upload-hint'); 1464 if (ut) ut.textContent = 'Choisir ou filmer'; 1465 if (ui) ui.textContent = 'ð¬'; 1466 if (ub) ub.style.display = 'flex'; 1467 if (uh) uh.style.display = 'block'; 1468 1469 const pv = this._$('submitPreviewVideo'); 1470 const pr = this._$('submitVideoPreview'); 1471 if (pv?.src) { URL.revokeObjectURL(pv.src); pv.src = ''; } 1472 if (pr) pr.classList.remove('active'); 1473 } 1474 1475 // ââ TERMS MODAL ââââââââââââââââââââââââââââââââââââââââââââ 1476 openResponseTerms(readOnly) { 1477 this._responseTermsFromSubmit = !readOnly; 1478 const body = this._$('responseTermsBody'); 1479 const btn = this._$('responseTermsAcceptBtn'); 1480 const hint = this._$('responseTermsHint');
1481 const overlay = this._$('responseTermsOverlay'); 1482 body.scrollTop = 0; 1483 overlay.classList.add('active'); 1484 if (readOnly) { 1485 btn.disabled = false; btn.style.opacity = '1'; 1486 btn.textContent = 'ð Fermer'; 1487 hint.style.display = 'none'; 1488 } else { 1489 btn.disabled = true; btn.style.opacity = '0.5'; 1490 btn.textContent = 'ð Défile pour débloquer'; 1491 hint.style.display = ''; 1492 requestAnimationFrame(() => { 1493 if (body.scrollHeight <= body.clientHeight + 40) { 1494 btn.disabled = false; btn.style.opacity = '1'; 1495 btn.textContent = "â J'accepte et j'envoie"; 1496 hint.style.display = 'none'; 1497 } 1498 }); 1499 } 1500 } 1501 1502 closeResponseTerms() { 1503 this._$('responseTermsOverlay').classList.remove('active'); 1504 } 1505 1506 _acceptResponseTerms() { 1507 this.closeResponseTerms(); 1508 this._doSubmitResponse(); 1509 } 1510 1511 _onTermsScroll() { 1512 if (!this._responseTermsFromSubmit) return; 1513 const body = this._$('responseTermsBody'); 1514 const atBottom = (body.scrollTop + body.clientHeight) >= (body.scrollHeight - 40); 1515 if (atBottom) { 1516 const btn = this._$('responseTermsAcceptBtn'); 1517 const hint = this._$('responseTermsHint'); 1518 btn.disabled = false; btn.style.opacity = '1'; 1519 btn.textContent = "â J'accepte et j'envoie"; 1520 if (hint) hint.style.display = 'none'; 1521 } 1522 } 1523 1524 // ââ SUBMIT FORM ââââââââââââââââââââââââââââââââââââââââââââ 1525 async _submitResponse(e) { 1526 e.preventDefault(); 1527 if (!this._selectedVideoFile) { alert('Sélectionne une vidéo !'); return; } 1528 const name = this._$('submitUserName').value.trim(); 1529 if (!name) { alert('Entre ton pseudo !'); return; } 1530 this.openResponseTerms(false); 1531 } 1532 1533 async _doSubmitResponse() { 1534 const name = this._$('submitUserName').value.trim(); 1535 const submitBtn = this._$('submitBtn'); 1536 const progressBar = this._$('submitProgressBar'); 1537 const progressFill = this._$('submitProgressFill'); 1538 1539 submitBtn.disabled = true; 1540 progressBar.classList.add('active'); 1541 1542 const formData = new FormData(); 1543 formData.append('page_slug', window.username || ''); 1544 formData.append('parent_id', this._currentParentVideoId); 1545 formData.append('video', this._selectedVideoFile); 1546 formData.append('name', name.replace('@', '')); 1547 formData.append('title', this._$('submitVideoTitle').value.trim()); 1548 1549 const xhr = new XMLHttpRequest(); 1550 xhr.upload.onprogress = (e) => { 1551 if (e.lengthComputable) progressFill.style.width = Math.round((e.loaded / e.total) * 100) + '%'; 1552 }; 1553 xhr.onload = () => { 1554 if (xhr.status >= 200 && xhr.status < 300) { 1555 try { 1556 const res = JSON.parse(xhr.responseText); 1557 if (res.success) { 1558 alert('â Réponse envoyée ! Elle sera visible après modération.'); 1559 this.closeSubmitModal(); 1560 delete this._responsesCache[this._currentParentVideoId]; 1561 } else { 1562 alert(res.error || "Erreur lors de l'envoi"); 1563 } 1564 } catch { alert('Erreur serveur'); } 1565 } else { 1566 alert('Erreur ' + xhr.status); 1567 } 1568 submitBtn.disabled = false; 1569 progressBar.classList.remove('active'); 1570 }; 1571 xhr.onerror = () => { 1572 alert('Erreur de connexion'); 1573 submitBtn.disabled = false; 1574 progressBar.classList.remove('active'); 1575 }; 1576 xhr.open('POST', (window.API_URL || '/api/v2') + '/submit-response'); 1577 xhr.send(formData); 1578 } 1579 1580 // ââ PRIVATE: helpers âââââââââââââââââââââââââââââââââââââââ 1581 _getReelSrc(embedUrl) { return embedUrl; } 1582 1583 _stopVideo(iframe) { 1584 if (!iframe) return; 1585 const platform = iframe.dataset.platform; 1586 const baseUrl = iframe.dataset.baseUrl; 1587 if (platform === 'tiktok') return; 1588 if (platform === 'spotify' && baseUrl) { iframe.src = baseUrl; return; } 1589 iframe.dataset.wasStopped = 'true'; 1590 iframe.src = 'about:blank'; 1591 } 1592 1593 _restoreVideo(iframe) { 1594 if (!iframe) return; 1595 if (iframe.dataset.wasStopped === 'true' && iframe.dataset.baseUrl) { 1596 iframe.src = iframe.dataset.baseUrl;
1597 iframe.dataset.wasStopped = 'false'; 1598 } 1599 } 1600 1601 _updateReelCounter() { 1602 const counter = this._$('reelCounter'); 1603 if (counter) { 1604 const wrapper = this.reelsWrapper; 1605 const items = wrapper.querySelectorAll('.reel-item'); 1606 const current = items[this._currentReelIndex]; 1607 if (current?.classList.contains('reel-ad')) { 1608 counter.textContent = 'Sponsorisé'; 1609 } else if (current?.dataset?.originalIndex !== undefined) { 1610 const oi = parseInt(current.dataset.originalIndex); 1611 counter.textContent = `${oi + 1} / ${this._reelsMedia.length}`; 1612 } else { 1613 counter.textContent = `${this._currentReelIndex + 1} / ${this._reelsMedia.length}`; 1614 } 1615 } 1616 this._updateFixedResponseButtons(); 1617 } 1618 1619 _updateFixedResponseButtons() { 1620 const fixedActions = this._$('fixedResponseActions'); 1621 const shareBtnEl = this._$('reelsShareBtn'); 1622 const logoEl = this._shadow.querySelector('.reels-logo'); 1623 if (!fixedActions) return; 1624 1625 const wrapper = this.reelsWrapper; 1626 const items = wrapper.querySelectorAll('.reel-item'); 1627 const current = items[this._currentReelIndex]; 1628 1629 if (current?.classList.contains('reel-ad') || current?.dataset?.isAd === 'true') { 1630 fixedActions.style.display = 'none'; 1631 if (logoEl) logoEl.style.display = 'none'; 1632 return; 1633 } 1634 1635 const oi = current?.dataset?.originalIndex; 1636 const currentMedia = oi !== undefined ? this._reelsMedia[parseInt(oi)] : null; 1637 if (!currentMedia) { fixedActions.style.display = 'none'; return; } 1638 1639 if (logoEl) logoEl.style.display = ''; 1640 if (this._isResponsesFeed) { 1641 if (logoEl) logoEl.style.display = 'none'; 1642 if (shareBtnEl) shareBtnEl.style.display = ''; 1643 } else { 1644 if (shareBtnEl) shareBtnEl.style.display = ''; 1645 if (logoEl) logoEl.style.display = ''; 1646 } 1647 1648 if (currentMedia.allows_responses == 1 || currentMedia.allows_responses === true) { 1649 fixedActions.style.display = 'flex'; 1650 const responsesBtn = this._$('fixedResponsesBtn'); 1651 if (responsesBtn) { 1652 const count = parseInt(currentMedia.responses_count) || 0; 1653 responsesBtn.querySelector('.action-icon').textContent = count > 0 ? 'ð¬ ' + count : 'ð¬'; 1654 responsesBtn.style.display = ''; 1655 } 1656 } else { 1657 fixedActions.style.display = 'none'; 1658 } 1659 } 1660 1661 _handleFixedResponsesClick() { 1662 const wrapper = this.reelsWrapper; 1663 const items = wrapper.querySelectorAll('.reel-item'); 1664 const current = items[this._currentReelIndex]; 1665 const oi = current?.dataset?.originalIndex; 1666 const media = oi !== undefined ? this._reelsMedia[parseInt(oi)] : null; 1667 if (media) this.showResponsesFeed(media.id, media.title); 1668 } 1669 1670 _handleFixedSubmitClick() { 1671 const wrapper = this.reelsWrapper; 1672 const items = wrapper.querySelectorAll('.reel-item'); 1673 const current = items[this._currentReelIndex]; 1674 const oi = current?.dataset?.originalIndex; 1675 const media = oi !== undefined ? this._reelsMedia[parseInt(oi)] : null; 1676 if (media) this.openSubmitModal(media.id, media.title); 1677 } 1678 1679 async _loadResponseFeedForMedia(responseId) { 1680 try { 1681 const res = await fetch(`${window.API_URL || '/api/v2'}/get-response-info?id=${encodeURIComponent(responseId)}`); 1682 const data = await res.json(); 1683 if (data.success && data.response) { 1684 if (!data.response.parent_id) { this.scrollToMedia(responseId); return; } 1685 const rootId = data.root_info?.id || data.parent_info?.id || data.response.parent_id; 1686 const rootTitle = data.root_info?.title || data.parent_info?.title || data.response.title || 'Vidéo'; 1687 if (rootId) { 1688 await this.showResponsesFeed(rootId, rootTitle); 1689 setTimeout(() => this.scrollToMedia(responseId), 300); 1690 } else { 1691 this.scrollToMedia(responseId); 1692 } 1693 } else { 1694 this.scrollToMedia(responseId); 1695 } 1696 } catch (e) { 1697 console.error('Error loading response feed:', e); 1698 this.scrollToMedia(responseId); 1699 } 1700 } 1701 1702 // ââ Expose globals (backward compat) âââââââââââââââââââââââ 1703 _exposeGlobals() { 1704 const self = this; 1705 window.initReelsMode = (m) => self.initReelsMode(m); 1706 window.enterReelsMode = () => self.enterReelsMode(); 1707 window.exitReelsMode = () => self.exitReelsMode(); 1708 window.renderReels = () => self.renderReels(); 1709 window.onReelsScroll = () => self.onReelsScroll(); 1710 window.toggleReelVideo = (c) => self.toggleReelVideo(c); 1711 window.autoplayVisibleReelVideo = () => self.autoplayVisibleReelVideo(); 1712 window.updateGalleryCounter = (el) => self.updateGalleryCounter(el); 1713 window.scrollToMedia = (id) => self.scrollToMedia(id); 1714 window.copyReelLink = () => self.copyReelLink(); 1715 window.shareReel = (i) => self.shareReel(i); 1716 window.navigateToTag = (slug) => self.navigateToTag(slug); 1717 window.filterByPlatform = (p) => self.filterByPlatform(p); 1718 window.showResponsesFeed = (id, t) => self.showResponsesFeed(id, t); 1719 window.exitResponsesFeed = () => self.exitResponsesFeed(); 1720 window.goToParentVideo = (id) => self.goToParentVideo(id); 1721 window.openSubmitModal = (id, t) => self.openSubmitModal(id, t); 1722 window.closeSubmitModal = () => self.closeSubmitModal(); 1723 window.handleVideoSelect = (input) => self.handleVideoSelect(input); 1724 window.removeSelectedVideo = () => self.removeSelectedVideo(); 1725 window.openResponseTerms = (ro) => self.openResponseTerms(ro); 1726 window.closeResponseTerms = () => self.closeResponseTerms(); 1727 window.acceptResponseTerms = () => self._acceptResponseTerms(); 1728 window.submitResponse = (e) => self._submitResponse(e); 1729 // isReelsMode getter 1730 Object.defineProperty(window, 'isReelsMode', { 1731 get: () => self._isReelsMode, 1732 configurable: true 1733 }); 1734 Object.defineProperty(window, 'isResponsesFeed', { 1735 get: () => self._isResponsesFeed, 1736 configurable: true 1737 }); 1738 // reelsMedia + currentReelIndex â requis par nmc-media-grid pour ouvrir un reel au tap 1739 Object.defineProperty(window, 'reelsMedia', { 1740 get: () => self._reelsMedia, 1741 configurable: true 1742 }); 1743 Object.defineProperty(window, 'currentReelIndex', { 1744 get: () => self._currentReelIndex, 1745 set: (v) => { self._currentReelIndex = v; }, 1746 configurable: true 1747 }); 1748 } 1749 } 1750 1751 if (!customElements.get('nmc-reels-player')) { 1752 customElements.define('nmc-reels-player', NmcReelsPlayer); 1753 } 1754})();
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.