Compare commits
3 Commits
81dbe4275d
...
7aef0210fd
| Author | SHA1 | Date | |
|---|---|---|---|
| 7aef0210fd | |||
| 6d300ac79c | |||
| 87e23ea50f |
30
app/Console/Commands/CreateISPWebAccounts.php
Normal file
30
app/Console/Commands/CreateISPWebAccounts.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class CreateISPWebAccounts extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'ext:create-ispweb-accounts';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Créer les comptes ISPWeb des membres en fonction de leur domaine';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ use App\Models\ListmonkMember;
|
||||
use App\Models\Member;
|
||||
use App\Models\Membership;
|
||||
use App\Models\Package;
|
||||
use App\Notifications\MemberRenewalReminderNotification;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Forms\Components\DatePicker;
|
||||
use Filament\Forms\Components\Select;
|
||||
@@ -17,6 +18,7 @@ use Filament\Forms\Components\Toggle;
|
||||
use Filament\Infolists\Components\RepeatableEntry;
|
||||
use Filament\Infolists\Components\TextEntry;
|
||||
use Filament\Infolists\Components\ViewEntry;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Schemas\Components\Grid;
|
||||
use Filament\Schemas\Components\Section;
|
||||
use Filament\Schemas\Components\Tabs;
|
||||
@@ -83,6 +85,11 @@ class MemberForm
|
||||
->default('physical')
|
||||
->required(),
|
||||
|
||||
Select::make('type_id')
|
||||
->label(Member::getAttributeLabel('type_id'))
|
||||
->relationship('type', 'name')
|
||||
->default(null),
|
||||
|
||||
Select::make('group_id')
|
||||
->label(Member::getAttributeLabel('group_id'))
|
||||
->relationship('group', 'name')
|
||||
@@ -98,6 +105,10 @@ class MemberForm
|
||||
->email()
|
||||
->required(),
|
||||
|
||||
TextInput::make('retzien_email')
|
||||
->label(Member::getAttributeLabel('retzien_email'))
|
||||
->email(),
|
||||
|
||||
TextInput::make('phone1')
|
||||
->label(Member::getAttributeLabel('phone1'))
|
||||
->tel(),
|
||||
@@ -366,8 +377,28 @@ class MemberForm
|
||||
->label(__('members.actions.send_renewal_mail'))
|
||||
->icon('heroicon-o-envelope')
|
||||
->color('primary')
|
||||
->action(function () {
|
||||
// Mail de relance à créer (Job)
|
||||
->requiresConfirmation()
|
||||
->modalHeading(__('members.actions.send_renewal_mail'))
|
||||
->modalDescription('Êtes-vous sûr de vouloir envoyer un mail de relance à cet adhérent ?')
|
||||
->action(function (Member $record) {
|
||||
try {
|
||||
$record->notify(new MemberRenewalReminderNotification($record));
|
||||
|
||||
Notification::make()
|
||||
->title('Mail de relance envoyé')
|
||||
->success()
|
||||
->send();
|
||||
} catch (\Throwable $e) {
|
||||
\Log::error('Erreur envoi mail de relance', [
|
||||
'member_id' => $record->id,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
Notification::make()
|
||||
->title('Erreur lors de l\'envoi du mail')
|
||||
->danger()
|
||||
->send();
|
||||
}
|
||||
}),
|
||||
])
|
||||
->extraAttributes(['class' => 'sticky top-4 h-fit']),
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace App\Http\Controllers\Forms;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Forms\MembershipRequest;
|
||||
use App\Models\MemberType;
|
||||
use App\Models\Package;
|
||||
use App\Models\Service;
|
||||
use App\Services\MemberService;
|
||||
@@ -18,23 +19,39 @@ class MembershipFormController extends Controller
|
||||
public function create(): Response
|
||||
{
|
||||
$remainingMonths = 13 - now()->month;
|
||||
$currentYear = now()->year;
|
||||
|
||||
$plans = Package::query()
|
||||
->where('is_active', true)
|
||||
->select('id', 'identifier', 'name', 'price', 'description')
|
||||
->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,
|
||||
]);
|
||||
->map(function (Package $p) use ($remainingMonths, $currentYear) {
|
||||
$months = match ($p->identifier) {
|
||||
'custom' => $remainingMonths,
|
||||
'one-year' => $remainingMonths + 12,
|
||||
'two-years' => $remainingMonths + 24,
|
||||
default => null,
|
||||
};
|
||||
|
||||
return [
|
||||
'id' => $p->id,
|
||||
'identifier' => $p->identifier,
|
||||
'name' => $p->name,
|
||||
'description' => $p->description,
|
||||
'price' => $months ?? (float) $p->price,
|
||||
'months' => $months,
|
||||
];
|
||||
});
|
||||
|
||||
$memberTypes = MemberType::query()
|
||||
->select('id', 'identifier', 'name')
|
||||
->get();
|
||||
|
||||
return Inertia::render('forms/membership', [
|
||||
'plans' => $plans,
|
||||
'services' => Service::query()->select('name', 'description')->get(),
|
||||
'member_types' => $memberTypes,
|
||||
'current_year' => $currentYear,
|
||||
'captcha_question' => $this->generateCaptcha(),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -29,10 +29,12 @@ class MembershipRequest extends FormRequest
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'member_type' => 'required|string|exists:member_types,identifier',
|
||||
'lastname' => 'required|string|max:255',
|
||||
'firstname' => 'required|string|max:255',
|
||||
'email' => 'required|email|max:255',
|
||||
'company' => 'nullable|string|max:255',
|
||||
'desired_retzien_email' => ['nullable', 'string', 'max:64', 'regex:/^[a-z0-9._-]+$/'],
|
||||
'address' => 'required|string|max:255',
|
||||
'zipcode' => ['required', 'digits:5'],
|
||||
'city' => 'required|string|max:255',
|
||||
|
||||
@@ -133,6 +133,11 @@ class Member extends Model
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function type(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(MemberType::class);
|
||||
}
|
||||
|
||||
public function group(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(MemberGroup::class);
|
||||
|
||||
17
app/Models/MemberType.php
Normal file
17
app/Models/MemberType.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class MemberType extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
protected $fillable = [
|
||||
'identifier',
|
||||
'name',
|
||||
'description',
|
||||
];
|
||||
}
|
||||
51
app/Notifications/MemberRenewalReminderNotification.php
Normal file
51
app/Notifications/MemberRenewalReminderNotification.php
Normal file
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace App\Notifications;
|
||||
|
||||
use App\Models\Member;
|
||||
use App\Models\NotificationTemplate;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
use Illuminate\Notifications\Notification;
|
||||
|
||||
class MemberRenewalReminderNotification extends Notification implements ShouldQueue
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
public function __construct(
|
||||
public readonly Member $member,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @return array<int, string>
|
||||
*/
|
||||
public function via(object $notifiable): array
|
||||
{
|
||||
return ['mail'];
|
||||
}
|
||||
|
||||
public function toMail(object $notifiable): MailMessage
|
||||
{
|
||||
$template = NotificationTemplate::findByIdentifier('member_renewal_reminder');
|
||||
|
||||
$vars = [
|
||||
'member_name' => $this->member->full_name,
|
||||
'app_name' => config('app.front_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 [];
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ namespace App\Services;
|
||||
use App\Events\MemberRegistered;
|
||||
use App\Models\Member;
|
||||
use App\Models\MemberGroup;
|
||||
use App\Models\MemberType;
|
||||
use App\Models\Package;
|
||||
use App\Notifications\MemberDeactivatedAdminNotification;
|
||||
use App\Notifications\MemberDeactivatedMemberNotification;
|
||||
@@ -22,11 +23,14 @@ class MemberService
|
||||
// Check if the member already exists
|
||||
$member = Member::where('email', $data['email'])->first();
|
||||
|
||||
$memberType = MemberType::where('identifier', $data['member_type'])->first();
|
||||
|
||||
if (! $member) {
|
||||
// Create a new member
|
||||
$member = new Member;
|
||||
$member->status = 'pending';
|
||||
$member->nature = 'physical';
|
||||
$member->type_id = $memberType?->id;
|
||||
$member->group_id = MemberGroup::where('identifier', 'website')->first()->id ?? null;
|
||||
$member->lastname = $data['lastname'];
|
||||
$member->firstname = $data['firstname'];
|
||||
@@ -37,6 +41,11 @@ class MemberService
|
||||
$member->city = $data['city'];
|
||||
$member->country = 'FR';
|
||||
$member->phone1 = $data['phone1'];
|
||||
|
||||
if (! empty($data['desired_retzien_email'])) {
|
||||
$member->retzien_email = $data['desired_retzien_email'].'@retzien.fr';
|
||||
}
|
||||
|
||||
$member->save();
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ class DatabaseSeeder extends Seeder
|
||||
{
|
||||
$this->call([
|
||||
MemberGroupSeeder::class,
|
||||
MemberTypeSeeder::class,
|
||||
PackageSeeder::class,
|
||||
ServiceSeeder::class,
|
||||
UserSeeder::class,
|
||||
|
||||
40
database/seeders/MemberTypeSeeder.php
Normal file
40
database/seeders/MemberTypeSeeder.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\MemberType;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class MemberTypeSeeder extends Seeder
|
||||
{
|
||||
public function run(): void
|
||||
{
|
||||
$types = [
|
||||
[
|
||||
'identifier' => 'individual',
|
||||
'name' => 'Particulier',
|
||||
'description' => 'Personne physique',
|
||||
],
|
||||
[
|
||||
'identifier' => 'company',
|
||||
'name' => 'Entreprise',
|
||||
'description' => 'Personne morale (société, auto-entrepreneur, etc.)',
|
||||
],
|
||||
[
|
||||
'identifier' => 'association',
|
||||
'name' => 'Association',
|
||||
'description' => 'Association loi 1901',
|
||||
],
|
||||
];
|
||||
|
||||
foreach ($types as $type) {
|
||||
MemberType::updateOrCreate(
|
||||
['identifier' => $type['identifier']],
|
||||
[
|
||||
'name' => $type['name'],
|
||||
'description' => $type['description'],
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -169,6 +169,25 @@ class NotificationTemplateSeeder extends Seeder
|
||||
]
|
||||
);
|
||||
|
||||
NotificationTemplate::updateOrCreate(
|
||||
['identifier' => 'member_renewal_reminder'],
|
||||
[
|
||||
'name' => 'Relance adhésion — générique',
|
||||
'subject' => 'Rappel concernant votre adhésion — {app_name}',
|
||||
'body' => '<p>Bonjour {member_name},</p>'
|
||||
.'<p>Nous vous contactons au sujet de votre adhésion au sein de notre association.</p>'
|
||||
.'<p>Si votre adhésion est arrivée à expiration ou si vous n\'avez pas encore finalisé votre inscription, '
|
||||
.'nous vous invitons à régulariser votre situation afin de continuer à bénéficier de l\'ensemble de nos services.</p>'
|
||||
.'<p>Pour toute question ou besoin d\'assistance, n\'hésitez pas à nous contacter.</p>'
|
||||
.'<p>Cordialement,<br>L\'équipe {app_name}</p>',
|
||||
'variables' => [
|
||||
'member_name' => 'Nom complet du membre',
|
||||
'app_name' => 'Nom de l\'application',
|
||||
],
|
||||
'is_active' => true,
|
||||
]
|
||||
);
|
||||
|
||||
NotificationTemplate::updateOrCreate(
|
||||
['identifier' => 'admin_password_reset'],
|
||||
[
|
||||
|
||||
@@ -12,20 +12,20 @@ class PackageSeeder extends Seeder
|
||||
$packages = [
|
||||
[
|
||||
'identifier' => 'custom',
|
||||
'name' => 'Sur-mesure',
|
||||
'description' => 'Calcul du nombre de mois restant dans l\'année',
|
||||
'name' => 'Année en cours',
|
||||
'description' => 'Mois restants dans l\'année civile en cours (1€/mois)',
|
||||
'price' => '1.00',
|
||||
],
|
||||
[
|
||||
'identifier' => 'one-year',
|
||||
'name' => 'Un an',
|
||||
'description' => '12 mois à compter de la date de validation de l\'adhésion du membre',
|
||||
'name' => 'Année en cours + 1 an',
|
||||
'description' => 'Jusqu\'à la fin de l\'année civile suivante (1€/mois)',
|
||||
'price' => '12.00',
|
||||
],
|
||||
[
|
||||
'identifier' => 'two-years',
|
||||
'name' => 'Deux ans',
|
||||
'description' => '24 mois à compter de la date de validation de l\'adhésion du membre',
|
||||
'name' => 'Année en cours + 2 ans',
|
||||
'description' => 'Jusqu\'à la fin de la 2e année civile suivante (1€/mois)',
|
||||
'price' => '24.00',
|
||||
],
|
||||
];
|
||||
@@ -42,4 +42,4 @@ class PackageSeeder extends Seeder
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,11 @@ return [
|
||||
'country' => 'Country',
|
||||
'phone1' => 'Phone 1',
|
||||
'phone2' => 'Phone 2',
|
||||
'type_id' => 'Member type',
|
||||
'individual' => 'Individual',
|
||||
'company' => 'Company',
|
||||
'association' => 'Association',
|
||||
'retzien_email' => 'Retzien email',
|
||||
'public_membership' => 'Public membership',
|
||||
'created_at' => 'Created at',
|
||||
'updated_at' => 'Updated at',
|
||||
|
||||
@@ -28,6 +28,11 @@ return [
|
||||
'country' => 'Pays',
|
||||
'phone1' => 'Téléphone fixe',
|
||||
'phone2' => 'Téléphone portable',
|
||||
'type_id' => 'Type d\'adhérent',
|
||||
'individual' => 'Particulier',
|
||||
'company' => 'Entreprise',
|
||||
'association' => 'Association',
|
||||
'retzien_email' => 'Adresse mail Retzien',
|
||||
'public_membership' => 'Adhésion publique',
|
||||
'created_at' => 'Créé le',
|
||||
'updated_at' => 'Mis à jour le',
|
||||
|
||||
1212
package-lock.json
generated
1212
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -54,6 +54,7 @@
|
||||
"input-otp": "^1.4.2",
|
||||
"laravel-vite-plugin": "^2.0",
|
||||
"lucide-react": "^0.475.0",
|
||||
"radix-ui": "^1.6.7",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"tailwind-merge": "^3.0.1",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { queryParams, type RouteQueryOptions, type RouteDefinition, type RouteFormDefinition } from './../../../../../wayfinder'
|
||||
/**
|
||||
* @see \App\Http\Controllers\Forms\MembershipFormController::create
|
||||
* @see app/Http/Controllers/Forms/MembershipFormController.php:18
|
||||
* @see app/Http/Controllers/Forms/MembershipFormController.php:19
|
||||
* @route '/membership'
|
||||
*/
|
||||
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.php:18
|
||||
* @see app/Http/Controllers/Forms/MembershipFormController.php:19
|
||||
* @route '/membership'
|
||||
*/
|
||||
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.php:18
|
||||
* @see app/Http/Controllers/Forms/MembershipFormController.php:19
|
||||
* @route '/membership'
|
||||
*/
|
||||
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.php:18
|
||||
* @see app/Http/Controllers/Forms/MembershipFormController.php:19
|
||||
* @route '/membership'
|
||||
*/
|
||||
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.php:18
|
||||
* @see app/Http/Controllers/Forms/MembershipFormController.php:19
|
||||
* @route '/membership'
|
||||
*/
|
||||
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.php:18
|
||||
* @see app/Http/Controllers/Forms/MembershipFormController.php:19
|
||||
* @route '/membership'
|
||||
*/
|
||||
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.php:18
|
||||
* @see app/Http/Controllers/Forms/MembershipFormController.php:19
|
||||
* @route '/membership'
|
||||
*/
|
||||
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.php:42
|
||||
* @see app/Http/Controllers/Forms/MembershipFormController.php:59
|
||||
* @route '/membership'
|
||||
*/
|
||||
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.php:42
|
||||
* @see app/Http/Controllers/Forms/MembershipFormController.php:59
|
||||
* @route '/membership'
|
||||
*/
|
||||
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.php:42
|
||||
* @see app/Http/Controllers/Forms/MembershipFormController.php:59
|
||||
* @route '/membership'
|
||||
*/
|
||||
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.php:42
|
||||
* @see app/Http/Controllers/Forms/MembershipFormController.php:59
|
||||
* @route '/membership'
|
||||
*/
|
||||
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.php:42
|
||||
* @see app/Http/Controllers/Forms/MembershipFormController.php:59
|
||||
* @route '/membership'
|
||||
*/
|
||||
storeForm.post = (options?: RouteQueryOptions): RouteFormDefinition<'post'> => ({
|
||||
|
||||
@@ -6,7 +6,7 @@ export function AboutSection() {
|
||||
<section id="about-section" className="w-full py-16">
|
||||
<div className="max-w-7xl mx-auto px-4">
|
||||
<SectionHeading title="Qui sommes-nous ?" color="secondary"
|
||||
subtitle="Le Retzien Libre, c’est une association qui promeut l’auto-hébergement et la décentralisation des services en ligne depuis 2017."
|
||||
subtitle="Le Retzien Libre est une association qui promeut l’auto-hébergement et la décentralisation des services en ligne depuis 2017."
|
||||
align='left'/>
|
||||
<div
|
||||
className="nb-shadow-static bg-white rounded-4xl mt-10 px-10 pt-20 pb-10">
|
||||
@@ -14,14 +14,14 @@ export function AboutSection() {
|
||||
<div className="flex flex-col gap-3 lg:border-r-2 border-black lg:pr-10 border-0">
|
||||
<h3 className="text-xl text-primary font-semibold">Une association locale</h3>
|
||||
<p>
|
||||
Nous voulons vous proposer à travers des outils libres, ouverts et solidaires, de
|
||||
Nous vous proposons, à travers des outils libres, ouverts et solidaires, de
|
||||
quitter l'industrie du G.A.F.A.M.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3 border-r-2 border-black pr-10">
|
||||
<h3 className="text-xl text-primary font-semibold">Notre mission</h3>
|
||||
<p>
|
||||
Nous nous positionnons comme "A.M.A.P. informatique", délivrant des services "bio",
|
||||
Nous sommes comme une "A.M.A.P. informatique", délivrant des services "bio",
|
||||
"éthiques" et "locaux" dans le Pays de Retz et Nantes.
|
||||
</p>
|
||||
</div>
|
||||
@@ -34,7 +34,12 @@ export function AboutSection() {
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full flex justify-center mt-20">
|
||||
<Button variant="default">En savoir plus en lisant notre Blog</Button>
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={() => window.open('https://blog.retzien.fr/index.php', '_blank')}
|
||||
>
|
||||
En savoir plus en lisant notre Blog
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -23,8 +23,8 @@ export function AlternativeSection() {
|
||||
Notre alternative : Le Retzien Libre
|
||||
</h2>
|
||||
<p>
|
||||
Ici, pas d’exploitation des vos données personnelles à des fins commerciales, ni de dépense aux services centralisés.<br/>
|
||||
Le Retzien Libre, c’est une association locale engagée pour la promotion du logiciel libre et la protection de vos données personnelles.
|
||||
Ici, pas d’exploitation des vos données personnelles à des fins commerciales, ni de dépendance aux services centralisés.<br/>
|
||||
Le Retzien Libre est une association locale engagée pour la promotion du logiciel libre et la protection de vos données personnelles.
|
||||
</p>
|
||||
{auth.user ? (
|
||||
<Link href={dashboard()}>
|
||||
|
||||
@@ -53,7 +53,7 @@ const services: Service[] = [
|
||||
},
|
||||
{
|
||||
title: 'Partage de fichiers',
|
||||
description: 'Partager facielement vos fichiers en toute sécurité',
|
||||
description: 'Partager facilement vos fichiers en toute sécurité',
|
||||
colorTitle: 'black',
|
||||
bgTitle: 'white',
|
||||
bgColor: 'primary',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {Link} from '@inertiajs/react';
|
||||
import {contact, home, membership} from '@/routes';
|
||||
import {contact, faq, home, membership} from '@/routes';
|
||||
import AppLogoIcon from '@/components/app-logo-icon';
|
||||
import LogoChaton from '@/img/utils/logo-chaton.png';
|
||||
|
||||
@@ -32,6 +32,8 @@ export function Footer() {
|
||||
<Link href={home()} className="text-sm text-white no-underline hover:underline">Accueil</Link>
|
||||
<Link href={contact()} className="text-sm text-white no-underline hover:underline">Contact</Link>
|
||||
<Link href={membership()} className="text-sm text-white no-underline hover:underline">Adhérer</Link>
|
||||
<Link href={faq()} className="text-sm text-white no-underline hover:underline">FAQ</Link>
|
||||
<a href="https://doc.retzien.fr" target="_blank" rel="noopener noreferrer" className="text-sm text-white no-underline hover:underline">Documentation</a>
|
||||
</nav>
|
||||
</div>
|
||||
<div className="flex flex-col lg:flex-row lg:justify-between gap-4 border-t border-black/20 pt-6 text-sm">
|
||||
|
||||
64
resources/js/components/ui/accordion.tsx
Normal file
64
resources/js/components/ui/accordion.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
import * as React from "react"
|
||||
import { ChevronDownIcon } from "lucide-react"
|
||||
import { Accordion as AccordionPrimitive } from "radix-ui"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Accordion({
|
||||
...props
|
||||
}: React.ComponentProps<typeof AccordionPrimitive.Root>) {
|
||||
return <AccordionPrimitive.Root data-slot="accordion" {...props} />
|
||||
}
|
||||
|
||||
function AccordionItem({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AccordionPrimitive.Item>) {
|
||||
return (
|
||||
<AccordionPrimitive.Item
|
||||
data-slot="accordion-item"
|
||||
className={cn("border-b last:border-b-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AccordionTrigger({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {
|
||||
return (
|
||||
<AccordionPrimitive.Header className="flex">
|
||||
<AccordionPrimitive.Trigger
|
||||
data-slot="accordion-trigger"
|
||||
className={cn(
|
||||
"flex flex-1 items-start justify-between gap-4 rounded-md border-0 shadow-none py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronDownIcon className="pointer-events-none size-4 shrink-0 translate-y-0.5 text-muted-foreground transition-transform duration-200" />
|
||||
</AccordionPrimitive.Trigger>
|
||||
</AccordionPrimitive.Header>
|
||||
)
|
||||
}
|
||||
|
||||
function AccordionContent({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AccordionPrimitive.Content>) {
|
||||
return (
|
||||
<AccordionPrimitive.Content
|
||||
data-slot="accordion-content"
|
||||
className="overflow-hidden text-sm data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
|
||||
{...props}
|
||||
>
|
||||
<div className={cn("pt-0 pb-4", className)}>{children}</div>
|
||||
</AccordionPrimitive.Content>
|
||||
)
|
||||
}
|
||||
|
||||
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
|
||||
@@ -34,7 +34,7 @@ function RadioGroupItem({
|
||||
data-slot="radio-group-indicator"
|
||||
className="relative flex items-center justify-center"
|
||||
>
|
||||
<CircleIcon className="fill-primary stroke-0 absolute top-1/2 -left-2 size-5 -translate-x-1/2 -translate-y-1/2" />
|
||||
<CircleIcon className="fill-secondary stroke-0 absolute top-1/2 -left-2 size-5 -translate-x-1/2 -translate-y-1/2" />
|
||||
</RadioGroupPrimitive.Indicator>
|
||||
</RadioGroupPrimitive.Item>
|
||||
)
|
||||
|
||||
@@ -66,9 +66,9 @@ export default function NavGuestLayout() {
|
||||
<Link href="#services" className="px-5 py-1.5 text-lg text-[#1b1b18] dark:text-[#EDEDEC] no-underline hover:underline">
|
||||
Nos Services
|
||||
</Link>
|
||||
<Link href="#" className="px-5 py-1.5 text-lg text-[#1b1b18] dark:text-[#EDEDEC] no-underline hover:underline">
|
||||
<a href="https://blog.retzien.fr/index.php" target="_blank" rel="noopener noreferrer" className="px-5 py-1.5 text-lg text-[#1b1b18] dark:text-[#EDEDEC] no-underline hover:underline">
|
||||
Le Blog
|
||||
</Link>
|
||||
</a>
|
||||
<Link href={contact()} className="px-5 py-1.5 text-lg text-[#1b1b18] dark:text-[#EDEDEC] no-underline hover:underline">
|
||||
Contact
|
||||
</Link>
|
||||
@@ -165,9 +165,9 @@ export default function NavGuestLayout() {
|
||||
<Link href="#services" onClick={closeMenu} className="text-lg py-3 border-b border-black/10 dark:border-white/10 no-underline">
|
||||
<span className="text-black hover:underline">Nos Services</span>
|
||||
</Link>
|
||||
<Link href="#" onClick={closeMenu} className="text-lg py-3 border-b border-black/10 dark:border-white/10 no-underline">
|
||||
<a href="https://blog.retzien.fr/index.php" target="_blank" onClick={closeMenu} className="text-lg py-3 border-b border-black/10 dark:border-white/10 no-underline">
|
||||
<span className="text-black hover:underline">Le Blog</span>
|
||||
</Link>
|
||||
</a>
|
||||
<Link href={contact()} onClick={closeMenu} className="text-lg py-3 no-underline">
|
||||
<span className="text-black hover:underline">Contact</span>
|
||||
</Link>
|
||||
|
||||
112
resources/js/pages/faq.tsx
Normal file
112
resources/js/pages/faq.tsx
Normal file
@@ -0,0 +1,112 @@
|
||||
import { Head } from '@inertiajs/react';
|
||||
import NavGuestLayout from '@/layouts/nav-guest-layout';
|
||||
import { Footer } from '@/components/footer';
|
||||
import { Container } from '@/components/common/Container';
|
||||
import { SectionHeading } from '@/components/common/SectionHeading';
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from '@/components/ui/accordion';
|
||||
|
||||
const faqItems = [
|
||||
{
|
||||
question: "Qu'est-ce que Le Retzien Libre ?",
|
||||
answer: "Le Retzien Libre est une association locale engagée pour un internet éthique, libre et respectueux de la vie privée. Nous proposons des services numériques basés sur des logiciels libres, hébergés localement, en alternative aux géants du numérique.",
|
||||
},
|
||||
{
|
||||
question: "Comment adhérer à l'association ?",
|
||||
answer: "Vous pouvez adhérer directement en ligne via notre formulaire d'adhésion. Choisissez votre formule (année en cours, 1 an ou 2 ans), remplissez vos informations et validez. Notre équipe traitera votre demande dans les meilleurs délais.",
|
||||
},
|
||||
{
|
||||
question: "Quels services sont inclus dans l'adhésion ?",
|
||||
answer: "L'adhésion inclut l'accès à l'ensemble de nos services : boîte mail @retzien.fr, stockage cloud via NextCloud, hébergement web, listes de diffusion et bien plus. Consultez notre documentation pour la liste complète des services disponibles.",
|
||||
},
|
||||
{
|
||||
question: "Combien coûte l'adhésion ?",
|
||||
answer: "L'adhésion est à 1€ par mois. Le montant est calculé en fonction de la durée choisie : reste de l'année en cours, année en cours + 1 an, ou année en cours + 2 ans. Les adhésions sont basées sur l'année civile (janvier à décembre).",
|
||||
},
|
||||
{
|
||||
question: "Comment fonctionne ma boîte mail @retzien.fr ?",
|
||||
answer: "Lors de votre adhésion, vous pouvez indiquer le nom de boîte mail souhaité (ex : prenom.nom@retzien.fr). Une fois votre adhésion validée, votre boîte sera créée et vous recevrez vos identifiants de connexion. Vous pouvez la consulter via webmail ou la configurer dans votre client mail préféré.",
|
||||
},
|
||||
{
|
||||
question: "Que se passe-t-il à l'expiration de mon adhésion ?",
|
||||
answer: "Vous recevrez un rappel par e-mail avant l'expiration de votre adhésion. Si vous ne renouvelez pas, vos services seront suspendus. Vos données sont conservées pendant une période raisonnable pour vous permettre de renouveler sans perte.",
|
||||
},
|
||||
{
|
||||
question: "Comment contacter l'association ?",
|
||||
answer: "Vous pouvez nous joindre via le formulaire de contact disponible sur le site, ou directement par e-mail. Pour les questions techniques, consultez d'abord notre documentation en ligne.",
|
||||
},
|
||||
{
|
||||
question: "Où trouver la documentation technique ?",
|
||||
answer: "Notre documentation complète est disponible à l'adresse doc.retzien.fr. Vous y trouverez des guides de configuration pour tous nos services, des tutoriels et une base de connaissances.",
|
||||
},
|
||||
];
|
||||
|
||||
export default function Faq() {
|
||||
return (
|
||||
<>
|
||||
<Head title="Foire aux questions" />
|
||||
<div className="flex flex-col min-h-screen bg-white dark:bg-[#0a0a0a] text-[#1b1b18] dark:text-[#EDEDEC]">
|
||||
<div className="flex flex-col items-center px-4">
|
||||
<NavGuestLayout />
|
||||
</div>
|
||||
|
||||
<main className="flex-1 py-12">
|
||||
<Container className="flex flex-col gap-10 max-w-4xl mx-auto">
|
||||
<SectionHeading
|
||||
title="Foire aux questions"
|
||||
color="primary"
|
||||
subtitle="Retrouvez les réponses aux questions les plus fréquentes sur notre association et nos services."
|
||||
align="left"
|
||||
/>
|
||||
|
||||
<Accordion type="single" collapsible className="w-full flex flex-col gap-3">
|
||||
{faqItems.map((item, index) => (
|
||||
<AccordionItem
|
||||
key={index}
|
||||
value={`item-${index}`}
|
||||
className="nb-shadow-static rounded-2xl border-3 border-black px-5 overflow-hidden"
|
||||
>
|
||||
<AccordionTrigger className="text-left font-semibold text-base py-4 hover:no-underline">
|
||||
{item.question}
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="text-muted-foreground pb-4">
|
||||
{item.answer}
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
))}
|
||||
</Accordion>
|
||||
|
||||
<div className="bg-primary dark:bg-[#171717] nb-shadow-static rounded-2xl p-6 text-center">
|
||||
<p className="text-accent font-semibold mb-2">Vous n'avez pas trouvé votre réponse ?</p>
|
||||
<p className="text-sm text-muted-foreground mb-4">
|
||||
Consultez notre documentation complète ou contactez-nous directement.
|
||||
</p>
|
||||
<div className="flex flex-wrap justify-center gap-3">
|
||||
<a
|
||||
href="https://doc.retzien.fr"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="nb-shadow inline-flex items-center justify-center rounded-md text-sm font-bold bg-secondary text-secondary-foreground hover:bg-secondary/80 h-10 px-4 py-2 no-underline"
|
||||
>
|
||||
Documentation
|
||||
</a>
|
||||
<a
|
||||
href="/contact"
|
||||
className="nb-shadow inline-flex items-center justify-center rounded-md text-sm font-bold bg-accent text-white hover:bg-accent/80 h-10 px-4 py-2 no-underline"
|
||||
>
|
||||
Nous contacter
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
</main>
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -54,6 +54,14 @@ export default function Contact() {
|
||||
className="mx-auto"
|
||||
/>
|
||||
|
||||
<p className="text-sm text-muted-foreground max-w-4xl mx-auto">
|
||||
Pensez à consulter notre{' '}
|
||||
<a href="/faq" className="text-primary underline hover:no-underline">FAQ</a>
|
||||
{' '}ou notre{' '}
|
||||
<a href="https://doc.retzien.fr" target="_blank" rel="noopener noreferrer" className="text-primary underline hover:no-underline">documentation</a>
|
||||
{' '}avant de nous écrire.
|
||||
</p>
|
||||
|
||||
{showFlashMessage && <FlashMessage messages={flash ?? {}} />}
|
||||
|
||||
<Form
|
||||
|
||||
@@ -8,6 +8,7 @@ import InputError from '@/components/input-error';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import NavGuestLayout from '@/layouts/nav-guest-layout';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { PageProps } from '@/types';
|
||||
import { FlashMessage } from '@/components/flash-message';
|
||||
@@ -17,12 +18,14 @@ import { Footer } from '@/components/footer';
|
||||
import IllustrationLogo from "@/img/utils/lrl-logo-full.svg";
|
||||
|
||||
export default function Membership() {
|
||||
const { flash, plans, services, captcha_question } = usePage().props as PageProps;
|
||||
const { flash, plans, services, member_types, current_year, captcha_question } = usePage().props as PageProps;
|
||||
const [showFlashMessage, setFlashMessage] = useState(!!flash);
|
||||
const [selectedPlan, setSelectedPlan] = useState(plans?.[0]?.identifier ?? null);
|
||||
const [amount, setAmount] = useState(plans?.[0]?.price ?? 0);
|
||||
const [memberType, setMemberType] = useState(member_types?.[0]?.identifier ?? 'individual');
|
||||
const [phone1Value, setPhone1Value] = useState('');
|
||||
const [zipcodeValue, setZipcodeValue] = useState('');
|
||||
const [retzienEmailValue, setRetzienEmailValue] = useState('');
|
||||
|
||||
const phone1LocalError = phone1Value.length > 0 && !/^\d{10}$/.test(phone1Value)
|
||||
? 'Le numéro doit contenir exactement 10 chiffres.'
|
||||
@@ -32,6 +35,10 @@ export default function Membership() {
|
||||
? 'Le code postal doit contenir exactement 5 chiffres.'
|
||||
: null;
|
||||
|
||||
const retzienEmailLocalError = retzienEmailValue.length > 0 && !/^[a-z0-9._-]+$/.test(retzienEmailValue)
|
||||
? 'Caractères autorisés : lettres minuscules, chiffres, points, tirets.'
|
||||
: null;
|
||||
|
||||
useEffect(() => {
|
||||
if (plans && selectedPlan) {
|
||||
const plan = plans.find((p) => p.identifier === selectedPlan);
|
||||
@@ -47,6 +54,20 @@ export default function Membership() {
|
||||
}
|
||||
}, [flash]);
|
||||
|
||||
const getPlanLabel = (plan: NonNullable<typeof plans>[number]) => {
|
||||
if (!current_year || !plan.months) return plan.name;
|
||||
if (plan.identifier === 'custom') {
|
||||
return `Année ${current_year} (${plan.months} mois restants)`;
|
||||
}
|
||||
if (plan.identifier === 'one-year') {
|
||||
return `Années ${current_year}–${current_year + 1}`;
|
||||
}
|
||||
if (plan.identifier === 'two-years') {
|
||||
return `Années ${current_year}–${current_year + 2}`;
|
||||
}
|
||||
return plan.name;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head title="Adhérer au Retzien Libre" />
|
||||
@@ -85,6 +106,28 @@ export default function Membership() {
|
||||
className="rounded-lg max-w-md w-full pt-4"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Member type radio */}
|
||||
<div className="grid gap-2">
|
||||
<Label className="text-accent font-semibold">Vous êtes*</Label>
|
||||
<RadioGroup
|
||||
value={memberType}
|
||||
onValueChange={setMemberType}
|
||||
className="flex flex-wrap gap-4"
|
||||
>
|
||||
{member_types?.map((type) => (
|
||||
<div key={type.identifier} className="flex items-center gap-2">
|
||||
<RadioGroupItem value={type.identifier} id={`member-type-${type.identifier}`} />
|
||||
<Label htmlFor={`member-type-${type.identifier}`} className="cursor-pointer">
|
||||
{type.name}
|
||||
</Label>
|
||||
</div>
|
||||
))}
|
||||
</RadioGroup>
|
||||
<input type="hidden" name="member_type" value={memberType} />
|
||||
<InputError message={errors.member_type} />
|
||||
</div>
|
||||
|
||||
<h2 className="text-lg font-semibold text-accent">Vos informations</h2>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
@@ -130,9 +173,32 @@ export default function Membership() {
|
||||
<InputError message={errors.company} />
|
||||
</div>
|
||||
|
||||
<div className="grid gap-1">
|
||||
<Label htmlFor="desired_retzien_email">
|
||||
Adresse mail Retzien souhaitée <span className="text-muted-foreground text-xs">(facultatif)</span>
|
||||
</Label>
|
||||
<div className="group/email flex items-center gap-0 rounded-md has-[:focus-visible]:shadow-[4px_4px_0px_rgba(0,0,0,1)] transition duration-100 ease-in-out">
|
||||
<Input
|
||||
id="desired_retzien_email"
|
||||
name="desired_retzien_email"
|
||||
type="text"
|
||||
tabIndex={6}
|
||||
autoComplete="off"
|
||||
placeholder="prenom.nom"
|
||||
className="rounded-r-none focus-visible:shadow-none"
|
||||
value={retzienEmailValue}
|
||||
onChange={(e) => setRetzienEmailValue(e.target.value.toLowerCase().replace(/[^a-z0-9._-]/g, ''))}
|
||||
/>
|
||||
<span className="inline-flex items-center px-3 h-9 border-3 border-l-0 border-black rounded-r-md bg-muted text-sm text-muted-foreground whitespace-nowrap">
|
||||
@retzien.fr
|
||||
</span>
|
||||
</div>
|
||||
<InputError message={retzienEmailLocalError ?? errors.desired_retzien_email} />
|
||||
</div>
|
||||
|
||||
<div className="grid gap-1">
|
||||
<Label htmlFor="address">Adresse*</Label>
|
||||
<Input id="address" name="address" type="text" required tabIndex={6} autoComplete="street-address" placeholder="Votre adresse" />
|
||||
<Input id="address" name="address" type="text" required tabIndex={7} autoComplete="street-address" placeholder="Votre adresse" />
|
||||
<InputError message={errors.address} />
|
||||
</div>
|
||||
|
||||
@@ -144,7 +210,7 @@ export default function Membership() {
|
||||
name="zipcode"
|
||||
type="text"
|
||||
required
|
||||
tabIndex={7}
|
||||
tabIndex={8}
|
||||
autoComplete="postal-code"
|
||||
inputMode="numeric"
|
||||
maxLength={5}
|
||||
@@ -156,7 +222,7 @@ export default function Membership() {
|
||||
</div>
|
||||
<div className="grid gap-1">
|
||||
<Label htmlFor="city">Ville*</Label>
|
||||
<Input id="city" name="city" type="text" required tabIndex={8} autoComplete="address-level2" placeholder="Ville" />
|
||||
<Input id="city" name="city" type="text" required tabIndex={9} autoComplete="address-level2" placeholder="Ville" />
|
||||
<InputError message={errors.city} />
|
||||
</div>
|
||||
</div>
|
||||
@@ -174,7 +240,7 @@ export default function Membership() {
|
||||
<button
|
||||
key={plan.id}
|
||||
type="button"
|
||||
tabIndex={9}
|
||||
tabIndex={10}
|
||||
onClick={() => setSelectedPlan(plan.identifier)}
|
||||
className={cn(
|
||||
'flex items-center justify-between rounded-xl border-3 border-black px-5 py-4 text-left transition-all duration-150',
|
||||
@@ -185,14 +251,12 @@ export default function Membership() {
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-col">
|
||||
<span className="font-bold text-base">{plan.name}</span>
|
||||
{plan.months != null ? (
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{plan.months} mois × 1€/mois
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-xs text-muted-foreground">{plan.description}</span>
|
||||
)}
|
||||
<span className="font-bold text-base">{getPlanLabel(plan)}</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{plan.months != null
|
||||
? `${plan.months} mois × 1€/mois`
|
||||
: plan.description}
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-2xl font-black">{plan.price}€</span>
|
||||
</button>
|
||||
@@ -204,7 +268,7 @@ export default function Membership() {
|
||||
<InputError message={errors.package} />
|
||||
|
||||
<p className="text-center text-sm text-muted-foreground border-t border-border pt-3">
|
||||
Montant total : <strong className="text-secondary text-lg">{amount}€</strong>
|
||||
Montant total : <strong className="text-accent text-2xl">{amount}€</strong>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -222,6 +286,9 @@ export default function Membership() {
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="text-center">
|
||||
Et plus encore ...
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Captcha + CGU + Submit */}
|
||||
@@ -232,7 +299,7 @@ export default function Membership() {
|
||||
id="captcha"
|
||||
name="captcha"
|
||||
type="text"
|
||||
tabIndex={10}
|
||||
tabIndex={11}
|
||||
placeholder="Votre réponse"
|
||||
autoComplete="off"
|
||||
className="max-w-[180px]"
|
||||
@@ -242,7 +309,7 @@ export default function Membership() {
|
||||
|
||||
<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" />
|
||||
<Checkbox id="cgu" name="cgu" tabIndex={12} 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.
|
||||
@@ -254,7 +321,7 @@ export default function Membership() {
|
||||
<Button
|
||||
type="submit"
|
||||
variant="secondary"
|
||||
tabIndex={12}
|
||||
tabIndex={13}
|
||||
className="nb-shadow w-full font-bold text-base py-5"
|
||||
>
|
||||
{processing && <LoaderCircle className="h-4 w-4 animate-spin" />}
|
||||
|
||||
@@ -365,6 +365,80 @@ dashboardForm.head = (options?: RouteQueryOptions): RouteFormDefinition<'get'> =
|
||||
|
||||
dashboard.form = dashboardForm
|
||||
|
||||
/**
|
||||
* @see routes/web.php:20
|
||||
* @route '/faq'
|
||||
*/
|
||||
export const faq = (options?: RouteQueryOptions): RouteDefinition<'get'> => ({
|
||||
url: faq.url(options),
|
||||
method: 'get',
|
||||
})
|
||||
|
||||
faq.definition = {
|
||||
methods: ["get","head"],
|
||||
url: '/faq',
|
||||
} satisfies RouteDefinition<["get","head"]>
|
||||
|
||||
/**
|
||||
* @see routes/web.php:20
|
||||
* @route '/faq'
|
||||
*/
|
||||
faq.url = (options?: RouteQueryOptions) => {
|
||||
return faq.definition.url + queryParams(options)
|
||||
}
|
||||
|
||||
/**
|
||||
* @see routes/web.php:20
|
||||
* @route '/faq'
|
||||
*/
|
||||
faq.get = (options?: RouteQueryOptions): RouteDefinition<'get'> => ({
|
||||
url: faq.url(options),
|
||||
method: 'get',
|
||||
})
|
||||
|
||||
/**
|
||||
* @see routes/web.php:20
|
||||
* @route '/faq'
|
||||
*/
|
||||
faq.head = (options?: RouteQueryOptions): RouteDefinition<'head'> => ({
|
||||
url: faq.url(options),
|
||||
method: 'head',
|
||||
})
|
||||
|
||||
/**
|
||||
* @see routes/web.php:20
|
||||
* @route '/faq'
|
||||
*/
|
||||
const faqForm = (options?: RouteQueryOptions): RouteFormDefinition<'get'> => ({
|
||||
action: faq.url(options),
|
||||
method: 'get',
|
||||
})
|
||||
|
||||
/**
|
||||
* @see routes/web.php:20
|
||||
* @route '/faq'
|
||||
*/
|
||||
faqForm.get = (options?: RouteQueryOptions): RouteFormDefinition<'get'> => ({
|
||||
action: faq.url(options),
|
||||
method: 'get',
|
||||
})
|
||||
|
||||
/**
|
||||
* @see routes/web.php:20
|
||||
* @route '/faq'
|
||||
*/
|
||||
faqForm.head = (options?: RouteQueryOptions): RouteFormDefinition<'get'> => ({
|
||||
action: faq.url({
|
||||
[options?.mergeQuery ? 'mergeQuery' : 'query']: {
|
||||
_method: 'HEAD',
|
||||
...(options?.query ?? options?.mergeQuery ?? {}),
|
||||
}
|
||||
}),
|
||||
method: 'get',
|
||||
})
|
||||
|
||||
faq.form = faqForm
|
||||
|
||||
/**
|
||||
* @see \App\Http\Controllers\Auth\RegisteredUserController::register
|
||||
* @see app/Http/Controllers/Auth/RegisteredUserController.php:21
|
||||
@@ -529,7 +603,7 @@ contact.form = contactForm
|
||||
|
||||
/**
|
||||
* @see \App\Http\Controllers\Forms\MembershipFormController::membership
|
||||
* @see app/Http/Controllers/Forms/MembershipFormController.php:18
|
||||
* @see app/Http/Controllers/Forms/MembershipFormController.php:19
|
||||
* @route '/membership'
|
||||
*/
|
||||
export const membership = (options?: RouteQueryOptions): RouteDefinition<'get'> => ({
|
||||
@@ -544,7 +618,7 @@ membership.definition = {
|
||||
|
||||
/**
|
||||
* @see \App\Http\Controllers\Forms\MembershipFormController::membership
|
||||
* @see app/Http/Controllers/Forms/MembershipFormController.php:18
|
||||
* @see app/Http/Controllers/Forms/MembershipFormController.php:19
|
||||
* @route '/membership'
|
||||
*/
|
||||
membership.url = (options?: RouteQueryOptions) => {
|
||||
@@ -553,7 +627,7 @@ membership.url = (options?: RouteQueryOptions) => {
|
||||
|
||||
/**
|
||||
* @see \App\Http\Controllers\Forms\MembershipFormController::membership
|
||||
* @see app/Http/Controllers/Forms/MembershipFormController.php:18
|
||||
* @see app/Http/Controllers/Forms/MembershipFormController.php:19
|
||||
* @route '/membership'
|
||||
*/
|
||||
membership.get = (options?: RouteQueryOptions): RouteDefinition<'get'> => ({
|
||||
@@ -563,7 +637,7 @@ membership.get = (options?: RouteQueryOptions): RouteDefinition<'get'> => ({
|
||||
|
||||
/**
|
||||
* @see \App\Http\Controllers\Forms\MembershipFormController::membership
|
||||
* @see app/Http/Controllers/Forms/MembershipFormController.php:18
|
||||
* @see app/Http/Controllers/Forms/MembershipFormController.php:19
|
||||
* @route '/membership'
|
||||
*/
|
||||
membership.head = (options?: RouteQueryOptions): RouteDefinition<'head'> => ({
|
||||
@@ -573,7 +647,7 @@ membership.head = (options?: RouteQueryOptions): RouteDefinition<'head'> => ({
|
||||
|
||||
/**
|
||||
* @see \App\Http\Controllers\Forms\MembershipFormController::membership
|
||||
* @see app/Http/Controllers/Forms/MembershipFormController.php:18
|
||||
* @see app/Http/Controllers/Forms/MembershipFormController.php:19
|
||||
* @route '/membership'
|
||||
*/
|
||||
const membershipForm = (options?: RouteQueryOptions): RouteFormDefinition<'get'> => ({
|
||||
@@ -583,7 +657,7 @@ const membershipForm = (options?: RouteQueryOptions): RouteFormDefinition<'get'>
|
||||
|
||||
/**
|
||||
* @see \App\Http\Controllers\Forms\MembershipFormController::membership
|
||||
* @see app/Http/Controllers/Forms/MembershipFormController.php:18
|
||||
* @see app/Http/Controllers/Forms/MembershipFormController.php:19
|
||||
* @route '/membership'
|
||||
*/
|
||||
membershipForm.get = (options?: RouteQueryOptions): RouteFormDefinition<'get'> => ({
|
||||
@@ -593,7 +667,7 @@ membershipForm.get = (options?: RouteQueryOptions): RouteFormDefinition<'get'> =
|
||||
|
||||
/**
|
||||
* @see \App\Http\Controllers\Forms\MembershipFormController::membership
|
||||
* @see app/Http/Controllers/Forms/MembershipFormController.php:18
|
||||
* @see app/Http/Controllers/Forms/MembershipFormController.php:19
|
||||
* @route '/membership'
|
||||
*/
|
||||
membershipForm.head = (options?: RouteQueryOptions): RouteFormDefinition<'get'> => ({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { queryParams, type RouteQueryOptions, type RouteDefinition, type RouteFormDefinition } from './../../wayfinder'
|
||||
/**
|
||||
* @see routes/web.php:20
|
||||
* @see routes/web.php:21
|
||||
* @route '/mentions-legales'
|
||||
*/
|
||||
export const mentions = (options?: RouteQueryOptions): RouteDefinition<'get'> => ({
|
||||
@@ -14,7 +14,7 @@ mentions.definition = {
|
||||
} satisfies RouteDefinition<["get","head"]>
|
||||
|
||||
/**
|
||||
* @see routes/web.php:20
|
||||
* @see routes/web.php:21
|
||||
* @route '/mentions-legales'
|
||||
*/
|
||||
mentions.url = (options?: RouteQueryOptions) => {
|
||||
@@ -22,7 +22,7 @@ mentions.url = (options?: RouteQueryOptions) => {
|
||||
}
|
||||
|
||||
/**
|
||||
* @see routes/web.php:20
|
||||
* @see routes/web.php:21
|
||||
* @route '/mentions-legales'
|
||||
*/
|
||||
mentions.get = (options?: RouteQueryOptions): RouteDefinition<'get'> => ({
|
||||
@@ -31,7 +31,7 @@ mentions.get = (options?: RouteQueryOptions): RouteDefinition<'get'> => ({
|
||||
})
|
||||
|
||||
/**
|
||||
* @see routes/web.php:20
|
||||
* @see routes/web.php:21
|
||||
* @route '/mentions-legales'
|
||||
*/
|
||||
mentions.head = (options?: RouteQueryOptions): RouteDefinition<'head'> => ({
|
||||
@@ -40,7 +40,7 @@ mentions.head = (options?: RouteQueryOptions): RouteDefinition<'head'> => ({
|
||||
})
|
||||
|
||||
/**
|
||||
* @see routes/web.php:20
|
||||
* @see routes/web.php:21
|
||||
* @route '/mentions-legales'
|
||||
*/
|
||||
const mentionsForm = (options?: RouteQueryOptions): RouteFormDefinition<'get'> => ({
|
||||
@@ -49,7 +49,7 @@ const mentionsForm = (options?: RouteQueryOptions): RouteFormDefinition<'get'> =
|
||||
})
|
||||
|
||||
/**
|
||||
* @see routes/web.php:20
|
||||
* @see routes/web.php:21
|
||||
* @route '/mentions-legales'
|
||||
*/
|
||||
mentionsForm.get = (options?: RouteQueryOptions): RouteFormDefinition<'get'> => ({
|
||||
@@ -58,7 +58,7 @@ mentionsForm.get = (options?: RouteQueryOptions): RouteFormDefinition<'get'> =>
|
||||
})
|
||||
|
||||
/**
|
||||
* @see routes/web.php:20
|
||||
* @see routes/web.php:21
|
||||
* @route '/mentions-legales'
|
||||
*/
|
||||
mentionsForm.head = (options?: RouteQueryOptions): RouteFormDefinition<'get'> => ({
|
||||
@@ -74,7 +74,7 @@ mentionsForm.head = (options?: RouteQueryOptions): RouteFormDefinition<'get'> =>
|
||||
mentions.form = mentionsForm
|
||||
|
||||
/**
|
||||
* @see routes/web.php:21
|
||||
* @see routes/web.php:22
|
||||
* @route '/conditions-generales'
|
||||
*/
|
||||
export const cgu = (options?: RouteQueryOptions): RouteDefinition<'get'> => ({
|
||||
@@ -88,7 +88,7 @@ cgu.definition = {
|
||||
} satisfies RouteDefinition<["get","head"]>
|
||||
|
||||
/**
|
||||
* @see routes/web.php:21
|
||||
* @see routes/web.php:22
|
||||
* @route '/conditions-generales'
|
||||
*/
|
||||
cgu.url = (options?: RouteQueryOptions) => {
|
||||
@@ -96,7 +96,7 @@ cgu.url = (options?: RouteQueryOptions) => {
|
||||
}
|
||||
|
||||
/**
|
||||
* @see routes/web.php:21
|
||||
* @see routes/web.php:22
|
||||
* @route '/conditions-generales'
|
||||
*/
|
||||
cgu.get = (options?: RouteQueryOptions): RouteDefinition<'get'> => ({
|
||||
@@ -105,7 +105,7 @@ cgu.get = (options?: RouteQueryOptions): RouteDefinition<'get'> => ({
|
||||
})
|
||||
|
||||
/**
|
||||
* @see routes/web.php:21
|
||||
* @see routes/web.php:22
|
||||
* @route '/conditions-generales'
|
||||
*/
|
||||
cgu.head = (options?: RouteQueryOptions): RouteDefinition<'head'> => ({
|
||||
@@ -114,7 +114,7 @@ cgu.head = (options?: RouteQueryOptions): RouteDefinition<'head'> => ({
|
||||
})
|
||||
|
||||
/**
|
||||
* @see routes/web.php:21
|
||||
* @see routes/web.php:22
|
||||
* @route '/conditions-generales'
|
||||
*/
|
||||
const cguForm = (options?: RouteQueryOptions): RouteFormDefinition<'get'> => ({
|
||||
@@ -123,7 +123,7 @@ const cguForm = (options?: RouteQueryOptions): RouteFormDefinition<'get'> => ({
|
||||
})
|
||||
|
||||
/**
|
||||
* @see routes/web.php:21
|
||||
* @see routes/web.php:22
|
||||
* @route '/conditions-generales'
|
||||
*/
|
||||
cguForm.get = (options?: RouteQueryOptions): RouteFormDefinition<'get'> => ({
|
||||
@@ -132,7 +132,7 @@ cguForm.get = (options?: RouteQueryOptions): RouteFormDefinition<'get'> => ({
|
||||
})
|
||||
|
||||
/**
|
||||
* @see routes/web.php:21
|
||||
* @see routes/web.php:22
|
||||
* @route '/conditions-generales'
|
||||
*/
|
||||
cguForm.head = (options?: RouteQueryOptions): RouteFormDefinition<'get'> => ({
|
||||
@@ -148,7 +148,7 @@ cguForm.head = (options?: RouteQueryOptions): RouteFormDefinition<'get'> => ({
|
||||
cgu.form = cguForm
|
||||
|
||||
/**
|
||||
* @see routes/web.php:22
|
||||
* @see routes/web.php:23
|
||||
* @route '/confidentialite'
|
||||
*/
|
||||
export const confidentialite = (options?: RouteQueryOptions): RouteDefinition<'get'> => ({
|
||||
@@ -162,7 +162,7 @@ confidentialite.definition = {
|
||||
} satisfies RouteDefinition<["get","head"]>
|
||||
|
||||
/**
|
||||
* @see routes/web.php:22
|
||||
* @see routes/web.php:23
|
||||
* @route '/confidentialite'
|
||||
*/
|
||||
confidentialite.url = (options?: RouteQueryOptions) => {
|
||||
@@ -170,7 +170,7 @@ confidentialite.url = (options?: RouteQueryOptions) => {
|
||||
}
|
||||
|
||||
/**
|
||||
* @see routes/web.php:22
|
||||
* @see routes/web.php:23
|
||||
* @route '/confidentialite'
|
||||
*/
|
||||
confidentialite.get = (options?: RouteQueryOptions): RouteDefinition<'get'> => ({
|
||||
@@ -179,7 +179,7 @@ confidentialite.get = (options?: RouteQueryOptions): RouteDefinition<'get'> => (
|
||||
})
|
||||
|
||||
/**
|
||||
* @see routes/web.php:22
|
||||
* @see routes/web.php:23
|
||||
* @route '/confidentialite'
|
||||
*/
|
||||
confidentialite.head = (options?: RouteQueryOptions): RouteDefinition<'head'> => ({
|
||||
@@ -188,7 +188,7 @@ confidentialite.head = (options?: RouteQueryOptions): RouteDefinition<'head'> =>
|
||||
})
|
||||
|
||||
/**
|
||||
* @see routes/web.php:22
|
||||
* @see routes/web.php:23
|
||||
* @route '/confidentialite'
|
||||
*/
|
||||
const confidentialiteForm = (options?: RouteQueryOptions): RouteFormDefinition<'get'> => ({
|
||||
@@ -197,7 +197,7 @@ const confidentialiteForm = (options?: RouteQueryOptions): RouteFormDefinition<'
|
||||
})
|
||||
|
||||
/**
|
||||
* @see routes/web.php:22
|
||||
* @see routes/web.php:23
|
||||
* @route '/confidentialite'
|
||||
*/
|
||||
confidentialiteForm.get = (options?: RouteQueryOptions): RouteFormDefinition<'get'> => ({
|
||||
@@ -206,7 +206,7 @@ confidentialiteForm.get = (options?: RouteQueryOptions): RouteFormDefinition<'ge
|
||||
})
|
||||
|
||||
/**
|
||||
* @see routes/web.php:22
|
||||
* @see routes/web.php:23
|
||||
* @route '/confidentialite'
|
||||
*/
|
||||
confidentialiteForm.head = (options?: RouteQueryOptions): RouteFormDefinition<'get'> => ({
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { queryParams, type RouteQueryOptions, type RouteDefinition, type RouteFormDefinition } from './../../wayfinder'
|
||||
/**
|
||||
* @see \App\Http\Controllers\Forms\MembershipFormController::store
|
||||
* @see app/Http/Controllers/Forms/MembershipFormController.php:42
|
||||
* @see app/Http/Controllers/Forms/MembershipFormController.php:59
|
||||
* @route '/membership'
|
||||
*/
|
||||
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.php:42
|
||||
* @see app/Http/Controllers/Forms/MembershipFormController.php:59
|
||||
* @route '/membership'
|
||||
*/
|
||||
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.php:42
|
||||
* @see app/Http/Controllers/Forms/MembershipFormController.php:59
|
||||
* @route '/membership'
|
||||
*/
|
||||
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.php:42
|
||||
* @see app/Http/Controllers/Forms/MembershipFormController.php:59
|
||||
* @route '/membership'
|
||||
*/
|
||||
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.php:42
|
||||
* @see app/Http/Controllers/Forms/MembershipFormController.php:59
|
||||
* @route '/membership'
|
||||
*/
|
||||
storeForm.post = (options?: RouteQueryOptions): RouteFormDefinition<'post'> => ({
|
||||
|
||||
8
resources/js/types/index.d.ts
vendored
8
resources/js/types/index.d.ts
vendored
@@ -58,6 +58,12 @@ export interface Plans {
|
||||
months?: number | null;
|
||||
}
|
||||
|
||||
export interface MemberTypeOption {
|
||||
id: number;
|
||||
identifier: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface MembershipService {
|
||||
name: string;
|
||||
description: string;
|
||||
@@ -114,6 +120,8 @@ export interface PageProps {
|
||||
auth?: Auth;
|
||||
plans?: Plans[];
|
||||
services?: MembershipService[];
|
||||
member_types?: MemberTypeOption[];
|
||||
current_year?: number;
|
||||
captcha_question?: string;
|
||||
member?: DashboardMember | null;
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ Route::middleware(['auth', 'verified'])->group(function () {
|
||||
Route::post('dashboard/service-activation', [DashboardController::class, 'requestServiceActivation'])->name('dashboard.service-activation');
|
||||
});
|
||||
|
||||
Route::get('/faq', fn () => Inertia::render('faq'))->name('faq');
|
||||
Route::get('/mentions-legales', fn () => Inertia::render('legal/mentions-legales'))->name('legal.mentions');
|
||||
Route::get('/conditions-generales', fn () => Inertia::render('legal/cgu'))->name('legal.cgu');
|
||||
Route::get('/confidentialite', fn () => Inertia::render('legal/confidentialite'))->name('legal.confidentialite');
|
||||
|
||||
Reference in New Issue
Block a user