secçoes personalizadas !
This commit is contained in:
176
src/App.jsx
176
src/App.jsx
@@ -6,7 +6,8 @@ import {
|
||||
Edit2, Image as ImageIcon, Check, RotateCcw, Trash,
|
||||
PanelLeftClose, PanelLeftOpen, Sparkles, CloudSun,
|
||||
ArrowRight, Droplets, CheckCircle2, PieChart, History,
|
||||
X, Download, Bell, Globe, Filter, ShoppingBag, Share2
|
||||
X, Download, Bell, Globe, Filter, ShoppingBag, Share2,
|
||||
FolderOpen, Tag
|
||||
} from 'lucide-react';
|
||||
|
||||
import {
|
||||
@@ -66,6 +67,14 @@ export default function App() {
|
||||
const [sharedLookCopying, setSharedLookCopying] = useState(false);
|
||||
const [copiedLookId, setCopiedLookId] = useState(null);
|
||||
|
||||
// Estado para Secções
|
||||
const [sections, setSections] = useState([]);
|
||||
const [activeSectionFilter, setActiveSectionFilter] = useState('all');
|
||||
const [showSectionManager, setShowSectionManager] = useState(false);
|
||||
const [newSectionName, setNewSectionName] = useState('');
|
||||
const [newSectionEmoji, setNewSectionEmoji] = useState('');
|
||||
const [itemSections, setItemSections] = useState([]);
|
||||
|
||||
const t = (key) => translations[language]?.[key] || translations['PT'][key] || key;
|
||||
|
||||
// Mapeamento de nomes de cor (PT) para valores CSS
|
||||
@@ -151,6 +160,7 @@ export default function App() {
|
||||
} else {
|
||||
setItemColors([]);
|
||||
}
|
||||
setItemSections(editingItem?.sections || []);
|
||||
}, [editingItem]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -179,6 +189,7 @@ export default function App() {
|
||||
setUser(null);
|
||||
setClothes([]);
|
||||
setLooks([]);
|
||||
setSections([]);
|
||||
setUserProfile({});
|
||||
setDarkMode(false);
|
||||
setTheme('theme-indigo');
|
||||
@@ -219,6 +230,12 @@ export default function App() {
|
||||
setLooks(snap.docs.map(d => ({ id: d.id, ...d.data() })));
|
||||
}, (err) => console.error(err));
|
||||
|
||||
// Secções
|
||||
const sectionsCol = collection(db, 'artifacts', appId, 'users', user.uid, 'sections');
|
||||
const unsubSections = onSnapshot(sectionsCol, (snap) => {
|
||||
setSections(snap.docs.map(d => ({ id: d.id, ...d.data() })).sort((a, b) => a.createdAt - b.createdAt));
|
||||
}, (err) => console.error(err));
|
||||
|
||||
// Profile
|
||||
const profileDoc = doc(db, 'artifacts', appId, 'users', user.uid, 'profile', 'data');
|
||||
const unsubProfile = onSnapshot(profileDoc, (snap) => {
|
||||
@@ -236,7 +253,7 @@ export default function App() {
|
||||
else setUserProfile({});
|
||||
}, (err) => console.error(err));
|
||||
|
||||
return () => { unsubClothes(); unsubLooks(); unsubProfile(); };
|
||||
return () => { unsubClothes(); unsubLooks(); unsubSections(); unsubProfile(); };
|
||||
}, [user]);
|
||||
|
||||
// Fetch Weather Data
|
||||
@@ -278,6 +295,35 @@ export default function App() {
|
||||
const wishlistClothes = useMemo(() => clothes.filter(c => c.status === 'wishlist'), [clothes]);
|
||||
const availableForLooks = useMemo(() => clothes.filter(c => c.status === 'active' || c.status === 'wishlist'), [clothes]);
|
||||
|
||||
// CRUD de Secções
|
||||
const saveSection = async () => {
|
||||
if (!newSectionName.trim() || !user) return;
|
||||
const sectionsCol = collection(db, 'artifacts', appId, 'users', user.uid, 'sections');
|
||||
await addDoc(sectionsCol, {
|
||||
name: newSectionName.trim(),
|
||||
emoji: newSectionEmoji.trim() || '💼',
|
||||
createdAt: new Date().getTime()
|
||||
});
|
||||
setNewSectionName('');
|
||||
setNewSectionEmoji('');
|
||||
};
|
||||
|
||||
const deleteSection = async (id) => {
|
||||
if (!window.confirm(t('confirmDeleteSection'))) return;
|
||||
const docRef = doc(db, 'artifacts', appId, 'users', user.uid, 'sections', id);
|
||||
await deleteDoc(docRef);
|
||||
// Remover a secção de todas as peças que a tinham
|
||||
const batch = writeBatch(db);
|
||||
clothes.forEach(item => {
|
||||
if (item.sections && item.sections.includes(id)) {
|
||||
const itemRef = doc(db, 'artifacts', appId, 'users', user.uid, 'clothes', item.id);
|
||||
batch.update(itemRef, { sections: item.sections.filter(s => s !== id) });
|
||||
}
|
||||
});
|
||||
await batch.commit();
|
||||
if (activeSectionFilter === id) setActiveSectionFilter('all');
|
||||
};
|
||||
|
||||
const baseClothes = view === 'wishlist' ? wishlistClothes : activeClothes;
|
||||
|
||||
const availableColors = useMemo(() => {
|
||||
@@ -313,6 +359,7 @@ export default function App() {
|
||||
(c.color || "").toLowerCase().includes(searchTerm.toLowerCase());
|
||||
const matchesCategory = categoryFilter === 'Todos' || categoryFilter === t('all') || c.category === categoryFilter;
|
||||
const matchesColor = !colorFilter || (c.color && c.color.includes(colorFilter));
|
||||
const matchesSection = activeSectionFilter === 'all' || (c.sections && c.sections.includes(activeSectionFilter));
|
||||
|
||||
let matchesAge = true;
|
||||
if (ageFilter !== 'any') {
|
||||
@@ -324,9 +371,9 @@ export default function App() {
|
||||
else if (ageFilter === 'older') matchesAge = days > 365;
|
||||
}
|
||||
|
||||
return matchesSearch && matchesCategory && matchesColor && matchesAge;
|
||||
return matchesSearch && matchesCategory && matchesColor && matchesAge && matchesSection;
|
||||
});
|
||||
}, [baseClothes, searchTerm, categoryFilter, colorFilter, ageFilter, t]);
|
||||
}, [baseClothes, searchTerm, categoryFilter, colorFilter, ageFilter, t, activeSectionFilter]);
|
||||
|
||||
// Ações de Itens
|
||||
const handleItemAction = async (action, item) => {
|
||||
@@ -362,6 +409,7 @@ export default function App() {
|
||||
imageUrl: formData.get('imageUrl') || 'https://images.unsplash.com/photo-1521572267360-ee0c2909d518?q=80&w=500&auto=format&fit=crop',
|
||||
status: formData.get('isWishlist') ? 'wishlist' : (editingItem && editingItem.status === 'wishlist' ? 'active' : (editingItem ? editingItem.status : 'active')),
|
||||
favorite: editingItem ? (editingItem.favorite || false) : false,
|
||||
sections: itemSections,
|
||||
updatedAt: new Date().getTime()
|
||||
};
|
||||
|
||||
@@ -868,6 +916,33 @@ export default function App() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Barra de Secções */}
|
||||
{view === 'closet' && (
|
||||
<div className="flex items-center gap-3 overflow-x-auto pb-1 custom-scrollbar">
|
||||
<button
|
||||
onClick={() => setActiveSectionFilter('all')}
|
||||
className={`shrink-0 flex items-center gap-2 px-5 py-2.5 rounded-2xl font-black text-[10px] uppercase tracking-widest transition-all ${activeSectionFilter === 'all' ? 'bg-primary-600 text-white shadow-lg shadow-primary-600/30' : (darkMode ? 'bg-gray-800 text-gray-400 hover:bg-gray-700' : 'bg-gray-100 text-gray-500 hover:bg-gray-200')}`}
|
||||
>
|
||||
<FolderOpen size={14} /> {t('allSections')}
|
||||
</button>
|
||||
{sections.map(sec => (
|
||||
<button
|
||||
key={sec.id}
|
||||
onClick={() => setActiveSectionFilter(sec.id)}
|
||||
className={`shrink-0 flex items-center gap-2 px-5 py-2.5 rounded-2xl font-black text-[10px] uppercase tracking-widest transition-all ${activeSectionFilter === sec.id ? 'bg-primary-600 text-white shadow-lg shadow-primary-600/30 scale-105' : (darkMode ? 'bg-gray-800 text-gray-400 hover:bg-gray-700' : 'bg-gray-100 text-gray-500 hover:bg-gray-200')}`}
|
||||
>
|
||||
<span>{sec.emoji}</span> {sec.name}
|
||||
</button>
|
||||
))}
|
||||
<button
|
||||
onClick={() => setShowSectionManager(true)}
|
||||
className={`shrink-0 flex items-center gap-2 px-4 py-2.5 rounded-2xl font-black text-[10px] uppercase tracking-widest transition-all border-2 border-dashed ${darkMode ? 'border-gray-700 text-gray-500 hover:border-primary-500 hover:text-primary-400' : 'border-gray-200 text-gray-400 hover:border-primary-400 hover:text-primary-600'}`}
|
||||
>
|
||||
<Settings size={14} /> {t('manageSections')}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-4 gap-10">
|
||||
{filteredClothes.map(item => (
|
||||
<div key={item.id} className="group">
|
||||
@@ -1149,6 +1224,33 @@ export default function App() {
|
||||
</div>
|
||||
<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">
|
||||
<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>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{sections.map(sec => (
|
||||
<button
|
||||
key={sec.id}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (itemSections.includes(sec.id))
|
||||
setItemSections(itemSections.filter(s => s !== sec.id));
|
||||
else
|
||||
setItemSections([...itemSections, sec.id]);
|
||||
}}
|
||||
className={`flex items-center gap-2 px-4 py-2 rounded-xl text-xs font-bold transition-all border-2 ${itemSections.includes(sec.id) ? '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'}`}
|
||||
>
|
||||
<span>{sec.emoji}</span> {sec.name}
|
||||
{itemSections.includes(sec.id) && <Check size={12} />}
|
||||
</button>
|
||||
))}
|
||||
</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>
|
||||
<button type="submit" className="flex-1 py-5 bg-primary-600 text-white rounded-[2rem] font-black uppercase tracking-widest text-[10px] shadow-2xl shadow-primary-600/40 hover:scale-[1.02] active:scale-95 transition-all">
|
||||
@@ -1366,6 +1468,72 @@ export default function App() {
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{/* 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)}>
|
||||
<Card className="w-full max-w-lg p-8 animate-in zoom-in-95 flex flex-col max-h-[90vh]" 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">
|
||||
<FolderOpen size={24} className="text-primary-600" /> {t('manageSections')}
|
||||
</h3>
|
||||
<button onClick={() => setShowSectionManager(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>
|
||||
|
||||
{/* Criar nova secção */}
|
||||
<div className={`flex gap-3 mb-8 p-4 rounded-2xl ${darkMode ? 'bg-gray-800' : 'bg-gray-50'}`}>
|
||||
<input
|
||||
value={newSectionEmoji}
|
||||
onChange={e => setNewSectionEmoji(e.target.value)}
|
||||
placeholder={t('emojiPlaceholder')}
|
||||
maxLength={2}
|
||||
className={`w-16 p-3 rounded-xl border-none outline-none font-bold text-center text-lg ${darkMode ? 'bg-gray-700 text-white' : 'bg-white'} shadow-sm`}
|
||||
/>
|
||||
<input
|
||||
value={newSectionName}
|
||||
onChange={e => setNewSectionName(e.target.value)}
|
||||
placeholder={t('sectionPlaceholder')}
|
||||
onKeyDown={e => e.key === 'Enter' && saveSection()}
|
||||
className={`flex-1 p-3 rounded-xl border-none outline-none font-bold ${darkMode ? 'bg-gray-700 text-white' : 'bg-white'} shadow-sm`}
|
||||
/>
|
||||
<button
|
||||
onClick={saveSection}
|
||||
disabled={!newSectionName.trim()}
|
||||
className="px-5 py-3 bg-primary-600 text-white rounded-xl font-black text-[10px] uppercase tracking-widest shadow-lg shadow-primary-600/30 hover:scale-105 active:scale-95 transition-all disabled:opacity-30"
|
||||
>
|
||||
<Plus size={18} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Lista de secções */}
|
||||
<div className="flex-1 overflow-y-auto space-y-3 custom-scrollbar">
|
||||
{sections.length === 0 ? (
|
||||
<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'}`}>
|
||||
<span className="text-2xl w-10 text-center">{sec.emoji}</span>
|
||||
<div className="flex-1">
|
||||
<p className="font-black text-sm">{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)
|
||||
</p>
|
||||
</div>
|
||||
<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>
|
||||
|
||||
<button onClick={() => setShowSectionManager(false)} className="mt-8 w-full py-4 font-black uppercase text-[10px] tracking-widest text-gray-500 hover:text-gray-900 dark:hover:text-white transition-colors">
|
||||
{t('cancel')}
|
||||
</button>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Modal de Filtros Avançados */}
|
||||
{showClosetFilters && (
|
||||
<div className="fixed inset-0 z-[200] flex items-center justify-center bg-black/60 backdrop-blur-sm p-6" onClick={() => setShowClosetFilters(false)}>
|
||||
|
||||
@@ -117,7 +117,20 @@ export const translations = {
|
||||
dob: "Data de Nascimento",
|
||||
bio: "Bio / Sobre mim",
|
||||
optional: "(Opcional)",
|
||||
saving: "A guardar..."
|
||||
saving: "A guardar...",
|
||||
sections: "Secções",
|
||||
manageSections: "Gerir Secções",
|
||||
newSection: "Nova Secção",
|
||||
sectionName: "Nome da Secção",
|
||||
sectionEmoji: "Emoji",
|
||||
noSections: "Nenhuma secção criada ainda.",
|
||||
addSection: "Adicionar Secção",
|
||||
deleteSection: "Apagar",
|
||||
assignSections: "Atribuir a Secções",
|
||||
allSections: "Todas",
|
||||
confirmDeleteSection: "Apagar esta secção?",
|
||||
sectionPlaceholder: "Ex: Trabalho, Festa...",
|
||||
emojiPlaceholder: "Ex: 💼"
|
||||
},
|
||||
EN: {
|
||||
loginModeIntro: "The Future of Your Style",
|
||||
@@ -237,7 +250,20 @@ export const translations = {
|
||||
dob: "Date of Birth",
|
||||
bio: "Bio / About me",
|
||||
optional: "(Optional)",
|
||||
saving: "Saving..."
|
||||
saving: "Saving...",
|
||||
sections: "Sections",
|
||||
manageSections: "Manage Sections",
|
||||
newSection: "New Section",
|
||||
sectionName: "Section Name",
|
||||
sectionEmoji: "Emoji",
|
||||
noSections: "No sections created yet.",
|
||||
addSection: "Add Section",
|
||||
deleteSection: "Delete",
|
||||
assignSections: "Assign to Sections",
|
||||
allSections: "All",
|
||||
confirmDeleteSection: "Delete this section?",
|
||||
sectionPlaceholder: "E.g.: Work, Party...",
|
||||
emojiPlaceholder: "E.g.: 💼"
|
||||
},
|
||||
ES: {
|
||||
loginModeIntro: "El Futuro de Tu Estilo",
|
||||
@@ -357,7 +383,20 @@ export const translations = {
|
||||
dob: "Fecha de Nacimiento",
|
||||
bio: "Bio / Sobre mí",
|
||||
optional: "(Opcional)",
|
||||
saving: "Guardando..."
|
||||
saving: "Guardando...",
|
||||
sections: "Secciones",
|
||||
manageSections: "Gestionar Secciones",
|
||||
newSection: "Nueva Sección",
|
||||
sectionName: "Nombre de la Sección",
|
||||
sectionEmoji: "Emoji",
|
||||
noSections: "Aún no hay secciones creadas.",
|
||||
addSection: "Añadir Sección",
|
||||
deleteSection: "Eliminar",
|
||||
assignSections: "Asignar a Secciones",
|
||||
allSections: "Todas",
|
||||
confirmDeleteSection: "¿Eliminar esta sección?",
|
||||
sectionPlaceholder: "Ej: Trabajo, Fiesta...",
|
||||
emojiPlaceholder: "Ej: 💼"
|
||||
},
|
||||
FR: {
|
||||
loginModeIntro: "Le Futur de Ton Style",
|
||||
@@ -477,7 +516,20 @@ export const translations = {
|
||||
dob: "Date de Naissance",
|
||||
bio: "Bio / À propos",
|
||||
optional: "(Optionnel)",
|
||||
saving: "Enregistrement..."
|
||||
saving: "Enregistrement...",
|
||||
sections: "Sections",
|
||||
manageSections: "Gérer les Sections",
|
||||
newSection: "Nouvelle Section",
|
||||
sectionName: "Nom de la Section",
|
||||
sectionEmoji: "Emoji",
|
||||
noSections: "Aucune section créée pour l'instant.",
|
||||
addSection: "Ajouter une Section",
|
||||
deleteSection: "Supprimer",
|
||||
assignSections: "Attribuer aux Sections",
|
||||
allSections: "Toutes",
|
||||
confirmDeleteSection: "Supprimer cette section ?",
|
||||
sectionPlaceholder: "Ex: Travail, Fête...",
|
||||
emojiPlaceholder: "Ex: 💼"
|
||||
},
|
||||
DE: {
|
||||
loginModeIntro: "Die Zukunft deines Stils",
|
||||
@@ -597,6 +649,19 @@ export const translations = {
|
||||
dob: "Geburtsdatum",
|
||||
bio: "Biografie / Über mich",
|
||||
optional: "(Optional)",
|
||||
saving: "Speichern..."
|
||||
saving: "Speichern...",
|
||||
sections: "Bereiche",
|
||||
manageSections: "Bereiche verwalten",
|
||||
newSection: "Neuer Bereich",
|
||||
sectionName: "Bereichsname",
|
||||
sectionEmoji: "Emoji",
|
||||
noSections: "Noch keine Bereiche erstellt.",
|
||||
addSection: "Bereich hinzufügen",
|
||||
deleteSection: "Löschen",
|
||||
assignSections: "Bereichen zuweisen",
|
||||
allSections: "Alle",
|
||||
confirmDeleteSection: "Diesen Bereich löschen?",
|
||||
sectionPlaceholder: "Zb: Arbeit, Party...",
|
||||
emojiPlaceholder: "Zb: 💼"
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user