diff --git a/.env.example b/.env.example index d91b5b6..328a128 100644 --- a/.env.example +++ b/.env.example @@ -7,6 +7,5 @@ VITE_FIREBASE_PROJECT_ID= VITE_FIREBASE_STORAGE_BUCKET= VITE_FIREBASE_MESSAGING_SENDER_ID= VITE_FIREBASE_APP_ID= - -# Optional (only if you enabled Analytics) VITE_FIREBASE_MEASUREMENT_ID= +VITE_FIREBASE_VAPID_KEY= diff --git a/public/badge-72x72.png b/public/badge-72x72.png new file mode 100644 index 0000000..d4f458b Binary files /dev/null and b/public/badge-72x72.png differ diff --git a/public/icons/192x192.png b/public/icons/192x192.png index fa2506c..ea007d5 100644 Binary files a/public/icons/192x192.png and b/public/icons/192x192.png differ diff --git a/public/icons/512x512.png b/public/icons/512x512.png index fa2506c..75ab941 100644 Binary files a/public/icons/512x512.png and b/public/icons/512x512.png differ diff --git a/public/pwa-192x192.png b/public/pwa-192x192.png new file mode 100644 index 0000000..ea007d5 Binary files /dev/null and b/public/pwa-192x192.png differ diff --git a/public/pwa-512x512.png b/public/pwa-512x512.png new file mode 100644 index 0000000..75ab941 Binary files /dev/null and b/public/pwa-512x512.png differ diff --git a/public/pwa-maskable-192x192.png b/public/pwa-maskable-192x192.png new file mode 100644 index 0000000..ea007d5 Binary files /dev/null and b/public/pwa-maskable-192x192.png differ diff --git a/public/pwa-maskable-512x512.png b/public/pwa-maskable-512x512.png new file mode 100644 index 0000000..75ab941 Binary files /dev/null and b/public/pwa-maskable-512x512.png differ diff --git a/public/screenshot-1.png b/public/screenshot-1.png new file mode 100644 index 0000000..75ab941 Binary files /dev/null and b/public/screenshot-1.png differ diff --git a/public/screenshot-2.png b/public/screenshot-2.png new file mode 100644 index 0000000..75ab941 Binary files /dev/null and b/public/screenshot-2.png differ diff --git a/src/App.tsx b/src/App.tsx index e98a059..fcc557d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -3,10 +3,14 @@ import Home from "./pages/Home"; import Admin from "./pages/Admin"; import Profile from "./pages/Profile"; import { auth, db } from "./firebase"; -import { useEffect, useState } from "react"; +import { useEffect, useState, useRef } from "react"; import { onAuthStateChanged } from "firebase/auth"; import type { user, video } from "./types"; import { collection, doc, onSnapshot, setDoc } from "firebase/firestore"; +import { requestForToken } from './firebase'; +import { EXPRESS_API_URL } from "./apiConfig"; +import Alert from "./components/common/Alert"; +import { getMessaging, onMessage } from "firebase/messaging"; // App.tsx @@ -15,7 +19,35 @@ export default function App() { const [videos, setVideos] = useState([]); const [loading, setLoading] = useState(true); const [isOffline, setIsOffline] = useState(() => !navigator.onLine); + const [notificationAlert, setNotificationAlert] = useState<{ title: string; message: string } | null>(null); + const prevSubscriptionsRef = useRef(null); + + useEffect(() => { + requestForToken(); + + const messaging = getMessaging(); + + const unsubscribeForeground = onMessage(messaging, (payload: any) => { + console.log("Notifica ricevuta in FOREGROUND:", payload); + + const title = payload?.notification?.title ?? payload?.data?.title ?? "Notifica"; + const body = payload?.notification?.body ?? payload?.data?.body ?? "Hai una nuova notifica! Controlla il tuo profilo."; + + // Mostriamo l'Alert a schermo + setNotificationAlert({ + title: `${title}`, + message: `${body}` + }); + }); + + return () => { + unsubscribeForeground(); + }; + + }, []); + + //Gestione autenticazione e dati utente in tempo reale useEffect(() => { let unsubscribeUserDoc: () => void = () => {}; @@ -23,10 +55,84 @@ export default function App() { if (firebaseUser) { const userDocRef = doc(db, "users", firebaseUser.uid); + const setupNotifications = async () => { + try { + const currentToken = await requestForToken(); + + if (currentToken) { + await fetch(`${EXPRESS_API_URL}/subscribeAll`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + token: currentToken, + uid: firebaseUser.uid + }) + }); + + console.log("Iscrizione ai topic sincronizzata!"); + } + } catch (error) { + console.error("Errore durante la registrazione per le notifiche:", error); + } + }; + setupNotifications(); + // Ascolta i cambiamenti in tempo reale sul documento dell'utente - unsubscribeUserDoc = onSnapshot(userDocRef, (docSnap) => { + unsubscribeUserDoc = onSnapshot(userDocRef, async (docSnap) => { if (docSnap.exists()) { const userData = docSnap.data(); + const newSubscriptions = userData.subscriptions || []; + + if (prevSubscriptionsRef.current !== null) { + const addedCountries = newSubscriptions.filter( + (country : string) => !prevSubscriptionsRef.current!.includes(country) + ); + + const removedCountries = prevSubscriptionsRef.current.filter( + (country : string) => !newSubscriptions.includes(country) + ); + + if (addedCountries.length > 0 || removedCountries.length > 0) { + console.log("Rilevate nuove iscrizioni da un altro dispositivo:", addedCountries); + console.log("Rilevate cancellazioni da un altro dispositivo:", removedCountries); + + try { + const currentToken = await requestForToken(); + if (currentToken) { + // eseguiamo tutte le iscrizioni ai nuovi paesi aggiunti + for (const country of addedCountries) { + await fetch(`${EXPRESS_API_URL}/subscribe`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + token: currentToken, + country: country, + uid: firebaseUser.uid + }) + }); + } + + // eseguiamo tutte le cancellazioni dai paesi rimossi + for (const country of removedCountries) { + await fetch(`${EXPRESS_API_URL}/unsubscribe`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + token: currentToken, + country: country, + uid: firebaseUser.uid + }) + }); + } + console.log("Dispositivo auto-sincronizzato con i nuovi paesi!"); + } + } catch (error) { + console.error("Errore di auto-sincronizzazione in background:", error); + } + } + } + prevSubscriptionsRef.current = newSubscriptions; + setUser({ uid: firebaseUser.uid, email: firebaseUser.email ?? "", @@ -34,6 +140,7 @@ export default function App() { role: (userData.role as "user" | "moderator" | "admin") ?? "user", photoURL: firebaseUser.photoURL ?? "", stats: userData.stats || { pendingVideos: 0, approvedVideos: 0, rejectedVideos: 0, suggestedVideos: 0 }, + subscriptions: userData.subscriptions || [], }); } else { // Se il documento non esiste, lo creiamo (fatto una sola volta) @@ -44,6 +151,7 @@ export default function App() { role: "user", photoURL: firebaseUser.photoURL ?? "", stats: { pendingVideos: 0, approvedVideos: 0, rejectedVideos: 0, suggestedVideos: 0 }, + subscriptions: [], }; setDoc(userDocRef, newUser); setUser(newUser); @@ -71,6 +179,7 @@ export default function App() { }; }, []); + // Gestione stato online/offline useEffect(() => { const handleOnline = () => setIsOffline(false); const handleOffline = () => setIsOffline(true); @@ -123,6 +232,14 @@ export default function App() { return ( <> {offlineBanner} + {notificationAlert && ( + setNotificationAlert(null)} + /> + )} } /> diff --git a/src/apiConfig.ts b/src/apiConfig.ts new file mode 100644 index 0000000..3165753 --- /dev/null +++ b/src/apiConfig.ts @@ -0,0 +1 @@ +export const EXPRESS_API_URL = "https://world-video-guide-backend.onrender.com/api"; \ No newline at end of file diff --git a/src/components/common/Alert.tsx b/src/components/common/Alert.tsx index 99232a0..ea1fa91 100644 --- a/src/components/common/Alert.tsx +++ b/src/components/common/Alert.tsx @@ -2,10 +2,10 @@ import { useEffect, useState } from "react"; import { AnimatePresence, motion } from "framer-motion"; -import { HiCheckCircle, HiExclamationTriangle } from "react-icons/hi2"; +import { HiCheckCircle, HiExclamationTriangle, HiInformationCircle } from "react-icons/hi2"; interface AlertProps { - type: "success" | "error"; + type: "success" | "error" | "info"; message: string; duration?: number; // Durata in millisecondi prima di scomparire onClose?: () => void; // callback opzionale chiamata dopo che l'alert si è nascosto @@ -35,9 +35,9 @@ export default function Alert({ type, message, duration = 3000, onClose }: Alert } }, [visible, onClose]); - const Icon = type === "success" ? HiCheckCircle : HiExclamationTriangle; - const iconColor = type === "success" ? "text-green-500" : "text-red-500"; - const bgColor = type === "success" ? "bg-green-500/10 border-green-500/20" : "bg-red-500/10 border-red-500/20"; + const Icon = type === "success" ? HiCheckCircle : type === "error" ? HiExclamationTriangle : HiInformationCircle; + const iconColor = type === "success" ? "text-green-500" : type === "error" ? "text-red-500" : "text-blue-500"; + const bgColor = type === "success" ? "bg-green-500/10 border-green-500/20" : type === "error" ? "bg-red-500/10 border-red-500/20" : "bg-blue-500/10 border-blue-500/20"; return ( diff --git a/src/components/common/Header.tsx b/src/components/common/Header.tsx index 59ab283..f1d8eb3 100644 --- a/src/components/common/Header.tsx +++ b/src/components/common/Header.tsx @@ -8,6 +8,9 @@ import { TbWorldSearch } from "react-icons/tb"; import { Link } from "react-router"; import { FiShield, FiUser } from "react-icons/fi"; import Alert from "./Alert"; +import { getToken } from "firebase/messaging"; +import { messaging } from "../../firebase" +import { EXPRESS_API_URL } from "../../apiConfig"; @@ -17,6 +20,42 @@ export default function Header({ user, page }: { user: user | null; page: string const [alert, setAlert] = useState<{ type: "success" | "error"; message: string } | null>(null); + const logOut = async () => { + // 1. Fase di pulizia: Disiscrizione da tutti i Topic + if (user) { + try { + const currentToken = await getToken(messaging, { + vapidKey: import.meta.env.VITE_FIREBASE_VAPID_KEY + }); + + if (currentToken) { + console.log("Tentativo di disiscrizione al logout in corso..."); + await fetch(`${EXPRESS_API_URL}/unsubscribeAll`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + token: currentToken, + uid: user.uid + }) + }); + } + } catch (error) { + // L'utente non ha mai accettato le notifiche o è offline. + // Va bene così, non blocchiamo il logout. + console.log("Nessun token attivo per la disiscrizione al logout."); + } + } + + // 2. Fase di disconnessione effettiva + try { + await signOut(auth); + setAlert({ type: "success", message: "Ti sei disconnesso con successo." }); + } catch (error) { + console.error("Errore disconnessione:", error); + setAlert({ type: "error", message: "Si è verificato un errore durante la disconnessione." }); + } + }; + return ( <> @@ -67,7 +106,7 @@ export default function Header({ user, page }: { user: user | null; page: string
{user ? ( - signOut(auth)} align="right" /> + logOut()} align="right" /> ) : (
-
- +
+ {filteredVideos.filter(v => v.status === "approved").length} Video + + {/* BOTTONE NOTIFICHE */} + {isSubscribed ? ( + + ) : ( + + )}
diff --git a/src/components/profile/FollowedCountry.tsx b/src/components/profile/FollowedCountry.tsx new file mode 100644 index 0000000..8f49870 --- /dev/null +++ b/src/components/profile/FollowedCountry.tsx @@ -0,0 +1,223 @@ +import { useEffect, useState } from "react"; +import { FiGlobe, FiTrash2 } from "react-icons/fi"; +import { arrayRemove, doc, updateDoc } from "firebase/firestore"; +import { getToken } from "firebase/messaging"; +import type { user } from "../../types"; +import { db, messaging } from "../../firebase"; +import { EXPRESS_API_URL } from "../../apiConfig"; + +type FollowedCountryItem = { + name: string; + label: string; + flagUrl?: string; +}; + +interface FollowedCountryProps { + user: user | null; +} + +async function fetchCountryDetails(countryName: string): Promise { + const endpoint = `https://restcountries.com/v3.1/name/${encodeURIComponent(countryName)}?fields=name,flags,translations,cca2`; + + try { + let response = await fetch(endpoint); + + if (!response.ok) { + throw new Error(`HTTP ${response.status}`); + } + + const data = await response.json(); + const country = data?.[0]; + + return { + name: countryName, + label: country?.translations?.ita?.common || country?.name?.common || countryName, + flagUrl: `https://hatscripts.github.io/circle-flags/flags/${country?.cca2?.toLowerCase()}.svg`, + }; + } catch { + return { + name: countryName, + label: countryName, + }; + } +} + +export default function FollowedCountry({ user }: FollowedCountryProps) { + const [countries, setCountries] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [removingCountry, setRemovingCountry] = useState(null); + + useEffect(() => { + let cancelled = false; + + const loadCountries = async () => { + if (!user?.subscriptions?.length) { + setCountries([]); + setLoading(false); + return; + } + + setLoading(true); + setError(null); + + try { + const uniqueCountries = Array.from(new Set(user.subscriptions)); + const details = await Promise.all(uniqueCountries.map((country) => fetchCountryDetails(country))); + + if (!cancelled) { + setCountries(details.sort((a, b) => a.label.localeCompare(b.label))); + } + } catch (loadError) { + console.error("Errore caricamento nazioni seguite:", loadError); + if (!cancelled) { + setError("Impossibile caricare le nazioni seguite."); + setCountries(user.subscriptions.map((country) => ({ name: country, label: country }))); + } + } finally { + if (!cancelled) { + setLoading(false); + } + } + }; + + loadCountries(); + + return () => { + cancelled = true; + }; + }, [user?.subscriptions]); + + const handleRemoveCountry = async (countryName: string) => { + if (!user) return; + + if (!navigator.onLine) { + setError("Sei offline. Connettiti a internet per rimuovere una nazione seguita."); + return; + } + + setRemovingCountry(countryName); + setError(null); + + try { + const currentToken = await getToken(messaging, { + vapidKey: import.meta.env.VITE_FIREBASE_VAPID_KEY, + }); + + if (!currentToken) { + throw new Error("Token notifiche non disponibile"); + } + + const response = await fetch(`${EXPRESS_API_URL}/unsubscribe`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + country: countryName, + uid: user.uid, + token: currentToken, + }), + }); + + const data = await response.json(); + + if (!response.ok || !data?.success) { + throw new Error(data?.message || "Errore durante la disiscrizione"); + } + + await updateDoc(doc(db, "users", user.uid), { + subscriptions: arrayRemove(countryName), + }); + + setCountries((current) => current.filter((country) => country.name !== countryName)); + } catch (removeError) { + console.error("Errore rimozione nazione seguita:", removeError); + setError("Non è stato possibile rimuovere la nazione seguita."); + } finally { + setRemovingCountry(null); + } + }; + + if (!user) return null; + + const hasCountries = countries.length > 0; + + return ( +
+
+
+
+
+ +
+
+

Nazioni seguite

+

Le nazioni salvate nel tuo profilo e sincronizzate con le notifiche.

+
+
+
+
+ {countries.length} + seguite +
+
+ + {loading ? ( +
+ {Array.from({ length: 3 }).map((_, index) => ( +
+ ))} +
+ ) : hasCountries ? ( +
+ {countries.map((country) => { + const isRemoving = removingCountry === country.name; + + return ( +
+
+ {country.flagUrl ? ( + {country.label} + ) : ( + {country.label.slice(0, 2).toUpperCase()} + )} +
+ +
+

{country.label}

+

{country.name}

+
+ + +
+ ); + })} +
+ ) : ( +
+ +

Nessuna nazione seguita

+

+ Quando segui un paese dalla mappa, comparirà qui e potrai rimuoverlo in qualsiasi momento. +

+
+ )} + + {error && ( +

+ {error} +

+ )} +
+ ); +} diff --git a/src/components/profile/UserVideoList.tsx b/src/components/profile/UserVideoList.tsx index 277bd9c..8dca509 100644 --- a/src/components/profile/UserVideoList.tsx +++ b/src/components/profile/UserVideoList.tsx @@ -55,16 +55,34 @@ export default function UserVideoList({ const videosWithThumbnails = await Promise.all( videosData.map(async (video) => { - const res = await fetch(`https://www.youtube.com/oembed?url=${video.url}&format=json`); - const data = await res.json(); - const countryInfo = await fetch(`https://restcountries.com/v3.1/alpha/${video.countryCode}`); - const countryData = await countryInfo.json(); - const flagUrl = countryData[0]?.flags?.png || null; + let datayoutube; + try { + const resYoutube = await fetch(`https://www.youtube.com/oembed?url=${video.url}&format=json`); + datayoutube = await resYoutube.json(); + } catch (error) { + console.error("Errore nel fetch dei dati di YouTube (possibile video non valido):", video.url, error); + datayoutube = { title: "Video non disponibile", thumbnail_url: null }; + } + + let datacountry; + try { + const countryCode = String(video.countryCode).padStart(3, '0'); // Assicurati che il codice sia a 3 cifre + const rescountry = await fetch(`https://restcountries.com/v3.1/alpha/${countryCode}`); + if (!rescountry.ok) { + throw new Error(`HTTP error! status: ${rescountry.status}`); + } + datacountry = await rescountry.json(); + } catch (error) { + console.error("Errore nel fetch dei dati del paese:", video.countryCode, error); + datacountry = [{ name: { common: "Paese non disponibile" }, flags: { png: null } }]; + } + + const flagUrl = datacountry[0]?.flags?.png || null; return { ...video, - thumbnail: data.thumbnail_url, - title: data.title, - country: countryData[0]?.name?.common || "Paese sconosciuto", + thumbnail: datayoutube.thumbnail_url, + title: datayoutube.title, + country: datacountry[0]?.name?.common || "Paese sconosciuto", flag: flagUrl, } as video & { thumbnail?: string }; diff --git a/src/firebase.ts b/src/firebase.ts index dc02e4d..c60462b 100644 --- a/src/firebase.ts +++ b/src/firebase.ts @@ -1,13 +1,8 @@ -// Import the functions you need from the SDKs you need import { initializeApp } from "firebase/app"; -// import { getAnalytics } from "firebase/analytics"; import { getAuth } from "firebase/auth"; import { initializeFirestore, persistentLocalCache } from "firebase/firestore"; -// TODO: Add SDKs for Firebase products that you want to use -// https://firebase.google.com/docs/web/setup#available-libraries +import { getMessaging, getToken } from "firebase/messaging"; -// Your web app's Firebase configuration -// For Firebase JS SDK v7.20.0 and later, measurementId is optional const firebaseConfig = { apiKey: import.meta.env.VITE_FIREBASE_API_KEY, authDomain: import.meta.env.VITE_FIREBASE_AUTH_DOMAIN, @@ -22,6 +17,28 @@ const firebaseConfig = { const app = initializeApp(firebaseConfig); export const auth = getAuth(app); export const db = initializeFirestore(app, { - // Keep Firestore documents available across app restarts while offline. localCache: persistentLocalCache(), -}); \ No newline at end of file +}); + + +// Inizializza Firebase Cloud Messaging +export const messaging = getMessaging(app); + +export const requestForToken = async () => { + try { + const registration = await navigator.serviceWorker.ready; + const currentToken = await getToken(messaging, { + vapidKey: import.meta.env.VITE_FIREBASE_VAPID_KEY, + serviceWorkerRegistration: registration, + }); + if (currentToken) { + console.log('Token generato:', currentToken); + // da implementare invio del token al backend per associarlo all'utente e poter inviare notifiche mirate + return currentToken; + } else { + console.log('Nessun token disponibile. Richiedi il permesso.'); + } + } catch (err) { + console.error('Errore durante il recupero del token:', err); + } +}; \ No newline at end of file diff --git a/src/pages/Admin.tsx b/src/pages/Admin.tsx index 3ed41c3..f4f45ed 100644 --- a/src/pages/Admin.tsx +++ b/src/pages/Admin.tsx @@ -22,14 +22,33 @@ export default function Admin({ user }: { user: user | null }) { const querySnapshot = await getDocs(q); const videosPromises = querySnapshot.docs.map(async (doc) => { - try { const d = doc.data() as any; - const res = await fetch(`https://www.youtube.com/oembed?url=${d.url}&format=json`); - const datayoutube = await res.json(); - const rescountry = await fetch(`https://restcountries.com/v3.1/alpha/${d.countryCode}`); - const datacountry = await rescountry.json(); + + let datayoutube; + try { + const resYoutube = await fetch(`https://www.youtube.com/oembed?url=${d.url}&format=json`); + datayoutube = await resYoutube.json(); + } catch (error) { + console.error("Errore nel fetch dei dati di YouTube (possibile video non valido):", d.url, error); + datayoutube = { title: "Video non disponibile", thumbnail_url: null }; + } + + let datacountry; + try { + const countryCode = String(d.countryCode).padStart(3, '0'); // Assicurati che il codice sia a 3 cifre + const rescountry = await fetch(`https://restcountries.com/v3.1/alpha/${countryCode}`); + if (!rescountry.ok) { + throw new Error(`HTTP error! status: ${rescountry.status}`); + } + datacountry = await rescountry.json(); + } catch (error) { + console.error("Errore nel fetch dei dati del paese:", d.countryCode, error); + datacountry = [{ name: { common: "Paese non disponibile" }, flags: { png: null } }]; + } + console.log("Dati fetchati per video:", datayoutube, datacountry); // Prendiamo anche il DisplayName dello user che ha suggerito il video const suggesterQuery = query(collection(db, "users"), where("uid", "==", d.submittedBy)); + console.log("Query per trovare suggeritore:", d); const suggesterSnapshot = await getDocs(suggesterQuery); const suggesterData = suggesterSnapshot.empty ? null : suggesterSnapshot.docs[0].data() as any; return { @@ -42,20 +61,6 @@ export default function Admin({ user }: { user: user | null }) { suggesterEmail: suggesterData?.email || "Email non disponibile", ...d } as video; - } catch (error) { - console.error("Errore nel recupero dei dati di YouTube per il video:", doc.id, error); - const d = doc.data() as any; - return { - id: doc.id, - thumbnail: null, - title: "Video senza titolo", - country: "Paese non disponibile", - flag: null, - suggesterName: "Utente sconosciuto", - suggesterEmail: "Email non disponibile", - ...d - } as video; - } }); const videos = await Promise.all(videosPromises); diff --git a/src/pages/Profile.tsx b/src/pages/Profile.tsx index 1dfbca6..cc64e65 100644 --- a/src/pages/Profile.tsx +++ b/src/pages/Profile.tsx @@ -2,6 +2,7 @@ import type { user } from "../types"; import Header from "../components/common/Header"; import ProfileHeader from "../components/profile/ProfileHeader"; import StatsGrid from "../components/profile/StatsGrid"; +import FollowedCountry from "../components/profile/FollowedCountry"; import UserVideoList from "../components/profile/UserVideoList"; import { useState } from "react"; import LoginOverlay from "../components/common/LoginOverlay"; @@ -44,6 +45,9 @@ export default function Profile({ user }: { user: user | null}) { {/* Stats Grid */} + {/* Followed Countries */} + + {/* User Video List */} diff --git a/src/sw.js b/src/sw.js index 4b328e8..140d21c 100644 --- a/src/sw.js +++ b/src/sw.js @@ -3,6 +3,8 @@ import { registerRoute, NavigationRoute } from 'workbox-routing'; import { CacheFirst } from 'workbox-strategies'; import { ExpirationPlugin } from 'workbox-expiration'; import { CacheableResponsePlugin } from 'workbox-cacheable-response'; +import { initializeApp } from "firebase/app"; +import { getMessaging, onBackgroundMessage } from "firebase/messaging/sw"; // 1. PRECACHING precacheAndRoute(self.__WB_MANIFEST); @@ -11,8 +13,7 @@ precacheAndRoute(self.__WB_MANIFEST); registerRoute( ({ url }) => url.host === 'upload.wikimedia.org' && url.pathname.includes('Flag_of_the_Taliban'), - async () => { - // Questo è l'URL della bandiera che VUOI far vedere + async () => { const urlGiusta = 'https://upload.wikimedia.org/wikipedia/commons/thumb/c/cd/Flag_of_Afghanistan_%282013%E2%80%932021%29.svg/960px-Flag_of_Afghanistan_%282013%E2%80%932021%29.svg.png'; try { @@ -20,7 +21,7 @@ registerRoute( return response; } catch (err) { console.error("Errore nel caricamento della bandiera sostitutiva", err); - return fetch('/icons/icon-192x192.png'); + return fetch('/icons/192x192.png'); } } ); @@ -77,3 +78,52 @@ registerRoute( ] }) ); + +const firebaseConfig = { + apiKey: 'AIzaSyBXIpv19n2NcA8uMpREfrHAgMhpVbQaKO8', + authDomain: 'prova-mappa-f1d90.firebaseapp.com', + projectId: 'prova-mappa-f1d90', + storageBucket: 'prova-mappa-f1d90.firebasestorage.app', + messagingSenderId: '657564154400', + appId: '1:657564154400:web:8daf0ff43bd049a7c1da45', +}; + +// Initialize Firebase +const app = initializeApp(firebaseConfig); +export const messaging = getMessaging(app); + +// Gestione notifiche quando l'app è chiusa o in background +onBackgroundMessage(messaging, async (payload) => { + console.log('[sw.js] Notifica ricevuta in background:', payload); + + const notificationTitle = payload.data.title || 'Nuovo aggiornamento!'; + + let iconUrl; + try { + const res = await fetch(`https://restcountries.com/v3.1/alpha/${payload.data.countryCode}`); + console.log('[sw.js] Risposta RestCountries:', res); + if (res.ok) { + const countryData = await res.json(); + const resIcon = await fetch(`https://hatscripts.github.io/circle-flags/flags/${countryData[0].cca2.toLowerCase()}.svg`) + if (resIcon.ok) { + iconUrl = `https://hatscripts.github.io/circle-flags/flags/${countryData[0].cca2.toLowerCase()}.svg`; + }else{ + iconUrl = '/icons/192x192.png'; + } + } + } catch (err) { + console.error('[sw.js] Errore nel recupero icona del paese:', err); + } + + const notificationOptions = { + body: payload.data.body, + icon: iconUrl, // Icona a colori (quella che si vede nel popup) + badge: '/badge-72x72.png', // Icona bianca (quella nella barra in alto) + vibrate: [100, 50, 100], // vibrazione personalizzata + data: { + url: payload.data?.url || '/' + } + }; + + self.registration.showNotification(notificationTitle, notificationOptions); +}); \ No newline at end of file diff --git a/src/types/index.ts b/src/types/index.ts index bab24c9..4a4b596 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -40,7 +40,8 @@ export interface user{ email: string; displayName: string; role: "user" | "moderator" | "admin"; - photoURL?: string; // URL dell'immagine del profilo (opzionale) + photoURL?: string; + subscriptions?: string[]; stats?: { pendingVideos: number; approvedVideos: number; diff --git a/vite.config.ts b/vite.config.ts index fd84695..3c8a2cf 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -13,7 +13,18 @@ export default defineConfig({ strategies: 'injectManifest', srcDir: 'src', filename: 'sw.js', - includeAssets: ['favicon.ico', 'apple-touch-icon.png', '/icons/192x192.png', '/offline.html'], + includeAssets: [ + '/icons/192x192.png', + '/icons/512x512.png', + '/pwa-192x192.png', + '/pwa-512x512.png', + '/pwa-maskable-192x192.png', + '/pwa-maskable-512x512.png', + '/screenshot-1.png', + '/screenshot-2.png', + '/offline.html', + '/badge-72x72.png' + ], manifest: { name: 'World Video Guide', // Il nome visualizzato nel banner di installazione short_name: 'WorldVGuide', // Versione abbreviata per la schermata home @@ -23,15 +34,47 @@ export default defineConfig({ start_url: '/', // Indica al browser da quale pagina iniziare icons: [ { - src: '/icons/192x192.png', + src: '/pwa-192x192.png', sizes: '192x192', - type: 'image/png' + type: 'image/png', + purpose: 'any' }, { - src: '/icons/512x512.png', + src: '/pwa-512x512.png', sizes: '512x512', type: 'image/png', purpose: 'any' + }, + { + src: '/pwa-maskable-192x192.png', + sizes: '192x192', + type: 'image/png', + purpose: 'maskable' + }, + { + src: '/pwa-maskable-512x512.png', + sizes: '512x512', + type: 'image/png', + purpose: 'maskable' + }, + { + src: '/badge-72x72.png', + sizes: '72x72', + type: 'image/png', + purpose: 'monochrome' + } + ], + screenshots: [ + { + src: '/screenshot-1.png', + sizes: '512x512', + type: 'image/png', + form_factor: 'wide' + }, + { + src: '/screenshot-2.png', + sizes: '512x512', + type: 'image/png' } ] },