import { COLORS, SUBTYPE_COLORS, SURVEILLANCE_COLORS } from "./globals.js"; let trendChart; let influenzaSubtypeChart; let covidDistributedByAgeChart; let covidLineageFrequencyChart; let influenzaSubtypeFrequencyChart; let map; let subtypeLayers = {}; let hiddenSubtypes = new Set(); /* |-------------------------------------------------------------------------- | Load Summary Cards |-------------------------------------------------------------------------- */ function loadSummary() { fetch('/api/dashboard/summary') .then(res => res.json()) .then(data => { let html = ''; const alerts = []; data.forEach(item => { let trendColor = 'text-secondary'; if (item.percent_change > 0) trendColor = 'text-danger'; if (item.percent_change < 0) trendColor = 'text-success'; html += `
${item.code} Cases

${item.current_total}

Last 7 days
${item.percent_change > 0 ? '▲' : item.percent_change < 0 ? '▼' : '–'} ${Math.abs(item.percent_change)}%
${item.previous_total ?? 0} last week
`; window._summaryData = data; updateAlerts(); }); document.getElementById('summary_cards').innerHTML = html; renderAlerts(alerts); }); } /* |-------------------------------------------------------------------------- | Load Trend Chart |-------------------------------------------------------------------------- */ function loadTrend(periodType, startYear, startWeek, endYear, endWeek) { fetch(`/api/dashboard/trend?period_type=${periodType}&start_year=${startYear}&start_week=${startWeek}&end_year=${endYear}&end_week=${endWeek}`) .then(res => res.json()) .then(data => { if (trendChart) trendChart.destroy(); const totalYears = endYear - startYear; const useYearlyView = totalYears >= 5; // const labelsSet = new Set(); // Object.values(data).forEach(program => { // program.forEach(row => { // labelsSet.add(`${row.year}-${row.period}`); // }); // }); // const labels = Array.from(labelsSet).sort((a, b) => { // const [yearA, weekA] = a.split('-').map(Number); // const [yearB, weekB] = b.split('-').map(Number); // if (yearA !== yearB) return yearA - yearB; // return weekA - weekB; // }); let labels = []; if (useYearlyView) { labels = [...new Set( Object.values(data) .flat() .map(row => row.year) )].sort((a, b) => a - b); } else { const labelsSet = new Set(); Object.values(data).forEach(program => { program.forEach(row => { labelsSet.add(`${row.year}-${row.period}`); }); }); labels = Array.from(labelsSet).sort((a, b) => { const [yearA, weekA] = a.split('-').map(Number); const [yearB, weekB] = b.split('-').map(Number); if (yearA !== yearB) return yearA - yearB; return weekA - weekB; }); } const datasets = []; const allowedPrograms = ['SARI', 'ILI', 'LBM', 'AFI', 'NDS']; Object.keys(data).forEach(code => { if (!allowedPrograms.includes(code)) return; // const values = labels.map(label => { // const found = data[code].find(row => `${row.year}-${row.period}` === label); // return found ? found.total : 0; // }); const values = labels.map(label => { if (useYearlyView) { return data[code] .filter(row => row.year == label) .reduce((sum, row) => sum + row.total, 0); } const found = data[code].find( row => `${row.year}-${row.period}` === label ); return found ? found.total : 0; }); datasets.push({ label: code, data: values, borderColor: SURVEILLANCE_COLORS[code], backgroundColor: SURVEILLANCE_COLORS[code], borderWidth: 3, pointRadius: 4, maxBarThickness: 50, fill: false, tension: 0.3 }); }); // const displayLabels = labels.map(l => { // const [year, week] = l.split('-'); // return `${year}-W${String(week).padStart(2, '0')}`; // }); const displayLabels = useYearlyView ? labels : labels.map(l => { const [year, week] = l.split('-'); return `${year}-W${String(week).padStart(2, '0')}`; }); trendChart = new Chart(document.getElementById('trendChart'), { type: 'line', data: { labels: displayLabels, datasets: datasets }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'bottom' }, datalabels: { display: false } }, interaction: { mode: 'index', intersect: false }, scales: { y: { beginAtZero: true, title: { display: true, text: 'Number of Cases' }, }, x: { grid: { display: false }, title: { display: true, text: 'Surveillance' }, } } } }); charts['trendChart'] = trendChart; }); } function loadInfluenzaSubtypeDistribution(periodType, startYear, startWeek, endYear, endWeek) { fetch(`/api/dashboard/influenza-subtype-distribution?period_type=${periodType}&start_year=${startYear}&start_week=${startWeek}&end_year=${endYear}&end_week=${endWeek}`) .then(res => res.json()) .then(data => { let displayLabels = data.map(item => item.subtype); let dataset = data.map(item => item.total); // const colors = displayLabels.map( // label => SUBTYPE_COLORS[label] || '#9ca3af' // ); if (influenzaSubtypeChart) influenzaSubtypeChart.destroy(); influenzaSubtypeChart = new Chart(document.getElementById('influenzaSubtypeDistribution'), { type: 'bar', data: { labels: displayLabels, datasets: [{ data: dataset, backgroundColor: displayLabels.map( label => SUBTYPE_COLORS[label] || '#9ca3af' ), }] }, options: { layout: { padding: { top: 20, right: 30, bottom: 20 } }, indexAxis: 'y', responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false, position: 'right' }, datalabels: { color: '#000000', borderRadius: 6, z: 1000, padding: { top: 6, bottom: 6, }, font: { weight: 'bold', size: 12 }, formatter: (value) => value, anchor: 'end', align: 'end', offset: 4, clamp: true, clip: false } }, scales: { x: { stacked: true, beginAtZero: true, title: { display: true, text: 'Number of Positive Influenza Subtypes' } }, y: { stacked: true, beginAtZero: true, title: { display: true, text: 'Influenza Subtypes' }, grid: { display: false } } } }, plugins: [ChartDataLabels] }); charts['influenzaSubtypeDistribution'] = influenzaSubtypeChart; }); } function loadCovidDistributedByAgeGroup(periodType, startYear, startWeek, endYear, endWeek) { fetch(`/api/dashboard/covid-distributed-by-age-group?period_type=${periodType}&start_year=${startYear}&start_week=${startWeek}&end_year=${endYear}&end_week=${endWeek}`) .then(res => res.json()) .then(data => { let displayLabels = data.map(item => item.age_group); let dataset = data.map(item => item.total); if (covidDistributedByAgeChart) covidDistributedByAgeChart.destroy(); covidDistributedByAgeChart = new Chart(document.getElementById('covidDistributedByAgeGroup'), { type: 'bar', data: { labels: displayLabels, datasets: [{ label: 'Total Covid-19 Detected', data: dataset, backgroundColor: COLORS, maxBarThickness: 50 }] }, options: { layout: { padding: { top: 50, bottom: 10, } }, responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false, position: 'bottom' }, datalabels: { color: '#000', borderRadius: 6, z: 1000, padding: { top: 6, bottom: 6, left: 10, right: 10 }, font: { weight: 'bold', size: 12 }, formatter: (value) => value, anchor: 'end', align: 'end', offset: 4, clamp: true, clip: false } }, scales: { x: { stacked: true, beginAtZero: true, title: { display: true, text: 'Patient Age Group' }, grid: { display: false } }, y: { stacked: true, beginAtZero: true, title: { display: true, text: 'Number of Positive SARS-CoV-2' } } } } }); charts['covidDistributedByAgeGroup'] = covidDistributedByAgeChart; }); } function loadCovidLineageFrequency(periodType, startYear, startWeek, endYear, endWeek) { fetch(`/api/dashboard/covid-lineage-frequency?period_type=${periodType}&start_year=${startYear}&start_week=${startWeek}&end_year=${endYear}&end_week=${endWeek}`) .then(res => res.json()) .then(data => { // const weeks = [...new Set(data.map(item => item.week))].sort(); const totalYears = endYear - startYear; const useYearlyView = totalYears >= 5; let periods; if (useYearlyView) { periods = [...new Set( data.map(item => item.week.split('-')[0]) )].sort((a, b) => a - b); } else { periods = [...new Set( data.map(item => item.week) )].sort(); } const lineages = [...new Set(data.map(item => item.lineage))]; const datasets = lineages.map((lineage, index) => { // const lineageData = weeks.map(week => { // const found = data.find( // item => item.week === week && item.lineage === lineage // ); // return found ? found.total : 0; // }); const lineageData = periods.map(period => { if (useYearlyView) { return data .filter(item => item.week.split('-')[0] == period && item.lineage === lineage ) .reduce((sum, item) => sum + item.total, 0); } const found = data.find( item => item.week === period && item.lineage === lineage ); return found ? found.total : 0; }); return { label: lineage, data: lineageData, fill: true, tension: 0.4, // borderColor: 'transparent', borderWidth: 0, pointRadius: 0, backgroundColor: hexToRGBA(colors[index % colors.length], 0.3), stack: 'total' }; }); if (covidLineageFrequencyChart) covidLineageFrequencyChart.destroy(); const ctx = document.getElementById('covidLineageFrequency').getContext('2d'); covidLineageFrequencyChart = new Chart(ctx, { type: 'line', data: { labels: periods, datasets: datasets }, options: { responsive: true, maintainAspectRatio: false, interaction: { mode: 'index', intersect: false }, plugins: { legend: { display: false }, tooltip: { mode: 'index', intersect: false }, datalabels: { display: false } }, scales: { x: { stacked: true, grid: { display: false } }, y: { stacked: true, beginAtZero: true, title: { display: true, text: 'Relative Frequency' }, } } } }); charts['covidLineageFrequency'] = covidLineageFrequencyChart; // ------------------------- // Custom right-side scrollable legend // ------------------------- const legendContainer = document.getElementById('legendContainer'); legendContainer.innerHTML = ''; datasets.forEach((dataset, index) => { const item = document.createElement('div'); item.style.display = 'flex'; item.style.alignItems = 'center'; item.style.marginBottom = '4px'; item.style.fontSize = '11px'; item.style.cursor = 'pointer'; item.innerHTML = ` ${dataset.label} `; item.addEventListener('click', () => { const meta = covidLineageFrequencyChart.getDatasetMeta(index); const allHidden = datasets.every((d, i) => covidLineageFrequencyChart.getDatasetMeta(i).hidden || i === index); if (!allHidden) { datasets.forEach((d, i) => { covidLineageFrequencyChart.getDatasetMeta(i).hidden = true; }); meta.hidden = false; } else { datasets.forEach((d, i) => { covidLineageFrequencyChart.getDatasetMeta(i).hidden = false; }); } covidLineageFrequencyChart.update(); Array.from(legendContainer.children).forEach((child, i) => { const metaItem = covidLineageFrequencyChart.getDatasetMeta(i); child.style.opacity = metaItem.hidden ? 0.5 : 1; }); }); legendContainer.appendChild(item); }); legendContainer.style.maxHeight = '375px'; legendContainer.style.overflowY = 'auto'; legendContainer.style.padding = '8px'; legendContainer.style.borderRadius = '0px'; }); } function loadInfluenzaSubtypeFrequency(periodType, startYear, startWeek, endYear, endWeek) { fetch(`/api/dashboard/influenza-relative-frequency?period_type=${periodType}&start_year=${startYear}&start_week=${startWeek}&end_year=${endYear}&end_week=${endWeek}`) .then(res => res.json()) .then(data => { const totalYears = endYear - startYear; const useYearlyView = totalYears >= 5; let periods; if (useYearlyView) { periods = [...new Set( data.map(item => item.week.split('-')[0]) )].sort((a, b) => a - b); } else { periods = [...new Set( data.map(item => item.week) )].sort(); } const lineages = [...new Set( data.map(item => item.lineage) )]; const lineageColors = lineages.map( label => SUBTYPE_COLORS[label] || '#9ca3af' ); const datasets = lineages.map((lineage, index) => { const lineageData = periods.map(period => { if (useYearlyView) { return data .filter(item => item.week.split('-')[0] == period && item.lineage === lineage ) .reduce((sum, item) => sum + item.total, 0); } const found = data.find( item => item.week === period && item.lineage === lineage ); return found ? found.total : 0; }); return { label: lineage, data: lineageData, fill: true, tension: 0.4, borderWidth: 0, pointRadius: 0, backgroundColor: hexToRGBA( lineageColors[index % lineageColors.length], 0.6 ), stack: 'total' }; }); if (influenzaSubtypeFrequencyChart) { influenzaSubtypeFrequencyChart.destroy(); } const ctx = document .getElementById('influenzaSubtypeFrequency') .getContext('2d'); influenzaSubtypeFrequencyChart = new Chart(ctx, { type: 'line', data: { labels: periods, datasets: datasets }, options: { responsive: true, maintainAspectRatio: false, interaction: { mode: 'index', intersect: false }, plugins: { legend: { display: false }, tooltip: { mode: 'index', intersect: false }, datalabels: { display: false } }, scales: { x: { stacked: true, grid: { display: false } }, y: { stacked: true, beginAtZero: true, title: { display: true, text: 'Relative Frequency' } } } } }); charts['influenzaSubtypeFrequency'] = influenzaSubtypeFrequencyChart; /* |-------------------------------------------------------------------------- | CUSTOM LEGEND |-------------------------------------------------------------------------- */ const legendContainer = document.getElementById( 'legendContainerInfluenzaSubtypeFrequency' ); legendContainer.innerHTML = ''; datasets.forEach((dataset, index) => { const item = document.createElement('div'); item.style.display = 'flex'; item.style.alignItems = 'center'; item.style.marginBottom = '4px'; item.style.fontSize = '11px'; item.style.cursor = 'pointer'; item.innerHTML = ` ${dataset.label} `; item.addEventListener('click', () => { const meta = influenzaSubtypeFrequencyChart .getDatasetMeta(index); const allHidden = datasets.every( (d, i) => influenzaSubtypeFrequencyChart .getDatasetMeta(i).hidden || i === index ); if (!allHidden) { datasets.forEach((d, i) => { influenzaSubtypeFrequencyChart .getDatasetMeta(i) .hidden = true; }); meta.hidden = false; } else { datasets.forEach((d, i) => { influenzaSubtypeFrequencyChart .getDatasetMeta(i) .hidden = false; }); } influenzaSubtypeFrequencyChart.update(); Array.from(legendContainer.children) .forEach((child, i) => { const metaItem = influenzaSubtypeFrequencyChart .getDatasetMeta(i); child.style.opacity = metaItem.hidden ? 0.5 : 1; }); }); legendContainer.appendChild(item); }); legendContainer.style.maxHeight = '375px'; legendContainer.style.overflowY = 'auto'; legendContainer.style.padding = '8px'; legendContainer.style.borderRadius = '0px'; }); } function hexToRGBA(hex, alpha) { const r = parseInt(hex.slice(1, 3), 16); const g = parseInt(hex.slice(3, 5), 16); const b = parseInt(hex.slice(5, 7), 16); return `rgba(${r}, ${g}, ${b}, ${alpha})`; } function updateAlerts() { if (!window._summaryData || !window._provinceData) return; const raw = buildAlerts(window._summaryData, window._provinceData); const finalAlerts = processAlerts(raw); renderAlerts(finalAlerts); } function generateAlerts(data) { const alerts = []; const summary = data.summary || {}; const programs = [ { key: 'influenza_rate', label: 'Influenza' }, { key: 'covid_rate', label: 'COVID-19' }, { key: 'positivity_rate', label: 'Overall positivity' } ]; programs.forEach(p => { const current = summary[p.key]?.current || 0; const previous = summary[p.key]?.previous || 0; const diff = previous ? ((current - previous) / previous) * 100 : 0; if (current >= 15) { alerts.push(`🔴 High ${p.label} (${current}%)`); } else if (current >= 10) { alerts.push(`🟠 Moderate ${p.label} (${current}%)`); } if (diff >= 10) { alerts.push(`🟡 Increasing ${p.label} (+${diff.toFixed(1)}%)`); } }); // ------------------------- // 2. Province-level alerts // ------------------------- const provinces = data.province_distribution || []; const top = [...provinces] .sort((a, b) => b.total - a.total) .slice(0, 3); top.forEach(p => { const percent = p.total ? ((p.positive / p.total) * 100) : 0; if (percent >= 15) { alerts.push(`🔴 High positivity in ${p.patient_province} (${percent.toFixed(1)}%)`); } else if (percent >= 10) { alerts.push(`🟠 Moderate positivity in ${p.patient_province}`); } if (p.total >= 50) { alerts.push(`🟡 High case volume in ${p.patient_province} (${p.total})`); } }); // ------------------------- // fallback // ------------------------- if (!alerts.length) { alerts.push("🟢 No unusual activity detected"); } return alerts; } function createAlert(type, message, priority) { return { type, message, priority }; } function buildAlerts(summaryData, provinceData) { const alerts = []; summaryData.forEach(item => { if (item.current_total >= 80) { alerts.push(createAlert( 'high', `High ${item.code} activity (${item.current_total} cases)`, 1 )); } // 🟠 Moderate else if (item.current_total >= 40) { alerts.push(createAlert( 'moderate', `${item.code} activity elevated (${item.current_total})`, 2 )); } // 🟡 Increasing trend if (item.percent_change >= 10) { alerts.push(createAlert( 'trend', `Increasing ${item.code} (+${item.percent_change}%)`, 3 )); } }); // ------------------------- // 2. Province alerts // ------------------------- const top = [...provinceData] .sort((a, b) => b.total - a.total) .slice(0, 5); top.forEach(p => { const percent = p.total ? ((p.positive / p.total) * 100) : 0; // 🔴 High positivity if (percent >= 15) { alerts.push(createAlert( 'high', `High positivity in ${p.patient_province} (${percent.toFixed(1)}%)`, 1 )); } // 🟠 Moderate positivity else if (percent >= 10) { alerts.push(createAlert( 'moderate', `Moderate positivity in ${p.patient_province}`, 2 )); } // 🟡 High volume if (p.total >= 50) { alerts.push(createAlert( 'volume', `High case volume in ${p.patient_province} (${p.total})`, 3 )); } }); return alerts; } function processAlerts(alerts) { const seen = new Set(); const unique = alerts.filter(a => { if (seen.has(a.message)) return false; seen.add(a.message); return true; }); // sort by priority unique.sort((a, b) => a.priority - b.priority); // limit to top 5 return unique.slice(0, 5); } function renderAlerts(alerts) { const container = document.getElementById('alertsList'); if (!container) return; if (!alerts.length) { container.innerHTML = `
  • 🟢 No unusual activity detected
  • `; return; } const colorMap = { high: 'text-danger', moderate: 'text-warning', trend: 'text-primary', volume: 'text-secondary' }; container.innerHTML = alerts.map(a => `
  • ${a.type === 'high' ? '🔴' : a.type === 'moderate' ? '🟠' : a.type === 'trend' ? '🟡' : '🔵'} ${a.message}
  • `).join(''); } function normalizeProvince(name, validSet) { if (!name || !validSet) return null; const clean = str => str.toLowerCase().replace(/\s+/g, ''); const raw = name.trim(); const map = { "kepville": "Kep", "sihanoukville": "Preah Sihanouk", "sihanoukvillecity": "Preah Sihanouk", "krongpailin": "Pailin", "mondulkiri": "Mondulkiri", "odormeanchey": "Oddar Meanchey", "tbongkhmom": "Tboung Khmum", "tboungkhmum": "Tboung Khmum", "rattanakiri": "Ratanak Kiri" }; const key = clean(raw); if (map[key] && validSet.has(map[key])) { return map[key]; } const match = [...validSet].find(p => clean(p) === key); return match || null; } function getRadius(total) { if (!total) return 0; const r = Math.sqrt(total); return Math.max(4, Math.min(r * 2, 22)); } function getPositivityColor(subtype) { return SUBTYPE_COLORS[subtype] || "#9ca3af"; } const getColorByPathogen = name => SUBTYPE_COLORS[name] || '#9ca3af'; function addPositivityLegend() { const legend = L.control({ position: "bottomleft" }); legend.onAdd = function () { const div = L.DomUtil.create( "div", "map-legend" ); const items = Object.keys(subtypeLayers) .filter(subtype => { const layer = subtypeLayers[subtype]; return layer && layer.getLayers().length > 0; }); div.innerHTML = `
    Influenza Subtypes
    ${items.map(item => `
    ${item}
    `).join('')}
    `; setTimeout(() => { div.querySelectorAll('.legend-item') .forEach(el => { el.addEventListener('click', () => { const subtype = el.dataset.subtype; const layer = subtypeLayers[subtype]; if (!layer) return; if (hiddenSubtypes.has(subtype)) { hiddenSubtypes.delete(subtype); map.addLayer(layer); el.style.opacity = 1; } else { hiddenSubtypes.add(subtype); map.removeLayer(layer); el.style.opacity = 0.35; } }); }); }, 0); return div; }; legend.addTo(map); } function loadProvinceMap( startYear, startWeek, endYear, endWeek ) { if (map) map.remove(); subtypeLayers = {}; hiddenSubtypes = new Set(); map = L.map('provinceMap') .setView([12.7, 104.9], 7); window.map = map; L.tileLayer( 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { attribution: '© OpenStreetMap' } ).addTo(map); Promise.all([ fetch('/geo/cambodia_provinces.geojson') .then(r => r.json()), fetch( `/api/dashboard/province-circles?start_year=${startYear}&start_week=${startWeek}&end_year=${endYear}&end_week=${endWeek}` ).then(r => r.json()) ]) .then(([geojson, data]) => { window._provinceData = data; updateAlerts(); const validProvinces = new Set( geojson.features.map( f => f.properties.ADM1_EN ) ); Object.keys(SUBTYPE_COLORS) .forEach(subtype => { subtypeLayers[subtype] = L.layerGroup().addTo(map); }); L.geoJSON(geojson, { style: { fillOpacity: 0.05, fillColor: '#2563eb', color: '#bbb', weight: 1 }, onEachFeature: function ( feature, layer ) { const province = feature.properties.ADM1_EN; const center = layer.getBounds().getCenter(); const rows = data.filter(d => { const name = normalizeProvince( d.patient_province, validProvinces ); return name === province; }); if (!rows.length) return; const tooltipHTML = `
    ${province}
    ${rows.map(r => `
    ${r.pathogen_name} ${r.total}
    `).join('')}
    `; layer.bindTooltip( tooltipHTML, { sticky: true, direction: 'top' } ); layer.on({ mouseover: function (e) { e.target.setStyle({ fillOpacity: 0.15, weight: 2, color: '#666' }); }, mouseout: function (e) { e.target.setStyle({ fillOpacity: 0.05, weight: 1, color: '#bbb' }); } }); const pathogens = [ ...new Set( rows.map( r => r.pathogen_name ) ) ]; const spacing = 0.12; rows.forEach(row => { const index = pathogens.indexOf(row.pathogen_name); const cols = 3; const col = index % cols; const rowIndex = Math.floor(index / cols); const xOffset = (col - (cols - 1) / 2) * spacing; const yOffset = rowIndex * spacing; const lat = center.lat + yOffset; const lng = center.lng + xOffset; const marker = L.circleMarker( [lat, lng], { radius: getRadius(row.total), fillColor: getColorByPathogen( row.pathogen_name ), fillOpacity: 0.85, stroke: false } ); if (subtypeLayers[row.pathogen_name]) { marker.addTo( subtypeLayers[ row.pathogen_name ] ); } }); } }).addTo(map); addPositivityLegend(); }); } document.addEventListener("DOMContentLoaded", () => { loadSummary(); new DashboardFilter((startYear, startWeek, endYear, endWeek) => { loadTrend('week', startYear, startWeek, endYear, endWeek); loadInfluenzaSubtypeDistribution('week', startYear, startWeek, endYear, endWeek); loadCovidDistributedByAgeGroup('week', startYear, startWeek, endYear, endWeek); loadInfluenzaSubtypeFrequency('week', startYear, startWeek, endYear, endWeek); loadCovidLineageFrequency('week', startYear, startWeek, endYear, endWeek); loadProvinceMap(startYear, startWeek, endYear, endWeek); const elements = document.querySelectorAll(".report-period"); elements.forEach(el => { el.textContent = 'Week ' + startWeek + ' of ' + startYear + ' to Week ' + endWeek + ' of ' + endYear; }); }); let currentSlide = 0; const slides = document.querySelectorAll('.slide'); const nextBtn = document.querySelector('.next-btn'); const prevBtn = document.querySelector('.prev-btn'); function showSlide(index) { slides.forEach((slide, i) => { slide.classList.remove('active', 'prev'); if (i === index) { slide.classList.add('active'); setTimeout(() => { if (typeof map !== 'undefined' && map) { map.invalidateSize(); } }, 300); } else if (i === index - 1) { slide.classList.add('prev'); } }); } function nextSlide() { currentSlide = (currentSlide + 1) % slides.length; showSlide(currentSlide); } function prevSlide() { currentSlide = (currentSlide - 1 + slides.length) % slides.length; showSlide(currentSlide); } if (nextBtn) { nextBtn.addEventListener('click', nextSlide); } if (prevBtn) { prevBtn.addEventListener('click', prevSlide); } const slideInterval = 2 * (15 * 1000); setInterval(nextSlide, slideInterval); showSlide(currentSlide); });