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 [categoryFilter, setCategoryFilter] = useState('Todos');
|
||||||
const [colorFilter, setColorFilter] = useState('');
|
const [colorFilter, setColorFilter] = useState('');
|
||||||
const [ageFilter, setAgeFilter] = useState('any');
|
const [ageFilter, setAgeFilter] = useState('any');
|
||||||
|
const [favoriteFilter, setFavoriteFilter] = useState(false);
|
||||||
const [showClosetFilters, setShowClosetFilters] = useState(false);
|
const [showClosetFilters, setShowClosetFilters] = useState(false);
|
||||||
|
|
||||||
// Estado para criação de Looks
|
// Estado para criação de Looks
|
||||||
@@ -315,9 +316,20 @@ export default function App() {
|
|||||||
return () => { unsubClothes(); unsubLooks(); unsubSections(); unsubProfile(); unsubNotif(); unsubPlans(); };
|
return () => { unsubClothes(); unsubLooks(); unsubSections(); unsubProfile(); unsubNotif(); unsubPlans(); };
|
||||||
}, [user]);
|
}, [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
|
// Fetch Weather Data
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (view !== 'dashboard') return;
|
if (!user) return;
|
||||||
const fetchWeather = async () => {
|
const fetchWeather = async () => {
|
||||||
try {
|
try {
|
||||||
const locName = userProfile?.location || 'Lisboa, Portugal';
|
const locName = userProfile?.location || 'Lisboa, Portugal';
|
||||||
@@ -326,16 +338,24 @@ export default function App() {
|
|||||||
|
|
||||||
if (geoData.results && geoData.results.length > 0) {
|
if (geoData.results && geoData.results.length > 0) {
|
||||||
const { latitude, longitude, name, country } = geoData.results[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();
|
const weatherRaw = await weatherRes.json();
|
||||||
|
|
||||||
if (weatherRaw.current_weather && weatherRaw.daily) {
|
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({
|
setWeatherData({
|
||||||
name: `${name}, ${country || ''}`.replace(/,\s*$/, ''),
|
name: `${name}, ${country || ''}`.replace(/,\s*$/, ''),
|
||||||
currentTemp: Math.round(weatherRaw.current_weather.temperature),
|
currentTemp: Math.round(weatherRaw.current_weather.temperature),
|
||||||
minTemp: Math.round(weatherRaw.daily.temperature_2m_min[0]),
|
minTemp: Math.round(weatherRaw.daily.temperature_2m_min[0]),
|
||||||
maxTemp: Math.round(weatherRaw.daily.temperature_2m_max[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();
|
fetchWeather();
|
||||||
}, [userProfile?.location, view]);
|
}, [userProfile?.location, user]);
|
||||||
|
|
||||||
// --- Lógicas de Negócio ---
|
// --- Lógicas de Negócio ---
|
||||||
|
|
||||||
@@ -491,9 +511,11 @@ export default function App() {
|
|||||||
else if (ageFilter === 'older') matchesAge = days > 365;
|
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
|
// Ações de Itens
|
||||||
const handleItemAction = async (action, item) => {
|
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) => {
|
const saveItem = async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!user) return;
|
if (!user) return;
|
||||||
@@ -1103,12 +1134,11 @@ export default function App() {
|
|||||||
{/* DASHBOARD */}
|
{/* DASHBOARD */}
|
||||||
{view === 'dashboard' && (
|
{view === 'dashboard' && (
|
||||||
<div className="space-y-12 animate-in fade-in duration-700">
|
<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('readyClothes'), val: activeClothes.length, icon: Shirt, col: 'primary' },
|
||||||
{ label: t('inLaundry'), val: laundryClothes.length, icon: Droplets, col: 'blue' },
|
{ label: t('inLaundry'), val: laundryClothes.length, icon: Droplets, col: 'blue' },
|
||||||
{ label: t('myLooks'), val: looks.length, icon: Sparkles, col: 'purple' },
|
{ 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) => (
|
].map((s, i) => (
|
||||||
<Card key={i} className="p-8 group hover:-translate-y-2" darkMode={darkMode}>
|
<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'}`}>
|
<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"
|
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')}
|
<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>
|
<span className="w-2 h-2 rounded-full bg-white animate-pulse"></span>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
@@ -1478,7 +1508,8 @@ export default function App() {
|
|||||||
return item && item.color && item.color.includes(colorFilter);
|
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 =>
|
const availableLooks = filteredBySectionLooks.filter(look =>
|
||||||
@@ -1517,6 +1548,13 @@ export default function App() {
|
|||||||
{copiedLookId === look.id ? t('linkCopied') : t('share')}
|
{copiedLookId === look.id ? t('linkCopied') : t('share')}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</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={() => { 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={() => 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>
|
<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 dayLooks = getLooksForDayGlobal(ds);
|
||||||
const isToday = ds === todayStrGlobal;
|
const isToday = ds === todayStrGlobal;
|
||||||
const isWeek = plannerMode === 'week';
|
const isWeek = plannerMode === 'week';
|
||||||
|
|
||||||
|
let dayWeather = null;
|
||||||
|
if (weatherAlerts && weatherData && weatherData.forecast) {
|
||||||
|
dayWeather = weatherData.forecast.find(f => f.date === ds);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
onClick={() => { setPlannerPickerDate(ds); setShowPlannerPicker(true); }}
|
onClick={() => { setPlannerPickerDate(ds); setShowPlannerPicker(true); }}
|
||||||
@@ -1675,7 +1719,14 @@ export default function App() {
|
|||||||
style={{ minHeight: isWeek ? '180px' : '100px' }}
|
style={{ minHeight: isWeek ? '180px' : '100px' }}
|
||||||
>
|
>
|
||||||
<div className={`px-3 py-2 flex items-center justify-between shrink-0 ${isToday ? 'bg-primary-600' : ''}`}>
|
<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>}
|
{isToday && <span className="text-[8px] font-black text-white/80 uppercase tracking-widest">{t('today')}</span>}
|
||||||
</div>
|
</div>
|
||||||
{dayLooks.length > 0 ? (
|
{dayLooks.length > 0 ? (
|
||||||
@@ -2412,10 +2463,24 @@ export default function App() {
|
|||||||
{availableColors.map(c => <option key={c} value={c}>{c}</option>)}
|
{availableColors.map(c => <option key={c} value={c}>{c}</option>)}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</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>
|
||||||
|
|
||||||
<div className="pt-8 flex gap-4 border-t mt-8 border-gray-100 dark:border-gray-800">
|
<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>
|
<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>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ export const translations = {
|
|||||||
dailyOutfit: "Outfit Diário",
|
dailyOutfit: "Outfit Diário",
|
||||||
noOutfitPlanned: "Nenhum Outfit Planeado",
|
noOutfitPlanned: "Nenhum Outfit Planeado",
|
||||||
goToPlanning: "Vá ao planeamento para adicionar",
|
goToPlanning: "Vá ao planeamento para adicionar",
|
||||||
|
onlyFavorites: "Apenas Favoritos",
|
||||||
logout: "Sair",
|
logout: "Sair",
|
||||||
overview: "Visão Geral",
|
overview: "Visão Geral",
|
||||||
myCloset: "O Meu Armário",
|
myCloset: "O Meu Armário",
|
||||||
@@ -223,6 +224,7 @@ export const translations = {
|
|||||||
dailyOutfit: "Daily Outfit",
|
dailyOutfit: "Daily Outfit",
|
||||||
noOutfitPlanned: "No Outfit Planned",
|
noOutfitPlanned: "No Outfit Planned",
|
||||||
goToPlanning: "Go to planning to add one",
|
goToPlanning: "Go to planning to add one",
|
||||||
|
onlyFavorites: "Favorites Only",
|
||||||
logout: "Logout",
|
logout: "Logout",
|
||||||
overview: "Overview",
|
overview: "Overview",
|
||||||
myCloset: "My Closet",
|
myCloset: "My Closet",
|
||||||
@@ -425,6 +427,7 @@ export const translations = {
|
|||||||
dailyOutfit: "Outfit Diario",
|
dailyOutfit: "Outfit Diario",
|
||||||
noOutfitPlanned: "Sin Outfit Planeado",
|
noOutfitPlanned: "Sin Outfit Planeado",
|
||||||
goToPlanning: "Ve a planificación para añadir",
|
goToPlanning: "Ve a planificación para añadir",
|
||||||
|
onlyFavorites: "Solo Favoritos",
|
||||||
logout: "Cerrar Sesión",
|
logout: "Cerrar Sesión",
|
||||||
overview: "Visión General",
|
overview: "Visión General",
|
||||||
myCloset: "Mi Armario",
|
myCloset: "Mi Armario",
|
||||||
@@ -627,6 +630,7 @@ export const translations = {
|
|||||||
dailyOutfit: "Tenue du Jour",
|
dailyOutfit: "Tenue du Jour",
|
||||||
noOutfitPlanned: "Aucune Tenue Prévue",
|
noOutfitPlanned: "Aucune Tenue Prévue",
|
||||||
goToPlanning: "Allez dans planification pour ajouter",
|
goToPlanning: "Allez dans planification pour ajouter",
|
||||||
|
onlyFavorites: "Favoris Uniquement",
|
||||||
logout: "Déconnexion",
|
logout: "Déconnexion",
|
||||||
overview: "Vue d'ensemble",
|
overview: "Vue d'ensemble",
|
||||||
myCloset: "Mon Placard",
|
myCloset: "Mon Placard",
|
||||||
@@ -829,6 +833,7 @@ export const translations = {
|
|||||||
dailyOutfit: "Tägliches Outfit",
|
dailyOutfit: "Tägliches Outfit",
|
||||||
noOutfitPlanned: "Kein Outfit Geplant",
|
noOutfitPlanned: "Kein Outfit Geplant",
|
||||||
goToPlanning: "Gehen Sie zur Planung, um eins hinzuzufügen",
|
goToPlanning: "Gehen Sie zur Planung, um eins hinzuzufügen",
|
||||||
|
onlyFavorites: "Nur Favoriten",
|
||||||
logout: "Abmelden",
|
logout: "Abmelden",
|
||||||
overview: "Übersicht",
|
overview: "Übersicht",
|
||||||
myCloset: "Mein Schrank",
|
myCloset: "Mein Schrank",
|
||||||
|
|||||||
Reference in New Issue
Block a user