nova secção wishlist
This commit is contained in:
95
src/App.jsx
95
src/App.jsx
@@ -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)} />
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user