mirror of
https://github.com/R0m1k3/PleinR.git
synced 2026-10-22 06:38:05 +02:00
Logo annuaire + modals devenir membre & contact
- Logo en haut a gauche de la page Annuaire (prop logo sur SiteHeader) - Modal "Devenir adherent" sur les CTA accueil + annuaire : cree une demande d'adhesion (membership_requests) et alerte les admins via le journal d'activite (compteur tableau de bord deja cable) - Formulaire de contact (modal depuis le pied de page) : nouvelle table contact_messages + alerte admin - Composant Modal partage, server actions publiques (sans auth) - Migration drizzle 0002 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
e2cf03c9f3
commit
141acffc1c
12 files changed
+1057
-17
No files matched your search
@@ -0,0 +1,10 @@
|
|||||||
|
CREATE TYPE "public"."contact_status" AS ENUM('new', 'read', 'archived');--> statement-breakpoint
|
||||||
|
CREATE TABLE "contact_messages" (
|
||||||
|
"id" serial PRIMARY KEY NOT NULL,
|
||||||
|
"name" varchar(200) NOT NULL,
|
||||||
|
"email" varchar(200) NOT NULL,
|
||||||
|
"subject" varchar(200),
|
||||||
|
"message" text NOT NULL,
|
||||||
|
"status" "contact_status" DEFAULT 'new' NOT NULL,
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
@@ -0,0 +1,592 @@
|
|||||||
|
{
|
||||||
|
"id": "0a329bb6-174e-4b49-b244-fe5be0324a99",
|
||||||
|
"prevId": "338e83f7-c966-41fc-a2c7-14a4147353c5",
|
||||||
|
"version": "7",
|
||||||
|
"dialect": "postgresql",
|
||||||
|
"tables": {
|
||||||
|
"public.activity_log": {
|
||||||
|
"name": "activity_log",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"dot": {
|
||||||
|
"name": "dot",
|
||||||
|
"type": "varchar(16)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'#2C6FB3'"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"name": "message",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.categories": {
|
||||||
|
"name": "categories",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"slug": {
|
||||||
|
"name": "slug",
|
||||||
|
"type": "varchar(80)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"label": {
|
||||||
|
"name": "label",
|
||||||
|
"type": "varchar(120)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"accent": {
|
||||||
|
"name": "accent",
|
||||||
|
"type": "varchar(16)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'#E0A63C'"
|
||||||
|
},
|
||||||
|
"tint": {
|
||||||
|
"name": "tint",
|
||||||
|
"type": "varchar(16)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'#f6efdc'"
|
||||||
|
},
|
||||||
|
"sort": {
|
||||||
|
"name": "sort",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"categories_slug_unique": {
|
||||||
|
"name": "categories_slug_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"slug"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.contact_messages": {
|
||||||
|
"name": "contact_messages",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "varchar(200)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"name": "email",
|
||||||
|
"type": "varchar(200)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"subject": {
|
||||||
|
"name": "subject",
|
||||||
|
"type": "varchar(200)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"name": "message",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"name": "status",
|
||||||
|
"type": "contact_status",
|
||||||
|
"typeSchema": "public",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'new'"
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.members": {
|
||||||
|
"name": "members",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "varchar(200)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"name": "email",
|
||||||
|
"type": "varchar(200)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"category_id": {
|
||||||
|
"name": "category_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"city": {
|
||||||
|
"name": "city",
|
||||||
|
"type": "varchar(120)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"address": {
|
||||||
|
"name": "address",
|
||||||
|
"type": "varchar(240)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"name": "description",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"name": "status",
|
||||||
|
"type": "member_status",
|
||||||
|
"typeSchema": "public",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'pending'"
|
||||||
|
},
|
||||||
|
"highlighted": {
|
||||||
|
"name": "highlighted",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"logo_url": {
|
||||||
|
"name": "logo_url",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"cover_url": {
|
||||||
|
"name": "cover_url",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"phone": {
|
||||||
|
"name": "phone",
|
||||||
|
"type": "varchar(40)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"website": {
|
||||||
|
"name": "website",
|
||||||
|
"type": "varchar(200)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"postal_code": {
|
||||||
|
"name": "postal_code",
|
||||||
|
"type": "varchar(20)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"member_since": {
|
||||||
|
"name": "member_since",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"hours": {
|
||||||
|
"name": "hours",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"tags": {
|
||||||
|
"name": "tags",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"members_category_id_categories_id_fk": {
|
||||||
|
"name": "members_category_id_categories_id_fk",
|
||||||
|
"tableFrom": "members",
|
||||||
|
"tableTo": "categories",
|
||||||
|
"columnsFrom": [
|
||||||
|
"category_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.membership_requests": {
|
||||||
|
"name": "membership_requests",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "varchar(200)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"name": "email",
|
||||||
|
"type": "varchar(200)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"name": "message",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"name": "status",
|
||||||
|
"type": "request_status",
|
||||||
|
"typeSchema": "public",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'new'"
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.promotions": {
|
||||||
|
"name": "promotions",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"name": "title",
|
||||||
|
"type": "varchar(200)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"text": {
|
||||||
|
"name": "text",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"category": {
|
||||||
|
"name": "category",
|
||||||
|
"type": "varchar(120)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"badge": {
|
||||||
|
"name": "badge",
|
||||||
|
"type": "varchar(40)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"image_url": {
|
||||||
|
"name": "image_url",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"member_id": {
|
||||||
|
"name": "member_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"name": "status",
|
||||||
|
"type": "promo_status",
|
||||||
|
"typeSchema": "public",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'pending'"
|
||||||
|
},
|
||||||
|
"valid_until": {
|
||||||
|
"name": "valid_until",
|
||||||
|
"type": "varchar(120)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"promotions_member_id_members_id_fk": {
|
||||||
|
"name": "promotions_member_id_members_id_fk",
|
||||||
|
"tableFrom": "promotions",
|
||||||
|
"tableTo": "members",
|
||||||
|
"columnsFrom": [
|
||||||
|
"member_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.users": {
|
||||||
|
"name": "users",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"name": "email",
|
||||||
|
"type": "varchar(200)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "varchar(200)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"password_hash": {
|
||||||
|
"name": "password_hash",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"role": {
|
||||||
|
"name": "role",
|
||||||
|
"type": "role",
|
||||||
|
"typeSchema": "public",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'member'"
|
||||||
|
},
|
||||||
|
"member_id": {
|
||||||
|
"name": "member_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"users_email_idx": {
|
||||||
|
"name": "users_email_idx",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "email",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": true,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {
|
||||||
|
"users_member_id_members_id_fk": {
|
||||||
|
"name": "users_member_id_members_id_fk",
|
||||||
|
"tableFrom": "users",
|
||||||
|
"tableTo": "members",
|
||||||
|
"columnsFrom": [
|
||||||
|
"member_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"enums": {
|
||||||
|
"public.contact_status": {
|
||||||
|
"name": "contact_status",
|
||||||
|
"schema": "public",
|
||||||
|
"values": [
|
||||||
|
"new",
|
||||||
|
"read",
|
||||||
|
"archived"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"public.member_status": {
|
||||||
|
"name": "member_status",
|
||||||
|
"schema": "public",
|
||||||
|
"values": [
|
||||||
|
"active",
|
||||||
|
"pending"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"public.promo_status": {
|
||||||
|
"name": "promo_status",
|
||||||
|
"schema": "public",
|
||||||
|
"values": [
|
||||||
|
"pending",
|
||||||
|
"live",
|
||||||
|
"expired",
|
||||||
|
"rejected"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"public.request_status": {
|
||||||
|
"name": "request_status",
|
||||||
|
"schema": "public",
|
||||||
|
"values": [
|
||||||
|
"new",
|
||||||
|
"approved",
|
||||||
|
"rejected"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"public.role": {
|
||||||
|
"name": "role",
|
||||||
|
"schema": "public",
|
||||||
|
"values": [
|
||||||
|
"admin",
|
||||||
|
"moderator",
|
||||||
|
"editor",
|
||||||
|
"member"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"schemas": {},
|
||||||
|
"sequences": {},
|
||||||
|
"roles": {},
|
||||||
|
"policies": {},
|
||||||
|
"views": {},
|
||||||
|
"_meta": {
|
||||||
|
"columns": {},
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,6 +15,13 @@
|
|||||||
"when": 1782508812450,
|
"when": 1782508812450,
|
||||||
"tag": "0001_dusty_cardiac",
|
"tag": "0001_dusty_cardiac",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 2,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1782544443365,
|
||||||
|
"tag": "0002_puzzling_kabuki",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
"use server";
|
||||||
|
|
||||||
|
import { revalidatePath } from "next/cache";
|
||||||
|
import { db } from "@/db";
|
||||||
|
import { activityLog, contactMessages, membershipRequests } from "@/db/schema";
|
||||||
|
|
||||||
|
export type SubmitResult = { ok: boolean; error?: string };
|
||||||
|
|
||||||
|
const isEmail = (v: string) => /^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(v);
|
||||||
|
|
||||||
|
async function logActivity(message: string, dot: string) {
|
||||||
|
await db.insert(activityLog).values({ message, dot });
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Demande pour devenir membre (public, sans authentification) ----
|
||||||
|
export async function submitMembershipRequest(formData: FormData): Promise<SubmitResult> {
|
||||||
|
const name = String(formData.get("name") ?? "").trim();
|
||||||
|
const email = String(formData.get("email") ?? "").trim();
|
||||||
|
const phone = String(formData.get("phone") ?? "").trim();
|
||||||
|
const note = String(formData.get("message") ?? "").trim();
|
||||||
|
|
||||||
|
if (!name) return { ok: false, error: "Le nom est requis." };
|
||||||
|
if (email && !isEmail(email)) return { ok: false, error: "Adresse e-mail invalide." };
|
||||||
|
|
||||||
|
// Pas de colonne téléphone dédiée : on l'intègre au message.
|
||||||
|
const message = [phone ? `Téléphone : ${phone}` : null, note || null]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join("\n")
|
||||||
|
.slice(0, 2000);
|
||||||
|
|
||||||
|
await db.insert(membershipRequests).values({
|
||||||
|
name,
|
||||||
|
email: email || null,
|
||||||
|
message: message || null,
|
||||||
|
status: "new",
|
||||||
|
});
|
||||||
|
|
||||||
|
await logActivity(
|
||||||
|
`<strong>${name}</strong> souhaite devenir adhérent`,
|
||||||
|
"#9a6638",
|
||||||
|
);
|
||||||
|
|
||||||
|
revalidatePath("/backend");
|
||||||
|
return { ok: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Message via le formulaire de contact (public) ----
|
||||||
|
export async function submitContactMessage(formData: FormData): Promise<SubmitResult> {
|
||||||
|
const name = String(formData.get("name") ?? "").trim();
|
||||||
|
const email = String(formData.get("email") ?? "").trim();
|
||||||
|
const subject = String(formData.get("subject") ?? "").trim();
|
||||||
|
const message = String(formData.get("message") ?? "").trim();
|
||||||
|
|
||||||
|
if (!name) return { ok: false, error: "Le nom est requis." };
|
||||||
|
if (!isEmail(email)) return { ok: false, error: "Adresse e-mail invalide." };
|
||||||
|
if (!message) return { ok: false, error: "Le message est requis." };
|
||||||
|
|
||||||
|
await db.insert(contactMessages).values({
|
||||||
|
name,
|
||||||
|
email,
|
||||||
|
subject: subject ? subject.slice(0, 200) : null,
|
||||||
|
message: message.slice(0, 4000),
|
||||||
|
status: "new",
|
||||||
|
});
|
||||||
|
|
||||||
|
await logActivity(
|
||||||
|
`Nouveau message de contact de <strong>${name}</strong>`,
|
||||||
|
"#2C6FB3",
|
||||||
|
);
|
||||||
|
|
||||||
|
revalidatePath("/backend");
|
||||||
|
return { ok: true };
|
||||||
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import Link from "next/link";
|
|
||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import { Sparkle } from "@/components/Sparkle";
|
import { Sparkle } from "@/components/Sparkle";
|
||||||
import { SiteHeader } from "@/components/SiteHeader";
|
import { SiteHeader } from "@/components/SiteHeader";
|
||||||
import { SiteFooter } from "@/components/SiteFooter";
|
import { SiteFooter } from "@/components/SiteFooter";
|
||||||
|
import { MembershipModalButton } from "@/components/MembershipModalButton";
|
||||||
import {
|
import {
|
||||||
getActiveMembersWithCategory,
|
getActiveMembersWithCategory,
|
||||||
getAllCategories,
|
getAllCategories,
|
||||||
@@ -45,7 +45,7 @@ export default async function AnnuairePage({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ background: "#F6F2E8", minHeight: "100vh", fontFamily: "'Public Sans',sans-serif", color: "#33291D" }}>
|
<div style={{ background: "#F6F2E8", minHeight: "100vh", fontFamily: "'Public Sans',sans-serif", color: "#33291D" }}>
|
||||||
<SiteHeader active="annuaire" />
|
<SiteHeader active="annuaire" logo />
|
||||||
|
|
||||||
<div className="container" style={{ paddingBottom: 56 }}>
|
<div className="container" style={{ paddingBottom: 56 }}>
|
||||||
{/* title block */}
|
{/* title block */}
|
||||||
@@ -107,13 +107,11 @@ export default async function AnnuairePage({
|
|||||||
Rejoignez l'annuaire Plein R
|
Rejoignez l'annuaire Plein R
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
<Link
|
<MembershipModalButton
|
||||||
href="/backend"
|
label="Adhérer à l'association"
|
||||||
className="font-display"
|
className="font-display"
|
||||||
style={{ textDecoration: "none", background: "#E0A63C", color: "#33291D", fontWeight: 700, fontSize: 15.5, padding: "14px 26px", borderRadius: 12, whiteSpace: "nowrap" }}
|
style={{ border: "none", background: "#E0A63C", color: "#33291D", fontWeight: 700, fontSize: 15.5, padding: "14px 26px", borderRadius: 12, whiteSpace: "nowrap" }}
|
||||||
>
|
/>
|
||||||
Adhérer à l'association
|
|
||||||
</Link>
|
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
+5
-6
@@ -3,6 +3,7 @@ import { Sparkle } from "@/components/Sparkle";
|
|||||||
import { Reveal } from "@/components/Reveal";
|
import { Reveal } from "@/components/Reveal";
|
||||||
import { SiteHeader } from "@/components/SiteHeader";
|
import { SiteHeader } from "@/components/SiteHeader";
|
||||||
import { SiteFooter } from "@/components/SiteFooter";
|
import { SiteFooter } from "@/components/SiteFooter";
|
||||||
|
import { MembershipModalButton } from "@/components/MembershipModalButton";
|
||||||
import { getHighlightedMembers, getLivePromotions } from "@/lib/queries";
|
import { getHighlightedMembers, getLivePromotions } from "@/lib/queries";
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
@@ -456,11 +457,11 @@ export default async function AccueilPage() {
|
|||||||
Rejoignez plus de 120 commerçants et entreprises qui font réseau, se rencontrent et réussissent
|
Rejoignez plus de 120 commerçants et entreprises qui font réseau, se rencontrent et réussissent
|
||||||
ensemble.
|
ensemble.
|
||||||
</p>
|
</p>
|
||||||
<a
|
<MembershipModalButton
|
||||||
href="#"
|
label="Devenir adhérent"
|
||||||
className="font-display lift-cta"
|
className="font-display lift-cta"
|
||||||
style={{
|
style={{
|
||||||
textDecoration: "none",
|
border: "none",
|
||||||
background: "#fff",
|
background: "#fff",
|
||||||
color: "#9a6638",
|
color: "#9a6638",
|
||||||
fontWeight: 700,
|
fontWeight: 700,
|
||||||
@@ -469,9 +470,7 @@ export default async function AccueilPage() {
|
|||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
display: "inline-block",
|
display: "inline-block",
|
||||||
}}
|
}}
|
||||||
>
|
/>
|
||||||
Devenir adhérent
|
|
||||||
</a>
|
|
||||||
</Reveal>
|
</Reveal>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,115 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import { Modal } from "./Modal";
|
||||||
|
import { submitContactMessage } from "@/app/actions/public";
|
||||||
|
|
||||||
|
export function ContactModalButton({
|
||||||
|
label,
|
||||||
|
className,
|
||||||
|
style,
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
className?: string;
|
||||||
|
style?: React.CSSProperties;
|
||||||
|
}) {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [pending, setPending] = useState(false);
|
||||||
|
const [done, setDone] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
function close() {
|
||||||
|
setOpen(false);
|
||||||
|
setTimeout(() => {
|
||||||
|
setDone(false);
|
||||||
|
setError(null);
|
||||||
|
}, 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onSubmit(e: React.FormEvent<HTMLFormElement>) {
|
||||||
|
e.preventDefault();
|
||||||
|
setPending(true);
|
||||||
|
setError(null);
|
||||||
|
const res = await submitContactMessage(new FormData(e.currentTarget));
|
||||||
|
setPending(false);
|
||||||
|
if (res.ok) setDone(true);
|
||||||
|
else setError(res.error ?? "Une erreur est survenue.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<button type="button" className={className} style={{ cursor: "pointer", ...style }} onClick={() => setOpen(true)}>
|
||||||
|
{label}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<Modal open={open} onClose={close} title="Nous contacter">
|
||||||
|
{done ? (
|
||||||
|
<div style={{ textAlign: "center", padding: "10px 0 6px" }}>
|
||||||
|
<div style={{ fontSize: 40, marginBottom: 8 }}>✓</div>
|
||||||
|
<p style={{ margin: "0 0 18px", fontSize: 15, color: "#4a4030" }}>
|
||||||
|
Message envoyé ! Nous vous répondrons dès que possible.
|
||||||
|
</p>
|
||||||
|
<button type="button" className="lift-cta" onClick={close} style={primaryBtn}>
|
||||||
|
Fermer
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<form onSubmit={onSubmit}>
|
||||||
|
<p style={{ margin: "0 0 18px", fontSize: 14, color: "#6c6150" }}>
|
||||||
|
Une question, une suggestion ? Écrivez-nous.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<Field label="Nom *">
|
||||||
|
<input name="name" required maxLength={200} className="field" placeholder="Votre nom" />
|
||||||
|
</Field>
|
||||||
|
<Field label="E-mail *">
|
||||||
|
<input name="email" type="email" required maxLength={200} className="field" placeholder="vous@exemple.fr" />
|
||||||
|
</Field>
|
||||||
|
<Field label="Objet">
|
||||||
|
<input name="subject" maxLength={200} className="field" placeholder="Objet du message" />
|
||||||
|
</Field>
|
||||||
|
<Field label="Message *">
|
||||||
|
<textarea name="message" required rows={5} maxLength={4000} className="field" placeholder="Votre message…" style={{ resize: "vertical" }} />
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
{error && <div style={errorBox}>{error}</div>}
|
||||||
|
|
||||||
|
<button type="submit" className="lift-cta" disabled={pending} style={{ ...primaryBtn, opacity: pending ? 0.6 : 1 }}>
|
||||||
|
{pending ? "Envoi…" : "Envoyer le message"}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Field({ label, children }: { label: string; children: React.ReactNode }) {
|
||||||
|
return (
|
||||||
|
<div style={{ marginBottom: 14 }}>
|
||||||
|
<label className="field-label">{label}</label>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const primaryBtn: React.CSSProperties = {
|
||||||
|
width: "100%",
|
||||||
|
border: "none",
|
||||||
|
background: "#13324F",
|
||||||
|
color: "#fff",
|
||||||
|
fontWeight: 700,
|
||||||
|
fontSize: 15,
|
||||||
|
padding: "13px 18px",
|
||||||
|
borderRadius: 12,
|
||||||
|
cursor: "pointer",
|
||||||
|
};
|
||||||
|
|
||||||
|
const errorBox: React.CSSProperties = {
|
||||||
|
background: "#fbe8e6",
|
||||||
|
color: "#a3372e",
|
||||||
|
fontSize: 13,
|
||||||
|
padding: "10px 12px",
|
||||||
|
borderRadius: 10,
|
||||||
|
marginBottom: 14,
|
||||||
|
};
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import { Modal } from "./Modal";
|
||||||
|
import { submitMembershipRequest } from "@/app/actions/public";
|
||||||
|
|
||||||
|
export function MembershipModalButton({
|
||||||
|
label,
|
||||||
|
className,
|
||||||
|
style,
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
className?: string;
|
||||||
|
style?: React.CSSProperties;
|
||||||
|
}) {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [pending, setPending] = useState(false);
|
||||||
|
const [done, setDone] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
function close() {
|
||||||
|
setOpen(false);
|
||||||
|
// Réinitialise après la fermeture (laisse l'animation finir).
|
||||||
|
setTimeout(() => {
|
||||||
|
setDone(false);
|
||||||
|
setError(null);
|
||||||
|
}, 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onSubmit(e: React.FormEvent<HTMLFormElement>) {
|
||||||
|
e.preventDefault();
|
||||||
|
setPending(true);
|
||||||
|
setError(null);
|
||||||
|
const res = await submitMembershipRequest(new FormData(e.currentTarget));
|
||||||
|
setPending(false);
|
||||||
|
if (res.ok) setDone(true);
|
||||||
|
else setError(res.error ?? "Une erreur est survenue.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<button type="button" className={className} style={{ cursor: "pointer", ...style }} onClick={() => setOpen(true)}>
|
||||||
|
{label}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<Modal open={open} onClose={close} title="Devenir adhérent">
|
||||||
|
{done ? (
|
||||||
|
<div style={{ textAlign: "center", padding: "10px 0 6px" }}>
|
||||||
|
<div style={{ fontSize: 40, marginBottom: 8 }}>✓</div>
|
||||||
|
<p style={{ margin: "0 0 18px", fontSize: 15, color: "#4a4030" }}>
|
||||||
|
Demande envoyée ! L'équipe Plein R vous recontactera rapidement.
|
||||||
|
</p>
|
||||||
|
<button type="button" className="lift-cta" onClick={close} style={primaryBtn}>
|
||||||
|
Fermer
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<form onSubmit={onSubmit}>
|
||||||
|
<p style={{ margin: "0 0 18px", fontSize: 14, color: "#6c6150" }}>
|
||||||
|
Laissez vos coordonnées : nous revenons vers vous pour finaliser votre adhésion.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<Field label="Nom / Raison sociale *">
|
||||||
|
<input name="name" required maxLength={200} className="field" placeholder="Votre nom ou entreprise" />
|
||||||
|
</Field>
|
||||||
|
<Field label="E-mail">
|
||||||
|
<input name="email" type="email" maxLength={200} className="field" placeholder="vous@exemple.fr" />
|
||||||
|
</Field>
|
||||||
|
<Field label="Téléphone">
|
||||||
|
<input name="phone" maxLength={40} className="field" placeholder="06 12 34 56 78" />
|
||||||
|
</Field>
|
||||||
|
<Field label="Message">
|
||||||
|
<textarea name="message" rows={4} maxLength={2000} className="field" placeholder="Votre activité, vos questions…" style={{ resize: "vertical" }} />
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
{error && <div style={errorBox}>{error}</div>}
|
||||||
|
|
||||||
|
<button type="submit" className="lift-cta" disabled={pending} style={{ ...primaryBtn, opacity: pending ? 0.6 : 1 }}>
|
||||||
|
{pending ? "Envoi…" : "Envoyer ma demande"}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Field({ label, children }: { label: string; children: React.ReactNode }) {
|
||||||
|
return (
|
||||||
|
<div style={{ marginBottom: 14 }}>
|
||||||
|
<label className="field-label">{label}</label>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const primaryBtn: React.CSSProperties = {
|
||||||
|
width: "100%",
|
||||||
|
border: "none",
|
||||||
|
background: "#9a6638",
|
||||||
|
color: "#fff",
|
||||||
|
fontWeight: 700,
|
||||||
|
fontSize: 15,
|
||||||
|
padding: "13px 18px",
|
||||||
|
borderRadius: 12,
|
||||||
|
cursor: "pointer",
|
||||||
|
};
|
||||||
|
|
||||||
|
const errorBox: React.CSSProperties = {
|
||||||
|
background: "#fbe8e6",
|
||||||
|
color: "#a3372e",
|
||||||
|
fontSize: 13,
|
||||||
|
padding: "10px 12px",
|
||||||
|
borderRadius: 10,
|
||||||
|
marginBottom: 14,
|
||||||
|
};
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
|
export function Modal({
|
||||||
|
open,
|
||||||
|
onClose,
|
||||||
|
title,
|
||||||
|
children,
|
||||||
|
maxWidth = 480,
|
||||||
|
}: {
|
||||||
|
open: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
title: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
maxWidth?: number;
|
||||||
|
}) {
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) return;
|
||||||
|
const onKey = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === "Escape") onClose();
|
||||||
|
};
|
||||||
|
document.addEventListener("keydown", onKey);
|
||||||
|
const prev = document.body.style.overflow;
|
||||||
|
document.body.style.overflow = "hidden";
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener("keydown", onKey);
|
||||||
|
document.body.style.overflow = prev;
|
||||||
|
};
|
||||||
|
}, [open, onClose]);
|
||||||
|
|
||||||
|
if (!open) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-label={title}
|
||||||
|
onClick={onClose}
|
||||||
|
style={{
|
||||||
|
position: "fixed",
|
||||||
|
inset: 0,
|
||||||
|
zIndex: 1000,
|
||||||
|
background: "rgba(25,20,12,0.55)",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
padding: 20,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
style={{
|
||||||
|
background: "#fff",
|
||||||
|
borderRadius: 18,
|
||||||
|
border: "1px solid #e6dcc6",
|
||||||
|
width: "100%",
|
||||||
|
maxWidth,
|
||||||
|
maxHeight: "90vh",
|
||||||
|
overflowY: "auto",
|
||||||
|
boxShadow: "0 24px 60px rgba(25,20,12,0.28)",
|
||||||
|
padding: "26px 28px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ display: "flex", alignItems: "flex-start", justifyContent: "space-between", gap: 16, marginBottom: 16 }}>
|
||||||
|
<h2 className="font-display" style={{ fontWeight: 800, fontSize: 22, margin: 0, color: "#26201a" }}>
|
||||||
|
{title}
|
||||||
|
</h2>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onClose}
|
||||||
|
aria-label="Fermer"
|
||||||
|
style={{
|
||||||
|
border: "none",
|
||||||
|
background: "#f1ead9",
|
||||||
|
color: "#6f6450",
|
||||||
|
width: 34,
|
||||||
|
height: 34,
|
||||||
|
borderRadius: 10,
|
||||||
|
fontSize: 18,
|
||||||
|
cursor: "pointer",
|
||||||
|
lineHeight: 1,
|
||||||
|
flexShrink: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { ContactModalButton } from "./ContactModalButton";
|
||||||
|
|
||||||
export function SiteFooter() {
|
export function SiteFooter() {
|
||||||
return (
|
return (
|
||||||
<footer style={{ background: "#EFE9DA", borderTop: "1px solid #e2d6bd", padding: "28px 56px" }}>
|
<footer style={{ background: "#EFE9DA", borderTop: "1px solid #e2d6bd", padding: "28px 56px" }}>
|
||||||
@@ -6,7 +8,19 @@ export function SiteFooter() {
|
|||||||
style={{ display: "flex", justifyContent: "flex-end", alignItems: "center" }}
|
style={{ display: "flex", justifyContent: "flex-end", alignItems: "center" }}
|
||||||
>
|
>
|
||||||
<div style={{ fontSize: 13, color: "#8c8068", textAlign: "right", lineHeight: 1.7 }}>
|
<div style={{ fontSize: 13, color: "#8c8068", textAlign: "right", lineHeight: 1.7 }}>
|
||||||
contact@plein-r.fr · Bassin de Pompey
|
<ContactModalButton
|
||||||
|
label="Nous contacter"
|
||||||
|
style={{
|
||||||
|
background: "none",
|
||||||
|
border: "none",
|
||||||
|
padding: 0,
|
||||||
|
font: "inherit",
|
||||||
|
color: "#9a6638",
|
||||||
|
fontWeight: 700,
|
||||||
|
textDecoration: "underline",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{" · Bassin de Pompey"}
|
||||||
<br />
|
<br />
|
||||||
Réseau · Rencontre · Réussite
|
Réseau · Rencontre · Réussite
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,18 +7,24 @@ const NAV = [
|
|||||||
{ key: "association", label: "L'association", href: "#" },
|
{ key: "association", label: "L'association", href: "#" },
|
||||||
];
|
];
|
||||||
|
|
||||||
export function SiteHeader({ active }: { active?: string }) {
|
export function SiteHeader({ active, logo = false }: { active?: string; logo?: boolean }) {
|
||||||
return (
|
return (
|
||||||
<header
|
<header
|
||||||
className="container"
|
className="container"
|
||||||
style={{
|
style={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
justifyContent: "flex-end",
|
justifyContent: logo ? "space-between" : "flex-end",
|
||||||
padding: "18px 56px",
|
padding: "18px 56px",
|
||||||
gap: 20,
|
gap: 20,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
{logo && (
|
||||||
|
<Link href="/" aria-label="Plein R — accueil" style={{ display: "inline-flex", alignItems: "center" }}>
|
||||||
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||||
|
<img src="/assets/logo.png" alt="Plein R" style={{ height: 46, width: "auto", display: "block" }} />
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
<nav style={{ display: "flex", alignItems: "center", gap: 30, flexWrap: "wrap" }}>
|
<nav style={{ display: "flex", alignItems: "center", gap: 30, flexWrap: "wrap" }}>
|
||||||
{NAV.map((n) => {
|
{NAV.map((n) => {
|
||||||
const isActive = active === n.key;
|
const isActive = active === n.key;
|
||||||
|
|||||||
@@ -25,6 +25,11 @@ export const requestStatusEnum = pgEnum("request_status", [
|
|||||||
"approved",
|
"approved",
|
||||||
"rejected",
|
"rejected",
|
||||||
]);
|
]);
|
||||||
|
export const contactStatusEnum = pgEnum("contact_status", [
|
||||||
|
"new",
|
||||||
|
"read",
|
||||||
|
"archived",
|
||||||
|
]);
|
||||||
|
|
||||||
// ---- Categories (métiers) ----
|
// ---- Categories (métiers) ----
|
||||||
export const categories = pgTable("categories", {
|
export const categories = pgTable("categories", {
|
||||||
@@ -99,6 +104,17 @@ export const membershipRequests = pgTable("membership_requests", {
|
|||||||
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ---- Contact messages (formulaire de contact) ----
|
||||||
|
export const contactMessages = pgTable("contact_messages", {
|
||||||
|
id: serial("id").primaryKey(),
|
||||||
|
name: varchar("name", { length: 200 }).notNull(),
|
||||||
|
email: varchar("email", { length: 200 }).notNull(),
|
||||||
|
subject: varchar("subject", { length: 200 }),
|
||||||
|
message: text("message").notNull(),
|
||||||
|
status: contactStatusEnum("status").notNull().default("new"),
|
||||||
|
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
||||||
|
});
|
||||||
|
|
||||||
// ---- Activity log (activité récente) ----
|
// ---- Activity log (activité récente) ----
|
||||||
export const activityLog = pgTable("activity_log", {
|
export const activityLog = pgTable("activity_log", {
|
||||||
id: serial("id").primaryKey(),
|
id: serial("id").primaryKey(),
|
||||||
@@ -135,4 +151,5 @@ export type Member = typeof members.$inferSelect;
|
|||||||
export type User = typeof users.$inferSelect;
|
export type User = typeof users.$inferSelect;
|
||||||
export type Promotion = typeof promotions.$inferSelect;
|
export type Promotion = typeof promotions.$inferSelect;
|
||||||
export type MembershipRequest = typeof membershipRequests.$inferSelect;
|
export type MembershipRequest = typeof membershipRequests.$inferSelect;
|
||||||
|
export type ContactMessage = typeof contactMessages.$inferSelect;
|
||||||
export type ActivityEntry = typeof activityLog.$inferSelect;
|
export type ActivityEntry = typeof activityLog.$inferSelect;
|
||||||
Reference in new issue
Block a user