Laravel

Un framework qui rend heureux

Voir cette catégorie
Vers le bas
Voir cette série
Shop : le compte client 1/2
Jeudi 16 janvier 2025 12:51

Désormais, nos clients peuvent créer un compte, se connecter et même récupérer leur mot de passe. Cependant, il nous reste encore à développer les fonctionnalités permettant aux clients de mettre à jour leurs informations personnelles, de gérer leurs adresses, d'accéder aux détails de leurs commandes et de consulter toutes leurs données, afin de satisfaire les exigences du RGPD.

Vous pouvez trouver le code dans ce dépôt Github.

Le profil

Lors de son inscription, un client donne un certain nombre de renseignements personnels. Il doit en conserver la maîtrise et pouvoir apporter des changements au besoin. En particulier, il doit pouvoir changer son nom, son prénom, son email, son mot de passe et son inscription à la lettre d'information. On va en conséquence créer un formulaire pour lui permettre d'effectuer ces changements.

On crée un nouveau composant :

php artisan make:volt account/profile --class

Avec ce code :

<?php

use App\Models\User;
use Illuminate\Support\Facades\{Auth, Hash};
use Illuminate\Support\Str;
use Illuminate\Validation\Rule;
use Livewire\Attributes\Title;
use Livewire\Volt\Component;
use Mary\Traits\Toast;
use App\Traits\ManageProfile;
use App\Rules\StrongPassword;

new #[Title('Profile')]
class extends Component {
	use Toast, ManageProfile;

	public User $user;
	
	public function mount(): void
	{
		$this->user = Auth::user();
		$this->fill($this->user);
	}

	public function save(): void
	{
		$data = $this->validate([
			'firstname'  => 'required|string|max:255',
			'name'       => 'required|string|max:255',
			'newsletter' => 'nullable',
			'email'      => ['required', 'email', Rule::unique('users')->ignore($this->user->id)],
			'password'   => ['required', 'string', 'min:8', 'confirmed', new StrongPassword()],
		]);

		
		if (empty($data['password'])) {
			unset($data['password']);
		} else {
			$data['password'] = Hash::make($data['password']);
		}

		$this->user->update($data);

		$this->success(__('Profile updated with success.'), redirectTo: '/');
	}

}; ?>

<div>
	<x-card class="flex items-center justify-center mt-6" title="{{ __('My personal informations') }}" shadow separator progress-indicator>

        <x-form wire:submit="save" x-data="{ rgpd: false }" class="pb-4" >
			<x-input label="{{ __('Your firstName') }}" wire:model="firstname" icon="o-user" required />
            <x-input label="{{ __('Your name') }}" wire:model="name" icon="o-user" required />
            <x-input label="{{ __('Your e-mail') }}" wire:model="email" icon="o-envelope" required />

            <hr>

            <x-input label="{{ __('Your password') }}" wire:model="password" icon="o-key" hint="{{ __('Please fill in only if you wish to change your password. Otherwise leave blank.') }}" clearable />
            <x-input label="{{ __('Confirm Password') }}" wire:model="password_confirmation" icon="o-key"  />
            <x-button label="{{ __('Generate a secure password') }}" wire:click="generatePassword()" icon="m-wrench"
                class="btn-outline btn-sm" />

			<hr>

			<x-checkbox label="{{ __('I would like to receive your newsletter') }}" wire:model="newsletter" />
			<x-checkbox label="{!! __('I accept the terms and conditions of the privacy policy') !!}" x-model="rgpd" />

			<p class="text-xs text-gray-500"><span class="text-red-600">*</span> @lang('Required information')</p>

            <x-slot:actions>
                <x-button label="{{ __('Cancel') }}" link="/" class="btn-ghost" />
                <x-button x-show="rgpd" label="{{ __('Save') }}" icon="o-paper-airplane" spinner="save" type="submit"
                    class="btn-primary" />
            </x-slot:actions>

        </x-form>
		<hr><br>
		<p class="text-sm text-gray-500">@lang('To find out about your rights regarding the personal data you entrust to us, please consult our Privacy Policy')</p>
    </x-card>
</div>

Quelques traductions :

"My personal informations": "Mes informations personnelles",
"To find out about your rights regarding the personal data you entrust to us, please consult our Privacy Policy": "Pour connaître vos droits concernant les données personnelles que vous nous confiez, veuillez consulter notre politique de confidentialité",
"Profile updated with success.": "Profil mis à jour avec succès.",
"Please fill in only if you wish to change your password. Otherwise leave blank.": "Veuillez remplir seulement si vous souhaitez changer votre mot de passe. Sinon laissez vide.",

La route dans un groupe d'utilisateurs authentifiés :

Route::middleware('auth')->group(function () {

	Route::prefix('account')->group(function () {
		Volt::route('/profile', 'account.profile')->name('profile');
	});
});

On renseigne la barre de navigation :

<span class="text-black">
    <x-menu-item title="{{ __('My profile') }}" link="{{ route('profile') }}" />

Et la barre latérale :

<x-menu-item title="{{ __('My profile') }}" icon="o-user" link="{{ route('profile') }}" />

Et on a notre formulaire :

Les adresses

Le client doit aussi disposer d'une gestion complète de ses adresses : ajout, modification, suppression.

Un composant pour afficher une adresse

Comme il faudra afficher des adresses sur diverses pages, on prévoit un composant Blade :

 php artisan make:component address --view

Avec ce code :

<ul class="p-2 mb-2">
    @isset($address->name)
        <li class="font-bold">{{ "$address->civility $address->name $address->firstname" }}</li>
    @endif
    @if ($address->company)
        <li class="font-bold">{{ $address->company }}</li>
    @endif
        <li>{{ $address->address }}</li>
    @if ($address->addressbis)
        <li>{{ $address->addressbis }}</li>
    @endif
    @if ($address->bp)
        <li>{{ $address->bp }}</li>
    @endif
    <li>{{ "$address->postal $address->city" }}</li>
    <li class="font-bold">{{ $address->country->name }}</li>
    <li><x-icon name="o-phone" /><em>{{ $address->phone }}</em></li>
</ul>

Les routes

On ajoute les trois routes qui vont nous être utiles (affichage des adresses, création et modification) :

Route::middleware('auth')->group(function () {

	Route::prefix('account')->group(function () {
		...
		Volt::route('/addresses', 'account.addresses.index')->name('addresses');
		Volt::route('/addresses/create', 'account.addresses.create')->name('addresses.create');
		Volt::route('/addresses/{address}/edit', 'account.addresses.edit')->name('addresses.edit');
	});
});

Et on renseigne la barre de navigation :

<x-menu-item title="{{ __('My addresses') }}" link="{{ route('addresses') }}" />

Ainsi que la barre latérale :

<x-menu-item title="{{ __('My addresses') }}" icon="o-map-pin" link="{{ route('addresses') }}" />

L'affichage des adresses

On va commencer par créer l'affichage des adresses avec un nouveau composant :

php artisan make:volt account/addresses/index --class

Avec ce code :

<?php

use App\Models\Address;
use Livewire\Volt\Component;
use Illuminate\Support\Collection;
use Livewire\Attributes\Title;

new #[Title('Addresses')]
class extends Component {

    public function deleteAddress(Address $address): void
    {
        $address->delete();
    }

    public function with(): array
    {
        return [
            'addresses' => Auth::user()->addresses()->with('country')->get(),
        ];
    }
    
}; ?>

<x-card class="flex justify-center items-center mt-6" title="{{ __('My addresses') }}" shadow separator >
    <div class="container mx-auto">
        <div class="grid gap-6 md:grid-cols-2">
            @foreach ($addresses as $address)
                <x-card
                    class="w-full shadow-md transition duration-500 ease-in-out shadow-gray-500 hover:shadow-xl hover:shadow-gray-500"
                    title="">
                    <x-address :address="$address" />
                    <hr>
                    <x-slot:actions>
                        <x-popover>
                            <x-slot:trigger>
                                <x-button 
                                    icon="s-arrow-path" 
                                    link="{{ route('addresses.edit', $address) }}"
                                    spinner 
                                    class="text-blue-500 btn-ghost btn-circle btn-sm" 
                                />
                            </x-slot:trigger>
                            <x-slot:content class="pop-small">
                                @lang('Update')
                            </x-slot:content>
                        </x-popover>
                        <x-popover>
                            <x-slot:trigger>
                                <x-button 
                                    icon="o-trash" 
                                    wire:click="deleteAddress({{ $address->id }})" 
                                    wire:confirm="{{ __('Are you sure to delete this address?') }}" 
                                    spinner 
                                    class="text-red-500 btn-ghost btn-circle btn-sm" 
                                />
                            </x-slot:trigger>
                            <x-slot:content class="pop-small">
                                @lang('Delete')
                            </x-slot:content>
                        </x-popover>
                    </x-slot:actions>
                </x-card>
            @endforeach
        </div>
    </div>
    <x-slot:actions>
        <x-button label="{{ __('Create a new address') }}" link="{{ route('addresses.create') }}" class="btn-primary" />
    </x-slot:actions>
</x-card>

On ajoute les traductions :

"Create a new address": "Créer une nouvelle adresse",
"Are you sure to delete this address?": "Etes-vous sur de vouloir supprimer cette adresse ?",

Pour améliorer les popovers j'ai prévu une classe dans ressources/css/app.css :

.pop-small {
    @apply !p-1 !px-2 text-sm border-info text-center
}

Et voilà l'aspect :

Pour chaque adresse, on a un bouton pour la modification et un pour la suppression, avec un joli popover :

Un formulaire pour les adresses

On va avoir un formulaire commun pour la création et la modification. On ajoute une vue partielle pour ce formulaire :

Avec ce formulaire :

<x-card class="flex justify-center items-center mt-6" title="{!! $title !!}" shadow separator progress-indicator>
    <x-form wire:submit="save" class="w-full sm:min-w-[50vw]">
        <x-checkbox label="{!! __('It is a professionnal address') !!}" wire:model="professionnal" wire:change="$refresh" />

        <x-radio label="{{ __('Civility') }}" :options="$civilities" wire:model="civility" :required="!$professionnal" />

        <x-input label="{{ __('Name') }}" type="text" wire:model="name" icon="o-user"
            hint="{{ $professionnal ? __('Optional') : '' }}" :required="!$professionnal" />
        <x-input label="{{ __('FirstName') }}" type="text" wire:model="firstname" icon="o-user"
            hint="{{ $professionnal ? __('Optional') : '' }}" :required="!$professionnal" />
        <x-input label="{{ __('Company name') }}" type="text" wire:model="company" icon="o-building-library"
            :disabled="!$professionnal" :required="$professionnal" />
        <x-input label="{{ __('Street number and name') }}" type="text" wire:model="address" icon="o-home"
            required />
        <x-input label="{{ __('Building') }}" type="text" hint="{{ __('Optional') }}" wire:model="addressbis"
            icon="o-home" />
        <x-input label="{{ __('Place name or PO') }}" type="text" hint="{{ __('Optional') }}" wire:model="bp"
            icon="c-map-pin" />
        <x-input label="{{ __('Postcode') }}" type="text" wire:model="postal" icon="c-map-pin" required />
        <x-input label="{{ __('City') }}" type="text" wire:model="city" icon="c-map-pin" required />
        <x-select label="{{ __('Country') }}" :options="$countries" wire:model="country_id" icon="c-map-pin" required />
        <x-input label="{{ __('Phone number') }}" type="text" wire:model="phone" icon="o-phone" required />
        <p class="text-xs text-gray-500"><span class="text-red-600">*</span> @lang('Required information')</p>
        <x-slot:actions>
            <x-button label="{{ __('Cancel') }}" link="/account/addresses" class="btn-ghost" />
            <x-button label="{{ __('Save') }}" icon="o-paper-airplane" spinner="save" type="submit"
                class="btn-primary" />
        </x-slot:actions>
    </x-form>
</x-card>

On ajoute quelques traductions (en anticipant un peu la suite) :

"Create a new address": "Créer une nouvelle adresse",
"Are you sure to delete this address?": "Etes-vous sur de vouloir supprimer cette adresse ?",
"It is a professionnal address": "C'est une adresse professionnelle",
"Create an address": "Création d'une adresse",
"Company name": "Raison sociale",
"Street number and name": "N° et nom de la rue",
"Building": "Immeuble",
"City": "Ville",
"Postcode": "Code postal",
"Phone number": "N° de téléphone",
"Place name or PO": "Lieu-dit ou BP",
"Country": "Pays",
"Optional": "Optionnel",
"Update an address": "Mettre à jour une adresse",
"Address updated with success.": "Adresse mise à jour avec succès.",
"Name": "Nom",
"FirstName": "Prénom",

Un trait pour les éléments communs

De la même manière, on va avoir des éléments PHP communs pour la création et la modification d'une adresse, on crée un trait :

On va y placer les propriétés, les règles de validation et une fonction :

<?php

namespace App\Traits;

use Illuminate\Support\Collection;

trait ManageAddress {
	public Collection $countries;
	public bool $professionnal      = false;
	public array $civilities        = [['id' => 'M', 'name' => 'M.'], ['id' => 'Mme', 'name' => 'Mme.']];
	public string $civility         = 'M';
	public ?string $firstname       = null;
	public ?string $name            = null;
	public ?string $company         = null;
	public string $address          = '';
	public ?string $addressbis      = null;
	public ?string $bp              = null;
	public string $postal           = '';
	public string $city             = '';
	public int $country_id;
	public string $phone = '';

	public function updatedprofessionnal(): void {
		$this->company = '';
	}

	protected function rules(): array {
		return [
			'professionnal'    => 'required|boolean',
			'civility'         => 'required_with:name|in:M,Mme',
			'firstname'        => 'required_unless:professionnal,true|nullable|string|max:100',
			'name'             => 'required_unless:professionnal,true|nullable|string|max:100',
			'company'          => 'required_unless:professionnal,false|nullable|string|max:100',
			'address'          => 'required|string|max:255',
			'addressbis'       => 'nullable|string|max:255',
			'bp'               => 'nullable|string|max:100',
			'postal'           => 'required|string',
			'city'             => 'required|string|max:100',
			'country_id'       => 'required|integer|exists:countries,id',
			'phone'            => 'required|numeric',
		];
	}
}

La création d'une adresse

On crée le composant pour la création d'une adresse :

php artisan make:volt account/addresses/create --class

Avec la création précédemment de la vue pour le formulaire et le trait, le code devient bien plus léger dans ce composant :

<?php

use App\Models\Country;
use Livewire\Volt\Component;
use Livewire\Attributes\Title;
use Mary\Traits\Toast;
use Illuminate\Support\Collection;
use App\Traits\ManageAddress;

new #[Title('Create address')] 
class extends Component {
    use Toast, ManageAddress;

    public function mount(): void
    {
        $this->countries = Country::all();
        $this->country_id = $this->countries->first()->id;
    }

    public function save(): void
    {
        $data = $this->validate($this->rules());

        Auth::user()->addresses()->create($data);

        $this->success(__('Address created successfully.'), redirectTo: '/account/addresses');
    }
};

?>

@include('livewire.account.addresses.components.form', ['title' => __('Create an address')])

On obtient ce gros formulaire :

Le nom et le prénom sont optionnels si on a affaire à une adresse professionnelle. Vérifiez que tout fonctionne correctement.

La modification d'une adresse

On crée le composant pour la modification d'une adresse :

php artisan make:volt account/addresses/edit --class

Là aussi le code est léger :

<?php

use App\Models\{Address, Country};
use Livewire\Volt\Component;
use Livewire\Attributes\Title;
use Mary\Traits\Toast;
use Illuminate\Support\Collection;
use App\Traits\ManageAddress;

new #[Title('Update address')] 
class extends Component {
    use Toast, ManageAddress;

    public Address $myAddress;

    public function mount(Address $address): void
    {
        $this->myAddress = $address;
        $this->fill($this->myAddress);
        $this->countries = Country::all();
    }

    public function save(): void
    {
        $data = $this->validate($this->rules());

        $this->myAddress->update($data);

        $this->success(__('Address updated with success.'), redirectTo: '/account/addresses');
    }
};

?>

@include('livewire.account.addresses.components.form', ['title' => __('Update an address')])

Le formulaire est évidemment identique, à la différence qu'il est maintenant renseigné avec l'adresse existante au chargement.

Conclusion

Les clients ont désormais accès à leurs données personnelles et peuvent même les modifier. D'autre part, ils disposent d'une gestion complète pour leurs adresses. La prochaine étape consistera à afficher les commandes passées et aussi de se plier aux exigences du RGPD.



Par bestmomo

Aucun commentaire

Article précédent : Shop : oubli du mot de passe
Article suivant : Shop : le compte client 2/2