630 lines
23 KiB
JavaScript
630 lines
23 KiB
JavaScript
/**
|
|
* Lycostorrent - Page Découvrir (Version simplifiée)
|
|
* 2 catégories : Films récents / Séries en cours
|
|
* Avec pré-cache des torrents
|
|
*/
|
|
|
|
// ============================================================
|
|
// ÉTAT GLOBAL
|
|
// ============================================================
|
|
|
|
let currentCategory = 'movies';
|
|
let currentMedia = null;
|
|
let torrentClientEnabled = false;
|
|
let cachedData = {}; // Cache local des données
|
|
|
|
// ============================================================
|
|
// INITIALISATION
|
|
// ============================================================
|
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
initTabs();
|
|
checkTorrentClient();
|
|
loadCategory('movies');
|
|
});
|
|
|
|
function initTabs() {
|
|
document.querySelectorAll('.discover-tab').forEach(tab => {
|
|
tab.addEventListener('click', () => {
|
|
const category = tab.dataset.category;
|
|
|
|
// Mettre à jour l'UI
|
|
document.querySelectorAll('.discover-tab').forEach(t => t.classList.remove('active'));
|
|
tab.classList.add('active');
|
|
|
|
// Charger la catégorie
|
|
loadCategory(category);
|
|
});
|
|
});
|
|
}
|
|
|
|
async function checkTorrentClient() {
|
|
try {
|
|
const response = await fetch('/api/torrent-client/status');
|
|
const data = await response.json();
|
|
torrentClientEnabled = data.success && data.enabled && data.connected;
|
|
} catch (error) {
|
|
torrentClientEnabled = false;
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// CHARGEMENT DES DONNÉES
|
|
// ============================================================
|
|
|
|
async function loadCategory(category) {
|
|
currentCategory = category;
|
|
|
|
const grid = document.getElementById('discoverGrid');
|
|
const loader = document.getElementById('discoverLoader');
|
|
const empty = document.getElementById('discoverEmpty');
|
|
|
|
// Afficher le loader
|
|
grid.innerHTML = '';
|
|
loader.classList.remove('hidden');
|
|
empty.classList.add('hidden');
|
|
hideCacheInfo();
|
|
|
|
// Essayer de charger depuis le cache d'abord
|
|
try {
|
|
const cacheResponse = await fetch(`/api/cache/data/discover/${category}`);
|
|
const cacheData = await cacheResponse.json();
|
|
|
|
if (cacheData.success && cacheData.cached && cacheData.data && cacheData.data.length > 0) {
|
|
loader.classList.add('hidden');
|
|
cachedData[category] = cacheData.data;
|
|
const mediaType = category === 'movies' ? 'movie' : 'tv';
|
|
renderGrid(cacheData.data, mediaType, true);
|
|
showCacheInfo(cacheData.timestamp);
|
|
console.log(`📦 Discover ${category} chargé depuis le cache: ${cacheData.data.length} résultats`);
|
|
return;
|
|
}
|
|
} catch (error) {
|
|
console.log('Pas de cache disponible, chargement en direct...');
|
|
}
|
|
|
|
// Si pas de cache, charger en direct
|
|
await loadCategoryLive(category);
|
|
}
|
|
|
|
async function loadCategoryLive(category) {
|
|
const grid = document.getElementById('discoverGrid');
|
|
const loader = document.getElementById('discoverLoader');
|
|
const empty = document.getElementById('discoverEmpty');
|
|
|
|
grid.innerHTML = '';
|
|
loader.classList.remove('hidden');
|
|
empty.classList.add('hidden');
|
|
hideCacheInfo();
|
|
|
|
try {
|
|
const response = await fetch(`/api/discover/${category}`);
|
|
const data = await response.json();
|
|
|
|
loader.classList.add('hidden');
|
|
|
|
if (data.success && data.results && data.results.length > 0) {
|
|
cachedData[category] = data.results;
|
|
renderGrid(data.results, data.media_type, false);
|
|
} else {
|
|
empty.classList.remove('hidden');
|
|
empty.querySelector('p').textContent = data.error || 'Aucun résultat trouvé';
|
|
}
|
|
} catch (error) {
|
|
loader.classList.add('hidden');
|
|
empty.classList.remove('hidden');
|
|
empty.querySelector('p').textContent = 'Erreur de chargement';
|
|
console.error('Erreur:', error);
|
|
}
|
|
}
|
|
|
|
function renderGrid(results, mediaType, fromCache) {
|
|
const grid = document.getElementById('discoverGrid');
|
|
|
|
grid.innerHTML = results.map((item, index) => {
|
|
const posterUrl = item.poster_path
|
|
? `https://image.tmdb.org/t/p/w300${item.poster_path}`
|
|
: null;
|
|
|
|
const title = item.title || item.name;
|
|
const year = (item.release_date || item.first_air_date || '').substring(0, 4);
|
|
const rating = item.vote_average ? item.vote_average.toFixed(1) : '--';
|
|
const type = mediaType === 'movie' ? '🎬' : '📺';
|
|
|
|
// Indicateur de torrents disponibles (si depuis le cache)
|
|
const torrentCount = item.torrent_count || 0;
|
|
const torrentBadge = fromCache && torrentCount > 0
|
|
? `<span class="torrent-badge">🧲 ${torrentCount}</span>`
|
|
: '';
|
|
|
|
return `
|
|
<div class="discover-card" onclick="openDetail(${item.id}, '${mediaType}', ${index})">
|
|
<div class="poster-container">
|
|
${posterUrl
|
|
? `<img src="${posterUrl}" alt="${escapeHtml(title)}" class="poster" loading="lazy">`
|
|
: `<div class="poster-placeholder">${type}</div>`
|
|
}
|
|
<span class="rating-badge">⭐ ${rating}</span>
|
|
${torrentBadge}
|
|
</div>
|
|
<div class="card-info">
|
|
<div class="card-title" title="${escapeHtml(title)}">${escapeHtml(title)}</div>
|
|
<div class="card-meta">
|
|
<span class="card-year">${year || 'N/A'}</span>
|
|
<span class="card-type">${type}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}).join('');
|
|
}
|
|
|
|
// Afficher les infos du cache
|
|
function showCacheInfo(timestamp) {
|
|
const cacheInfo = document.getElementById('cacheInfo');
|
|
const cacheTimestampEl = document.getElementById('cacheTimestamp');
|
|
|
|
if (cacheInfo && timestamp) {
|
|
const date = new Date(timestamp);
|
|
const now = new Date();
|
|
const diffMinutes = Math.floor((now - date) / 60000);
|
|
|
|
let timeAgo;
|
|
if (diffMinutes < 1) {
|
|
timeAgo = "à l'instant";
|
|
} else if (diffMinutes < 60) {
|
|
timeAgo = `il y a ${diffMinutes} min`;
|
|
} else {
|
|
const hours = Math.floor(diffMinutes / 60);
|
|
timeAgo = `il y a ${hours}h`;
|
|
}
|
|
|
|
cacheTimestampEl.textContent = timeAgo;
|
|
cacheInfo.classList.remove('hidden');
|
|
}
|
|
}
|
|
|
|
function hideCacheInfo() {
|
|
const cacheInfo = document.getElementById('cacheInfo');
|
|
if (cacheInfo) {
|
|
cacheInfo.classList.add('hidden');
|
|
}
|
|
}
|
|
|
|
function refreshLive() {
|
|
loadCategoryLive(currentCategory);
|
|
}
|
|
|
|
// ============================================================
|
|
// MODAL DÉTAILS
|
|
// ============================================================
|
|
|
|
async function openDetail(id, mediaType, index) {
|
|
const modal = document.getElementById('detailModal');
|
|
const listEl = document.getElementById('torrentsList');
|
|
const loadingEl = document.getElementById('torrentsLoading');
|
|
const emptyEl = document.getElementById('torrentsEmpty');
|
|
|
|
// Réinitialiser
|
|
listEl.innerHTML = '';
|
|
loadingEl.classList.add('hidden');
|
|
emptyEl.classList.add('hidden');
|
|
|
|
modal.classList.remove('hidden');
|
|
|
|
// Vérifier si on a les données en cache local (avec détails + torrents pré-chargés)
|
|
const category = mediaType === 'movie' ? 'movies' : 'tv';
|
|
const cachedItem = cachedData[category] ? cachedData[category][index] : null;
|
|
|
|
// Si les détails sont pré-cachés, on les utilise directement (INSTANTANÉ)
|
|
if (cachedItem && cachedItem.details_cached) {
|
|
console.log(`📦 Détails + torrents depuis le cache pour: ${cachedItem.title || cachedItem.name}`);
|
|
|
|
currentMedia = cachedItem;
|
|
currentMedia.media_type = mediaType;
|
|
|
|
// Afficher les détails depuis le cache
|
|
renderDetailFromCache(cachedItem, mediaType);
|
|
|
|
// Afficher les torrents depuis le cache
|
|
if (cachedItem.torrents && cachedItem.torrents.length > 0) {
|
|
renderTorrents(cachedItem.torrents);
|
|
} else {
|
|
emptyEl.classList.remove('hidden');
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Sinon, fallback : charger depuis l'API (pour les items sans cache)
|
|
try {
|
|
const response = await fetch(`/api/discover/detail/${mediaType}/${id}`);
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
currentMedia = data.detail;
|
|
currentMedia.media_type = mediaType;
|
|
renderDetail(data.detail, mediaType);
|
|
|
|
// Si on a des torrents pré-cachés, les afficher
|
|
if (cachedItem && cachedItem.torrents && cachedItem.torrents.length > 0) {
|
|
renderTorrents(cachedItem.torrents);
|
|
} else {
|
|
// Sinon, rechercher en direct
|
|
searchTorrents(data.detail, mediaType);
|
|
}
|
|
} else {
|
|
closeDetailModal();
|
|
alert('Erreur lors du chargement des détails');
|
|
}
|
|
} catch (error) {
|
|
console.error('Erreur:', error);
|
|
closeDetailModal();
|
|
}
|
|
}
|
|
|
|
// Afficher les détails depuis le cache (nouvelle fonction)
|
|
function renderDetailFromCache(item, mediaType) {
|
|
const title = item.title || item.name;
|
|
const year = (item.release_date || item.first_air_date || '').substring(0, 4);
|
|
const posterUrl = item.poster_path
|
|
? `https://image.tmdb.org/t/p/w300${item.poster_path}`
|
|
: '/static/icons/icon-192x192.png';
|
|
|
|
document.getElementById('detailPoster').src = posterUrl;
|
|
document.getElementById('detailPoster').alt = title;
|
|
document.getElementById('detailTitle').textContent = title;
|
|
document.getElementById('detailYear').textContent = year;
|
|
document.getElementById('detailRating').textContent = `⭐ ${item.vote_average ? item.vote_average.toFixed(1) : '--'}`;
|
|
document.getElementById('detailOverview').textContent = item.overview || 'Aucune description disponible.';
|
|
|
|
// Genres
|
|
const genresContainer = document.getElementById('detailGenres');
|
|
if (item.genres && item.genres.length > 0) {
|
|
genresContainer.innerHTML = item.genres.map(g => `<span>${g.name}</span>`).join('');
|
|
} else {
|
|
genresContainer.innerHTML = '';
|
|
}
|
|
|
|
// Bande-annonce YouTube
|
|
const trailerSection = document.getElementById('detailTrailer');
|
|
const trailerFrame = document.getElementById('trailerFrame');
|
|
|
|
if (item.trailer_url) {
|
|
trailerFrame.src = item.trailer_url;
|
|
trailerSection.classList.remove('hidden');
|
|
} else {
|
|
trailerFrame.src = '';
|
|
trailerSection.classList.add('hidden');
|
|
}
|
|
}
|
|
|
|
function renderDetail(detail, mediaType) {
|
|
const title = detail.title || detail.name;
|
|
const year = (detail.release_date || detail.first_air_date || '').substring(0, 4);
|
|
const posterUrl = detail.poster_path
|
|
? `https://image.tmdb.org/t/p/w300${detail.poster_path}`
|
|
: '/static/icons/icon-192x192.png';
|
|
|
|
document.getElementById('detailPoster').src = posterUrl;
|
|
document.getElementById('detailPoster').alt = title;
|
|
document.getElementById('detailTitle').textContent = title;
|
|
document.getElementById('detailYear').textContent = year;
|
|
document.getElementById('detailRating').textContent = `⭐ ${detail.vote_average ? detail.vote_average.toFixed(1) : '--'}`;
|
|
document.getElementById('detailOverview').textContent = detail.overview || 'Aucune description disponible.';
|
|
|
|
// Genres
|
|
const genresContainer = document.getElementById('detailGenres');
|
|
if (detail.genres && detail.genres.length > 0) {
|
|
genresContainer.innerHTML = detail.genres.map(g => `<span>${g.name}</span>`).join('');
|
|
} else {
|
|
genresContainer.innerHTML = '';
|
|
}
|
|
|
|
// Bande-annonce YouTube
|
|
const trailerSection = document.getElementById('detailTrailer');
|
|
const trailerFrame = document.getElementById('trailerFrame');
|
|
|
|
if (detail.trailer_url) {
|
|
trailerFrame.src = detail.trailer_url;
|
|
trailerSection.classList.remove('hidden');
|
|
} else {
|
|
trailerFrame.src = '';
|
|
trailerSection.classList.add('hidden');
|
|
}
|
|
}
|
|
|
|
function closeDetailModal() {
|
|
document.getElementById('detailModal').classList.add('hidden');
|
|
// Arrêter la vidéo YouTube
|
|
document.getElementById('trailerFrame').src = '';
|
|
currentMedia = null;
|
|
}
|
|
|
|
// Fermer le modal en cliquant à l'extérieur
|
|
document.getElementById('detailModal')?.addEventListener('click', (e) => {
|
|
if (e.target.id === 'detailModal') {
|
|
closeDetailModal();
|
|
}
|
|
});
|
|
|
|
// Fermer avec Escape
|
|
document.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Escape') {
|
|
closeDetailModal();
|
|
}
|
|
});
|
|
|
|
// ============================================================
|
|
// RECHERCHE DE TORRENTS (fallback si pas en cache)
|
|
// ============================================================
|
|
|
|
async function searchTorrents(detail, mediaType) {
|
|
const loadingEl = document.getElementById('torrentsLoading');
|
|
const listEl = document.getElementById('torrentsList');
|
|
const emptyEl = document.getElementById('torrentsEmpty');
|
|
|
|
loadingEl.classList.remove('hidden');
|
|
listEl.innerHTML = '';
|
|
emptyEl.classList.add('hidden');
|
|
|
|
const title = detail.title || detail.name;
|
|
const originalTitle = detail.original_title || detail.original_name || '';
|
|
const year = (detail.release_date || detail.first_air_date || '').substring(0, 4);
|
|
|
|
try {
|
|
const response = await fetch('/api/discover/search-torrents', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
title: title,
|
|
original_title: originalTitle,
|
|
year: year,
|
|
media_type: mediaType,
|
|
tmdb_id: detail.id
|
|
})
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
loadingEl.classList.add('hidden');
|
|
|
|
if (data.success && data.results && data.results.length > 0) {
|
|
renderTorrents(data.results);
|
|
} else {
|
|
emptyEl.classList.remove('hidden');
|
|
}
|
|
} catch (error) {
|
|
loadingEl.classList.add('hidden');
|
|
emptyEl.classList.remove('hidden');
|
|
console.error('Erreur recherche torrents:', error);
|
|
}
|
|
}
|
|
|
|
function renderTorrents(torrents) {
|
|
const listEl = document.getElementById('torrentsList');
|
|
|
|
listEl.innerHTML = torrents.slice(0, 20).map((torrent, index) => {
|
|
const size = torrent.Size ? formatSize(torrent.Size) : 'N/A';
|
|
const seeds = torrent.Seeders || 0;
|
|
const quality = torrent.parsed?.quality || '';
|
|
const tracker = torrent.Tracker || torrent.TrackerName || 'Unknown';
|
|
|
|
const magnetUrl = torrent.MagnetUri || '';
|
|
const downloadUrl = torrent.Link || '';
|
|
const detailsUrl = torrent.Details || torrent.Guid || '';
|
|
const torrentUrl = magnetUrl || downloadUrl;
|
|
|
|
return `
|
|
<div class="torrent-item">
|
|
<div class="torrent-info">
|
|
<div class="torrent-name" title="${escapeHtml(torrent.Title)}">${escapeHtml(torrent.Title)}</div>
|
|
<div class="torrent-meta">
|
|
<span class="tracker">📡 ${escapeHtml(tracker)}</span>
|
|
<span class="size">💾 ${size}</span>
|
|
<span class="seeds">🌱 ${seeds}</span>
|
|
${quality ? `<span class="quality">${escapeHtml(quality)}</span>` : ''}
|
|
</div>
|
|
</div>
|
|
<div class="torrent-actions">
|
|
${detailsUrl ? `<a href="${detailsUrl}" target="_blank" class="btn-link" title="Voir sur le tracker">🔗</a>` : ''}
|
|
${magnetUrl ? `<a href="${magnetUrl}" class="btn-magnet" title="Magnet">🧲</a>` : ''}
|
|
${downloadUrl ? `<a href="${downloadUrl}" target="_blank" class="btn-download" title="Télécharger .torrent">⬇️</a>` : ''}
|
|
${torrentClientEnabled && torrentUrl ?
|
|
`<button class="btn-send" id="send-btn-${index}" onclick="handleSendToClient('${escapeHtml(torrentUrl)}', 'send-btn-${index}')" title="Envoyer au client">📥</button>`
|
|
: ''}
|
|
</div>
|
|
</div>
|
|
`;
|
|
}).join('');
|
|
}
|
|
|
|
function handleSendToClient(url, buttonId) {
|
|
const button = document.getElementById(buttonId);
|
|
sendToClient(url, button);
|
|
}
|
|
|
|
// ============================================================
|
|
// ENVOI AU CLIENT TORRENT
|
|
// ============================================================
|
|
|
|
async function sendToClient(url, buttonElement) {
|
|
if (!url) return;
|
|
showTorrentOptionsModal(url, buttonElement);
|
|
}
|
|
|
|
async function showTorrentOptionsModal(url, button) {
|
|
let modal = document.getElementById('torrentOptionsModal');
|
|
if (!modal) {
|
|
modal = document.createElement('div');
|
|
modal.id = 'torrentOptionsModal';
|
|
modal.className = 'torrent-options-modal';
|
|
modal.innerHTML = `
|
|
<div class="torrent-options-content">
|
|
<h3>📥 Options de téléchargement</h3>
|
|
<div class="torrent-option-group">
|
|
<label for="torrentCategory">Catégorie</label>
|
|
<select id="torrentCategory">
|
|
<option value="">-- Aucune --</option>
|
|
</select>
|
|
</div>
|
|
<div class="torrent-option-group">
|
|
<label for="torrentSavePath">Dossier (optionnel)</label>
|
|
<input type="text" id="torrentSavePath" placeholder="/chemin/vers/dossier">
|
|
</div>
|
|
<div class="torrent-option-group checkbox-group">
|
|
<input type="checkbox" id="torrentPaused">
|
|
<label for="torrentPaused">Démarrer en pause</label>
|
|
</div>
|
|
<div class="torrent-options-buttons">
|
|
<button class="btn-cancel" onclick="closeTorrentOptionsModal()">Annuler</button>
|
|
<button class="btn-confirm" id="confirmTorrentAdd">Envoyer</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
document.body.appendChild(modal);
|
|
|
|
modal.addEventListener('click', (e) => {
|
|
if (e.target === modal) closeTorrentOptionsModal();
|
|
});
|
|
}
|
|
|
|
const categorySelect = document.getElementById('torrentCategory');
|
|
const savePathInput = document.getElementById('torrentSavePath');
|
|
categorySelect.innerHTML = '<option value="">-- Chargement... --</option>';
|
|
|
|
let categoriesWithPaths = {};
|
|
|
|
try {
|
|
const response = await fetch('/api/torrent-client/categories');
|
|
const data = await response.json();
|
|
|
|
categorySelect.innerHTML = '<option value="">-- Aucune --</option>';
|
|
if (data.success && data.categories) {
|
|
data.categories.forEach(cat => {
|
|
categorySelect.innerHTML += `<option value="${escapeHtml(cat)}">${escapeHtml(cat)}</option>`;
|
|
});
|
|
categoriesWithPaths = data.custom_categories || {};
|
|
}
|
|
} catch (error) {
|
|
categorySelect.innerHTML = '<option value="">-- Aucune --</option>';
|
|
}
|
|
|
|
categorySelect.onchange = () => {
|
|
const selectedCat = categorySelect.value;
|
|
if (selectedCat && categoriesWithPaths[selectedCat]) {
|
|
savePathInput.value = categoriesWithPaths[selectedCat];
|
|
} else {
|
|
savePathInput.value = '';
|
|
}
|
|
};
|
|
|
|
savePathInput.value = '';
|
|
document.getElementById('torrentPaused').checked = false;
|
|
|
|
const confirmBtn = document.getElementById('confirmTorrentAdd');
|
|
confirmBtn.onclick = async () => {
|
|
const category = document.getElementById('torrentCategory').value;
|
|
const savePath = document.getElementById('torrentSavePath').value.trim();
|
|
const paused = document.getElementById('torrentPaused').checked;
|
|
|
|
closeTorrentOptionsModal();
|
|
await doSendToTorrentClient(url, button, category, savePath, paused);
|
|
};
|
|
|
|
modal.classList.add('visible');
|
|
}
|
|
|
|
function closeTorrentOptionsModal() {
|
|
const modal = document.getElementById('torrentOptionsModal');
|
|
if (modal) {
|
|
modal.classList.remove('visible');
|
|
}
|
|
}
|
|
|
|
async function doSendToTorrentClient(url, button, category, savePath, paused) {
|
|
if (button) {
|
|
button.textContent = '⏳';
|
|
button.disabled = true;
|
|
}
|
|
|
|
try {
|
|
const body = { url: url };
|
|
if (category) body.category = category;
|
|
if (savePath) body.save_path = savePath;
|
|
if (paused) body.paused = paused;
|
|
|
|
const response = await fetch('/api/torrent-client/add', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(body)
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
if (button) {
|
|
button.textContent = '✅';
|
|
setTimeout(() => {
|
|
button.textContent = '📥';
|
|
button.disabled = false;
|
|
}, 2000);
|
|
}
|
|
showToast('Torrent envoyé !', 'success');
|
|
} else {
|
|
if (button) {
|
|
button.textContent = '❌';
|
|
setTimeout(() => {
|
|
button.textContent = '📥';
|
|
button.disabled = false;
|
|
}, 2000);
|
|
}
|
|
showToast(data.error || 'Erreur', 'error');
|
|
}
|
|
} catch (error) {
|
|
if (button) {
|
|
button.textContent = '❌';
|
|
setTimeout(() => {
|
|
button.textContent = '📥';
|
|
button.disabled = false;
|
|
}, 2000);
|
|
}
|
|
showToast('Erreur de connexion', 'error');
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// UTILITAIRES
|
|
// ============================================================
|
|
|
|
function formatSize(bytes) {
|
|
if (!bytes) return 'N/A';
|
|
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
|
return (bytes / Math.pow(1024, i)).toFixed(2) + ' ' + sizes[i];
|
|
}
|
|
|
|
function escapeHtml(text) {
|
|
if (!text) return '';
|
|
const div = document.createElement('div');
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
|
|
function showToast(message, type = 'info') {
|
|
let toast = document.getElementById('toast');
|
|
if (!toast) {
|
|
toast = document.createElement('div');
|
|
toast.id = 'toast';
|
|
toast.className = 'toast';
|
|
document.body.appendChild(toast);
|
|
}
|
|
|
|
toast.textContent = message;
|
|
toast.className = `toast ${type}`;
|
|
toast.classList.remove('hidden');
|
|
|
|
setTimeout(() => {
|
|
toast.classList.add('hidden');
|
|
}, 3000);
|
|
}
|