This commit is contained in:
2026-05-12 17:11:54 +01:00
parent c1c4933cfc
commit 6089ba11d5

View File

@@ -7,7 +7,7 @@ import {
PanelLeftClose, PanelLeftOpen, Sparkles, CloudSun,
ArrowRight, Droplets, CheckCircle2, PieChart, History,
X, Download, Bell, Globe, Filter, ShoppingBag, Share2,
FolderOpen, Tag, Link, Calendar, ChevronLeft, ChevronRight, Users
FolderOpen, Tag, Link, Calendar, ChevronLeft, ChevronRight, ChevronDown, Users, MapPin, Copy
} from 'lucide-react';
import {
@@ -69,8 +69,11 @@ export default function App() {
// Estado da Comunidade
const [communitySearchTerm, setCommunitySearchTerm] = useState('');
const [showRecommended, setShowRecommended] = useState(false);
const [communityUsers, setCommunityUsers] = useState([]);
const [selectedCommunityUser, setSelectedCommunityUser] = useState(null);
const [selectedUserProfile, setSelectedUserProfile] = useState(null);
const [showInspectModal, setShowInspectModal] = useState(false);
const [selectedUserClothes, setSelectedUserClothes] = useState([]);
const [selectedUserLooks, setSelectedUserLooks] = useState([]);
@@ -433,7 +436,8 @@ export default function App() {
username: userProfile.username || '',
fullName: userProfile.fullName || '',
avatar: userProfile.avatar || null,
isPrivate: userProfile.settings?.isPrivate || false
isPrivate: userProfile.settings?.isPrivate || false,
location: userProfile.location || ''
}, { merge: true }).catch(err => {
console.error("Erro ao sincronizar perfil público:", err);
if (err.code === 'permission-denied') {
@@ -441,7 +445,7 @@ export default function App() {
}
});
}
}, [user, userProfile?.username, userProfile?.fullName, userProfile?.avatar, userProfile?.settings?.isPrivate]);
}, [user, userProfile?.username, userProfile?.fullName, userProfile?.avatar, userProfile?.settings?.isPrivate, userProfile?.location]);
// Fetch utilizadores da comunidade
useEffect(() => {
@@ -452,16 +456,25 @@ export default function App() {
const snap = await getDocs(profilesRef);
const users = snap.docs.map(d => d.data()).filter(u => u.uid !== user?.uid);
if (communitySearchTerm.trim()) {
if (showRecommended) {
const normalizeLoc = (str) => str ? str.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase().trim() : "";
const myLocation = normalizeLoc(userProfile?.location);
if (myLocation) {
setCommunityUsers(users.filter(u =>
normalizeLoc(u.location) === myLocation
));
} else {
setCommunityUsers([]);
}
} else if (communitySearchTerm.trim()) {
let term = communitySearchTerm.trim().toLowerCase();
if (term.startsWith('@')) term = term.substring(1);
setCommunityUsers(users.filter(u => {
const uname = u.username ? u.username.toLowerCase() : '';
const fname = u.fullName ? u.fullName.toLowerCase() : '';
return uname.includes(term) || fname.includes(term);
return uname.includes(term);
}));
} else {
setCommunityUsers(users);
setCommunityUsers([]);
}
} catch (err) {
console.error("Erro ao buscar comunidade", err);
@@ -469,13 +482,14 @@ export default function App() {
}
};
fetchUsers();
}, [view, communitySearchTerm, user?.uid]);
}, [view, communitySearchTerm, showRecommended, user?.uid, userProfile?.location]);
const viewCommunityUser = async (targetUser) => {
setSelectedCommunityUser(targetUser);
if (targetUser.isPrivate) {
setSelectedUserClothes([]);
setSelectedUserLooks([]);
setSelectedUserProfile(null);
return;
}
@@ -489,6 +503,15 @@ export default function App() {
const looksCol = collection(db, 'artifacts', appId, 'users', targetUser.uid, 'looks');
const snapLooks = await getDocs(looksCol);
setSelectedUserLooks(snapLooks.docs.map(d => ({id: d.id, ...d.data()})));
// Profile
const profileDocRef = doc(db, 'artifacts', appId, 'users', targetUser.uid, 'profile', 'data');
const snapProfile = await getDoc(profileDocRef);
if (snapProfile.exists()) {
setSelectedUserProfile(snapProfile.data());
} else {
setSelectedUserProfile({});
}
} catch (err) {
console.error("Erro ao carregar perfil do utilizador", err);
}
@@ -1094,7 +1117,8 @@ export default function App() {
fullName: fd.get('fullName') || '',
dob: dob,
bio: fd.get('bio') || '',
location: fd.get('location') || ''
location: fd.get('location') || '',
createdAt: userProfile?.createdAt || new Date().getTime()
}, { merge: true }).catch(err => {
console.error(err);
});
@@ -2126,7 +2150,21 @@ export default function App() {
</div>
<div>
<h3 className="text-3xl font-black tracking-tighter">{userProfile?.fullName || t('yourAccount')}</h3>
<div className="flex items-center gap-2 mt-1">
<p className="opacity-60 font-bold text-sm">@{userProfile?.username || user?.email?.split('@')[0] || t('papMode')}</p>
<button
onClick={(e) => {
e.stopPropagation();
navigator.clipboard.writeText(`@${userProfile?.username || user?.email?.split('@')[0] || t('papMode')}`);
setToastMessage('Username copiado!' || t('copied'));
setTimeout(() => setToastMessage(null), 2000);
}}
className="text-blue-500 hover:text-blue-600 bg-blue-500/10 p-1.5 rounded-md transition-colors"
title="Copiar Username"
>
<Copy size={14} />
</button>
</div>
</div>
</div>
</Card>
@@ -2146,18 +2184,27 @@ export default function App() {
<div className="space-y-2">
<label className="text-[10px] font-black uppercase opacity-40 tracking-widest ml-1 text-inherit">{t('dob')} {t('optional')}</label>
<div className="flex gap-2">
<select name="dobDay" defaultValue={userProfile?.dob?.split('-')[2] || ''} className={`flex-1 p-4 rounded-xl border-none outline-none focus:ring-2 focus:ring-primary-500 font-bold ${darkMode ? 'bg-gray-800 text-white' : 'bg-gray-50'}`}>
<div className="relative flex-1">
<select name="dobDay" defaultValue={userProfile?.dob?.split('-')[2] || ''} className={`w-full appearance-none p-4 pr-10 rounded-xl border-none outline-none focus:ring-2 focus:ring-primary-500/40 font-bold transition-all cursor-pointer ${darkMode ? 'bg-gray-800 text-white hover:bg-gray-700' : 'bg-gray-100 text-gray-900 hover:bg-gray-200'}`}>
<option value="">DD</option>
{Array.from({ length: 31 }, (_, i) => String(i + 1).padStart(2, '0')).map(d => <option key={d} value={d}>{d}</option>)}
</select>
<select name="dobMonth" defaultValue={userProfile?.dob?.split('-')[1] || ''} className={`flex-1 p-4 rounded-xl border-none outline-none focus:ring-2 focus:ring-primary-500 font-bold ${darkMode ? 'bg-gray-800 text-white' : 'bg-gray-50'}`}>
<ChevronDown size={14} className="absolute right-3 top-1/2 -translate-y-1/2 opacity-40 pointer-events-none" />
</div>
<div className="relative flex-1">
<select name="dobMonth" defaultValue={userProfile?.dob?.split('-')[1] || ''} className={`w-full appearance-none p-4 pr-10 rounded-xl border-none outline-none focus:ring-2 focus:ring-primary-500/40 font-bold transition-all cursor-pointer ${darkMode ? 'bg-gray-800 text-white hover:bg-gray-700' : 'bg-gray-100 text-gray-900 hover:bg-gray-200'}`}>
<option value="">MM</option>
{Array.from({ length: 12 }, (_, i) => String(i + 1).padStart(2, '0')).map(m => <option key={m} value={m}>{m}</option>)}
</select>
<select name="dobYear" defaultValue={userProfile?.dob?.split('-')[0] || ''} className={`flex-[1.5] p-4 rounded-xl border-none outline-none focus:ring-2 focus:ring-primary-500 font-bold ${darkMode ? 'bg-gray-800 text-white' : 'bg-gray-50'}`}>
<ChevronDown size={14} className="absolute right-3 top-1/2 -translate-y-1/2 opacity-40 pointer-events-none" />
</div>
<div className="relative flex-[1.5]">
<select name="dobYear" defaultValue={userProfile?.dob?.split('-')[0] || ''} className={`w-full appearance-none p-4 pr-10 rounded-xl border-none outline-none focus:ring-2 focus:ring-primary-500/40 font-bold transition-all cursor-pointer ${darkMode ? 'bg-gray-800 text-white hover:bg-gray-700' : 'bg-gray-100 text-gray-900 hover:bg-gray-200'}`}>
<option value="">YYYY</option>
{Array.from({ length: 100 }, (_, i) => new Date().getFullYear() - i).map(y => <option key={y} value={y}>{y}</option>)}
</select>
<ChevronDown size={14} className="absolute right-3 top-1/2 -translate-y-1/2 opacity-40 pointer-events-none" />
</div>
</div>
</div>
<Input label={`${t('bio')} ${t('optional')}`} name="bio" defaultValue={userProfile?.bio || ''} placeholder="..." />
@@ -2176,16 +2223,31 @@ export default function App() {
<div className="max-w-7xl mx-auto space-y-12 animate-in fade-in duration-700 pb-20">
{!selectedCommunityUser ? (
<>
<div className="relative mb-8">
<div className="relative mb-8 flex gap-4">
<div className="relative flex-1">
<Search className="absolute left-6 top-1/2 -translate-y-1/2 opacity-40 text-inherit" size={24} />
<input
type="text"
placeholder={t('searchUsers')}
value={communitySearchTerm}
onChange={(e) => setCommunitySearchTerm(e.target.value)}
onChange={(e) => {
setCommunitySearchTerm(e.target.value);
if (e.target.value) setShowRecommended(false);
}}
className={`w-full p-6 pl-16 rounded-3xl font-black text-lg outline-none focus:ring-4 focus:ring-primary-500/20 transition-all shadow-xl shadow-black/5 text-inherit ${darkMode ? 'bg-gray-800' : 'bg-white'}`}
/>
</div>
<button
onClick={() => {
setShowRecommended(!showRecommended);
if (!showRecommended) setCommunitySearchTerm('');
}}
className={`px-8 rounded-3xl font-black transition-all shadow-xl shadow-black/5 flex items-center justify-center gap-2 whitespace-nowrap ${showRecommended ? 'bg-primary-600 text-white' : (darkMode ? 'bg-gray-800 text-inherit hover:bg-gray-700' : 'bg-white text-inherit hover:bg-gray-50')}`}
>
<MapPin size={20} />
<span className="hidden sm:inline">{t('recommended') || 'Recomendados'}</span>
</button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{communityUsers.length === 0 ? (
<div className="col-span-full text-center py-12 opacity-50 text-inherit font-black text-xl">
@@ -2210,7 +2272,7 @@ export default function App() {
</>
) : (
<div className="space-y-8">
<button onClick={() => setSelectedCommunityUser(null)} className="flex items-center gap-2 opacity-60 hover:opacity-100 transition-opacity font-black text-inherit uppercase text-xs tracking-widest">
<button onClick={() => { setSelectedCommunityUser(null); setShowInspectModal(false); }} className="flex items-center gap-2 opacity-60 hover:opacity-100 transition-opacity font-black text-inherit uppercase text-xs tracking-widest">
<ChevronLeft size={16} /> Voltar
</button>
@@ -2219,13 +2281,80 @@ export default function App() {
<div className="w-24 h-24 rounded-[2.5rem] bg-primary-600 text-white flex items-center justify-center font-black text-4xl overflow-hidden">
{selectedCommunityUser.avatar ? <img src={selectedCommunityUser.avatar} className="w-full h-full object-cover" alt="Avatar"/> : <span>{(selectedCommunityUser.fullName?.[0] || selectedCommunityUser.username?.[0] || 'U').toUpperCase()}</span>}
</div>
<div className="flex-1 flex justify-between items-start sm:items-center flex-col sm:flex-row gap-4">
<div>
<h3 className="text-3xl font-black tracking-tighter">{selectedCommunityUser.fullName || t('userTitle')}</h3>
<div className="flex items-center gap-2 mt-1">
<p className="opacity-60 font-bold text-sm">@{selectedCommunityUser.username || 'user'}</p>
<button
onClick={(e) => {
e.stopPropagation();
navigator.clipboard.writeText(`@${selectedCommunityUser.username || 'user'}`);
setToastMessage('Username copiado!' || t('copied'));
setTimeout(() => setToastMessage(null), 2000);
}}
className="text-blue-500 hover:text-blue-600 bg-blue-500/10 p-1.5 rounded-md transition-colors"
title="Copiar Username"
>
<Copy size={14} />
</button>
</div>
</div>
<button onClick={() => setShowInspectModal(true)} className="px-5 py-3 bg-primary-100 text-primary-700 dark:bg-primary-900/50 dark:text-primary-300 rounded-2xl font-black text-sm transition-all hover:scale-105 flex items-center gap-2">
<Search size={16} /> Inspecionar
</button>
</div>
</div>
</Card>
{showInspectModal && (
<div className="fixed inset-0 z-[200] flex items-center justify-center bg-black/60 backdrop-blur-sm p-6" onClick={() => setShowInspectModal(false)}>
<Card className="w-full max-w-lg p-8 animate-in zoom-in-95 relative" darkMode={darkMode} onClick={e => e.stopPropagation()}>
<button onClick={() => setShowInspectModal(false)} className="absolute top-6 right-6 opacity-50 hover:opacity-100 text-inherit">
<X size={24} />
</button>
<div className="flex items-center gap-6 mb-8 text-inherit">
<div className="w-20 h-20 rounded-[2rem] bg-primary-600 text-white flex items-center justify-center font-black text-3xl overflow-hidden">
{selectedCommunityUser.avatar ? <img src={selectedCommunityUser.avatar} className="w-full h-full object-cover" alt="Avatar"/> : <span>{(selectedCommunityUser.fullName?.[0] || selectedCommunityUser.username?.[0] || 'U').toUpperCase()}</span>}
</div>
<div>
<h3 className="text-2xl font-black">{selectedCommunityUser.fullName || t('userTitle')}</h3>
<p className="opacity-60 font-bold">@{selectedCommunityUser.username || 'user'}</p>
</div>
</div>
<div className="space-y-6 text-inherit bg-gray-50 dark:bg-gray-800/50 p-6 rounded-3xl">
<div>
<p className="text-xs font-black uppercase tracking-widest opacity-50 mb-1">Data de Nascimento</p>
<p className="font-bold">{selectedUserProfile?.dob ? new Date(selectedUserProfile.dob).toLocaleDateString() : 'Não especificada'}</p>
</div>
<div>
<p className="text-xs font-black uppercase tracking-widest opacity-50 mb-1">Localidade</p>
<p className="font-bold">{selectedUserProfile?.location || selectedCommunityUser.location || 'Não especificada'}</p>
</div>
<div>
<p className="text-xs font-black uppercase tracking-widest opacity-50 mb-1">Bio</p>
<p className="font-bold opacity-80">{selectedUserProfile?.bio || 'Sem biografia'}</p>
</div>
<div className="grid grid-cols-2 gap-4 pt-4 border-t border-gray-200 dark:border-gray-700">
<div>
<p className="text-xs font-black uppercase tracking-widest opacity-50 mb-1">Peças Registadas</p>
<p className="text-2xl font-black">{selectedUserClothes.length}</p>
</div>
<div>
<p className="text-xs font-black uppercase tracking-widest opacity-50 mb-1">Outfits Criados</p>
<p className="text-2xl font-black">{selectedUserLooks.length}</p>
</div>
</div>
<div className="pt-4 border-t border-gray-200 dark:border-gray-700">
<p className="text-xs font-black uppercase tracking-widest opacity-50 mb-1">Data de Registo da Conta</p>
<p className="font-bold">{selectedUserProfile?.createdAt ? new Date(selectedUserProfile.createdAt).toLocaleDateString() : 'Desconhecida'}</p>
</div>
</div>
</Card>
</div>
)}
{selectedCommunityUser.isPrivate ? (
<div className="text-center py-20 opacity-50 font-black text-2xl text-inherit">
<ShieldAlert className="w-16 h-16 mx-auto mb-4 opacity-50" />