novo botao

This commit is contained in:
2026-05-06 11:02:38 +01:00
parent b7824ac999
commit c37fdd321a
2 changed files with 243 additions and 64 deletions

View File

@@ -90,6 +90,7 @@ export default function App() {
const [plannerMode, setPlannerMode] = useState('month');
const [plannerCurrentDate, setPlannerCurrentDate] = useState(new Date());
const [outfitPlans, setOutfitPlans] = useState([]);
const [showDailyOutfitModal, setShowDailyOutfitModal] = useState(false);
const [showPlannerPicker, setShowPlannerPicker] = useState(false);
const [plannerPickerDate, setPlannerPickerDate] = useState(null);
@@ -407,13 +408,42 @@ export default function App() {
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() });
const existingPlan = outfitPlans.find(p => p.date === dateStr);
let currentLookIds = [];
if (existingPlan) {
currentLookIds = existingPlan.lookIds || (existingPlan.lookId ? [existingPlan.lookId] : []);
}
if (lookId === null) {
await deleteDoc(planRef);
return;
}
if (currentLookIds.includes(lookId)) {
currentLookIds = currentLookIds.filter(id => id !== lookId);
} else {
currentLookIds = [...currentLookIds, lookId];
}
if (currentLookIds.length > 0) {
await setDoc(planRef, { date: dateStr, lookIds: currentLookIds, updatedAt: new Date().getTime() }, { merge: true });
} else {
await deleteDoc(planRef);
}
};
const todayObj = new Date();
todayObj.setHours(0, 0, 0, 0);
const todayStrGlobal = `${todayObj.getFullYear()}-${String(todayObj.getMonth()+1).padStart(2,'0')}-${String(todayObj.getDate()).padStart(2,'0')}`;
const getLooksForDayGlobal = (ds) => {
const p = outfitPlans.find(plan => plan.date === ds);
if (!p) return [];
const ids = p.lookIds || (p.lookId ? [p.lookId] : []);
return ids.map(id => looks.find(l => l.id === id)).filter(Boolean);
};
const dailyLooks = getLooksForDayGlobal(todayStrGlobal);
const baseClothes = view === 'wishlist' ? wishlistClothes : activeClothes;
const availableColors = useMemo(() => {
@@ -1048,6 +1078,9 @@ export default function App() {
</div>
<div className="flex items-center gap-4">
<button onClick={() => setShowDailyOutfitModal(true)} className="p-3 bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-200 rounded-2xl hover:scale-105 active:scale-95 transition-all">
<Sparkles size={20} />
</button>
<div className="flex bg-gray-100 dark:bg-gray-800 p-1.5 rounded-2xl">
<button onClick={() => handleDarkModeToggle(false)} className={`p-2 rounded-xl ${!darkMode ? 'bg-white shadow-md text-primary-600' : 'text-gray-500'}`}><Sun size={18} /></button>
<button onClick={() => handleDarkModeToggle(true)} className={`p-2 rounded-xl ${darkMode ? 'bg-gray-900 shadow-md text-primary-400' : 'text-gray-500'}`}><Moon size={18} /></button>
@@ -1196,50 +1229,122 @@ export default function App() {
: cardSize === 'medium' ? 'grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 2xl:grid-cols-5 gap-8'
: 'grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-4 gap-10'
}>
{filteredClothes.map(item => (
<div key={item.id} className="group">
<Card className={`overflow-hidden p-0 relative border-none hover:shadow-2xl transition-all duration-500 ${cardSize === 'small' ? 'h-[180px]' : cardSize === 'medium' ? 'h-[320px]' : 'h-[480px]'}`} darkMode={darkMode}>
<img src={item.imageUrl} className="w-full h-full object-cover transition-transform duration-1000 group-hover:scale-110" alt={item.name} />
{filteredClothes.map(item => {
const styles = {
small: {
badgeClass: 'top-2 left-2 scale-[0.65] origin-top-left',
heartContClass: 'top-2 right-2',
heartBtnClass: 'p-1.5 rounded-lg',
heartIcon: 10,
overlayContClass: 'p-2 pb-[50px]',
btnPyClass: 'py-1 px-1',
btnTextClass: 'hidden',
btnIcon: 10,
btnGap: 'gap-1',
btnRadius: 'rounded-md',
infoContClass: 'bottom-2 left-2 right-2 p-2 rounded-xl',
titleClass: 'text-xs',
colorDotClass: 'w-2 h-2',
colorTextClass: 'text-[8px]',
secTextClass: 'text-[8px] px-1 py-0 rounded',
},
medium: {
badgeClass: 'top-4 left-4 scale-90 origin-top-left',
heartContClass: 'top-4 right-4',
heartBtnClass: 'p-2 rounded-xl',
heartIcon: 14,
overlayContClass: 'p-4 pb-[90px]',
btnPyClass: 'py-2 px-2',
btnTextClass: 'text-[8px]',
btnIcon: 12,
btnGap: 'gap-1.5',
btnRadius: 'rounded-xl',
infoContClass: 'bottom-4 left-4 right-4 p-4 rounded-2xl',
titleClass: 'text-sm',
colorDotClass: 'w-3 h-3',
colorTextClass: 'text-[9px]',
secTextClass: 'text-[9px] px-1.5 py-0.5 rounded-md',
},
large: {
badgeClass: 'top-6 left-6',
heartContClass: 'top-6 right-6',
heartBtnClass: 'p-3 rounded-2xl',
heartIcon: 18,
overlayContClass: 'p-6 pb-[136px]',
btnPyClass: 'py-3 px-2',
btnTextClass: 'text-[9px]',
btnIcon: 14,
btnGap: 'gap-2',
btnRadius: 'rounded-xl',
infoContClass: 'bottom-6 left-6 right-6 p-6 rounded-3xl',
titleClass: 'text-xl',
colorDotClass: 'w-4 h-4',
colorTextClass: 'text-[10px]',
secTextClass: 'text-[10px] px-2 py-0.5 rounded-md',
}
}[cardSize] || {
badgeClass: 'top-6 left-6',
heartContClass: 'top-6 right-6',
heartBtnClass: 'p-3 rounded-2xl',
heartIcon: 18,
overlayContClass: 'p-6 pb-[136px]',
btnPyClass: 'py-3 px-2',
btnTextClass: 'text-[9px]',
btnIcon: 14,
btnGap: 'gap-2',
btnRadius: 'rounded-xl',
infoContClass: 'bottom-6 left-6 right-6 p-6 rounded-3xl',
titleClass: 'text-xl',
colorDotClass: 'w-4 h-4',
colorTextClass: 'text-[10px]',
secTextClass: 'text-[10px] px-2 py-0.5 rounded-md',
};
<div className="absolute inset-0 bg-gradient-to-t from-black/80 via-transparent opacity-0 group-hover:opacity-100 transition-all duration-300 flex flex-col justify-end p-6 pb-[136px] text-white z-10 pointer-events-none">
<div className="grid grid-cols-2 gap-2 pointer-events-auto">
<button onClick={() => { setEditingItem(item); setImageUrlDraft(item.imageUrl || ''); setView('edit'); }} className="py-3 px-2 bg-white text-primary-600 rounded-xl font-black text-[9px] uppercase flex items-center justify-center gap-1.5 hover:bg-primary-50"><Edit2 size={14} /> {t('edit')}</button>
<button onClick={() => handleItemAction('laundry', item)} className="py-3 px-2 bg-blue-600 text-white rounded-xl font-black text-[9px] uppercase flex items-center justify-center gap-1.5 hover:bg-blue-700"><Droplets size={14} /> {t('makeDirty')}</button>
<button onClick={() => handleItemAction('trash', item)} className="py-3 px-2 bg-red-600/20 text-red-100 backdrop-blur-md rounded-xl font-black text-[9px] uppercase hover:bg-red-600 transition-colors col-span-2">{t('moveToTrash')}</button>
</div>
</div>
return (
<div key={item.id} className="group">
<Card className={`overflow-hidden p-0 relative border-none hover:shadow-2xl transition-all duration-500 ${cardSize === 'small' ? 'h-[180px]' : cardSize === 'medium' ? 'h-[320px]' : 'h-[480px]'}`} darkMode={darkMode}>
<img src={item.imageUrl} className="w-full h-full object-cover transition-transform duration-1000 group-hover:scale-110" alt={item.name} />
<div className="absolute top-6 left-6 z-20"><Badge>{item.category}</Badge></div>
<div className="absolute top-6 right-6 z-20 pointer-events-auto">
<button onClick={() => handleItemAction('favorite', item)} className={`p-3 rounded-2xl shadow-xl backdrop-blur-md transition-all ${item.favorite ? 'bg-rose-500 text-white' : 'bg-white/90 text-gray-400'}`}>
<Heart size={18} fill={item.favorite ? "currentColor" : "none"} />
</button>
</div>
<div className="absolute bottom-6 left-6 right-6 p-6 bg-white/95 dark:bg-gray-900/95 backdrop-blur-2xl rounded-3xl shadow-2xl transform transition-transform group-hover:-translate-y-2 z-20 pointer-events-auto">
<h4 className="text-xl font-black tracking-tighter truncate">{item.name}</h4>
<div className="flex items-center gap-3 mt-2">
<div className="flex items-center gap-1.5 border-r border-gray-200 dark:border-gray-700 pr-3">
<div className="w-4 h-4 rounded-full border border-black/10 shrink-0" style={getColorStyle(item.color)}></div>
<span className="text-[10px] font-black opacity-40 uppercase tracking-widest">{item.color}</span>
<div className={`absolute inset-0 bg-gradient-to-t from-black/80 via-transparent opacity-0 group-hover:opacity-100 transition-all duration-300 flex flex-col justify-end text-white z-10 pointer-events-none ${styles.overlayContClass}`}>
<div className={`grid grid-cols-2 ${styles.btnGap} pointer-events-auto`}>
<button onClick={() => { setEditingItem(item); setImageUrlDraft(item.imageUrl || ''); setView('edit'); }} className={`${styles.btnPyClass} bg-white text-primary-600 ${styles.btnRadius} font-black uppercase flex items-center justify-center gap-1.5 hover:bg-primary-50`}><Edit2 size={styles.btnIcon} /> <span className={styles.btnTextClass}>{t('edit')}</span></button>
<button onClick={() => handleItemAction('laundry', item)} className={`${styles.btnPyClass} bg-blue-600 text-white ${styles.btnRadius} font-black uppercase flex items-center justify-center gap-1.5 hover:bg-blue-700`}><Droplets size={styles.btnIcon} /> <span className={styles.btnTextClass}>{t('makeDirty')}</span></button>
<button onClick={() => handleItemAction('trash', item)} className={`${styles.btnPyClass} bg-red-600/20 text-red-100 backdrop-blur-md ${styles.btnRadius} font-black uppercase flex items-center justify-center gap-1.5 hover:bg-red-600 transition-colors col-span-2`}><Trash2 size={styles.btnIcon} /> <span className={styles.btnTextClass}>{t('moveToTrash')}</span></button>
</div>
{item.sections && item.sections.length > 0 && (
<div className="flex items-center gap-1 overflow-x-auto custom-scrollbar no-scrollbar">
{item.sections.map(secId => {
const sec = sections.find(s => s.id === secId);
return sec ? (
<span key={sec.id} className="text-[10px] font-bold px-2 py-0.5 rounded-md bg-gray-100 dark:bg-gray-800 text-gray-500 whitespace-nowrap">
{sec.name}
</span>
) : null;
})}
</div>
)}
</div>
</div>
</Card>
</div>
))}
<div className={`absolute z-20 ${styles.badgeClass}`}><Badge>{item.category}</Badge></div>
<div className={`absolute z-20 pointer-events-auto ${styles.heartContClass}`}>
<button onClick={() => handleItemAction('favorite', item)} className={`${styles.heartBtnClass} shadow-xl backdrop-blur-md transition-all ${item.favorite ? 'bg-rose-500 text-white' : 'bg-white/90 text-gray-400'}`}>
<Heart size={styles.heartIcon} fill={item.favorite ? "currentColor" : "none"} />
</button>
</div>
<div className={`absolute bg-white/95 dark:bg-gray-900/95 backdrop-blur-2xl shadow-2xl transform transition-transform group-hover:-translate-y-2 z-20 pointer-events-auto ${styles.infoContClass}`}>
<h4 className={`${styles.titleClass} font-black tracking-tighter truncate`}>{item.name}</h4>
<div className="flex items-center gap-2 mt-1">
<div className="flex items-center gap-1.5 border-r border-gray-200 dark:border-gray-700 pr-2">
<div className={`${styles.colorDotClass} rounded-full border border-black/10 shrink-0`} style={getColorStyle(item.color)}></div>
<span className={`${styles.colorTextClass} font-black opacity-40 uppercase tracking-widest`}>{item.color}</span>
</div>
{item.sections && item.sections.length > 0 && (
<div className="flex items-center gap-1 overflow-x-auto custom-scrollbar no-scrollbar">
{item.sections.map(secId => {
const sec = sections.find(s => s.id === secId);
return sec ? (
<span key={sec.id} className={`${styles.secTextClass} font-bold bg-gray-100 dark:bg-gray-800 text-gray-500 whitespace-nowrap`}>
{sec.name}
</span>
) : null;
})}
</div>
)}
</div>
</div>
</Card>
</div>
);
})}
</div>
</div>
)}
@@ -1516,17 +1621,11 @@ export default function App() {
{/* 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);
@@ -1566,29 +1665,33 @@ export default function App() {
const DayCell = ({ date, cur = true }) => {
const ds = fmtDate(date);
const look = getLookForDay(ds);
const isToday = ds === todayStr;
const dayLooks = getLooksForDayGlobal(ds);
const isToday = ds === todayStrGlobal;
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'}`}
className={`relative rounded-2xl overflow-hidden cursor-pointer transition-all group border-2 flex flex-col ${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' : ''}`}>
<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>
{isToday && <span className="text-[8px] font-black text-white/80 uppercase tracking-widest">{t('today')}</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} {t('piecesShort')}</p>}
{dayLooks.length > 0 ? (
<div className="px-2 pb-2 pt-1 flex-1 overflow-y-auto custom-scrollbar space-y-3">
{dayLooks.map(look => (
<div key={look.id} className="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} {t('piecesShort')}</p>}
</div>
))}
</div>
) : (
cur && <div className="absolute inset-0 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity">
@@ -2051,11 +2154,13 @@ export default function App() {
{looks.length === 0 ? (
<div className="py-12 text-center opacity-30 font-black uppercase tracking-[0.3em] text-sm">{t('noOutfitCreated')}</div>
) : looks.map(look => {
const isSelected = outfitPlans.find(p => p.date === plannerPickerDate)?.lookId === look.id;
const plan = outfitPlans.find(p => p.date === plannerPickerDate);
const selectedIds = plan ? (plan.lookIds || (plan.lookId ? [plan.lookId] : [])) : [];
const isSelected = selectedIds.includes(look.id);
return (
<button
key={look.id}
onClick={async () => { await assignOutfitToDay(plannerPickerDate, look.id); setShowPlannerPicker(false); }}
onClick={async () => { await assignOutfitToDay(plannerPickerDate, look.id); }}
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">
@@ -2420,6 +2525,65 @@ export default function App() {
</div>
</div>
)}
{/* Modal de Outfit Diário */}
{showDailyOutfitModal && (
<div className="fixed inset-0 z-[300] flex items-center justify-center bg-black/70 backdrop-blur-md p-6" onClick={() => setShowDailyOutfitModal(false)}>
<div
className={`w-full max-w-lg rounded-[2rem] shadow-2xl overflow-hidden animate-in zoom-in-95 duration-300 ${darkMode ? 'bg-gray-900' : 'bg-white'}`}
onClick={e => e.stopPropagation()}
>
<div className="relative p-8 pb-6 bg-gradient-to-br from-primary-600 to-primary-400">
<div className="absolute inset-0 opacity-20" style={{ backgroundImage: 'radial-gradient(circle at 80% 20%, white 0%, transparent 60%)' }} />
<div className="relative z-10">
<div className="flex items-center gap-3 mb-2">
<div className="p-2 bg-white/20 rounded-xl backdrop-blur-sm">
<Sparkles size={20} className="text-white" />
</div>
<span className="text-white/80 font-black uppercase text-[10px] tracking-widest">{t('dailyOutfit')}</span>
</div>
<h2 className="text-3xl font-black text-white tracking-tight">{t('today')}</h2>
</div>
</div>
<div className={`p-8 ${darkMode ? 'bg-gray-900' : 'bg-white'} max-h-[60vh] overflow-y-auto custom-scrollbar`}>
{dailyLooks.length > 0 ? (
<div className="space-y-6">
{dailyLooks.map(look => (
<div key={look.id} className={`p-4 rounded-2xl border-2 border-gray-100 dark:border-gray-800 ${darkMode ? 'bg-gray-800/50' : 'bg-gray-50'}`}>
<div className="flex items-center justify-between mb-4">
<h4 className="font-black text-lg text-inherit">{look.name}</h4>
<span className="text-[10px] font-black uppercase tracking-widest opacity-40">{look.items.length} {t('piecesShort')}</span>
</div>
<div className="flex -space-x-4 overflow-x-auto custom-scrollbar pb-2">
{look.items.map(itemId => {
const item = clothes.find(c => c.id === itemId);
return item ? (
<div key={itemId} className="w-16 h-16 rounded-xl overflow-hidden border-2 border-white dark:border-gray-700 shrink-0 shadow-sm relative group">
<img src={item.imageUrl} className="w-full h-full object-cover" alt="" />
</div>
) : null;
})}
</div>
</div>
))}
</div>
) : (
<div className="py-12 flex flex-col items-center justify-center text-center opacity-50">
<Shirt size={48} className="mb-4 text-gray-400" />
<p className="font-black text-lg text-inherit">{t('noOutfitPlanned')}</p>
<p className="text-xs mt-2 uppercase tracking-widest">{t('goToPlanning')}</p>
</div>
)}
</div>
<div className={`p-4 border-t ${darkMode ? 'border-gray-800 bg-gray-900' : 'border-gray-100 bg-gray-50'}`}>
<button onClick={() => setShowDailyOutfitModal(false)} className="w-full py-4 bg-gray-200 dark:bg-gray-800 hover:bg-gray-300 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300 rounded-2xl font-black uppercase text-[10px] tracking-widest transition-all">
{t('close')}
</button>
</div>
</div>
</div>
)}
</div>
);
}

View File

@@ -18,6 +18,9 @@ export const translations = {
outfits: "Outfits",
settings: "Definições",
online: "Online",
dailyOutfit: "Outfit Diário",
noOutfitPlanned: "Nenhum Outfit Planeado",
goToPlanning: "Vá ao planeamento para adicionar",
logout: "Sair",
overview: "Visão Geral",
myCloset: "O Meu Armário",
@@ -217,6 +220,9 @@ export const translations = {
outfits: "Outfits",
settings: "Settings",
online: "Online",
dailyOutfit: "Daily Outfit",
noOutfitPlanned: "No Outfit Planned",
goToPlanning: "Go to planning to add one",
logout: "Logout",
overview: "Overview",
myCloset: "My Closet",
@@ -416,6 +422,9 @@ export const translations = {
outfits: "Outfits",
settings: "Ajustes",
online: "En línea",
dailyOutfit: "Outfit Diario",
noOutfitPlanned: "Sin Outfit Planeado",
goToPlanning: "Ve a planificación para añadir",
logout: "Cerrar Sesión",
overview: "Visión General",
myCloset: "Mi Armario",
@@ -615,6 +624,9 @@ export const translations = {
outfits: "Tenues",
settings: "Paramètres",
online: "En ligne",
dailyOutfit: "Tenue du Jour",
noOutfitPlanned: "Aucune Tenue Prévue",
goToPlanning: "Allez dans planification pour ajouter",
logout: "Déconnexion",
overview: "Vue d'ensemble",
myCloset: "Mon Placard",
@@ -814,6 +826,9 @@ export const translations = {
outfits: "Outfits",
settings: "Einstellungen",
online: "Online",
dailyOutfit: "Tägliches Outfit",
noOutfitPlanned: "Kein Outfit Geplant",
goToPlanning: "Gehen Sie zur Planung, um eins hinzuzufügen",
logout: "Abmelden",
overview: "Übersicht",
myCloset: "Mein Schrank",