diff --git a/README.md b/README.md
index 378a5ed..4b1f28c 100644
--- a/README.md
+++ b/README.md
@@ -19,9 +19,7 @@ Built with a serverless architecture on Cloudflare Pages, it's secure, infinitel
- **Duplicate Prevention**: Intelligently prevents users from adding a song that is already in the queue or currently playing.
- **Consent-Driven Analytics**: Integrates with **Google Analytics** and asks for user consent via a two-step welcome modal, ensuring privacy compliance.
- **Robust Search & Previews**: Instantly search Spotify's entire library and listen to 30-second audio previews before queueing.
-- **Full Admin Control**:
- - **Maintenance Mode**: Easily take the site offline with a single environment variable, redirecting all traffic to a maintenance page.
- - **Protected Rate-Limit Reset**: A dedicated `/reset.html` page, secured by **hCaptcha**, allows users to reset their local anti-spam limits.
+- **Maintenance Mode**: Easily take the site offline with a single environment variable, redirecting all traffic to a maintenance page.
- **Event Customization**: Centrally manage your event's name and branding through environment variables.
- **Modern UI/UX**: A polished dark-mode interface with smooth animations, toast notifications, and confetti effects for a premium user experience.
diff --git a/functions/_middleware.js b/functions/_middleware.js
new file mode 100644
index 0000000..ea8726c
--- /dev/null
+++ b/functions/_middleware.js
@@ -0,0 +1,23 @@
+export async function onRequest(context) {
+ const { request, env, next } = context;
+ const url = new URL(request.url);
+ const pathname = url.pathname;
+
+ if (env.MAINTENANCE !== 'TRUE') {
+ return await next();
+ }
+
+ if (pathname.startsWith('/maintenance.html')) {
+ return await next();
+ }
+
+ const maintenanceAsset = await env.ASSETS.fetch(new URL('/maintenance.html', request.url));
+
+ return new Response(maintenanceAsset.body, {
+ status: 503,
+ statusText: 'Service Unavailable',
+ headers: {
+ 'Content-Type': 'text/html; charset=utf-8',
+ },
+ });
+}
diff --git a/functions/api/config.js b/functions/api/config.js
index 8d4e42b..34f7388 100644
--- a/functions/api/config.js
+++ b/functions/api/config.js
@@ -1,15 +1,12 @@
-// functions/api/config.js
-
export async function onRequestGet(context) {
- // Legge la variabile d'ambiente EVENT_NAME. Se non è definita, usa un valore di default.
const eventName = context.env.EVENT_NAME || "Il Nostro Fantastico Evento";
+ const googleAnalyticsId = context.env.GOOGLE_ANALYTICS_ID || null;
- // Prepara i dati di configurazione da inviare al frontend
const config = {
eventName: eventName,
+ googleAnalyticsId: googleAnalyticsId,
};
- // Restituisce la configurazione in formato JSON
return new Response(JSON.stringify(config), {
status: 200,
headers: { 'Content-Type': 'application/json' },
diff --git a/functions/api/queue.js b/functions/api/queue.js
new file mode 100644
index 0000000..6689817
--- /dev/null
+++ b/functions/api/queue.js
@@ -0,0 +1,60 @@
+async function getAccessToken(context) {
+ const { SPOTIFY_CLIENT_ID, SPOTIFY_CLIENT_SECRET, SPOTIFY_REFRESH_TOKEN } = context.env;
+ if (!SPOTIFY_CLIENT_ID || !SPOTIFY_CLIENT_SECRET || !SPOTIFY_REFRESH_TOKEN) {
+ throw new Error("Variabili d'ambiente del server non configurate.");
+ }
+
+ const basicAuth = btoa(`${SPOTIFY_CLIENT_ID}:${SPOTIFY_CLIENT_SECRET}`);
+ const response = await fetch('https://accounts.spotify.com/api/token', {
+ method: 'POST',
+ headers: { 'Authorization': `Basic ${basicAuth}`, 'Content-Type': 'application/x-www-form-urlencoded' },
+ body: new URLSearchParams({ grant_type: 'refresh_token', refresh_token: SPOTIFY_REFRESH_TOKEN }),
+ });
+
+ if (!response.ok) throw new Error("Impossibile rinfrescare il token di Spotify.");
+ const data = await response.json();
+ return data.access_token;
+}
+
+export async function onRequestGet(context) {
+ try {
+ const accessToken = await getAccessToken(context);
+ const queueEndpoint = 'https://api.spotify.com/v1/me/player/queue';
+
+ const queueResponse = await fetch(queueEndpoint, {
+ headers: { 'Authorization': `Bearer ${accessToken}` },
+ });
+
+ if (queueResponse.status === 204 || queueResponse.status > 400) {
+ return new Response(JSON.stringify({ nowPlaying: null, queue: [] }), {
+ status: 200,
+ headers: { 'Content-Type': 'application/json' },
+ });
+ }
+
+ const data = await queueResponse.json();
+
+ const nowPlaying = data.currently_playing ? {
+ uri: data.currently_playing.uri,
+ } : null;
+
+ const queue = (data.queue || []).map(item => ({
+ track: item.name || 'Traccia Sconosciuta',
+ artists: (item.artists || []).map(a => a.name).join(', '),
+ albumCover: item.album?.images?.[0]?.url || '',
+ uri: item.uri || '',
+ }));
+
+ return new Response(JSON.stringify({ nowPlaying, queue }), {
+ status: 200,
+ headers: { 'Content-Type': 'application/json' },
+ });
+
+ } catch (error) {
+ console.error("Errore nella funzione queue:", error.message);
+ return new Response(JSON.stringify({ error: error.message }), {
+ status: 500,
+ headers: { 'Content-Type': 'application/json' },
+ });
+ }
+}
diff --git a/maintenance.html b/maintenance.html
new file mode 100644
index 0000000..713b634
--- /dev/null
+++ b/maintenance.html
@@ -0,0 +1,83 @@
+
+
+
+
+
+
+ Temporanemente non disponibile
+
+
+
+
+
+
+
+
Servizio momentaneamente non disponibile
+
+
+ Ci scusiamo per il disagio.
L'applicazione è in fase di manutenzione o l'evento potrebbe non essere ancora iniziato!
Riprova tra qualche minuto.
+
+
+
+
+
+
diff --git a/reset.html b/reset.html
deleted file mode 100644
index 5af5391..0000000
--- a/reset.html
+++ /dev/null
@@ -1,185 +0,0 @@
-
-
-
-
-
-
- Song Limit Reset
-
-
-
-
-
-
-
-
-
Song limit reset
-
-
-
-
Reset the local anti-spam limit on this device/browser (3 songs / 10 minutes).
-
-
-
-
-
-
-
-
Status
-
Ready.
-
- Remaining: —
- Window: 10 min
-
-
-
-
-
-
-
-
- ✓
-
-
-
-
-
diff --git a/script.js b/script.js
index d71956e..0f53394 100644
--- a/script.js
+++ b/script.js
@@ -1,4 +1,4 @@
-let accessToken = null; // caricato da env cloudflare pages
+let accessToken = null;
const ADD_LIMIT = 3;
const TIME_LIMIT_MINUTES = 10;
@@ -10,15 +10,14 @@ const headersAuth = () => ({
'Content-Type': 'application/json'
});
-const toast = (msg) => alert(msg);
-
function canAddTrack() {
const now = Date.now();
const hist = JSON.parse(localStorage.getItem('addHistory')) || [];
const recent = hist.filter(ts => now - ts < TIME_LIMIT_MINUTES * 60 * 1000);
if (recent.length >= ADD_LIMIT) {
const minutesLeft = Math.ceil((TIME_LIMIT_MINUTES * 60 * 1000 - (now - recent[0])) / (60 * 1000));
- alert(`Hey, slow down! You can add another song in ${minutesLeft} minutes.`);
+ const message = `Limite raggiunto! Potrai aggiungere un'altra canzone tra ${minutesLeft} minuti.`;
+ window.UIBridge?.showToast?.(message);
return false;
}
return true;
@@ -35,8 +34,8 @@ window.canAddTrack = canAddTrack;
window.saveTrackAddition = saveTrackAddition;
async function addToQueue(trackUri) {
- if (!accessToken) { toast('Error: No token available to add the song.'); return false; }
- if (!trackUri) { toast('Error: invalid track.'); return false; }
+ if (!accessToken) { window.UIBridge?.showToast?.('Errore: Token non disponibile.'); return false; }
+ if (!trackUri) { window.UIBridge?.showToast?.('Errore: Traccia non valida.'); return false; }
try {
const res = await fetch(`${API_BASE}/me/player/queue?uri=${encodeURIComponent(trackUri)}`, {
@@ -45,23 +44,22 @@ async function addToQueue(trackUri) {
});
if (res.ok) {
- toast('Hands up! Your song will be played soon!');
+ window.UIBridge?.showToast?.('Aggiunto! Il tuo brano verrà suonato a breve!');
return true;
}
- if (res.status === 404) { toast('No active devices, please make sure at least one device is playing content.'); return false; }
- if (res.status === 401) { toast('Token expired or invalid. Please refresh your token.'); return false; }
- if (res.status === 429) { toast('Spotify rate limit hit. Try again in a bit.'); return false; }
- if (res.status === 403) { toast('Permission denied. Check scopes.'); return false; }
+ if (res.status === 404) { window.UIBridge?.showToast?.('Nessun dispositivo attivo. Assicurati che Spotify stia suonando.'); return false; }
+ if (res.status === 401) { window.UIBridge?.showToast?.('Token scaduto o non valido.'); return false; }
+ if (res.status === 429) { window.UIBridge?.showToast?.('Limite richieste Spotify raggiunto. Riprova tra poco.'); return false; }
+ if (res.status === 403) { window.UIBridge?.showToast?.('Permesso negato.'); return false; }
- const txt = await res.text().catch(()=> '');
- console.error('Queue error:', res.status, res.statusText, txt);
- toast('Could not add to queue. Try again.');
+ console.error('Errore Coda:', res.status, await res.text().catch(()=>''));
+ window.UIBridge?.showToast?.('Impossibile aggiungere alla coda. Riprova.');
return false;
} catch (err) {
- console.error('Error adding the track to the queue:', err);
- toast('Network error adding the track. Try again.');
+ console.error('Errore di rete nell\'aggiunta:', err);
+ window.UIBridge?.showToast?.('Errore di rete. Riprova.');
return false;
}
}
@@ -71,9 +69,9 @@ window.addToQueue = addToQueue;
let searchAbort;
async function doSearch(query, limit = 10) {
- if (!accessToken) { toast('There was a problem. Please make sure you entered the token correctly.'); return; }
+ if (!accessToken) { window.UIBridge?.showToast?.('Problema di configurazione. Token non trovato.'); return; }
const q = (query || '').trim();
- if (!q) { toast('Please enter a valid search term.'); return; }
+ if (!q) { window.UIBridge?.showToast?.('Inserisci un termine di ricerca valido.'); return; }
try {
if (searchAbort) searchAbort.abort();
@@ -92,44 +90,41 @@ async function doSearch(query, limit = 10) {
let lastErr = null;
for (const url of tries) {
- const res = await fetch(url, { headers: headersAuth(), signal });
- if (res.ok) {
- data = await res.json();
- break;
- } else {
- const bodyText = await res.text().catch(()=> '');
- lastErr = { status: res.status, statusText: res.statusText, bodyText };
- if (res.status === 401) break;
+ try {
+ const res = await fetch(url, { headers: headersAuth(), signal });
+ if (res.ok) {
+ data = await res.json();
+ break;
+ } else {
+ lastErr = { status: res.status, body: await res.text().catch(()=>'') };
+ if (res.status === 401) break;
+ }
+ } catch (loopErr) {
+ lastErr = { status: 'NETWORK_ERROR', body: loopErr.message };
}
}
-
+
if (!data) {
- if (lastErr) {
- console.error('Search failed:', lastErr.status, lastErr.statusText, lastErr.bodyText);
- }
- toast('Song search error. Make sure the token is valid.');
- window.UIBridge?.showError?.('Song search error. Try again.');
- return;
+ console.error('Errore Ricerca Finale:', lastErr);
+ window.UIBridge?.showError?.('Errore nella ricerca. Assicurati che il token sia valido.');
+ return;
}
const items = (data?.tracks?.items || []).map(t => ({
image: t?.album?.images?.[0]?.url || '',
- title: t?.name || 'Unknown title',
- subtitle:(t?.artists || []).map(a => a?.name).filter(Boolean).join(', ') || 'Unknown artist',
+ title: t?.name || 'Titolo Sconosciuto',
+ subtitle:(t?.artists || []).map(a => a?.name).filter(Boolean).join(', ') || 'Artista Sconosciuto',
uri: t?.uri || '',
preview: t?.preview_url || null,
- onclick: () => addToQueue(t?.uri)
}));
window.UIBridge?.renderItems?.(items, q);
} catch (err) {
if (err.name !== 'AbortError') {
- console.error('Song search error:', err);
- toast('Search failed. Please try again.');
- window.UIBridge?.showError?.('Search failed. Please try again.');
+ console.error('Errore Ricerca:', err);
+ window.UIBridge?.showError?.('Ricerca fallita. Riprova.');
}
- } finally {
}
}
@@ -142,29 +137,24 @@ document.addEventListener('ui:search', e => {
async function fetchAccessToken() {
try {
const response = await fetch('/api/token');
- if (!response.ok) {
- const errBody = await response.text();
- console.error('Errore dal server token:', errBody);
- throw new Error('Impossibile recuperare il token di accesso dal server.');
- }
+ if (!response.ok) throw new Error('Impossibile recuperare il token dal server.');
+
const data = await response.json();
if (data.accessToken) {
accessToken = data.accessToken;
- console.log('Token caricato con successo.');
+ console.log('Token caricato.');
document.getElementById('song-query').disabled = false;
document.getElementById('search-btn').disabled = false;
} else {
- throw new Error('Il token ricevuto non è valido.');
+ throw new Error('Token ricevuto non valido.');
}
} catch (err) {
console.error(err);
- alert('ERRORE CRITICO: Impossibile caricare la configurazione. L\'app non funzionerà.');
+ window.UIBridge?.showError?.('ERRORE CRITICO: Impossibile caricare la configurazione. L\'app non funzionerà.');
document.getElementById('song-query').placeholder = 'Errore di configurazione';
- window.UIBridge?.showError?.('Errore di configurazione del server. Contattare l\'organizzatore.');
}
}
window.addEventListener('load', () => {
- // La ricerca è già disabilitata via HTML, la funzione fetchAccessToken la abiliterà
fetchAccessToken();
});