Add profile avatar uploads
linter / quality (pull_request) Successful in 1m28s
tests / ci (8.4) (pull_request) Successful in 1m53s

This commit is contained in:
2026-06-24 13:55:27 +02:00
parent 732f49ee80
commit 598f8631f9
18 changed files with 547 additions and 24 deletions
+14 -5
View File
@@ -40,7 +40,7 @@ const form = useForm({
});
const submit = (): void => {
form.patch(save(props.user.id), {
form.patch(save.url(props.user.id), {
preserveScroll: true,
});
};
@@ -126,7 +126,9 @@ const submit = (): void => {
>
<input
id="must_change_password_on_next_login"
v-model="form.must_change_password_on_next_login"
v-model="
form.must_change_password_on_next_login
"
type="checkbox"
class="border-input text-primary focus:ring-ring mt-0.5 size-4 rounded"
/>
@@ -134,13 +136,20 @@ const submit = (): void => {
<span class="block font-medium">
Must change password on next login
</span>
<span class="text-muted-foreground block text-sm">
Keep the user locked to the security settings page until they save a new password.
<span
class="text-muted-foreground block text-sm"
>
Keep the user locked to the security
settings page until they save a new
password.
</span>
</span>
</Label>
<InputError
:message="form.errors.must_change_password_on_next_login"
:message="
form.errors
.must_change_password_on_next_login
"
/>
</div>
+75 -10
View File
@@ -1,10 +1,11 @@
<script setup lang="ts">
import { Form, Head, Link, usePage } from '@inertiajs/vue3';
import { computed } from 'vue';
import { computed, onBeforeUnmount, ref } from 'vue';
import ProfileController from '@/actions/App/Http/Controllers/Settings/ProfileController';
import DeleteUser from '@/components/DeleteUser.vue';
import Heading from '@/components/Heading.vue';
import InputError from '@/components/InputError.vue';
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';
@@ -12,14 +13,10 @@ import AppLayout from '@/layouts/AppLayout.vue';
import SettingsLayout from '@/layouts/settings/Layout.vue';
import { edit } from '@/routes/profile/index';
import { send } from '@/routes/verification';
import type { BreadcrumbItem } from '@/types';
import { getInitials } from '@/composables/useInitials';
import type { BreadcrumbItem, SettingsProfilePageProps } from '@/types';
type Props = {
mustVerifyEmail: boolean;
status?: string;
};
defineProps<Props>();
defineProps<SettingsProfilePageProps>();
const breadcrumbItems: BreadcrumbItem[] = [
{
@@ -30,6 +27,30 @@ const breadcrumbItems: BreadcrumbItem[] = [
const page = usePage();
const user = computed(() => page.props.auth.user);
const avatarPreview = ref<string | null>(null);
const displayedAvatar = computed(
() => avatarPreview.value ?? user.value.avatar,
);
const handleAvatarChange = (event: Event) => {
const input = event.target as HTMLInputElement;
const file = input.files?.[0];
if (avatarPreview.value) {
URL.revokeObjectURL(avatarPreview.value);
avatarPreview.value = null;
}
if (file) {
avatarPreview.value = URL.createObjectURL(file);
}
};
onBeforeUnmount(() => {
if (avatarPreview.value) {
URL.revokeObjectURL(avatarPreview.value);
}
});
</script>
<template>
@@ -43,14 +64,58 @@ const user = computed(() => page.props.auth.user);
<Heading
variant="small"
title="Profile information"
description="Update your name and email address"
description="Update your name, email address, and profile picture"
/>
<Form
v-bind="ProfileController.update.form()"
class="space-y-6"
v-slot="{ errors, processing, recentlySuccessful }"
v-slot="{
errors,
processing,
progress,
recentlySuccessful,
}"
>
<div class="grid gap-3">
<Label for="avatar">Profile picture</Label>
<div class="flex items-center gap-4">
<Avatar class="size-16 overflow-hidden rounded-lg">
<AvatarImage
v-if="displayedAvatar"
:src="displayedAvatar"
:alt="user.name"
/>
<AvatarFallback
class="rounded-lg font-semibold text-black dark:text-white"
>
{{ getInitials(user.name) }}
</AvatarFallback>
</Avatar>
<div class="grid flex-1 gap-2">
<Input
id="avatar"
name="avatar"
type="file"
accept="image/png,image/jpeg,image/webp"
@change="handleAvatarChange"
/>
<p class="text-muted-foreground text-sm">
PNG, JPG, or WebP up to 2 MB.
</p>
</div>
</div>
<progress
v-if="progress"
class="h-1 w-full overflow-hidden rounded-full"
:value="progress.percentage"
max="100"
>
{{ progress.percentage }}%
</progress>
<InputError class="mt-1" :message="errors.avatar" />
</div>
<div class="grid gap-2">
<Label for="name">Name</Label>
<Input