nova secção wishlist

This commit is contained in:
2026-04-22 12:41:03 +01:00
parent 92857c8d3a
commit d8a37915ab

View File

@@ -6,7 +6,7 @@ import {
Edit2, Image as ImageIcon, Check, RotateCcw, Trash,
PanelLeftClose, PanelLeftOpen, Sparkles, CloudSun,
ArrowRight, Droplets, CheckCircle2, PieChart, History,
X, Download, Bell, Globe, Filter
X, Download, Bell, Globe, Filter, ShoppingBag
} from 'lucide-react';
import {
@@ -34,6 +34,7 @@ export default function App() {
const [darkMode, setDarkMode] = useState(false);
const [searchTerm, setSearchTerm] = useState('');
const [imageUrlDraft, setImageUrlDraft] = useState('');
const [itemColors, setItemColors] = useState([]);
const [sidebarOpen, setSidebarOpen] = useState(true);
const [authMode, setAuthMode] = useState('login');
const [authError, setAuthError] = useState('');
@@ -98,6 +99,14 @@ export default function App() {
saveUserSetting('weatherAlerts', newVal);
};
useEffect(() => {
if (editingItem && editingItem.color) {
setItemColors(editingItem.color.split(',').map(c => c.trim()).filter(Boolean));
} else {
setItemColors([]);
}
}, [editingItem]);
useEffect(() => {
document.documentElement.classList.remove('theme-indigo', 'theme-rose', 'theme-emerald', 'theme-amber', 'theme-slate');
document.documentElement.classList.add(theme);
@@ -193,11 +202,15 @@ export default function App() {
const activeClothes = useMemo(() => clothes.filter(c => c.status === 'active'), [clothes]);
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 baseClothes = view === 'wishlist' ? wishlistClothes : activeClothes;
const availableColors = useMemo(() => {
const colors = new Set(activeClothes.map(c => c.color).filter(Boolean));
const colors = new Set(baseClothes.map(c => c.color).filter(Boolean));
return Array.from(colors);
}, [activeClothes]);
}, [baseClothes]);
const colorStats = useMemo(() => {
if (!activeClothes.length) return [];
@@ -222,11 +235,11 @@ export default function App() {
}, [activeClothes]);
const filteredClothes = useMemo(() => {
return activeClothes.filter(c => {
return baseClothes.filter(c => {
const matchesSearch = (c.name || "").toLowerCase().includes(searchTerm.toLowerCase()) ||
(c.color || "").toLowerCase().includes(searchTerm.toLowerCase());
const matchesCategory = categoryFilter === 'Todos' || categoryFilter === t('all') || c.category === categoryFilter;
const matchesColor = !colorFilter || c.color === colorFilter;
const matchesColor = !colorFilter || (c.color && c.color.includes(colorFilter));
let matchesAge = true;
if (ageFilter !== 'any') {
@@ -240,7 +253,7 @@ export default function App() {
return matchesSearch && matchesCategory && matchesColor && matchesAge;
});
}, [activeClothes, searchTerm, categoryFilter, colorFilter, ageFilter, t]);
}, [baseClothes, searchTerm, categoryFilter, colorFilter, ageFilter, t]);
// Ações de Itens
const handleItemAction = async (action, item) => {
@@ -260,14 +273,21 @@ export default function App() {
const saveItem = async (e) => {
e.preventDefault();
if (!user) return;
setLoading(true);
const formData = new FormData(e.target);
const colorVal = formData.get('color');
if (!colorVal || colorVal.trim() === '') {
alert('Por favor selecione pelo menos uma cor.');
return;
}
setLoading(true);
const itemData = {
name: formData.get('name'),
category: formData.get('category'),
color: formData.get('color'),
imageUrl: formData.get('imageUrl') || 'https://images.unsplash.com/photo-1521572267360-ee0c2909d518?q=80&w=500&auto=format&fit=crop',
status: 'active',
status: formData.get('isWishlist') ? 'wishlist' : (editingItem && editingItem.status === 'wishlist' ? 'active' : (editingItem ? editingItem.status : 'active')),
favorite: editingItem ? (editingItem.favorite || false) : false,
updatedAt: new Date().getTime()
};
@@ -279,7 +299,11 @@ export default function App() {
// Navegação instantânea (Optimistic UI Update)
setEditingItem(null);
setImageUrlDraft('');
setView('closet');
setCategoryFilter('Todos');
setColorFilter('');
setAgeFilter('any');
setSearchTerm('');
setView(formData.get('isWishlist') ? 'wishlist' : 'closet');
if (currentEditId) {
const docRef = doc(db, 'artifacts', appId, 'users', user.uid, 'clothes', currentEditId);
@@ -328,6 +352,21 @@ export default function App() {
await deleteDoc(docRef);
};
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);
look.items.forEach(itemId => {
const docRef = doc(db, 'artifacts', appId, 'users', user.uid, 'clothes', itemId);
batch.update(docRef, { status: 'laundry' });
});
await batch.commit();
alert(t('lookSentToLaundry') || 'Peças enviadas para a lavandaria!');
} catch (e) { console.error(e); }
finally { setLoading(false); }
};
const handleAuth = async (e) => {
e.preventDefault();
setAuthError('');
@@ -513,6 +552,7 @@ export default function App() {
{[
{ id: 'dashboard', label: t('dashboard'), icon: LayoutDashboard },
{ id: 'closet', label: t('closet'), icon: Shirt },
{ id: 'wishlist', label: t('wishlist') || 'Lista de Desejos', icon: ShoppingBag },
{ id: 'laundry', label: t('laundry'), icon: Droplets },
{ id: 'outfits', label: t('outfits'), icon: Sparkles },
{ id: 'settings', label: t('settings'), icon: Settings },
@@ -561,6 +601,7 @@ export default function App() {
<h2 className="text-3xl font-black tracking-tighter">
{view === 'dashboard' && t('overview')}
{view === 'closet' && t('myCloset')}
{view === 'wishlist' && (t('wishlist') || 'Lista de Desejos')}
{view === 'laundry' && t('laundry')}
{view === 'outfits' && t('outfitsAndStyle')}
{view === 'settings' && t('settings')}
@@ -653,8 +694,8 @@ export default function App() {
</div>
)}
{/* ARMÁRIO */}
{view === 'closet' && (
{/* ARMÁRIO & WISHLIST */}
{(view === 'closet' || view === 'wishlist') && (
<div className="space-y-10 animate-in slide-in-from-bottom-8 duration-700">
<div className="flex flex-col xl:flex-row gap-8 items-center justify-between">
<div className="relative w-full max-w-2xl">
@@ -785,9 +826,10 @@ export default function App() {
<div className="space-y-4">
<p className="text-xs font-black uppercase opacity-50 tracking-widest px-2">{t('closetLabel')}</p>
<div className="grid grid-cols-4 gap-3 max-h-96 overflow-y-auto pr-2 custom-scrollbar">
{activeClothes.map(c => (
{availableForLooks.map(c => (
<button key={c.id} onClick={() => !selectedForLook.includes(c.id) && setSelectedForLook([...selectedForLook, c.id])} className={`relative rounded-xl overflow-hidden aspect-square border-2 transition-all ${selectedForLook.includes(c.id) ? 'border-primary-600 scale-90' : 'border-transparent hover:border-primary-200'}`}>
<img src={c.imageUrl} className="w-full h-full object-cover" alt="" />
{c.status === 'wishlist' && <div className="absolute top-1 left-1 bg-yellow-500 text-white p-1 rounded-md shadow-md"><ShoppingBag size={10} /></div>}
{selectedForLook.includes(c.id) && <div className="absolute inset-0 bg-primary-600/40 flex items-center justify-center text-white"><Check size={20} /></div>}
</button>
))}
@@ -807,6 +849,7 @@ export default function App() {
</div>
<div className="flex gap-2">
<button onClick={() => { setEditingLook(look); setSelectedForLook(look.items); }} className="p-2 text-gray-300 hover:text-primary-500 transition-colors"><Edit2 size={18} /></button>
<button onClick={() => sendLookToLaundry(look)} className="p-2 text-gray-300 hover:text-blue-500 transition-colors" title="Lavar look inteiro"><Droplets size={18} /></button>
<button onClick={() => deleteLook(look.id)} className="p-2 text-gray-300 hover:text-red-500 transition-colors"><Trash size={18} /></button>
</div>
</div>
@@ -849,14 +892,38 @@ export default function App() {
<Card className="p-10 shadow-2xl" darkMode={darkMode}>
<form onSubmit={saveItem} className="space-y-8">
<Input label={t('name')} name="name" defaultValue={editingItem?.name} required />
<div className="grid grid-cols-2 gap-6">
<div className="space-y-2">
<label className="text-[10px] font-black uppercase opacity-40 tracking-widest ml-1 text-inherit">{t('category')}</label>
<select name="category" defaultValue={editingItem?.category || 'Tops'} className={`w-full p-5 rounded-2xl border-none outline-none focus:ring-4 focus:ring-primary-500/10 font-bold ${darkMode ? 'bg-gray-700 text-white' : 'bg-gray-100'}`}>
<option>{t('tops')}</option><option>{t('bottoms')}</option><option>{t('footwear')}</option><option>{t('coats')}</option><option>{t('accessories')}</option>
</select>
<div className="space-y-2">
<label className="text-[10px] font-black uppercase opacity-40 tracking-widest ml-1 text-inherit">{t('category')}</label>
<select name="category" defaultValue={editingItem?.category || 'Tops'} className={`w-full p-5 rounded-2xl border-none outline-none focus:ring-4 focus:ring-primary-500/10 font-bold ${darkMode ? 'bg-gray-700 text-white' : 'bg-gray-100'}`}>
<option>{t('tops')}</option><option>{t('bottoms')}</option><option>{t('footwear')}</option><option>{t('coats')}</option><option>{t('accessories')}</option>
</select>
</div>
<label className="flex items-center gap-3 p-4 rounded-xl border border-gray-100 dark:border-gray-800 cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors">
<input type="checkbox" name="isWishlist" defaultChecked={editingItem?.status === 'wishlist'} className="w-5 h-5 text-primary-600 focus:ring-primary-500 rounded-lg" />
<div>
<span className="font-bold text-sm text-inherit">{t('wishlist') || 'Lista de Desejos'}</span>
<p className="text-[10px] uppercase tracking-widest opacity-50">Adicionar peça como compra futura</p>
</div>
<Input label={t('color')} name="color" defaultValue={editingItem?.color} required />
</label>
<div className="space-y-2">
<label className="text-[10px] font-black uppercase opacity-40 tracking-widest ml-1 text-inherit">{t('color')} *</label>
<div className="flex flex-wrap gap-2">
{['Vermelho', 'Azul', 'Amarelo', 'Verde', 'Laranja', 'Roxo', 'Branco', 'Preto', 'Cinzento', 'Bege'].map(c => (
<button
key={c}
type="button"
onClick={() => {
if (itemColors.includes(c)) setItemColors(itemColors.filter(color => color !== c));
else setItemColors([...itemColors, c]);
}}
className={`px-4 py-2 rounded-xl text-xs font-bold transition-all border-2 ${itemColors.includes(c) ? 'border-primary-600 bg-primary-600 text-white shadow-lg 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'}`}
>
{c}
</button>
))}
</div>
<input type="hidden" name="color" value={itemColors.join(', ')} />
{itemColors.length === 0 && <p className="text-[10px] text-red-500 uppercase tracking-widest font-black mt-2">Selecione pelo menos uma cor</p>}
</div>
<Input label={t('imageUrl')} name="imageUrl" defaultValue={editingItem?.imageUrl} onChange={(v) => setImageUrlDraft(v)} />