share look good
This commit is contained in:
189
src/App.jsx
189
src/App.jsx
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user