Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Binary file added public/badge-72x72.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified public/icons/192x192.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified public/icons/512x512.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/pwa-192x192.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/pwa-512x512.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/pwa-maskable-192x192.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/pwa-maskable-512x512.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/screenshot-1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/screenshot-2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
121 changes: 119 additions & 2 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -15,25 +19,128 @@ export default function App() {
const [videos, setVideos] = useState<video[]>([]);
const [loading, setLoading] = useState(true);
const [isOffline, setIsOffline] = useState(() => !navigator.onLine);
const [notificationAlert, setNotificationAlert] = useState<{ title: string; message: string } | null>(null);

const prevSubscriptionsRef = useRef<string[] | null>(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 = () => {};

const unsubscribeAuth = onAuthStateChanged(auth, async (firebaseUser) => {
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 ?? "",
displayName: userData.displayName ?? "",
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)
Expand All @@ -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);
Expand Down Expand Up @@ -71,6 +179,7 @@ export default function App() {
};
}, []);

// Gestione stato online/offline
useEffect(() => {
const handleOnline = () => setIsOffline(false);
const handleOffline = () => setIsOffline(true);
Expand Down Expand Up @@ -123,6 +232,14 @@ export default function App() {
return (
<>
{offlineBanner}
{notificationAlert && (
<Alert
type="info"
message={notificationAlert.message}
duration={5000}
onClose={() => setNotificationAlert(null)}
/>
)}
<Router>
<Routes>
<Route path="/" element={<Home user={user} videos={videos} />} />
Expand Down
1 change: 1 addition & 0 deletions src/apiConfig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const EXPRESS_API_URL = "https://world-video-guide-backend.onrender.com/api";
10 changes: 5 additions & 5 deletions src/components/common/Alert.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 (
<AnimatePresence>
Expand Down
41 changes: 40 additions & 1 deletion src/components/common/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";



Expand All @@ -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 (
<>

Expand Down Expand Up @@ -67,7 +106,7 @@ export default function Header({ user, page }: { user: user | null; page: string

<div className="flex items-center gap-3">
{user ? (
<UserMenu user={user} onLogout={() => signOut(auth)} align="right" />
<UserMenu user={user} onLogout={() => logOut()} align="right" />
) : (
<button
onClick={() => {
Expand Down
Loading
Loading