Files
nrml_dashboard/dashboard/public/js/overview.js
2026-07-06 16:38:24 +07:00

1312 lines
37 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import * as Charts from "./dashboard/globals.js";
let trendChart;
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 += `
<div class="col-md-2 mb-3">
<div class="card shadow-sm h-100">
<div class="card-body">
<div class="d-flex justify-content-between">
<div>
<h6 class="fw-bold">
${item.code === 'H5N1'
? 'A/H5N1 Cases'
: `${item.code} Cases`}
</h6>
<h3 class="mb-1">${item.current_total}</h3>
<small class="text-muted">Last 7 days</small>
</div>
<div class="text-end">
<div class="${trendColor} fw-bold">
${item.percent_change > 0 ? '▲' : item.percent_change < 0 ? '▼' : ''}
${Math.abs(item.percent_change)}%
</div>
<small class="text-muted">
${item.previous_total ?? 0} last week
</small>
</div>
</div>
</div>
</div>
</div>
`;
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 {
yearlyView: useYearlyView,
labels
} = Charts.buildPeriodLabels(
Object.values(data),
startYear,
endYear
);
const datasets = [];
const allowedPrograms = ['SARI', 'ILI', 'LBM', 'AFI', 'NDS'];
Object.keys(data).forEach(code => {
if (!allowedPrograms.includes(code)) return;
const values = labels.map(label => {
if (useYearlyView) {
return data[code]
.filter(row => String(row.year) === String(label))
.reduce((sum, row) => sum + Number(row.total || 0), 0);
}
const found = data[code].find(
row => `${row.year}-${row.period}` === label
);
return found ? found.total : 0;
});
datasets.push({
label: code,
data: values,
borderColor: Charts.SURVEILLANCE_COLORS[code],
backgroundColor: Charts.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
},
}
}
}
});
Charts.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 => {
data.forEach(row => {
if (row.subtype === 'B/VIC') {
row.subtype = 'B/Vic';
}
});
Charts.buildDistributionChart(
'influenzaSubtypeDistribution',
'bar',
data,
'subtype',
'total',
label => Charts.SUBTYPE_COLORS[label] || '#9ca3af'
);
});
}
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 => {
Charts.buildDistributionChart('covidDistributedByAgeGroup', 'bar', data, 'age_group');
});
}
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 {
yearlyView: useYearlyView,
periods
} = Charts.buildPeriods(
data,
'week',
startYear,
endYear
);
const lineages = [...new Set(data.map(item => item.lineage))];
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,
// borderColor: 'transparent',
borderWidth: 0,
pointRadius: 0,
backgroundColor: hexToRGBA(Charts.COLORS[index % Charts.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.charts['covidLineageFrequency'] = covidLineageFrequencyChart;
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 = `
<span style="width:15px;height:15px;background:${dataset.backgroundColor};display:inline-block;margin-right:8px;"></span>
${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 {
yearlyView: useYearlyView,
periods
} = Charts.buildPeriods(
data,
'week',
startYear,
endYear
);
const lineages = [...new Set(
data.map(item => item.lineage === 'B/VIC' ? 'B/Vic' : item.lineage)
)];
const lineageColors = lineages.map(
label => Charts.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.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 = `
<span style="
width:15px;
height:15px;
background:${dataset.backgroundColor};
display:inline-block;
margin-right:8px;
"></span>
${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.latestProvinceData) return;
const raw = buildAlerts(window._summaryData, window.latestProvinceData);
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 = `
<li class="list-group-item text-success">
🟢 No unusual activity detected
</li>
`;
return;
}
const colorMap = {
high: 'text-danger',
moderate: 'text-warning',
trend: 'text-primary',
volume: 'text-secondary'
};
container.innerHTML = alerts.map(a => `
<li class="list-group-item ${colorMap[a.type] || ''}">
${a.type === 'high' ? '🔴' :
a.type === 'moderate' ? '🟠' :
a.type === 'trend' ? '🟡' :
'🔵'}
${a.message}
</li>
`).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;
}
window.normalizeProvince = normalizeProvince;
function getRadius(total) {
if (!total) return 0;
const r = Math.sqrt(total);
return Math.max(4, Math.min(r * 2, 22));
}
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 = `
<div style="
background:white;
padding:8px 10px;
border-radius:8px;
box-shadow:0 2px 8px rgba(0,0,0,0.15);
font-size:11px;
max-width:160px;
max-height:220px;
overflow-y:auto;
">
<div style="
font-weight:600;
margin-bottom:8px;
font-size:12px;
">
Influenza Subtypes
</div>
${items.map(item => `
<div
class="legend-item"
data-subtype="${item}"
style="
display:flex;
align-items:center;
margin-bottom:6px;
cursor:pointer;
transition:0.2s;
"
>
<span style="
background:${Charts.SUBTYPE_COLORS[item]};
width:10px;
height:10px;
display:inline-block;
margin-right:6px;
border-radius:50%;
flex-shrink:0;
"></span>
<span>
${item}
</span>
</div>
`).join('')}
</div>
`;
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.latestProvinceData = data;
updateAlerts();
const validProvinces = new Set(
geojson.features.map(
f => f.properties.ADM1_EN
)
);
Object.keys(Charts.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 = `
<div style="min-width:170px">
<div style="
font-weight:600;
margin-bottom:6px;
font-size:13px;
">
${province}
</div>
${rows.map(r => `
<div style="
display:flex;
justify-content:space-between;
gap:12px;
margin-bottom:2px;
">
<span>
${r.pathogen_name === 'B/VIC' ? 'B/Vic' : r.pathogen_name}
</span>
<span style="font-weight:600">
${r.total}
</span>
</div>
`).join('')}
</div>
`;
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: Charts.SUBTYPE_COLORS[row.pathogen_name === 'B/VIC' ? 'B/Vic' : row.pathogen_name] || '#9ca3af',
fillOpacity: 0.85,
stroke: false
}
);
if (subtypeLayers[row.pathogen_name === 'B/VIC' ? 'B/Vic' : row.pathogen_name]) {
marker.addTo(
subtypeLayers[
row.pathogen_name === 'B/VIC' ? 'B/Vic' : 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);
});