PageSourceSearch

https://omodajaecoo.com.my/combine/006bf9e8a6dbb07f5980dbe5df8944e3-1759313874

js omodajaecoo.com.my collected 2026-10-08 09:52:01 UTC 27,981 bytes, 814 lines download raw bytes

1/**
2 * Main Application Module
3 * Handles Swipers, News Loading, Share Functionality, and Calendar
4 */
5
6class AppManager {
7    constructor() {
8        this.newsManager = null;
9        this.calendar = null;
10        this.init();
11    }
12
13    init() {
14        // Wait for DOM to be ready
15        if (document.readyState === 'loading') {
16            document.addEventListener('DOMContentLoaded', () => this.initializeApp());
17        } else {
18            this.initializeApp();
19        }
20    }
21
22    initializeApp() {
23        this.initializeSwipers();
24        this.initializeShareButtons();
25        this.calendar = new CustomCalendar(this.newsManager);
26        this.newsManager = new NewsManager(this.calendar);
27        this.initializeCustomScrollbars();
28    }
29
30    /**
31     * Initialize all Swiper instances
32     */
33    initializeSwipers() {
34        const swiperConfigs = [
35            {
36                selector: '.global-specials-swiper',
37                options: {
38                    slidesPerView: 1,
39                    spaceBetween: 0,
40                    breakpoints: {
41                        640: { slidesPerView: 2, spaceBetween: 20 },
42                        969: { slidesPerView: 3, spaceBetween: 40 }
43                    },
44                    navigation: {
45                        nextEl: '.global-specials-slider-btn-next',
46                        prevEl: '.global-specials-slider-btn-prev'
47                    }
48                }
49            },
50            {
51                selector: '.event-holder .swiper',
52                options: {
53                    loop: false,
54                    slidesPerView: 1,
55                    spaceBetween: 0,
56                    breakpoints: {
57                        969: { slidesPerView: 2, spaceBetween: 30 }
58                    },
59                    navigation: {
60                        nextEl: '.event-slider-btn-next',
61                        prevEl: '.event-slider-btn-prev'
62                    }
63                }
64            }
65        ];
66
67        swiperConfigs.forEach(config => {
68            const element = document.querySelector(config.selector);
69            if (element) {
70                new Swiper(config.selector, config.options);
71            }
72        });
73    }
74
75    /**
76     * Initialize share button functionality
77     */
78    initializeCustomScrollbars() {
79        const containers = document.querySelectorAll('.event-items-container');
80
81        containers.forEach(container => {
82            const scrollbarContainer = container.parentElement.querySelector('.scrollbar-container');
83            if (!scrollbarContainer) return;
84            const scrollbar = scrollbarContainer.querySelector('.scrollbar');
85            if (!scrollbar) return;
86
87            const updateScrollbar = () => {
88                const scrollHeight = container.scrollHeight;
89                const clientHeight = container.clientHeight;
90                const scrollbarContainerHeight = scrollbarContainer.clientHeight;
91                // Calculate thumb height based on visible ratio (clientHeight / scrollHeight)
92                // Ensure the thumb is not too small for usability
93                const minThumbHeight = 30; // px – you can tweak this
94                let thumbHeight = (clientHeight / scrollHeight) * scrollbarContainerHeight;
95                thumbHeight = Math.max(minThumbHeight, Math.min(thumbHeight, scrollbarContainerHeight));
96                
97                if (scrollHeight <= clientHeight) {
98                    // No scrolling needed, hide scrollbar
99                    scrollbar.style.display = 'none';
100                    return;
101                }
102                
103                scrollbar.style.display = 'block';
104                
105                // Apply thumb height
106                scrollbar.style.height = `${thumbHeight}px`;
107
108                // Calculate how far we can move the thumb
109                const maxThumbPosition = scrollbarContainerHeight - thumbHeight;
110                
111                // Calculate current scroll percentage
112                const scrollTop = container.scrollTop;
113                const maxScroll = scrollHeight - clientHeight;
114                const scrollPercent = scrollTop / maxScroll;
115                
116                // Position the thumb
117                const thumbPosition = scrollPercent * maxThumbPosition;
118                scrollbar.style.top = `${thumbPosition}px`;
119            };
120
121            // Initialize
122            updateScrollbar();
123
124            // Bind events
125            container.addEventListener('scroll', updateScrollbar);
126            window.addEventListener('resize', updateScrollbar);
127        });
128    }
129
130    initializeShareButtons() {
131        const shareButtons = document.querySelectorAll('.share-btn');
132        
133        shareButtons.forEach(button => {
134            button.addEventListener('click', this.handleShare.bind(this));
135        });
136    }
137
138    /**
139     * Handle share button clicks
140     */
141    async handleShare(event) {
142        const button = event.currentTarget;
143        const link = button.dataset.link;
144
145        if (!navigator.share) {
146            console.warn('Web Share API not supported');
147            return;
148        }
149
150        const shareData = {
151            title: 'JAECOO',
152            text: 'Check out this JAECOO post!',
153            url: `${window.location.origin}${link}`
154        };
155
156        try {
157            await navigator.share(shareData);
158        } catch (error) {
159            if (error.name !== 'AbortError') {
160                console.error('Share failed:', error);
161            }
162        }
163    }
164}
165
166/**
167 * News Manager Class
168 * Handles loading and displaying news items
169 */
170class NewsManager {
171    constructor(calendar) {
172        this.containers = document.querySelectorAll('.news-container');
173        this.viewMoreBtn = document.getElementById('view-more');
174        this.initialItems = 6;
175        this.additionalItems = 3;
176        this.currentItems = this.initialItems;
177
178        // Elements for filtering
179        this.tabs = document.querySelectorAll('.tab-btn');
180        this.stateSelect = document.querySelector('.state-dropdown select');
181        this.eventItems = document.querySelectorAll('.event-item');
182        this.currentTab = 'Today';
183        this.calendar = calendar;
184        // Register to receive date selection events from the calendar
185        if (this.calendar && typeof this.calendar.setDateSelectCallback === 'function') {
186            this.calendar.setDateSelectCallback((date) => this.filterByDate(date));
187        }
188        
189        this.init();
190    }
191
192    init() {
193        if (this.containers.length === 0) return;
194        
195        this.showInitialItems();
196        this.setupViewMoreButton();
197        this.setupFilters();
198        this.applyFilters();
199    }
200
201    showInitialItems() {
202        this.containers.forEach((container, index) => {
203            container.style.display = index < this.initialItems ? 'flex' : 'none';
204        });
205    }
206
207    setupViewMoreButton() {
208        if (!this.viewMoreBtn) return;
209
210        if (this.containers.length <= this.initialItems) {
211            this.viewMoreBtn.style.display = 'none';
212            return;
213        }
214
215        // Bind the method to maintain context
216        this.viewMoreBtn.addEventListener('click', this.showMoreNews.bind(this));
217        
218        // Also expose globally for backward compatibility
219        window.showMoreNews = this.showMoreNews.bind(this);
220    }
221
222    // -----------------------------
223    // Filtering helpers
224    // -----------------------------
225
226    eventCount() {
227        const visibleItems = Array.from(this.eventItems).filter(el => {
228            return window.getComputedStyle(el).display !== 'none';
229        });
230        return visibleItems.length ? visibleItems.length : 0;
231    }
232
233    setupFilters() {
234        if (this.tabs.length === 0) return;
235
236        this.tabs.forEach(tab => {
237            tab.addEventListener('click', () => this.handleTabClick(tab));
238        });
239
240        if (this.stateSelect) {
241            this.stateSelect.addEventListener('change', () => this.applyFilters());
242        }
243
244        // Mobile dropdown for tab filtering
245        this.tabSelect = document.querySelector('.tab-select');
246        if (this.tabSelect) {
247            this.tabSelect.addEventListener('change', () => {
248                this.currentTab = this.tabSelect.value;
249                // Sync button active state
250                this.tabs.forEach(t => {
251                    if (t.textContent.trim() === this.currentTab) {
252                        t.classList.add('active');
253                    } else {
254                        t.classList.remove('active');
255                    }
256                });
257
258                // Highlight calendar accordingly
259                if (this.calendar) {
260                    if (this.currentTab === 'Today') {
261                        this.calendar.selectToday();
262                        this.calendar.selectionMode = 'date';
263                    } else if (this.currentTab === 'This Week') {
264                        this.calendar.selectCurrentWeek();
265                        this.calendar.selectionMode = 'week';
266                    } else if (this.currentTab === 'Next Week') {
267                        this.calendar.selectRelativeWeek(1);
268                        this.calendar.selectionMode = 'week';
269                    }
270                }
271
272                this.applyFilters();
273            });
274        }
275    }
276
277    
278
279    handleTabClick(tab) {
280        this.tabs.forEach(t => t.classList.remove('active'));
281        tab.classList.add('active');
282        this.currentTab = tab.textContent.trim();
283
284        // Highlight calendar accordingly
285        if (this.calendar) {
286            if (this.currentTab === 'Today') {
287                this.calendar.selectToday();
288            } else if (this.currentTab === 'This Week') {
289                this.calendar.selectCurrentWeek();
290            } else if (this.currentTab === 'Next Week') {
291                this.calendar.selectRelativeWeek(1);
292            }
293
294        }
295
296        this.applyFilters();
297    }
298
299    getDateRange() {
300        const today = new Date();
301        today.setHours(0, 0, 0, 0);
302        let start = new Date(today);
303        let end = new Date(today);
304
305        if (this.currentTab === 'This Week' || this.currentTab === 'Next Week') {
306            const dayOfWeek = today.getDay(); // 0 (Sun) - 6 (Sat)
307            // Start of current week (Sunday)
308            start.setDate(today.getDate() - dayOfWeek);
309            end = new Date(start);
310            end.setDate(start.getDate() + 6);
311
312            if (this.currentTab === 'Next Week') {
313                start.setDate(start.getDate() + 7);
314                end.setDate(end.getDate() + 7);
315            }
316        }
317
318        return { start, end };
319    }
320
321    applyFilters() {
322        if (this.eventItems.length === 0) return;
323
324        const stateFilter = this.stateSelect && this.stateSelect.value ? this.stateSelect.value : null;
325        const { start, end } = this.getDateRange();
326
327        this.eventItems.forEach(item => {
328            const eventStartStr = item.dataset.eventStartDate;
329            const eventEndStr = item.dataset.eventEndDate || eventStartStr;
330            const eventState = item.dataset.eventState;
331
332            const showByState = !stateFilter || stateFilter === eventState;
333            let showByDate = true;
334
335            const parseDate = (str) => {
336                if (!str) return null;
337                if (str.includes('-')) {
338                    const [y, m, d] = str.split('-').map(Number);
339                    return new Date(y, m - 1, d);
340                }
341                return new Date(str);
342            };
343
344            if (eventStartStr) {
345                const eventStart = parseDate(eventStartStr);
346                const eventEnd = parseDate(eventEndStr);
347                eventStart.setHours(0, 0, 0, 0);
348                eventEnd.setHours(23, 59, 59, 999);
349                // Show the event if its date range overlaps with the current filter range
350                showByDate = eventStart <= end && eventEnd >= start;
351            }
352
353            item.style.display = (showByState && showByDate) ? 'flex' : 'none';
354        });
355
356        this.calendar.updateSelectionInfo(this.eventCount());
357    }
358
359    /**
360     * Filter events by a specific date
361     * @param {Date} date
362     */
363    filterByDate(date) {
364        const stateFilter = this.stateSelect && this.stateSelect.value ? this.stateSelect.value : null;
365        const start = new Date(date);
366        start.setHours(0,0,0,0);
367        const end = new Date(date);
368        end.setHours(23,59,59,999);
369
370        this.eventItems.forEach(item => {
371            const eventStartStr = item.dataset.eventStartDate;
372            const eventEndStr = item.dataset.eventEndDate || eventStartStr;
373            const eventState = item.dataset.eventState;
374
375            const showByState = !stateFilter || stateFilter === eventState;
376            let showByDate = true;
377
378            const parseDate = (str) => {
379                if (!str) return null;
380                if (str.includes('-')) {
381                    const [y, m, d] = str.split('-').map(Number);
382                    return new Date(y, m - 1, d);
383                }
384                return new Date(str);
385            };
386
387            if (eventStartStr) {
388                const eventStart = parseDate(eventStartStr);
389                const eventEnd = parseDate(eventEndStr);
390                eventStart.setHours(0, 0, 0, 0);
391                eventEnd.setHours(23, 59, 59, 999);
392                const selectedTime = start.getTime(); // Beginning of selected day
393                showByDate = eventStart.getTime() <= selectedTime && eventEnd.getTime() >= selectedTime;
394            }
395
396            item.style.display = (showByState && showByDate) ? 'flex' : 'none';
397        });
398
399        if (this.calendar && typeof this.calendar.updateSelectionInfo === 'function') {
400            this.calendar.updateSelectionInfo(this.eventCount());
401        }
402    }
403
404    // -----------------------------
405    // Existing methods
406    // -----------------------------
407    showMoreNews() {
408        const endIndex = Math.min(
409            this.currentItems + this.additionalItems, 
410            this.containers.length
411        );
412
413        for (let i = this.currentItems; i < endIndex; i++) {
414            this.containers[i].style.display = 'flex';
415        }
416
417        this.currentItems = endIndex;
418
419        if (this.currentItems >= this.containers.length) {
420            this.viewMoreBtn.style.display = 'none';
421        }
422    }
423}
424
425/**
426 * Custom Calendar Class
427 * Handles calendar functionality with date and week selection
428 */
429class CustomCalendar {
430    constructor(newsManager) {
431        this.newsManager = newsManager;
432        this.currentDate = new Date();
433        this.selectedDates = new Set();
434        this.selectedWeek = null;
435        this.selectedWeekRef = null;
436        this.selectionMode = 'date';
437        this.eventDates = new Set();
438        
439        this.monthNames = [
440            'January', 'February', 'March', 'April', 'May', 'June',
441            'July', 'August', 'September', 'October', 'November', 'December'
442        ];
443
444        this.noEvent = document.querySelector('.no-event');
445
446        // Cache DOM elements
447        this.elements = {
448            prevMonth: document.getElementById('prevMonth'),
449            nextMonth: document.getElementById('nextMonth'),
450            monthYear: document.getElementById('monthYear'),
451            calendarWeeks: document.getElementById('calendarWeeks'),
452            selectionInfo: document.getElementById('selectionInfo')
453        };
454        
455        this.init();
456    }
457    
458    init() {
459        if (!this.validateElements()) return;
460        
461        this.bindEvents();
462        // Collect initial event dates from DOM before first render
463        this.updateEventDatesFromDOM(false);
464        this.render();
465        
466        // Auto-select today's date on init
467        this.selectToday();
468        
469        // Expose globally for backward compatibility
470        window.calendar = this;
471    }
472
473    validateElements() {
474        return Object.values(this.elements).every(element => element !== null);
475    }
476    
477    bindEvents() {
478        this.elements.prevMonth.addEventListener('click', () => {
479            this.currentDate.setMonth(this.currentDate.getMonth() - 1);
480            this.updateWeekSelectionForMonth();
481            this.render();
482        });
483
484        this.elements.nextMonth.addEventListener('click', () => {
485            this.currentDate.setMonth(this.currentDate.getMonth() + 1);
486            this.updateWeekSelectionForMonth();
487            this.render();
488        });
489    }
490
491    /**
492     * Update selectedWeek based on stored reference date when month changes
493     */
494    updateWeekSelectionForMonth() {
495        if (!this.selectedWeekRef) {
496            this.selectedWeek = null;
497            return;
498        }
499
500        console.log(this)
501        const ref = this.selectedWeekRef;
502        if (ref.getFullYear() === this.currentDate.getFullYear() && ref.getMonth() === this.currentDate.getMonth()) {
503            // Compute week index of ref in current month view
504            const firstDay = new Date(this.currentDate.getFullYear(), this.currentDate.getMonth(), 1);
505            const startDate = new Date(firstDay);
506            startDate.setDate(startDate.getDate() - firstDay.getDay());
507            const diffDays = Math.floor((ref - startDate) / (24 * 60 * 60 * 1000));
508            this.selectedWeek = Math.floor(diffDays / 7);
509            // this.selectionMode = 'week';
510        } else {
511            this.selectedWeek = null;
512            // this.selectionMode = 'date';
513        }
514    }
515
516    render() {
517        this.renderHeader();
518        this.renderCalendar();
519        this.updateSelectionInfo();
520    }
521    
522    renderHeader() {
523        const monthYear = `${this.monthNames[this.currentDate.getMonth()]} ${this.currentDate.getFullYear()}`;
524        this.elements.monthYear.textContent = monthYear;
525    }
526    
527    renderCalendar() {
528        // Helper to build YYYY-MM-DD in local time
529    }
530
531    /**
532     * Format a Date object as YYYY-MM-DD using local timezone
533     */
534    formatDate(dateObj) {
535        const y = dateObj.getFullYear();
536        const m = String(dateObj.getMonth() + 1).padStart(2, '0');
537        const d = String(dateObj.getDate()).padStart(2, '0');
538        return `${y}-${m}-${d}`;
539    }
540
541    renderCalendar() {
542        this.elements.calendarWeeks.innerHTML = '';
543        
544        const year = this.currentDate.getFullYear();
545        const month = this.currentDate.getMonth();
546        const firstDay = new Date(year, month, 1);
547        const startDate = new Date(firstDay);
548        startDate.setDate(startDate.getDate() - firstDay.getDay());
549        
550        const today = new Date();
551        today.setHours(0, 0, 0, 0);
552        
553        // Create document fragment for better performance
554        const fragment = document.createDocumentFragment();
555        
556        // Generate 6 weeks
557        for (let week = 0; week < 5; week++) {
558            const weekRow = this.createWeekRow(week, startDate, month, today);
559            fragment.appendChild(weekRow);
560        }
561        
562        this.elements.calendarWeeks.appendChild(fragment);
563    }
564
565    createWeekRow(week, startDate, month, today) {
566        const weekRow = document.createElement('div');
567        weekRow.className = 'calendar-week';
568        weekRow.dataset.week = week;
569        
570        // Add week click handler with event delegation
571        // weekRow.addEventListener('click', (e) => {
572        //     if (!e.target.classList.contains('calendar-day')) {
573        //         this.selectWeek(week);
574        //     }
575        // });
576        
577        // Create days for this week
578        for (let day = 0; day < 7; day++) {
579            const currentDate = new Date(startDate);
580            currentDate.setDate(startDate.getDate() + (week * 7) + day);
581            
582            const dayElement = this.createDayElement(currentDate, month, week, today);
583            weekRow.appendChild(dayElement);
584        }
585        
586        if (this.selectedWeek === week && this.selectionMode === 'week') {
587            weekRow.classList.add('selected-week');
588        }
589        
590        return weekRow;
591    }
592
593    createDayElement(currentDate, month, week, today) {
594        const dayElement = document.createElement('div');
595        dayElement.className = 'calendar-day';
596        dayElement.textContent = currentDate.getDate();
597        
598        const dateString = this.formatDate(currentDate);
599
600        dayElement.dataset.date = dateString;
601        
602        // Add appropriate classes
603        const classes = [];
604        if (currentDate.getMonth() !== month) classes.push('other-month');
605        if (currentDate.getTime() === today.getTime()) classes.push('today');
606        if (this.selectedDates.has(dateString)) classes.push('selected');
607        if (this.selectedWeek !== null && this.selectedWeek === week && currentDate.getMonth() === month) {
608            classes.push('in-selected-week');
609        }
610        // Mark dates that have at least one event on that day
611        if (this.eventDates.has(dateString)) classes.push('has-event');
612        
613        dayElement.classList.add(...classes);
614        
615        // Add click handler
616        dayElement.addEventListener('click', (e) => {
617            e.stopPropagation();
618            this.selectDate(dateString);
619            app.calendar.selectionMode = 'date';
620
621            if(dateString === this.formatDate(today)) {
622                app.newsManager.handleTabClick(app.newsManager.tabs[0]);
623                return;
624            }
625            else {
626                app.newsManager.tabs.forEach(t => t.classList.remove('active'));
627            }
628        });
629        
630        return dayElement;
631    }
632    
633    /**
634     * Scan the DOM for .event-item elements and populate eventDates set.
635     * If onlyVisible is true, only consider currently visible event items
636     * (after filters), otherwise include all.
637     */
638    updateEventDatesFromDOM(onlyVisible = false) {
639        this.eventDates.clear();
640        const items = document.querySelectorAll('.event-item');
641        const parseDate = (str) => {
642            if (!str) return null;
643            if (str.includes('-')) {
644                const [y, m, d] = str.split('-').map(Number);
645                return new Date(y, m - 1, d);
646            }
647            return new Date(str);
648        };
649
650        items.forEach(item => {
651            if (onlyVisible) {
652                const style = window.getComputedStyle(item);
653                if (style.display === 'none') return;
654            }
655
656            const startStr = item.dataset.eventStartDate;
657            const endStr = item.dataset.eventEndDate || startStr;
658            const start = parseDate(startStr);
659            const end = parseDate(endStr);
660            if (!start || !end) return;
661            start.setHours(0,0,0,0);
662            end.setHours(0,0,0,0);
663
664            // Add each date in the inclusive range to the set
665            const cur = new Date(start);
666            while (cur.getTime() <= end.getTime()) {
667                this.eventDates.add(this.formatDate(cur));
668                cur.setDate(cur.getDate() + 1);
669            }
670        });
671    }
672    
673    selectDate(dateString) {
674        this.selectedWeek = null;
675        this.selectionMode = 'date';
676        
677        // Toggle date selection
678        if (this.selectedDates.has(dateString)) {
679            // this.selectedDates.delete(dateString);
680            return;
681        } else {
682            this.selectedDates.clear(); // Single selection
683            this.selectedDates.add(dateString);
684        }
685        
686        this.render();
687        
688        // Notify external listener (e.g., NewsManager) about selected date
689        if (typeof this.dateSelectCallback === 'function') {
690            this.dateSelectCallback(new Date(dateString));
691        }
692    }
693    
694    selectWeek(weekIndex) {
695        this.selectedDates.clear();
696        this.selectionMode = 'week';
697        // Toggle week selection
698        this.selectedWeek = weekIndex;
699        this.render();
700    }
701    
702    setDateSelectCallback(callback) {
703        this.dateSelectCallback = callback;
704    }
705
706    updateSelectionInfo(eventCount) {
707        let content = '';
708        
709        // if (this.selectionMode === 'date' && this.selectedDates.size > 0) {
710        //     const dates = Array.from(this.selectedDates)
711        //         .map(date => new Date(date).toLocaleDateString())
712        //         .join(', ');
713        //     content = `<span class="selection-type">Selected Date:</span> ${dates}`;
714        //     content += '<button class="clear-btn" onclick="calendar.clearSelection()">Clear</button>';
715        // } else if (this.selectionMode === 'week' && this.selectedWeek !== null) {
716        //     const weekDates = this.getWeekDates(this.selectedWeek);
717        //     content = `<span class="selection-type">Selected Week:</span> ${weekDates}`;
718        //     content += '<button class="clear-btn" onclick="calendar.clearSelection()">Clear</button>';
719        // } else {
720        //     content = 'Total Events: ';
721        // }
722
723        content = 'Total Events: ' + (eventCount ?? 0);
724
725        if(eventCount === 0) {
726            this.noEvent.classList.remove('hidden');
727        } else {
728            this.noEvent.classList.add('hidden');
729        }
730        
731        this.elements.selectionInfo.innerHTML = content;
732    }
733    
734    getWeekDates(weekIndex) {
735        const year = this.currentDate.getFullYear();
736        const month = this.currentDate.getMonth();
737        const firstDay = new Date(year, month, 1);
738        const startDate = new Date(firstDay);
739        startDate.setDate(startDate.getDate() - firstDay.getDay());
740        
741        const weekStart = new Date(startDate);
742        weekStart.setDate(startDate.getDate() + (weekIndex * 7));
743        
744        const weekEnd = new Date(weekStart);
745        weekEnd.setDate(weekStart.getDate() + 6);
746        
747        return `${weekStart.toLocaleDateString()} - ${weekEnd.toLocaleDateString()}`;
748    }
749    
750    clearSelection() {
751        this.selectedDates.clear();
752        this.selectedWeek = null;
753        this.selectedWeekRef = null;
754        this.render();
755    }
756
757    // -----------------------------
758    // Convenience selection helpers
759    // -----------------------------
760    selectToday() {
761        const todayStr = this.formatDate(new Date());
762        this.selectDate(todayStr);
763    }
764
765    /**
766     * Select the calendar week that contains the provided date
767     * @param {Date} dateObj
768     */
769    selectWeekByDate(dateObj) {
770        // Ensure the calendar is on the same month as the target date
771        this.currentDate = new Date(dateObj.getFullYear(), dateObj.getMonth(), 1);
772
773        const firstDay = new Date(this.currentDate.getFullYear(), this.currentDate.getMonth(), 1);
774        const startDate = new Date(firstDay);
775        startDate.setDate(startDate.getDate() - firstDay.getDay());
776
777        const diffDays = Math.floor((dateObj - startDate) / (24 * 60 * 60 * 1000));
778        const weekIndex = Math.floor(diffDays / 7);
779
780        this.selectWeek(weekIndex);
781        this.selectedWeekRef = new Date(dateObj.getFullYear(), dateObj.getMonth(), dateObj.getDate());
782    }
783
784    /** Highlight current week */
785    selectCurrentWeek() {
786        this.selectWeekByDate(new Date());
787    }
788
789    /** Highlight week relative to current (e.g., +1 for next week, -1 for previous) */
790    selectRelativeWeek(offset) {
791        const ref = new Date();
792        ref.setDate(ref.getDate() + (offset * 7));
793        this.selectWeekByDate(ref);
794    }
795    
796    // Public API methods
797    getSelectedDates() {
798        return Array.from(this.selectedDates);
799    }
800    
801    getSelectedWeek() {
802        return this.selectedWeek === null ? null : this.getWeekDates(this.selectedWeek);
803    }
804}
805
806// Initialize the application
807const app = new AppManager();
808
809// Legacy jQuery support (if needed)
810if (typeof $ !== 'undefined') {
811    $(document).ready(() => {
812        console.log('jQuery compatibility layer loaded');
813    });
814}

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.