import React, { useState, useEffect, useMemo, useRef } from 'react'; import { Plus, Search, LayoutDashboard, Shirt, LogOut, Trash2, Heart, Loader2, AlertCircle, UserCircle, Settings, Moon, Sun, ShieldAlert, Edit2, Image as ImageIcon, Check, RotateCcw, Trash, PanelLeftClose, PanelLeftOpen, Sparkles, CloudSun, ArrowRight, Droplets, CheckCircle2, PieChart, History, X, Download, Bell, Globe, Filter, ShoppingBag, Share2, FolderOpen, Tag, Link, Calendar, ChevronLeft, ChevronRight } from 'lucide-react'; import { signInWithEmailAndPassword, createUserWithEmailAndPassword, onAuthStateChanged, signOut, signInWithCustomToken, sendPasswordResetEmail } from 'firebase/auth'; import { collection, doc, onSnapshot, addDoc, updateDoc, deleteDoc, writeBatch, setDoc, getDoc, query, where } from 'firebase/firestore'; import { auth, db, appId } from './lib/firebase'; import { Card } from './components/ui/Card'; import { Badge } from './components/ui/Badge'; import { Input } from './components/ui/Input'; import { translations } from './lib/i18n'; export default function App() { const [view, setView] = useState('auth'); const [user, setUser] = useState(null); const [loading, setLoading] = useState(true); const [clothes, setClothes] = useState([]); const [looks, setLooks] = useState([]); const [editingItem, setEditingItem] = useState(null); const [darkMode, setDarkMode] = useState(false); const [searchTerm, setSearchTerm] = useState(''); const [imageUrlDraft, setImageUrlDraft] = useState(''); const [itemColors, setItemColors] = useState([]); const [sidebarOpen, setSidebarOpen] = useState(true); const [authMode, setAuthMode] = useState('login'); const [authError, setAuthError] = useState(''); const [showForgotPasswordModal, setShowForgotPasswordModal] = useState(false); const [forgotPasswordEmail, setForgotPasswordEmail] = useState(''); const [categoryFilter, setCategoryFilter] = useState('Todos'); const [colorFilter, setColorFilter] = useState(''); const [ageFilter, setAgeFilter] = useState('any'); const [showClosetFilters, setShowClosetFilters] = useState(false); // Estado para criação de Looks const [selectedForLook, setSelectedForLook] = useState([]); const [editingLook, setEditingLook] = useState(null); // Perfil do Utilizador const [userProfile, setUserProfile] = useState({}); const [savingProfile, setSavingProfile] = useState(false); // Estado para Definições const [notificationsEnabled, setNotificationsEnabled] = useState(true); const [weatherAlerts, setWeatherAlerts] = useState(true); const [language, setLanguage] = useState('PT'); const [showLangModal, setShowLangModal] = useState(false); const [theme, setTheme] = useState('theme-indigo'); const [cardSize, setCardSize] = useState('large'); const [weatherData, setWeatherData] = useState(null); // Estado para Partilha de Looks const sharedLookRef = useRef(''); const [sharedLookData, setSharedLookData] = useState(null); const [showSharedLookModal, setShowSharedLookModal] = useState(false); const [sharedLookCopying, setSharedLookCopying] = useState(false); const [copiedLookId, setCopiedLookId] = useState(null); // Estado para Secções const [sections, setSections] = useState([]); const [activeSectionFilter, setActiveSectionFilter] = useState('all'); const [showSectionManager, setShowSectionManager] = useState(false); 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); // Estado do Planeador const [plannerMode, setPlannerMode] = useState('month'); const [plannerCurrentDate, setPlannerCurrentDate] = useState(new Date()); const [outfitPlans, setOutfitPlans] = useState([]); const [showPlannerPicker, setShowPlannerPicker] = useState(false); const [plannerPickerDate, setPlannerPickerDate] = useState(null); const t = (key) => translations[language]?.[key] || translations['PT'][key] || key; // Mapeamento de nomes de cor (PT) para valores CSS const COLOR_MAP = { 'Vermelho': '#ef4444', 'Azul': '#3b82f6', 'Amarelo': '#eab308', 'Verde': '#22c55e', 'Laranja': '#f97316', 'Roxo': '#a855f7', 'Branco': '#f8fafc', 'Preto': '#0f172a', 'Cinzento': '#6b7280', 'Bege': '#d4b896', }; const getColorStyle = (colorStr) => { if (!colorStr) return { backgroundColor: '#e5e7eb' }; const parts = colorStr.split(',').map(c => c.trim()).filter(Boolean); const cssColors = parts.map(p => COLOR_MAP[p] || p.toLowerCase()); if (cssColors.length === 1) return { backgroundColor: cssColors[0] }; return { background: `linear-gradient(135deg, ${cssColors.join(', ')})` }; }; const saveUserSetting = async (key, value) => { if (!user) return; try { const profileDoc = doc(db, 'artifacts', appId, 'users', user.uid, 'profile', 'data'); await setDoc(profileDoc, { settings: { [key]: value } }, { merge: true }); } catch (err) { console.error('Error saving setting:', err); } }; const handleDarkModeToggle = (newVal) => { setDarkMode(newVal); saveUserSetting('darkMode', newVal); }; const handleThemeChange = (newVal) => { setTheme(newVal); saveUserSetting('theme', newVal); }; const handleLanguageChange = (newVal) => { setLanguage(newVal); saveUserSetting('language', newVal); setShowLangModal(false); }; const handleNotificationsToggle = (newVal) => { setNotificationsEnabled(newVal); saveUserSetting('notificationsEnabled', newVal); }; const handleWeatherAlertsToggle = (newVal) => { setWeatherAlerts(newVal); saveUserSetting('weatherAlerts', newVal); }; const handleCardSizeChange = (newVal) => { setCardSize(newVal); saveUserSetting('cardSize', newVal); }; // Buscar o look partilhado pelo link const fetchSharedLook = async (lookId) => { if (!lookId) return; try { const lookDoc = doc(db, 'artifacts', appId, 'sharedLooks', lookId); const snap = await getDoc(lookDoc); if (snap.exists()) { setSharedLookData({ id: snap.id, ...snap.data() }); setShowSharedLookModal(true); // Limpar o parâmetro do URL sem recarregar a página window.history.replaceState({}, '', window.location.pathname); } } catch (err) { console.error('Erro ao buscar look partilhado:', err); } }; const handlePasteSharedLink = () => { const link = window.prompt(t('pasteSharedLookLink') || 'Cole o link do look partilhado:'); if (link) { try { const url = new URL(link); const sharedId = url.searchParams.get('shared'); if (sharedId) { fetchSharedLook(sharedId); } else { alert(t('invalidSharedLink') || 'Link inválido. Certifique-se de copiar o link completo.'); } } catch (e) { alert(t('invalidSharedLink') || 'Link inválido. Certifique-se de copiar o link completo.'); } } }; useEffect(() => { if (editingItem && editingItem.color) { setItemColors(editingItem.color.split(',').map(c => c.trim()).filter(Boolean)); } else { setItemColors([]); } 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 const activeTheme = view === 'auth' ? 'theme-indigo' : theme; document.documentElement.classList.add(activeTheme); // Guardar tema por utilizador (não partilhado entre contas) if (view !== 'auth' && user?.uid) { localStorage.setItem(`app-theme-${user.uid}`, theme); } }, [theme, view, user?.uid]); // 1. Inicializar Autenticação useEffect(() => { const initAuth = async () => { const token = import.meta.env.VITE_INITIAL_AUTH_TOKEN; if (token) { try { await signInWithCustomToken(auth, token); } catch (e) { } } }; initAuth(); const unsubscribe = onAuthStateChanged(auth, (currentUser) => { if (!currentUser) { // Reset de todo o estado ao fazer logout para não contaminar a próxima conta setUser(null); setClothes([]); setLooks([]); setSections([]); setUserProfile({}); setDarkMode(false); setTheme('theme-indigo'); setLanguage('PT'); setNotificationsEnabled(true); setWeatherAlerts(true); setWeatherData(null); setView('auth'); } else { // Carregar tema guardado para este utilizador específico const savedTheme = localStorage.getItem(`app-theme-${currentUser.uid}`) || 'theme-indigo'; setTheme(savedTheme); setUser(currentUser); setView('dashboard'); // Verificar se há um look partilhado no URL const sharedId = sharedLookRef.current || new URLSearchParams(window.location.search).get('shared'); sharedLookRef.current = ''; if (sharedId) fetchSharedLook(sharedId); } setLoading(false); }); return () => unsubscribe(); }, []); // 2. Dados em Tempo Real (Roupas e Looks) useEffect(() => { if (!user) return; // Roupas const clothesCol = collection(db, 'artifacts', appId, 'users', user.uid, 'clothes'); const unsubClothes = onSnapshot(clothesCol, (snap) => { setClothes(snap.docs.map(d => ({ id: d.id, ...d.data() }))); }, (err) => console.error(err)); // Looks const looksCol = collection(db, 'artifacts', appId, 'users', user.uid, 'looks'); const unsubLooks = onSnapshot(looksCol, (snap) => { setLooks(snap.docs.map(d => ({ id: d.id, ...d.data() }))); }, (err) => console.error(err)); // Secções const sectionsCol = collection(db, 'artifacts', appId, 'users', user.uid, 'sections'); const unsubSections = onSnapshot(sectionsCol, (snap) => { setSections(snap.docs.map(d => ({ id: d.id, ...d.data() })).sort((a, b) => a.createdAt - b.createdAt)); }, (err) => console.error(err)); // Planeador de Outfits const plansCol = collection(db, 'artifacts', appId, 'users', user.uid, 'outfitPlans'); const unsubPlans = onSnapshot(plansCol, (snap) => { setOutfitPlans(snap.docs.map(d => ({ id: d.id, ...d.data() }))); }, (err) => console.error(err)); // Profile const profileDoc = doc(db, 'artifacts', appId, 'users', user.uid, 'profile', 'data'); const unsubProfile = onSnapshot(profileDoc, (snap) => { if (snap.exists()) { const data = snap.data(); setUserProfile(data); if (data.settings) { if (data.settings.language !== undefined) setLanguage(data.settings.language); if (data.settings.darkMode !== undefined) setDarkMode(data.settings.darkMode); if (data.settings.theme !== undefined) setTheme(data.settings.theme); if (data.settings.notificationsEnabled !== undefined) setNotificationsEnabled(data.settings.notificationsEnabled); if (data.settings.weatherAlerts !== undefined) setWeatherAlerts(data.settings.weatherAlerts); if (data.settings.cardSize !== undefined) setCardSize(data.settings.cardSize); } } else setUserProfile({}); }, (err) => console.error(err)); // Notificações (coleção pública partilhada, filtrada por recipientUid) const notifCol = collection(db, 'artifacts', appId, 'inboxNotifications'); const notifQuery = query(notifCol, where('recipientUid', '==', user.uid)); const unsubNotif = onSnapshot(notifQuery, (snap) => { setNotifications(snap.docs.map(d => ({ id: d.id, ...d.data() })).sort((a, b) => b.createdAt - a.createdAt)); }, (err) => console.error('Notif listener error:', err)); return () => { unsubClothes(); unsubLooks(); unsubSections(); unsubProfile(); unsubNotif(); unsubPlans(); }; }, [user]); // Fetch Weather Data useEffect(() => { if (view !== 'dashboard') return; const fetchWeather = async () => { try { const locName = userProfile?.location || 'Lisboa, Portugal'; const geoRes = await fetch(`https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(locName)}&count=1&language=pt&format=json`); const geoData = await geoRes.json(); if (geoData.results && geoData.results.length > 0) { const { latitude, longitude, name, country } = geoData.results[0]; const weatherRes = await fetch(`https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}¤t_weather=true&daily=temperature_2m_max,temperature_2m_min&timezone=auto`); const weatherRaw = await weatherRes.json(); if (weatherRaw.current_weather && weatherRaw.daily) { setWeatherData({ name: `${name}, ${country || ''}`.replace(/,\s*$/, ''), currentTemp: Math.round(weatherRaw.current_weather.temperature), minTemp: Math.round(weatherRaw.daily.temperature_2m_min[0]), maxTemp: Math.round(weatherRaw.daily.temperature_2m_max[0]), avgTemp: Math.round((weatherRaw.daily.temperature_2m_min[0] + weatherRaw.daily.temperature_2m_max[0]) / 2) }); } } } catch (err) { console.error("Error fetching weather", err); } }; fetchWeather(); }, [userProfile?.location, view]); // --- Lógicas de Negócio --- const activeClothes = useMemo(() => clothes.filter(c => c.status === 'active'), [clothes]); 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 => { 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'); await addDoc(sectionsCol, { name: newSectionName.trim(), emoji: newSectionEmoji.trim() || '💼', createdAt: new Date().getTime() }); setNewSectionName(''); setNewSectionEmoji(''); }; const deleteSection = async (id) => { if (!window.confirm(t('confirmDeleteSection'))) return; const docRef = doc(db, 'artifacts', appId, 'users', user.uid, 'sections', id); await deleteDoc(docRef); // Remover a secção de todas as peças que a tinham const batch = writeBatch(db); clothes.forEach(item => { if (item.sections && item.sections.includes(id)) { const itemRef = doc(db, 'artifacts', appId, 'users', user.uid, 'clothes', item.id); 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'); }; const assignOutfitToDay = async (dateStr, lookId) => { if (!user) return; const planRef = doc(db, 'artifacts', appId, 'users', user.uid, 'outfitPlans', dateStr); if (lookId) { await setDoc(planRef, { date: dateStr, lookId, updatedAt: new Date().getTime() }); } else { await deleteDoc(planRef); } }; const baseClothes = view === 'wishlist' ? wishlistClothes : activeClothes; const availableColors = useMemo(() => { const colors = new Set(baseClothes.map(c => c.color).filter(Boolean)); return Array.from(colors); }, [baseClothes]); const colorStats = useMemo(() => { if (!activeClothes.length) return []; const colorCounts = {}; let totalWithColor = 0; activeClothes.forEach(c => { if (c.color) { colorCounts[c.color] = (colorCounts[c.color] || 0) + 1; totalWithColor++; } }); if (totalWithColor === 0) return []; return Object.entries(colorCounts) .sort((a, b) => b[1] - a[1]) .slice(0, 3) .map(([color, count]) => ({ color, count, percentage: Math.round((count / totalWithColor) * 100) })); }, [activeClothes]); const filteredClothes = useMemo(() => { return baseClothes.filter(c => { const matchesSearch = (c.name || "").toLowerCase().includes(searchTerm.toLowerCase()) || (c.color || "").toLowerCase().includes(searchTerm.toLowerCase()); const matchesCategory = categoryFilter === 'Todos' || categoryFilter === t('all') || c.category === categoryFilter; const matchesColor = !colorFilter || (c.color && c.color.includes(colorFilter)); const matchesSection = activeSectionFilter === 'all' || (c.sections && c.sections.includes(activeSectionFilter)); let matchesAge = true; if (ageFilter !== 'any') { const ageInMs = new Date().getTime() - (c.createdAt || new Date().getTime()); const days = ageInMs / (1000 * 60 * 60 * 24); if (ageFilter === 'month') matchesAge = days <= 30; else if (ageFilter === '6months') matchesAge = days <= 180; else if (ageFilter === '1year') matchesAge = days <= 365; else if (ageFilter === 'older') matchesAge = days > 365; } return matchesSearch && matchesCategory && matchesColor && matchesAge && matchesSection; }); }, [baseClothes, searchTerm, categoryFilter, colorFilter, ageFilter, t, activeSectionFilter]); // Ações de Itens const handleItemAction = async (action, item) => { if (!user) return; const docRef = doc(db, 'artifacts', appId, 'users', user.uid, 'clothes', item.id || item); switch (action) { case 'favorite': await updateDoc(docRef, { favorite: !item.favorite }); break; case 'trash': await updateDoc(docRef, { status: 'trash', trashedAt: new Date().getTime() }); break; case 'restore': await updateDoc(docRef, { status: 'active', trashedAt: null }); break; case 'laundry': await updateDoc(docRef, { status: 'laundry' }); break; case 'clean': await updateDoc(docRef, { status: 'active' }); break; case 'delete': if (window.confirm(t('confirmDeletePerm'))) await deleteDoc(docRef); break; } }; const saveItem = async (e) => { e.preventDefault(); if (!user) return; const formData = new FormData(e.target); const colorVal = formData.get('color'); if (!colorVal || colorVal.trim() === '') { alert('Por favor selecione pelo menos uma cor.'); return; } setLoading(true); const itemData = { name: formData.get('name'), category: formData.get('category'), color: formData.get('color'), imageUrl: formData.get('imageUrl') || 'https://images.unsplash.com/photo-1521572267360-ee0c2909d518?q=80&w=500&auto=format&fit=crop', status: formData.get('isWishlist') ? 'wishlist' : (editingItem && editingItem.status === 'wishlist' ? 'active' : (editingItem ? editingItem.status : 'active')), favorite: editingItem ? (editingItem.favorite || false) : false, sections: itemSections, updatedAt: new Date().getTime() }; try { // Guardamos o id se for edição antes de apagar o estado const currentEditId = editingItem ? editingItem.id : null; // Navegação instantânea (Optimistic UI Update) setEditingItem(null); setImageUrlDraft(''); setCategoryFilter('Todos'); setColorFilter(''); setAgeFilter('any'); setSearchTerm(''); setView(formData.get('isWishlist') ? 'wishlist' : 'closet'); if (currentEditId) { const docRef = doc(db, 'artifacts', appId, 'users', user.uid, 'clothes', currentEditId); await updateDoc(docRef, itemData); } else { itemData.createdAt = new Date().getTime(); const clothesCol = collection(db, 'artifacts', appId, 'users', user.uid, 'clothes'); await addDoc(clothesCol, itemData); } } catch (e) { console.error(e); } finally { setLoading(false); } }; const saveLook = async (e) => { e.preventDefault(); if (selectedForLook.length < 2) return; setLoading(true); const fd = new FormData(e.target); const lookData = { name: fd.get('lookName'), items: selectedForLook, sections: lookSections, updatedAt: new Date().getTime() }; try { if (editingLook) { const docRef = doc(db, 'artifacts', appId, 'users', user.uid, 'looks', editingLook.id); await updateDoc(docRef, lookData); } else { lookData.createdAt = new Date().getTime(); const looksCol = collection(db, 'artifacts', appId, 'users', user.uid, 'looks'); await addDoc(looksCol, lookData); } setSelectedForLook([]); setEditingLook(null); setView('outfits'); } catch (e) { console.error(e); } finally { setLoading(false); } }; const deleteLook = async (id) => { if (!window.confirm(t('confirmDeleteLook'))) return; const docRef = doc(db, 'artifacts', appId, 'users', user.uid, 'looks', id); await deleteDoc(docRef); }; // Gerar link de partilha e copiar para clipboard const shareLook = async (look) => { if (!user) return; try { const lookItems = look.items.map(itemId => { const item = clothes.find(c => c.id === itemId); return item ? { name: item.name, category: item.category, color: item.color, imageUrl: item.imageUrl, } : null; }).filter(Boolean); const sharedCol = collection(db, 'artifacts', appId, 'sharedLooks'); const docRef = doc(sharedCol); // Gerar ID de forma síncrona para contornar restrições do Safari // Força o uso do domínio oficial se estiver em localhost (para os links de partilha funcionarem) const baseUrl = (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') ? 'https://mycloset.epvc.pt' : window.location.origin; const shareUrl = `${baseUrl}${window.location.pathname}?shared=${docRef.id}`; let copySuccess = false; // Tentar copiar para o clipboard imediatamente no fluxo do clique try { if (navigator.clipboard && window.isSecureContext) { await navigator.clipboard.writeText(shareUrl); copySuccess = true; } else { throw new Error("Clipboard API indisponível"); } } catch (err) { // Fallback para Safari antigo ou ambientes sem HTTPS try { const textArea = document.createElement("textarea"); textArea.value = shareUrl; textArea.style.position = "fixed"; textArea.style.left = "-9999px"; document.body.appendChild(textArea); textArea.focus(); textArea.select(); copySuccess = document.execCommand('copy'); document.body.removeChild(textArea); } catch (fallbackErr) { console.error('Erro no fallback de clipboard:', fallbackErr); } } // Opcional: usar Web Share API se estiver no telemóvel/Mac (comentado caso prefiram só copiar) // if (navigator.share) { // await navigator.share({ title: 'MyCloset Look', url: shareUrl }); // } // Agora podemos guardar no Firebase de forma assíncrona await setDoc(docRef, { lookName: look.name, ownerUid: user.uid, ownerEmail: user.email || '', items: lookItems, createdAt: new Date().getTime(), }); if (copySuccess) { setCopiedLookId(look.id); setTimeout(() => setCopiedLookId(null), 3000); } else { alert('Link de partilha: ' + shareUrl); } } catch (err) { console.error('Erro ao partilhar look:', err); alert('Erro ao gerar link de partilha.'); } }; // Copiar o look partilhado para o armário do utilizador atual const copySharedLook = async () => { if (!user || !sharedLookData) return; setSharedLookCopying(true); try { const newItemIds = []; for (const item of sharedLookData.items) { const clothesCol = collection(db, 'artifacts', appId, 'users', user.uid, 'clothes'); const newItemRef = await addDoc(clothesCol, { name: item.name, category: item.category, color: item.color, imageUrl: item.imageUrl, status: 'active', favorite: false, createdAt: new Date().getTime(), updatedAt: new Date().getTime(), }); newItemIds.push(newItemRef.id); } const looksCol = collection(db, 'artifacts', appId, 'users', user.uid, 'looks'); await addDoc(looksCol, { name: sharedLookData.lookName, items: newItemIds, createdAt: new Date().getTime(), updatedAt: new Date().getTime(), }); // Notify the owner via coleção pública (inboxNotifications) if (sharedLookData.ownerUid && sharedLookData.ownerUid !== user.uid) { try { const inboxCol = collection(db, 'artifacts', appId, 'inboxNotifications'); await addDoc(inboxCol, { type: 'look_copied', recipientUid: sharedLookData.ownerUid, lookName: sharedLookData.lookName, copiedByEmail: userProfile?.username || user.email || 'Alguém', createdAt: new Date().getTime(), read: false }); } catch (notifErr) { console.error('Não foi possível enviar notificação ao dono do look:', notifErr); } } setShowSharedLookModal(false); setSharedLookData(null); setView('outfits'); } catch (err) { console.error('Erro ao copiar look:', err); alert('Erro ao copiar look.'); } finally { setSharedLookCopying(false); } }; const sendLookToLaundry = async (look) => { setLoading(true); try { const batch = writeBatch(db); look.items.forEach(itemId => { const docRef = doc(db, 'artifacts', appId, 'users', user.uid, 'clothes', itemId); batch.update(docRef, { status: 'laundry' }); }); await batch.commit(); setToastMessage(t('lookSentToLaundry') || 'Peças enviadas para a lavandaria!'); setTimeout(() => setToastMessage(null), 3000); } catch (e) { console.error(e); } finally { setLoading(false); } }; const handleAuth = async (e) => { e.preventDefault(); setAuthError(''); setLoading(true); const fd = new FormData(e.target); const email = fd.get('email'); const password = fd.get('password'); try { if (authMode === 'login') await signInWithEmailAndPassword(auth, email, password); else await createUserWithEmailAndPassword(auth, email, password); } catch (err) { console.error(err); if (err.code === 'auth/operation-not-allowed') { setAuthError(t('authErrorDisabled')); } else { setAuthError(err.message); } } finally { setLoading(false); } }; const handleForgotPassword = () => { setAuthError(''); setShowForgotPasswordModal(true); }; const handleForgotPasswordSubmit = async (e) => { e.preventDefault(); const email = forgotPasswordEmail; if (!email) return; setLoading(true); setAuthError(''); try { await sendPasswordResetEmail(auth, email); setShowForgotPasswordModal(false); setForgotPasswordEmail(''); setToastMessage(t('passwordResetSent') || "E-mail de recuperação enviado! Verifique a sua caixa de entrada."); } catch (err) { console.error(err); setAuthError(err.message); } finally { setLoading(false); } }; const emptyTrashPermanently = async () => { if (!user || !window.confirm(t('confirmEmptyTrash'))) return; setLoading(true); try { const batch = writeBatch(db); trashClothes.forEach(item => { const docRef = doc(db, 'artifacts', appId, 'users', user.uid, 'clothes', item.id); batch.delete(docRef); }); await batch.commit(); } catch (e) { console.error(e); } finally { setLoading(false); } }; const clearAllToTrash = async () => { if (!user || !window.confirm(t('confirmClearAll'))) return; setLoading(true); try { const batch = writeBatch(db); activeClothes.forEach(item => { const docRef = doc(db, 'artifacts', appId, 'users', user.uid, 'clothes', item.id); batch.update(docRef, { status: 'trash', trashedAt: new Date().getTime() }); }); await batch.commit(); } catch (e) { console.error(e); } finally { setLoading(false); } }; const handleProfileImageUpload = (e) => { const file = e.target.files[0]; if (!file || !user) return; const reader = new FileReader(); reader.onload = (event) => { const img = new Image(); img.onload = async () => { const canvas = document.createElement('canvas'); const MAX_SIZE = 400; let width = img.width; let height = img.height; if (width > height) { if (width > MAX_SIZE) { height *= MAX_SIZE / width; width = MAX_SIZE; } } else { if (height > MAX_SIZE) { width *= MAX_SIZE / height; height = MAX_SIZE; } } canvas.width = width; canvas.height = height; const ctx = canvas.getContext('2d'); ctx.drawImage(img, 0, 0, width, height); const base64Data = canvas.toDataURL('image/jpeg', 0.8); try { const profileDoc = doc(db, 'artifacts', appId, 'users', user.uid, 'profile', 'data'); await setDoc(profileDoc, { avatar: base64Data }, { merge: true }); } catch (err) { console.error("Error uploading image:", err); } }; img.src = event.target.result; }; reader.readAsDataURL(file); }; const handleItemImageUpload = (e) => { const file = e.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = (event) => { const img = new Image(); img.onload = () => { const canvas = document.createElement('canvas'); const MAX_SIZE = 800; // Tamanho maior para roupas let width = img.width; let height = img.height; if (width > height) { if (width > MAX_SIZE) { height *= MAX_SIZE / width; width = MAX_SIZE; } } else { if (height > MAX_SIZE) { width *= MAX_SIZE / height; height = MAX_SIZE; } } canvas.width = width; canvas.height = height; const ctx = canvas.getContext('2d'); ctx.drawImage(img, 0, 0, width, height); const base64Data = canvas.toDataURL('image/jpeg', 0.8); setImageUrlDraft(base64Data); }; img.src = event.target.result; }; reader.readAsDataURL(file); }; const saveProfile = async (e) => { e.preventDefault(); setSavingProfile(true); const fd = new FormData(e.target); try { const profileDoc = doc(db, 'artifacts', appId, 'users', user.uid, 'profile', 'data'); const dobDay = fd.get('dobDay'); const dobMonth = fd.get('dobMonth'); const dobYear = fd.get('dobYear'); let dob = fd.get('dob') || ''; if (dobDay && dobMonth && dobYear) { dob = `${dobYear}-${dobMonth}-${dobDay}`; } // Perform optimistc setDoc without blocking the UI setDoc(profileDoc, { username: fd.get('username') || '', fullName: fd.get('fullName') || '', dob: dob, bio: fd.get('bio') || '', location: fd.get('location') || '' }, { merge: true }).catch(err => { console.error(err); }); } catch (err) { console.error(err); } finally { // Re-enable the button shortly after for smooth optimistic UI setTimeout(() => { setSavingProfile(false); }, 600); } }; if (loading && !user) return
; if (view === 'auth') { return (
{authMode === 'login' ? : }

MyCloset

{authError &&
{authError}
}
{authMode === 'login' && (
)}
{/* Modal de Forgot Password */} {showForgotPasswordModal && (
setShowForgotPasswordModal(false)}> e.stopPropagation()}>

{t('forgotPassword')}

{t('forgotPasswordPrompt')}

setForgotPasswordEmail(e.target.value)} className="w-full p-5 rounded-2xl bg-gray-50 dark:bg-gray-800 border-none focus:ring-2 focus:ring-primary-500 outline-none font-bold text-inherit" />
)}
); } return (
{/* Sidebar - Design Futurista */} {/* Área Principal */}
{/* Header Superior */}

{view === 'dashboard' && t('overview')} {view === 'closet' && t('myCloset')} {view === 'wishlist' && (t('wishlist') || 'Carrinho')} {view === 'laundry' && t('laundry')} {view === 'outfits' && t('outfitsAndStyle')} {view === 'planner' && t('planning')} {view === 'settings' && t('settings')} {view === 'profile' && t('profileInfo')}

{/* Conteúdo Dinâmico */}
{/* DASHBOARD */} {view === 'dashboard' && (
{[ { label: t('readyClothes'), val: activeClothes.length, icon: Shirt, col: 'primary' }, { label: t('inLaundry'), val: laundryClothes.length, icon: Droplets, col: 'blue' }, { label: t('myLooks'), val: looks.length, icon: Sparkles, col: 'purple' }, { label: t('favorites'), val: activeClothes.filter(c => c.favorite).length, icon: Heart, col: 'rose' }, ].map((s, i) => (

{s.label}

{s.val}

))}
{weatherData ? weatherData.name : t('todayIn')}

{weatherData ? t('weatherCurrentAvg').replace('{current}', weatherData.currentTemp).replace('{avg}', weatherData.avgTemp) : t('weatherUpdate')}

{weatherData ? `${t('weatherForecastDesc').replace('{max}', weatherData.maxTemp).replace('{min}', weatherData.minTemp)} ${t('weatherMsg')}` : t('weatherMsg')}

{activeClothes.filter(c => c.category === 'Tops').slice(0, 2).map(c => (
))}

{t('topColors')}

{colorStats.length > 0 ? colorStats.map(stat => (
{stat.color} {stat.percentage}% ({stat.count})
)) : (

{t('addColorsToItems')}

)}
)} {/* ARMÁRIO & WISHLIST */} {(view === 'closet' || view === 'wishlist') && (
setSearchTerm(e.target.value)} />
{/* Barra de Secções */} {(view === 'closet' || view === 'wishlist' || view === 'outfits') && (
{sections.map(sec => ( ))}
)}
{filteredClothes.map(item => (
{item.name}
{item.category}

{item.name}

{item.color}
{item.sections && item.sections.length > 0 && (
{item.sections.map(secId => { const sec = sections.find(s => s.id === secId); return sec ? ( {sec.name} ) : null; })}
)}
))}
)} {/* LAVANDARIA */} {view === 'laundry' && (

{t('laundryBasket')}

{laundryClothes.map(item => (

{item.name}

{t('washing')}
))} {laundryClothes.length === 0 && (
{t('emptyBasket')}
)}
)} {/* LOOKS */} {view === 'outfits' && (

{editingLook ? t('editLook') || 'Editar Outfit' : t('createNewLook')}

{t('selectedPieces')} ({selectedForLook.length})

{selectedForLook.map(id => { const item = clothes.find(c => c.id === id); return (
); })} {selectedForLook.length === 0 &&

{t('selectPieces')}

}
{sections.length === 0 ? (

{t('noSectionsCreated')}

) : (
{sections.map(sec => ( ))}
)}
{editingLook && ( )}

{t('closetLabel')}

{availableForLooks.map(c => ( ))}
{(() => { const filteredBySectionLooks = looks.filter(look => { const matchesSection = activeSectionFilter === 'all' || (look.sections && look.sections.includes(activeSectionFilter)); let matchesColor = true; if (colorFilter) { matchesColor = look.items.some(id => { const item = clothes.find(c => c.id === id); return item && item.color && item.color.includes(colorFilter); }); } return matchesSection && matchesColor; }); const availableLooks = filteredBySectionLooks.filter(look => look.items.every(id => { const item = clothes.find(c => c.id === id); return !item || item.status !== 'laundry'; }) ); const laundryLooks = filteredBySectionLooks.filter(look => look.items.some(id => { const item = clothes.find(c => c.id === id); return item && item.status === 'laundry'; }) ); const renderLookCard = (look) => { const hasLaundryPieces = look.items.some(id => { const item = clothes.find(c => c.id === id); return item && item.status === 'laundry'; }); return (

{look.name}

{look.items.length} {t('pieces')} • {new Date(look.createdAt).toLocaleDateString()}

{look.items.map(itemId => { const item = clothes.find(c => c.id === itemId); const inLaundry = item?.status === 'laundry'; return (
{inLaundry && (
)}
); })}
{hasLaundryPieces && (

{look.items.filter(id => { const it = clothes.find(c => c.id === id); return it?.status === 'laundry'; }).length} {t('piecesInLaundry')}

)} {look.sections && look.sections.length > 0 && (
{look.sections.map(secId => { const sec = sections.find(s => s.id === secId); return sec ? ( {sec.name} ) : null; })}
)}
); }; return ( <> {/* Looks disponíveis */}

{t('lookHistory')} — {t('availableLooks')} ({availableLooks.length})

{availableLooks.length > 0 ? (
{availableLooks.map(renderLookCard)}
) : (
{t('noLooksAvailable')}
)}
{/* Looks com peças na lavandaria */} {laundryLooks.length > 0 && (

{t('toBeWashed')} — {t('unavailable')} ({laundryLooks.length})

{laundryLooks.map(renderLookCard)}
)} ); })()}
)} {/* PLANEADOR */} {view === 'planner' && (() => { const today = new Date(); today.setHours(0, 0, 0, 0); const todayStr = `${today.getFullYear()}-${String(today.getMonth()+1).padStart(2,'0')}-${String(today.getDate()).padStart(2,'0')}`; const year = plannerCurrentDate.getFullYear(); const month = plannerCurrentDate.getMonth(); const fmtDate = (d) => `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`; const getPlan = (ds) => outfitPlans.find(p => p.date === ds); const getLookForDay = (ds) => { const p = getPlan(ds); return p ? looks.find(l => l.id === p.lookId) : null; }; const getMonthDays = () => { const first = new Date(year, month, 1); const last = new Date(year, month + 1, 0); const offset = (first.getDay() + 6) % 7; const days = []; for (let i = 0; i < offset; i++) days.push({ date: new Date(year, month, 1 - offset + i), cur: false }); for (let d = 1; d <= last.getDate(); d++) days.push({ date: new Date(year, month, d), cur: true }); const rem = (7 - (days.length % 7)) % 7; for (let i = 1; i <= rem; i++) days.push({ date: new Date(year, month + 1, i), cur: false }); return days; }; const getWeekDays = () => { const d = new Date(plannerCurrentDate); const off = (d.getDay() + 6) % 7; const mon = new Date(d); mon.setDate(d.getDate() - off); return Array.from({ length: 7 }, (_, i) => { const x = new Date(mon); x.setDate(mon.getDate() + i); return x; }); }; const langLocaleMap = { PT: 'pt-PT', EN: 'en-GB', ES: 'es-ES', FR: 'fr-FR', DE: 'de-DE' }; const locale = langLocaleMap[language] || 'pt-PT'; const monthNames = Array.from({ length: 12 }, (_, i) => { const d = new Date(2000, i, 1); const name = d.toLocaleDateString(locale, { month: 'long' }); return name.charAt(0).toUpperCase() + name.slice(1); }); const dayHeaders = Array.from({ length: 7 }, (_, i) => { const d = new Date(2024, 0, i + 1); // 2024-01-01 is Monday return d.toLocaleDateString(locale, { weekday: 'short' }).replace('.', ''); }); const prev = () => { const d = new Date(plannerCurrentDate); plannerMode === 'month' ? d.setMonth(month-1) : d.setDate(d.getDate()-7); setPlannerCurrentDate(d); }; const next = () => { const d = new Date(plannerCurrentDate); plannerMode === 'month' ? d.setMonth(month+1) : d.setDate(d.getDate()+7); setPlannerCurrentDate(d); }; const wDays = getWeekDays(); const weekLabel = `${wDays[0].getDate()} ${monthNames[wDays[0].getMonth()]} — ${wDays[6].getDate()} ${monthNames[wDays[6].getMonth()]} ${wDays[6].getFullYear()}`; const DayCell = ({ date, cur = true }) => { const ds = fmtDate(date); const look = getLookForDay(ds); const isToday = ds === todayStr; const isWeek = plannerMode === 'week'; return (
{ setPlannerPickerDate(ds); setShowPlannerPicker(true); }} className={`relative rounded-2xl overflow-hidden cursor-pointer transition-all group border-2 ${isToday ? 'border-primary-600 shadow-lg shadow-primary-600/20' : !cur ? 'border-transparent opacity-30' : 'border-transparent hover:border-primary-300 dark:hover:border-primary-700'} ${darkMode ? 'bg-gray-800/80' : 'bg-gray-50'}`} style={{ minHeight: isWeek ? '180px' : '100px' }} >
{date.getDate()} {isToday && {t('today')}}
{look ? (
{look.items.slice(0, isWeek ? 4 : 3).map(itemId => { const it = clothes.find(c => c.id === itemId); return it ?
: null; })}

{look.name}

{isWeek &&

{look.items.length} {t('piecesShort')}

}
) : ( cur &&
Outfit
)}
); }; return (
{/* Controles */}

{plannerMode === 'month' ? `${monthNames[month]} ${year}` : weekLabel}

{['month','week'].map(m => ( ))}
{/* Cabeçalhos dos dias */}
{dayHeaders.map(h => (
{h}
))}
{/* Grelha */} {plannerMode === 'month' ? (
{getMonthDays().map(({ date, cur }) => )}
) : (
{getWeekDays().map(date => )}
)}
); })()} {/* ADICIONAR / EDITAR */} {(view === 'add' || view === 'edit') && (

{editingItem ? t('edit') : t('newItem')}

{imageUrlDraft ? ( ) : (

{t('preview')}

)}
{['Vermelho', 'Azul', 'Amarelo', 'Verde', 'Laranja', 'Roxo', 'Branco', 'Preto', 'Cinzento', 'Bege'].map(c => ( ))}
{itemColors.length === 0 &&

{t('selectOneColor')}

}
setImageUrlDraft(e.target.value)} placeholder="https://..." className={`w-full p-5 rounded-2xl border-none outline-none focus:ring-4 focus:ring-primary-500/10 font-bold transition-all ${darkMode ? 'bg-gray-700 text-white' : 'bg-gray-100 text-gray-900'}`} />
{t('or')}
{/* Campo de Secções */}
{sections.length === 0 ? (

{t('noSectionsCreated')}

) : (
{sections.map(sec => ( ))}
)}
)} {/* PERFIL */} {view === 'profile' && (
{userProfile?.avatar ? ( Profile ) : ( {(userProfile?.fullName?.[0] || userProfile?.username?.[0] || user?.email?.[0] || 'U').toUpperCase()} )}

{userProfile?.fullName || t('yourAccount')}

@{userProfile?.username || user?.email?.split('@')[0] || t('papMode')}

{t('profileInfo')}

)} {/* DEFINIÇÕES */} {view === 'settings' && (
{/* Preferências */}

{t('preferences')}

{t('darkMode')}

{t('interfaceAppearance')}

{t('themeColor') || 'Cor do Tema'}

{t('personalizeColor') || 'Personalize a cor'}

{[ { id: 'theme-indigo', color: '#4f46e5' }, { id: 'theme-rose', color: '#e11d48' }, { id: 'theme-emerald', color: '#10b981' }, { id: 'theme-amber', color: '#f59e0b' }, { id: 'theme-slate', color: '#64748b' } ].map(tObj => ( ))}

{t('notifications')}

{t('lookReminders')}

{t('weatherAlerts')}

{t('weatherSuggestions')}

{t('cardSize') || 'Tamanho do Card'}

{t('cardSizeDesc') || 'Tamanho no armário/carrinho'}

{['small', 'medium', 'large'].map(s => ( ))}

{t('appLanguage')}

{language === 'PT' ? '🇵🇹 ' + t('portuguese') : language === 'EN' ? '🇬🇧 ' + t('english') : language === 'ES' ? '🇪🇸 ' + t('spanish') : language === 'FR' ? '🇫🇷 ' + t('french') : language === 'DE' ? '🇩🇪 ' + t('german') : language}

{t('feedbackTitle') || 'Suporte e Feedback'}

{t('feedbackDesc') || 'Tem alguma ideia, sugestão ou encontrou algum problema? Envie uma mensagem diretamente para nós!'}

{ e.preventDefault(); const fd = new FormData(e.target); const type = fd.get('type'); const msg = fd.get('message'); const email = "faiker027@gmail.com"; try { const response = await fetch(`https://formsubmit.co/ajax/${email}`, { method: "POST", headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' }, body: JSON.stringify({ Tipo: type, Mensagem: msg, Utilizador: user?.email || 'Desconhecido', _subject: `MyCloset Feedback: ${type}` }) }); if (response.ok) { setToastMessage(t('msgSentSuccess')); setTimeout(() => setToastMessage(null), 4000); e.target.reset(); } else { throw new Error('Falha no envio'); } } catch (error) { console.error("Erro ao enviar feedback:", error); setToastMessage(t('msgSendError')); setTimeout(() => setToastMessage(null), 4000); } }} className="space-y-4">

{t('recycleBin')}

{trashClothes.length > 0 && }
{trashClothes.map(item => (

{item.name}

{t('deleted')}

))}

{t('criticalZone')}

{t('fullCleanActions')}

)}
{/* Modal do Planeador - Escolher Outfit */} {showPlannerPicker && plannerPickerDate && (
setShowPlannerPicker(false)}> e.stopPropagation()}>

{t('chooseOutfit')}

{(() => { const locMap = { PT: 'pt-PT', EN: 'en-GB', ES: 'es-ES', FR: 'fr-FR', DE: 'de-DE' }; return new Date(plannerPickerDate + 'T12:00:00').toLocaleDateString(locMap[language] || 'pt-PT', { weekday: 'long', day: 'numeric', month: 'long' }); })()}

{outfitPlans.find(p => p.date === plannerPickerDate) && ( )}
{looks.length === 0 ? (
{t('noOutfitCreated')}
) : looks.map(look => { const isSelected = outfitPlans.find(p => p.date === plannerPickerDate)?.lookId === look.id; return ( ); })}
)} {/* Toast Message */} {toastMessage && (
{toastMessage}
)} {/* Modal de Notificações */} {showNotificationsModal && (
setShowNotificationsModal(false)}> e.stopPropagation()}> {/* Header */}

{t('notificationsModal')}

{notifications.filter(n => !n.read).length > 0 && (

{notifications.filter(n => !n.read).length} {language === 'PT' ? 'nova(s)' : language === 'EN' ? 'new' : language === 'ES' ? 'nueva(s)' : language === 'FR' ? 'nouvelle(s)' : 'neue'}

)}
{notifications.filter(n => !n.read).length > 0 && ( )}
{/* Lista */}
{notifications.length === 0 ? (
{t('noNotifications')}
) : notifications.map(notif => (
{/* Ícone da Notificação */}
{notif.type === 'look_copied' ? '✂️' : }
{/* Conteúdo */}

{notif.type === 'look_copied' && ( <> {notif.copiedByEmail} {' '}{t('lookCopiedBy')}{' '} "{notif.lookName}" )}

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

{/* Botão marcar como lida */} {!notif.read && ( )}
))}
)} {/* Modal de Gestão de Secções */} {showSectionManager && (
setShowSectionManager(false)}> e.stopPropagation()}>

{t('manageSections')}

{/* Criar nova secção */}
setNewSectionName(e.target.value)} placeholder={t('sectionPlaceholder')} onKeyDown={e => e.key === 'Enter' && saveSection()} className={`flex-1 p-3 rounded-xl border-none outline-none font-bold ${darkMode ? 'bg-gray-700 text-white' : 'bg-white'} shadow-sm`} />
{/* Lista de secções */}
{sections.length === 0 ? (
{t('noSections')}
) : sections.map(sec => (
{editingSectionId === sec.id ? ( <> 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.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)

)}
))}
)} {/* Modal de Filtros Avançados */} {showClosetFilters && (
setShowClosetFilters(false)}> e.stopPropagation()}>

{t('advancedFilters')}

{[t('all'), t('tops'), t('bottoms'), t('footwear'), t('coats'), t('accessories')].map(cat => ( ))}
)} {/* Modal de Idioma */} {showLangModal && (
setShowLangModal(false)}> e.stopPropagation()}>

{t('appLanguage')}

{[ { id: 'PT', flag: '🇵🇹', label: t('portuguese') }, { id: 'EN', flag: '🇬🇧', label: t('english') }, { id: 'ES', flag: '🇪🇸', label: t('spanish') }, { id: 'FR', flag: '🇫🇷', label: t('french') }, { id: 'DE', flag: '🇩🇪', label: t('german') } ].map(lang => ( ))}
)} {/* Modal de {t('sharedLookTitle')} */} {showSharedLookModal && sharedLookData && (
{ setShowSharedLookModal(false); setSharedLookData(null); }}>
e.stopPropagation()} > {/* Header com gradiente */}
{t('sharedLookTitle')}

{sharedLookData.lookName}

{sharedLookData.items.length} peça{sharedLookData.items.length !== 1 ? 's' : ''} • {t('sharedBy')} {sharedLookData.ownerEmail?.split('@')[0] || t('someone')}

{/* Peças do look */}

{t('includedPieces')}

{sharedLookData.items.map((item, idx) => (
{item.name}
{item.name}
))}
{/* Descrição das peças */}
{sharedLookData.items.map((item, idx) => (
{item.name} {item.category}
))}
{/* Ações */}
)}
); }