feat(Notification & membership route)

This commit is contained in:
2025-10-26 00:16:25 +02:00
parent 868b9a837b
commit ac0a89e34d
19 changed files with 393 additions and 279 deletions

View File

@@ -27,7 +27,7 @@ class ContactFormController extends Controller
{ {
$validated = $request->validated(); $validated = $request->validated();
try { try {
$contact = $this->contactService->registerNewContactRequest($validated); $this->contactService->registerNewContactRequest($validated);
} catch (\Throwable $e) { } catch (\Throwable $e) {
\Log::error('Erreur lors de la création d\'un contact', [ \Log::error('Erreur lors de la création d\'un contact', [
'error' => $e->getMessage(), 'error' => $e->getMessage(),

View File

@@ -4,13 +4,11 @@ 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\Member;
use App\Models\Membership; use App\Models\Membership;
use App\Models\Package; use App\Models\Package;
use App\Services\MemberService; use App\Services\MemberService;
use Carbon\Carbon; use Carbon\Carbon;
use Illuminate\Http\RedirectResponse; use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Inertia\Inertia; use Inertia\Inertia;
class MembershipFormController extends Controller class MembershipFormController extends Controller
@@ -25,7 +23,7 @@ class MembershipFormController extends Controller
return Inertia::render('forms/membership', [ return Inertia::render('forms/membership', [
'plans' => Package::query() 'plans' => Package::query()
->where('is_active', true) ->where('is_active', true)
->select('id', 'name', 'price', 'description') ->select('id', 'identifier', 'name', 'price', 'description')
->get() ->get()
]); ]);
} }
@@ -36,6 +34,7 @@ class MembershipFormController extends Controller
*/ */
public function store(MembershipRequest $request): RedirectResponse public function store(MembershipRequest $request): RedirectResponse
{ {
dd($request->validated());
$validated = $request->validated(); $validated = $request->validated();
try { try {
@@ -49,11 +48,11 @@ class MembershipFormController extends Controller
return redirect() return redirect()
->route('membership') ->route('membership')
->with('error', __('memberships.subscription.error')); ->with('error', Membership::getAttributeLabel('memberships.subscription.error'));
} }
return redirect() return redirect()
->route('membership') ->route('membership')
->with('success', __('memberships.subscription.success')); ->with('success', Membership::getAttributeLabel('memberships.subscription.success'));
} }
} }

View File

@@ -11,7 +11,7 @@ class ContactRequest extends FormRequest
*/ */
public function authorize(): bool public function authorize(): bool
{ {
return false; return true;
} }
/** /**

View File

@@ -11,7 +11,7 @@ class MembershipRequest extends FormRequest
*/ */
public function authorize(): bool public function authorize(): bool
{ {
return false; return true;
} }
/** /**
@@ -26,12 +26,11 @@ class MembershipRequest 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',
'company' => 'required|string|max:255', 'company' => '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',
'group_id' => 'required|string|max:255',
// Membership // Membership
'package' => 'required|string|max:255', 'package' => 'required|string|max:255',

View File

@@ -3,6 +3,7 @@
namespace App\Listeners; namespace App\Listeners;
use App\Events\MemberRegistered; use App\Events\MemberRegistered;
use App\Models\User;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\InteractsWithQueue;
@@ -21,6 +22,9 @@ class NotifyAdminForMembershipRequest
*/ */
public function handle(MemberRegistered $event): void public function handle(MemberRegistered $event): void
{ {
// $admin = User::where('name', 'SuperAdmin')->first();
$admin->notify(new AdminNewUserPending($event->user));
} }
} }

View File

@@ -6,6 +6,7 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Notifications\Notifiable;
/** /**
* @property int $id * @property int $id
@@ -63,7 +64,7 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
*/ */
class Member extends Model class Member extends Model
{ {
use HasFactory; use HasFactory, Notifiable;
protected $fillable = [ protected $fillable = [
'user_id', 'user_id',
'keycloak_id', 'keycloak_id',

View File

@@ -59,7 +59,7 @@ class Membership extends Model
public static function getAttributeLabel(string $attribute): string public static function getAttributeLabel(string $attribute): string
{ {
return __("membership.fields.$attribute"); return __("memberships.fields.$attribute");
} }
public function member(): BelongsTo public function member(): BelongsTo

View File

@@ -17,5 +17,9 @@ class ContactService
$contact->fill($data); $contact->fill($data);
$contact->save(); $contact->save();
// Envoyer un email à l'administrateur
return $contact;
} }
} }

View File

@@ -2,6 +2,7 @@
namespace App\Services; namespace App\Services;
use App\Events\MemberRegistered;
use App\Models\Member; use App\Models\Member;
use App\Models\MemberGroup; use App\Models\MemberGroup;
use App\Models\Package; use App\Models\Package;
@@ -29,7 +30,6 @@ class MemberService
$member->firstname = $data['firstname']; $member->firstname = $data['firstname'];
$member->email = $data['email']; $member->email = $data['email'];
$member->company = $data['company'] ?? null; $member->company = $data['company'] ?? null;
$member->date_of_birth = Carbon::parse($data['date_of_birth'])->format('Y-m-d H:i:s') ?? null;
$member->address = $data['address']; $member->address = $data['address'];
$member->zipcode = $data['zipcode']; $member->zipcode = $data['zipcode'];
$member->city = $data['city']; $member->city = $data['city'];
@@ -38,7 +38,7 @@ class MemberService
$member->save(); $member->save();
} }
$package = Package::where('id', $data['package_id']) $package = Package::where('identifier', $data['package'])
->where('is_active', true) ->where('is_active', true)
->firstOrFail(); ->firstOrFail();
@@ -47,11 +47,13 @@ class MemberService
'status' => 'pending', 'status' => 'pending',
'package_id' => $package->id ?? null, 'package_id' => $package->id ?? null,
'amount' => $data['amount'], 'amount' => $data['amount'],
'payment_status' => 'pending', 'payment_status' => 'unpaid',
]); ]);
// Notify Admin // Notify Admin
$admin = Member::where('role', 'admin')->first();
event(new MemberRegistered($admin));
return $member; return $member;

View File

@@ -18,7 +18,7 @@ return [
| |
*/ */
'default' => env('LOG_CHANNEL', 'stack'), 'default' => env('LOG_CHANNEL', 'daily'),
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
@@ -126,7 +126,12 @@ return [
'emergency' => [ 'emergency' => [
'path' => storage_path('logs/laravel.log'), 'path' => storage_path('logs/laravel.log'),
], ],
'query' => [
'driver' => 'daily',
'path' => storage_path('logs/db/query.log'),
'level' => 'debug',
'days' => 3,
],
], ],
]; ];

View File

@@ -16,7 +16,7 @@ return new class extends Migration
$table->foreignId('member_id')->constrained('members')->onDelete('cascade'); $table->foreignId('member_id')->constrained('members')->onDelete('cascade');
$table->foreignId('admin_id')->nullable()->constrained('users')->onDelete('set null'); $table->foreignId('admin_id')->nullable()->constrained('users')->onDelete('set null');
$table->foreignId('package_id')->constrained('packages')->onDelete('cascade'); $table->foreignId('package_id')->constrained('packages')->onDelete('cascade');
$table->date('start_date'); $table->date('start_date')->nullable();
$table->date('end_date')->nullable(); $table->date('end_date')->nullable();
$table->enum('status', ['active', 'expired', 'pending'])->default('pending'); $table->enum('status', ['active', 'expired', 'pending'])->default('pending');
$table->decimal('amount', 10, 2)->default(0); $table->decimal('amount', 10, 2)->default(0);

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:23 * @see app/Http/Controllers/Forms/MembershipFormController.php:21
* @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:23 * @see app/Http/Controllers/Forms/MembershipFormController.php:21
* @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:23 * @see app/Http/Controllers/Forms/MembershipFormController.php:21
* @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:23 * @see app/Http/Controllers/Forms/MembershipFormController.php:21
* @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:23 * @see app/Http/Controllers/Forms/MembershipFormController.php:21
* @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:23 * @see app/Http/Controllers/Forms/MembershipFormController.php:21
* @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:23 * @see app/Http/Controllers/Forms/MembershipFormController.php:21
* @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:37 * @see app/Http/Controllers/Forms/MembershipFormController.php:35
* @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:37 * @see app/Http/Controllers/Forms/MembershipFormController.php:35
* @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:37 * @see app/Http/Controllers/Forms/MembershipFormController.php:35
* @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:37 * @see app/Http/Controllers/Forms/MembershipFormController.php:35
* @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:37 * @see app/Http/Controllers/Forms/MembershipFormController.php:35
* @route '/membership' * @route '/membership'
*/ */
storeForm.post = (options?: RouteQueryOptions): RouteFormDefinition<'post'> => ({ storeForm.post = (options?: RouteQueryOptions): RouteFormDefinition<'post'> => ({

View File

@@ -0,0 +1,41 @@
import {FlashMessages} from '@/types';
import {Alert, AlertDescription} from '@/components/ui/alert';
import {CheckCircle2, AlertCircle, AlertTriangle, Info} from 'lucide-react';
interface FlashMessageProps {
messages: FlashMessages;
}
export function FlashMessage({messages}: FlashMessageProps) {
if (!messages || Object.keys(messages).length === 0) return null;
return (
<div className="space-y-2">
{messages.success && (
<Alert className="border-green-500 bg-green-50 text-green-900">
<CheckCircle2 className="h-4 w-4 text-green-600"/>
<AlertDescription>{messages.success}</AlertDescription>
</Alert>
)}
{messages.error && (
<Alert className="border-red-500 bg-red-50 text-red-900">
<AlertCircle className="h-4 w-4 text-red-600"/>
<AlertDescription>{messages.error}</AlertDescription>
</Alert>
)}
{messages.warning && (
<Alert className="border-yellow-500 bg-yellow-50 text-yellow-900">
<AlertTriangle className="h-4 w-4 text-yellow-600"/>
<AlertDescription>{messages.warning}</AlertDescription>
</Alert>
)}
{messages.info && (
<Alert className="border-blue-500 bg-blue-50 text-blue-900">
<Info className="h-4 w-4 text-blue-600"/>
<AlertDescription>{messages.info}</AlertDescription>
</Alert>
)}
</div>
);
}

View File

@@ -1,7 +1,5 @@
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 {Alert, AlertDescription, AlertTitle} from "@/components/ui/alert";
import {CheckCircle2} from "lucide-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";
@@ -19,28 +17,31 @@ import {
} 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 {FlashMessage} from "@/components/flash-message";
export default function Contact() { export default function Contact() {
const {flash} = usePage().props; const {flash} = usePage().props as PageProps;
const [showSuccess, setShowSuccess] = useState(!!flash?.success); const [showFlashMessage, setFlashMessage] = useState(!!flash);
useEffect(() => { useEffect(() => {
if (flash?.success) { if (flash) {
setShowSuccess(true); setFlashMessage(true);
const timer = setTimeout(() => setShowSuccess(false), 5000); const timer = setTimeout(() => setFlashMessage(false), 5000);
return () => clearTimeout(timer); return () => clearTimeout(timer);
} }
}, [flash]); }, [flash]);
return ( return (
<> <>
<Head title="Nous contacter"/> <Head title="Nous contacter"/>
<div <div
className="flex min-h-screen flex-col items-center bg-[#F5F5F5] p-6 text-[#1b1b18] lg:justify-center lg:p-8 dark:bg-[#0a0a0a]"> className="flex flex-col items-center bg-[#F5F5F5] p-6 text-[#1b1b18] lg:justify-center lg:p-8 dark:bg-[#0a0a0a]">
<NavGuestLayout/> <NavGuestLayout/>
<div className="flex flex-col items-center justify-center gap-4"> <section className="flex flex-col h-screen items-center mt-15 gap-4">
<div> <div>
<h1>Nous contacter</h1> <h1>Nous contacter</h1>
<p> <p>
@@ -48,15 +49,8 @@ export default function Contact() {
</p> </p>
</div> </div>
{showSuccess && ( {showFlashMessage && (
<Alert className="border-green-500 bg-green-50 text-green-800"> <FlashMessage messages={flash ?? {}} />
<CheckCircle2 className="h-5 w-5 text-green-600"/>
<AlertTitle>Message envoyé !</AlertTitle>
<AlertDescription>{flash.success}</AlertDescription>
</Alert>
// Clean form
)} )}
<Form <Form
@@ -68,7 +62,7 @@ export default function Contact() {
<div className="lg:w-5xl px-10"> <div className="lg:w-5xl px-10">
<div className="flex gap-6 w-full"> <div className="flex gap-6 w-full">
<div className="w-1/2"> <div className="w-1/2">
<div className="grid gap-2"> <div className="grid gap-2 my-4">
<Label htmlFor="lastname">Nom*</Label> <Label htmlFor="lastname">Nom*</Label>
<Input <Input
id="lastname" id="lastname"
@@ -86,7 +80,7 @@ export default function Contact() {
/> />
</div> </div>
<div className="grid gap-2"> <div className="grid gap-2 my-4">
<Label htmlFor="firstname">Prénom*</Label> <Label htmlFor="firstname">Prénom*</Label>
<Input <Input
id="firstname" id="firstname"
@@ -104,7 +98,7 @@ export default function Contact() {
/> />
</div> </div>
<div className="grid gap-2"> <div className="grid gap-2 my-4">
<Label htmlFor="email">Adresse Mail*</Label> <Label htmlFor="email">Adresse Mail*</Label>
<Input <Input
id="email" id="email"
@@ -118,7 +112,7 @@ export default function Contact() {
<InputError message={errors.email}/> <InputError message={errors.email}/>
</div> </div>
<div className="grid gap-2"> <div className="grid gap-2 my-4">
<Label htmlFor="address">Votre adresse postale</Label> <Label htmlFor="address">Votre adresse postale</Label>
<Input <Input
id="address" id="address"
@@ -138,7 +132,7 @@ export default function Contact() {
</div> </div>
<div className="w-1/2"> <div className="w-1/2">
<div className="grid gap-2"> <div className="grid gap-2 my-4">
<Label htmlFor="subject">Objet de votre demande*</Label> <Label htmlFor="subject">Objet de votre demande*</Label>
<Select name="subject" required> <Select name="subject" required>
<SelectTrigger tabIndex={5}> <SelectTrigger tabIndex={5}>
@@ -156,9 +150,10 @@ export default function Contact() {
</Select> </Select>
</div> </div>
<div className="grid gap-2"> <div className="grid gap-2 my-4">
<Label htmlFor="message">Votre message</Label> <Label htmlFor="message">Votre message</Label>
<Textarea <Textarea
className="h-28"
id="message" id="message"
name="message" name="message"
tabIndex={6} tabIndex={6}
@@ -167,7 +162,7 @@ export default function Contact() {
/> />
</div> </div>
<div className="grid gap-2"> <div className="grid gap-2 my-4">
<Label htmlFor="captcha">Captcha</Label> <Label htmlFor="captcha">Captcha</Label>
<Input <Input
id="captcha" id="captcha"
@@ -197,7 +192,7 @@ export default function Contact() {
</div> </div>
)} )}
</Form> </Form>
</div> </section>
</div> </div>
</> </>
) )

View File

@@ -1,6 +1,6 @@
import {Form, Head} from "@inertiajs/react"; import {Form, Head, usePage} from "@inertiajs/react";
import {cn} from "@/lib/utils"; import {cn} from "@/lib/utils";
import {CheckIcon, LoaderCircle} from 'lucide-react'; import {CheckCircle2, 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";
@@ -8,38 +8,15 @@ 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 {useState} from "react"; import {useEffect, useState} from "react";
import {PageProps} from "@/types";
import {FlashMessage} from "@/components/flash-message";
export default function Membership() { export default function Membership() {
const today = new Date(); const {flash, plans} = usePage().props as PageProps
const actualMonth = today.getMonth() + 1; const [showFlashMessage, setFlashMessage] = useState(!!flash);
const leftMonths = 12 - actualMonth; const [selectedPlan, setSelectedPlan] = useState(plans?.[0]?.identifier ?? null);
const [amount, setAmount] = useState(plans?.[0]?.price ?? 0);
const [selectedPlan, setSelectedPlan] = useState<
"custom" | "one-year" | "two-year"
>("one-year");
const plans = [
{
id: "custom" as const,
name: "Sur-mesure",
price: `${leftMonths}`,
description: "Derniers mois de l'année.",
},
{
id: "one-year" as const,
name: "Un an",
price: "12€",
description: "Pour nous soutenir durant un an.",
},
{
id: "two-year" as const,
name: "Deux ans",
price: "24€",
description: "Pour nous soutenir durant deux ans.",
},
];
const features = [ const features = [
"Boîte Mail", "Boîte Mail",
"NextCloud", "NextCloud",
@@ -49,6 +26,42 @@ export default function Membership() {
"Et plus encore ...", "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(() => {
if (plans && selectedPlan) {
const plan = plans.find(p => p.identifier === selectedPlan);
if (plan) {
setAmount(plan.price);
}
}
}, [selectedPlan, plans]);
useEffect(() => {
if (flash) {
setFlashMessage(true);
const timer = setTimeout(() => setFlashMessage(false), 5000);
return () => clearTimeout(timer);
}
}, [flash]);
return ( return (
<> <>
<Head title="Adhérer au Retzien Libre"/> <Head title="Adhérer au Retzien Libre"/>
@@ -56,13 +69,18 @@ export default function Membership() {
className="flex min-h-screen flex-col items-center bg-[#F5F5F5] p-6 text-[#1b1b18] lg:justify-center lg:p-8 dark:bg-[#0a0a0a]"> className="flex min-h-screen flex-col items-center bg-[#F5F5F5] p-6 text-[#1b1b18] lg:justify-center lg:p-8 dark:bg-[#0a0a0a]">
<NavGuestLayout/> <NavGuestLayout/>
<div className="flex flex-col items-center justify-center gap-4"> <section className="flex flex-col items-center justify-center gap-4">
<div> <div>
<h1>Adhérer au Retzien Libre</h1> <h1>Adhérer au Retzien Libre</h1>
<p> <p>
Saisissez vos informations ci-dessous pour créer une demande d'adhésion : Saisissez vos informations ci-dessous pour créer une demande d'adhésion :
</p> </p>
</div> </div>
{showFlashMessage && (
<FlashMessage messages={flash ?? {}}/>
)}
<Form <Form
{...MembershipFormController.store.form()} {...MembershipFormController.store.form()}
resetOnSuccess resetOnSuccess
@@ -70,170 +88,189 @@ export default function Membership() {
className="flex flex-col gap-6" className="flex flex-col gap-6"
> >
{({processing, errors}) => ( {({processing, errors}) => (
<> <div className="lg:w-5xl px-10">
<div className="grid gap-6"> <div className="flex flex-col md:flex-row gap-6 w-full">
<div className="grid gap-2"> <div className="w-full lg:w-1/2">
<Label htmlFor="lastname">Nom*</Label> <div className="grid gap-2 my-4">
<Input <Label htmlFor="lastname">Nom*</Label>
id="lastname" <Input
type="text" id="lastname"
required type="text"
autoFocus required
tabIndex={1} autoFocus
autoComplete="lastname" tabIndex={1}
name="lastname" autoComplete="lastname"
placeholder="Votre Nom" name="lastname"
/> placeholder="Votre Nom"
<InputError />
message={errors.lastname} <InputError
className="mt-2" message={errors.lastname}
/> className="mt-2"
/>
</div>
<div className="grid gap-2 my-4">
<Label htmlFor="firstname">Prénom*</Label>
<Input
id="firstname"
type="text"
required
autoFocus
tabIndex={2}
autoComplete="firstname"
name="firstname"
placeholder="Votre Prénom"
/>
<InputError
message={errors.firstname}
className="mt-2"
/>
</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 className="grid gap-2 my-4">
<Label htmlFor="email">Adresse Mail*</Label>
<Input
id="email"
type="email"
required
tabIndex={4}
autoComplete="email"
name="email"
placeholder="email@exemple.com"
/>
<InputError message={errors.email}/>
</div>
<div className="grid gap-2 my-4">
<Label htmlFor="phone1">Téléphone*</Label>
<Input
id="phone1"
type="phone"
required
tabIndex={5}
autoComplete="phone"
name="phone1"
placeholder="Votre numéro de téléphone"
/>
<InputError message={errors.phone}/>
</div>
<div className="grid gap-2 my-4">
<Label htmlFor="address">Votre adresse*</Label>
<Input
id="address"
type="text"
required
autoFocus
tabIndex={6}
autoComplete="address"
name="address"
placeholder="Votre adresse"
/>
<InputError
message={errors.address}
className="mt-2"
/>
</div>
<div className="grid gap-2 my-4">
<Label htmlFor="zipcode">Votre code postale*</Label>
<Input
id="zipcode"
type="text"
required
autoFocus
tabIndex={7}
autoComplete="zipcode"
name="zipcode"
placeholder="Votre code postale"
/>
<InputError
message={errors.zipcode}
className="mt-2"
/>
</div>
<div className="grid gap-2 my-4">
<Label htmlFor="city">Votre ville*</Label>
<Input
id="city"
type="text"
required
autoFocus
tabIndex={8}
autoComplete="city"
name="city"
placeholder="Votre ville"
/>
<InputError
message={errors.city}
className="mt-2"
/>
</div>
</div> </div>
<div className="w-full lg:w-1/2">
<div className="grid gap-2"> <div className="space-y-4">
<Label htmlFor="firstname">Prénom*</Label> <Label htmlFor="package">Formule d'adhésion*</Label>
<Input <div className="grid grid-cols-3 gap-2 my-4">
id="firstname" {plans?.map((plan) => (
type="text" <button
required key={plan.id}
autoFocus type="button"
tabIndex={2} tabIndex={8}
autoComplete="firstname" onClick={() => setSelectedPlan(plan.identifier)}
name="firstname" className={cn(
placeholder="Votre Prénom" "flex flex-col items-center justify-center rounded border-3 p-4 transition-colors",
/> selectedPlan === plan.identifier
<InputError ? "border-primary"
message={errors.firstname} : "border-black hover:border-primary"
className="mt-2" )}
/> >
</div> <span className="font-semibold">{plan.name}</span>
<span className="font-bold text-lg">{plan.price}</span>
<div className="grid gap-2"> <span className="text-center text-muted-foreground text-xs">
<Label htmlFor="email">Adresse Mail*</Label> {plan.description}
<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">
<Label htmlFor="phone1">Téléphone*</Label>
<Input
id="phone1"
type="phone"
required
tabIndex={4}
autoComplete="phone"
name="phone1"
placeholder="Votre numéro de téléphone"
/>
<InputError message={errors.phone}/>
</div>
<div className="grid gap-2">
<Label htmlFor="address">Votre adresse*</Label>
<Input
id="address"
type="text"
required
autoFocus
tabIndex={5}
autoComplete="address"
name="address"
placeholder="Votre adresse"
/>
<InputError
message={errors.address}
className="mt-2"
/>
</div>
<div className="grid gap-2">
<Label htmlFor="zipcode">Votre code postale*</Label>
<Input
id="zipcode"
type="text"
required
autoFocus
tabIndex={6}
autoComplete="zipcode"
name="zipcode"
placeholder="Votre code postale"
/>
<InputError
message={errors.zipcode}
className="mt-2"
/>
</div>
<div className="grid gap-2">
<Label htmlFor="city">Votre ville*</Label>
<Input
id="city"
type="text"
required
autoFocus
tabIndex={6}
autoComplete="city"
name="city"
placeholder="Votre ville"
/>
<InputError
message={errors.city}
className="mt-2"
/>
</div>
<div className="space-y-4">
<Label htmlFor="package">Formule d'adhésion*</Label>
<div className="grid grid-cols-3 gap-2">
{plans.map((plan) => (
<button
className={cn(
"flex flex-col items-center justify-center rounded border-3 p-4 transition-colors",
selectedPlan === plan.id
? "border-primary"
: "border-black hover:border-primary"
)}
key={plan.id}
onClick={() => setSelectedPlan(plan.id)}
type="button"
>
<span className="font-semibold">{plan.name}</span>
<span className="font-bold text-lg">{plan.price}</span>
<span className="text-center text-muted-foreground text-xs">
{plan.description}
</span> </span>
</button> </button>
))}
</div>
<div className="space-y-2">
<h4 className="font-semibold text-sm">Fonctionnalités inclues :</h4>
<ul className="space-y-2">
{features.map((feature, index) => (
<li className="flex items-center gap-2 text-sm" key={index}>
<CheckIcon className="h-4 w-4 text-primary"/>
<span>{feature}</span>
</li>
))} ))}
</ul> <input type="hidden" name="package" value={selectedPlan ?? ''} />
<input type="hidden" name="amount" value={amount} />
</div>
<div className="flex-col gap-6 ">
<div className="text-center">
<p className="text-center font-semibold text-lg">
Montant à payer : <br/> <span className="text-primary">{amount} </span>
</p>
</div>
<div className="pl-10 space-y-2">
<h4 className="font-semibold text-sm">Fonctionnalités inclues :</h4>
<ul className="space-y-2">
{features.map((feature, index) => (
<li className="flex items-center gap-2 my-4 text-sm"
key={index}>
<CheckIcon className="h-4 w-4 text-primary"/>
<span>{feature}</span>
</li>
))}
</ul>
</div>
</div>
</div> </div>
</div> </div>
</div>
{/*<div className="grid gap-2"> <div className="mx-auto justify-center">
<div className="flex items-center space-x-2"> <div className="w-[300px] grid gap-2 my-4">
<Switch id="cloud-access" tabIndex={8}/>
<Label htmlFor="cloud-access">Me créer un accès au service du "cloud" ?</Label>
</div>
</div>*/}
<div className="grid gap-2">
<Label htmlFor="captcha">Captcha</Label> <Label htmlFor="captcha">Captcha</Label>
<Input <Input
id="captcha" id="captcha"
@@ -244,8 +281,7 @@ export default function Membership() {
placeholder="Entrez le captcha ci-dessous" placeholder="Entrez le captcha ci-dessous"
/> />
</div> </div>
<div className="grid gap-2 my-4">
<div className="grid gap-2">
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<Checkbox <Checkbox
id="cgu" id="cgu"
@@ -253,29 +289,31 @@ export default function Membership() {
tabIndex={10} tabIndex={10}
required required
/> />
<Label htmlFor="remember">J'ai lu et j'accepte les <a href="#">C.G.U.</a>, <Label htmlFor="remember">J'ai lu et j'accepte les <a
href="#">C.G.U.</a>,
je comprends la nécessité des enregistrements de mes données je comprends la nécessité des enregistrements de mes données
personnelles.</Label> personnelles.</Label>
</div> </div>
</div> </div>
<div className="flex justify-center items-center">
<Button <Button
variant="outline" variant="outline"
type="submit" type="submit"
className="mt-2 w-full" className="mt-2 w-full max-w-1/3"
tabIndex={11} tabIndex={11}
data-test="register-user-button" data-test="register-user-button"
> >
{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>
)} )}
</Form> </Form>
</div> </section>
</div> </div>
</> </>
) )

View File

@@ -448,7 +448,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:23 * @see app/Http/Controllers/Forms/MembershipFormController.php:21
* @route '/membership' * @route '/membership'
*/ */
export const membership = (options?: RouteQueryOptions): RouteDefinition<'get'> => ({ export const membership = (options?: RouteQueryOptions): RouteDefinition<'get'> => ({
@@ -463,7 +463,7 @@ membership.definition = {
/** /**
* @see \App\Http\Controllers\Forms\MembershipFormController::membership * @see \App\Http\Controllers\Forms\MembershipFormController::membership
* @see app/Http/Controllers/Forms/MembershipFormController.php:23 * @see app/Http/Controllers/Forms/MembershipFormController.php:21
* @route '/membership' * @route '/membership'
*/ */
membership.url = (options?: RouteQueryOptions) => { membership.url = (options?: RouteQueryOptions) => {
@@ -472,7 +472,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:23 * @see app/Http/Controllers/Forms/MembershipFormController.php:21
* @route '/membership' * @route '/membership'
*/ */
membership.get = (options?: RouteQueryOptions): RouteDefinition<'get'> => ({ membership.get = (options?: RouteQueryOptions): RouteDefinition<'get'> => ({
@@ -482,7 +482,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:23 * @see app/Http/Controllers/Forms/MembershipFormController.php:21
* @route '/membership' * @route '/membership'
*/ */
membership.head = (options?: RouteQueryOptions): RouteDefinition<'head'> => ({ membership.head = (options?: RouteQueryOptions): RouteDefinition<'head'> => ({
@@ -492,7 +492,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:23 * @see app/Http/Controllers/Forms/MembershipFormController.php:21
* @route '/membership' * @route '/membership'
*/ */
const membershipForm = (options?: RouteQueryOptions): RouteFormDefinition<'get'> => ({ const membershipForm = (options?: RouteQueryOptions): RouteFormDefinition<'get'> => ({
@@ -502,7 +502,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:23 * @see app/Http/Controllers/Forms/MembershipFormController.php:21
* @route '/membership' * @route '/membership'
*/ */
membershipForm.get = (options?: RouteQueryOptions): RouteFormDefinition<'get'> => ({ membershipForm.get = (options?: RouteQueryOptions): RouteFormDefinition<'get'> => ({
@@ -512,7 +512,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:23 * @see app/Http/Controllers/Forms/MembershipFormController.php:21
* @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:37 * @see app/Http/Controllers/Forms/MembershipFormController.php:35
* @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:37 * @see app/Http/Controllers/Forms/MembershipFormController.php:35
* @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:37 * @see app/Http/Controllers/Forms/MembershipFormController.php:35
* @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:37 * @see app/Http/Controllers/Forms/MembershipFormController.php:35
* @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:37 * @see app/Http/Controllers/Forms/MembershipFormController.php:35
* @route '/membership' * @route '/membership'
*/ */
storeForm.post = (options?: RouteQueryOptions): RouteFormDefinition<'post'> => ({ storeForm.post = (options?: RouteQueryOptions): RouteFormDefinition<'post'> => ({

View File

@@ -41,3 +41,29 @@ export interface User {
updated_at: string; updated_at: string;
[key: string]: unknown; // This allows for additional properties... [key: string]: unknown; // This allows for additional properties...
} }
export interface FlashMessages {
success?: string;
error?: string;
warning?: string;
info?: string;
}
export interface Plans {
id: number;
identifier: string;
name: string;
price: number;
description?: string,
is_active: boolean;
}
export interface PageProps {
flash?: FlashMessages;
auth?: Auth;
plans?: Plans[];
[key: string]: unknown;
}