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 {
Plus, Search, LayoutDashboard, Shirt, LogOut,
Trash2, Heart, Loader2, AlertCircle,
@@ -6,7 +6,7 @@ import {
Edit2, Image as ImageIcon, Check, RotateCcw, Trash,
PanelLeftClose, PanelLeftOpen, Sparkles, CloudSun,
ArrowRight, Droplets, CheckCircle2, PieChart, History,
X, Download, Bell, Globe, Filter, ShoppingBag
X, Download, Bell, Globe, Filter, ShoppingBag, Share2
} from 'lucide-react';
import {
@@ -15,7 +15,7 @@ import {
} from 'firebase/auth';
import {
collection, doc, onSnapshot, addDoc, updateDoc,
deleteDoc, writeBatch, setDoc
deleteDoc, writeBatch, setDoc, getDoc
} from 'firebase/firestore';
import { auth, db, appId } from './lib/firebase';
@@ -59,6 +59,13 @@ export default function App() {
const [theme, setTheme] = useState('theme-indigo');
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;
// Mapeamento de nomes de cor (PT) para valores CSS
@@ -121,6 +128,23 @@ export default function App() {
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(() => {
if (editingItem && editingItem.color) {
setItemColors(editingItem.color.split(',').map(c => c.trim()).filter(Boolean));
@@ -169,6 +193,10 @@ export default function App() {
setTheme(savedTheme);
setUser(currentUser);
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);
});
@@ -397,6 +425,77 @@ export default function App() {
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) => {
if (!window.confirm(t('confirmSendLookToLaundry') || 'Enviar todas as peças deste look para a lavandaria?')) return;
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>
</div>
<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={() => 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>
@@ -1353,6 +1462,80 @@ export default function App() {
</Card>
</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>
);
}