PageSourceSearch

https://yeu-tiengtrung.com/js/calendar.js?v=23.59

js yeu-tiengtrung.com collected 2026-09-24 14:58:15 UTC 22,366 bytes, 432 lines download raw bytes

1window.YTT = window.YTT || {};
2
3const MILESTONE_CONFIG = [
4    { days: 7,   itemId: 'phaptuong_cal7',   src: 'images/gift_dang_nhap/gift1.webm', name: 'Hoàng Cực Kim Thân', desc: 'Pháp tướng ngưng tụ từ vạn năm tu vi đạo gia. Toàn thân tỏa ra vạn trượng kim quang chói lọi, vạn tà bất xâm, thể hiện phong thái chí tôn của bậc đại năng ngộ đạo.' },
5    { days: 14,  itemId: 'phaptuong_cal14',  src: 'images/gift_dang_nhap/gift2.webm', name: 'Thần Ma Nhất Niệm', desc: 'Dị tượng sinh ra khi tâm trí giằng xé giữa Phật môn và Ma đạo. Một nửa xót thương độ thế, một nửa sát phạt quyết đoán. Uy lực nghịch thiên, phá vỡ mọi quy tắc luân hồi.' },
6    { days: 21,  itemId: 'phaptuong_cal21',  src: 'images/gift_dang_nhap/gift3.webm', name: 'Phần Thiên Chân Hỏa', desc: 'Hỏa diễm bùng phát từ đan điền, ngưng tụ hỏa linh khí tinh thuần nhất. Thích hợp cho người tu luyện hỏa hệ công pháp vận khí đột phá, mượn lửa thiêu rụi mọi tâm ma.' },
7    { days: 28,  itemId: 'phaptuong_cal28',  src: 'images/gift_dang_nhap/gift5.webm', name: 'Bích Thủy Uẩn Thần', desc: 'Hấp thụ linh thủy từ cửu u hàn tuyền, giữ tâm trí luôn tĩnh lặng như mặt hồ không gợn sóng. Pháp môn dưỡng thần bậc nhất, giúp xoa dịu kinh mạch và cường hóa thần thức.' },
8    { days: 50,  itemId: 'phaptuong_cal50',  src: 'images/gift_dang_nhap/gift6.webm', name: 'Vạn Tự Kim Trận', desc: 'Hình ảnh tu chân giả chìm vào cõi mộng ngộ đạo, xung quanh là trận pháp chân ngôn xoay chuyển liên tục. Giúp khai mở linh trí, gia tăng tốc độ lĩnh ngộ thiên cơ.' },
9    { days: 80,  itemId: 'phaptuong_cal80',  src: 'images/gift_dang_nhap/gift8.webm', name: 'Xích Viêm Hộ Thể', desc: 'Vận chuyển bí pháp tạo ra một lớp huyết diễm hình cầu bao bọc bảo hộ toàn thân. Vừa là bức tường phòng ngự kiên cố, vừa có khả năng phản chấn mọi tà khí xâm nhập.' },
10    { days: 100, itemId: 'phaptuong_cal100', src: 'images/gift_dang_nhap/gift9.webm', name: 'U Minh Huyết Sát', desc: 'Pháp tướng hung ác...', highlight: true },
11    { days: 150, itemId: 'phaptuong_cal150', src: 'images/gift_dang_nhap/gift4.webm', name: 'Mốc 150 Ngày', desc: 'Chăm chỉ tu luyện 150 ngày', highlight: true },
12    { days: 200, itemId: 'phaptuong_cal200', src: 'images/gift_dang_nhap/gift7.webm', name: 'Mốc 200 Ngày', desc: 'Chăm chỉ tu luyện 200 ngày', highlight: true },
13    { days: 250, itemId: 'phaptuong_cal250', src: 'images/gift_dang_nhap/gift10.webm', name: 'Mốc 250 Ngày', desc: 'Chăm chỉ tu luyện 250 ngày', highlight: true },
14    { days: 300, itemId: 'phaptuong_cal300', src: 'images/gift_dang_nhap/gift11.webm', name: 'Mốc 300 Ngày', desc: 'Chăm chỉ tu luyện 300 ngày', highlight: true },
15    { days: 365, itemId: 'phaptuong_cal365', src: 'images/gift_dang_nhap/gift12.webm', name: 'Mốc 1 Năm', desc: 'Tu luyện tròn 1 năm kiên trì', highlight: true },
16];
17
18const renderMilestones = (totalCheckedIn, claimedMilestones = []) => {
19    const inner = document.getElementById('milestone-scroll-inner');
20    if (!inner) return;
21
22    // Xóa gift nodes cũ, giữ lại 2 progress line
23    inner.querySelectorAll('.gift-node').forEach(n => n.remove());
24
25    const maxDays = MILESTONE_CONFIG[MILESTONE_CONFIG.length - 1].days;
26    const percent = Math.min((totalCheckedIn / maxDays) * 100, 100);
27    const fill = document.getElementById('cal-progress-fill');
28    if (fill) fill.style.width = percent + '%';
29
30    MILESTONE_CONFIG.forEach(m => {
31        const reached = totalCheckedIn >= m.days;
32        const claimed = claimedMilestones.includes(m.itemId);
33        const showBtn = reached && !claimed;
34        
35        const node = document.createElement('div');
36        node.className = 'gift-node' + (reached ? ' reached' : '');
37        node.id = `milestone-node-${m.days}`;
38        node.innerHTML = `
39            <div class="gift-video-wrapper" title="${m.name}: ${m.desc}">
40                ${m.src.endsWith('.webm') || m.src.endsWith('.mp4') 
41                    ? `<video class="milestone-vid" autoplay loop muted playsinline data-src="${m.src}"></video>` 
42                    : `<img class="milestone-vid" src="${m.src}" alt="Gift">`}
43            </div>
44            <span class="gift-label" ${m.highlight ? 'style="color: red; font-weight: bold;"' : ''}>${m.days} Ngày</span>
45            ${showBtn ? `<button class="btn-claim-milestone" onclick="window.YTT.Calendar.doClaimMilestone(${m.days})">NHẬN</button>` : ''}
46            ${claimed ? `<span class="milestone-claimed-badge"><i class="fas fa-check"></i></span>` : ''}
47        `;
48        inner.appendChild(node);
49    });
50    napVideoQua(inner);
51
52    // Auto scroll đến mốc hiện tại
53    const allReached = inner.querySelectorAll('.gift-node.reached');
54    const lastReached = allReached.length ? allReached[allReached.length - 1] : null;
55    if (lastReached) lastReached.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' });
56};
57
58// [22/09/2026] Video quà điểm danh phát từ BLOB, không gán thẳng URL: Firebase Hosting nén gzip cả
59// video/webm và tính Range trên luồng đã nén ⇒ Chrome nhận Content-Range sai cỡ ⇒ đỏ console
60// "416 Range Not Satisfiable" mỗi lần mở lịch và vòng lặp có thể chết sau lượt đầu (cùng gốc với
61// nền phòng chờ trò chơi — xem napNenPhong trong js/trochoi-ui.js). fetch() để trình duyệt tự giải
62// nén đúng, phát từ bộ nhớ; blob giữ theo src để mở lịch lần sau không tải lại.
63const _blobVideoQua = {};
64function napVideoQua(goc) {
65    if (!goc || !window.fetch || !window.URL || !URL.createObjectURL) {
66        (goc ? goc.querySelectorAll('video[data-src]') : []).forEach((v) => { v.src = v.dataset.src; });
67        return;
68    }
69    goc.querySelectorAll('video[data-src]').forEach((v) => {
70        const src = v.dataset.src;
71        const dat = (url) => { v.src = url; const p = v.play && v.play(); if (p && p.catch) p.catch(() => {}); };
72        if (_blobVideoQua[src]) { dat(_blobVideoQua[src]); return; }
73        fetch(src).then((r) => (r.ok ? r.blob() : null))
74            .then((b) => { if (!b) return; _blobVideoQua[src] = URL.createObjectURL(b); dat(_blobVideoQua[src]); })
75            .catch(() => {});
76    });
77}
78
79window.YTT.Calendar = (function() {
80    let isLoading = false;
81    let currentUserDoc = null;
82
83    // Cache Firebase Functions module — import 1 lần, dùng mãi
84    let _fnModulePromise = null;
85    const _getFnModule = () => {
86        if (!_fnModulePromise) {
87            _fnModulePromise = Promise.all([
88                import("https://www.gstatic.com/firebasejs/10.7.1/firebase-functions.js"),
89                import('./firebase-config.js'),
90            ]).then(([fnMod, cfgMod]) => ({
91                getFunctions: fnMod.getFunctions,
92                httpsCallable: fnMod.httpsCallable,
93                app: cfgMod.app,
94            }));
95        }
96        return _fnModulePromise;
97    };
98
99    const getVNTimeVars = () => {
100        const now = new Date();
101        const vnTime = new Date(now.getTime() + (7 * 60 * 60 * 1000));
102        const vnToday = `${vnTime.getUTCFullYear()}-${String(vnTime.getUTCMonth()+1).padStart(2, '0')}-${String(vnTime.getUTCDate()).padStart(2, '0')}`;
103        const vnMonth = vnToday.substring(0, 7); // "YYYY-MM"
104        const vnDay = vnTime.getUTCDate();
105        return { vnTime, vnToday, vnMonth, vnDay };
106    };
107
108    const loadCSS = () => {
109        if (document.getElementById("calendar-css")) return;
110        const link = document.createElement("link");
111        link.id = "calendar-css";
112        link.rel = "stylesheet";
113        link.href = "css/calendar.css?v=23.59";
114        document.head.appendChild(link);
115    };
116
117    // [FEATURE FLAG GUARD] — Chặn Lịch Điểm Danh khi flag bị tắt
118    const triggerOpenModal = () => {
119        window.guardFeature('calendar', () => {
120            const user = window.YTT_currentUser || null;
121            if (!user) {
122                window.showHippoLoginModal ? window.showHippoLoginModal("Lịch Điểm Danh") : alert("Vui lòng đăng nhập!");
123                return;
124            }
125            // Gọi Lazy Load qua Main.js
126            window.YTT.Main.loadModalOnDemand("calendarModal", openModal);
127        });
128    };
129
130    const openModal = async () => {
131        loadCSS();
132        const calModal = document.getElementById("calendarModal");
133        const calBackdrop = document.getElementById("calendar-backdrop");
134        if (!calModal) {
135            window.xianxiaAlert ? window.xianxiaAlert("Trận pháp Điểm Danh đang khởi động, xin đợi 1 giây!") : alert("Đang tải...");
136            return;
137        }
138        calBackdrop.classList.add("show");
139        setTimeout(() => calModal.classList.add("open"), 10);
140
141        // Render ngay lập tức từ dữ liệu đã có trong memory — không chờ Firestore
142        if (window.CURRENT_USER_DATA) {
143            currentUserDoc = window.CURRENT_USER_DATA;
144            renderCalendar();
145        }
146
147        // Background refresh để đồng bộ, không block UI
148        fetchUserCheckIns();
149        // Pre-warm Firebase Functions module
150        _getFnModule();
151    };
152
153    const closeModal = () => {
154        const calModal = document.getElementById("calendarModal");
155        const calBackdrop = document.getElementById("calendar-backdrop");
156        if(calModal) calModal.classList.remove("open");
157        if(calBackdrop) calBackdrop.classList.remove("show");
158    };
159
160    const fetchUserCheckIns = async () => {
161        const user = window.YTT_currentUser;
162        if (!user) return;
163        try {
164            const { db, doc, getDoc } = await import('./firebase-config.js');
165            const docRef = doc(db, "users", user.uid);
166            const docSnap = await getDoc(docRef);
167            if (docSnap.exists()) {
168                currentUserDoc = docSnap.data();
169                renderCalendar();
170            }
171        } catch (e) {
172            console.error("Lỗi lấy dữ liệu điểm danh:", e);
173        }
174    };
175
176    const renderCalendar = () => {
177        const container = document.getElementById("cal-days-container");
178        if (!container) return;
179
180        const monthYearEl = document.getElementById("calendarMonthTitle");
181        const btnDaily = document.getElementById("btn-daily-checkin");
182        const streakEl = document.getElementById("cal-streak");
183
184        const { vnTime, vnToday, vnMonth, vnDay } = getVNTimeVars();
185        
186        const year = vnTime.getUTCFullYear();
187        const month = vnTime.getUTCMonth(); // 0-based
188        if(monthYearEl) {
189            document.getElementById('cal-month-skeleton')?.style.setProperty('display','none');
190            monthYearEl.textContent = `Tháng ${String(month + 1).padStart(2, '0')}/${year} - Hôm nay: ${String(vnDay).padStart(2, '0')}/${String(month + 1).padStart(2, '0')}`;
191        }
192
193        const checkInMap = currentUserDoc?.checkIns || {};
194        const monthKey = `${year}-${String(month+1).padStart(2, '0')}`;
195        const checkInsThisMonth = checkInMap[monthKey] || [];
196        const lastCheckIn = currentUserDoc?.lastCheckInDate;
197
198        // [NÂNG CẤP THUẬT TOÁN STREAK V3]: Đếm ngược "Vượt Thời Gian" qua các tháng (Không bị đứt chuỗi ngọn lửa mồng 1)
199        let streak = 0;
200        let scanDate = new Date(Date.UTC(year, month, vnDay));
201        let checkingToday = true;
202
203        // Vòng lặp đếm lùi an toàn, tự động break khi thấy đứt chuỗi
204        while (streak < 3650) { // Giới hạn max 10 năm để phòng hờ lặp vô hạn
205            const sYear = scanDate.getUTCFullYear();
206            const sMonth = scanDate.getUTCMonth();
207            const sDay = scanDate.getUTCDate();
208            const sMonthKey = `${sYear}-${String(sMonth + 1).padStart(2, '0')}`;
209            
210            const sArr = checkInMap[sMonthKey] || [];
211            const hasCheckIn = sArr.includes(sDay);
212
213            if (checkingToday) {
214                if (hasCheckIn) streak++;
215                checkingToday = false;
216            } else {
217                if (hasCheckIn) {
218                    streak++;
219                } else {
220                    break; // Gặp ngày đứt chuỗi -> Dừng quét!
221                }
222            }
223            // Lùi 1 ngày an toàn qua mốc thời gian (JavaScript tự lo lùi tháng/năm)
224            scanDate.setUTCDate(scanDate.getUTCDate() - 1);
225        }
226        
227        streakEl.innerHTML = `<i class="fas fa-fire text-warning"></i> Liên tiếp: ${streak}`;
228
229        // Cập nhật Nút chính (dùng mảng checkInsThisMonth cho nhất quán)
230        const hasCheckedInToday = checkInsThisMonth.includes(vnDay);
231        if (hasCheckedInToday) {
232            if (btnDaily) {
233                btnDaily.classList.add("disabled-claimed");
234                btnDaily.innerHTML = `<i class="fas fa-check-circle"></i> ĐÃ ĐIỂM DANH HÔM NAY`;
235            }
236        } else {
237            if (btnDaily) {
238                btnDaily.classList.remove("disabled-claimed");
239                btnDaily.innerHTML = `ĐIỂM DANH HÔM NAY <span class="badge bg-white text-dark ms-2" style="font-size: 0.9rem; vertical-align: middle;">+50 <i class="fas fa-gem text-info"></i></span>`;
240            }
241        }
242
243        // --- Cập nhật Milestone Progress Bar ---
244        // [VÁ LỖI CALENDAR: TASK 2] Lấy tổng tích lũy toàn hệ thống kể từ khi tạo nick (Không reset khi sang tháng)
245        let totalCheckedIn = currentUserDoc?.total_checkins;
246        if (totalCheckedIn === undefined) {
247            // Migration dự phòng trên client nếu Cloud Function chưa kịp cập nhật hoặc data cũ
248            totalCheckedIn = 0;
249            if (currentUserDoc?.checkIns) {
250                for (const m in currentUserDoc.checkIns) {
251                    if (Array.isArray(currentUserDoc.checkIns[m])) {
252                        totalCheckedIn += currentUserDoc.checkIns[m].length;
253                    }
254                }
255            } else {
256                totalCheckedIn = checkInsThisMonth.length;
257            }
258        }
259        
260        const claimedMilestones = currentUserDoc?.claimedMilestones || [];
261        renderMilestones(totalCheckedIn, claimedMilestones);
262
263        const daysInMonth = new Date(Date.UTC(year, month + 1, 0)).getUTCDate();
264
265        let html = "";
266
267        for (let d = 1; d <= daysInMonth; d++) {
268            let cellClass = "day-orb";
269            let onClick = "";
270            
271            let dateObj = new Date(Date.UTC(year, month, d));
272            let dayOfWeek = dateObj.getUTCDay();
273            let dayStr = dayOfWeek === 0 ? "CN" : `T${dayOfWeek + 1}`;
274            let innerHtml = `<span style="font-size: 0.65rem; color: #888; font-weight: normal; margin-bottom: 2px;">${dayStr}</span><span style="line-height: 1;">${d}</span>`;
275
276            const isCheckedIn = checkInsThisMonth.includes(d);
277
278            if (d < vnDay) {
279                if (isCheckedIn) {
280                    cellClass += " claimed";
281                } else {
282                    cellClass += " missed";
283                    onClick = `onclick="window.YTT.Calendar.doMakeupCheckIn(${d})"`;
284                    innerHtml += `<br><i class="fas fa-gem" style="font-size: 0.65rem; color: #00bcd4; margin-top: 2px; filter: drop-shadow(0 0 2px #00bcd4)"></i>`;
285                }
286            } else if (d === vnDay) {
287                if (isCheckedIn) {
288                    cellClass += " claimed";
289                } else {
290                    cellClass += " today-uncheck";
291                    onClick = `onclick="window.YTT.Calendar.doDailyCheckIn()"`;
292                }
293            } else {
294                cellClass += " future";
295            }
296
297            html += `<div class="${cellClass}" data-day="${d}" ${onClick}>${innerHtml}</div>`;
298        }
299        container.innerHTML = html;
300
301        const todayOrb = container.querySelector(`.today-uncheck, .claimed[data-day="${vnDay}"]`);
302        if (todayOrb) {
303            todayOrb.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' });
304        } else {
305            const lastClaimed = container.querySelectorAll('.claimed');
306            if (lastClaimed.length) lastClaimed[lastClaimed.length - 1].scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' });
307        }
308    };
309
310    const doDailyCheckIn = async () => {
311        if (isLoading) return;
312        isLoading = true;
313
314        // [OPTIMISTIC] Cập nhật RAM + UI ngay, CF call chạy nền
315        const { vnToday, vnMonth, vnDay: today } = getVNTimeVars();
316        let rollbackCheckIns = null;
317        let rollbackLastCheckIn = null;
318        if (currentUserDoc) {
319            if (!currentUserDoc.checkIns) currentUserDoc.checkIns = {};
320            if (!currentUserDoc.checkIns[vnMonth]) currentUserDoc.checkIns[vnMonth] = [];
321            if (!currentUserDoc.checkIns[vnMonth].includes(today)) {
322                rollbackCheckIns = currentUserDoc.checkIns[vnMonth].slice();
323                rollbackLastCheckIn = currentUserDoc.lastCheckInDate || null;
324                currentUserDoc.checkIns[vnMonth].push(today);
325                currentUserDoc.lastCheckInDate = vnToday; // optimistic — để _updateStats tính streak ngay
326                if (currentUserDoc.total_checkins !== undefined) currentUserDoc.total_checkins += 1;
327            }
328        }
329        renderCalendar();
330        // Cập nhật streak + mission toast ngay (không chờ Firestore onSnapshot)
331        if (window.YDH && window.YDH.refresh) window.YDH.refresh();
332
333        try {
334            const { getFunctions, httpsCallable, app } = await _getFnModule();
335            const fnRegion = getFunctions(app, 'asia-southeast1');
336            const dailyCheckInFn = httpsCallable(fnRegion, 'dailyCheckIn');
337            await dailyCheckInFn();
338            // Không show toast ở đây — mission complete toast đã hiện ở bước optimistic trên
339
340            await fetchUserCheckIns();
341            if (window.updateNavbarBalance) window.updateNavbarBalance();
342
343        } catch (e) {
344            console.error(e);
345            // [ROLLBACK] Hoàn tác nếu CF báo lỗi
346            if (rollbackCheckIns !== null && currentUserDoc?.checkIns?.[vnMonth]) {
347                currentUserDoc.checkIns[vnMonth] = rollbackCheckIns;
348                currentUserDoc.lastCheckInDate = rollbackLastCheckIn;
349                if (currentUserDoc.total_checkins !== undefined) currentUserDoc.total_checkins -= 1;
350            }
351            renderCalendar();
352            if (window.YDH && window.YDH.refresh) window.YDH.refresh();
353            if (window.showToast) window.showToast(e.message || "Điểm danh lỗi!", "error");
354        } finally {
355            isLoading = false;
356        }
357    };
358
359    const doMakeupCheckIn = async (day) => {
360        if (isLoading) return;
361
362        const confirmStr = `Đạo hữu có muốn tiêu hao 50 Linh Thạch để điểm danh bù cho ngày ${day} không?`;
363        const isOk = window.ytt
364            ? await window.ytt.confirm(confirmStr, { okText: 'Bù Đắp Thiếu Sót', cancelText: '❌ Hủy' })
365            : window.confirm(confirmStr);
366        if (!isOk) return;
367
368        isLoading = true;
369
370        // [OPTIMISTIC] Cập nhật RAM + UI ngay, CF call chạy nền
371        const { vnMonth } = getVNTimeVars();
372        let rollbackCheckIns = null;
373        if (currentUserDoc) {
374            if (!currentUserDoc.checkIns) currentUserDoc.checkIns = {};
375            if (!currentUserDoc.checkIns[vnMonth]) currentUserDoc.checkIns[vnMonth] = [];
376            if (!currentUserDoc.checkIns[vnMonth].includes(day)) {
377                rollbackCheckIns = currentUserDoc.checkIns[vnMonth].slice();
378                currentUserDoc.checkIns[vnMonth].push(day);
379                if (currentUserDoc.total_checkins !== undefined) currentUserDoc.total_checkins += 1;
380            }
381        }
382        renderCalendar();
383
384        try {
385            const { getFunctions, httpsCallable, app } = await _getFnModule();
386            const fnRegion = getFunctions(app, 'asia-southeast1');
387            const makeupCheckInFn = httpsCallable(fnRegion, 'makeupCheckIn');
388            const res = await makeupCheckInFn({ targetDay: day });
389            if (window.showToast) window.showToast(res.data.message, "success");
390
391            await fetchUserCheckIns();
392            if (window.updateNavbarBalance) window.updateNavbarBalance();
393
394        } catch (e) {
395            console.error(e);
396            // [ROLLBACK] Hoàn tác nếu CF báo lỗi (thiếu LT, đã điểm danh...)
397            if (rollbackCheckIns !== null && currentUserDoc?.checkIns?.[vnMonth]) {
398                currentUserDoc.checkIns[vnMonth] = rollbackCheckIns;
399                if (currentUserDoc.total_checkins !== undefined) currentUserDoc.total_checkins -= 1;
400            }
401            renderCalendar();
402            if (window.showToast) window.showToast("Lỗi: " + (e.message || "Tài lực không đủ!"), "error");
403        } finally {
404            isLoading = false;
405        }
406    };
407
408    const doClaimMilestone = async (milestoneDays) => {
409        try {
410            const { getFunctions, httpsCallable, app } = await _getFnModule();
411            const fns = getFunctions(app, 'asia-southeast1');
412            const claimFn = httpsCallable(fns, 'claimMilestoneReward');
413            if (window.showToast) window.showToast('Đang xử lý...', 'info');
414            const result = await claimFn({ milestoneDays });
415            if (result.data.success) {
416                if (window.showToast) window.showToast('Nhận quà thành công! Vật phẩm đã vào túi đồ.', 'success');
417                await fetchUserCheckIns();
418            }
419        } catch (e) {
420            const msg = e?.message || 'Có lỗi xảy ra!';
421            if (window.showToast) window.showToast('Lỗi: ' + msg, 'error');
422        }
423    };
424
425    return {
426        openModal: triggerOpenModal,
427        closeModal,
428        doDailyCheckIn,
429        doMakeupCheckIn,
430        doClaimMilestone
431    };
432})();

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.