notificacoes, seccoes feito completo e botao lavandaria feito

This commit is contained in:
2026-04-28 17:11:43 +01:00
parent 7fa2eee3c7
commit e377258671
5 changed files with 255 additions and 38 deletions

View File

@@ -74,6 +74,14 @@ export default function App() {
const [newSectionName, setNewSectionName] = useState(''); const [newSectionName, setNewSectionName] = useState('');
const [newSectionEmoji, setNewSectionEmoji] = useState(''); const [newSectionEmoji, setNewSectionEmoji] = useState('');
const [itemSections, setItemSections] = useState([]); const [itemSections, setItemSections] = useState([]);
const [lookSections, setLookSections] = useState([]);
const [editingSectionId, setEditingSectionId] = useState(null);
const [editSectionName, setEditSectionName] = useState('');
const [editSectionEmoji, setEditSectionEmoji] = useState('');
const [notifications, setNotifications] = useState([]);
const [showNotificationsModal, setShowNotificationsModal] = useState(false);
const [toastMessage, setToastMessage] = useState(null);
const t = (key) => translations[language]?.[key] || translations['PT'][key] || key; const t = (key) => translations[language]?.[key] || translations['PT'][key] || key;
@@ -163,6 +171,10 @@ export default function App() {
setItemSections(editingItem?.sections || []); setItemSections(editingItem?.sections || []);
}, [editingItem]); }, [editingItem]);
useEffect(() => {
setLookSections(editingLook?.sections || []);
}, [editingLook]);
useEffect(() => { useEffect(() => {
document.documentElement.classList.remove('theme-indigo', 'theme-rose', 'theme-emerald', 'theme-amber', 'theme-slate'); document.documentElement.classList.remove('theme-indigo', 'theme-rose', 'theme-emerald', 'theme-amber', 'theme-slate');
// Ecrã de login/registo: sempre theme-indigo, independentemente das preferências do utilizador // Ecrã de login/registo: sempre theme-indigo, independentemente das preferências do utilizador
@@ -253,7 +265,13 @@ export default function App() {
else setUserProfile({}); else setUserProfile({});
}, (err) => console.error(err)); }, (err) => console.error(err));
return () => { unsubClothes(); unsubLooks(); unsubSections(); unsubProfile(); }; // Notificações
const notifCol = collection(db, 'artifacts', appId, 'users', user.uid, 'notifications');
const unsubNotif = onSnapshot(notifCol, (snap) => {
setNotifications(snap.docs.map(d => ({ id: d.id, ...d.data() })).sort((a, b) => b.createdAt - a.createdAt));
}, (err) => console.error(err));
return () => { unsubClothes(); unsubLooks(); unsubSections(); unsubProfile(); unsubNotif(); };
}, [user]); }, [user]);
// Fetch Weather Data // Fetch Weather Data
@@ -293,9 +311,25 @@ export default function App() {
const laundryClothes = useMemo(() => clothes.filter(c => c.status === 'laundry'), [clothes]); const laundryClothes = useMemo(() => clothes.filter(c => c.status === 'laundry'), [clothes]);
const trashClothes = useMemo(() => clothes.filter(c => c.status === 'trash'), [clothes]); const trashClothes = useMemo(() => clothes.filter(c => c.status === 'trash'), [clothes]);
const wishlistClothes = useMemo(() => clothes.filter(c => c.status === 'wishlist'), [clothes]); const wishlistClothes = useMemo(() => clothes.filter(c => c.status === 'wishlist'), [clothes]);
const availableForLooks = useMemo(() => clothes.filter(c => c.status === 'active' || c.status === 'wishlist'), [clothes]); const availableForLooks = useMemo(() => clothes.filter(c => {
const isAvailable = c.status !== 'trash';
const matchesSection = activeSectionFilter === 'all' || (c.sections && c.sections.includes(activeSectionFilter));
return isAvailable && matchesSection;
}), [clothes, activeSectionFilter]);
// CRUD de Secções // CRUD de Secções
const updateSection = async () => {
if (!editSectionName.trim() || !user || !editingSectionId) return;
const docRef = doc(db, 'artifacts', appId, 'users', user.uid, 'sections', editingSectionId);
await updateDoc(docRef, {
name: editSectionName.trim(),
emoji: editSectionEmoji.trim() || '💼'
});
setEditingSectionId(null);
setEditSectionName('');
setEditSectionEmoji('');
};
const saveSection = async () => { const saveSection = async () => {
if (!newSectionName.trim() || !user) return; if (!newSectionName.trim() || !user) return;
const sectionsCol = collection(db, 'artifacts', appId, 'users', user.uid, 'sections'); const sectionsCol = collection(db, 'artifacts', appId, 'users', user.uid, 'sections');
@@ -320,6 +354,12 @@ export default function App() {
batch.update(itemRef, { sections: item.sections.filter(s => s !== id) }); batch.update(itemRef, { sections: item.sections.filter(s => s !== id) });
} }
}); });
looks.forEach(look => {
if (look.sections && look.sections.includes(id)) {
const lookRef = doc(db, 'artifacts', appId, 'users', user.uid, 'looks', look.id);
batch.update(lookRef, { sections: look.sections.filter(s => s !== id) });
}
});
await batch.commit(); await batch.commit();
if (activeSectionFilter === id) setActiveSectionFilter('all'); if (activeSectionFilter === id) setActiveSectionFilter('all');
}; };
@@ -449,6 +489,7 @@ export default function App() {
const lookData = { const lookData = {
name: fd.get('lookName'), name: fd.get('lookName'),
items: selectedForLook, items: selectedForLook,
sections: lookSections,
updatedAt: new Date().getTime() updatedAt: new Date().getTime()
}; };
try { try {
@@ -533,6 +574,19 @@ export default function App() {
createdAt: new Date().getTime(), createdAt: new Date().getTime(),
updatedAt: new Date().getTime(), updatedAt: new Date().getTime(),
}); });
// Notify the owner
if (sharedLookData.ownerUid && sharedLookData.ownerUid !== user.uid) {
const notificationsCol = collection(db, 'artifacts', appId, 'users', sharedLookData.ownerUid, 'notifications');
await addDoc(notificationsCol, {
type: 'look_copied',
lookName: sharedLookData.lookName,
copiedByEmail: userProfile?.username || user.email || 'Alguém',
createdAt: new Date().getTime(),
read: false
});
}
setShowSharedLookModal(false); setShowSharedLookModal(false);
setSharedLookData(null); setSharedLookData(null);
setView('outfits'); setView('outfits');
@@ -545,7 +599,6 @@ export default function App() {
}; };
const sendLookToLaundry = async (look) => { const sendLookToLaundry = async (look) => {
if (!window.confirm(t('confirmSendLookToLaundry') || 'Enviar todas as peças deste look para a lavandaria?')) return;
setLoading(true); setLoading(true);
try { try {
const batch = writeBatch(db); const batch = writeBatch(db);
@@ -554,7 +607,8 @@ export default function App() {
batch.update(docRef, { status: 'laundry' }); batch.update(docRef, { status: 'laundry' });
}); });
await batch.commit(); await batch.commit();
alert(t('lookSentToLaundry') || 'Peças enviadas para a lavandaria!'); setToastMessage(t('lookSentToLaundry') || 'Peças enviadas para a lavandaria!');
setTimeout(() => setToastMessage(null), 3000);
} catch (e) { console.error(e); } } catch (e) { console.error(e); }
finally { setLoading(false); } finally { setLoading(false); }
}; };
@@ -810,6 +864,12 @@ export default function App() {
<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(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> <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>
</div> </div>
<button onClick={() => setShowNotificationsModal(true)} className="relative p-4 bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-200 rounded-2xl hover:scale-105 active:scale-95 transition-all">
<Bell size={24} />
{notifications.filter(n => !n.read).length > 0 && (
<span className="absolute top-2 right-2 w-3 h-3 bg-red-500 rounded-full border-2 border-white dark:border-gray-800"></span>
)}
</button>
<button onClick={() => { setEditingItem(null); setImageUrlDraft(''); setView('add'); setEditingLook(null); setSelectedForLook([]); }} className="p-4 bg-primary-600 text-white rounded-2xl shadow-xl shadow-primary-600/30 hover:scale-105 active:scale-95 transition-all"> <button onClick={() => { setEditingItem(null); setImageUrlDraft(''); setView('add'); setEditingLook(null); setSelectedForLook([]); }} className="p-4 bg-primary-600 text-white rounded-2xl shadow-xl shadow-primary-600/30 hover:scale-105 active:scale-95 transition-all">
<Plus size={24} /> <Plus size={24} />
</button> </button>
@@ -917,7 +977,7 @@ export default function App() {
</div> </div>
{/* Barra de Secções */} {/* Barra de Secções */}
{view === 'closet' && ( {(view === 'closet' || view === 'outfits') && (
<div className="flex items-center gap-3 overflow-x-auto pb-1 custom-scrollbar"> <div className="flex items-center gap-3 overflow-x-auto pb-1 custom-scrollbar">
<button <button
onClick={() => setActiveSectionFilter('all')} onClick={() => setActiveSectionFilter('all')}
@@ -967,8 +1027,22 @@ export default function App() {
<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"> <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> <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-3 mt-2">
<div className="w-4 h-4 rounded-full border border-black/10 shrink-0" style={getColorStyle(item.color)}></div> <div className="flex items-center gap-1.5 border-r border-gray-200 dark:border-gray-700 pr-3">
<span className="text-[10px] font-black opacity-40 uppercase tracking-widest">{item.color}</span> <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>
{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.emoji} {sec.name}
</span>
) : null;
})}
</div>
)}
</div> </div>
</div> </div>
</Card> </Card>
@@ -991,14 +1065,16 @@ export default function App() {
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-8"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-8">
{laundryClothes.map(item => ( {laundryClothes.map(item => (
<Card key={item.id} className="p-6 flex items-center gap-6 border-blue-100" darkMode={darkMode}> <Card key={item.id} className="p-4 flex items-center gap-4 border-blue-200 dark:border-blue-900/50" darkMode={darkMode}>
<img src={item.imageUrl} className="w-20 h-20 rounded-2xl object-cover shadow-lg" alt="" /> <img src={item.imageUrl} className="w-16 h-16 rounded-2xl object-cover shadow-sm shrink-0" alt="" />
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0 flex flex-col justify-center items-start">
<p className="font-black truncate">{item.name}</p> <p className="font-black text-sm truncate w-full text-inherit">{item.name}</p>
<Badge variant="warning">{t('washing')}</Badge> <div className="mt-1.5">
<Badge variant="warning">{t('washing')}</Badge>
</div>
</div> </div>
<button onClick={() => handleItemAction('clean', item)} className="p-4 bg-green-500 text-white rounded-2xl shadow-lg shadow-green-500/30 hover:scale-110 transition-all"> <button onClick={() => handleItemAction('clean', item)} className="p-3 bg-green-500 text-white rounded-xl shadow-md hover:scale-105 transition-all shrink-0">
<CheckCircle2 size={24} /> <CheckCircle2 size={20} />
</button> </button>
</Card> </Card>
))} ))}
@@ -1035,6 +1111,40 @@ export default function App() {
{selectedForLook.length === 0 && <p className="text-xs text-gray-400 italic">{t('selectPieces')}</p>} {selectedForLook.length === 0 && <p className="text-xs text-gray-400 italic">{t('selectPieces')}</p>}
</div> </div>
</div> </div>
<div className="space-y-3 pt-3 border-t border-inherit">
<div className="flex items-center justify-between">
<label className="text-[10px] font-black uppercase tracking-widest opacity-50 flex items-center gap-2">
<Tag size={12} /> {t('assignSections')}
</label>
<button type="button" onClick={() => setShowSectionManager(true)} className="text-[10px] font-black uppercase tracking-widest text-primary-600 hover:text-primary-700 flex items-center gap-1">
<Plus size={10} /> Criar Secção
</button>
</div>
{sections.length === 0 ? (
<div className="p-4 border-2 border-dashed border-gray-200 dark:border-gray-800 rounded-2xl text-center">
<p className="text-[10px] font-black uppercase tracking-widest opacity-40">Ainda não tem secções criadas</p>
</div>
) : (
<div className="flex flex-wrap gap-2">
{sections.map(sec => (
<button
key={sec.id}
type="button"
onClick={() => {
if (lookSections.includes(sec.id))
setLookSections(lookSections.filter(s => s !== sec.id));
else
setLookSections([...lookSections, sec.id]);
}}
className={`flex items-center gap-2 px-3 py-1.5 rounded-xl text-xs font-bold transition-all border-2 ${lookSections.includes(sec.id) ? 'border-primary-600 bg-primary-600 text-white shadow-md shadow-primary-600/30' : 'border-transparent bg-gray-100 dark:bg-gray-800 text-gray-500 hover:bg-gray-200 dark:hover:bg-gray-700'}`}
>
<span>{sec.emoji}</span> {sec.name}
{lookSections.includes(sec.id) && <Check size={12} />}
</button>
))}
</div>
)}
</div>
<div className="flex gap-4"> <div className="flex gap-4">
{editingLook && ( {editingLook && (
<button type="button" onClick={() => { setEditingLook(null); setSelectedForLook([]); }} className="flex-1 py-4 font-black uppercase text-[10px] tracking-widest text-gray-500 hover:text-gray-900 transition-colors">{t('cancel')}</button> <button type="button" onClick={() => { setEditingLook(null); setSelectedForLook([]); }} className="flex-1 py-4 font-black uppercase text-[10px] tracking-widest text-gray-500 hover:text-gray-900 transition-colors">{t('cancel')}</button>
@@ -1062,13 +1172,17 @@ export default function App() {
<div className="lg:col-span-2 space-y-10"> <div className="lg:col-span-2 space-y-10">
{(() => { {(() => {
const availableLooks = looks.filter(look => const filteredBySectionLooks = looks.filter(look =>
activeSectionFilter === 'all' || (look.sections && look.sections.includes(activeSectionFilter))
);
const availableLooks = filteredBySectionLooks.filter(look =>
look.items.every(id => { look.items.every(id => {
const item = clothes.find(c => c.id === id); const item = clothes.find(c => c.id === id);
return !item || item.status !== 'laundry'; return !item || item.status !== 'laundry';
}) })
); );
const laundryLooks = looks.filter(look => const laundryLooks = filteredBySectionLooks.filter(look =>
look.items.some(id => { look.items.some(id => {
const item = clothes.find(c => c.id === id); const item = clothes.find(c => c.id === id);
return item && item.status === 'laundry'; return item && item.status === 'laundry';
@@ -1225,11 +1339,20 @@ export default function App() {
<Input label={t('imageUrl')} name="imageUrl" defaultValue={editingItem?.imageUrl} onChange={(v) => setImageUrlDraft(v)} /> <Input label={t('imageUrl')} name="imageUrl" defaultValue={editingItem?.imageUrl} onChange={(v) => setImageUrlDraft(v)} />
{/* Campo de Secções */} {/* Campo de Secções */}
{sections.length > 0 && ( <div className="space-y-3">
<div className="space-y-3"> <div className="flex items-center justify-between">
<label className="text-[10px] font-black uppercase opacity-40 tracking-widest ml-1 text-inherit flex items-center gap-2"> <label className="text-[10px] font-black uppercase opacity-40 tracking-widest ml-1 text-inherit flex items-center gap-2">
<Tag size={12} /> {t('assignSections')} <Tag size={12} /> {t('assignSections')}
</label> </label>
<button type="button" onClick={() => setShowSectionManager(true)} className="text-[10px] font-black uppercase tracking-widest text-primary-600 hover:text-primary-700 flex items-center gap-1">
<Plus size={10} /> Criar Secção
</button>
</div>
{sections.length === 0 ? (
<div className="p-4 border-2 border-dashed border-gray-200 dark:border-gray-800 rounded-2xl text-center">
<p className="text-[10px] font-black uppercase tracking-widest opacity-40">Ainda não tem secções criadas</p>
</div>
) : (
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
{sections.map(sec => ( {sections.map(sec => (
<button <button
@@ -1248,8 +1371,8 @@ export default function App() {
</button> </button>
))} ))}
</div> </div>
</div> )}
)} </div>
<div className="flex gap-4 pt-6"> <div className="flex gap-4 pt-6">
<button type="button" onClick={() => { setEditingItem(null); setImageUrlDraft(''); setView('closet'); }} className="flex-1 font-black uppercase text-[10px] opacity-40 hover:opacity-100 tracking-widest transition-all text-inherit">{t('cancel')}</button> <button type="button" onClick={() => { setEditingItem(null); setImageUrlDraft(''); setView('closet'); }} className="flex-1 font-black uppercase text-[10px] opacity-40 hover:opacity-100 tracking-widest transition-all text-inherit">{t('cancel')}</button>
@@ -1468,6 +1591,60 @@ export default function App() {
</div> </div>
</main> </main>
{/* 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">
<div className="bg-gray-900 text-white px-6 py-3 rounded-full shadow-2xl font-bold text-sm tracking-wide flex items-center gap-3">
<CheckCircle2 size={18} className="text-green-400" />
{toastMessage}
</div>
</div>
)}
{/* Modal de Notificações */}
{showNotificationsModal && (
<div className="fixed inset-0 z-[200] flex items-center justify-center bg-black/60 backdrop-blur-sm p-6" onClick={() => setShowNotificationsModal(false)}>
<Card className="w-full max-w-md 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-8">
<h3 className="text-2xl font-black text-inherit flex items-center gap-3"><Bell size={24} className="text-primary-600" /> Notificações</h3>
<button onClick={() => setShowNotificationsModal(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>
<div className="flex-1 overflow-y-auto space-y-4 custom-scrollbar">
{notifications.length === 0 ? (
<div className="py-12 text-center opacity-30 font-black uppercase tracking-[0.3em] text-sm">Sem Notificações</div>
) : notifications.map(notif => (
<div key={notif.id} className={`p-4 rounded-2xl flex items-start gap-4 ${!notif.read ? 'bg-primary-50 dark:bg-primary-900/20' : 'bg-gray-50 dark:bg-gray-800'}`}>
<div className={`p-3 rounded-full ${!notif.read ? 'bg-primary-100 text-primary-600 dark:bg-primary-900/40 dark:text-primary-400' : 'bg-gray-200 text-gray-500 dark:bg-gray-700'}`}>
<Bell size={20} />
</div>
<div className="flex-1">
<p className="font-bold text-sm text-inherit">
{notif.type === 'look_copied' && (
<>O utilizador <span className="text-primary-600">{notif.copiedByEmail}</span> guardou o seu look "{notif.lookName}" no armário dele!</>
)}
</p>
<p className="text-[10px] uppercase font-black tracking-widest opacity-40 mt-2">
{new Date(notif.createdAt).toLocaleDateString()} às {new Date(notif.createdAt).toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'})}
</p>
</div>
{!notif.read && (
<button
onClick={async () => {
const docRef = doc(db, 'artifacts', appId, 'users', user.uid, 'notifications', notif.id);
await updateDoc(docRef, { read: true });
}}
className="p-2 text-primary-600 hover:bg-primary-100 dark:hover:bg-primary-900/40 rounded-xl"
>
<Check size={16} />
</button>
)}
</div>
))}
</div>
</Card>
</div>
)}
{/* Modal de Gestão de Secções */} {/* Modal de Gestão de Secções */}
{showSectionManager && ( {showSectionManager && (
<div className="fixed inset-0 z-[200] flex items-center justify-center bg-black/60 backdrop-blur-sm p-6" onClick={() => setShowSectionManager(false)}> <div className="fixed inset-0 z-[200] flex items-center justify-center bg-black/60 backdrop-blur-sm p-6" onClick={() => setShowSectionManager(false)}>
@@ -1510,19 +1687,50 @@ export default function App() {
<div className="py-12 text-center opacity-30 font-black uppercase tracking-[0.3em] text-sm">{t('noSections')}</div> <div className="py-12 text-center opacity-30 font-black uppercase tracking-[0.3em] text-sm">{t('noSections')}</div>
) : sections.map(sec => ( ) : sections.map(sec => (
<div key={sec.id} className={`flex items-center gap-4 p-4 rounded-2xl transition-all ${darkMode ? 'bg-gray-800' : 'bg-gray-50'}`}> <div key={sec.id} className={`flex items-center gap-4 p-4 rounded-2xl transition-all ${darkMode ? 'bg-gray-800' : 'bg-gray-50'}`}>
<span className="text-2xl w-10 text-center">{sec.emoji}</span> {editingSectionId === sec.id ? (
<div className="flex-1"> <>
<p className="font-black text-sm">{sec.name}</p> <input
<p className="text-[10px] opacity-40 font-bold uppercase tracking-widest"> value={editSectionEmoji}
{clothes.filter(c => c.sections && c.sections.includes(sec.id)).length} peça(s) onChange={e => setEditSectionEmoji(e.target.value)}
</p> maxLength={2}
</div> className={`w-12 p-2 rounded-xl border-none outline-none font-bold text-center text-lg ${darkMode ? 'bg-gray-700 text-white' : 'bg-white'} shadow-sm`}
<button />
onClick={() => deleteSection(sec.id)} <input
className="p-2 text-red-400 hover:text-red-600 hover:bg-red-50 dark:hover:bg-red-900/20 rounded-xl transition-all" value={editSectionName}
> onChange={e => setEditSectionName(e.target.value)}
<Trash size={16} /> onKeyDown={e => e.key === 'Enter' && updateSection()}
</button> className={`flex-1 p-2 rounded-xl border-none outline-none font-bold text-sm ${darkMode ? 'bg-gray-700 text-white' : 'bg-white'} shadow-sm`}
/>
<button onClick={updateSection} disabled={!editSectionName.trim()} className="p-2 bg-green-500 text-white rounded-xl shadow-md hover:scale-105 disabled:opacity-30"><Check size={16} /></button>
<button onClick={() => setEditingSectionId(null)} className="p-2 bg-gray-200 dark:bg-gray-700 text-gray-500 dark:text-gray-400 rounded-xl hover:scale-105"><X size={16} /></button>
</>
) : (
<>
<span className="text-2xl w-10 text-center">{sec.emoji}</span>
<div className="flex-1 min-w-0">
<p className="font-black text-sm truncate">{sec.name}</p>
<p className="text-[10px] opacity-40 font-bold uppercase tracking-widest">
{clothes.filter(c => c.sections && c.sections.includes(sec.id)).length} {t('pieces')} {looks.filter(l => l.sections && l.sections.includes(sec.id)).length} look(s)
</p>
</div>
<button
onClick={() => {
setEditingSectionId(sec.id);
setEditSectionName(sec.name);
setEditSectionEmoji(sec.emoji);
}}
className="p-2 text-gray-400 hover:text-primary-600 hover:bg-primary-50 dark:hover:bg-primary-900/20 rounded-xl transition-all"
>
<Edit2 size={16} />
</button>
<button
onClick={() => deleteSection(sec.id)}
className="p-2 text-red-400 hover:text-red-600 hover:bg-red-50 dark:hover:bg-red-900/20 rounded-xl transition-all"
>
<Trash size={16} />
</button>
</>
)}
</div> </div>
))} ))}
</div> </div>

View File

@@ -8,7 +8,7 @@ export const Badge = ({ children, variant = "default" }) => {
warning: "bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-300" warning: "bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-300"
}; };
return ( return (
<span className={`px-3 py-1 rounded-full text-[10px] font-black uppercase tracking-widest ${styles[variant]}`}> <span className={`px-3 py-1 rounded-full text-[10px] font-black uppercase tracking-widest whitespace-nowrap ${styles[variant]}`}>
{children} {children}
</span> </span>
); );

View File

@@ -1,10 +1,13 @@
import React from 'react'; import React from 'react';
export const Card = ({ children, className = "", darkMode }) => ( export const Card = ({ children, className = "", darkMode, ...props }) => (
<div className={`rounded-[2rem] border transition-all duration-300 ${darkMode <div
className={`rounded-[2rem] border transition-all duration-300 ${darkMode
? 'bg-gray-800/40 border-gray-700/50 backdrop-blur-md' ? 'bg-gray-800/40 border-gray-700/50 backdrop-blur-md'
: 'bg-white/80 border-gray-200/50 backdrop-blur-md shadow-sm' : 'bg-white/80 border-gray-200/50 backdrop-blur-md shadow-sm'
} ${className}`}> } ${className}`}
{...props}
>
{children} {children}
</div> </div>
); );

View File

@@ -38,6 +38,7 @@ export const translations = {
makeDirty: "Sujar", makeDirty: "Sujar",
moveToTrash: "Mover para Lixo", moveToTrash: "Mover para Lixo",
laundryBasket: "Cesto da Roupa", laundryBasket: "Cesto da Roupa",
lookSentToLaundry: "Peças enviadas para a lavandaria!",
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",
@@ -171,6 +172,7 @@ export const translations = {
makeDirty: "Make Dirty", makeDirty: "Make Dirty",
moveToTrash: "Move to Trash", moveToTrash: "Move to Trash",
laundryBasket: "Laundry Basket", laundryBasket: "Laundry Basket",
lookSentToLaundry: "Pieces sent to the laundry!",
laundryMsg: "Here you find the pieces you marked as dirty. Wash them to return them to the main closet.", laundryMsg: "Here you find the pieces you marked as dirty. Wash them to return them to the main closet.",
washing: "Washing", washing: "Washing",
emptyBasket: "Empty Basket", emptyBasket: "Empty Basket",
@@ -304,6 +306,7 @@ export const translations = {
makeDirty: "Ensuciar", makeDirty: "Ensuciar",
moveToTrash: "Mover a la Papelera", moveToTrash: "Mover a la Papelera",
laundryBasket: "Cesto de Ropa", laundryBasket: "Cesto de Ropa",
lookSentToLaundry: "¡Piezas enviadas a la lavandería!",
laundryMsg: "Aquí encuentras las piezas que marcaste como sucias. Lávalas para que vuelvan al armario principal.", laundryMsg: "Aquí encuentras las piezas que marcaste como sucias. Lávalas para que vuelvan al armario principal.",
washing: "Lavando", washing: "Lavando",
emptyBasket: "Cesto Vacío", emptyBasket: "Cesto Vacío",
@@ -437,6 +440,7 @@ export const translations = {
makeDirty: "Salir", makeDirty: "Salir",
moveToTrash: "Mettre à la corbeille", moveToTrash: "Mettre à la corbeille",
laundryBasket: "Panier à linge", laundryBasket: "Panier à linge",
lookSentToLaundry: "Pièces envoyées à la blanchisserie !",
laundryMsg: "Ici vous trouvez les pièces que vous avez marquées comme sales. Lavez-les pour les remettre dans le placard principal.", laundryMsg: "Ici vous trouvez les pièces que vous avez marquées comme sales. Lavez-les pour les remettre dans le placard principal.",
washing: "En lavage", washing: "En lavage",
emptyBasket: "Panier Vide", emptyBasket: "Panier Vide",
@@ -570,6 +574,7 @@ export const translations = {
makeDirty: "Schmutzig machen", makeDirty: "Schmutzig machen",
moveToTrash: "In den Papierkorb verschieben", moveToTrash: "In den Papierkorb verschieben",
laundryBasket: "Wäschekorb", laundryBasket: "Wäschekorb",
lookSentToLaundry: "Stücke in die Wäsche geschickt!",
laundryMsg: "Hier findest du die Stücke, die du als schmutzig markiert hast. Wasche sie, um sie in den Hauptschrank zurückzulegen.", laundryMsg: "Hier findest du die Stücke, die du als schmutzig markiert hast. Wasche sie, um sie in den Hauptschrank zurückzulegen.",
washing: "Waschen", washing: "Waschen",
emptyBasket: "Leerer Korb", emptyBasket: "Leerer Korb",

View File

@@ -6,7 +6,8 @@ export default defineConfig({
plugins: [react()], plugins: [react()],
server: { server: {
host: true, // Permite acesso externo host: true, // Permite acesso externo
port: 5173, // Opcional, para garantir a porta port: 5173, // Garante que apenas esta porta será usada
strictPort: true, // Se a porta estiver em uso, falha em vez de abrir outra
allowedHosts: ['mycloset.epvc.pt', 'localhost'], allowedHosts: ['mycloset.epvc.pt', 'localhost'],
} }
}) })