seccao planeamento

This commit is contained in:
2026-05-05 01:59:57 +01:00
parent 93aae28012
commit 5dc41414a4
2 changed files with 219 additions and 9 deletions

View File

@@ -7,7 +7,7 @@ import {
PanelLeftClose, PanelLeftOpen, Sparkles, CloudSun, PanelLeftClose, PanelLeftOpen, Sparkles, CloudSun,
ArrowRight, Droplets, CheckCircle2, PieChart, History, ArrowRight, Droplets, CheckCircle2, PieChart, History,
X, Download, Bell, Globe, Filter, ShoppingBag, Share2, X, Download, Bell, Globe, Filter, ShoppingBag, Share2,
FolderOpen, Tag, Link FolderOpen, Tag, Link, Calendar, ChevronLeft, ChevronRight
} from 'lucide-react'; } from 'lucide-react';
import { import {
@@ -83,6 +83,13 @@ export default function App() {
const [showNotificationsModal, setShowNotificationsModal] = useState(false); const [showNotificationsModal, setShowNotificationsModal] = useState(false);
const [toastMessage, setToastMessage] = useState(null); const [toastMessage, setToastMessage] = useState(null);
// Estado do Planeador
const [plannerMode, setPlannerMode] = useState('month');
const [plannerCurrentDate, setPlannerCurrentDate] = useState(new Date());
const [outfitPlans, setOutfitPlans] = useState([]);
const [showPlannerPicker, setShowPlannerPicker] = useState(false);
const [plannerPickerDate, setPlannerPickerDate] = 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 // Mapeamento de nomes de cor (PT) para valores CSS
@@ -265,6 +272,12 @@ export default function App() {
setSections(snap.docs.map(d => ({ id: d.id, ...d.data() })).sort((a, b) => a.createdAt - b.createdAt)); setSections(snap.docs.map(d => ({ id: d.id, ...d.data() })).sort((a, b) => a.createdAt - b.createdAt));
}, (err) => console.error(err)); }, (err) => console.error(err));
// Planeador de Outfits
const plansCol = collection(db, 'artifacts', appId, 'users', user.uid, 'outfitPlans');
const unsubPlans = onSnapshot(plansCol, (snap) => {
setOutfitPlans(snap.docs.map(d => ({ id: d.id, ...d.data() })));
}, (err) => console.error(err));
// Profile // Profile
const profileDoc = doc(db, 'artifacts', appId, 'users', user.uid, 'profile', 'data'); const profileDoc = doc(db, 'artifacts', appId, 'users', user.uid, 'profile', 'data');
const unsubProfile = onSnapshot(profileDoc, (snap) => { const unsubProfile = onSnapshot(profileDoc, (snap) => {
@@ -289,7 +302,7 @@ export default function App() {
setNotifications(snap.docs.map(d => ({ id: d.id, ...d.data() })).sort((a, b) => b.createdAt - a.createdAt)); setNotifications(snap.docs.map(d => ({ id: d.id, ...d.data() })).sort((a, b) => b.createdAt - a.createdAt));
}, (err) => console.error('Notif listener error:', err)); }, (err) => console.error('Notif listener error:', err));
return () => { unsubClothes(); unsubLooks(); unsubSections(); unsubProfile(); unsubNotif(); }; return () => { unsubClothes(); unsubLooks(); unsubSections(); unsubProfile(); unsubNotif(); unsubPlans(); };
}, [user]); }, [user]);
// Fetch Weather Data // Fetch Weather Data
@@ -382,6 +395,16 @@ export default function App() {
if (activeSectionFilter === id) setActiveSectionFilter('all'); if (activeSectionFilter === id) setActiveSectionFilter('all');
}; };
const assignOutfitToDay = async (dateStr, lookId) => {
if (!user) return;
const planRef = doc(db, 'artifacts', appId, 'users', user.uid, 'outfitPlans', dateStr);
if (lookId) {
await setDoc(planRef, { date: dateStr, lookId, updatedAt: new Date().getTime() });
} else {
await deleteDoc(planRef);
}
};
const baseClothes = view === 'wishlist' ? wishlistClothes : activeClothes; const baseClothes = view === 'wishlist' ? wishlistClothes : activeClothes;
const availableColors = useMemo(() => { const availableColors = useMemo(() => {
@@ -905,6 +928,7 @@ export default function App() {
{ id: 'wishlist', label: t('wishlist') || 'Carrinho', icon: ShoppingBag }, { id: 'wishlist', label: t('wishlist') || 'Carrinho', icon: ShoppingBag },
{ id: 'laundry', label: t('laundry'), icon: Droplets }, { id: 'laundry', label: t('laundry'), icon: Droplets },
{ id: 'outfits', label: t('outfits'), icon: Sparkles }, { id: 'outfits', label: t('outfits'), icon: Sparkles },
{ id: 'planner', label: 'Planeamento', icon: Calendar },
{ id: 'settings', label: t('settings'), icon: Settings }, { id: 'settings', label: t('settings'), icon: Settings },
].map(item => ( ].map(item => (
<button <button
@@ -958,6 +982,7 @@ export default function App() {
{view === 'wishlist' && (t('wishlist') || 'Carrinho')} {view === 'wishlist' && (t('wishlist') || 'Carrinho')}
{view === 'laundry' && t('laundry')} {view === 'laundry' && t('laundry')}
{view === 'outfits' && t('outfitsAndStyle')} {view === 'outfits' && t('outfitsAndStyle')}
{view === 'planner' && 'Planeamento'}
{view === 'settings' && t('settings')} {view === 'settings' && t('settings')}
{view === 'profile' && t('profileInfo')} {view === 'profile' && t('profileInfo')}
</h2> </h2>
@@ -1426,6 +1451,132 @@ export default function App() {
</div> </div>
)} )}
{/* PLANEADOR */}
{view === 'planner' && (() => {
const today = new Date();
today.setHours(0, 0, 0, 0);
const todayStr = `${today.getFullYear()}-${String(today.getMonth()+1).padStart(2,'0')}-${String(today.getDate()).padStart(2,'0')}`;
const year = plannerCurrentDate.getFullYear();
const month = plannerCurrentDate.getMonth();
const fmtDate = (d) => `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`;
const getPlan = (ds) => outfitPlans.find(p => p.date === ds);
const getLookForDay = (ds) => { const p = getPlan(ds); return p ? looks.find(l => l.id === p.lookId) : null; };
const getMonthDays = () => {
const first = new Date(year, month, 1);
const last = new Date(year, month + 1, 0);
const offset = (first.getDay() + 6) % 7;
const days = [];
for (let i = 0; i < offset; i++) days.push({ date: new Date(year, month, 1 - offset + i), cur: false });
for (let d = 1; d <= last.getDate(); d++) days.push({ date: new Date(year, month, d), cur: true });
const rem = (7 - (days.length % 7)) % 7;
for (let i = 1; i <= rem; i++) days.push({ date: new Date(year, month + 1, i), cur: false });
return days;
};
const getWeekDays = () => {
const d = new Date(plannerCurrentDate);
const off = (d.getDay() + 6) % 7;
const mon = new Date(d); mon.setDate(d.getDate() - off);
return Array.from({ length: 7 }, (_, i) => { const x = new Date(mon); x.setDate(mon.getDate() + i); return x; });
};
const monthNames = ['Janeiro','Fevereiro','Março','Abril','Maio','Junho','Julho','Agosto','Setembro','Outubro','Novembro','Dezembro'];
const dayHeaders = ['Seg','Ter','Qua','Qui','Sex','Sáb','Dom'];
const prev = () => { const d = new Date(plannerCurrentDate); plannerMode === 'month' ? d.setMonth(month-1) : d.setDate(d.getDate()-7); setPlannerCurrentDate(d); };
const next = () => { const d = new Date(plannerCurrentDate); plannerMode === 'month' ? d.setMonth(month+1) : d.setDate(d.getDate()+7); setPlannerCurrentDate(d); };
const wDays = getWeekDays();
const weekLabel = `${wDays[0].getDate()} ${monthNames[wDays[0].getMonth()]}${wDays[6].getDate()} ${monthNames[wDays[6].getMonth()]} ${wDays[6].getFullYear()}`;
const DayCell = ({ date, cur = true }) => {
const ds = fmtDate(date);
const look = getLookForDay(ds);
const isToday = ds === todayStr;
const isWeek = plannerMode === 'week';
return (
<div
onClick={() => { setPlannerPickerDate(ds); setShowPlannerPicker(true); }}
className={`relative rounded-2xl overflow-hidden cursor-pointer transition-all group border-2 ${isToday ? 'border-primary-600 shadow-lg shadow-primary-600/20' : !cur ? 'border-transparent opacity-30' : 'border-transparent hover:border-primary-300 dark:hover:border-primary-700'} ${darkMode ? 'bg-gray-800/80' : 'bg-gray-50'}`}
style={{ minHeight: isWeek ? '180px' : '100px' }}
>
<div className={`px-3 py-2 flex items-center justify-between ${isToday ? 'bg-primary-600' : ''}`}>
<span className={`text-xs font-black ${isToday ? 'text-white' : ''}`}>{date.getDate()}</span>
{isToday && <span className="text-[8px] font-black text-white/80 uppercase tracking-widest">Hoje</span>}
</div>
{look ? (
<div className="px-2 pb-2 space-y-1">
<div className="flex -space-x-2">
{look.items.slice(0, isWeek ? 4 : 3).map(itemId => {
const it = clothes.find(c => c.id === itemId);
return it ? <div key={itemId} className={`${isWeek ? 'w-10 h-10' : 'w-7 h-7'} rounded-lg overflow-hidden border-2 border-white dark:border-gray-700 shrink-0`}><img src={it.imageUrl} className="w-full h-full object-cover" alt="" /></div> : null;
})}
</div>
<p className="text-[9px] font-black uppercase tracking-widest opacity-50 truncate">{look.name}</p>
{isWeek && <p className="text-[9px] opacity-40 font-bold">{look.items.length} peças</p>}
</div>
) : (
cur && <div className="absolute inset-0 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity">
<div className={`flex items-center gap-1 px-3 py-1.5 rounded-xl text-[9px] font-black uppercase tracking-widest ${darkMode ? 'bg-gray-700 text-primary-400' : 'bg-white text-primary-600 shadow-sm'}`}>
<Plus size={10} /> Outfit
</div>
</div>
)}
</div>
);
};
return (
<div className="space-y-6 animate-in fade-in duration-700 pb-20">
{/* Controles */}
<div className="flex items-center justify-between flex-wrap gap-4">
<div className="flex items-center gap-3">
<button onClick={prev} className={`p-3 rounded-2xl transition-all border ${darkMode ? 'bg-gray-800 border-gray-700 hover:bg-gray-700' : 'bg-white border-gray-200 hover:bg-gray-50'} shadow-sm`}>
<ChevronLeft size={20} />
</button>
<h3 className="text-lg font-black tracking-tight min-w-[220px] text-center">
{plannerMode === 'month' ? `${monthNames[month]} ${year}` : weekLabel}
</h3>
<button onClick={next} className={`p-3 rounded-2xl transition-all border ${darkMode ? 'bg-gray-800 border-gray-700 hover:bg-gray-700' : 'bg-white border-gray-200 hover:bg-gray-50'} shadow-sm`}>
<ChevronRight size={20} />
</button>
<button onClick={() => setPlannerCurrentDate(new Date())} className="px-4 py-2 text-[10px] font-black uppercase tracking-widest text-primary-600 bg-primary-50 dark:bg-primary-900/20 rounded-xl hover:bg-primary-100 dark:hover:bg-primary-900/40 transition-colors">
Hoje
</button>
</div>
<div className={`flex p-1.5 rounded-2xl gap-1 ${darkMode ? 'bg-gray-800' : 'bg-gray-100'}`}>
{['month','week'].map(m => (
<button key={m} onClick={() => setPlannerMode(m)} className={`px-5 py-2 rounded-xl font-black text-[10px] uppercase tracking-widest transition-all ${plannerMode === m ? `${darkMode ? 'bg-gray-700' : 'bg-white'} shadow-md text-primary-600` : 'text-gray-500 hover:text-gray-700'}`}>
{m === 'month' ? 'Mês' : 'Semana'}
</button>
))}
</div>
</div>
{/* Cabeçalhos dos dias */}
<div className="grid grid-cols-7 gap-2">
{dayHeaders.map(h => (
<div key={h} className="text-center text-[10px] font-black uppercase tracking-widest opacity-40 py-1">{h}</div>
))}
</div>
{/* Grelha */}
{plannerMode === 'month' ? (
<div className="grid grid-cols-7 gap-2">
{getMonthDays().map(({ date, cur }) => <DayCell key={fmtDate(date)} date={date} cur={cur} />)}
</div>
) : (
<div className="grid grid-cols-7 gap-3">
{getWeekDays().map(date => <DayCell key={fmtDate(date)} date={date} cur={true} />)}
</div>
)}
</div>
);
})()}
{/* ADICIONAR / EDITAR */} {/* ADICIONAR / EDITAR */}
{(view === 'add' || view === 'edit') && ( {(view === 'add' || view === 'edit') && (
<div className="max-w-4xl mx-auto animate-in zoom-in-95 duration-500"> <div className="max-w-4xl mx-auto animate-in zoom-in-95 duration-500">
@@ -1782,6 +1933,65 @@ export default function App() {
</div> </div>
</main> </main>
{/* Modal do Planeador - Escolher Outfit */}
{showPlannerPicker && plannerPickerDate && (
<div className="fixed inset-0 z-[200] flex items-center justify-center bg-black/60 backdrop-blur-sm p-6" onClick={() => setShowPlannerPicker(false)}>
<Card className="w-full max-w-lg p-8 animate-in zoom-in-95 flex flex-col max-h-[80vh]" darkMode={darkMode} onClick={e => e.stopPropagation()}>
<div className="flex items-center justify-between mb-6">
<div>
<h3 className="text-xl font-black text-inherit flex items-center gap-3">
<Calendar size={22} className="text-primary-600" /> Escolher Outfit
</h3>
<p className="text-[10px] font-black uppercase tracking-widest opacity-40 mt-1">
{new Date(plannerPickerDate + 'T12:00:00').toLocaleDateString('pt-PT', { weekday: 'long', day: 'numeric', month: 'long' })}
</p>
</div>
<button onClick={() => setShowPlannerPicker(false)} className="p-2 bg-gray-100 dark:bg-gray-800 rounded-full hover:scale-110 transition-all text-inherit"><X size={20} /></button>
</div>
{outfitPlans.find(p => p.date === plannerPickerDate) && (
<button
onClick={async () => { await assignOutfitToDay(plannerPickerDate, null); setShowPlannerPicker(false); }}
className="mb-4 w-full py-3 border-2 border-dashed border-red-200 dark:border-red-900/50 text-red-400 rounded-2xl font-black text-[10px] uppercase tracking-widest hover:border-red-400 hover:text-red-500 transition-all flex items-center justify-center gap-2"
>
<Trash size={14} /> Remover Outfit deste Dia
</button>
)}
<div className="flex-1 overflow-y-auto space-y-3 custom-scrollbar">
{looks.length === 0 ? (
<div className="py-12 text-center opacity-30 font-black uppercase tracking-[0.3em] text-sm">Nenhum outfit criado</div>
) : looks.map(look => {
const isSelected = outfitPlans.find(p => p.date === plannerPickerDate)?.lookId === look.id;
return (
<button
key={look.id}
onClick={async () => { await assignOutfitToDay(plannerPickerDate, look.id); setShowPlannerPicker(false); }}
className={`w-full flex items-center gap-4 p-4 rounded-2xl transition-all border-2 text-left ${isSelected ? 'border-primary-600 bg-primary-50 dark:bg-primary-900/20' : `border-transparent ${darkMode ? 'bg-gray-800 hover:bg-gray-700' : 'bg-gray-50 hover:bg-gray-100'}`}`}
>
<div className="flex -space-x-2 shrink-0">
{look.items.slice(0, 3).map(itemId => {
const item = clothes.find(c => c.id === itemId);
return item ? (
<div key={itemId} className="w-12 h-12 rounded-xl overflow-hidden border-2 border-white dark:border-gray-700">
<img src={item.imageUrl} className="w-full h-full object-cover" alt="" />
</div>
) : null;
})}
</div>
<div className="flex-1 min-w-0">
<p className="font-black text-sm truncate text-inherit">{look.name}</p>
<p className="text-[10px] uppercase tracking-widest opacity-40 font-bold">{look.items.length} peças</p>
</div>
{isSelected && <Check size={18} className="text-primary-600 shrink-0" />}
</button>
);
})}
</div>
</Card>
</div>
)}
{/* Toast Message */} {/* Toast Message */}
{toastMessage && ( {toastMessage && (
<div className="fixed bottom-8 left-1/2 transform -translate-x-1/2 z-[300] animate-in slide-in-from-bottom-5"> <div className="fixed bottom-8 left-1/2 transform -translate-x-1/2 z-[300] animate-in slide-in-from-bottom-5">

View File

@@ -8,16 +8,16 @@ export const translations = {
createAccount: "Criar Nova Conta", createAccount: "Criar Nova Conta",
haveAccount: "Já Tenho Conta", haveAccount: "Já Tenho Conta",
authErrorDisabled: "O login por e-mail está desativado.", authErrorDisabled: "O login por e-mail está desativado.",
dashboard: "Dashboard", dashboard: "Painel",
closet: "Armário", closet: "Armário",
laundry: "Lavandaria", laundry: "Lavandaria",
outfits: "Looks", outfits: "Outfits",
settings: "Definições", settings: "Definições",
online: "Online", online: "Online",
logout: "Sair", logout: "Sair",
overview: "Visão Geral", overview: "Visão Geral",
myCloset: "O Meu Armário", myCloset: "O Meu Armário",
outfitsAndStyle: "Looks & Estilo", outfitsAndStyle: "Outfits",
readyClothes: "Roupas Prontas", readyClothes: "Roupas Prontas",
inLaundry: "Na Lavandaria", inLaundry: "Na Lavandaria",
myLooks: "Meus Looks", myLooks: "Meus Looks",
@@ -42,13 +42,13 @@ export const translations = {
laundryMsg: "Aqui encontras as peças que marcaste como sujas. Lava-as para que voltem ao armário principal.", laundryMsg: "Aqui encontras as peças que marcaste como sujas. Lava-as para que voltem ao armário principal.",
washing: "A lavar", washing: "A lavar",
emptyBasket: "Cesto Vazio", emptyBasket: "Cesto Vazio",
createNewLook: "Criar Novo Look", createNewLook: "Criar Novo Outfit",
lookName: "Nome do Look", lookName: "Nome do Outfit",
selectedPieces: "Peças Selecionadas", selectedPieces: "Peças Selecionadas",
selectPieces: "Seleciona peças...", selectPieces: "Seleciona peças...",
saveLook: "Guardar Look", saveLook: "Guardar Outfit",
closetLabel: "Armário", closetLabel: "Armário",
lookHistory: "Histórico de Looks", lookHistory: "Histórico de Outfits",
pieces: "Peças", pieces: "Peças",
newItem: "Novo Item", newItem: "Novo Item",
preview: "Preview", preview: "Preview",