Skip to content

Commit

Permalink
implement our own avatars
Browse files Browse the repository at this point in the history
  • Loading branch information
joshmanders committed Oct 12, 2024
1 parent e8086a0 commit 3bb3358
Show file tree
Hide file tree
Showing 10 changed files with 201 additions and 9 deletions.
6 changes: 6 additions & 0 deletions app/Http/Controllers/AccountController.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use Illuminate\Http\Request;
use \Illuminate\Validation\ValidationException;
use Inertia\Response;
use NiftyCo\Attachments\Attachment;

class AccountController extends Controller
{
Expand All @@ -22,12 +23,17 @@ public function update(Request $request)
$request->validate([
'name' => 'required|string',
'email' => 'required|email:rfc,dns',
'avatar' => ['nullable', 'mimes:jpg,jpeg,png', 'max:1024'],
]);

$user = auth()->user();

$user->fill($request->only('name'));

if ($request->hasFile('avatar')) {
$user->avatar = Attachment::fromFile($request->file('avatar'), folder: 'avatars');
}

if ($user->email !== $request->email) {
$user->fill([
'confirmed_at' => null,
Expand Down
31 changes: 31 additions & 0 deletions app/Http/Controllers/DestroyAvatarController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;

class DestroyAvatarController extends Controller
{
/**
* Handle the incoming request.
*/
public function __invoke(Request $request)
{
$user = $request->user();

if (is_null($user->avatar)) {
return inertia()->location(route('account'));
}

$avatar = $user->avatar->toArray();

Storage::disk($avatar['disk'])->delete($avatar['name']);

$user->forceFill([
'avatar' => null,
])->save();

return inertia()->location(route('account'));
}
}
3 changes: 2 additions & 1 deletion app/Http/Resources/UserResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ public function toArray(Request $request): array
return [
'id' => $this->id,
'name' => $this->name,
'email' => $this->email
'email' => $this->email,
'avatar' => $this->avatar ? $this->avatar->url : 'https://ui-avatars.com/api/?background=a0a0a0&name=' . urlencode($this->name),
];
}
}
2 changes: 2 additions & 0 deletions app/Models/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use Illuminate\Contracts\Auth\Access\Authorizable as AuthorizableContract;
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
use Illuminate\Foundation\Auth\Access\Authorizable;
use NiftyCo\Attachments\Casts\AsAttachment;

class User extends Model implements AuthenticatableContract, AuthorizableContract
{
Expand All @@ -27,6 +28,7 @@ protected function casts(): array
return [
'confirmed_at' => 'datetime',
'password' => 'hashed',
'avatar' => AsAttachment::class,
];
}

Expand Down
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"require": {
"php": "^8.2",
"aniftyco/laravel-advanced-db-sessions": "dev-master",
"aniftyco/laravel-attachments": "dev-master",
"inertiajs/inertia-laravel": "^1.3",
"laravel/framework": "^11.9",
"laravel/tinker": "^2.9",
Expand Down
67 changes: 65 additions & 2 deletions composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions database/migrations/0001_01_01_000000_create_users_table.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ public function up(): void

$table->string('role')->default(UserRole::CUSTOMER)->index();

$table->jsonb('avatar')->nullable();

$table->timestamp('confirmed_at')->nullable();
$table->timestamps();
$table->softDeletes();
Expand Down
86 changes: 83 additions & 3 deletions resources/client/forms/UpdateAccountInformation.vue
Original file line number Diff line number Diff line change
@@ -1,24 +1,73 @@
<script setup lang="ts">
import { useForm } from '@inertiajs/vue3';
import { router, useForm } from '@inertiajs/vue3';
import { ActionMessage, Button, FormSection, Field, Label, Input, ErrorMessage } from '@app/components/ui';
import { ref } from 'vue';
export type Props = {
user: {
name: string;
email: string;
avatar?: string;
};
};
const props = defineProps<Props>();
const form = useForm(props.user);
const form = useForm({
_method: 'PUT',
name: props.user.name,
email: props.user.email,
avatar: null,
});
const avatarPreview = ref(null);
const avatarInput = ref(null);
const submit = () => {
form.put(route('account.update'), {
if (avatarInput.value) {
form.avatar = avatarInput.value.files[0];
}
form.post(route('account.update'), {
errorBag: 'submit',
preserveScroll: true,
onSuccess: () => clearAvatarFileInput(),
});
};
const selectNewAvatar = () => {
avatarInput.value.click();
};
const updateAvatarPreview = () => {
const avatar = avatarInput.value.files[0];
if (!avatar) return;
const reader = new FileReader();
reader.onload = (e) => {
avatarPreview.value = e.target.result;
};
reader.readAsDataURL(avatar);
};
const deleteAvatar = () => {
router.delete(route('avatar.destroy'), {
preserveScroll: true,
onSuccess: () => {
avatarPreview.value = null;
clearAvatarFileInput();
},
});
};
const clearAvatarFileInput = () => {
if (avatarInput.value?.value) {
avatarInput.value.value = null;
}
};
</script>

<template>
Expand All @@ -28,6 +77,37 @@ const submit = () => {
<template #description>Update your account information and email address.</template>

<template #form>
<!-- Profile avatar -->
<Field class="col-span-6 sm:col-span-4">
<!-- Profile avatar File Input -->
<input id="avatar" ref="avatarInput" type="file" class="hidden" @change="updateAvatarPreview" />

<Label for="avatar">Avatar</Label>

<!-- Current Profile avatar -->
<div v-show="!avatarPreview" class="mt-2">
<img :src="user.avatar" :alt="user.name" class="rounded-full h-20 w-20 object-cover" />
</div>

<!-- New Profile avatar Preview -->
<div v-show="avatarPreview" class="mt-2">
<span
class="block rounded-full w-20 h-20 bg-cover bg-no-repeat bg-center"
:style="'background-image: url(\'' + avatarPreview + '\');'"
/>
</div>

<Button type="button" class="mt-2 me-2" variant="secondary" v-on:click.prevent="selectNewAvatar"
>Select A New Avatar</Button
>

<Button type="button" v-if="user.avatar" variant="secondary" class="mt-2" v-on:click.prevent="deleteAvatar">
Remove Avatar
</Button>

<ErrorMessage v-if="form.errors.avatar" class="mt-2">{{ form.errors.avatar }}</ErrorMessage>
</Field>

<!-- Name -->
<Field class="col-span-6 sm:col-span-4">
<Label for="name">Name</Label>
Expand Down
9 changes: 6 additions & 3 deletions resources/client/layouts/app.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,14 @@ type PageProps = {
user: {
name: string;
email: string;
avatar?: string;
};
};
};
const { props } = usePage<PageProps>();
const showingNavigationDropdown = ref(false);
const gravatar = 'https://gravatar.com/userimage/19287721/9cd577bdd3c1f36f600a246863a5f7bb.jpeg?size=256';
type Team = {
id: number;
name: string;
Expand Down Expand Up @@ -160,7 +159,11 @@ const signOut = () => {
type="button"
class="inline-flex items-center px-2 py-1.5 border border-transparent text-sm leading-4 font-medium rounded-md text-background-500 dark:text-background-400 bg-white dark:bg-background-800 hover:text-background-700 dark:hover:text-background-300 focus:outline-none focus:bg-background-100 dark:focus:bg-background-700 hover:bg-background-100 dark:hover:bg-background-700 active:bg-background-100 dark:active:bg-background-700 transition ease-in-out duration-150"
>
<img class="size-5 rounded-full object-cover" :src="gravatar" :alt="props.auth.user.name" />
<img
class="size-5 rounded-full object-cover"
:src="props.auth.user.avatar"
:alt="props.auth.user.name"
/>

<span class="ms-2 -me-0.5">{{ props.auth.user.name }}</span>

Expand Down
3 changes: 3 additions & 0 deletions routes/web.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

use App\Http\Controllers\AccountController;
use App\Http\Controllers\ConfirmPasswordController;
use App\Http\Controllers\DestroyAvatarController;
use App\Http\Controllers\ForgotPasswordController;
use App\Http\Controllers\PasswordController;
use App\Http\Controllers\ResetPasswordController;
Expand Down Expand Up @@ -53,6 +54,8 @@

Route::delete('/account/sessions', [SessionsController::class, 'destroy'])->name('sessions.destroy');

Route::delete('/account/avatar', DestroyAvatarController::class)->name('avatar.destroy');

Route::get('/banner', function () {
session()->flash('flash', [
'banner' => [
Expand Down

0 comments on commit 3bb3358

Please sign in to comment.