muita coisa, foto de perfil, edit looks, botoes design mlhr

This commit is contained in:
2026-04-22 10:11:46 +01:00
parent 7a7a4ca511
commit 2cf8184feb

View File

@@ -44,6 +44,7 @@ export default function App() {
// Estado para criação de Looks
const [selectedForLook, setSelectedForLook] = useState([]);
const [editingLook, setEditingLook] = useState(null);
// Perfil do Utilizador
const [userProfile, setUserProfile] = useState({});
@@ -295,19 +296,27 @@ export default function App() {
}
};
const createLook = async (e) => {
const saveLook = async (e) => {
e.preventDefault();
if (selectedForLook.length < 2) return;
setLoading(true);
const fd = new FormData(e.target);
try {
const looksCol = collection(db, 'artifacts', appId, 'users', user.uid, 'looks');
await addDoc(looksCol, {
const lookData = {
name: fd.get('lookName'),
items: selectedForLook,
createdAt: new Date().getTime()
});
updatedAt: new Date().getTime()
};
try {
if (editingLook) {
const docRef = doc(db, 'artifacts', appId, 'users', user.uid, 'looks', editingLook.id);
await updateDoc(docRef, lookData);
} else {
lookData.createdAt = new Date().getTime();
const looksCol = collection(db, 'artifacts', appId, 'users', user.uid, 'looks');
await addDoc(looksCol, lookData);
}
setSelectedForLook([]);
setEditingLook(null);
setView('outfits');
} catch (e) { console.error(e); }
finally { setLoading(false); }
@@ -370,6 +379,49 @@ export default function App() {
finally { setLoading(false); }
};
const handleProfileImageUpload = (e) => {
const file = e.target.files[0];
if (!file || !user) return;
const reader = new FileReader();
reader.onload = (event) => {
const img = new Image();
img.onload = async () => {
const canvas = document.createElement('canvas');
const MAX_SIZE = 400;
let width = img.width;
let height = img.height;
if (width > height) {
if (width > MAX_SIZE) {
height *= MAX_SIZE / width;
width = MAX_SIZE;
}
} else {
if (height > MAX_SIZE) {
width *= MAX_SIZE / height;
height = MAX_SIZE;
}
}
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0, width, height);
const base64Data = canvas.toDataURL('image/jpeg', 0.8);
try {
const profileDoc = doc(db, 'artifacts', appId, 'users', user.uid, 'profile', 'data');
await setDoc(profileDoc, { avatar: base64Data }, { merge: true });
} catch (err) {
console.error("Error uploading image:", err);
}
};
img.src = event.target.result;
};
reader.readAsDataURL(file);
};
const saveProfile = async (e) => {
e.preventDefault();
setSavingProfile(true);
@@ -478,8 +530,12 @@ export default function App() {
<div className="mt-auto pt-10 border-t border-inherit">
<button onClick={() => setView('profile')} className="w-full flex items-center gap-4 mb-8 px-2 text-left hover:bg-gray-100 dark:hover:bg-gray-800 py-3 rounded-2xl transition-all cursor-pointer">
<div className={`w-12 h-12 rounded-2xl shrink-0 flex items-center justify-center font-black text-white shadow-xl ${darkMode ? 'bg-primary-500' : 'bg-primary-600'}`}>
{(userProfile?.fullName?.[0] || userProfile?.username?.[0] || user?.email?.[0] || 'U').toUpperCase()}
<div className={`w-12 h-12 rounded-2xl shrink-0 flex items-center justify-center font-black text-white shadow-xl overflow-hidden ${darkMode ? 'bg-primary-500' : 'bg-primary-600'}`}>
{userProfile?.avatar ? (
<img src={userProfile.avatar} className="w-full h-full object-cover" alt="Avatar" />
) : (
(userProfile?.fullName?.[0] || userProfile?.username?.[0] || user?.email?.[0] || 'U').toUpperCase()
)}
</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-black truncate">{userProfile?.username || userProfile?.fullName || user?.email?.split('@')[0] || t('userTitle')}</p>
@@ -517,7 +573,7 @@ export default function App() {
<button onClick={() => handleDarkModeToggle(false)} className={`p-2 rounded-xl ${!darkMode ? 'bg-white shadow-md text-primary-600' : 'text-gray-500'}`}><Sun size={18} /></button>
<button onClick={() => handleDarkModeToggle(true)} className={`p-2 rounded-xl ${darkMode ? 'bg-gray-900 shadow-md text-primary-400' : 'text-gray-500'}`}><Moon size={18} /></button>
</div>
<button onClick={() => { setEditingItem(null); setImageUrlDraft(''); setView('add'); }} className="p-4 bg-primary-600 text-white rounded-2xl shadow-xl shadow-primary-600/30 hover:scale-105 active:scale-95 transition-all">
<button onClick={() => { setEditingItem(null); setImageUrlDraft(''); setView('add'); setEditingLook(null); setSelectedForLook([]); }} className="p-4 bg-primary-600 text-white rounded-2xl shadow-xl shadow-primary-600/30 hover:scale-105 active:scale-95 transition-all">
<Plus size={24} />
</button>
</div>
@@ -629,22 +685,22 @@ export default function App() {
<Card className="overflow-hidden p-0 h-[480px] relative border-none hover:shadow-2xl transition-all duration-500" darkMode={darkMode}>
<img src={item.imageUrl} className="w-full h-full object-cover transition-transform duration-1000 group-hover:scale-110" alt={item.name} />
<div className="absolute inset-0 bg-gradient-to-t from-black/80 via-transparent opacity-0 group-hover:opacity-100 transition-all duration-300 flex flex-col justify-end p-8 text-white">
<div className="grid grid-cols-2 gap-3">
<button onClick={() => { setEditingItem(item); setImageUrlDraft(''); setView('edit'); }} className="py-4 bg-white text-primary-600 rounded-2xl font-black text-[10px] uppercase flex items-center justify-center gap-2 hover:bg-primary-50"><Edit2 size={16} /> {t('edit')}</button>
<button onClick={() => handleItemAction('laundry', item)} className="py-4 bg-blue-600 text-white rounded-2xl font-black text-[10px] uppercase flex items-center justify-center gap-2 hover:bg-blue-700"><Droplets size={16} /> {t('makeDirty')}</button>
<button onClick={() => handleItemAction('trash', item)} className="py-4 bg-red-600/20 text-red-100 backdrop-blur-md rounded-2xl font-black text-[10px] uppercase hover:bg-red-600 transition-colors col-span-2">{t('moveToTrash')}</button>
<div className="absolute inset-0 bg-gradient-to-t from-black/80 via-transparent opacity-0 group-hover:opacity-100 transition-all duration-300 flex flex-col justify-end p-6 pb-[136px] text-white z-10 pointer-events-none">
<div className="grid grid-cols-2 gap-2 pointer-events-auto">
<button onClick={() => { setEditingItem(item); setImageUrlDraft(''); setView('edit'); }} className="py-3 px-2 bg-white text-primary-600 rounded-xl font-black text-[9px] uppercase flex items-center justify-center gap-1.5 hover:bg-primary-50"><Edit2 size={14} /> {t('edit')}</button>
<button onClick={() => handleItemAction('laundry', item)} className="py-3 px-2 bg-blue-600 text-white rounded-xl font-black text-[9px] uppercase flex items-center justify-center gap-1.5 hover:bg-blue-700"><Droplets size={14} /> {t('makeDirty')}</button>
<button onClick={() => handleItemAction('trash', item)} className="py-3 px-2 bg-red-600/20 text-red-100 backdrop-blur-md rounded-xl font-black text-[9px] uppercase hover:bg-red-600 transition-colors col-span-2">{t('moveToTrash')}</button>
</div>
</div>
<div className="absolute top-6 left-6"><Badge>{item.category}</Badge></div>
<div className="absolute top-6 right-6">
<div className="absolute top-6 left-6 z-20"><Badge>{item.category}</Badge></div>
<div className="absolute top-6 right-6 z-20 pointer-events-auto">
<button onClick={() => handleItemAction('favorite', item)} className={`p-3 rounded-2xl shadow-xl backdrop-blur-md transition-all ${item.favorite ? 'bg-rose-500 text-white' : 'bg-white/90 text-gray-400'}`}>
<Heart size={18} fill={item.favorite ? "currentColor" : "none"} />
</button>
</div>
<div className="absolute bottom-6 left-6 right-6 p-6 bg-white/95 dark:bg-gray-900/95 backdrop-blur-2xl rounded-3xl shadow-2xl transform transition-transform group-hover:-translate-y-2">
<div className="absolute bottom-6 left-6 right-6 p-6 bg-white/95 dark:bg-gray-900/95 backdrop-blur-2xl rounded-3xl shadow-2xl transform transition-transform group-hover:-translate-y-2 z-20 pointer-events-auto">
<h4 className="text-xl font-black tracking-tighter truncate">{item.name}</h4>
<div className="flex items-center gap-3 mt-2">
<div className="w-4 h-4 rounded-full border border-black/5" style={{ backgroundColor: (item.color || "").toLowerCase() }}></div>
@@ -695,9 +751,11 @@ export default function App() {
<div className="grid grid-cols-1 lg:grid-cols-3 gap-12">
<div className="lg:col-span-1 space-y-8">
<Card className="p-8 border-primary-200" darkMode={darkMode}>
<h3 className="text-2xl font-black tracking-tighter mb-6 flex items-center gap-3 text-inherit"><Sparkles className="text-primary-600" /> {t('createNewLook')}</h3>
<form onSubmit={createLook} className="space-y-6">
<input name="lookName" placeholder={t('lookName')} required className={`w-full p-4 rounded-xl border-none shadow-inner font-bold ${darkMode ? 'bg-gray-700' : 'bg-gray-100'}`} />
<h3 className="text-2xl font-black tracking-tighter mb-6 flex items-center gap-3 text-inherit">
<Sparkles className="text-primary-600" /> {editingLook ? t('editLook') || 'Editar Look' : t('createNewLook')}
</h3>
<form key={editingLook ? editingLook.id : 'new'} onSubmit={saveLook} className="space-y-6">
<input name="lookName" placeholder={t('lookName')} defaultValue={editingLook?.name || ''} required className={`w-full p-4 rounded-xl border-none shadow-inner font-bold ${darkMode ? 'bg-gray-700' : 'bg-gray-100'}`} />
<div className="space-y-3">
<p className="text-[10px] font-black uppercase opacity-40 tracking-widest">{t('selectedPieces')} ({selectedForLook.length})</p>
<div className="flex flex-wrap gap-2">
@@ -706,16 +764,21 @@ export default function App() {
return (
<div key={id} className="relative group">
<img src={item?.imageUrl} className="w-12 h-12 rounded-lg object-cover border-2 border-primary-500" alt="" />
<button onClick={() => setSelectedForLook(selectedForLook.filter(i => i !== id))} className="absolute -top-1 -right-1 bg-red-500 text-white rounded-full p-0.5 opacity-0 group-hover:opacity-100 transition-opacity"><X size={10} /></button>
<button type="button" onClick={() => setSelectedForLook(selectedForLook.filter(i => i !== id))} className="absolute -top-1 -right-1 bg-red-500 text-white rounded-full p-0.5 opacity-0 group-hover:opacity-100 transition-opacity"><X size={10} /></button>
</div>
);
})}
{selectedForLook.length === 0 && <p className="text-xs text-gray-400 italic">{t('selectPieces')}</p>}
</div>
</div>
<button disabled={selectedForLook.length < 2} className="w-full py-4 bg-primary-600 text-white rounded-2xl font-black uppercase tracking-widest text-xs shadow-xl shadow-primary-600/30 disabled:opacity-30 transition-all">
{t('saveLook')}
<div className="flex gap-4">
{editingLook && (
<button type="button" onClick={() => { setEditingLook(null); setSelectedForLook([]); }} className="flex-1 py-4 font-black uppercase text-[10px] tracking-widest text-gray-500 hover:text-gray-900 transition-colors">{t('cancel')}</button>
)}
<button disabled={selectedForLook.length < 2} className="flex-[2] py-4 bg-primary-600 text-white rounded-2xl font-black uppercase tracking-widest text-xs shadow-xl shadow-primary-600/30 disabled:opacity-30 transition-all">
{editingLook ? t('saveChanges') || 'Guardar' : t('saveLook')}
</button>
</div>
</form>
</Card>
@@ -742,8 +805,11 @@ export default function App() {
<h4 className="text-xl font-black tracking-tight">{look.name}</h4>
<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={() => { setEditingLook(look); setSelectedForLook(look.items); }} className="p-2 text-gray-300 hover:text-primary-500 transition-colors"><Edit2 size={18} /></button>
<button onClick={() => deleteLook(look.id)} className="p-2 text-gray-300 hover:text-red-500 transition-colors"><Trash size={18} /></button>
</div>
</div>
<div className="flex -space-x-4">
{look.items.map(itemId => {
const item = clothes.find(c => c.id === itemId);
@@ -811,8 +877,17 @@ export default function App() {
<div className="max-w-4xl mx-auto space-y-12 animate-in fade-in duration-700 pb-20">
<Card className="p-10 border-primary-100 relative overflow-hidden" darkMode={darkMode}>
<div className="flex items-center gap-8 relative z-10 text-inherit">
<div className="w-24 h-24 rounded-[2.5rem] bg-primary-600 flex items-center justify-center text-white text-4xl font-black shadow-2xl">
{(userProfile?.fullName?.[0] || userProfile?.username?.[0] || user?.email?.[0] || 'U').toUpperCase()}
<div className="w-24 h-24 rounded-[2.5rem] bg-primary-600 flex items-center justify-center text-white text-4xl font-black shadow-2xl relative overflow-hidden group cursor-pointer">
{userProfile?.avatar ? (
<img src={userProfile.avatar} className="w-full h-full object-cover" alt="Profile" />
) : (
<span>{(userProfile?.fullName?.[0] || userProfile?.username?.[0] || user?.email?.[0] || 'U').toUpperCase()}</span>
)}
<label className="absolute inset-0 bg-black/50 flex flex-col items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity cursor-pointer text-white">
<Edit2 size={20} />
<span className="text-[8px] uppercase font-black mt-1 tracking-widest">{t('edit')}</span>
<input type="file" accept="image/*" className="hidden" onChange={handleProfileImageUpload} />
</label>
</div>
<div>
<h3 className="text-3xl font-black tracking-tighter">{userProfile?.fullName || t('yourAccount')}</h3>