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, Edit2, Image as ImageIcon, Check, RotateCcw, Trash,
PanelLeftClose, PanelLeftOpen, Sparkles, CloudSun, PanelLeftClose, PanelLeftOpen, Sparkles, CloudSun,
ArrowRight, Droplets, CheckCircle2, PieChart, History, ArrowRight, Droplets, CheckCircle2, PieChart, History,
X, Download, Bell, Globe, Filter X, Download, Bell, Globe, Filter, ShoppingBag
} from 'lucide-react'; } from 'lucide-react';
import { import {
@@ -34,6 +34,7 @@ export default function App() {
const [darkMode, setDarkMode] = useState(false); const [darkMode, setDarkMode] = useState(false);
const [searchTerm, setSearchTerm] = useState(''); const [searchTerm, setSearchTerm] = useState('');
const [imageUrlDraft, setImageUrlDraft] = useState(''); const [imageUrlDraft, setImageUrlDraft] = useState('');
const [itemColors, setItemColors] = useState([]);
const [sidebarOpen, setSidebarOpen] = useState(true); const [sidebarOpen, setSidebarOpen] = useState(true);
const [authMode, setAuthMode] = useState('login'); const [authMode, setAuthMode] = useState('login');
const [authError, setAuthError] = useState(''); const [authError, setAuthError] = useState('');
@@ -98,6 +99,14 @@ export default function App() {
saveUserSetting('weatherAlerts', newVal); saveUserSetting('weatherAlerts', newVal);
}; };
useEffect(() => {
if (editingItem && editingItem.color) {
setItemColors(editingItem.color.split(',').map(c => c.trim()).filter(Boolean));
} else {
setItemColors([]);
}
}, [editingItem]);
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');
document.documentElement.classList.add(theme); document.documentElement.classList.add(theme);
@@ -193,11 +202,15 @@ export default function App() {
const activeClothes = useMemo(() => clothes.filter(c => c.status === 'active'), [clothes]); const activeClothes = useMemo(() => clothes.filter(c => c.status === 'active'), [clothes]);
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 availableForLooks = useMemo(() => clothes.filter(c => c.status === 'active' || c.status === 'wishlist'), [clothes]);
const baseClothes = view === 'wishlist' ? wishlistClothes : activeClothes;
const availableColors = useMemo(() => { 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); return Array.from(colors);
}, [activeClothes]); }, [baseClothes]);
const colorStats = useMemo(() => { const colorStats = useMemo(() => {
if (!activeClothes.length) return []; if (!activeClothes.length) return [];
@@ -222,11 +235,11 @@ export default function App() {
}, [activeClothes]); }, [activeClothes]);
const filteredClothes = useMemo(() => { const filteredClothes = useMemo(() => {
return activeClothes.filter(c => { return baseClothes.filter(c => {
const matchesSearch = (c.name || "").toLowerCase().includes(searchTerm.toLowerCase()) || const matchesSearch = (c.name || "").toLowerCase().includes(searchTerm.toLowerCase()) ||
(c.color || "").toLowerCase().includes(searchTerm.toLowerCase()); (c.color || "").toLowerCase().includes(searchTerm.toLowerCase());
const matchesCategory = categoryFilter === 'Todos' || categoryFilter === t('all') || c.category === categoryFilter; 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; let matchesAge = true;
if (ageFilter !== 'any') { if (ageFilter !== 'any') {
@@ -240,7 +253,7 @@ export default function App() {
return matchesSearch && matchesCategory && matchesColor && matchesAge; return matchesSearch && matchesCategory && matchesColor && matchesAge;
}); });
}, [activeClothes, searchTerm, categoryFilter, colorFilter, ageFilter, t]); }, [baseClothes, searchTerm, categoryFilter, colorFilter, ageFilter, t]);
// Ações de Itens // Ações de Itens
const handleItemAction = async (action, item) => { const handleItemAction = async (action, item) => {
@@ -260,14 +273,21 @@ export default function App() {
const saveItem = async (e) => { const saveItem = async (e) => {
e.preventDefault(); e.preventDefault();
if (!user) return; if (!user) return;
setLoading(true);
const formData = new FormData(e.target); 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 = { const itemData = {
name: formData.get('name'), name: formData.get('name'),
category: formData.get('category'), category: formData.get('category'),
color: formData.get('color'), color: formData.get('color'),
imageUrl: formData.get('imageUrl') || 'https://images.unsplash.com/photo-1521572267360-ee0c2909d518?q=80&w=500&auto=format&fit=crop', 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, favorite: editingItem ? (editingItem.favorite || false) : false,
updatedAt: new Date().getTime() updatedAt: new Date().getTime()
}; };
@@ -279,7 +299,11 @@ export default function App() {
// Navegação instantânea (Optimistic UI Update) // Navegação instantânea (Optimistic UI Update)
setEditingItem(null); setEditingItem(null);
setImageUrlDraft(''); setImageUrlDraft('');
setView('closet'); setCategoryFilter('Todos');
setColorFilter('');
setAgeFilter('any');
setSearchTerm('');
setView(formData.get('isWishlist') ? 'wishlist' : 'closet');
if (currentEditId) { if (currentEditId) {
const docRef = doc(db, 'artifacts', appId, 'users', user.uid, 'clothes', currentEditId); const docRef = doc(db, 'artifacts', appId, 'users', user.uid, 'clothes', currentEditId);
@@ -328,6 +352,21 @@ export default function App() {
await deleteDoc(docRef); 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) => { const handleAuth = async (e) => {
e.preventDefault(); e.preventDefault();
setAuthError(''); setAuthError('');
@@ -513,6 +552,7 @@ export default function App() {
{[ {[
{ id: 'dashboard', label: t('dashboard'), icon: LayoutDashboard }, { id: 'dashboard', label: t('dashboard'), icon: LayoutDashboard },
{ id: 'closet', label: t('closet'), icon: Shirt }, { 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: 'laundry', label: t('laundry'), icon: Droplets },
{ id: 'outfits', label: t('outfits'), icon: Sparkles }, { id: 'outfits', label: t('outfits'), icon: Sparkles },
{ id: 'settings', label: t('settings'), icon: Settings }, { id: 'settings', label: t('settings'), icon: Settings },
@@ -561,6 +601,7 @@ export default function App() {
<h2 className="text-3xl font-black tracking-tighter"> <h2 className="text-3xl font-black tracking-tighter">
{view === 'dashboard' && t('overview')} {view === 'dashboard' && t('overview')}
{view === 'closet' && t('myCloset')} {view === 'closet' && t('myCloset')}
{view === 'wishlist' && (t('wishlist') || 'Lista de Desejos')}
{view === 'laundry' && t('laundry')} {view === 'laundry' && t('laundry')}
{view === 'outfits' && t('outfitsAndStyle')} {view === 'outfits' && t('outfitsAndStyle')}
{view === 'settings' && t('settings')} {view === 'settings' && t('settings')}
@@ -653,8 +694,8 @@ export default function App() {
</div> </div>
)} )}
{/* ARMÁRIO */} {/* ARMÁRIO & WISHLIST */}
{view === 'closet' && ( {(view === 'closet' || view === 'wishlist') && (
<div className="space-y-10 animate-in slide-in-from-bottom-8 duration-700"> <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="flex flex-col xl:flex-row gap-8 items-center justify-between">
<div className="relative w-full max-w-2xl"> <div className="relative w-full max-w-2xl">
@@ -785,9 +826,10 @@ export default function App() {
<div className="space-y-4"> <div className="space-y-4">
<p className="text-xs font-black uppercase opacity-50 tracking-widest px-2">{t('closetLabel')}</p> <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"> <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'}`}> <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="" /> <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>} {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> </button>
))} ))}
@@ -807,6 +849,7 @@ export default function App() {
</div> </div>
<div className="flex gap-2"> <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={() => { 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> <button onClick={() => deleteLook(look.id)} className="p-2 text-gray-300 hover:text-red-500 transition-colors"><Trash size={18} /></button>
</div> </div>
</div> </div>
@@ -849,14 +892,38 @@ export default function App() {
<Card className="p-10 shadow-2xl" darkMode={darkMode}> <Card className="p-10 shadow-2xl" darkMode={darkMode}>
<form onSubmit={saveItem} className="space-y-8"> <form onSubmit={saveItem} className="space-y-8">
<Input label={t('name')} name="name" defaultValue={editingItem?.name} required /> <Input label={t('name')} name="name" defaultValue={editingItem?.name} required />
<div className="grid grid-cols-2 gap-6">
<div className="space-y-2"> <div className="space-y-2">
<label className="text-[10px] font-black uppercase opacity-40 tracking-widest ml-1 text-inherit">{t('category')}</label> <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'}`}> <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> <option>{t('tops')}</option><option>{t('bottoms')}</option><option>{t('footwear')}</option><option>{t('coats')}</option><option>{t('accessories')}</option>
</select> </select>
</div> </div>
<Input label={t('color')} name="color" defaultValue={editingItem?.color} required /> <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>
</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> </div>
<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)} />