Wire avatar upload on Create/Update profile pages
Camera button now triggers file input -> uploads to POST /users/avatar -> updates auth context and preview. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import { useState } from 'react';
|
import { useState, useRef } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { User, Mail, Phone, Camera } from 'lucide-react';
|
import { User, Mail, Phone, Camera } from 'lucide-react';
|
||||||
import { Input } from '../components/ui/Input';
|
import { Input } from '../components/ui/Input';
|
||||||
@@ -19,6 +19,22 @@ export function CreateProfilePage() {
|
|||||||
const [bio, setBio] = useState('');
|
const [bio, setBio] = useState('');
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
|
const [avatarPreview, setAvatarPreview] = useState<string | undefined>(user?.avatar || undefined);
|
||||||
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
const handleAvatarChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const file = e.target.files?.[0];
|
||||||
|
if (!file) return;
|
||||||
|
setAvatarPreview(URL.createObjectURL(file));
|
||||||
|
try {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('avatar', file);
|
||||||
|
const result = await api.upload<Record<string, unknown>>('/users/avatar', formData);
|
||||||
|
updateUser(result);
|
||||||
|
} catch {
|
||||||
|
setError('Failed to upload avatar');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -51,8 +67,9 @@ export function CreateProfilePage() {
|
|||||||
|
|
||||||
<div className="flex justify-center mb-8">
|
<div className="flex justify-center mb-8">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Avatar name={fullName || 'U'} size="xl" />
|
<Avatar name={fullName || 'U'} size="xl" src={avatarPreview} />
|
||||||
<button type="button" className="absolute -bottom-1 -right-1 p-2 bg-gradient-to-r from-pink-500 to-primary-600 rounded-full text-white shadow-lg cursor-pointer">
|
<input ref={fileInputRef} type="file" accept="image/*" className="hidden" onChange={handleAvatarChange} />
|
||||||
|
<button type="button" onClick={() => fileInputRef.current?.click()} className="absolute -bottom-1 -right-1 p-2 bg-gradient-to-r from-pink-500 to-primary-600 rounded-full text-white shadow-lg cursor-pointer">
|
||||||
<Camera className="w-4 h-4" />
|
<Camera className="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState } from 'react';
|
import { useState, useRef } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { Camera } from 'lucide-react';
|
import { Camera } from 'lucide-react';
|
||||||
import { Input } from '../components/ui/Input';
|
import { Input } from '../components/ui/Input';
|
||||||
@@ -23,6 +23,20 @@ export function UpdateProfilePage() {
|
|||||||
const [showPhone, setShowPhone] = useState(user?.showPhone ?? true);
|
const [showPhone, setShowPhone] = useState(user?.showPhone ?? true);
|
||||||
const [showLocation, setShowLocation] = useState(user?.showLocation ?? true);
|
const [showLocation, setShowLocation] = useState(user?.showLocation ?? true);
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
|
const [avatarPreview, setAvatarPreview] = useState<string | undefined>(user?.avatar || undefined);
|
||||||
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
const handleAvatarChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const file = e.target.files?.[0];
|
||||||
|
if (!file) return;
|
||||||
|
setAvatarPreview(URL.createObjectURL(file));
|
||||||
|
try {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('avatar', file);
|
||||||
|
const result = await api.upload<Record<string, unknown>>('/users/avatar', formData);
|
||||||
|
updateUser(result);
|
||||||
|
} catch {}
|
||||||
|
};
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -47,8 +61,9 @@ export function UpdateProfilePage() {
|
|||||||
|
|
||||||
<div className="flex justify-center mb-8">
|
<div className="flex justify-center mb-8">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Avatar name={fullName} size="xl" />
|
<Avatar name={fullName} size="xl" src={avatarPreview} />
|
||||||
<button type="button" className="absolute -bottom-1 -right-1 p-2 bg-gradient-to-r from-pink-500 to-primary-600 rounded-full text-white shadow-lg cursor-pointer">
|
<input ref={fileInputRef} type="file" accept="image/*" className="hidden" onChange={handleAvatarChange} />
|
||||||
|
<button type="button" onClick={() => fileInputRef.current?.click()} className="absolute -bottom-1 -right-1 p-2 bg-gradient-to-r from-pink-500 to-primary-600 rounded-full text-white shadow-lg cursor-pointer">
|
||||||
<Camera className="w-4 h-4" />
|
<Camera className="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user