feat(Contact & membership forms update with their notification)
All checks were successful
Deploy Roxane to Preprod / deploy (push) Successful in 26h10m46s

This commit is contained in:
2026-04-07 18:06:20 +02:00
parent ca464e8e06
commit c848a8b47f
16 changed files with 602 additions and 517 deletions

View File

@@ -7,25 +7,26 @@ use App\Http\Requests\Forms\ContactRequest;
use App\Services\ContactService; use App\Services\ContactService;
use Illuminate\Http\RedirectResponse; use Illuminate\Http\RedirectResponse;
use Inertia\Inertia; use Inertia\Inertia;
use Inertia\Response;
class ContactFormController extends Controller class ContactFormController extends Controller
{ {
public function __construct(protected ContactService $contactService) {} public function __construct(protected ContactService $contactService) {}
/**
* Show the contact form page. public function create(): Response
*/
public function create()
{ {
return Inertia::render('forms/contact'); return Inertia::render('forms/contact', [
'captcha_question' => $this->generateCaptcha(),
]);
} }
/** /**
* Handle an incoming contact form submission.
* @throws \Illuminate\Validation\ValidationException * @throws \Illuminate\Validation\ValidationException
*/ */
public function store(ContactRequest $request): RedirectResponse public function store(ContactRequest $request): RedirectResponse
{ {
$validated = $request->validated(); $validated = $request->validated();
try { try {
$this->contactService->registerNewContactRequest($validated); $this->contactService->registerNewContactRequest($validated);
} catch (\Throwable $e) { } catch (\Throwable $e) {
@@ -41,4 +42,13 @@ class ContactFormController extends Controller
return to_route('contact')->with('success', __('contacts.responses.success')); return to_route('contact')->with('success', __('contacts.responses.success'));
} }
private function generateCaptcha(): string
{
$a = random_int(1, 9);
$b = random_int(1, 9);
session(['captcha_contact' => (string) ($a + $b)]);
return "Combien font {$a} + {$b} ?";
}
} }

View File

@@ -4,55 +4,69 @@ namespace App\Http\Controllers\Forms;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Http\Requests\Forms\MembershipRequest; use App\Http\Requests\Forms\MembershipRequest;
use App\Models\Membership;
use App\Models\Package; use App\Models\Package;
use App\Models\Service;
use App\Services\MemberService; use App\Services\MemberService;
use Carbon\Carbon;
use Illuminate\Http\RedirectResponse; use Illuminate\Http\RedirectResponse;
use Inertia\Inertia; use Inertia\Inertia;
use Inertia\Response;
class MembershipFormController extends Controller class MembershipFormController extends Controller
{ {
public function __construct(protected MemberService $memberService) {} public function __construct(protected MemberService $memberService) {}
/** public function create(): Response
* Show the contact form page.
*/
public function create()
{ {
return Inertia::render('forms/membership', [ $remainingMonths = 13 - now()->month;
'plans' => Package::query()
$plans = Package::query()
->where('is_active', true) ->where('is_active', true)
->select('id', 'identifier', 'name', 'price', 'description') ->select('id', 'identifier', 'name', 'price', 'description')
->get() ->get()
->map(fn (Package $p) => [
'id' => $p->id,
'identifier' => $p->identifier,
'name' => $p->name,
'description' => $p->description,
'price' => $p->identifier === 'custom' ? $remainingMonths : (float) $p->price,
'months' => $p->identifier === 'custom' ? $remainingMonths : null,
]);
return Inertia::render('forms/membership', [
'plans' => $plans,
'services' => Service::query()->select('name', 'description')->get(),
'captcha_question' => $this->generateCaptcha(),
]); ]);
} }
/**
* Handle an incoming membership form request.
*
*/
public function store(MembershipRequest $request): RedirectResponse public function store(MembershipRequest $request): RedirectResponse
{ {
dd($request->validated());
$validated = $request->validated(); $validated = $request->validated();
try { try {
$this->memberService->registerNewMember($validated); $this->memberService->registerNewMember($validated);
} catch (\Throwable $e) { } catch (\Throwable $e) {
\Log::error('Erreur lors de la création dun membre', [ \Log::error('Erreur lors de la création d\'un membre', [
'error' => $e->getMessage(), 'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(), 'trace' => $e->getTraceAsString(),
'data' => $validated, 'data' => $validated,
]); ]);
return redirect() return to_route('membership')
->route('membership') ->with('error', __('memberships.fields.subscription.error'));
->with('error', Membership::getAttributeLabel('memberships.subscription.error'));
} }
return redirect() return to_route('membership')
->route('membership') ->with('success', __('memberships.fields.subscription.success'));
->with('success', Membership::getAttributeLabel('memberships.subscription.success')); }
private function generateCaptcha(): string
{
$a = random_int(1, 9);
$b = random_int(1, 9);
session(['captcha_membership' => (string) ($a + $b)]);
return "Combien font {$a} + {$b} ?";
} }
} }

View File

@@ -2,21 +2,17 @@
namespace App\Http\Requests\Forms; namespace App\Http\Requests\Forms;
use App\Rules\ValidCaptcha;
use Illuminate\Foundation\Http\FormRequest; use Illuminate\Foundation\Http\FormRequest;
class ContactRequest extends FormRequest class ContactRequest extends FormRequest
{ {
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool public function authorize(): bool
{ {
return true; return true;
} }
/** /**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string> * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/ */
public function rules(): array public function rules(): array
@@ -25,9 +21,10 @@ class ContactRequest extends FormRequest
'lastname' => 'required|string|max:255', 'lastname' => 'required|string|max:255',
'firstname' => 'required|string|max:255', 'firstname' => 'required|string|max:255',
'email' => 'required|email|max:255', 'email' => 'required|email|max:255',
'address' => 'string|max:255', 'address' => 'nullable|string|max:255',
'subject' => 'required|string|max:255', 'subject' => 'required|string|max:255',
'message' => 'required|string', 'message' => 'required|string',
'captcha' => ['required', new ValidCaptcha('captcha_contact')],
]; ];
} }
} }

View File

@@ -2,39 +2,34 @@
namespace App\Http\Requests\Forms; namespace App\Http\Requests\Forms;
use App\Rules\ValidCaptcha;
use Illuminate\Foundation\Http\FormRequest; use Illuminate\Foundation\Http\FormRequest;
class MembershipRequest extends FormRequest class MembershipRequest extends FormRequest
{ {
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool public function authorize(): bool
{ {
return true; return true;
} }
/** /**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string> * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/ */
public function rules(): array public function rules(): array
{ {
return [ return [
// Member
'lastname' => 'required|string|max:255', 'lastname' => 'required|string|max:255',
'firstname' => 'required|string|max:255', 'firstname' => 'required|string|max:255',
'email' => 'required|email|max:255', 'email' => 'required|email|max:255',
'company' => 'string|max:255', 'company' => 'nullable|string|max:255',
'address' => 'required|string|max:255', 'address' => 'required|string|max:255',
'zipcode' => 'required|string|max:255', 'zipcode' => 'required|string|max:255',
'city' => 'required|string|max:255', 'city' => 'required|string|max:255',
'phone1' => 'required|string|max:255', 'phone1' => 'required|string|max:255',
// Membership
'package' => 'required|string|max:255', 'package' => 'required|string|max:255',
'amount' => 'required|string|max:255', 'amount' => 'required|numeric|min:0',
'cgu' => 'required|accepted',
'captcha' => ['required', new ValidCaptcha('captcha_membership')],
]; ];
} }
} }

View File

@@ -0,0 +1,65 @@
<?php
namespace App\Notifications;
use App\Filament\Resources\Members\MemberResource;
use App\Models\Member;
use App\Models\NotificationTemplate;
use App\Models\Package;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
class MemberNewRequestAdminNotification extends Notification implements ShouldQueue
{
use Queueable;
public function __construct(
public readonly Member $member,
public readonly Package $package,
public readonly float $amount,
) {}
/**
* @return array<int, string>
*/
public function via(object $notifiable): array
{
return ['mail'];
}
public function toMail(object $notifiable): MailMessage
{
$template = NotificationTemplate::findByIdentifier('member_new_request_admin');
$vars = [
'member_name' => $this->member->full_name,
'member_email' => $this->member->email ?? '',
'member_phone' => $this->member->phone1 ?? '',
'member_address' => implode(', ', array_filter([
$this->member->address,
$this->member->zipcode,
$this->member->city,
])),
'package_name' => $this->package->name,
'amount' => number_format($this->amount, 2, ',', ' '),
'member_url' => MemberResource::getUrl('edit', ['record' => $this->member->id]),
'app_name' => config('app.name'),
];
return (new MailMessage)
->subject($template->renderSubject($vars))
->view('notifications.mail-template', [
'body' => $template->renderBody($vars),
]);
}
/**
* @return array<string, mixed>
*/
public function toArray(object $notifiable): array
{
return [];
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace App\Rules;
use Closure;
use Illuminate\Contracts\Validation\ValidationRule;
class ValidCaptcha implements ValidationRule
{
public function __construct(private readonly string $sessionKey = 'captcha_answer') {}
/**
* @param \Closure(string, ?string=): \Illuminate\Translation\PotentiallyTranslatedString $fail
*/
public function validate(string $attribute, mixed $value, Closure $fail): void
{
if (trim((string) $value) !== (string) session($this->sessionKey)) {
$fail('Le code de vérification est incorrect.');
}
}
}

View File

@@ -8,6 +8,7 @@ use App\Models\MemberGroup;
use App\Models\Package; use App\Models\Package;
use App\Notifications\MemberDeactivatedAdminNotification; use App\Notifications\MemberDeactivatedAdminNotification;
use App\Notifications\MemberDeactivatedMemberNotification; use App\Notifications\MemberDeactivatedMemberNotification;
use App\Notifications\MemberNewRequestAdminNotification;
use Illuminate\Support\Facades\Notification; use Illuminate\Support\Facades\Notification;
class MemberService class MemberService
@@ -51,6 +52,9 @@ class MemberService
]); ]);
Notification::route('mail', config('app.admin_email'))
->notify(new MemberNewRequestAdminNotification($member, $package, (float) $data['amount']));
event(new MemberRegistered($member)); event(new MemberRegistered($member));
return $member; return $member;

View File

@@ -47,6 +47,35 @@ class NotificationTemplateSeeder extends Seeder
] ]
); );
NotificationTemplate::updateOrCreate(
['identifier' => 'member_new_request_admin'],
[
'name' => 'Nouvelle demande d\'adhésion — admin',
'subject' => 'Nouvelle demande d\'adhésion : {member_name}',
'body' => '<p>Une nouvelle demande d\'adhésion a été reçue.</p>'
.'<p>'
.'<strong>Nom :</strong> {member_name}<br>'
.'<strong>Email :</strong> {member_email}<br>'
.'<strong>Téléphone :</strong> {member_phone}<br>'
.'<strong>Adresse :</strong> {member_address}<br>'
.'<strong>Formule :</strong> {package_name}<br>'
.'<strong>Montant :</strong> {amount} €'
.'</p>'
.'<p><a href="{member_url}" style="display:inline-block;padding:10px 20px;background:#f5a623;color:#000;font-weight:bold;text-decoration:none;border:3px solid #000;border-radius:6px;">Voir la fiche adhérent</a></p>',
'variables' => [
'member_name' => 'Nom complet du membre',
'member_email' => 'Adresse email du membre',
'member_phone' => 'Téléphone du membre',
'member_address' => 'Adresse postale du membre',
'package_name' => 'Nom de la formule choisie',
'amount' => 'Montant de la cotisation',
'member_url' => 'URL de la fiche dans le back office',
'app_name' => 'Nom de l\'application',
],
'is_active' => true,
]
);
NotificationTemplate::updateOrCreate( NotificationTemplate::updateOrCreate(
['identifier' => 'contact_new_request'], ['identifier' => 'contact_new_request'],
[ [

View File

@@ -1,7 +1,7 @@
import { queryParams, type RouteQueryOptions, type RouteDefinition, type RouteFormDefinition } from './../../../../../wayfinder' import { queryParams, type RouteQueryOptions, type RouteDefinition, type RouteFormDefinition } from './../../../../../wayfinder'
/** /**
* @see \App\Http\Controllers\Forms\ContactFormController::create * @see \App\Http\Controllers\Forms\ContactFormController::create
* @see app/Http/Controllers/Forms/ContactFormController.php:17 * @see app/Http/Controllers/Forms/ContactFormController.php:16
* @route '/contact' * @route '/contact'
*/ */
export const create = (options?: RouteQueryOptions): RouteDefinition<'get'> => ({ export const create = (options?: RouteQueryOptions): RouteDefinition<'get'> => ({
@@ -16,7 +16,7 @@ create.definition = {
/** /**
* @see \App\Http\Controllers\Forms\ContactFormController::create * @see \App\Http\Controllers\Forms\ContactFormController::create
* @see app/Http/Controllers/Forms/ContactFormController.php:17 * @see app/Http/Controllers/Forms/ContactFormController.php:16
* @route '/contact' * @route '/contact'
*/ */
create.url = (options?: RouteQueryOptions) => { create.url = (options?: RouteQueryOptions) => {
@@ -25,7 +25,7 @@ create.url = (options?: RouteQueryOptions) => {
/** /**
* @see \App\Http\Controllers\Forms\ContactFormController::create * @see \App\Http\Controllers\Forms\ContactFormController::create
* @see app/Http/Controllers/Forms/ContactFormController.php:17 * @see app/Http/Controllers/Forms/ContactFormController.php:16
* @route '/contact' * @route '/contact'
*/ */
create.get = (options?: RouteQueryOptions): RouteDefinition<'get'> => ({ create.get = (options?: RouteQueryOptions): RouteDefinition<'get'> => ({
@@ -35,7 +35,7 @@ create.get = (options?: RouteQueryOptions): RouteDefinition<'get'> => ({
/** /**
* @see \App\Http\Controllers\Forms\ContactFormController::create * @see \App\Http\Controllers\Forms\ContactFormController::create
* @see app/Http/Controllers/Forms/ContactFormController.php:17 * @see app/Http/Controllers/Forms/ContactFormController.php:16
* @route '/contact' * @route '/contact'
*/ */
create.head = (options?: RouteQueryOptions): RouteDefinition<'head'> => ({ create.head = (options?: RouteQueryOptions): RouteDefinition<'head'> => ({
@@ -45,7 +45,7 @@ create.head = (options?: RouteQueryOptions): RouteDefinition<'head'> => ({
/** /**
* @see \App\Http\Controllers\Forms\ContactFormController::create * @see \App\Http\Controllers\Forms\ContactFormController::create
* @see app/Http/Controllers/Forms/ContactFormController.php:17 * @see app/Http/Controllers/Forms/ContactFormController.php:16
* @route '/contact' * @route '/contact'
*/ */
const createForm = (options?: RouteQueryOptions): RouteFormDefinition<'get'> => ({ const createForm = (options?: RouteQueryOptions): RouteFormDefinition<'get'> => ({
@@ -55,7 +55,7 @@ const createForm = (options?: RouteQueryOptions): RouteFormDefinition<'get'> =>
/** /**
* @see \App\Http\Controllers\Forms\ContactFormController::create * @see \App\Http\Controllers\Forms\ContactFormController::create
* @see app/Http/Controllers/Forms/ContactFormController.php:17 * @see app/Http/Controllers/Forms/ContactFormController.php:16
* @route '/contact' * @route '/contact'
*/ */
createForm.get = (options?: RouteQueryOptions): RouteFormDefinition<'get'> => ({ createForm.get = (options?: RouteQueryOptions): RouteFormDefinition<'get'> => ({
@@ -65,7 +65,7 @@ createForm.get = (options?: RouteQueryOptions): RouteFormDefinition<'get'> => ({
/** /**
* @see \App\Http\Controllers\Forms\ContactFormController::create * @see \App\Http\Controllers\Forms\ContactFormController::create
* @see app/Http/Controllers/Forms/ContactFormController.php:17 * @see app/Http/Controllers/Forms/ContactFormController.php:16
* @route '/contact' * @route '/contact'
*/ */
createForm.head = (options?: RouteQueryOptions): RouteFormDefinition<'get'> => ({ createForm.head = (options?: RouteQueryOptions): RouteFormDefinition<'get'> => ({

View File

@@ -1,7 +1,7 @@
import { queryParams, type RouteQueryOptions, type RouteDefinition, type RouteFormDefinition } from './../../../../../wayfinder' import { queryParams, type RouteQueryOptions, type RouteDefinition, type RouteFormDefinition } from './../../../../../wayfinder'
/** /**
* @see \App\Http\Controllers\Forms\MembershipFormController::create * @see \App\Http\Controllers\Forms\MembershipFormController::create
* @see app/Http/Controllers/Forms/MembershipFormController.php:21 * @see app/Http/Controllers/Forms/MembershipFormController.php:18
* @route '/membership' * @route '/membership'
*/ */
export const create = (options?: RouteQueryOptions): RouteDefinition<'get'> => ({ export const create = (options?: RouteQueryOptions): RouteDefinition<'get'> => ({
@@ -16,7 +16,7 @@ create.definition = {
/** /**
* @see \App\Http\Controllers\Forms\MembershipFormController::create * @see \App\Http\Controllers\Forms\MembershipFormController::create
* @see app/Http/Controllers/Forms/MembershipFormController.php:21 * @see app/Http/Controllers/Forms/MembershipFormController.php:18
* @route '/membership' * @route '/membership'
*/ */
create.url = (options?: RouteQueryOptions) => { create.url = (options?: RouteQueryOptions) => {
@@ -25,7 +25,7 @@ create.url = (options?: RouteQueryOptions) => {
/** /**
* @see \App\Http\Controllers\Forms\MembershipFormController::create * @see \App\Http\Controllers\Forms\MembershipFormController::create
* @see app/Http/Controllers/Forms/MembershipFormController.php:21 * @see app/Http/Controllers/Forms/MembershipFormController.php:18
* @route '/membership' * @route '/membership'
*/ */
create.get = (options?: RouteQueryOptions): RouteDefinition<'get'> => ({ create.get = (options?: RouteQueryOptions): RouteDefinition<'get'> => ({
@@ -35,7 +35,7 @@ create.get = (options?: RouteQueryOptions): RouteDefinition<'get'> => ({
/** /**
* @see \App\Http\Controllers\Forms\MembershipFormController::create * @see \App\Http\Controllers\Forms\MembershipFormController::create
* @see app/Http/Controllers/Forms/MembershipFormController.php:21 * @see app/Http/Controllers/Forms/MembershipFormController.php:18
* @route '/membership' * @route '/membership'
*/ */
create.head = (options?: RouteQueryOptions): RouteDefinition<'head'> => ({ create.head = (options?: RouteQueryOptions): RouteDefinition<'head'> => ({
@@ -45,7 +45,7 @@ create.head = (options?: RouteQueryOptions): RouteDefinition<'head'> => ({
/** /**
* @see \App\Http\Controllers\Forms\MembershipFormController::create * @see \App\Http\Controllers\Forms\MembershipFormController::create
* @see app/Http/Controllers/Forms/MembershipFormController.php:21 * @see app/Http/Controllers/Forms/MembershipFormController.php:18
* @route '/membership' * @route '/membership'
*/ */
const createForm = (options?: RouteQueryOptions): RouteFormDefinition<'get'> => ({ const createForm = (options?: RouteQueryOptions): RouteFormDefinition<'get'> => ({
@@ -55,7 +55,7 @@ const createForm = (options?: RouteQueryOptions): RouteFormDefinition<'get'> =>
/** /**
* @see \App\Http\Controllers\Forms\MembershipFormController::create * @see \App\Http\Controllers\Forms\MembershipFormController::create
* @see app/Http/Controllers/Forms/MembershipFormController.php:21 * @see app/Http/Controllers/Forms/MembershipFormController.php:18
* @route '/membership' * @route '/membership'
*/ */
createForm.get = (options?: RouteQueryOptions): RouteFormDefinition<'get'> => ({ createForm.get = (options?: RouteQueryOptions): RouteFormDefinition<'get'> => ({
@@ -65,7 +65,7 @@ createForm.get = (options?: RouteQueryOptions): RouteFormDefinition<'get'> => ({
/** /**
* @see \App\Http\Controllers\Forms\MembershipFormController::create * @see \App\Http\Controllers\Forms\MembershipFormController::create
* @see app/Http/Controllers/Forms/MembershipFormController.php:21 * @see app/Http/Controllers/Forms/MembershipFormController.php:18
* @route '/membership' * @route '/membership'
*/ */
createForm.head = (options?: RouteQueryOptions): RouteFormDefinition<'get'> => ({ createForm.head = (options?: RouteQueryOptions): RouteFormDefinition<'get'> => ({
@@ -82,7 +82,7 @@ create.form = createForm
/** /**
* @see \App\Http\Controllers\Forms\MembershipFormController::store * @see \App\Http\Controllers\Forms\MembershipFormController::store
* @see app/Http/Controllers/Forms/MembershipFormController.php:35 * @see app/Http/Controllers/Forms/MembershipFormController.php:42
* @route '/membership' * @route '/membership'
*/ */
export const store = (options?: RouteQueryOptions): RouteDefinition<'post'> => ({ export const store = (options?: RouteQueryOptions): RouteDefinition<'post'> => ({
@@ -97,7 +97,7 @@ store.definition = {
/** /**
* @see \App\Http\Controllers\Forms\MembershipFormController::store * @see \App\Http\Controllers\Forms\MembershipFormController::store
* @see app/Http/Controllers/Forms/MembershipFormController.php:35 * @see app/Http/Controllers/Forms/MembershipFormController.php:42
* @route '/membership' * @route '/membership'
*/ */
store.url = (options?: RouteQueryOptions) => { store.url = (options?: RouteQueryOptions) => {
@@ -106,7 +106,7 @@ store.url = (options?: RouteQueryOptions) => {
/** /**
* @see \App\Http\Controllers\Forms\MembershipFormController::store * @see \App\Http\Controllers\Forms\MembershipFormController::store
* @see app/Http/Controllers/Forms/MembershipFormController.php:35 * @see app/Http/Controllers/Forms/MembershipFormController.php:42
* @route '/membership' * @route '/membership'
*/ */
store.post = (options?: RouteQueryOptions): RouteDefinition<'post'> => ({ store.post = (options?: RouteQueryOptions): RouteDefinition<'post'> => ({
@@ -116,7 +116,7 @@ store.post = (options?: RouteQueryOptions): RouteDefinition<'post'> => ({
/** /**
* @see \App\Http\Controllers\Forms\MembershipFormController::store * @see \App\Http\Controllers\Forms\MembershipFormController::store
* @see app/Http/Controllers/Forms/MembershipFormController.php:35 * @see app/Http/Controllers/Forms/MembershipFormController.php:42
* @route '/membership' * @route '/membership'
*/ */
const storeForm = (options?: RouteQueryOptions): RouteFormDefinition<'post'> => ({ const storeForm = (options?: RouteQueryOptions): RouteFormDefinition<'post'> => ({
@@ -126,7 +126,7 @@ const storeForm = (options?: RouteQueryOptions): RouteFormDefinition<'post'> =>
/** /**
* @see \App\Http\Controllers\Forms\MembershipFormController::store * @see \App\Http\Controllers\Forms\MembershipFormController::store
* @see app/Http/Controllers/Forms/MembershipFormController.php:35 * @see app/Http/Controllers/Forms/MembershipFormController.php:42
* @route '/membership' * @route '/membership'
*/ */
storeForm.post = (options?: RouteQueryOptions): RouteFormDefinition<'post'> => ({ storeForm.post = (options?: RouteQueryOptions): RouteFormDefinition<'post'> => ({

View File

@@ -1,11 +1,11 @@
import {useEffect, useState} from "react"; import { useEffect, useState } from 'react';
import {Form, Head, usePage} from "@inertiajs/react"; import { Form, Head, usePage } from '@inertiajs/react';
import {LoaderCircle} from 'lucide-react'; import { LoaderCircle } from 'lucide-react';
import ContactFormController from "@/actions/App/Http/Controllers/Forms/ContactFormController"; import ContactFormController from '@/actions/App/Http/Controllers/Forms/ContactFormController';
import {Label} from "@/components/ui/label"; import { Label } from '@/components/ui/label';
import {Input} from "@/components/ui/input"; import { Input } from '@/components/ui/input';
import InputError from "@/components/input-error"; import InputError from '@/components/input-error';
import {Button} from "@/components/ui/button"; import { Button } from '@/components/ui/button';
import { import {
Select, Select,
SelectContent, SelectContent,
@@ -13,16 +13,18 @@ import {
SelectItem, SelectItem,
SelectLabel, SelectLabel,
SelectTrigger, SelectTrigger,
SelectValue SelectValue,
} from "@/components/ui/select"; } from '@/components/ui/select';
import {Textarea} from "@/components/ui/textarea"; import { Textarea } from '@/components/ui/textarea';
import NavGuestLayout from "@/layouts/nav-guest-layout"; import NavGuestLayout from '@/layouts/nav-guest-layout';
import {PageProps} from "@/types"; import { PageProps } from '@/types';
import {FlashMessage} from "@/components/flash-message"; import { FlashMessage } from '@/components/flash-message';
import { Container } from '@/components/common/Container';
import { SectionHeading } from '@/components/common/SectionHeading';
import { Footer } from '@/components/footer';
export default function Contact() { export default function Contact() {
const {flash} = usePage().props as PageProps; const { flash, captcha_question } = usePage().props as PageProps;
const [showFlashMessage, setFlashMessage] = useState(!!flash); const [showFlashMessage, setFlashMessage] = useState(!!flash);
useEffect(() => { useEffect(() => {
@@ -33,167 +35,129 @@ export default function Contact() {
} }
}, [flash]); }, [flash]);
return ( return (
<> <>
<Head title="Nous contacter"/> <Head title="Nous contacter" />
<div <div className="flex flex-col min-h-screen bg-white dark:bg-[#0a0a0a] text-[#1b1b18] dark:text-[#EDEDEC]">
className="flex flex-col items-center bg-[#F5F5F5] p-6 text-[#1b1b18] lg:justify-center lg:p-8 dark:bg-[#0a0a0a]"> <div className="flex flex-col items-center px-4">
<NavGuestLayout/> <NavGuestLayout />
<section className="flex flex-col h-screen items-center mt-15 gap-4">
<div>
<h1>Nous contacter</h1>
<p>
Vous désirez nous contacter, merci de remplir le formulaire suivant :
</p>
</div> </div>
{showFlashMessage && ( <main className="flex-1 py-12">
<FlashMessage messages={flash ?? {}} /> <Container className="flex flex-col gap-10">
)} <SectionHeading
title="Nous contacter"
color="primary"
subtitle="Une question, une remarque ? Remplissez le formulaire ci-dessous, nous vous répondrons dans les plus brefs délais."
align="left"
/>
{showFlashMessage && <FlashMessage messages={flash ?? {}} />}
<Form <Form
{...ContactFormController.store.form()} {...ContactFormController.store.form()}
resetOnSuccess resetOnSuccess
disableWhileProcessing disableWhileProcessing
> >
{({processing, errors}) => ( {({ processing, errors }) => (
<div className="lg:w-5xl px-10"> <div className="grid grid-cols-1 lg:grid-cols-2 gap-8 max-w-4xl">
<div className="flex gap-6 w-full">
<div className="w-1/2"> {/* Left — Identité + adresse */}
<div className="grid gap-2 my-4"> <div className="bg-white dark:bg-[#171717] rounded-2xl border-3 border-black p-6 shadow-[4px_4px_0px_rgba(0,0,0,1)] flex flex-col gap-4">
<h2 className="text-lg font-semibold text-primary">Vos informations</h2>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div className="grid gap-1">
<Label htmlFor="lastname">Nom*</Label> <Label htmlFor="lastname">Nom*</Label>
<Input <Input id="lastname" name="lastname" type="text" required tabIndex={1} autoComplete="family-name" placeholder="Votre nom" />
id="lastname" <InputError message={errors.lastname} />
type="text"
required
autoFocus
tabIndex={1}
autoComplete="lastname"
name="lastname"
placeholder="Nom"
/>
<InputError
message={errors.name}
className="mt-2"
/>
</div> </div>
<div className="grid gap-1">
<div className="grid gap-2 my-4">
<Label htmlFor="firstname">Prénom*</Label> <Label htmlFor="firstname">Prénom*</Label>
<Input <Input id="firstname" name="firstname" type="text" required tabIndex={2} autoComplete="given-name" placeholder="Votre prénom" />
id="firstname" <InputError message={errors.firstname} />
type="text"
required
autoFocus
tabIndex={2}
autoComplete="firstname"
name="firstname"
placeholder="Prénom"
/>
<InputError
message={errors.name}
className="mt-2"
/>
</div>
<div className="grid gap-2 my-4">
<Label htmlFor="email">Adresse Mail*</Label>
<Input
id="email"
type="email"
required
tabIndex={3}
autoComplete="email"
name="email"
placeholder="email@exemple.com"
/>
<InputError message={errors.email}/>
</div>
<div className="grid gap-2 my-4">
<Label htmlFor="address">Votre adresse postale</Label>
<Input
id="address"
type="text"
required
autoFocus
tabIndex={4}
autoComplete="address"
name="address"
placeholder="Votre adresse postale (facultatif)"
/>
<InputError
message={errors.name}
className="mt-2"
/>
</div> </div>
</div> </div>
<div className="w-1/2"> <div className="grid gap-1">
<div className="grid gap-2 my-4"> <Label htmlFor="email">Adresse e-mail*</Label>
<Label htmlFor="subject">Objet de votre demande*</Label> <Input id="email" name="email" type="email" required tabIndex={3} autoComplete="email" placeholder="email@exemple.com" />
<InputError message={errors.email} />
</div>
<div className="grid gap-1">
<Label htmlFor="address">
Adresse postale <span className="text-muted-foreground text-xs">(facultatif)</span>
</Label>
<Input id="address" name="address" type="text" tabIndex={4} autoComplete="street-address" placeholder="Votre adresse postale" />
<InputError message={errors.address} />
</div>
</div>
{/* Right — Message + captcha + submit */}
<div className="flex flex-col gap-6">
<div className="bg-white dark:bg-[#171717] rounded-2xl border-3 border-black p-6 shadow-[4px_4px_0px_rgba(0,0,0,1)] flex flex-col gap-4">
<h2 className="text-lg font-semibold text-primary">Votre message</h2>
<div className="grid gap-1">
<Label htmlFor="subject">Objet*</Label>
<Select name="subject" required> <Select name="subject" required>
<SelectTrigger tabIndex={5}> <SelectTrigger tabIndex={5}>
<SelectValue placeholder="Sélectionnez un objet"/> <SelectValue placeholder="Sélectionnez un objet" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectGroup> <SelectGroup>
<SelectLabel>Objets</SelectLabel> <SelectLabel>Objets</SelectLabel>
<SelectItem value="info-request">Demande <SelectItem value="info-request">Demande d'informations</SelectItem>
d'informations</SelectItem>
<SelectItem value="service-request">Services</SelectItem> <SelectItem value="service-request">Services</SelectItem>
<SelectItem value="other">Autres</SelectItem> <SelectItem value="other">Autres</SelectItem>
</SelectGroup> </SelectGroup>
</SelectContent> </SelectContent>
</Select> </Select>
<InputError message={errors.subject} />
</div> </div>
<div className="grid gap-2 my-4"> <div className="grid gap-1">
<Label htmlFor="message">Votre message</Label> <Label htmlFor="message">Message*</Label>
<Textarea <Textarea id="message" name="message" tabIndex={6} required placeholder="Entrez votre message ici..." className="h-32 resize-none" />
className="h-28" <InputError message={errors.message} />
id="message" </div>
name="message"
tabIndex={6}
required
placeholder="Entrez votre message ici..."
/>
</div> </div>
<div className="grid gap-2 my-4"> <div className="bg-white dark:bg-[#171717] rounded-2xl border-3 border-black p-6 shadow-[4px_4px_0px_rgba(0,0,0,1)] flex flex-col gap-5">
<Label htmlFor="captcha">Captcha</Label> <div className="grid gap-1">
<Label htmlFor="captcha" className="font-semibold">{captcha_question}</Label>
<Input <Input
id="captcha" id="captcha"
type="text"
autoFocus
tabIndex={7}
name="captcha" name="captcha"
placeholder="Entrez le captcha ci-dessous" type="text"
tabIndex={7}
placeholder="Votre réponse"
autoComplete="off"
className="max-w-[180px]"
/> />
<InputError message={errors.captcha} />
</div> </div>
</div>
</div>
<div className="mx-auto justify-center">
<Button <Button
variant="outline"
type="submit" type="submit"
className="mt-2" variant="secondary"
tabIndex={8} tabIndex={8}
data-test="register-user-button" className="w-full border-3 border-black shadow-[4px_4px_0px_rgba(0,0,0,1)] hover:shadow-none hover:translate-x-0.5 hover:translate-y-0.5 font-bold text-base py-5"
> >
{processing && ( {processing && <LoaderCircle className="h-4 w-4 animate-spin" />}
<LoaderCircle className="h-4 w-4 animate-spin"/>
)}
Envoyer Envoyer
</Button> </Button>
</div> </div>
</div> </div>
</div>
)} )}
</Form> </Form>
</section> </Container>
</main>
<Footer />
</div> </div>
</> </>
) );
} }

View File

@@ -1,56 +1,30 @@
import {Form, Head, usePage} from "@inertiajs/react"; import { Form, Head, usePage } from '@inertiajs/react';
import {cn} from "@/lib/utils"; import { cn } from '@/lib/utils';
import {CheckCircle2, CheckIcon, LoaderCircle} from 'lucide-react'; import { CheckIcon, LoaderCircle } from 'lucide-react';
import MembershipFormController from "@/actions/App/Http/Controllers/Forms/MembershipFormController"; import MembershipFormController from '@/actions/App/Http/Controllers/Forms/MembershipFormController';
import {Label} from "@/components/ui/label"; import { Label } from '@/components/ui/label';
import {Input} from "@/components/ui/input"; import { Input } from '@/components/ui/input';
import InputError from "@/components/input-error"; import InputError from '@/components/input-error';
import {Button} from "@/components/ui/button"; import { Button } from '@/components/ui/button';
import NavGuestLayout from "@/layouts/nav-guest-layout"; import NavGuestLayout from '@/layouts/nav-guest-layout';
import {Checkbox} from "@/components/ui/checkbox"; import { Checkbox } from '@/components/ui/checkbox';
import {useEffect, useState} from "react"; import { useEffect, useState } from 'react';
import {PageProps} from "@/types"; import { PageProps } from '@/types';
import {FlashMessage} from "@/components/flash-message"; import { FlashMessage } from '@/components/flash-message';
import { Container } from '@/components/common/Container';
import { SectionHeading } from '@/components/common/SectionHeading';
import { Footer } from '@/components/footer';
export default function Membership() { export default function Membership() {
const {flash, plans} = usePage().props as PageProps const { flash, plans, services, captcha_question } = usePage().props as PageProps;
const [showFlashMessage, setFlashMessage] = useState(!!flash); const [showFlashMessage, setFlashMessage] = useState(!!flash);
const [selectedPlan, setSelectedPlan] = useState(plans?.[0]?.identifier ?? null); const [selectedPlan, setSelectedPlan] = useState(plans?.[0]?.identifier ?? null);
const [amount, setAmount] = useState(plans?.[0]?.price ?? 0); const [amount, setAmount] = useState(plans?.[0]?.price ?? 0);
const features = [
"Boîte Mail",
"NextCloud",
"Mailing list",
"Hébergement de site",
"Sondage",
"Et plus encore ...",
];
// /!\ Existant à discuter avec client
/*const today = new Date();
const actualMonth = today.getMonth() + 1;
const leftMonths = 12 - actualMonth;*/
/*const getAmount = (plan: string | null): number => {
if (!plan) return 0;
const baseAmount = leftMonths;
switch (plan) {
case 'custom':
return baseAmount;
case 'one-year':
return baseAmount + 12;
case 'two-year':
return baseAmount + 24;
default:
return 0;
}
};*/
useEffect(() => { useEffect(() => {
if (plans && selectedPlan) { if (plans && selectedPlan) {
const plan = plans.find(p => p.identifier === selectedPlan); const plan = plans.find((p) => p.identifier === selectedPlan);
if (plan) { if (plan) setAmount(plan.price);
setAmount(plan.price);
}
} }
}, [selectedPlan, plans]); }, [selectedPlan, plans]);
@@ -64,257 +38,196 @@ export default function Membership() {
return ( return (
<> <>
<Head title="Adhérer au Retzien Libre"/> <Head title="Adhérer au Retzien Libre" />
<div <div className="flex flex-col min-h-screen bg-white dark:bg-[#0a0a0a] text-[#1b1b18] dark:text-[#EDEDEC]">
className="flex min-h-screen flex-col items-center bg-[#F5F5F5] p-6 text-[#1b1b18] lg:justify-center lg:p-8 dark:bg-[#0a0a0a]"> <div className="flex flex-col items-center px-4">
<NavGuestLayout/> <NavGuestLayout />
<section className="flex flex-col items-center justify-center gap-4">
<div>
<h1>Adhérer au Retzien Libre</h1>
<p>
Saisissez vos informations ci-dessous pour créer une demande d'adhésion :
</p>
</div> </div>
<main className="flex-1 py-12">
<Container className="flex flex-col gap-10">
<SectionHeading
title="Adhérer au Retzien Libre"
color="primary"
subtitle="Rejoignez notre association et accédez à des outils libres, éthiques et respectueux de vos données."
align="left"
/>
{showFlashMessage && ( {showFlashMessage && (
<FlashMessage messages={flash ?? {}}/> <FlashMessage messages={flash ?? {}} />
)} )}
<Form <Form
{...MembershipFormController.store.form()} {...MembershipFormController.store.form()}
resetOnSuccess resetOnSuccess
disableWhileProcessing disableWhileProcessing
className="flex flex-col gap-6"
> >
{({processing, errors}) => ( {({ processing, errors }) => (
<div className="lg:w-5xl px-10"> <div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
<div className="flex flex-col md:flex-row gap-6 w-full">
<div className="w-full lg:w-1/2"> {/* Left — Personal info */}
<div className="grid gap-2 my-4"> <div className="bg-white dark:bg-[#171717] rounded-2xl border-3 border-black p-6 shadow-[4px_4px_0px_rgba(0,0,0,1)] flex flex-col gap-4">
<h2 className="text-lg font-semibold text-primary">Vos informations</h2>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div className="grid gap-1">
<Label htmlFor="lastname">Nom*</Label> <Label htmlFor="lastname">Nom*</Label>
<Input <Input id="lastname" name="lastname" type="text" required tabIndex={1} autoComplete="family-name" placeholder="Votre nom" />
id="lastname" <InputError message={errors.lastname} />
type="text"
required
autoFocus
tabIndex={1}
autoComplete="lastname"
name="lastname"
placeholder="Votre Nom"
/>
<InputError
message={errors.lastname}
className="mt-2"
/>
</div> </div>
<div className="grid gap-2 my-4"> <div className="grid gap-1">
<Label htmlFor="firstname">Prénom*</Label> <Label htmlFor="firstname">Prénom*</Label>
<Input <Input id="firstname" name="firstname" type="text" required tabIndex={2} autoComplete="given-name" placeholder="Votre prénom" />
id="firstname" <InputError message={errors.firstname} />
type="text"
required
autoFocus
tabIndex={2}
autoComplete="firstname"
name="firstname"
placeholder="Votre Prénom"
/>
<InputError
message={errors.firstname}
className="mt-2"
/>
</div> </div>
<div className="grid gap-2 my-4">
<Label htmlFor="company">Société</Label>
<Input
id="company"
type="text"
autoFocus
tabIndex={3}
autoComplete="company"
name="company"
placeholder="Votre société"
/>
<InputError
message={errors.firstname}
className="mt-2"
/>
</div> </div>
<div className="grid gap-2 my-4">
<Label htmlFor="email">Adresse Mail*</Label> <div className="grid gap-1">
<Input <Label htmlFor="email">Adresse e-mail*</Label>
id="email" <Input id="email" name="email" type="email" required tabIndex={3} autoComplete="email" placeholder="email@exemple.com" />
type="email" <InputError message={errors.email} />
required
tabIndex={4}
autoComplete="email"
name="email"
placeholder="email@exemple.com"
/>
<InputError message={errors.email}/>
</div> </div>
<div className="grid gap-2 my-4">
<div className="grid gap-1">
<Label htmlFor="phone1">Téléphone*</Label> <Label htmlFor="phone1">Téléphone*</Label>
<Input <Input id="phone1" name="phone1" type="tel" required tabIndex={4} autoComplete="tel" placeholder="Votre numéro de téléphone" />
id="phone1" <InputError message={errors.phone1} />
type="phone"
required
tabIndex={5}
autoComplete="phone"
name="phone1"
placeholder="Votre numéro de téléphone"
/>
<InputError message={errors.phone}/>
</div> </div>
<div className="grid gap-2 my-4">
<Label htmlFor="address">Votre adresse*</Label> <div className="grid gap-1">
<Input <Label htmlFor="company">Société <span className="text-muted-foreground text-xs">(facultatif)</span></Label>
id="address" <Input id="company" name="company" type="text" tabIndex={5} autoComplete="organization" placeholder="Votre société" />
type="text" <InputError message={errors.company} />
required
autoFocus
tabIndex={6}
autoComplete="address"
name="address"
placeholder="Votre adresse"
/>
<InputError
message={errors.address}
className="mt-2"
/>
</div> </div>
<div className="grid gap-2 my-4">
<Label htmlFor="zipcode">Votre code postale*</Label> <div className="grid gap-1">
<Input <Label htmlFor="address">Adresse*</Label>
id="zipcode" <Input id="address" name="address" type="text" required tabIndex={6} autoComplete="street-address" placeholder="Votre adresse" />
type="text" <InputError message={errors.address} />
required
autoFocus
tabIndex={7}
autoComplete="zipcode"
name="zipcode"
placeholder="Votre code postale"
/>
<InputError
message={errors.zipcode}
className="mt-2"
/>
</div> </div>
<div className="grid gap-2 my-4">
<Label htmlFor="city">Votre ville*</Label> <div className="grid grid-cols-2 gap-4">
<Input <div className="grid gap-1">
id="city" <Label htmlFor="zipcode">Code postal*</Label>
type="text" <Input id="zipcode" name="zipcode" type="text" required tabIndex={7} autoComplete="postal-code" placeholder="Code postal" />
required <InputError message={errors.zipcode} />
autoFocus </div>
tabIndex={8} <div className="grid gap-1">
autoComplete="city" <Label htmlFor="city">Ville*</Label>
name="city" <Input id="city" name="city" type="text" required tabIndex={8} autoComplete="address-level2" placeholder="Ville" />
placeholder="Votre ville" <InputError message={errors.city} />
/>
<InputError
message={errors.city}
className="mt-2"
/>
</div> </div>
</div> </div>
<div className="w-full lg:w-1/2"> </div>
<div className="space-y-4">
<Label htmlFor="package">Formule d'adhésion*</Label> {/* Right — Plan, services, captcha, submit */}
<div className="grid grid-cols-3 gap-2 my-4"> <div className="flex flex-col gap-6">
{/* Plan selection */}
<div className="bg-white dark:bg-[#171717] rounded-2xl border-3 border-black p-6 shadow-[4px_4px_0px_rgba(0,0,0,1)] flex flex-col gap-4">
<h2 className="text-lg font-semibold text-primary">Choisissez votre formule</h2>
<div className="flex flex-col gap-3">
{plans?.map((plan) => ( {plans?.map((plan) => (
<button <button
key={plan.id} key={plan.id}
type="button" type="button"
tabIndex={8} tabIndex={9}
onClick={() => setSelectedPlan(plan.identifier)} onClick={() => setSelectedPlan(plan.identifier)}
className={cn( className={cn(
"flex flex-col items-center justify-center rounded border-3 p-4 transition-colors", 'flex items-center justify-between rounded-xl border-3 border-black px-5 py-4 text-left transition-all duration-150',
'shadow-[3px_3px_0px_rgba(0,0,0,1)] hover:shadow-none hover:translate-x-0.5 hover:translate-y-0.5',
selectedPlan === plan.identifier selectedPlan === plan.identifier
? "border-primary" ? 'bg-primary text-black'
: "border-black hover:border-primary" : 'bg-white dark:bg-[#1a1a1a] hover:bg-primary/20',
)} )}
> >
<span className="font-semibold">{plan.name}</span> <div className="flex flex-col">
<span className="font-bold text-lg">{plan.price}</span> <span className="font-bold text-base">{plan.name}</span>
<span className="text-center text-muted-foreground text-xs"> {plan.months != null ? (
{plan.description} <span className="text-xs text-muted-foreground">
{plan.months} mois × 1/mois
</span> </span>
) : (
<span className="text-xs text-muted-foreground">{plan.description}</span>
)}
</div>
<span className="text-2xl font-black">{plan.price}</span>
</button> </button>
))} ))}
</div>
<input type="hidden" name="package" value={selectedPlan ?? ''} /> <input type="hidden" name="package" value={selectedPlan ?? ''} />
<input type="hidden" name="amount" value={amount} /> <input type="hidden" name="amount" value={amount} />
</div> <InputError message={errors.package} />
<div className="flex-col gap-6 ">
<div className="text-center"> <p className="text-center text-sm text-muted-foreground border-t border-border pt-3">
<p className="text-center font-semibold text-lg"> Montant total : <strong className="text-primary text-lg">{amount}</strong>
Montant à payer : <br/> <span className="text-primary">{amount} </span>
</p> </p>
</div> </div>
<div className="pl-10 space-y-2">
<h4 className="font-semibold text-sm">Fonctionnalités inclues :</h4> {/* Services included */}
<ul className="space-y-2"> <div className="bg-white dark:bg-[#171717] rounded-2xl border-3 border-black p-6 shadow-[4px_4px_0px_rgba(0,0,0,1)] flex flex-col gap-4">
{features.map((feature, index) => ( <h2 className="text-lg font-semibold text-primary">Services inclus</h2>
<li className="flex items-center gap-2 my-4 text-sm" <div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
key={index}> {services?.map((service) => (
<CheckIcon className="h-4 w-4 text-primary"/> <div key={service.name} className="flex items-start gap-2">
<span>{feature}</span> <CheckIcon className="h-4 w-4 mt-0.5 shrink-0 text-accent" />
</li> <div>
<p className="text-sm font-medium leading-tight">{service.name}</p>
<p className="text-xs text-muted-foreground">{service.description}</p>
</div>
</div>
))} ))}
</ul>
</div> </div>
</div> </div>
</div> {/* Captcha + CGU + Submit */}
</div> <div className="bg-white dark:bg-[#171717] rounded-2xl border-3 border-black p-6 shadow-[4px_4px_0px_rgba(0,0,0,1)] flex flex-col gap-5">
</div> <div className="grid gap-1">
<div className="mx-auto justify-center"> <Label htmlFor="captcha" className="font-semibold">{captcha_question}</Label>
<div className="w-[300px] grid gap-2 my-4">
<Label htmlFor="captcha">Captcha</Label>
<Input <Input
id="captcha" id="captcha"
type="text"
autoFocus
tabIndex={9}
name="captcha" name="captcha"
placeholder="Entrez le captcha ci-dessous" type="text"
/>
</div>
<div className="grid gap-2 my-4">
<div className="flex items-center space-x-2">
<Checkbox
id="cgu"
name="cgu"
tabIndex={10} tabIndex={10}
required placeholder="Votre réponse"
autoComplete="off"
className="max-w-[180px]"
/> />
<Label htmlFor="remember">J'ai lu et j'accepte les <a <InputError message={errors.captcha} />
href="#">C.G.U.</a>,
je comprends la nécessité des enregistrements de mes données
personnelles.</Label>
</div> </div>
<div className="flex flex-col gap-1">
<div className="flex items-start gap-3">
<Checkbox id="cgu" name="cgu" tabIndex={11} required className="mt-0.5" />
<Label htmlFor="cgu" className="text-sm leading-snug cursor-pointer">
J'ai lu et j'accepte les <a href="#">C.G.U.</a> et je comprends la
nécessité des enregistrements de mes données personnelles.
</Label>
</div> </div>
<div className="flex justify-center items-center"> <InputError message={errors.cgu} />
</div>
<Button <Button
variant="outline"
type="submit" type="submit"
className="mt-2 w-full max-w-1/3" variant="secondary"
tabIndex={11} tabIndex={12}
data-test="register-user-button" className="w-full border-3 border-black shadow-[4px_4px_0px_rgba(0,0,0,1)] hover:shadow-none hover:translate-x-0.5 hover:translate-y-0.5 font-bold text-base py-5"
> >
{processing && ( {processing && <LoaderCircle className="h-4 w-4 animate-spin" />}
<LoaderCircle className="h-4 w-4 animate-spin"/> Envoyer ma demande d'adhésion
)}
Envoyer
</Button> </Button>
</div> </div>
</div> </div>
</div> </div>
)} )}
</Form> </Form>
</section> </Container>
</main>
<Footer />
</div> </div>
</> </>
) );
} }

View File

@@ -441,7 +441,7 @@ register.form = registerForm
/** /**
* @see \App\Http\Controllers\Forms\ContactFormController::contact * @see \App\Http\Controllers\Forms\ContactFormController::contact
* @see app/Http/Controllers/Forms/ContactFormController.php:17 * @see app/Http/Controllers/Forms/ContactFormController.php:16
* @route '/contact' * @route '/contact'
*/ */
export const contact = (options?: RouteQueryOptions): RouteDefinition<'get'> => ({ export const contact = (options?: RouteQueryOptions): RouteDefinition<'get'> => ({
@@ -456,7 +456,7 @@ contact.definition = {
/** /**
* @see \App\Http\Controllers\Forms\ContactFormController::contact * @see \App\Http\Controllers\Forms\ContactFormController::contact
* @see app/Http/Controllers/Forms/ContactFormController.php:17 * @see app/Http/Controllers/Forms/ContactFormController.php:16
* @route '/contact' * @route '/contact'
*/ */
contact.url = (options?: RouteQueryOptions) => { contact.url = (options?: RouteQueryOptions) => {
@@ -465,7 +465,7 @@ contact.url = (options?: RouteQueryOptions) => {
/** /**
* @see \App\Http\Controllers\Forms\ContactFormController::contact * @see \App\Http\Controllers\Forms\ContactFormController::contact
* @see app/Http/Controllers/Forms/ContactFormController.php:17 * @see app/Http/Controllers/Forms/ContactFormController.php:16
* @route '/contact' * @route '/contact'
*/ */
contact.get = (options?: RouteQueryOptions): RouteDefinition<'get'> => ({ contact.get = (options?: RouteQueryOptions): RouteDefinition<'get'> => ({
@@ -475,7 +475,7 @@ contact.get = (options?: RouteQueryOptions): RouteDefinition<'get'> => ({
/** /**
* @see \App\Http\Controllers\Forms\ContactFormController::contact * @see \App\Http\Controllers\Forms\ContactFormController::contact
* @see app/Http/Controllers/Forms/ContactFormController.php:17 * @see app/Http/Controllers/Forms/ContactFormController.php:16
* @route '/contact' * @route '/contact'
*/ */
contact.head = (options?: RouteQueryOptions): RouteDefinition<'head'> => ({ contact.head = (options?: RouteQueryOptions): RouteDefinition<'head'> => ({
@@ -485,7 +485,7 @@ contact.head = (options?: RouteQueryOptions): RouteDefinition<'head'> => ({
/** /**
* @see \App\Http\Controllers\Forms\ContactFormController::contact * @see \App\Http\Controllers\Forms\ContactFormController::contact
* @see app/Http/Controllers/Forms/ContactFormController.php:17 * @see app/Http/Controllers/Forms/ContactFormController.php:16
* @route '/contact' * @route '/contact'
*/ */
const contactForm = (options?: RouteQueryOptions): RouteFormDefinition<'get'> => ({ const contactForm = (options?: RouteQueryOptions): RouteFormDefinition<'get'> => ({
@@ -495,7 +495,7 @@ const contactForm = (options?: RouteQueryOptions): RouteFormDefinition<'get'> =>
/** /**
* @see \App\Http\Controllers\Forms\ContactFormController::contact * @see \App\Http\Controllers\Forms\ContactFormController::contact
* @see app/Http/Controllers/Forms/ContactFormController.php:17 * @see app/Http/Controllers/Forms/ContactFormController.php:16
* @route '/contact' * @route '/contact'
*/ */
contactForm.get = (options?: RouteQueryOptions): RouteFormDefinition<'get'> => ({ contactForm.get = (options?: RouteQueryOptions): RouteFormDefinition<'get'> => ({
@@ -505,7 +505,7 @@ contactForm.get = (options?: RouteQueryOptions): RouteFormDefinition<'get'> => (
/** /**
* @see \App\Http\Controllers\Forms\ContactFormController::contact * @see \App\Http\Controllers\Forms\ContactFormController::contact
* @see app/Http/Controllers/Forms/ContactFormController.php:17 * @see app/Http/Controllers/Forms/ContactFormController.php:16
* @route '/contact' * @route '/contact'
*/ */
contactForm.head = (options?: RouteQueryOptions): RouteFormDefinition<'get'> => ({ contactForm.head = (options?: RouteQueryOptions): RouteFormDefinition<'get'> => ({
@@ -522,7 +522,7 @@ contact.form = contactForm
/** /**
* @see \App\Http\Controllers\Forms\MembershipFormController::membership * @see \App\Http\Controllers\Forms\MembershipFormController::membership
* @see app/Http/Controllers/Forms/MembershipFormController.php:21 * @see app/Http/Controllers/Forms/MembershipFormController.php:18
* @route '/membership' * @route '/membership'
*/ */
export const membership = (options?: RouteQueryOptions): RouteDefinition<'get'> => ({ export const membership = (options?: RouteQueryOptions): RouteDefinition<'get'> => ({
@@ -537,7 +537,7 @@ membership.definition = {
/** /**
* @see \App\Http\Controllers\Forms\MembershipFormController::membership * @see \App\Http\Controllers\Forms\MembershipFormController::membership
* @see app/Http/Controllers/Forms/MembershipFormController.php:21 * @see app/Http/Controllers/Forms/MembershipFormController.php:18
* @route '/membership' * @route '/membership'
*/ */
membership.url = (options?: RouteQueryOptions) => { membership.url = (options?: RouteQueryOptions) => {
@@ -546,7 +546,7 @@ membership.url = (options?: RouteQueryOptions) => {
/** /**
* @see \App\Http\Controllers\Forms\MembershipFormController::membership * @see \App\Http\Controllers\Forms\MembershipFormController::membership
* @see app/Http/Controllers/Forms/MembershipFormController.php:21 * @see app/Http/Controllers/Forms/MembershipFormController.php:18
* @route '/membership' * @route '/membership'
*/ */
membership.get = (options?: RouteQueryOptions): RouteDefinition<'get'> => ({ membership.get = (options?: RouteQueryOptions): RouteDefinition<'get'> => ({
@@ -556,7 +556,7 @@ membership.get = (options?: RouteQueryOptions): RouteDefinition<'get'> => ({
/** /**
* @see \App\Http\Controllers\Forms\MembershipFormController::membership * @see \App\Http\Controllers\Forms\MembershipFormController::membership
* @see app/Http/Controllers/Forms/MembershipFormController.php:21 * @see app/Http/Controllers/Forms/MembershipFormController.php:18
* @route '/membership' * @route '/membership'
*/ */
membership.head = (options?: RouteQueryOptions): RouteDefinition<'head'> => ({ membership.head = (options?: RouteQueryOptions): RouteDefinition<'head'> => ({
@@ -566,7 +566,7 @@ membership.head = (options?: RouteQueryOptions): RouteDefinition<'head'> => ({
/** /**
* @see \App\Http\Controllers\Forms\MembershipFormController::membership * @see \App\Http\Controllers\Forms\MembershipFormController::membership
* @see app/Http/Controllers/Forms/MembershipFormController.php:21 * @see app/Http/Controllers/Forms/MembershipFormController.php:18
* @route '/membership' * @route '/membership'
*/ */
const membershipForm = (options?: RouteQueryOptions): RouteFormDefinition<'get'> => ({ const membershipForm = (options?: RouteQueryOptions): RouteFormDefinition<'get'> => ({
@@ -576,7 +576,7 @@ const membershipForm = (options?: RouteQueryOptions): RouteFormDefinition<'get'>
/** /**
* @see \App\Http\Controllers\Forms\MembershipFormController::membership * @see \App\Http\Controllers\Forms\MembershipFormController::membership
* @see app/Http/Controllers/Forms/MembershipFormController.php:21 * @see app/Http/Controllers/Forms/MembershipFormController.php:18
* @route '/membership' * @route '/membership'
*/ */
membershipForm.get = (options?: RouteQueryOptions): RouteFormDefinition<'get'> => ({ membershipForm.get = (options?: RouteQueryOptions): RouteFormDefinition<'get'> => ({
@@ -586,7 +586,7 @@ membershipForm.get = (options?: RouteQueryOptions): RouteFormDefinition<'get'> =
/** /**
* @see \App\Http\Controllers\Forms\MembershipFormController::membership * @see \App\Http\Controllers\Forms\MembershipFormController::membership
* @see app/Http/Controllers/Forms/MembershipFormController.php:21 * @see app/Http/Controllers/Forms/MembershipFormController.php:18
* @route '/membership' * @route '/membership'
*/ */
membershipForm.head = (options?: RouteQueryOptions): RouteFormDefinition<'get'> => ({ membershipForm.head = (options?: RouteQueryOptions): RouteFormDefinition<'get'> => ({

View File

@@ -1,7 +1,7 @@
import { queryParams, type RouteQueryOptions, type RouteDefinition, type RouteFormDefinition } from './../../wayfinder' import { queryParams, type RouteQueryOptions, type RouteDefinition, type RouteFormDefinition } from './../../wayfinder'
/** /**
* @see \App\Http\Controllers\Forms\MembershipFormController::store * @see \App\Http\Controllers\Forms\MembershipFormController::store
* @see app/Http/Controllers/Forms/MembershipFormController.php:35 * @see app/Http/Controllers/Forms/MembershipFormController.php:42
* @route '/membership' * @route '/membership'
*/ */
export const store = (options?: RouteQueryOptions): RouteDefinition<'post'> => ({ export const store = (options?: RouteQueryOptions): RouteDefinition<'post'> => ({
@@ -16,7 +16,7 @@ store.definition = {
/** /**
* @see \App\Http\Controllers\Forms\MembershipFormController::store * @see \App\Http\Controllers\Forms\MembershipFormController::store
* @see app/Http/Controllers/Forms/MembershipFormController.php:35 * @see app/Http/Controllers/Forms/MembershipFormController.php:42
* @route '/membership' * @route '/membership'
*/ */
store.url = (options?: RouteQueryOptions) => { store.url = (options?: RouteQueryOptions) => {
@@ -25,7 +25,7 @@ store.url = (options?: RouteQueryOptions) => {
/** /**
* @see \App\Http\Controllers\Forms\MembershipFormController::store * @see \App\Http\Controllers\Forms\MembershipFormController::store
* @see app/Http/Controllers/Forms/MembershipFormController.php:35 * @see app/Http/Controllers/Forms/MembershipFormController.php:42
* @route '/membership' * @route '/membership'
*/ */
store.post = (options?: RouteQueryOptions): RouteDefinition<'post'> => ({ store.post = (options?: RouteQueryOptions): RouteDefinition<'post'> => ({
@@ -35,7 +35,7 @@ store.post = (options?: RouteQueryOptions): RouteDefinition<'post'> => ({
/** /**
* @see \App\Http\Controllers\Forms\MembershipFormController::store * @see \App\Http\Controllers\Forms\MembershipFormController::store
* @see app/Http/Controllers/Forms/MembershipFormController.php:35 * @see app/Http/Controllers/Forms/MembershipFormController.php:42
* @route '/membership' * @route '/membership'
*/ */
const storeForm = (options?: RouteQueryOptions): RouteFormDefinition<'post'> => ({ const storeForm = (options?: RouteQueryOptions): RouteFormDefinition<'post'> => ({
@@ -45,7 +45,7 @@ const storeForm = (options?: RouteQueryOptions): RouteFormDefinition<'post'> =>
/** /**
* @see \App\Http\Controllers\Forms\MembershipFormController::store * @see \App\Http\Controllers\Forms\MembershipFormController::store
* @see app/Http/Controllers/Forms/MembershipFormController.php:35 * @see app/Http/Controllers/Forms/MembershipFormController.php:42
* @route '/membership' * @route '/membership'
*/ */
storeForm.post = (options?: RouteQueryOptions): RouteFormDefinition<'post'> => ({ storeForm.post = (options?: RouteQueryOptions): RouteFormDefinition<'post'> => ({

View File

@@ -54,8 +54,13 @@ export interface Plans {
identifier: string; identifier: string;
name: string; name: string;
price: number; price: number;
description?: string, description?: string;
is_active: boolean; months?: number | null;
}
export interface MembershipService {
name: string;
description: string;
} }
@@ -73,6 +78,8 @@ export interface PageProps {
flash?: FlashMessages; flash?: FlashMessages;
auth?: Auth; auth?: Auth;
plans?: Plans[]; plans?: Plans[];
services?: MembershipService[];
captcha_question?: string;
[key: string]: unknown; [key: string]: unknown;
} }

View File

@@ -1,3 +1,69 @@
<x-mail::message> <!DOCTYPE html>
{!! $body !!} <html lang="fr">
</x-mail::message> <head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
background-color: #f4f4f4;
margin: 0;
padding: 0;
color: #1b1b18;
}
.wrapper {
max-width: 600px;
margin: 40px auto;
background: #ffffff;
border: 3px solid #000;
border-radius: 12px;
overflow: hidden;
}
.header {
background-color: #f5a623;
padding: 24px 32px;
border-bottom: 3px solid #000;
}
.header h1 {
margin: 0;
font-size: 20px;
font-weight: 800;
color: #000;
}
.content {
padding: 32px;
font-size: 15px;
line-height: 1.7;
color: #1b1b18;
}
.content p {
margin: 0 0 16px;
}
.content a {
color: #00473e;
font-weight: bold;
}
.footer {
padding: 16px 32px;
background-color: #f9f9f9;
border-top: 2px solid #e0e0e0;
font-size: 12px;
color: #888;
text-align: center;
}
</style>
</head>
<body>
<div class="wrapper">
<div class="header">
<h1>{{ config('app.name') }}</h1>
</div>
<div class="content">
{!! $body !!}
</div>
<div class="footer">
&copy; {{ date('Y') }} {{ config('app.name') }}. Tous droits réservés.
</div>
</div>
</body>
</html>