seccao planeamento
This commit is contained in:
214
src/App.jsx
214
src/App.jsx
@@ -7,7 +7,7 @@ import {
|
||||
PanelLeftClose, PanelLeftOpen, Sparkles, CloudSun,
|
||||
ArrowRight, Droplets, CheckCircle2, PieChart, History,
|
||||
X, Download, Bell, Globe, Filter, ShoppingBag, Share2,
|
||||
FolderOpen, Tag, Link
|
||||
FolderOpen, Tag, Link, Calendar, ChevronLeft, ChevronRight
|
||||
} from 'lucide-react';
|
||||
|
||||
import {
|
||||
@@ -83,6 +83,13 @@ export default function App() {
|
||||
const [showNotificationsModal, setShowNotificationsModal] = useState(false);
|
||||
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;
|
||||
|
||||
// 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));
|
||||
}, (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
|
||||
const profileDoc = doc(db, 'artifacts', appId, 'users', user.uid, 'profile', 'data');
|
||||
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));
|
||||
}, (err) => console.error('Notif listener error:', err));
|
||||
|
||||
return () => { unsubClothes(); unsubLooks(); unsubSections(); unsubProfile(); unsubNotif(); };
|
||||
return () => { unsubClothes(); unsubLooks(); unsubSections(); unsubProfile(); unsubNotif(); unsubPlans(); };
|
||||
}, [user]);
|
||||
|
||||
// Fetch Weather Data
|
||||
@@ -382,6 +395,16 @@ export default function App() {
|
||||
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 availableColors = useMemo(() => {
|
||||
@@ -905,6 +928,7 @@ export default function App() {
|
||||
{ id: 'wishlist', label: t('wishlist') || 'Carrinho', icon: ShoppingBag },
|
||||
{ id: 'laundry', label: t('laundry'), icon: Droplets },
|
||||
{ id: 'outfits', label: t('outfits'), icon: Sparkles },
|
||||
{ id: 'planner', label: 'Planeamento', icon: Calendar },
|
||||
{ id: 'settings', label: t('settings'), icon: Settings },
|
||||
].map(item => (
|
||||
<button
|
||||
@@ -958,6 +982,7 @@ export default function App() {
|
||||
{view === 'wishlist' && (t('wishlist') || 'Carrinho')}
|
||||
{view === 'laundry' && t('laundry')}
|
||||
{view === 'outfits' && t('outfitsAndStyle')}
|
||||
{view === 'planner' && 'Planeamento'}
|
||||
{view === 'settings' && t('settings')}
|
||||
{view === 'profile' && t('profileInfo')}
|
||||
</h2>
|
||||
@@ -1426,6 +1451,132 @@ export default function App() {
|
||||
</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 */}
|
||||
{(view === 'add' || view === 'edit') && (
|
||||
<div className="max-w-4xl mx-auto animate-in zoom-in-95 duration-500">
|
||||
@@ -1782,6 +1933,65 @@ export default function App() {
|
||||
</div>
|
||||
</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 */}
|
||||
{toastMessage && (
|
||||
<div className="fixed bottom-8 left-1/2 transform -translate-x-1/2 z-[300] animate-in slide-in-from-bottom-5">
|
||||
|
||||
@@ -8,16 +8,16 @@ export const translations = {
|
||||
createAccount: "Criar Nova Conta",
|
||||
haveAccount: "Já Tenho Conta",
|
||||
authErrorDisabled: "O login por e-mail está desativado.",
|
||||
dashboard: "Dashboard",
|
||||
dashboard: "Painel",
|
||||
closet: "Armário",
|
||||
laundry: "Lavandaria",
|
||||
outfits: "Looks",
|
||||
outfits: "Outfits",
|
||||
settings: "Definições",
|
||||
online: "Online",
|
||||
logout: "Sair",
|
||||
overview: "Visão Geral",
|
||||
myCloset: "O Meu Armário",
|
||||
outfitsAndStyle: "Looks & Estilo",
|
||||
outfitsAndStyle: "Outfits",
|
||||
readyClothes: "Roupas Prontas",
|
||||
inLaundry: "Na Lavandaria",
|
||||
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.",
|
||||
washing: "A lavar",
|
||||
emptyBasket: "Cesto Vazio",
|
||||
createNewLook: "Criar Novo Look",
|
||||
lookName: "Nome do Look",
|
||||
createNewLook: "Criar Novo Outfit",
|
||||
lookName: "Nome do Outfit",
|
||||
selectedPieces: "Peças Selecionadas",
|
||||
selectPieces: "Seleciona peças...",
|
||||
saveLook: "Guardar Look",
|
||||
saveLook: "Guardar Outfit",
|
||||
closetLabel: "Armário",
|
||||
lookHistory: "Histórico de Looks",
|
||||
lookHistory: "Histórico de Outfits",
|
||||
pieces: "Peças",
|
||||
newItem: "Novo Item",
|
||||
preview: "Preview",
|
||||
|
||||
Reference in New Issue
Block a user