share look good

This commit is contained in:
2026-04-24 14:06:24 +01:00
parent 964c048db7
commit 91d5cbfcff

View File

@@ -1,4 +1,4 @@
import React, { useState, useEffect, useMemo } from 'react'; import React, { useState, useEffect, useMemo, useRef } from 'react';
import { import {
Plus, Search, LayoutDashboard, Shirt, LogOut, Plus, Search, LayoutDashboard, Shirt, LogOut,
Trash2, Heart, Loader2, AlertCircle, Trash2, Heart, Loader2, AlertCircle,
@@ -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, ShoppingBag X, Download, Bell, Globe, Filter, ShoppingBag, Share2
} from 'lucide-react'; } from 'lucide-react';
import { import {
@@ -15,7 +15,7 @@ import {
} from 'firebase/auth'; } from 'firebase/auth';
import { import {
collection, doc, onSnapshot, addDoc, updateDoc, collection, doc, onSnapshot, addDoc, updateDoc,
deleteDoc, writeBatch, setDoc deleteDoc, writeBatch, setDoc, getDoc
} from 'firebase/firestore'; } from 'firebase/firestore';
import { auth, db, appId } from './lib/firebase'; import { auth, db, appId } from './lib/firebase';
@@ -59,6 +59,13 @@ export default function App() {
const [theme, setTheme] = useState('theme-indigo'); const [theme, setTheme] = useState('theme-indigo');
const [weatherData, setWeatherData] = useState(null); const [weatherData, setWeatherData] = useState(null);
// Estado para Partilha de Looks
const sharedLookRef = useRef('');
const [sharedLookData, setSharedLookData] = useState(null);
const [showSharedLookModal, setShowSharedLookModal] = useState(false);
const [sharedLookCopying, setSharedLookCopying] = useState(false);
const [copiedLookId, setCopiedLookId] = useState(null);
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
@@ -121,6 +128,23 @@ export default function App() {
saveUserSetting('weatherAlerts', newVal); saveUserSetting('weatherAlerts', newVal);
}; };
// Buscar o look partilhado pelo link
const fetchSharedLook = async (lookId) => {
if (!lookId) return;
try {
const lookDoc = doc(db, 'artifacts', appId, 'sharedLooks', lookId);
const snap = await getDoc(lookDoc);
if (snap.exists()) {
setSharedLookData({ id: snap.id, ...snap.data() });
setShowSharedLookModal(true);
// Limpar o parâmetro do URL sem recarregar a página
window.history.replaceState({}, '', window.location.pathname);
}
} catch (err) {
console.error('Erro ao buscar look partilhado:', err);
}
};
useEffect(() => { useEffect(() => {
if (editingItem && editingItem.color) { if (editingItem && editingItem.color) {
setItemColors(editingItem.color.split(',').map(c => c.trim()).filter(Boolean)); setItemColors(editingItem.color.split(',').map(c => c.trim()).filter(Boolean));
@@ -169,6 +193,10 @@ export default function App() {
setTheme(savedTheme); setTheme(savedTheme);
setUser(currentUser); setUser(currentUser);
setView('dashboard'); setView('dashboard');
// Verificar se há um look partilhado no URL
const sharedId = sharedLookRef.current || new URLSearchParams(window.location.search).get('shared');
sharedLookRef.current = '';
if (sharedId) fetchSharedLook(sharedId);
} }
setLoading(false); setLoading(false);
}); });
@@ -397,6 +425,77 @@ export default function App() {
await deleteDoc(docRef); await deleteDoc(docRef);
}; };
// Gerar link de partilha e copiar para clipboard
const shareLook = async (look) => {
if (!user) return;
try {
const lookItems = look.items.map(itemId => {
const item = clothes.find(c => c.id === itemId);
return item ? {
name: item.name,
category: item.category,
color: item.color,
imageUrl: item.imageUrl,
} : null;
}).filter(Boolean);
const sharedCol = collection(db, 'artifacts', appId, 'sharedLooks');
const docRef = await addDoc(sharedCol, {
lookName: look.name,
ownerUid: user.uid,
ownerEmail: user.email || '',
items: lookItems,
createdAt: new Date().getTime(),
});
const shareUrl = `${window.location.origin}${window.location.pathname}?shared=${docRef.id}`;
await navigator.clipboard.writeText(shareUrl);
setCopiedLookId(look.id);
setTimeout(() => setCopiedLookId(null), 3000);
} catch (err) {
console.error('Erro ao partilhar look:', err);
alert('Erro ao gerar link de partilha.');
}
};
// Copiar o look partilhado para o armário do utilizador atual
const copySharedLook = async () => {
if (!user || !sharedLookData) return;
setSharedLookCopying(true);
try {
const newItemIds = [];
for (const item of sharedLookData.items) {
const clothesCol = collection(db, 'artifacts', appId, 'users', user.uid, 'clothes');
const newItemRef = await addDoc(clothesCol, {
name: item.name,
category: item.category,
color: item.color,
imageUrl: item.imageUrl,
status: 'active',
favorite: false,
createdAt: new Date().getTime(),
updatedAt: new Date().getTime(),
});
newItemIds.push(newItemRef.id);
}
const looksCol = collection(db, 'artifacts', appId, 'users', user.uid, 'looks');
await addDoc(looksCol, {
name: sharedLookData.lookName,
items: newItemIds,
createdAt: new Date().getTime(),
updatedAt: new Date().getTime(),
});
setShowSharedLookModal(false);
setSharedLookData(null);
setView('outfits');
} catch (err) {
console.error('Erro ao copiar look:', err);
alert('Erro ao copiar look.');
} finally {
setSharedLookCopying(false);
}
};
const sendLookToLaundry = async (look) => { const sendLookToLaundry = async (look) => {
if (!window.confirm(t('confirmSendLookToLaundry') || 'Enviar todas as peças deste look para a lavandaria?')) return; if (!window.confirm(t('confirmSendLookToLaundry') || 'Enviar todas as peças deste look para a lavandaria?')) return;
setLoading(true); setLoading(true);
@@ -914,6 +1013,16 @@ export default function App() {
<p className="text-[10px] opacity-40 font-bold uppercase tracking-widest">{look.items.length} {t('pieces')} {new Date(look.createdAt).toLocaleDateString()}</p> <p className="text-[10px] opacity-40 font-bold uppercase tracking-widest">{look.items.length} {t('pieces')} {new Date(look.createdAt).toLocaleDateString()}</p>
</div> </div>
<div className="flex gap-2"> <div className="flex gap-2">
<button
onClick={() => shareLook(look)}
className={`p-2 transition-colors relative group/share ${copiedLookId === look.id ? 'text-green-500' : 'text-gray-300 hover:text-green-500'}`}
title="Partilhar look"
>
{copiedLookId === look.id ? <Check size={18} /> : <Share2 size={18} />}
<span className="absolute -top-8 left-1/2 -translate-x-1/2 bg-gray-900 text-white text-[9px] font-black uppercase tracking-widest px-2 py-1 rounded-lg whitespace-nowrap opacity-0 group-hover/share:opacity-100 transition-opacity pointer-events-none">
{copiedLookId === look.id ? 'Link copiado!' : 'Partilhar'}
</span>
</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={() => { 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={() => 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>
@@ -1353,6 +1462,80 @@ export default function App() {
</Card> </Card>
</div> </div>
)} )}
{/* Modal de Look Partilhado */}
{showSharedLookModal && sharedLookData && (
<div className="fixed inset-0 z-[300] flex items-center justify-center bg-black/70 backdrop-blur-md p-6" onClick={() => { setShowSharedLookModal(false); setSharedLookData(null); }}>
<div
className={`w-full max-w-lg rounded-[2rem] shadow-2xl overflow-hidden animate-in zoom-in-95 duration-300 ${darkMode ? 'bg-gray-900' : 'bg-white'}`}
onClick={e => e.stopPropagation()}
>
{/* Header com gradiente */}
<div className="relative p-8 pb-6" style={{ background: 'linear-gradient(135deg, hsl(var(--primary-600)), hsl(var(--primary-400)))' }}>
<div className="absolute inset-0 opacity-20" style={{ backgroundImage: 'radial-gradient(circle at 80% 20%, white 0%, transparent 60%)' }} />
<div className="relative z-10">
<div className="flex items-center gap-3 mb-2">
<div className="p-2 bg-white/20 rounded-xl backdrop-blur-sm">
<Share2 size={20} className="text-white" />
</div>
<span className="text-white/80 font-black uppercase text-[10px] tracking-widest">Look Partilhado</span>
</div>
<h2 className="text-3xl font-black text-white tracking-tight">{sharedLookData.lookName}</h2>
<p className="text-white/60 text-sm font-bold mt-1">{sharedLookData.items.length} peça{sharedLookData.items.length !== 1 ? 's' : ''} Partilhado por {sharedLookData.ownerEmail?.split('@')[0] || 'alguém'}</p>
</div>
</div>
{/* Peças do look */}
<div className={`p-8 ${darkMode ? 'bg-gray-900' : 'bg-white'}`}>
<p className="text-[10px] font-black uppercase tracking-widest opacity-40 mb-4">Peças incluídas</p>
<div className="flex flex-wrap gap-3 mb-8">
{sharedLookData.items.map((item, idx) => (
<div key={idx} className="relative group/item">
<div className="w-20 h-20 rounded-2xl overflow-hidden border-2 border-gray-100 dark:border-gray-700 shadow-lg">
<img src={item.imageUrl} alt={item.name} className="w-full h-full object-cover group-hover/item:scale-110 transition-transform duration-500" />
</div>
<div className="absolute -bottom-1 left-1/2 -translate-x-1/2 bg-gray-900/90 text-white text-[8px] font-black uppercase tracking-wide px-2 py-0.5 rounded-full whitespace-nowrap opacity-0 group-hover/item:opacity-100 transition-opacity">
{item.name}
</div>
</div>
))}
</div>
{/* Descrição das peças */}
<div className={`space-y-2 mb-8 max-h-32 overflow-y-auto custom-scrollbar`}>
{sharedLookData.items.map((item, idx) => (
<div key={idx} className={`flex items-center gap-3 px-4 py-2.5 rounded-xl ${darkMode ? 'bg-gray-800' : 'bg-gray-50'}`}>
<span className="text-xs font-black truncate flex-1">{item.name}</span>
<span className={`text-[9px] font-black uppercase tracking-widest opacity-40 shrink-0`}>{item.category}</span>
</div>
))}
</div>
{/* Ações */}
<div className="flex gap-3">
<button
onClick={() => { setShowSharedLookModal(false); setSharedLookData(null); }}
className={`flex-1 py-4 font-black uppercase text-[10px] tracking-widest rounded-2xl transition-all ${darkMode ? 'bg-gray-800 text-gray-400 hover:bg-gray-700' : 'bg-gray-100 text-gray-500 hover:bg-gray-200'}`}
>
Ignorar
</button>
<button
onClick={copySharedLook}
disabled={sharedLookCopying}
className="flex-[2] py-4 font-black uppercase text-[10px] tracking-widest rounded-2xl text-white shadow-xl transition-all hover:scale-[1.02] active:scale-95 disabled:opacity-60 flex items-center justify-center gap-2"
style={{ background: 'linear-gradient(135deg, hsl(var(--primary-600)), hsl(var(--primary-500)))' }}
>
{sharedLookCopying ? (
<><Loader2 size={16} className="animate-spin" /> A copiar...</>
) : (
<><Check size={16} /> Copiar para o meu armário</>
)}
</button>
</div>
</div>
</div>
</div>
)}
</div> </div>
); );
} }