1/* ========================================================================== 2 FLOW STORY - Haber Sitesi Story Komponenti 3 Kategori bazlı, Swiper entegreli, mobil odaklı 4 Nova CMS Entegrasyonu 5 ========================================================================== */ 6 7(function() { 8 'use strict'; 9 10 // Yapılandırma 11 const CONFIG = { 12 duration: 5000, // Her story için süre (ms) 13 localStorageKey: 'flow-story-seen', 14 autoClose: true // Son story'de otomatik kapat 15 }; 16 17 // Global deÄiÅkenler 18 let modal = null; 19 let categorySwiper = null; 20 let storySwiper = null; 21 let currentCategory = 0; 22 let currentStory = 0; 23 let progressTimer = null; 24 let isPaused = false; 25 let storiesData = []; 26 27 // LocalStorage yardımcıları 28 const storage = { 29 get: function() { 30 try { 31 return JSON.parse(localStorage.getItem(CONFIG.localStorageKey)) || {}; 32 } catch (e) { 33 return {}; 34 } 35 }, 36 set: function(data) { 37 try { 38 localStorage.setItem(CONFIG.localStorageKey, JSON.stringify(data)); 39 } catch (e) {} 40 }, 41 markSeen: function(categoryId, storyId) { 42 const data = this.get(); 43 if (!data[categoryId]) data[categoryId] = []; 44 if (!data[categoryId].includes(storyId)) { 45 data[categoryId].push(storyId); 46 this.set(data); 47 } 48 }, 49 isSeen: function(categoryId, storyId) { 50 const data = this.get(); 51 return data[categoryId] && data[categoryId].includes(storyId); 52 }, 53 isCategorySeen: function(categoryId, totalStories) { 54 const data = this.get(); 55 return data[categoryId] && data[categoryId].length >= totalStories; 56 } 57 }; 58 59 // Zaman formatlama 60 function timeAgo(timestamp) { 61 const now = Date.now(); 62 const diff = now - timestamp * 1000; 63 const minutes = Math.floor(diff / 60000); 64 const hours = Math.floor(diff / 3600000); 65 const days = Math.floor(diff / 86400000); 66 67 if (minutes < 1) return 'Az önce'; 68 if (minutes < 60) return minutes + ' dk önce'; 69 if (hours < 24) return hours + ' saat önce'; 70 if (days < 7) return days + ' gün önce'; 71 return new Date(timestamp * 1000).toLocaleDateString('tr-TR'); 72 } 73 74 // Modal HTML oluÅtur 75 function createModal() { 76 const modalEl = document.createElement('div'); 77 modalEl.className = 'flow-story-modal'; 78 modalEl.id = 'flow-story-modal'; 79 modalEl.innerHTML = ` 80 <div class="flow-story-header"> 81 <div class="flow-story-progress"></div> 82 <div class="flow-story-info"> 83 <div class="flow-story-info-left"> 84 <img class="flow-story-info-avatar" src="" alt=""> 85 <div> 86 <div class="flow-story-info-name"></div> 87 <div class="flow-story-info-time"></div> 88 </div> 89 </div> 90 <button class="flow-story-close" aria-label="Kapat">×</button> 91 </div> 92 </div> 93 <div class="flow-story-content"> 94 <div class="swiper flow-story-swiper"> 95 <div class="swiper-wrapper"></div> 96 </div> 97 <div class="flow-story-touch prev"></div> 98 <div class="flow-story-touch next"></div> 99 <button class="flow-story-nav prev"><i class="fa fa-chevron-left"></i></button> 100 <button class="flow-story-nav next"><i class="fa fa-chevron-right"></i></button> 101 </div> 102 `; 103 document.body.appendChild(modalEl); 104 return modalEl; 105 } 106 107 // Progress bar'ları oluÅtur 108 function createProgressBars(count) { 109 const container = modal.querySelector('.flow-story-progress'); 110 container.innerHTML = ''; 111 for (let i = 0; i < count; i++) { 112 const bar = document.createElement('div'); 113 bar.className = 'flow-story-progress-bar'; 114 bar.innerHTML = '<div class="fill"></div>'; 115 container.appendChild(bar); 116 } 117 } 118 119 // Progress bar'ları güncelle 120 function updateProgressBars(activeIndex) { 121 const bars = modal.querySelectorAll('.flow-story-progress-bar'); 122 bars.forEach((bar, i) => { 123 bar.classList.remove('active', 'seen'); 124 if (i < activeIndex) { 125 bar.classList.add('seen'); 126 } else if (i === activeIndex) { 127 bar.classList.add('active'); 128 } 129 }); 130 } 131 132 // Header bilgilerini güncelle 133 function updateHeader(category, story) { 134 modal.querySelector('.flow-story-info-avatar').src = category.cover; 135 modal.querySelector('.flow-story-info-name').textContent = category.name; 136 modal.querySelector('.flow-story-info-time').textContent = timeAgo(story.time); 137 } 138 139 // Slide HTML oluÅtur 140 function createSlideHTML(story) { 141 return ` 142 <div class="swiper-slide flow-story-slide" data-story-id="${story.id}" data-link="${story.link}"> 143 <img class="media" src="${story.image}" alt="${story.title}" loading="eager"> 144 <a href="${story.link}" class="flow-story-overlay"> 145 <div class="flow-story-title">${story.title}</div>
146 <span class="flow-story-link"> 147 Haberi Oku <i class="fa fa-arrow-right"></i> 148 </span> 149 </a> 150 </div> 151 `; 152 } 153 154 // Swiper'ı baÅlat/güncelle 155 function initSwiper(category) { 156 const wrapper = modal.querySelector('.swiper-wrapper'); 157 wrapper.innerHTML = category.stories.map(createSlideHTML).join(''); 158 159 if (storySwiper) { 160 storySwiper.destroy(true, true); 161 } 162 163 storySwiper = new Swiper('.flow-story-swiper', { 164 allowTouchMove: true, 165 speed: 300, 166 on: { 167 slideChange: function() { 168 currentStory = this.activeIndex; 169 const story = category.stories[currentStory]; 170 171 // Header güncelle 172 updateHeader(category, story); 173 174 // Progress güncelle 175 updateProgressBars(currentStory); 176 177 // Görüldü iÅaretle 178 storage.markSeen(category.id, story.id); 179 180 // Timer'ı yeniden baÅlat 181 startTimer(); 182 } 183 } 184 }); 185 } 186 187 // Timer baÅlat 188 function startTimer() { 189 stopTimer(); 190 if (isPaused) return; 191 192 const category = storiesData[currentCategory]; 193 const story = category.stories[currentStory]; 194 const duration = (story.duration || CONFIG.duration); 195 196 // CSS animasyon süresini ayarla 197 modal.style.setProperty('--story-duration', duration + 'ms'); 198 199 progressTimer = setTimeout(() => { 200 nextStory(); 201 }, duration); 202 } 203 204 // Timer durdur 205 function stopTimer() { 206 if (progressTimer) { 207 clearTimeout(progressTimer); 208 progressTimer = null; 209 } 210 } 211 212 // Sonraki story 213 function nextStory() { 214 const category = storiesData[currentCategory]; 215 216 if (currentStory < category.stories.length - 1) { 217 // Aynı kategoride sonraki story 218 storySwiper.slideNext(); 219 } else if (currentCategory < storiesData.length - 1) { 220 // Sonraki kategori 221 currentCategory++; 222 currentStory = 0; 223 loadCategory(currentCategory); 224 } else if (CONFIG.autoClose) { 225 // Son story, modal'ı kapat 226 closeModal(); 227 } 228 } 229 230 // Ãnceki story 231 function prevStory() { 232 if (currentStory > 0) { 233 // Aynı kategoride önceki story 234 storySwiper.slidePrev(); 235 } else if (currentCategory > 0) { 236 // Ãnceki kategori 237 currentCategory--; 238 const prevCategory = storiesData[currentCategory]; 239 currentStory = prevCategory.stories.length - 1; 240 loadCategory(currentCategory, currentStory); 241 } 242 } 243 244 // Kategoriyi yükle 245 function loadCategory(categoryIndex, startStory = 0) { 246 const category = storiesData[categoryIndex]; 247 248 // Progress bar'ları oluÅtur 249 createProgressBars(category.stories.length); 250 251 // Swiper'ı baÅlat 252 initSwiper(category); 253 254 // BaÅlangıç story'sine git 255 if (startStory > 0) {
256 storySwiper.slideTo(startStory, 0); 257 } 258 259 // İlk story için header güncelle 260 updateHeader(category, category.stories[startStory]); 261 updateProgressBars(startStory); 262 263 // İlk story'yi görüldü iÅaretle 264 storage.markSeen(category.id, category.stories[startStory].id); 265 266 // Timer baÅlat 267 startTimer(); 268 } 269 270 // Modal'ı aç 271 function openModal(categoryIndex) { 272 if (!modal) { 273 modal = createModal(); 274 bindEvents(); 275 } 276 277 currentCategory = categoryIndex; 278 currentStory = 0; 279 isPaused = false; 280 281 loadCategory(categoryIndex); 282 283 modal.classList.add('active'); 284 document.body.style.overflow = 'hidden'; 285 } 286 287 // Modal'ı kapat 288 function closeModal() { 289 if (!modal) return; 290 291 stopTimer(); 292 modal.classList.remove('active'); 293 document.body.style.overflow = ''; 294 295 // Timeline'daki görüldü durumlarını güncelle 296 updateTimelineSeen(); 297 } 298 299 // Timeline'daki görüldü durumlarını güncelle 300 function updateTimelineSeen() { 301 document.querySelectorAll('.flow-story-item').forEach(item => { 302 const categoryId = item.dataset.categoryId; 303 const category = storiesData.find(c => c.id === categoryId); 304 if (category && storage.isCategorySeen(categoryId, category.stories.length)) { 305 item.classList.add('seen'); 306 } 307 }); 308 } 309 310 // Event listener'ları baÄla 311 function bindEvents() { 312 // Kapat butonu 313 modal.querySelector('.flow-story-close').addEventListener('click', closeModal); 314 315 // Sol touch alanı - önceki story 316 modal.querySelector('.flow-story-touch.prev').addEventListener('click', function(e) { 317 e.preventDefault(); 318 e.stopPropagation(); 319 prevStory(); 320 }); 321 322 // SaÄ touch alanı - sonraki story 323 modal.querySelector('.flow-story-touch.next').addEventListener('click', function(e) { 324 e.preventDefault(); 325 e.stopPropagation(); 326 nextStory(); 327 }); 328 329 // Nav butonları (masaüstü) 330 modal.querySelector('.flow-story-nav.prev').addEventListener('click', function(e) { 331 e.preventDefault(); 332 e.stopPropagation(); 333 prevStory(); 334 }); 335 modal.querySelector('.flow-story-nav.next').addEventListener('click', function(e) { 336 e.preventDefault(); 337 e.stopPropagation(); 338 nextStory(); 339 }); 340 341 // Pause/Resume (basılı tutma) - sadece görsel alanında 342 let pressTimer; 343 modal.addEventListener('touchstart', function(e) { 344 // Overlay, link veya close butonuna dokunulursa pause yapma 345 if (e.target.closest('.flow-story-overlay') || e.target.closest('.flow-story-close')) return; 346 347 pressTimer = setTimeout(() => { 348 isPaused = true; 349 modal.classList.add('paused'); 350 stopTimer(); 351 }, 200); 352 }); 353 354 modal.addEventListener('touchend', function() { 355 clearTimeout(pressTimer); 356 if (isPaused) { 357 isPaused = false; 358 modal.classList.remove('paused'); 359 startTimer(); 360 } 361 }); 362 363 // Escape tuÅu 364 document.addEventListener('keydown', function(e) { 365 if (e.key === 'Escape' && modal.classList.contains('active')) { 366 closeModal(); 367 } 368 if (e.key === 'ArrowRight' && modal.classList.contains('active')) { 369 nextStory(); 370 } 371 if (e.key === 'ArrowLeft' && modal.classList.contains('active')) { 372 prevStory(); 373 } 374 }); 375 } 376 377 /** 378 * Zuck Story formatını Flow Story formatına dönüÅtür 379 * Nova CMS'deki get_story_posts() fonksiyonu Zuck formatında veri döndürüyor 380 * Bu fonksiyon o veriyi Flow Story formatına çevirir 381 * 382 * Zuck format: 383 * { id, photo, name, items: [{ id, type, length, src, preview, link, linkText, time }] } 384 * 385 * Flow format: 386 * { id, name, cover, stories: [{ id, title, image, link, time, duration }] } 387 */ 388 function convertZuckToFlowFormat(zuckData) { 389 if (!zuckData || !Array.isArray(zuckData)) return []; 390 391 return zuckData 392 .filter(category => category && category.items && category.items.length > 0) 393 .map(category => ({ 394 id: category.id, 395 name: category.name, 396 cover: category.photo, 397 stories: category.items.map(item => ({ 398 id: item.id, 399 title: item.linkText || '', 400 image: item.src || item.preview, 401 link: item.link, 402 time: item.time, 403 duration: (item.length || 10) * 1000 // length saniye cinsinden, ms'ye çevir 404 })) 405 })); 406 } 407 408 // Timeline'ı baÅlat 409 function initTimeline(data) { 410 // Veriyi Flow formatına dönüÅtür (Zuck formatından geliyorsa) 411 if (data && data.length > 0 && data[0].items) { 412 // Zuck formatı algılandı, dönüÅtür
413 storiesData = convertZuckToFlowFormat(data); 414 } else { 415 // Zaten Flow formatında 416 storiesData = data; 417 } 418 419 if (!storiesData || storiesData.length === 0) return; 420 421 // Timeline item'larına click event ekle 422 document.querySelectorAll('.flow-story-item').forEach((item) => { 423 const categoryId = item.dataset.categoryId; 424 const idx = storiesData.findIndex(c => c.id === categoryId); 425 426 // EÅleÅen veri yoksa thumb'ı gizle (boÅ kategoriler tıklanamaz olmasın diye render edilse de) 427 if (idx < 0) { 428 item.style.display = 'none'; 429 return; 430 } 431 432 item.addEventListener('click', () => openModal(idx)); 433 434 // Görüldü durumunu kontrol et 435 if (storage.isCategorySeen(categoryId, storiesData[idx].stories.length)) { 436 item.classList.add('seen'); 437 } 438 }); 439 } 440 441 // Global API 442 window.FlowStory = { 443 init: initTimeline, 444 open: openModal, 445 close: closeModal, 446 config: function(options) { 447 Object.assign(CONFIG, options); 448 } 449 }; 450 451})();
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.