muita coisa, foto de perfil, edit looks, botoes design mlhr
This commit is contained in:
131
src/App.jsx
131
src/App.jsx
@@ -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);
|
||||
const lookData = {
|
||||
name: fd.get('lookName'),
|
||||
items: selectedForLook,
|
||||
updatedAt: new Date().getTime()
|
||||
};
|
||||
try {
|
||||
const looksCol = collection(db, 'artifacts', appId, 'users', user.uid, 'looks');
|
||||
await addDoc(looksCol, {
|
||||
name: fd.get('lookName'),
|
||||
items: selectedForLook,
|
||||
createdAt: new Date().getTime()
|
||||
});
|
||||
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')}
|
||||
</button>
|
||||
<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,7 +805,10 @@ 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>
|
||||
<button onClick={() => deleteLook(look.id)} className="p-2 text-gray-300 hover:text-red-500 transition-colors"><Trash size={18} /></button>
|
||||
<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 => {
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user