secçoes personalizadas !

This commit is contained in:
2026-04-27 21:06:24 +01:00
parent 91d5cbfcff
commit 7fa2eee3c7
2 changed files with 242 additions and 9 deletions

View File

@@ -6,7 +6,8 @@ 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, ShoppingBag, Share2 X, Download, Bell, Globe, Filter, ShoppingBag, Share2,
FolderOpen, Tag
} from 'lucide-react'; } from 'lucide-react';
import { import {
@@ -66,6 +67,14 @@ export default function App() {
const [sharedLookCopying, setSharedLookCopying] = useState(false); const [sharedLookCopying, setSharedLookCopying] = useState(false);
const [copiedLookId, setCopiedLookId] = useState(null); 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; const t = (key) => translations[language]?.[key] || translations['PT'][key] || key;
// Mapeamento de nomes de cor (PT) para valores CSS // Mapeamento de nomes de cor (PT) para valores CSS
@@ -151,6 +160,7 @@ export default function App() {
} else { } else {
setItemColors([]); setItemColors([]);
} }
setItemSections(editingItem?.sections || []);
}, [editingItem]); }, [editingItem]);
useEffect(() => { useEffect(() => {
@@ -179,6 +189,7 @@ export default function App() {
setUser(null); setUser(null);
setClothes([]); setClothes([]);
setLooks([]); setLooks([]);
setSections([]);
setUserProfile({}); setUserProfile({});
setDarkMode(false); setDarkMode(false);
setTheme('theme-indigo'); setTheme('theme-indigo');
@@ -219,6 +230,12 @@ export default function App() {
setLooks(snap.docs.map(d => ({ id: d.id, ...d.data() }))); setLooks(snap.docs.map(d => ({ id: d.id, ...d.data() })));
}, (err) => console.error(err)); }, (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 // Profile
const profileDoc = doc(db, 'artifacts', appId, 'users', user.uid, 'profile', 'data'); const profileDoc = doc(db, 'artifacts', appId, 'users', user.uid, 'profile', 'data');
const unsubProfile = onSnapshot(profileDoc, (snap) => { const unsubProfile = onSnapshot(profileDoc, (snap) => {
@@ -236,7 +253,7 @@ export default function App() {
else setUserProfile({}); else setUserProfile({});
}, (err) => console.error(err)); }, (err) => console.error(err));
return () => { unsubClothes(); unsubLooks(); unsubProfile(); }; return () => { unsubClothes(); unsubLooks(); unsubSections(); unsubProfile(); };
}, [user]); }, [user]);
// Fetch Weather Data // Fetch Weather Data
@@ -278,6 +295,35 @@ export default function App() {
const wishlistClothes = useMemo(() => clothes.filter(c => c.status === 'wishlist'), [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 => 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 baseClothes = view === 'wishlist' ? wishlistClothes : activeClothes;
const availableColors = useMemo(() => { const availableColors = useMemo(() => {
@@ -313,6 +359,7 @@ export default function App() {
(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 && c.color.includes(colorFilter)); const matchesColor = !colorFilter || (c.color && c.color.includes(colorFilter));
const matchesSection = activeSectionFilter === 'all' || (c.sections && c.sections.includes(activeSectionFilter));
let matchesAge = true; let matchesAge = true;
if (ageFilter !== 'any') { if (ageFilter !== 'any') {
@@ -324,9 +371,9 @@ export default function App() {
else if (ageFilter === 'older') matchesAge = days > 365; 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 // Ações de Itens
const handleItemAction = async (action, item) => { 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', 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')), 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,
sections: itemSections,
updatedAt: new Date().getTime() updatedAt: new Date().getTime()
}; };
@@ -868,6 +916,33 @@ export default function App() {
</div> </div>
</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"> <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-4 gap-10">
{filteredClothes.map(item => ( {filteredClothes.map(item => (
<div key={item.id} className="group"> <div key={item.id} className="group">
@@ -1149,6 +1224,33 @@ export default function App() {
</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)} />
{/* 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"> <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="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"> <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> </div>
</main> </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 */} {/* Modal de Filtros Avançados */}
{showClosetFilters && ( {showClosetFilters && (
<div className="fixed inset-0 z-[200] flex items-center justify-center bg-black/60 backdrop-blur-sm p-6" onClick={() => setShowClosetFilters(false)}> <div className="fixed inset-0 z-[200] flex items-center justify-center bg-black/60 backdrop-blur-sm p-6" onClick={() => setShowClosetFilters(false)}>

View File

@@ -117,7 +117,20 @@ export const translations = {
dob: "Data de Nascimento", dob: "Data de Nascimento",
bio: "Bio / Sobre mim", bio: "Bio / Sobre mim",
optional: "(Opcional)", 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: { EN: {
loginModeIntro: "The Future of Your Style", loginModeIntro: "The Future of Your Style",
@@ -237,7 +250,20 @@ export const translations = {
dob: "Date of Birth", dob: "Date of Birth",
bio: "Bio / About me", bio: "Bio / About me",
optional: "(Optional)", 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: { ES: {
loginModeIntro: "El Futuro de Tu Estilo", loginModeIntro: "El Futuro de Tu Estilo",
@@ -357,7 +383,20 @@ export const translations = {
dob: "Fecha de Nacimiento", dob: "Fecha de Nacimiento",
bio: "Bio / Sobre mí", bio: "Bio / Sobre mí",
optional: "(Opcional)", 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: { FR: {
loginModeIntro: "Le Futur de Ton Style", loginModeIntro: "Le Futur de Ton Style",
@@ -477,7 +516,20 @@ export const translations = {
dob: "Date de Naissance", dob: "Date de Naissance",
bio: "Bio / À propos", bio: "Bio / À propos",
optional: "(Optionnel)", 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: { DE: {
loginModeIntro: "Die Zukunft deines Stils", loginModeIntro: "Die Zukunft deines Stils",
@@ -597,6 +649,19 @@ export const translations = {
dob: "Geburtsdatum", dob: "Geburtsdatum",
bio: "Biografie / Über mich", bio: "Biografie / Über mich",
optional: "(Optional)", 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: 💼"
} }
}; };