From e377258671c7e0019569d08146fff497148f243e Mon Sep 17 00:00:00 2001 From: 230419 <230419@epvc.pt> Date: Tue, 28 Apr 2026 17:11:43 +0100 Subject: [PATCH] notificacoes, seccoes feito completo e botao lavandaria feito --- src/App.jsx | 274 +++++++++++++++++++++++++++++++----- src/components/ui/Badge.jsx | 2 +- src/components/ui/Card.jsx | 9 +- src/lib/i18n.js | 5 + vite.config.js | 3 +- 5 files changed, 255 insertions(+), 38 deletions(-) diff --git a/src/App.jsx b/src/App.jsx index dd08ca9..5628184 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -74,6 +74,14 @@ export default function App() { const [newSectionName, setNewSectionName] = useState(''); const [newSectionEmoji, setNewSectionEmoji] = useState(''); const [itemSections, setItemSections] = useState([]); + const [lookSections, setLookSections] = useState([]); + const [editingSectionId, setEditingSectionId] = useState(null); + const [editSectionName, setEditSectionName] = useState(''); + const [editSectionEmoji, setEditSectionEmoji] = useState(''); + + const [notifications, setNotifications] = useState([]); + const [showNotificationsModal, setShowNotificationsModal] = useState(false); + const [toastMessage, setToastMessage] = useState(null); const t = (key) => translations[language]?.[key] || translations['PT'][key] || key; @@ -163,6 +171,10 @@ export default function App() { setItemSections(editingItem?.sections || []); }, [editingItem]); + useEffect(() => { + setLookSections(editingLook?.sections || []); + }, [editingLook]); + useEffect(() => { document.documentElement.classList.remove('theme-indigo', 'theme-rose', 'theme-emerald', 'theme-amber', 'theme-slate'); // Ecrã de login/registo: sempre theme-indigo, independentemente das preferências do utilizador @@ -253,7 +265,13 @@ export default function App() { else setUserProfile({}); }, (err) => console.error(err)); - return () => { unsubClothes(); unsubLooks(); unsubSections(); unsubProfile(); }; + // Notificações + const notifCol = collection(db, 'artifacts', appId, 'users', user.uid, 'notifications'); + const unsubNotif = onSnapshot(notifCol, (snap) => { + setNotifications(snap.docs.map(d => ({ id: d.id, ...d.data() })).sort((a, b) => b.createdAt - a.createdAt)); + }, (err) => console.error(err)); + + return () => { unsubClothes(); unsubLooks(); unsubSections(); unsubProfile(); unsubNotif(); }; }, [user]); // Fetch Weather Data @@ -293,9 +311,25 @@ export default function App() { const laundryClothes = useMemo(() => clothes.filter(c => c.status === 'laundry'), [clothes]); const trashClothes = useMemo(() => clothes.filter(c => c.status === 'trash'), [clothes]); const wishlistClothes = useMemo(() => clothes.filter(c => c.status === 'wishlist'), [clothes]); - const availableForLooks = useMemo(() => clothes.filter(c => c.status === 'active' || c.status === 'wishlist'), [clothes]); + const availableForLooks = useMemo(() => clothes.filter(c => { + const isAvailable = c.status !== 'trash'; + const matchesSection = activeSectionFilter === 'all' || (c.sections && c.sections.includes(activeSectionFilter)); + return isAvailable && matchesSection; + }), [clothes, activeSectionFilter]); // CRUD de Secções + const updateSection = async () => { + if (!editSectionName.trim() || !user || !editingSectionId) return; + const docRef = doc(db, 'artifacts', appId, 'users', user.uid, 'sections', editingSectionId); + await updateDoc(docRef, { + name: editSectionName.trim(), + emoji: editSectionEmoji.trim() || '💼' + }); + setEditingSectionId(null); + setEditSectionName(''); + setEditSectionEmoji(''); + }; + const saveSection = async () => { if (!newSectionName.trim() || !user) return; const sectionsCol = collection(db, 'artifacts', appId, 'users', user.uid, 'sections'); @@ -320,6 +354,12 @@ export default function App() { batch.update(itemRef, { sections: item.sections.filter(s => s !== id) }); } }); + looks.forEach(look => { + if (look.sections && look.sections.includes(id)) { + const lookRef = doc(db, 'artifacts', appId, 'users', user.uid, 'looks', look.id); + batch.update(lookRef, { sections: look.sections.filter(s => s !== id) }); + } + }); await batch.commit(); if (activeSectionFilter === id) setActiveSectionFilter('all'); }; @@ -449,6 +489,7 @@ export default function App() { const lookData = { name: fd.get('lookName'), items: selectedForLook, + sections: lookSections, updatedAt: new Date().getTime() }; try { @@ -533,6 +574,19 @@ export default function App() { createdAt: new Date().getTime(), updatedAt: new Date().getTime(), }); + + // Notify the owner + if (sharedLookData.ownerUid && sharedLookData.ownerUid !== user.uid) { + const notificationsCol = collection(db, 'artifacts', appId, 'users', sharedLookData.ownerUid, 'notifications'); + await addDoc(notificationsCol, { + type: 'look_copied', + lookName: sharedLookData.lookName, + copiedByEmail: userProfile?.username || user.email || 'Alguém', + createdAt: new Date().getTime(), + read: false + }); + } + setShowSharedLookModal(false); setSharedLookData(null); setView('outfits'); @@ -545,7 +599,6 @@ export default function App() { }; const sendLookToLaundry = async (look) => { - if (!window.confirm(t('confirmSendLookToLaundry') || 'Enviar todas as peças deste look para a lavandaria?')) return; setLoading(true); try { const batch = writeBatch(db); @@ -554,7 +607,8 @@ export default function App() { batch.update(docRef, { status: 'laundry' }); }); await batch.commit(); - alert(t('lookSentToLaundry') || 'Peças enviadas para a lavandaria!'); + setToastMessage(t('lookSentToLaundry') || 'Peças enviadas para a lavandaria!'); + setTimeout(() => setToastMessage(null), 3000); } catch (e) { console.error(e); } finally { setLoading(false); } }; @@ -810,6 +864,12 @@ export default function App() { + @@ -917,7 +977,7 @@ export default function App() { {/* Barra de Secções */} - {view === 'closet' && ( + {(view === 'closet' || view === 'outfits') && (
))} @@ -1035,6 +1111,40 @@ export default function App() { {selectedForLook.length === 0 &&

{t('selectPieces')}

}
+
+
+ + +
+ {sections.length === 0 ? ( +
+

Ainda não tem secções criadas

+
+ ) : ( +
+ {sections.map(sec => ( + + ))} +
+ )} +
{editingLook && ( @@ -1062,13 +1172,17 @@ export default function App() {
{(() => { - const availableLooks = looks.filter(look => + const filteredBySectionLooks = looks.filter(look => + activeSectionFilter === 'all' || (look.sections && look.sections.includes(activeSectionFilter)) + ); + + const availableLooks = filteredBySectionLooks.filter(look => look.items.every(id => { const item = clothes.find(c => c.id === id); return !item || item.status !== 'laundry'; }) ); - const laundryLooks = looks.filter(look => + const laundryLooks = filteredBySectionLooks.filter(look => look.items.some(id => { const item = clothes.find(c => c.id === id); return item && item.status === 'laundry'; @@ -1225,11 +1339,20 @@ export default function App() { setImageUrlDraft(v)} /> {/* Campo de Secções */} - {sections.length > 0 && ( -
+
+
+ +
+ {sections.length === 0 ? ( +
+

Ainda não tem secções criadas

+
+ ) : (
{sections.map(sec => (
-
- )} + )} +
@@ -1468,6 +1591,60 @@ export default function App() {
+ {/* Toast Message */} + {toastMessage && ( +
+
+ + {toastMessage} +
+
+ )} + + {/* Modal de Notificações */} + {showNotificationsModal && ( +
setShowNotificationsModal(false)}> + e.stopPropagation()}> +
+

Notificações

+ +
+
+ {notifications.length === 0 ? ( +
Sem Notificações
+ ) : notifications.map(notif => ( +
+
+ +
+
+

+ {notif.type === 'look_copied' && ( + <>O utilizador {notif.copiedByEmail} guardou o seu look "{notif.lookName}" no armário dele! + )} +

+

+ {new Date(notif.createdAt).toLocaleDateString()} às {new Date(notif.createdAt).toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'})} +

+
+ {!notif.read && ( + + )} +
+ ))} +
+
+
+ )} + {/* Modal de Gestão de Secções */} {showSectionManager && (
setShowSectionManager(false)}> @@ -1510,19 +1687,50 @@ export default function App() {
{t('noSections')}
) : sections.map(sec => (
- {sec.emoji} -
-

{sec.name}

-

- {clothes.filter(c => c.sections && c.sections.includes(sec.id)).length} peça(s) -

-
- + {editingSectionId === sec.id ? ( + <> + setEditSectionEmoji(e.target.value)} + maxLength={2} + className={`w-12 p-2 rounded-xl border-none outline-none font-bold text-center text-lg ${darkMode ? 'bg-gray-700 text-white' : 'bg-white'} shadow-sm`} + /> + setEditSectionName(e.target.value)} + onKeyDown={e => e.key === 'Enter' && updateSection()} + className={`flex-1 p-2 rounded-xl border-none outline-none font-bold text-sm ${darkMode ? 'bg-gray-700 text-white' : 'bg-white'} shadow-sm`} + /> + + + + ) : ( + <> + {sec.emoji} +
+

{sec.name}

+

+ {clothes.filter(c => c.sections && c.sections.includes(sec.id)).length} {t('pieces')} • {looks.filter(l => l.sections && l.sections.includes(sec.id)).length} look(s) +

+
+ + + + )}
))}
diff --git a/src/components/ui/Badge.jsx b/src/components/ui/Badge.jsx index 03927e7..c34b92f 100644 --- a/src/components/ui/Badge.jsx +++ b/src/components/ui/Badge.jsx @@ -8,7 +8,7 @@ export const Badge = ({ children, variant = "default" }) => { warning: "bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-300" }; return ( - + {children} ); diff --git a/src/components/ui/Card.jsx b/src/components/ui/Card.jsx index 286f25c..81229ec 100644 --- a/src/components/ui/Card.jsx +++ b/src/components/ui/Card.jsx @@ -1,10 +1,13 @@ import React from 'react'; -export const Card = ({ children, className = "", darkMode }) => ( -
( +
+ } ${className}`} + {...props} + > {children}
); diff --git a/src/lib/i18n.js b/src/lib/i18n.js index 4ea89ea..47923dd 100644 --- a/src/lib/i18n.js +++ b/src/lib/i18n.js @@ -38,6 +38,7 @@ export const translations = { makeDirty: "Sujar", moveToTrash: "Mover para Lixo", laundryBasket: "Cesto da Roupa", + lookSentToLaundry: "Peças enviadas para a lavandaria!", laundryMsg: "Aqui encontras as peças que marcaste como sujas. Lava-as para que voltem ao armário principal.", washing: "A lavar", emptyBasket: "Cesto Vazio", @@ -171,6 +172,7 @@ export const translations = { makeDirty: "Make Dirty", moveToTrash: "Move to Trash", laundryBasket: "Laundry Basket", + lookSentToLaundry: "Pieces sent to the laundry!", laundryMsg: "Here you find the pieces you marked as dirty. Wash them to return them to the main closet.", washing: "Washing", emptyBasket: "Empty Basket", @@ -304,6 +306,7 @@ export const translations = { makeDirty: "Ensuciar", moveToTrash: "Mover a la Papelera", laundryBasket: "Cesto de Ropa", + lookSentToLaundry: "¡Piezas enviadas a la lavandería!", laundryMsg: "Aquí encuentras las piezas que marcaste como sucias. Lávalas para que vuelvan al armario principal.", washing: "Lavando", emptyBasket: "Cesto Vacío", @@ -437,6 +440,7 @@ export const translations = { makeDirty: "Salir", moveToTrash: "Mettre à la corbeille", laundryBasket: "Panier à linge", + lookSentToLaundry: "Pièces envoyées à la blanchisserie !", laundryMsg: "Ici vous trouvez les pièces que vous avez marquées comme sales. Lavez-les pour les remettre dans le placard principal.", washing: "En lavage", emptyBasket: "Panier Vide", @@ -570,6 +574,7 @@ export const translations = { makeDirty: "Schmutzig machen", moveToTrash: "In den Papierkorb verschieben", laundryBasket: "Wäschekorb", + lookSentToLaundry: "Stücke in die Wäsche geschickt!", laundryMsg: "Hier findest du die Stücke, die du als schmutzig markiert hast. Wasche sie, um sie in den Hauptschrank zurückzulegen.", washing: "Waschen", emptyBasket: "Leerer Korb", diff --git a/vite.config.js b/vite.config.js index 36508cf..db56983 100644 --- a/vite.config.js +++ b/vite.config.js @@ -6,7 +6,8 @@ export default defineConfig({ plugins: [react()], server: { host: true, // Permite acesso externo - port: 5173, // Opcional, para garantir a porta + port: 5173, // Garante que apenas esta porta será usada + strictPort: true, // Se a porta estiver em uso, falha em vez de abrir outra allowedHosts: ['mycloset.epvc.pt', 'localhost'], } })