Files
KanbanGit/resources/js/pages/settings/Profile.vue
T
Twyco 1f16393dca
linter / quality (pull_request) Successful in 1m28s
tests / ci (8.4) (pull_request) Successful in 1m54s
Added UserAvatar
2026-06-24 16:34:00 +02:00

212 lines
8.2 KiB
Vue

<script setup lang="ts">
import { Form, Head, Link, useForm, usePage } from '@inertiajs/vue3';
import { Trash2 } from 'lucide-vue-next';
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 { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
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, SettingsProfilePageProps } from '@/types';
import UserAvatar from '@/components/ui/avatar/UserAvatar.vue';
defineProps<SettingsProfilePageProps>();
const breadcrumbItems: BreadcrumbItem[] = [
{
title: 'Profile settings',
href: edit(),
},
];
const page = usePage();
const user = computed(() => page.props.auth.user);
const avatarPreview = ref<string | null>(null);
const avatarDeleteForm = useForm({});
const displayedAvatar = computed(
() => avatarPreview.value ?? user.value.avatar,
);
const canRemoveAvatar = computed(
() => user.value.avatar !== null && !user.value.avatar_is_generated,
);
const clearAvatarPreview = (): void => {
if (avatarPreview.value) {
URL.revokeObjectURL(avatarPreview.value);
avatarPreview.value = null;
}
};
const handleAvatarChange = (event: Event) => {
const input = event.target as HTMLInputElement;
const file = input.files?.[0];
clearAvatarPreview();
if (file) {
avatarPreview.value = URL.createObjectURL(file);
}
};
const removeAvatar = (): void => {
avatarDeleteForm.delete(ProfileController.destroyAvatar.url(), {
preserveScroll: true,
onSuccess: clearAvatarPreview,
});
};
onBeforeUnmount(() => {
clearAvatarPreview();
});
</script>
<template>
<AppLayout :breadcrumbs="breadcrumbItems">
<Head title="Profile settings" />
<h1 class="sr-only">Profile settings</h1>
<SettingsLayout>
<div class="flex flex-col space-y-6">
<Heading
variant="small"
title="Profile information"
description="Update your name, email address, and profile picture"
/>
<Form
v-bind="ProfileController.update.form()"
class="space-y-6"
v-slot="{
errors,
processing,
progress,
recentlySuccessful,
}"
>
<div class="grid gap-3">
<Label for="avatar">Profile picture</Label>
<div class="flex items-center gap-4">
<UserAvatar class="size-16" />
<div class="grid flex-1 gap-2">
<div class="flex flex-col gap-2 sm:flex-row">
<Input
id="avatar"
name="avatar"
type="file"
accept="image/png,image/jpeg,image/webp"
@change="handleAvatarChange"
/>
<Button
v-if="canRemoveAvatar"
type="button"
variant="outline"
:disabled="avatarDeleteForm.processing"
data-test="delete-profile-picture-button"
@click="removeAvatar"
>
<Trash2 class="size-4" />
Remove
</Button>
</div>
<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
id="name"
class="mt-1 block w-full"
name="name"
:default-value="user.name"
required
autocomplete="name"
placeholder="Full name"
/>
<InputError class="mt-2" :message="errors.name" />
</div>
<div class="grid gap-2">
<Label for="email">Email address</Label>
<Input
id="email"
type="email"
class="mt-1 block w-full"
name="email"
:default-value="user.email"
required
autocomplete="username"
placeholder="Email address"
/>
<InputError class="mt-2" :message="errors.email" />
</div>
<div v-if="mustVerifyEmail && !user.email_verified_at">
<p class="text-muted-foreground -mt-4 text-sm">
Your email address is unverified.
<Link
:href="send()"
as="button"
class="text-foreground hover:decoration-current! underline decoration-neutral-300 underline-offset-4 transition-colors duration-300 ease-out dark:decoration-neutral-500"
>
Click here to resend the verification email.
</Link>
</p>
<div
v-if="status === 'verification-link-sent'"
class="mt-2 text-sm font-medium text-green-600"
>
A new verification link has been sent to your email
address.
</div>
</div>
<div class="flex items-center gap-4">
<Button
:disabled="processing"
data-test="update-profile-button"
>Save</Button
>
<Transition
enter-active-class="transition ease-in-out"
enter-from-class="opacity-0"
leave-active-class="transition ease-in-out"
leave-to-class="opacity-0"
>
<p
v-show="recentlySuccessful"
class="text-sm text-neutral-600"
>
Saved.
</p>
</Transition>
</div>
</Form>
</div>
<DeleteUser />
</SettingsLayout>
</AppLayout>
</template>