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 {
|
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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user