att, pronto para apresentar!
This commit is contained in:
89
src/App.jsx
89
src/App.jsx
@@ -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}¤t_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}¤t_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>
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user