ARMS-Result / static /script.js
sidhesh01's picture
Update static/script.js
aafdb30 verified
Raw History Blame Contribute Delete
32.9 kB
document.addEventListener('DOMContentLoaded', () => {
const form = document.getElementById('login-form');
const submitBtn = document.getElementById('submit-btn');
const btnText = submitBtn.querySelector('.btn-text');
const loader = submitBtn.querySelector('.loader');
const loginView = document.getElementById('login-view-container');
const dashboardView = document.getElementById('dashboard-view');
const errorMessage = document.getElementById('error-message');
const errorText = document.getElementById('error-text');
const logoutBtn = document.getElementById('logout-btn');
// Pages
const homePage = document.getElementById('home-page');
const searchPage = document.getElementById('search-page');
const tabHome = document.getElementById('tab-home');
const tabSearch = document.getElementById('tab-search');
// Session state
let currentSessionId = null;
let currentUsername = null;
// ========== TAB SWITCHING ==========
document.getElementById('nav-tabs').addEventListener('click', (e) => {
const tab = e.target.closest('.nav-tab');
if (!tab) return;
const targetTab = tab.dataset.tab;
// Update active tab styles
document.querySelectorAll('.nav-tab').forEach(t => t.classList.remove('active'));
tab.classList.add('active');
// Show/hide pages
if (targetTab === 'home') {
homePage.classList.remove('hidden');
searchPage.classList.add('hidden');
} else if (targetTab === 'search') {
homePage.classList.add('hidden');
searchPage.classList.remove('hidden');
}
});
// ========== LOGIN ==========
form.addEventListener('submit', async (e) => {
e.preventDefault();
const username = document.getElementById('username').value.trim();
const password = document.getElementById('password').value;
if (!username || !password) return;
setLoading(true);
errorMessage.classList.add('hidden');
try {
const formData = new FormData();
formData.append('username', username);
formData.append('password', password);
const response = await fetch('/api/login', {
method: 'POST',
body: formData
});
const data = await response.json().catch(() => ({}));
if (!response.ok) {
throw new Error(data.detail || `Server error: ${response.status}`);
}
if (data.success && data['ASP.NET_SessionId']) {
currentSessionId = data['ASP.NET_SessionId'];
currentUsername = username;
loginView.classList.add('hidden');
dashboardView.classList.remove('hidden');
// Start fetch marks process
fetchMarks(username, currentSessionId);
} else {
throw new Error("Login succeeded but no session cookie was returned.");
}
} catch (error) {
console.error("Login error:", error);
errorText.textContent = error.message || "Failed to login. Please try again.";
errorMessage.classList.remove('hidden');
const newError = errorMessage.cloneNode(true);
errorMessage.parentNode.replaceChild(newError, errorMessage);
} finally {
setLoading(false);
}
});
// ========== LOGOUT ==========
logoutBtn.addEventListener('click', () => {
dashboardView.classList.add('hidden');
loginView.classList.remove('hidden');
form.reset();
currentSessionId = null;
currentUsername = null;
// Reset to home tab
tabHome.click();
// Clear search results
document.getElementById('search-cards-container').innerHTML = '';
document.getElementById('search-cards-container').classList.add('hidden');
document.getElementById('username').focus();
});
// ========== FETCH MARKS (HOME) ==========
async function fetchMarks(username, sessionId) {
const loading = document.getElementById('dashboard-loading');
const container = document.getElementById('cards-container');
const errorMsg = document.getElementById('dashboard-error');
const userDisplay = document.getElementById('user-display');
const circle = document.querySelector('.progress-ring__circle:not(.search-ring-circle)');
const percentageText = document.getElementById('progress-percentage');
const progressStatus = document.getElementById('progress-text');
const radius = circle.r.baseVal.value;
const circumference = radius * 2 * Math.PI;
circle.style.strokeDasharray = `${circumference} ${circumference}`;
circle.style.strokeDashoffset = circumference;
userDisplay.textContent = `${username}`;
loading.classList.remove('hidden');
container.classList.add('hidden');
errorMsg.classList.add('hidden');
function setProgress(percent) {
const offset = circumference - percent / 100 * circumference;
circle.style.strokeDashoffset = offset;
percentageText.textContent = Math.round(percent);
}
setProgress(0);
progressStatus.textContent = 'Connecting to SIMATS...';
let progress = 0;
const progressInterval = setInterval(() => {
const remaining = 99 - progress;
progress += (Math.random() * 0.1 + 0.05) * remaining;
setProgress(progress);
if (progress > 30 && progress < 60) {
progressStatus.textContent = 'Fetching course enrollments...';
} else if (progress >= 60 && progress < 85) {
progressStatus.textContent = 'Aggregating detailed mark splits...';
} else if (progress >= 85) {
progressStatus.textContent = 'Finalizing data format...';
}
}, 500);
try {
const formData = new FormData();
formData.append('username', username);
formData.append('session_id', sessionId);
const response = await fetch('/api/fetch-marks', {
method: 'POST',
body: formData
});
const data = await response.json().catch(() => ({}));
clearInterval(progressInterval);
if (!response.ok) throw new Error(data.detail || 'Failed to fetch marks');
setProgress(100);
progressStatus.textContent = 'Data loaded successfully!';
setTimeout(() => {
try {
loading.classList.add('hidden');
renderCards(data.data, container);
container.classList.remove('hidden');
} catch (renderError) {
console.error("Rendering error:", renderError);
loading.classList.add('hidden');
errorMsg.textContent = "An error occurred while displaying your marks. " + renderError.message;
errorMsg.classList.remove('hidden');
}
}, 800);
} catch (e) {
clearInterval(progressInterval);
loading.classList.add('hidden');
errorMsg.textContent = e.message;
errorMsg.classList.remove('hidden');
container.innerHTML = '';
}
}
// ========== SEARCH STUDENT ==========
const searchForm = document.getElementById('search-form');
const searchSubmitBtn = document.getElementById('search-submit-btn');
searchForm.addEventListener('submit', async (e) => {
e.preventDefault();
const registerNo = document.getElementById('search-register').value.trim();
if (!registerNo) return;
if (!currentSessionId) {
alert('Session expired. Please log in again.');
return;
}
const searchBtnText = searchSubmitBtn.querySelector('.btn-text');
const searchBtnLoader = searchSubmitBtn.querySelector('.loader');
const searchError = document.getElementById('search-error');
const searchLoading = document.getElementById('search-loading');
const searchContainer = document.getElementById('search-cards-container');
// Setup search loading ring
const searchCircle = document.querySelector('.search-ring-circle');
const searchPercentText = document.getElementById('search-progress-percentage');
const searchProgressStatus = document.getElementById('search-progress-text');
const radius = searchCircle.r.baseVal.value;
const circumference = radius * 2 * Math.PI;
searchCircle.style.strokeDasharray = `${circumference} ${circumference}`;
searchCircle.style.strokeDashoffset = circumference;
function setSearchProgress(percent) {
const offset = circumference - percent / 100 * circumference;
searchCircle.style.strokeDashoffset = offset;
searchPercentText.textContent = Math.round(percent);
}
// UI state
searchSubmitBtn.disabled = true;
searchBtnText.classList.add('hidden');
searchBtnLoader.classList.remove('hidden');
searchError.classList.add('hidden');
searchContainer.classList.add('hidden');
searchContainer.innerHTML = '';
searchLoading.classList.remove('hidden');
setSearchProgress(0);
searchProgressStatus.textContent = 'Searching student records...';
let progress = 0;
const progressInterval = setInterval(() => {
const remaining = 95 - progress;
// Faster increment for a snappier UI
progress += (Math.random() * 0.12 + 0.05) * remaining;
setSearchProgress(progress);
if (progress > 20 && progress < 50) {
searchProgressStatus.textContent = 'Scanning course databases...';
} else if (progress >= 50 && progress < 80) {
searchProgressStatus.textContent = 'Matching register number...';
} else if (progress >= 80) {
searchProgressStatus.textContent = 'Compiling results...';
}
}, 300);
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 75000); // 75s timeout
try {
const formData = new FormData();
formData.append('register_no', registerNo);
formData.append('session_id', currentSessionId);
const response = await fetch('/api/search-student', {
method: 'POST',
body: formData,
signal: controller.signal
});
clearTimeout(timeoutId);
const data = await response.json().catch(() => ({}));
clearInterval(progressInterval);
if (!response.ok) throw new Error(data.detail || 'Search failed');
setSearchProgress(100);
searchProgressStatus.textContent = 'Search complete!';
setTimeout(() => {
searchLoading.classList.add('hidden');
if (!data.data || data.data.length === 0) {
searchError.innerHTML = `
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="12"></line><line x1="12" y1="16" x2="12.01" y2="16"></line></svg>
<span>No results found for register number: <strong>${registerNo}</strong></span>`;
searchError.classList.remove('hidden');
return;
}
// Add student badge before results
const badge = document.createElement('div');
badge.style.textAlign = 'center';
badge.innerHTML = `
<div class="search-result-badge">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
Results for: <strong>${registerNo.toUpperCase()}</strong> &nbsp;·&nbsp; ${data.data.length} course(s) found
</div>`;
searchContainer.appendChild(badge);
renderCards(data.data, searchContainer, true);
searchContainer.classList.remove('hidden');
}, 600);
} catch (err) {
clearInterval(progressInterval);
searchLoading.classList.add('hidden');
searchError.innerHTML = `
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="12"></line><line x1="12" y1="16" x2="12.01" y2="16"></line></svg>
<span>${err.message}</span>`;
searchError.classList.remove('hidden');
} finally {
searchSubmitBtn.disabled = false;
searchBtnText.classList.remove('hidden');
searchBtnLoader.classList.add('hidden');
}
});
// ========== RENDER CARDS (shared between Home & Search) ==========
function renderCards(data, container, isSearch = false) {
if (!data || data.length === 0) {
container.innerHTML = '<div style="color:var(--text-muted); text-align:center; width: 100%; grid-column: 1 / -1; padding: 40px;">No course enrollments found for this period.</div>';
return;
}
let totalGradePoints = 0;
let totalCredits = 0;
let validCourses = 0;
const gradeValues = { 'S': 10, 'A': 9, 'B': 8, 'C': 7, 'D': 6, 'E': 5 };
let gradeCounts = { 'S': 0, 'A': 0, 'B': 0, 'C': 0, 'D': 0, 'E': 0 };
data.forEach((item, index) => {
const card = document.createElement('div');
card.className = 'course-card';
card.style.opacity = '0';
card.animate([
{ opacity: 0, transform: 'translateY(30px) scale(0.95)' },
{ opacity: 1, transform: 'translateY(0) scale(1)' }
], {
duration: 500,
easing: 'cubic-bezier(0.175, 0.885, 0.32, 1.275)',
fill: 'forwards',
delay: index * 100
});
const c = item.course;
const marks = item.marks;
const error = item.error;
const grade = c.FinalGrade ? c.FinalGrade.trim().toUpperCase() : '';
if (gradeValues[grade] !== undefined) {
const credit = parseFloat(c.Credit || c.Credits || 1);
totalGradePoints += gradeValues[grade] * credit;
totalCredits += credit;
validCourses++;
if (gradeCounts[grade] !== undefined) {
gradeCounts[grade]++;
}
}
let marksHtml = '';
let totalObtained = 0;
if (error) {
marksHtml = `<div class="card-error">${error}</div>`;
} else if (marks && marks.length > 0) {
const targetCategories = ["Class Test (IA)", "Research", "Class Practical", "University Theory", "University Practical"];
marks.forEach(m => {
const category = m.RubricCategory ? m.RubricCategory.trim() : "";
if (targetCategories.includes(category)) {
const val = parseFloat(m.OrginalConvertedMark);
if (!isNaN(val)) {
totalObtained += val;
}
}
});
totalObtained = Math.round(totalObtained * 100) / 100;
const list = marks.map(m => {
const isPass = m.IsPassed;
const statusClass = isPass ? 'mark-pass' : 'mark-fail';
const statusText = isPass ? 'PASS' : 'FAIL';
return `
<li class="mark-item">
<div class="mark-info">
<span class="mark-name">${m.RubricCategory}</span>
<span class="mark-type">${m.Type}</span>
</div>
<div class="mark-score-container">
<span class="mark-value">${m.OrginalConvertedMark} <span style="opacity:0.4;font-size:12px;">/ ${m.RubricsMaxMark}</span></span>
<div><span class="mark-status ${statusClass}">${statusText}</span></div>
</div>
</li>
`;
}).join('');
marksHtml = `<ul class="mark-list">${list}</ul>`;
} else {
marksHtml = `<div style="padding:40px 20px; color:var(--text-muted); text-align:center;">No marks available for this course</div>`;
}
card.innerHTML = `
<div class="card-header">
<span class="course-code">${c.CourseCode || '-'}</span>
<h3>${c.CourseName || 'Unknown Course'}</h3>
<div class="card-meta">
<span class="month-badge">${c.MonthYearValue || '-'}</span>
<div style="display: flex; gap: 8px; align-items: center;">
${marks && !error ? `<span class="grade-badge" style="background: rgba(255,255,255,0.15); border-color: rgba(255,255,255,0.3); color: #fff;">Total: ${totalObtained}/500</span>` : ''}
<span class="grade-badge">Grade: ${c.FinalGrade || 'N/A'}</span>
</div>
</div>
</div>
<div class="card-body">
${marksHtml}
</div>
`;
container.appendChild(card);
});
if (validCourses > 0 && totalCredits > 0) {
const cgpa = (totalGradePoints / totalCredits).toFixed(2);
const cgpaCard = document.createElement('div');
cgpaCard.className = 'course-card';
cgpaCard.style.opacity = '0';
cgpaCard.style.gridColumn = '1 / -1';
cgpaCard.style.background = 'var(--card-bg)';
cgpaCard.style.border = '2px solid rgba(255, 255, 255, 0.4)';
cgpaCard.style.textAlign = 'center';
cgpaCard.style.marginBottom = '20px';
cgpaCard.animate([
{ opacity: 0, transform: 'translateY(-20px)' },
{ opacity: 1, transform: 'translateY(0)' }
], {
duration: 500,
easing: 'cubic-bezier(0.175, 0.885, 0.32, 1.275)',
fill: 'forwards',
delay: 0
});
// Use unique IDs for search vs home to avoid conflicts
const prefix = isSearch ? 'search-calc' : 'home-calc';
cgpaCard.innerHTML = `
<div style="padding: 40px;">
<div style="display: flex; flex-wrap: wrap; justify-content: center; gap: 60px; margin-bottom: 30px;">
<div>
<h2 style="font-size: 18px; font-weight: 500; letter-spacing: 0.15em; text-transform: uppercase; margin-bottom: 12px; color: var(--text-muted);">Overall CGPA</h2>
<div style="font-size: 64px; font-weight: 700; line-height: 1; letter-spacing: -0.02em; background: var(--primary-grad); -webkit-background-clip: text; -webkit-text-fill-color: transparent; text-shadow: 0 10px 30px rgba(255, 255, 255, 0.2);">
${cgpa}
</div>
</div>
<div style="width: 1px; height: 80px; background: rgba(255,255,255,0.2); display: block;"></div>
<div>
<h2 style="font-size: 18px; font-weight: 500; letter-spacing: 0.15em; text-transform: uppercase; margin-bottom: 12px; color: var(--text-muted);">Courses Completed</h2>
<div style="font-size: 64px; font-weight: 700; line-height: 1; letter-spacing: -0.02em; color: #fff; text-shadow: 0 10px 30px rgba(255, 255, 255, 0.1);">
${validCourses}
</div>
</div>
</div>
<div style="padding-top: 25px; border-top: 1px solid rgba(255,255,255,0.2);">
<h3 style="font-size: 15px; color: var(--text-main); margin-bottom: 15px; text-transform: uppercase; letter-spacing: 0.05em;">Target CGPA Planner</h3>
<div style="display: flex; gap: 15px; justify-content: center; align-items: center; flex-wrap: wrap;">
<div style="display: flex; flex-direction: column; align-items: flex-start;">
<label style="color: var(--text-muted); font-size: 12px; margin-bottom: 4px;">Target CGPA</label>
<input type="number" id="${prefix}-target" value="9.0" step="0.1" min="0" max="10" style="background: rgba(0,0,0,0.4); border: 1px solid rgba(255,255,255,0.5); color: #fff; padding: 8px 12px; border-radius: 8px; width: 100px; font-family: inherit; outline: none;">
</div>
<div style="display: flex; flex-direction: column; align-items: flex-start;">
<label style="color: var(--text-muted); font-size: 12px; margin-bottom: 4px;">Expected Grade</label>
<select id="${prefix}-grade" style="background: rgba(0,0,0,0.4); border: 1px solid rgba(255,255,255,0.5); color: #fff; padding: 8px 12px; border-radius: 8px; font-family: inherit; outline: none; cursor: pointer;">
<option value="10">S</option>
<option value="9">A</option>
<option value="8">B</option>
<option value="7">C</option>
<option value="6">D</option>
<option value="5">E</option>
</select>
</div>
</div>
<div id="${prefix}-result" style="margin-top: 20px; font-size: 15px; color: #ffffff; font-weight: 500; min-height: 24px;"></div>
</div>
</div>
`;
container.insertBefore(cgpaCard, container.firstChild);
const updateCalc = () => {
const target = parseFloat(document.getElementById(`${prefix}-target`).value);
const futureGp = parseFloat(document.getElementById(`${prefix}-grade`).value);
const resultDiv = document.getElementById(`${prefix}-result`);
const curr = totalGradePoints / totalCredits;
if (isNaN(target) || isNaN(futureGp)) return;
if (curr >= target) {
resultDiv.innerHTML = `<span style="color: #34d399;">Already has a ${target.toFixed(1)}+ CGPA!</span>`;
return;
}
if (futureGp <= target) {
resultDiv.innerHTML = `<span style="color: #f87171;">Impossible to reach a ${target.toFixed(1)} CGPA by scoring ${futureGp} points or less.</span>`;
return;
}
const selectEl = document.getElementById(`${prefix}-grade`);
const gradeName = selectEl.options[selectEl.selectedIndex].text;
const reqCredits = (target * totalCredits - totalGradePoints) / (futureGp - target);
resultDiv.innerHTML = `Need <strong style="font-size: 20px; color: #fff;">${Math.ceil(reqCredits)}</strong> more credits of <span style="color: #fff; font-weight: bold;">${gradeName}</span> grade to reach a ${target.toFixed(1)} CGPA.`;
};
document.getElementById(`${prefix}-target`).addEventListener('input', updateCalc);
document.getElementById(`${prefix}-grade`).addEventListener('change', updateCalc);
updateCalc();
// Grade Distribution Chart
const totalGradesCount = Object.values(gradeCounts).reduce((a, b) => a + b, 0);
if (totalGradesCount > 0 && typeof Chart !== 'undefined') {
const chartId = isSearch ? 'searchChart' : 'performanceChart';
const graphCard = document.createElement('div');
graphCard.className = 'course-card';
graphCard.style.opacity = '0';
graphCard.style.gridColumn = '1 / -1';
graphCard.style.background = 'var(--card-bg)';
graphCard.style.border = '1px solid rgba(255, 255, 255, 0.2)';
graphCard.style.padding = '30px';
graphCard.style.marginTop = '20px';
graphCard.animate([
{ opacity: 0, transform: 'translateY(30px) scale(0.95)' },
{ opacity: 1, transform: 'translateY(0) scale(1)' }
], {
duration: 500,
easing: 'cubic-bezier(0.175, 0.885, 0.32, 1.275)',
fill: 'forwards',
delay: (data.length + 1) * 100
});
graphCard.innerHTML = `
<h2 style="font-size: 18px; font-weight: 500; letter-spacing: 0.1em; text-transform: uppercase; margin-bottom: 20px; color: var(--text-main); text-align: center;">Grade Distribution</h2>
<div style="position: relative; height: 300px; width: 100%;">
<canvas id="${chartId}"></canvas>
</div>
`;
container.appendChild(graphCard);
const pieLabels = [];
const pieData = [];
const pieColors = [];
const ctx = document.getElementById(chartId).getContext('2d');
const createGrad = (c1, c2) => {
const g = ctx.createLinearGradient(0, 0, 0, 400);
g.addColorStop(0, c1);
g.addColorStop(1, c2);
return g;
};
const colorPalette = {
'S': createGrad('rgba(255, 215, 0, 1)', 'rgba(218, 165, 32, 0.6)'),
'A': createGrad('rgba(52, 211, 153, 1)', 'rgba(5, 150, 105, 0.6)'),
'B': createGrad('rgba(96, 165, 250, 1)', 'rgba(37, 99, 235, 0.6)'),
'C': createGrad('rgba(167, 139, 250, 1)', 'rgba(124, 58, 237, 0.6)'),
'D': createGrad('rgba(251, 146, 60, 1)', 'rgba(234, 88, 12, 0.6)'),
'E': createGrad('rgba(248, 113, 113, 1)', 'rgba(220, 38, 38, 0.6)')
};
for (const g in gradeCounts) {
if (gradeCounts[g] > 0) {
pieLabels.push(g);
pieData.push(gradeCounts[g]);
pieColors.push(colorPalette[g] || '#888');
}
}
const shadowPlugin = {
id: 'shadowPlugin_' + chartId,
beforeDatasetDraw: (chart) => {
const ctx = chart.ctx;
ctx.save();
ctx.shadowColor = 'rgba(0, 0, 0, 0.4)';
ctx.shadowBlur = 15;
ctx.shadowOffsetX = 0;
ctx.shadowOffsetY = 8;
},
afterDatasetDraw: (chart) => {
chart.ctx.restore();
}
};
const centerTextPlugin = {
id: 'centerText_' + chartId,
afterDraw: (chart) => {
if (chart.data.datasets.length === 0) return;
const ctx = chart.ctx;
const meta = chart.getDatasetMeta(0);
if (!meta.data || !meta.data.length) return;
const center = meta.data[0];
const x = center.x;
const y = center.y;
ctx.save();
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.font = "bold 54px 'Outfit', sans-serif";
ctx.fillStyle = "#ffffff";
ctx.shadowColor = 'rgba(255,255,255,0.1)';
ctx.shadowBlur = 10;
ctx.fillText(totalGradesCount.toString(), x, y - 8);
ctx.font = "400 13px 'Outfit', sans-serif";
ctx.fillStyle = "#a1a1aa";
ctx.shadowBlur = 0;
ctx.letterSpacing = "1px";
ctx.fillText("TOTAL GRADES", x, y + 28);
ctx.restore();
}
};
new Chart(ctx, {
type: 'doughnut',
data: {
labels: pieLabels,
datasets: [{
data: pieData,
backgroundColor: pieColors,
borderColor: 'transparent',
borderWidth: 0,
hoverOffset: 12,
borderRadius: 10,
spacing: 8
}]
},
plugins: [shadowPlugin, centerTextPlugin],
options: {
cutout: '78%',
responsive: true,
maintainAspectRatio: false,
layout: {
padding: 20
},
animation: {
animateScale: true,
animateRotate: true,
duration: 1500,
easing: 'easeOutQuart'
},
plugins: {
legend: {
position: 'right',
labels: {
color: '#d1d5db',
font: { size: 14, family: "'Outfit', sans-serif", weight: '500' },
usePointStyle: true,
pointStyle: 'circle',
padding: 25
}
},
tooltip: {
backgroundColor: 'rgba(15, 15, 20, 0.95)',
titleFont: { size: 15, family: "'Outfit', sans-serif", weight: '600' },
bodyFont: { size: 14, family: "'Outfit', sans-serif" },
padding: 15,
cornerRadius: 12,
displayColors: true,
usePointStyle: true,
borderColor: 'rgba(255, 255, 255, 0.2)',
borderWidth: 1,
boxPadding: 6
}
}
}
});
}
}
}
function setLoading(isLoading) {
submitBtn.disabled = isLoading;
if (isLoading) {
btnText.classList.add('hidden');
loader.classList.remove('hidden');
} else {
btnText.classList.remove('hidden');
loader.classList.add('hidden');
}
}
});