import React, { useState, useEffect, useMemo } 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
} from 'lucide-react';
import {
signInWithEmailAndPassword, createUserWithEmailAndPassword,
onAuthStateChanged, signOut, signInWithCustomToken
} from 'firebase/auth';
import {
collection, doc, onSnapshot, addDoc, updateDoc,
deleteDoc, writeBatch, setDoc
} 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 [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(() => localStorage.getItem('app-theme') || 'theme-indigo');
const [weatherData, setWeatherData] = useState(null);
const t = (key) => translations[language]?.[key] || translations['PT'][key] || key;
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);
};
useEffect(() => {
if (editingItem && editingItem.color) {
setItemColors(editingItem.color.split(',').map(c => c.trim()).filter(Boolean));
} else {
setItemColors([]);
}
}, [editingItem]);
useEffect(() => {
document.documentElement.classList.remove('theme-indigo', 'theme-rose', 'theme-emerald', 'theme-amber', 'theme-slate');
document.documentElement.classList.add(theme);
localStorage.setItem('app-theme', theme);
}, [theme]);
// 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) => {
setUser(currentUser);
currentUser ? setView('dashboard') : setView('auth');
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));
// 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);
}
}
else setUserProfile({});
}, (err) => console.error(err));
return () => { unsubClothes(); unsubLooks(); unsubProfile(); };
}, [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 => c.status === 'active' || c.status === 'wishlist'), [clothes]);
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));
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;
});
}, [baseClothes, searchTerm, categoryFilter, colorFilter, ageFilter, t]);
// 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,
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,
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);
};
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);
look.items.forEach(itemId => {
const docRef = doc(db, 'artifacts', appId, 'users', user.uid, 'clothes', itemId);
batch.update(docRef, { status: 'laundry' });
});
await batch.commit();
alert(t('lookSentToLaundry') || 'Peças enviadas para a lavandaria!');
} 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 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 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 &&
}
);
}
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 ? `${weatherData.currentTemp}°C Atual • Média ${weatherData.avgTemp}°C` : t('weatherUpdate')}
{weatherData ? `O dia de hoje tem máximas de ${weatherData.maxTemp}°C e mínimas de ${weatherData.minTemp}°C. ${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})
)) : (
Adicione cores aos seus itens.
)}
)}
{/* ARMÁRIO & WISHLIST */}
{(view === 'closet' || view === 'wishlist') && (
{filteredClothes.map(item => (
{item.category}
))}
)}
{/* LAVANDARIA */}
{view === 'laundry' && (
{t('laundryBasket')}
{t('laundryMsg')}
{laundryClothes.map(item => (
{item.name}
{t('washing')}
))}
{laundryClothes.length === 0 && (
{t('emptyBasket')}
)}
)}
{/* LOOKS */}
{view === 'outfits' && (
{editingLook ? t('editLook') || 'Editar Look' : t('createNewLook')}
{t('closetLabel')}
{availableForLooks.map(c => (
))}
{t('lookHistory')}
{looks.map(look => (
{look.name}
{look.items.length} {t('pieces')} • {new Date(look.createdAt).toLocaleDateString()}
{look.items.map(itemId => {
const item = clothes.find(c => c.id === itemId);
return (
);
})}
))}
)}
{/* ADICIONAR / EDITAR */}
{(view === 'add' || view === 'edit') && (
{editingItem ? t('edit') : t('newItem')}
{editingItem?.imageUrl || imageUrlDraft.startsWith('http') ? (
) : (
)}
)}
{/* 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('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 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 => (
))}
{[
{ id: 'any', label: t('anyAge') },
{ id: 'month', label: t('lessThanMonth') },
{ id: '6months', label: t('lessThan6Months') },
{ id: '1year', label: t('lessThanYear') },
{ id: 'older', label: t('older') }
].map(ageOpt => (
))}
)}
{/* 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 => (
))}
)}
);
}