diff --git a/src/App.tsx b/src/App.tsx index fcc557d..ea5fc4f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -29,7 +29,6 @@ export default function App() { 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."; @@ -69,7 +68,6 @@ export default function App() { }) }); - console.log("Iscrizione ai topic sincronizzata!"); } } catch (error) { console.error("Errore durante la registrazione per le notifiche:", error); diff --git a/src/components/admin/CategoryEditor.tsx b/src/components/admin/CategoryEditor.tsx new file mode 100644 index 0000000..b59365e --- /dev/null +++ b/src/components/admin/CategoryEditor.tsx @@ -0,0 +1,195 @@ +import { useState } from "react"; +import { FiLink, FiX } from "react-icons/fi"; + +interface CategoryEditorProps { + editedCategories: string[]; + setEditedCategories: React.Dispatch>; + toggleCategory: (category: string) => void; + officialCategories: { value: string; label: string; aliases?: string[] }[]; + onAddAlias: (categoryId: string, newAlias: string) => Promise; + onClose: () => void; +} + +export default function CategoryEditor({ + editedCategories, + setEditedCategories, + toggleCategory, + officialCategories, + onAddAlias, + onClose, +}: CategoryEditorProps) { + const [customCategory, setCustomCategory] = useState(""); + + // Stati per la gestione degli Alias + const [aliasTarget, setAliasTarget] = useState<{ originalName: string; categoryId: string } | null>(null); + const [isSavingAlias, setIsSavingAlias] = useState(false); + + const addCustomCategory = () => { + const nextCategory = customCategory.trim().toLowerCase(); + if (!nextCategory) return; + + setEditedCategories((current) => (current.includes(nextCategory) ? current : [...current, nextCategory])); + setCustomCategory(""); + }; + + const handleMergeToAlias = async () => { + if (!aliasTarget) return; + setIsSavingAlias(true); + + try { + await onAddAlias(aliasTarget.categoryId, aliasTarget.originalName); + + const officialCat = officialCategories.find((c) => c.value === aliasTarget.categoryId); + + if (officialCat) { + setEditedCategories((current) => { + const filtered = current.filter((c) => c !== aliasTarget.originalName); + if (!filtered.includes(officialCat.label)) { + filtered.push(officialCat.label); + } + return filtered; + }); + } + + setAliasTarget(null); + } catch (error) { + console.error("Errore salvataggio alias", error); + } finally { + setIsSavingAlias(false); + } + }; + + return ( +
{ + e.preventDefault(); + e.stopPropagation(); + }} + > + + {/* SEZIONE 1: Categorie Attuali */} +
+

Categorie del Video

+
+ {editedCategories.map((catName) => { + const isOfficial = officialCategories.some((c) => c.label === catName || c.aliases?.includes(catName)); + + return ( +
+ + + {!isOfficial && ( +
+ + + {aliasTarget?.originalName === catName && ( +
+

Unisci "{catName}" a:

+ +
+ + +
+
+ )} +
+ )} +
+ ); + })} +
+
+ + {/* SEZIONE 2: Aggiungi Categoria Ufficiale */} +
+

Aggiungi categorie ufficiali

+
+ {officialCategories + .filter((c) => !editedCategories.includes(c.label)) + .map((category) => ( + + ))} +
+
+ + {/* SEZIONE 3: Aggiungi personalizzata */} +
+ setCustomCategory(e.target.value)} + placeholder="Nuova categoria personalizzata" + className="flex-1 bg-neutral-950 border border-white/10 rounded-xl px-3 py-2 text-sm text-white placeholder:text-neutral-600 focus:outline-none focus:ring-2 focus:ring-cyan-500/40 focus:border-cyan-500/40" + /> + +
+
+ ); +} diff --git a/src/components/admin/VideoCard.tsx b/src/components/admin/VideoCard.tsx index 7451ee7..35c69bd 100644 --- a/src/components/admin/VideoCard.tsx +++ b/src/components/admin/VideoCard.tsx @@ -1,73 +1,112 @@ -import { useState } from "react"; -import { FaPlay } from "react-icons/fa"; +import { useEffect, useState } from "react"; +import { FaEdit, FaPlay } from "react-icons/fa"; import type { video } from "../../types/index"; import RejectionModal from "./RejectionModal"; +import CategoryEditor from "./CategoryEditor"; interface VideoCardProps { video: video; updating: string | null; - onUpdateStatus: (videoId: string, newStatus: "approved" | "rejected", reason?: string) => void; + officialCategories: { value: string; label: string; aliases?: string[] }[]; + onUpdateStatus: ( + videoId: string, + newStatus: "approved" | "rejected", + reason?: string, + updatedCategories?: string[], + brandNewCategories?: string[] + ) => void; + onAddAlias: (categoryId: string, newAlias: string) => Promise; } -export default function VideoCard({ video, updating, onUpdateStatus }: VideoCardProps) { +export default function VideoCard({ video, updating, officialCategories, onUpdateStatus, onAddAlias }: VideoCardProps) { const [isRejectModalOpen, setIsRejectModalOpen] = useState(false); + const [editedCategories, setEditedCategories] = useState(video.categories || []); + const [isEditingCategories, setIsEditingCategories] = useState(false); + + useEffect(() => { + setEditedCategories(video.categories || []); + }, [video.categories, video.id]); const handleReject = (reason: string) => { onUpdateStatus(video.id, "rejected", reason); setIsRejectModalOpen(false); }; + const handleApprove = () => { + // 1. Filtriamo per trovare SOLO le categorie veramente nuove + const brandNewCategories = editedCategories.filter((cat) => { + const alreadyExists = officialCategories.some((officialCat) => { + return officialCat.label === cat || officialCat.aliases?.includes(cat); + }); + + // Se non esiste (false), la teniamo nell'array restituendo true + return !alreadyExists; + }); + + onUpdateStatus(video.id, "approved", undefined, editedCategories, brandNewCategories); + }; + + const toggleCategory = (category: string) => { + setEditedCategories((current) => + current.includes(category) + ? current.filter((item) => item !== category) + : [...current, category] + ); + }; + return ( -
- {/* Video Info (Linkabile) */} - - {video.thumbnail && ( -
+
+ {/* Video Info (image clickable only) */} +
+ {video.thumbnail && ( + Thumbnail -
-
- -
+
+
+ +
-
- )} +
+ )}
-

+

{video.title || "Video senza titolo"}

{video.url}

-
+

Paese: {video.country || video.countryCode}

{video.flag && ( - Flag + Flag )}
- +

- Suggerito da: {video.suggesterName || "Utente sconosciuto"} + Suggerito da: {video.suggesterName || "Utente sconosciuto"}

{video.suggesterEmail && ( - @@ -77,35 +116,62 @@ export default function VideoCard({ video, updating, onUpdateStatus }: VideoCard
- {/* Categorie del video */} - {video.categories && video.categories.length > 0 && ( -
- {video.categories.map((cat, index) => ( - - {cat} - - ))} +
+
+ {editedCategories.length > 0 ? ( + editedCategories.map((cat, index) => ( + + {cat} + + )) + ) : ( + Nessuna categoria selezionata + )} + +
- )} + + {isEditingCategories && ( + setIsEditingCategories(false)} + /> + )} +
- +
{/* Pulsanti Azione */}
@@ -121,4 +187,4 @@ export default function VideoCard({ video, updating, onUpdateStatus }: VideoCard )}
); -} +} \ No newline at end of file diff --git a/src/components/admin/VideoList.tsx b/src/components/admin/VideoList.tsx index 7570ccd..01db3ee 100644 --- a/src/components/admin/VideoList.tsx +++ b/src/components/admin/VideoList.tsx @@ -8,10 +8,12 @@ interface VideoListProps { videos: video[]; loading: boolean; updating: string | null; - onUpdateStatus: (videoId: string, newStatus: "approved" | "rejected", reason?: string) => void; + onUpdateStatus: (videoId: string, newStatus: "approved" | "rejected", reason?: string, updatedCategories?: string[], brandNewCategories?: string[]) => void; + officialCategories: { value: string; label: string; aliases?: string[] }[]; + onAddAlias: (categoryId: string, newAlias: string) => Promise; } -export default function VideoList({ videos, loading, updating, onUpdateStatus }: VideoListProps) { +export default function VideoList({ videos, loading, updating, onUpdateStatus, officialCategories, onAddAlias }: VideoListProps) { const [searchTerm, setSearchTerm] = useState(''); const filteredVideos = videos.filter(v => @@ -83,7 +85,9 @@ export default function VideoList({ videos, loading, updating, onUpdateStatus }: ))} diff --git a/src/components/common/Header.tsx b/src/components/common/Header.tsx index f1d8eb3..1a78cbb 100644 --- a/src/components/common/Header.tsx +++ b/src/components/common/Header.tsx @@ -29,7 +29,6 @@ export default function Header({ user, page }: { user: user | null; page: string }); if (currentToken) { - console.log("Tentativo di disiscrizione al logout in corso..."); await fetch(`${EXPRESS_API_URL}/unsubscribeAll`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, diff --git a/src/components/home/CountryOverlay.tsx b/src/components/home/CountryOverlay.tsx index dc1b5af..4a2f3d7 100644 --- a/src/components/home/CountryOverlay.tsx +++ b/src/components/home/CountryOverlay.tsx @@ -128,8 +128,6 @@ export default function CountryOverlay({ country, videos : videosWithoutMetadata } else { throw new Error("Errore dal server"); } - console.log("utente iscritto: ", user); - console.log("stato iscritto: ", country.name, user.subscriptions?.includes(country.name)); } catch (error) { console.error("Errore iscrizione:", error); setAlert({ type: "error", message: "Si è verificato un errore durante l'attivazione delle notifiche." }); diff --git a/src/components/home/SuggestVideoOverlay.tsx b/src/components/home/SuggestVideoOverlay.tsx index e8c2c2e..4121a44 100644 --- a/src/components/home/SuggestVideoOverlay.tsx +++ b/src/components/home/SuggestVideoOverlay.tsx @@ -46,7 +46,7 @@ export default function SuggestVideoModal({ onClose, onSubmit, countryName }: S }; const addCustomCategory = () => { - const nextCategory = customCategory.trim(); + const nextCategory = customCategory.trim().toLocaleLowerCase(); if (!nextCategory) return; diff --git a/src/components/home/WorldMap.tsx b/src/components/home/WorldMap.tsx index 45f0123..6d2e975 100644 --- a/src/components/home/WorldMap.tsx +++ b/src/components/home/WorldMap.tsx @@ -74,7 +74,6 @@ export default function WorldMap({videos, SelectedCountry, SelectCountry, OverCo name: geo.properties.name, capitalName: countriesByCode.get(String(geo.id))?.capital?.[0] || countriesData.find((c: any) => String(c.name.common) === String(geo.properties.name))?.capital?.[0] || "N/A", }; - console.log("Paese cliccato:", country); SelectCountry(country); }; diff --git a/src/firebase.ts b/src/firebase.ts index c60462b..82eb34d 100644 --- a/src/firebase.ts +++ b/src/firebase.ts @@ -32,11 +32,7 @@ export const requestForToken = async () => { 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); diff --git a/src/pages/Admin.tsx b/src/pages/Admin.tsx index f4f45ed..cd966a3 100644 --- a/src/pages/Admin.tsx +++ b/src/pages/Admin.tsx @@ -1,7 +1,7 @@ import { Link } from "react-router-dom"; import { useEffect, useState } from "react"; import { db } from "../firebase"; -import { collection, query, where, getDocs, updateDoc, doc, increment } from "firebase/firestore"; +import { collection, query, where, getDocs, updateDoc, doc, increment, arrayUnion, setDoc } from "firebase/firestore"; import type { user, video } from "../types/index" import Header from "../components/common/Header"; import VideoList from "../components/admin/VideoList"; @@ -11,6 +11,27 @@ export default function Admin({ user }: { user: user | null }) { const [pendingVideos, setPendingVideos] = useState([]); const [loading, setLoading] = useState(true); const [updating, setUpdating] = useState(null); + const [officialCategories, setOfficialCategories] = useState<{ value: string; label: string; aliases?: string[] }[]>([]); + + // Carica le categorie ufficiali + useEffect(() => { + const fetchOfficialCategories = async () => { + try { + const categoriesColRef = collection(db, "categories"); + const querySnapshot = await getDocs(categoriesColRef); + const categoriesArray = querySnapshot.docs.map(doc => ({ + value: doc.id, + label: doc.data().category, + aliases: doc.data().aliases || [], + })); + setOfficialCategories(categoriesArray); + } catch (error) { + console.error("Errore nel caricamento delle categorie ufficiali:", error); + } + }; + + fetchOfficialCategories(); + }, []); // Carica i video in stato pending useEffect(() => { @@ -45,10 +66,8 @@ export default function Admin({ user }: { user: user | null }) { 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 { @@ -79,7 +98,7 @@ export default function Admin({ user }: { user: user | null }) { }, [user]); // Aggiorna lo stato del video - const updateVideoStatus = async (videoId: string, newStatus: "approved" | "rejected", reason?: string) => { + const updateVideoStatus = async (videoId: string, newStatus: "approved" | "rejected", reason?: string, updatedCategories?: string[], brandNewCategories?: string[]) => { try { setUpdating(videoId); @@ -94,6 +113,9 @@ export default function Admin({ user }: { user: user | null }) { if (newStatus === "rejected" && reason) { updateData.rejectionReason = reason; } + if (newStatus === "approved") { + updateData.categories = updatedCategories || []; + } await updateDoc(videoRef, updateData); // Usiamo submittedBy che abbiamo salvato in fase di creazione @@ -114,6 +136,47 @@ export default function Admin({ user }: { user: user | null }) { } finally { setUpdating(null); } + + // Se ci sono nuove categorie da aggiungere, le aggiungiamo al database + if (brandNewCategories && brandNewCategories.length > 0) { + try { + await Promise.all(brandNewCategories.map(async (cat) => { + const newCategoryRef = doc(collection(db, "categories")); + + const capitalizedCat = cat.charAt(0).toUpperCase() + cat.slice(1); + await setDoc(newCategoryRef, { + category: capitalizedCat, + aliases: [] + }); + + // Aggiorniamo anche lo stato locale di Admin per non dover ricaricare la pagina + setOfficialCategories(prev => [...prev, { value: newCategoryRef.id, label: capitalizedCat, aliases: [] }]); + })); + } catch (error) { + console.error("Errore nell'aggiungere le nuove categorie:", error); + } + } + }; + + // Aggiunge un alias a una categoria ufficiale + const handleAddAlias = async (categoryId: string, newAlias: string) => { + try { + const categoryDocRef = doc(db, "categories", categoryId); + await updateDoc(categoryDocRef, { + aliases: arrayUnion(newAlias.toLowerCase()) + }); + // Aggiorniamo lo stato locale per riflettere il cambiamento + setOfficialCategories(current => + current.map(cat => + cat.value === categoryId + ? { ...cat, aliases: [...(cat.aliases || []), newAlias.toLowerCase()] } + : cat + ) + ); + } catch (error) { + console.error("Errore nell'aggiungere l'alias:", error); + throw error; // Rilanciamo l'errore per gestirlo nel componente figlio se necessario + } }; @@ -144,7 +207,9 @@ export default function Admin({ user }: { user: user | null }) { videos={pendingVideos} loading={loading} updating={updating} - onUpdateStatus={updateVideoStatus} + onUpdateStatus={updateVideoStatus} + officialCategories={officialCategories} + onAddAlias={handleAddAlias} />
{user.role === "admin" && ( diff --git a/src/sw.js b/src/sw.js index 140d21c..21d57f9 100644 --- a/src/sw.js +++ b/src/sw.js @@ -94,14 +94,12 @@ 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`)