Users can now upload an avatar from Profile settings.

This commit is contained in:
2026-05-16 04:00:06 +03:30
parent f46f32aa4c
commit 61b4cf9252
13 changed files with 274 additions and 19 deletions

View File

@@ -1,11 +1,15 @@
import { Form, Head, Link, usePage } from '@inertiajs/react';
import { ImagePlus } from 'lucide-react';
import { useEffect, useRef, useState } from 'react';
import ProfileController from '@/actions/App/Http/Controllers/Settings/ProfileController';
import DeleteUser from '@/components/delete-user';
import Heading from '@/components/heading';
import InputError from '@/components/input-error';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { useInitials } from '@/hooks/use-initials';
import { edit } from '@/routes/profile';
import { send } from '@/routes/verification';
@@ -18,11 +22,34 @@ export default function Profile({
}) {
const { auth } = usePage().props;
const user = auth.user;
const getInitials = useInitials();
const avatarInputRef = useRef<HTMLInputElement>(null);
const [avatarPreviewUrl, setAvatarPreviewUrl] = useState<string | null>(
null,
);
useEffect(() => {
return () => {
if (avatarPreviewUrl) {
URL.revokeObjectURL(avatarPreviewUrl);
}
};
}, [avatarPreviewUrl]);
if (!user) {
return null;
}
function previewAvatar(file: File | null) {
setAvatarPreviewUrl((currentUrl) => {
if (currentUrl) {
URL.revokeObjectURL(currentUrl);
}
return file ? URL.createObjectURL(file) : null;
});
}
return (
<>
<Head title="Profile settings" />
@@ -33,7 +60,7 @@ export default function Profile({
<Heading
variant="small"
title="Profile information"
description="Update your name and email address"
description="Update your name, email address, and avatar"
/>
<Form
@@ -41,10 +68,66 @@ export default function Profile({
options={{
preserveScroll: true,
}}
resetOnSuccess={['avatar']}
onSuccess={() => {
setAvatarPreviewUrl(null);
if (avatarInputRef.current) {
avatarInputRef.current.value = '';
}
}}
encType="multipart/form-data"
className="space-y-6"
>
{({ processing, errors }) => (
{({ processing, progress, errors }) => (
<>
<div className="flex flex-col gap-4 rounded-md border bg-card p-4 sm:flex-row sm:items-center">
<Avatar className="size-20 rounded-md">
<AvatarImage
src={
avatarPreviewUrl ??
user.avatar ??
undefined
}
alt={user.name}
className="object-cover"
/>
<AvatarFallback className="rounded-md bg-secondary text-lg font-semibold text-secondary-foreground">
{getInitials(user.name)}
</AvatarFallback>
</Avatar>
<div className="grid min-w-0 flex-1 gap-2">
<Label htmlFor="avatar">Avatar</Label>
<Input
id="avatar"
ref={avatarInputRef}
name="avatar"
type="file"
accept="image/jpeg,image/png,image/webp"
onChange={(event) =>
previewAvatar(
event.target.files?.[0] ?? null,
)
}
/>
<div className="flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
<ImagePlus className="size-3.5" />
<span>
JPG, PNG, or WebP up to 2 MB.
</span>
</div>
{progress && (
<progress
value={progress.percentage}
max="100"
className="h-1.5 w-full"
/>
)}
<InputError message={errors.avatar} />
</div>
</div>
<div className="grid gap-2">
<Label htmlFor="name">Name</Label>