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 &&
}
{/* Modal de Forgot Password */}
{showForgotPasswordModal && (
setShowForgotPasswordModal(false)}>
e.stopPropagation()}>
{t('forgotPassword')}
{t('forgotPasswordPrompt')}
)}
);
}
return (
{/* Sidebar - Design Futurista */}
{/* Área Principal */}
{/* Header Superior */}
{/* 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') && (
{/* Barra de Secções */}
{(view === 'closet' || view === 'wishlist' || view === 'outfits') && (
{sections.map(sec => (
))}
)}
{filteredClothes.map(item => (
{item.category}
{item.name}
{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' && (
{laundryClothes.map(item => (
{item.name}
{t('washing')}
))}
{laundryClothes.length === 0 && (
{t('emptyBasket')}
)}
)}
{/* LOOKS */}
{view === 'outfits' && (
{editingLook ? t('editLook') || 'Editar Outfit' : t('createNewLook')}
{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 &&
)}
);
};
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 ? (
) : (
)}
)}
{/* PERFIL */}
{view === 'profile' && (
{userProfile?.avatar ? (

) : (
{(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!'}
{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 && (
)}
{/* 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 */}
{/* 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 */}
{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) => (
))}
{/* Descrição das peças */}
{sharedLookData.items.map((item, idx) => (
{item.name}
{item.category}
))}
{/* Ações */}
)}
);
}