att, pronto para apresentar!

This commit is contained in:
2026-05-06 11:46:48 +01:00
parent c37fdd321a
commit 197a2e326d
2 changed files with 82 additions and 12 deletions

View File

@@ -44,6 +44,7 @@ export default function App() {
const [categoryFilter, setCategoryFilter] = useState('Todos');
const [colorFilter, setColorFilter] = useState('');
const [ageFilter, setAgeFilter] = useState('any');
const [favoriteFilter, setFavoriteFilter] = useState(false);
const [showClosetFilters, setShowClosetFilters] = useState(false);
// Estado para criação de Looks
@@ -315,9 +316,20 @@ export default function App() {
return () => { unsubClothes(); unsubLooks(); unsubSections(); unsubProfile(); unsubNotif(); unsubPlans(); };
}, [user]);
const getWeatherEmoji = (code) => {
if (code === 0) return '☀️';
if ([1, 2, 3].includes(code)) return '⛅';
if ([45, 48].includes(code)) return '🌫️';
if ([51, 53, 55, 56, 57, 61, 63, 65, 66, 67].includes(code)) return '🌧️';
if ([71, 73, 75, 77, 85, 86].includes(code)) return '❄️';
if ([80, 81, 82].includes(code)) return '🌦️';
if ([95, 96, 99].includes(code)) return '⛈️';
return '☀️';
};
// Fetch Weather Data
useEffect(() => {
if (view !== 'dashboard') return;
if (!user) return;
const fetchWeather = async () => {
try {
const locName = userProfile?.location || 'Lisboa, Portugal';
@@ -326,16 +338,24 @@ export default function App() {
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}&current_weather=true&daily=temperature_2m_max,temperature_2m_min&timezone=auto`);
const weatherRes = await fetch(`https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}&current_weather=true&daily=temperature_2m_max,temperature_2m_min,weathercode&timezone=auto`);
const weatherRaw = await weatherRes.json();
if (weatherRaw.current_weather && weatherRaw.daily) {
const dailyForecast = weatherRaw.daily.time.map((dateStr, idx) => ({
date: dateStr,
min: Math.round(weatherRaw.daily.temperature_2m_min[idx]),
max: Math.round(weatherRaw.daily.temperature_2m_max[idx]),
weathercode: weatherRaw.daily.weathercode[idx]
}));
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)
avgTemp: Math.round((weatherRaw.daily.temperature_2m_min[0] + weatherRaw.daily.temperature_2m_max[0]) / 2),
forecast: dailyForecast
});
}
}
@@ -344,7 +364,7 @@ export default function App() {
}
};
fetchWeather();
}, [userProfile?.location, view]);
}, [userProfile?.location, user]);
// --- Lógicas de Negócio ---
@@ -491,9 +511,11 @@ export default function App() {
else if (ageFilter === 'older') matchesAge = days > 365;
}
return matchesSearch && matchesCategory && matchesColor && matchesAge && matchesSection;
const matchesFavorite = !favoriteFilter || c.favorite;
return matchesSearch && matchesCategory && matchesColor && matchesAge && matchesSection && matchesFavorite;
});
}, [baseClothes, searchTerm, categoryFilter, colorFilter, ageFilter, t, activeSectionFilter]);
}, [baseClothes, searchTerm, categoryFilter, colorFilter, ageFilter, t, activeSectionFilter, favoriteFilter]);
// Ações de Itens
const handleItemAction = async (action, item) => {
@@ -510,6 +532,15 @@ export default function App() {
}
};
const handleLookAction = async (action, look) => {
if (!user) return;
const docRef = doc(db, 'artifacts', appId, 'users', user.uid, 'looks', look.id || look);
switch (action) {
case 'favorite': await updateDoc(docRef, { favorite: !look.favorite }); break;
}
};
const saveItem = async (e) => {
e.preventDefault();
if (!user) return;
@@ -1103,12 +1134,11 @@ export default function App() {
{/* DASHBOARD */}
{view === 'dashboard' && (
<div className="space-y-12 animate-in fade-in duration-700">
<div className="grid grid-cols-1 md:grid-cols-4 gap-8">
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
{[
{ 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) => (
<Card key={i} className="p-8 group hover:-translate-y-2" darkMode={darkMode}>
<div className={`w-14 h-14 rounded-2xl flex items-center justify-center mb-6 shadow-inner ${darkMode ? 'bg-gray-700 text-primary-400' : 'bg-primary-50 text-primary-600'}`}>
@@ -1190,7 +1220,7 @@ export default function App() {
className="flex items-center gap-3 px-8 py-4 bg-primary-600 text-white rounded-[2rem] font-black uppercase text-[10px] tracking-widest shadow-xl shadow-primary-600/30 hover:scale-105 transition-all"
>
<Filter size={18} /> {t('advancedFilters')}
{(colorFilter || ageFilter !== 'any' || (categoryFilter !== 'Todos' && categoryFilter !== t('all'))) && (
{(colorFilter || favoriteFilter || ageFilter !== 'any' || (categoryFilter !== 'Todos' && categoryFilter !== t('all'))) && (
<span className="w-2 h-2 rounded-full bg-white animate-pulse"></span>
)}
</button>
@@ -1478,7 +1508,8 @@ export default function App() {
return item && item.color && item.color.includes(colorFilter);
});
}
return matchesSection && matchesColor;
const matchesFavorite = !favoriteFilter || look.favorite;
return matchesSection && matchesColor && matchesFavorite;
});
const availableLooks = filteredBySectionLooks.filter(look =>
@@ -1517,6 +1548,13 @@ export default function App() {
{copiedLookId === look.id ? t('linkCopied') : t('share')}
</span>
</button>
<button
onClick={() => handleLookAction('favorite', look)}
className={`p-2 transition-colors relative group/fav ${look.favorite ? 'text-rose-500' : 'text-gray-300 hover:text-rose-500'}`}
title="Favorito"
>
<Heart size={18} fill={look.favorite ? 'currentColor' : 'none'} />
</button>
<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 outfit 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>
@@ -1668,6 +1706,12 @@ export default function App() {
const dayLooks = getLooksForDayGlobal(ds);
const isToday = ds === todayStrGlobal;
const isWeek = plannerMode === 'week';
let dayWeather = null;
if (weatherAlerts && weatherData && weatherData.forecast) {
dayWeather = weatherData.forecast.find(f => f.date === ds);
}
return (
<div
onClick={() => { setPlannerPickerDate(ds); setShowPlannerPicker(true); }}
@@ -1675,7 +1719,14 @@ export default function App() {
style={{ minHeight: isWeek ? '180px' : '100px' }}
>
<div className={`px-3 py-2 flex items-center justify-between shrink-0 ${isToday ? 'bg-primary-600' : ''}`}>
<span className={`text-xs font-black ${isToday ? 'text-white' : ''}`}>{date.getDate()}</span>
<div className="flex items-center gap-1.5">
<span className={`text-xs font-black ${isToday ? 'text-white' : ''}`}>{date.getDate()}</span>
{dayWeather && (
<span className="text-sm drop-shadow-sm" title={`${dayWeather.min}ºC - ${dayWeather.max}ºC`}>
{getWeatherEmoji(dayWeather.weathercode)}
</span>
)}
</div>
{isToday && <span className="text-[8px] font-black text-white/80 uppercase tracking-widest">{t('today')}</span>}
</div>
{dayLooks.length > 0 ? (
@@ -2412,10 +2463,24 @@ export default function App() {
{availableColors.map(c => <option key={c} value={c}>{c}</option>)}
</select>
</div>
<div className="space-y-4">
<label className="text-[10px] font-black uppercase opacity-40 tracking-widest ml-1 text-inherit">{t('favorites')}</label>
<button
onClick={() => setFavoriteFilter(!favoriteFilter)}
className={`w-full p-4 rounded-2xl flex items-center justify-between font-bold transition-all border-2 ${favoriteFilter ? 'border-rose-500 bg-rose-50 text-rose-600 dark:bg-rose-900/20 dark:text-rose-400' : `border-transparent ${darkMode ? 'bg-gray-800 text-gray-400 hover:bg-gray-700' : 'bg-gray-50 text-gray-500 hover:bg-gray-100'}`}`}
>
<div className="flex items-center gap-3">
<Heart size={20} fill={favoriteFilter ? "currentColor" : "none"} />
<span>{t('onlyFavorites')}</span>
</div>
{favoriteFilter && <Check size={20} />}
</button>
</div>
</div>
<div className="pt-8 flex gap-4 border-t mt-8 border-gray-100 dark:border-gray-800">
<button onClick={() => { setCategoryFilter('Todos'); setColorFilter(''); setAgeFilter('any'); }} className="flex-1 py-4 font-black uppercase text-[10px] tracking-widest text-gray-500 hover:text-gray-900 dark:hover:text-white transition-colors">{t('clearAll')}</button>
<button onClick={() => { setCategoryFilter('Todos'); setColorFilter(''); setAgeFilter('any'); setFavoriteFilter(false); }} className="flex-1 py-4 font-black uppercase text-[10px] tracking-widest text-gray-500 hover:text-gray-900 dark:hover:text-white transition-colors">{t('clearAll')}</button>
<button onClick={() => setShowClosetFilters(false)} className="flex-1 py-4 bg-primary-600 text-white rounded-2xl font-black uppercase text-[10px] tracking-widest shadow-xl shadow-primary-600/30 hover:scale-105 transition-all">{t('applyFilters')}</button>
</div>
</Card>

View File

@@ -21,6 +21,7 @@ export const translations = {
dailyOutfit: "Outfit Diário",
noOutfitPlanned: "Nenhum Outfit Planeado",
goToPlanning: "Vá ao planeamento para adicionar",
onlyFavorites: "Apenas Favoritos",
logout: "Sair",
overview: "Visão Geral",
myCloset: "O Meu Armário",
@@ -223,6 +224,7 @@ export const translations = {
dailyOutfit: "Daily Outfit",
noOutfitPlanned: "No Outfit Planned",
goToPlanning: "Go to planning to add one",
onlyFavorites: "Favorites Only",
logout: "Logout",
overview: "Overview",
myCloset: "My Closet",
@@ -425,6 +427,7 @@ export const translations = {
dailyOutfit: "Outfit Diario",
noOutfitPlanned: "Sin Outfit Planeado",
goToPlanning: "Ve a planificación para añadir",
onlyFavorites: "Solo Favoritos",
logout: "Cerrar Sesión",
overview: "Visión General",
myCloset: "Mi Armario",
@@ -627,6 +630,7 @@ export const translations = {
dailyOutfit: "Tenue du Jour",
noOutfitPlanned: "Aucune Tenue Prévue",
goToPlanning: "Allez dans planification pour ajouter",
onlyFavorites: "Favoris Uniquement",
logout: "Déconnexion",
overview: "Vue d'ensemble",
myCloset: "Mon Placard",
@@ -829,6 +833,7 @@ export const translations = {
dailyOutfit: "Tägliches Outfit",
noOutfitPlanned: "Kein Outfit Geplant",
goToPlanning: "Gehen Sie zur Planung, um eins hinzuzufügen",
onlyFavorites: "Nur Favoriten",
logout: "Abmelden",
overview: "Übersicht",
myCloset: "Mein Schrank",