wishlist, salva tudo agora, correcao do tudo branco card dashboard
This commit is contained in:
201
src/App.jsx
201
src/App.jsx
@@ -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" />
|
||||||
|
|||||||
Reference in New Issue
Block a user