Silver343
Posted on October 6, 2023
The first new feature we are going to tackle is a public profile page for users.
On this page, we should only be able to see Chirps the profile owner has created.
New Route
To start we will add a new route in the web.php file. There are already routes for updating and deleting the user profile so we can add our new route for showing it there.
Route::middleware('auth')->group(function () {
Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit');
Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update');
Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy');
+ Route::get('/users/{user}', [ProfileController::class, 'show'])->middleware('verified')->name('profile.show');
});
Note we have added an extra middleware to our new route as we only want the public profiles to be accessible to users who have verified their email address.
We must also update our user model to implement MustVerifyEmail to use the middleware.
- //use Illuminate\Contracts\Auth\MustVerifyEmail;
+ use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;
+class User extends Authenticatable implements MustVerifyEmail
You’ll notice the {user}
in our new route, this is a route parameter, with this parameter we will inject the id of the user whose profile we wish to show, Laravel provides a convenient way to do this via route model binding.
New method in the Profile Controller
In the Profile controller, we will add a new show method. We want to pass the user model and their Chirps to a new Vue component.
public function show(User $user): Response
{
return Inertia::render('Profile/Show',[
'user' => $user->only(['id', 'name', 'created_at']),
'chirps' => $user->chirps()->latest()->get()->map(fn (Chirp $chirp) => $chirp->setRelation('user',$user)),
]);
}
We use the setRelation on each of the users chirps to manually set the chirps author, this allows us to avoid loading the user for each chirp avoiding a n+1 problem.
Create Profile show page
In our controller above, we are passing our user and chirps to a new Vue component, which we now need to create.
Create a Vue file at resources/js/Pages/Profile and name it Show.vue.
Add the following code.
<script setup>
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue';
import Heading from '@/Components/Heading.vue';
import Chirp from '@/Components/Chirp.vue';
import { Head } from '@inertiajs/vue3';
defineProps(['user','chirps']);
</script>
<template>
<Head :title="user.name" />
<AuthenticatedLayout>
<Heading :user="user"/>
<div class="max-w-2xl mx-auto p-4 sm:p-6 lg:p-8">
<div class="mt-6 bg-white shadow-sm rounded-lg divide-y">
<Chirp
v-for="chirp in chirps"
:key="chirp.id"
:chirp="chirp"
/>
</div>
</div>
</AuthenticatedLayout>
</template>
We are using the Authenticated Layout included with breeze in addition to the Chirp component you created in the bootcamp.
There is a new Heading component above the chirps, which we will create now.
Create the new heading component
To create the new heading component, create a new Vue file at resources/js/components and save it as Heading.vue. Add the following code.
<script setup>
import dayjs from 'dayjs';
import { Link } from '@inertiajs/vue3'
const props = defineProps(['user']);
</script>
<template>
<div class="bg-white">
<div class="max-w-7xl mx-auto pb-1 px-4 sm:px-6 lg:px-8 p-4 sm:py-6 lg:py-8 sm:flex sm:items-center sm:justify-between sm:space-x-5">
<div class="flex items-start space-x-5">
<div class="pt-1.5">
<h1 class="text-2xl font-bold text-gray-900 capitalize">{{ user.name }}</h1>
<p class="text-sm font-medium text-gray-500">
Joined
<time :datetime="dayjs(user.created_at).format('YYYY-MM')">
{{ dayjs(user.created_at).format('MMMM YYYY') }}
</time>
</p>
</div>
</div>
<div v-if="user.id === $page.props.auth.user.id" class="my-3 flex flex-col-reverse justify-stretch sm:mt-0 sm:pr-3">
<Link :href="route('profile.edit')" class="inline-flex items-center justify-center px-4 py-2 bg-gray-800 border border-transparent rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-gray-700 focus:bg-gray-700 active:bg-gray-900 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 transition ease-in-out duration-150">Edit Profile</Link>
</div>
</div>
</div>
</template>
This heading is based on this on included with Tailwind UI
We have removed the avatar, and we only have one action.
We are using the day.js library to format the user's created_at field to show the month and year they created their account.
If a user is visiting their own profile the component renders a link to allow them to edit it, this uses the included profile.edit route included with breeze.
How do we visit profiles?
We have created our new profile page, but it is only reachable by manually updating the URL.
We will add two ways to navigate to the profile screen, one in the chirp component to view the profile of the chirps author and one in the settings dropdown to allow users to view their own profile.
Update your Chirp component like so.
<script setup>
import Dropdown from '@/Components/Dropdown.vue';
import DropdownLink from '@/components/DropdownLink.vue';
import InputError from '@/Components/InputError.vue';
import PrimaryButton from '@/Components/PrimaryButton.vue';
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
-import { useForm } from '@inertiajs/vue3';
+import { Link, useForm } from '@inertiajs/vue3';
import { ref } from 'vue';
dayjs.extend(relativeTime);
<div class="flex-1">
<div class="flex justify-between items-center">
<div>
- <span class="text-gray-800">{{ chirp.user.name }}</span>
+ <Link :href="route('profile.show', chirp.user.id)" class="text-gray-800 hover:text-gray-500 hover:underline focus:text-gray-500 active:text-gray-900 capitalize">{{ chirp.user.name }}</Link>
<small class="ml-2 text-sm text-gray-600">{{ dayjs(chirp.created_at).fromNow() }}</small>
<small v-if="chirp.created_at !== chirp.updated_at" class="text-sm text-gray-600"> · edited</small>
</div>
We have updated the HTML element containing the chirps author name from a <span>
to an Inertia.js Link. This link navigates to the profile page and uses the chirps authors ID as a parameter.
To update the included settings dropdown, open the authenticated Layout component at resources/js/Layouts.
Update the primary navigation dropdown like so from line 70.
<template #content>
- <DropdownLink :href="route('profile.edit')"> Profile </DropdownLink>
+ <DropdownLink :href="route('profile.show', $page.props.auth.user.id)"> View Profile </DropdownLink>
+ <DropdownLink :href="route('profile.edit')"> Edit Profile </DropdownLink>
<DropdownLink :href="route('logout')" method="post" as="button">
Log Out
</DropdownLink>
</template>
Don’t forget the mobile navigation at line 136, which you can update like so.
<div class="mt-3 space-y-1">
- <ResponsiveNavLink :href="route('profile.edit')"> Profile </ResponsiveNavLink>
+ <ResponsiveNavLink :href="route('profile.show', $page.props.auth.user.id)"> Show Profile </ResponsiveNavLink>
+ <ResponsiveNavLink :href="route('profile.edit')"> Edit Profile </ResponsiveNavLink>
<ResponsiveNavLink :href="route('logout')" method="post" as="button">
Log Out
</ResponsiveNavLink>
</div>
Testing our new feature
We need to test the public profile page renders and only include Chirps that belong to the owner of the profile page.
You'll find that there are already tests for the user profile at tests/Feature/ProfileTest.php. I have moved this file to match the Chirp Controller tests created in the previous part to tests/Feature/Controllers.
In your test file add the following test methods.
public function test_public_profile_is_displayed(): void
{
$user = User::factory()->create();
$response = $this
->actingAs($user)
->get(route('profile.show',$user->id));
$response->assertOk();
}
public function test_users_must_be_verified_to_view_profiles(): void
{
$user = User::factory()->unverified()->create();
$profileUser = User::factory()->create();
$response = $this
->actingAs($user)
->get(route('profile.show',$profileUser->id));
$response->assertRedirect(route('verification.notice'));
}
public function test_chirps_belonging_to_profile_owner_are_included(): void
{
$user = User::factory()->create();
$profileUser = User::factory()
->has(Chirp::Factory()->count(5))
->create();
$this->actingAs($user)
->get(route('profile.show',$profileUser->id))
->assertInertia(fn (Assert $page) => $page
->component('Profile/Show')
->has('user', fn (Assert $page) => $page
->where('id', $profileUser->id)
->where('name', $profileUser->name)
->missing('email')
->missing('password')
->etc()
)
->has('chirps', 5, fn (Assert $page) => $page
->where('id', $profileUser->chirps()->first()->id)
->where('message', $profileUser->chirps()->first()->message)
->etc()
->has('user', fn (Assert $page) => $page
->where('id', $profileUser->id)
->etc()
)
)
);
}
public function test_chirps_belonging_to_other_uses_are_not_included(): void
{
$user = User::factory()->create();
$otherUser = User::factory()
->has(Chirp::Factory()->count(5))
->create();
$this->actingAs($user)
->get(route('profile.show',$user->id))
->assertInertia(fn (Assert $page) => $page
->component('Profile/Show')
->has('user', fn (Assert $page) => $page
->where('id', $user->id)
->where('name', $user->name)
->missing('email')
->missing('password')
->etc()
)
->has('chirps',0)
);
}
In the first test, we visit a public profile page and assert a 200 'ok' response is returned.
In the second test, we are asserting only a verified user can view a public profile.
In the third and fourth tests, we are testing that the only chirps on the page are those which belong to the profile owner, by asserting against the props available in the Vue component.
Find out more about testing with Inertia.js.
Posted on October 6, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.