wishlist, salva tudo agora, correcao do tudo branco card dashboard

This commit is contained in:
2026-04-23 16:08:52 +01:00
parent d8a37915ab
commit 964c048db7

View File

@@ -56,11 +56,33 @@ export default function App() {
const [weatherAlerts, setWeatherAlerts] = useState(true); const [weatherAlerts, setWeatherAlerts] = useState(true);
const [language, setLanguage] = useState('PT'); const [language, setLanguage] = useState('PT');
const [showLangModal, setShowLangModal] = useState(false); const [showLangModal, setShowLangModal] = useState(false);
const [theme, setTheme] = useState(() => localStorage.getItem('app-theme') || 'theme-indigo'); const [theme, setTheme] = useState('theme-indigo');
const [weatherData, setWeatherData] = useState(null); const [weatherData, setWeatherData] = useState(null);
const t = (key) => translations[language]?.[key] || translations['PT'][key] || key; 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) => { const saveUserSetting = async (key, value) => {
if (!user) return; if (!user) return;
try { try {
@@ -109,9 +131,14 @@ export default function App() {
useEffect(() => { useEffect(() => {
document.documentElement.classList.remove('theme-indigo', 'theme-rose', 'theme-emerald', 'theme-amber', 'theme-slate'); document.documentElement.classList.remove('theme-indigo', 'theme-rose', 'theme-emerald', 'theme-amber', 'theme-slate');
document.documentElement.classList.add(theme); // Ecrã de login/registo: sempre theme-indigo, independentemente das preferências do utilizador
localStorage.setItem('app-theme', theme); const activeTheme = view === 'auth' ? 'theme-indigo' : theme;
}, [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 // 1. Inicializar Autenticação
useEffect(() => { useEffect(() => {
@@ -123,8 +150,26 @@ export default function App() {
}; };
initAuth(); initAuth();
const unsubscribe = onAuthStateChanged(auth, (currentUser) => { const unsubscribe = onAuthStateChanged(auth, (currentUser) => {
setUser(currentUser); if (!currentUser) {
currentUser ? setView('dashboard') : setView('auth'); // Reset de todo o estado ao fazer logout para não contaminar a próxima conta
setUser(null);
setClothes([]);
setLooks([]);
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');
}
setLoading(false); setLoading(false);
}); });
return () => unsubscribe(); return () => unsubscribe();
@@ -582,7 +627,11 @@ export default function App() {
<Badge variant="success">{t('online')}</Badge> <Badge variant="success">{t('online')}</Badge>
</div> </div>
</button> </button>
<button onClick={() => signOut(auth)} className="w-full py-4 text-red-500 font-black uppercase tracking-widest text-[10px] hover:bg-red-500/10 rounded-2xl transition-all flex items-center justify-center gap-3"> <button onClick={() => {
// Limpar dados locais antes de fazer logout
if (user?.uid) localStorage.removeItem(`app-theme-${user.uid}`);
signOut(auth);
}} className="w-full py-4 text-red-500 font-black uppercase tracking-widest text-[10px] hover:bg-red-500/10 rounded-2xl transition-all flex items-center justify-center gap-3">
<LogOut size={16} /> {t('logout')} <LogOut size={16} /> {t('logout')}
</button> </button>
</div> </div>
@@ -644,33 +693,33 @@ export default function App() {
</div> </div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8"> <div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
<Card className="lg:col-span-2 p-10 bg-primary-600 text-white border-none shadow-2xl shadow-primary-600/40 relative overflow-hidden" darkMode={darkMode}> <div className="lg:col-span-2 p-10 rounded-[2rem] relative overflow-hidden shadow-2xl" style={{ backgroundColor: 'hsl(var(--primary-600))', color: 'white' }}>
<div className="relative z-10 flex flex-col justify-between h-full"> <div className="relative z-10 flex flex-col justify-between h-full">
<div> <div>
<div className="flex items-center gap-3 mb-4"> <div className="flex items-center gap-3 mb-4">
<CloudSun size={28} className="text-primary-200" /> <CloudSun size={28} style={{ color: 'rgba(255,255,255,0.7)' }} />
<Badge variant="warning">{weatherData ? weatherData.name : t('todayIn')}</Badge> <Badge variant="warning">{weatherData ? weatherData.name : t('todayIn')}</Badge>
</div> </div>
<h3 className="text-5xl font-black tracking-tighter mb-4"> <h3 className="text-5xl font-black tracking-tighter mb-4" style={{ color: 'white' }}>
{weatherData ? `${weatherData.currentTemp}°C Atual • Média ${weatherData.avgTemp}°C` : t('weatherUpdate')} {weatherData ? `${weatherData.currentTemp}°C Atual • Média ${weatherData.avgTemp}°C` : t('weatherUpdate')}
</h3> </h3>
<p className="text-primary-100 text-lg font-medium max-w-lg leading-relaxed"> <p className="text-lg font-medium max-w-lg leading-relaxed" style={{ color: 'rgba(255,255,255,0.8)' }}>
{weatherData ? `O dia de hoje tem máximas de ${weatherData.maxTemp}°C e mínimas de ${weatherData.minTemp}°C. ${t('weatherMsg')}` : t('weatherMsg')} {weatherData ? `O dia de hoje tem máximas de ${weatherData.maxTemp}°C e mínimas de ${weatherData.minTemp}°C. ${t('weatherMsg')}` : t('weatherMsg')}
</p> </p>
</div> </div>
<div className="mt-10 flex gap-4"> <div className="mt-10 flex gap-4 items-center">
{activeClothes.filter(c => c.category === 'Tops').slice(0, 2).map(c => ( {activeClothes.filter(c => c.category === 'Tops').slice(0, 2).map(c => (
<div key={c.id} className="w-16 h-16 rounded-xl overflow-hidden border-2 border-primary-400"> <div key={c.id} className="w-16 h-16 rounded-xl overflow-hidden border-2" style={{ borderColor: 'rgba(255,255,255,0.4)' }}>
<img src={c.imageUrl} className="w-full h-full object-cover" alt="" /> <img src={c.imageUrl} className="w-full h-full object-cover" alt="" />
</div> </div>
))} ))}
<button onClick={() => setView('closet')} className="flex items-center gap-2 font-black uppercase text-xs tracking-widest hover:translate-x-2 transition-transform"> <button onClick={() => setView('closet')} className="flex items-center gap-2 font-black uppercase text-xs tracking-widest hover:translate-x-2 transition-transform" style={{ color: 'white' }}>
{t('exploreSuggestions')} <ArrowRight size={18} /> {t('exploreSuggestions')} <ArrowRight size={18} />
</button> </button>
</div> </div>
</div> </div>
<CloudSun size={350} className="absolute -bottom-20 -right-20 text-white/10" /> <CloudSun size={350} className="absolute -bottom-20 -right-20" style={{ color: 'rgba(255,255,255,0.1)' }} />
</Card> </div>
<Card className="p-8" darkMode={darkMode}> <Card className="p-8" darkMode={darkMode}>
<h3 className="text-lg font-black tracking-tight mb-8 flex items-center gap-2 text-inherit"><PieChart size={20} /> {t('topColors')}</h3> <h3 className="text-lg font-black tracking-tight mb-8 flex items-center gap-2 text-inherit"><PieChart size={20} /> {t('topColors')}</h3>
@@ -744,7 +793,7 @@ export default function App() {
<div className="absolute bottom-6 left-6 right-6 p-6 bg-white/95 dark:bg-gray-900/95 backdrop-blur-2xl rounded-3xl shadow-2xl transform transition-transform group-hover:-translate-y-2 z-20 pointer-events-auto"> <div className="absolute bottom-6 left-6 right-6 p-6 bg-white/95 dark:bg-gray-900/95 backdrop-blur-2xl rounded-3xl shadow-2xl transform transition-transform group-hover:-translate-y-2 z-20 pointer-events-auto">
<h4 className="text-xl font-black tracking-tighter truncate">{item.name}</h4> <h4 className="text-xl font-black tracking-tighter truncate">{item.name}</h4>
<div className="flex items-center gap-3 mt-2"> <div className="flex items-center gap-3 mt-2">
<div className="w-4 h-4 rounded-full border border-black/5" style={{ backgroundColor: (item.color || "").toLowerCase() }}></div> <div className="w-4 h-4 rounded-full border border-black/10 shrink-0" style={getColorStyle(item.color)}></div>
<span className="text-[10px] font-black opacity-40 uppercase tracking-widest">{item.color}</span> <span className="text-[10px] font-black opacity-40 uppercase tracking-widest">{item.color}</span>
</div> </div>
</div> </div>
@@ -837,35 +886,99 @@ export default function App() {
</div> </div>
</div> </div>
<div className="lg:col-span-2 space-y-8"> <div className="lg:col-span-2 space-y-10">
<h3 className="text-2xl font-black tracking-tighter flex items-center gap-3 px-2 text-inherit"><History className="text-gray-400" /> {t('lookHistory')}</h3> {(() => {
<div className="grid grid-cols-1 md:grid-cols-2 gap-8"> const availableLooks = looks.filter(look =>
{looks.map(look => ( look.items.every(id => {
<Card key={look.id} className="p-8 group hover:shadow-2xl transition-all border-none shadow-md" darkMode={darkMode}> const item = clothes.find(c => c.id === id);
<div className="flex justify-between items-start mb-6"> return !item || item.status !== 'laundry';
<div className="text-inherit"> })
<h4 className="text-xl font-black tracking-tight">{look.name}</h4> );
<p className="text-[10px] opacity-40 font-bold uppercase tracking-widest">{look.items.length} {t('pieces')} {new Date(look.createdAt).toLocaleDateString()}</p> const laundryLooks = looks.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 (
<Card key={look.id} className={`p-8 group hover:shadow-2xl transition-all border-none shadow-md ${hasLaundryPieces ? 'opacity-75' : ''}`} darkMode={darkMode}>
<div className="flex justify-between items-start mb-6">
<div className="text-inherit">
<h4 className="text-xl font-black tracking-tight">{look.name}</h4>
<p className="text-[10px] opacity-40 font-bold uppercase tracking-widest">{look.items.length} {t('pieces')} {new Date(look.createdAt).toLocaleDateString()}</p>
</div>
<div className="flex gap-2">
<button onClick={() => { setEditingLook(look); setSelectedForLook(look.items); }} className="p-2 text-gray-300 hover:text-primary-500 transition-colors"><Edit2 size={18} /></button>
<button onClick={() => sendLookToLaundry(look)} className="p-2 text-gray-300 hover:text-blue-500 transition-colors" title="Lavar look inteiro"><Droplets size={18} /></button>
<button onClick={() => deleteLook(look.id)} className="p-2 text-gray-300 hover:text-red-500 transition-colors"><Trash size={18} /></button>
</div>
</div> </div>
<div className="flex gap-2"> <div className="flex -space-x-4 mb-4">
<button onClick={() => { setEditingLook(look); setSelectedForLook(look.items); }} className="p-2 text-gray-300 hover:text-primary-500 transition-colors"><Edit2 size={18} /></button> {look.items.map(itemId => {
<button onClick={() => sendLookToLaundry(look)} className="p-2 text-gray-300 hover:text-blue-500 transition-colors" title="Lavar look inteiro"><Droplets size={18} /></button> const item = clothes.find(c => c.id === itemId);
<button onClick={() => deleteLook(look.id)} className="p-2 text-gray-300 hover:text-red-500 transition-colors"><Trash size={18} /></button> const inLaundry = item?.status === 'laundry';
return (
<div key={itemId} className={`relative w-20 h-20 rounded-2xl border-4 overflow-hidden shadow-lg transform group-hover:rotate-6 transition-transform ${inLaundry ? 'border-blue-400' : 'border-white dark:border-gray-800'}`}>
<img src={item?.imageUrl} className={`w-full h-full object-cover ${inLaundry ? 'brightness-75' : ''}`} alt="" />
{inLaundry && (
<div className="absolute inset-0 flex items-center justify-center bg-blue-500/30 backdrop-blur-[1px]">
<Droplets size={18} className="text-white drop-shadow" />
</div>
)}
</div>
);
})}
</div> </div>
{hasLaundryPieces && (
<div className="flex items-center gap-2 mt-2 px-3 py-2 bg-blue-50 dark:bg-blue-900/20 rounded-xl">
<Droplets size={14} className="text-blue-500 shrink-0" />
<p className="text-[10px] font-black uppercase tracking-widest text-blue-500">
{look.items.filter(id => { const it = clothes.find(c => c.id === id); return it?.status === 'laundry'; }).length} peça(s) na lavandaria
</p>
</div>
)}
</Card>
);
};
return (
<>
{/* Looks disponíveis */}
<div className="space-y-6">
<div className="flex items-center gap-3 px-2">
<div className="w-2.5 h-2.5 rounded-full bg-green-500"></div>
<h3 className="text-2xl font-black tracking-tighter text-inherit">{t('lookHistory')} <span className="text-sm font-bold opacity-40"> Disponíveis ({availableLooks.length})</span></h3>
</div>
{availableLooks.length > 0 ? (
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
{availableLooks.map(renderLookCard)}
</div>
) : (
<div className="py-12 text-center opacity-20 font-black uppercase tracking-[0.3em] text-sm">Nenhum look disponível</div>
)}
</div> </div>
<div className="flex -space-x-4">
{look.items.map(itemId => { {/* Looks com peças na lavandaria */}
const item = clothes.find(c => c.id === itemId); {laundryLooks.length > 0 && (
return ( <div className="space-y-6">
<div key={itemId} className="w-20 h-20 rounded-2xl border-4 border-white dark:border-gray-800 overflow-hidden shadow-lg transform group-hover:rotate-6 transition-transform"> <div className="flex items-center gap-3 px-2">
<img src={item?.imageUrl} className="w-full h-full object-cover" alt="" /> <div className="w-2.5 h-2.5 rounded-full bg-blue-400"></div>
</div> <h3 className="text-2xl font-black tracking-tighter text-inherit">A ser lavados <span className="text-sm font-bold opacity-40"> Indisponíveis ({laundryLooks.length})</span></h3>
); </div>
})} <div className="grid grid-cols-1 md:grid-cols-2 gap-8">
</div> {laundryLooks.map(renderLookCard)}
</Card> </div>
))} </div>
</div> )}
</>
);
})()}
</div> </div>
</div> </div>
</div> </div>
@@ -965,7 +1078,7 @@ export default function App() {
<Card className="p-8" darkMode={darkMode}> <Card className="p-8" darkMode={darkMode}>
<h3 className="text-xl font-black mb-6 flex items-center gap-3 text-inherit"><UserCircle className="text-primary-600" /> {t('profileInfo')}</h3> <h3 className="text-xl font-black mb-6 flex items-center gap-3 text-inherit"><UserCircle className="text-primary-600" /> {t('profileInfo')}</h3>
<form onSubmit={saveProfile} className="space-y-6"> <form key={`${userProfile?.username}-${userProfile?.fullName}-${userProfile?.dob}-${userProfile?.bio}-${userProfile?.location}`} onSubmit={saveProfile} className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6"> <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<Input label={t('username')} name="username" defaultValue={userProfile?.username || ''} placeholder="Ex: amari" /> <Input label={t('username')} name="username" defaultValue={userProfile?.username || ''} placeholder="Ex: amari" />
<Input label={t('fullName')} name="fullName" defaultValue={userProfile?.fullName || ''} placeholder="Ex: Amari Rodriguez" /> <Input label={t('fullName')} name="fullName" defaultValue={userProfile?.fullName || ''} placeholder="Ex: Amari Rodriguez" />