notificacoes, seccoes feito completo e botao lavandaria feito
This commit is contained in:
244
src/App.jsx
244
src/App.jsx
@@ -74,6 +74,14 @@ export default function App() {
|
||||
const [newSectionName, setNewSectionName] = useState('');
|
||||
const [newSectionEmoji, setNewSectionEmoji] = 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;
|
||||
|
||||
@@ -163,6 +171,10 @@ export default function App() {
|
||||
setItemSections(editingItem?.sections || []);
|
||||
}, [editingItem]);
|
||||
|
||||
useEffect(() => {
|
||||
setLookSections(editingLook?.sections || []);
|
||||
}, [editingLook]);
|
||||
|
||||
useEffect(() => {
|
||||
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
|
||||
@@ -253,7 +265,13 @@ export default function App() {
|
||||
else setUserProfile({});
|
||||
}, (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]);
|
||||
|
||||
// Fetch Weather Data
|
||||
@@ -293,9 +311,25 @@ export default function App() {
|
||||
const laundryClothes = useMemo(() => clothes.filter(c => c.status === 'laundry'), [clothes]);
|
||||
const trashClothes = useMemo(() => clothes.filter(c => c.status === 'trash'), [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
|
||||
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 () => {
|
||||
if (!newSectionName.trim() || !user) return;
|
||||
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) });
|
||||
}
|
||||
});
|
||||
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();
|
||||
if (activeSectionFilter === id) setActiveSectionFilter('all');
|
||||
};
|
||||
@@ -449,6 +489,7 @@ export default function App() {
|
||||
const lookData = {
|
||||
name: fd.get('lookName'),
|
||||
items: selectedForLook,
|
||||
sections: lookSections,
|
||||
updatedAt: new Date().getTime()
|
||||
};
|
||||
try {
|
||||
@@ -533,6 +574,19 @@ export default function App() {
|
||||
createdAt: 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);
|
||||
setSharedLookData(null);
|
||||
setView('outfits');
|
||||
@@ -545,7 +599,6 @@ export default function App() {
|
||||
};
|
||||
|
||||
const sendLookToLaundry = async (look) => {
|
||||
if (!window.confirm(t('confirmSendLookToLaundry') || 'Enviar todas as peças deste look para a lavandaria?')) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
const batch = writeBatch(db);
|
||||
@@ -554,7 +607,8 @@ export default function App() {
|
||||
batch.update(docRef, { status: 'laundry' });
|
||||
});
|
||||
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); }
|
||||
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(true)} className={`p-2 rounded-xl ${darkMode ? 'bg-gray-900 shadow-md text-primary-400' : 'text-gray-500'}`}><Moon size={18} /></button>
|
||||
</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">
|
||||
<Plus size={24} />
|
||||
</button>
|
||||
@@ -917,7 +977,7 @@ export default function App() {
|
||||
</div>
|
||||
|
||||
{/* Barra de Secções */}
|
||||
{view === 'closet' && (
|
||||
{(view === 'closet' || view === 'outfits') && (
|
||||
<div className="flex items-center gap-3 overflow-x-auto pb-1 custom-scrollbar">
|
||||
<button
|
||||
onClick={() => setActiveSectionFilter('all')}
|
||||
@@ -967,9 +1027,23 @@ 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">
|
||||
<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>
|
||||
{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>
|
||||
</Card>
|
||||
</div>
|
||||
@@ -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">
|
||||
{laundryClothes.map(item => (
|
||||
<Card key={item.id} className="p-6 flex items-center gap-6 border-blue-100" darkMode={darkMode}>
|
||||
<img src={item.imageUrl} className="w-20 h-20 rounded-2xl object-cover shadow-lg" alt="" />
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="font-black truncate">{item.name}</p>
|
||||
<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-16 h-16 rounded-2xl object-cover shadow-sm shrink-0" alt="" />
|
||||
<div className="flex-1 min-w-0 flex flex-col justify-center items-start">
|
||||
<p className="font-black text-sm truncate w-full text-inherit">{item.name}</p>
|
||||
<div className="mt-1.5">
|
||||
<Badge variant="warning">{t('washing')}</Badge>
|
||||
</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">
|
||||
<CheckCircle2 size={24} />
|
||||
</div>
|
||||
<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={20} />
|
||||
</button>
|
||||
</Card>
|
||||
))}
|
||||
@@ -1035,6 +1111,40 @@ export default function App() {
|
||||
{selectedForLook.length === 0 && <p className="text-xs text-gray-400 italic">{t('selectPieces')}</p>}
|
||||
</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">
|
||||
{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>
|
||||
@@ -1062,13 +1172,17 @@ export default function App() {
|
||||
|
||||
<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 => {
|
||||
const item = clothes.find(c => c.id === id);
|
||||
return !item || item.status !== 'laundry';
|
||||
})
|
||||
);
|
||||
const laundryLooks = looks.filter(look =>
|
||||
const laundryLooks = filteredBySectionLooks.filter(look =>
|
||||
look.items.some(id => {
|
||||
const item = clothes.find(c => c.id === id);
|
||||
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)} />
|
||||
|
||||
{/* Campo de Secções */}
|
||||
{sections.length > 0 && (
|
||||
<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">
|
||||
<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
|
||||
@@ -1248,8 +1371,8 @@ export default function App() {
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<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>
|
||||
@@ -1468,6 +1591,60 @@ export default function App() {
|
||||
</div>
|
||||
</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 */}
|
||||
{showSectionManager && (
|
||||
<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>
|
||||
) : 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'}`}>
|
||||
{editingSectionId === sec.id ? (
|
||||
<>
|
||||
<input
|
||||
value={editSectionEmoji}
|
||||
onChange={e => setEditSectionEmoji(e.target.value)}
|
||||
maxLength={2}
|
||||
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`}
|
||||
/>
|
||||
<input
|
||||
value={editSectionName}
|
||||
onChange={e => setEditSectionName(e.target.value)}
|
||||
onKeyDown={e => e.key === 'Enter' && updateSection()}
|
||||
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">
|
||||
<p className="font-black text-sm">{sec.name}</p>
|
||||
<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} peça(s)
|
||||
{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>
|
||||
|
||||
@@ -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"
|
||||
};
|
||||
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}
|
||||
</span>
|
||||
);
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import React from 'react';
|
||||
|
||||
export const Card = ({ children, className = "", darkMode }) => (
|
||||
<div className={`rounded-[2rem] border transition-all duration-300 ${darkMode
|
||||
export const Card = ({ children, className = "", darkMode, ...props }) => (
|
||||
<div
|
||||
className={`rounded-[2rem] border transition-all duration-300 ${darkMode
|
||||
? 'bg-gray-800/40 border-gray-700/50 backdrop-blur-md'
|
||||
: 'bg-white/80 border-gray-200/50 backdrop-blur-md shadow-sm'
|
||||
} ${className}`}>
|
||||
} ${className}`}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -38,6 +38,7 @@ export const translations = {
|
||||
makeDirty: "Sujar",
|
||||
moveToTrash: "Mover para Lixo",
|
||||
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.",
|
||||
washing: "A lavar",
|
||||
emptyBasket: "Cesto Vazio",
|
||||
@@ -171,6 +172,7 @@ export const translations = {
|
||||
makeDirty: "Make Dirty",
|
||||
moveToTrash: "Move to Trash",
|
||||
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.",
|
||||
washing: "Washing",
|
||||
emptyBasket: "Empty Basket",
|
||||
@@ -304,6 +306,7 @@ export const translations = {
|
||||
makeDirty: "Ensuciar",
|
||||
moveToTrash: "Mover a la Papelera",
|
||||
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.",
|
||||
washing: "Lavando",
|
||||
emptyBasket: "Cesto Vacío",
|
||||
@@ -437,6 +440,7 @@ export const translations = {
|
||||
makeDirty: "Salir",
|
||||
moveToTrash: "Mettre à la corbeille",
|
||||
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.",
|
||||
washing: "En lavage",
|
||||
emptyBasket: "Panier Vide",
|
||||
@@ -570,6 +574,7 @@ export const translations = {
|
||||
makeDirty: "Schmutzig machen",
|
||||
moveToTrash: "In den Papierkorb verschieben",
|
||||
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.",
|
||||
washing: "Waschen",
|
||||
emptyBasket: "Leerer Korb",
|
||||
|
||||
@@ -6,7 +6,8 @@ export default defineConfig({
|
||||
plugins: [react()],
|
||||
server: {
|
||||
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'],
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user