mirror of
https://github.com/R0m1k3/PleinR.git
synced 2026-10-11 17:27:54 +02:00
Fiche adhérent complète, fidèle à la maquette
Enrichit la fiche : couverture + avatar, identité (catégorie, localisation, adhérent depuis, boutons Appeler/Itinéraire), à propos multi-paragraphes + tags, informations pratiques (horaires + contact tél/email/site), carte Google Maps, colonne promotions en cours et CTA contact. Ajoute les champs members: cover_url, phone, website, postal_code, member_since, hours, tags (migration 0001) ; éditables dans le back-office ; seed enrichi (Au Bon Pain conforme à la maquette). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XwfhYHzC9hQWPDnQ2p53GL
This commit is contained in:
10 files changed
+843
-249
No files matched your search
@@ -0,0 +1,7 @@
|
|||||||
|
ALTER TABLE "members" ADD COLUMN "cover_url" text;--> statement-breakpoint
|
||||||
|
ALTER TABLE "members" ADD COLUMN "phone" varchar(40);--> statement-breakpoint
|
||||||
|
ALTER TABLE "members" ADD COLUMN "website" varchar(200);--> statement-breakpoint
|
||||||
|
ALTER TABLE "members" ADD COLUMN "postal_code" varchar(20);--> statement-breakpoint
|
||||||
|
ALTER TABLE "members" ADD COLUMN "member_since" integer;--> statement-breakpoint
|
||||||
|
ALTER TABLE "members" ADD COLUMN "hours" text;--> statement-breakpoint
|
||||||
|
ALTER TABLE "members" ADD COLUMN "tags" text;
|
||||||
@@ -0,0 +1,525 @@
|
|||||||
|
{
|
||||||
|
"id": "338e83f7-c966-41fc-a2c7-14a4147353c5",
|
||||||
|
"prevId": "bf4dd99e-65a5-4e31-9d25-5fce8e585460",
|
||||||
|
"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.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.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": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,13 @@
|
|||||||
"when": 1782475322634,
|
"when": 1782475322634,
|
||||||
"tag": "0000_numerous_roughhouse",
|
"tag": "0000_numerous_roughhouse",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 1,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1782508812450,
|
||||||
|
"tag": "0001_dusty_cardiac",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
+236
-245
@@ -8,12 +8,25 @@ export const dynamic = "force-dynamic";
|
|||||||
|
|
||||||
const STRIPE_WARM =
|
const STRIPE_WARM =
|
||||||
"repeating-linear-gradient(45deg,#efe9da,#efe9da 12px,#e6ddc9 12px,#e6ddc9 24px)";
|
"repeating-linear-gradient(45deg,#efe9da,#efe9da 12px,#e6ddc9 12px,#e6ddc9 24px)";
|
||||||
|
const COVER_STRIPE =
|
||||||
|
"repeating-linear-gradient(45deg,#e9e1cd,#e9e1cd 16px,#e2d8c1 16px,#e2d8c1 32px)";
|
||||||
|
|
||||||
|
const TAG_COLORS = [
|
||||||
|
{ bg: "#f6efdc", color: "#9a6638" },
|
||||||
|
{ bg: "#e7f0f3", color: "#3f8aa3" },
|
||||||
|
{ bg: "#eef0ec", color: "#5a7a5a" },
|
||||||
|
{ bg: "#eaf0f6", color: "#2C6FB3" },
|
||||||
|
];
|
||||||
|
|
||||||
function badgeColor(badge: string | null) {
|
function badgeColor(badge: string | null) {
|
||||||
if (!badge) return "#1f8a5b";
|
if (!badge) return "#1f8a5b";
|
||||||
return badge.includes("%") ? "#d8472b" : "#1f8a5b";
|
return badge.includes("%") ? "#d8472b" : "#1f8a5b";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function initialsOf(name: string) {
|
||||||
|
return name.split(/\s+/).map((w) => w[0]).join("").slice(0, 2).toUpperCase();
|
||||||
|
}
|
||||||
|
|
||||||
export async function generateMetadata({
|
export async function generateMetadata({
|
||||||
params,
|
params,
|
||||||
}: {
|
}: {
|
||||||
@@ -22,10 +35,7 @@ export async function generateMetadata({
|
|||||||
const { id } = await params;
|
const { id } = await params;
|
||||||
const member = await getPublicMember(Number(id));
|
const member = await getPublicMember(Number(id));
|
||||||
if (!member) return { title: "Adhérent — Plein R" };
|
if (!member) return { title: "Adhérent — Plein R" };
|
||||||
return {
|
return { title: `${member.name} — Plein R`, description: member.description ?? undefined };
|
||||||
title: `${member.name} — Plein R`,
|
|
||||||
description: member.description ?? undefined,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function FicheAdherentPage({
|
export default async function FicheAdherentPage({
|
||||||
@@ -42,90 +52,66 @@ export default async function FicheAdherentPage({
|
|||||||
|
|
||||||
const promos = await getMemberLivePromotions(memberId);
|
const promos = await getMemberLivePromotions(memberId);
|
||||||
const accent = member.accent ?? "#E0A63C";
|
const accent = member.accent ?? "#E0A63C";
|
||||||
const location = [member.address, member.city].filter(Boolean).join(" · ");
|
|
||||||
|
const fullAddress = [member.address, [member.postalCode, member.city].filter(Boolean).join(" ")]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(" · ");
|
||||||
|
const mapQuery = encodeURIComponent(
|
||||||
|
[member.address, member.postalCode, member.city].filter(Boolean).join(", ")
|
||||||
|
);
|
||||||
|
const paragraphs = (member.description ?? "").split("\n").map((s) => s.trim()).filter(Boolean);
|
||||||
|
const tags = (member.tags ?? "").split(",").map((s) => s.trim()).filter(Boolean);
|
||||||
|
const hours = (member.hours ?? "")
|
||||||
|
.split("\n")
|
||||||
|
.map((line) => line.split("|").map((s) => s.trim()))
|
||||||
|
.filter((p) => p[0]);
|
||||||
|
|
||||||
|
const sectionCard: React.CSSProperties = {
|
||||||
|
background: "#fff",
|
||||||
|
border: "1px solid #e6dcc6",
|
||||||
|
borderRadius: 18,
|
||||||
|
padding: "26px 28px",
|
||||||
|
};
|
||||||
|
const h2: React.CSSProperties = {
|
||||||
|
fontWeight: 700,
|
||||||
|
fontSize: 20,
|
||||||
|
margin: "0 0 12px",
|
||||||
|
color: "#26201a",
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div style={{ background: "#F6F2E8", minHeight: "100vh", fontFamily: "'Public Sans',sans-serif", color: "#33291D" }}>
|
||||||
style={{
|
|
||||||
background: "#F6F2E8",
|
|
||||||
minHeight: "100vh",
|
|
||||||
fontFamily: "'Public Sans',sans-serif",
|
|
||||||
color: "#33291D",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{/* header */}
|
{/* header */}
|
||||||
<header
|
<header className="container" style={{ display: "flex", alignItems: "center", justifyContent: "space-between", padding: "18px 56px", gap: 20 }}>
|
||||||
className="container"
|
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "space-between",
|
|
||||||
padding: "18px 56px",
|
|
||||||
gap: 20,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Link href="/" style={{ display: "flex", alignItems: "center", gap: 10, textDecoration: "none" }}>
|
<Link href="/" style={{ display: "flex", alignItems: "center", gap: 10, textDecoration: "none" }}>
|
||||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||||
<img src="/assets/logo.png" alt="Plein R" style={{ height: 44, width: "auto" }} />
|
<img src="/assets/logo.png" alt="Plein R" style={{ height: 44, width: "auto" }} />
|
||||||
<span className="font-display" style={{ fontWeight: 800, fontSize: 20, color: "#13324F" }}>
|
<span className="font-display" style={{ fontWeight: 800, fontSize: 20, color: "#13324F" }}>Plein R</span>
|
||||||
Plein R
|
|
||||||
</span>
|
|
||||||
</Link>
|
</Link>
|
||||||
<nav style={{ display: "flex", alignItems: "center", gap: 30, flexWrap: "wrap" }}>
|
<nav style={{ display: "flex", alignItems: "center", gap: 30, flexWrap: "wrap" }}>
|
||||||
<Link href="/annuaire" style={{ textDecoration: "none", color: "#3c3322", fontWeight: 600, fontSize: 15 }}>
|
<Link href="/" style={{ textDecoration: "none", color: "#6f6450", fontWeight: 500, fontSize: 15 }}>Accueil</Link>
|
||||||
Annuaire
|
<Link href="/annuaire" style={{ textDecoration: "none", color: "#3c3322", fontWeight: 600, fontSize: 15 }}>Annuaire</Link>
|
||||||
</Link>
|
<Link href="/#promotions" style={{ textDecoration: "none", color: "#6f6450", fontWeight: 500, fontSize: 15 }}>Promotions</Link>
|
||||||
<Link href="/#promotions" style={{ textDecoration: "none", color: "#6f6450", fontWeight: 500, fontSize: 15 }}>
|
<Link href="/backend" style={{ textDecoration: "none", color: "#fff", background: "#9a6638", fontWeight: 600, fontSize: 14.5, padding: "10px 18px", borderRadius: 999 }}>Espace adhérent</Link>
|
||||||
Promotions
|
|
||||||
</Link>
|
|
||||||
<Link
|
|
||||||
href="/backend"
|
|
||||||
style={{
|
|
||||||
textDecoration: "none",
|
|
||||||
color: "#fff",
|
|
||||||
background: "#9a6638",
|
|
||||||
fontWeight: 600,
|
|
||||||
fontSize: 14.5,
|
|
||||||
padding: "10px 18px",
|
|
||||||
borderRadius: 999,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Espace adhérent
|
|
||||||
</Link>
|
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div className="container" style={{ paddingBottom: 50 }}>
|
<div className="container" style={{ paddingBottom: 56 }}>
|
||||||
<div style={{ padding: "10px 0 18px" }}>
|
{/* breadcrumb */}
|
||||||
<Link href="/annuaire" className="link-arrow" style={{ textDecoration: "none", color: "#9a6638", fontWeight: 700, fontSize: 14.5 }}>
|
<div style={{ display: "flex", alignItems: "center", gap: 8, fontSize: 13.5, color: "#9a8d72", padding: "6px 0 18px", flexWrap: "wrap" }}>
|
||||||
← Retour à l'annuaire
|
<Link href="/annuaire" style={{ textDecoration: "none", color: "#9a6638", fontWeight: 600 }}>Annuaire</Link>
|
||||||
</Link>
|
{member.categoryLabel && (<><span>›</span><span style={{ color: "#9a8d72" }}>{member.categoryLabel}</span></>)}
|
||||||
|
<span>›</span>
|
||||||
|
<span style={{ color: "#3c3322", fontWeight: 600 }}>{member.name}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* hero card */}
|
{/* cover + identity */}
|
||||||
<section
|
<section style={{ position: "relative", borderRadius: 22, overflow: "hidden", border: "1px solid #e6dcc6" }}>
|
||||||
className="grid hero-grid"
|
|
||||||
style={{
|
|
||||||
position: "relative",
|
|
||||||
background: "#fff",
|
|
||||||
border: "1px solid #e6dcc6",
|
|
||||||
borderRadius: 24,
|
|
||||||
padding: 28,
|
|
||||||
overflow: "hidden",
|
|
||||||
boxShadow: "0 26px 60px -40px rgba(40,30,15,0.5)",
|
|
||||||
gap: 32,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Sparkle color={accent} size={18} style={{ top: 20, right: 24 }} duration={3.4} />
|
|
||||||
<Sparkle color="#6FB0C6" size={12} style={{ top: 52, right: 70 }} duration={2.7} delay={0.5} />
|
|
||||||
|
|
||||||
{/* vitrine photo */}
|
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
position: "relative",
|
position: "relative",
|
||||||
minHeight: 240,
|
height: 230,
|
||||||
borderRadius: 18,
|
background: member.coverUrl ? `url(${member.coverUrl})` : COVER_STRIPE,
|
||||||
background: member.logoUrl ? `url(${member.logoUrl})` : STRIPE_WARM,
|
|
||||||
backgroundSize: "cover",
|
backgroundSize: "cover",
|
||||||
backgroundPosition: "center",
|
backgroundPosition: "center",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
@@ -133,197 +119,202 @@ export default async function FicheAdherentPage({
|
|||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{!member.logoUrl && (
|
{!member.coverUrl && (
|
||||||
<span style={{ fontSize: 11, letterSpacing: "0.12em", color: "#a99c82", textTransform: "uppercase" }}>
|
<span style={{ fontSize: 11, letterSpacing: "0.14em", color: "#b3a888", textTransform: "uppercase" }}>photo de couverture</span>
|
||||||
photo vitrine
|
|
||||||
</span>
|
|
||||||
)}
|
)}
|
||||||
|
<Sparkle color={accent} size={20} style={{ top: 20, right: 24 }} duration={3.2} />
|
||||||
</div>
|
</div>
|
||||||
|
<div style={{ background: "#fff", padding: "0 30px 26px", display: "flex", alignItems: "flex-end", gap: 22, flexWrap: "wrap" }}>
|
||||||
{/* info */}
|
|
||||||
<div style={{ display: "flex", flexDirection: "column", justifyContent: "center" }}>
|
|
||||||
{member.categoryLabel && (
|
|
||||||
<span
|
|
||||||
style={{
|
|
||||||
alignSelf: "flex-start",
|
|
||||||
background: "#9a6638",
|
|
||||||
color: "#fff",
|
|
||||||
borderRadius: 999,
|
|
||||||
padding: "5px 13px",
|
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: 700,
|
|
||||||
marginBottom: 14,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{member.categoryLabel}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
<h1 className="font-display" style={{ fontWeight: 800, fontSize: 40, lineHeight: 1.05, letterSpacing: "-0.02em", margin: "0 0 10px", color: "#26201a" }}>
|
|
||||||
{member.name}
|
|
||||||
</h1>
|
|
||||||
{location && (
|
|
||||||
<div style={{ display: "flex", alignItems: "center", gap: 8, fontSize: 14, color: "#6c6150", marginBottom: 14 }}>
|
|
||||||
<span style={{ width: 8, height: 8, borderRadius: "50%", background: accent }} />
|
|
||||||
{location}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{member.description && (
|
|
||||||
<p style={{ fontSize: 16, lineHeight: 1.6, color: "#6c6150", margin: "0 0 22px", maxWidth: 520 }}>
|
|
||||||
{member.description}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
<div style={{ display: "flex", gap: 10, flexWrap: "wrap" }}>
|
|
||||||
{member.email && (
|
|
||||||
<a
|
|
||||||
href={`mailto:${member.email}`}
|
|
||||||
className="font-display"
|
|
||||||
style={{
|
|
||||||
textDecoration: "none",
|
|
||||||
background: "#2C6FB3",
|
|
||||||
color: "#fff",
|
|
||||||
fontWeight: 700,
|
|
||||||
fontSize: 15,
|
|
||||||
padding: "12px 22px",
|
|
||||||
borderRadius: 11,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Contacter
|
|
||||||
</a>
|
|
||||||
)}
|
|
||||||
{promos.length > 0 && (
|
|
||||||
<a
|
|
||||||
href="#promotions"
|
|
||||||
className="font-display"
|
|
||||||
style={{
|
|
||||||
textDecoration: "none",
|
|
||||||
background: "#fff",
|
|
||||||
color: "#9a6638",
|
|
||||||
border: "1px solid #d8cdb4",
|
|
||||||
fontWeight: 700,
|
|
||||||
fontSize: 15,
|
|
||||||
padding: "12px 22px",
|
|
||||||
borderRadius: 11,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Voir les promotions
|
|
||||||
</a>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* promotions */}
|
|
||||||
<section id="promotions" style={{ paddingTop: 44 }}>
|
|
||||||
<h2 className="font-display" style={{ fontWeight: 800, fontSize: 26, margin: "0 0 20px", color: "#26201a" }}>
|
|
||||||
Promotions en cours
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
{promos.length === 0 ? (
|
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
background: "#fff",
|
width: 118,
|
||||||
border: "1px dashed #d8cdb4",
|
height: 118,
|
||||||
borderRadius: 16,
|
borderRadius: 22,
|
||||||
padding: "28px 24px",
|
background: member.logoUrl ? `url(${member.logoUrl})` : "#F6F2E8",
|
||||||
color: "#8c8068",
|
backgroundSize: "cover",
|
||||||
fontSize: 14.5,
|
backgroundPosition: "center",
|
||||||
|
border: "4px solid #fff",
|
||||||
|
marginTop: -54,
|
||||||
|
flexShrink: 0,
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
boxShadow: "0 14px 30px -16px rgba(40,30,15,0.4)",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Cet adhérent n'a pas de promotion en cours pour le moment.
|
{!member.logoUrl && (
|
||||||
|
<span className="font-display" style={{ fontWeight: 800, fontSize: 34, color: "#9a6638" }}>{initialsOf(member.name)}</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
<div style={{ flex: 1, minWidth: 240, paddingTop: 20 }}>
|
||||||
<div className="grid grid-3">
|
<div style={{ display: "flex", alignItems: "center", gap: 12, flexWrap: "wrap" }}>
|
||||||
{promos.map((p) => (
|
<h1 className="font-display" style={{ fontWeight: 800, fontSize: 32, letterSpacing: "-0.02em", margin: 0, color: "#26201a" }}>{member.name}</h1>
|
||||||
<article
|
{member.categoryLabel && (
|
||||||
key={p.id}
|
<span style={{ background: "#9a6638", color: "#fff", borderRadius: 999, padding: "5px 13px", fontSize: 12, fontWeight: 700 }}>{member.categoryLabel}</span>
|
||||||
className="lift-card"
|
)}
|
||||||
style={{
|
</div>
|
||||||
background: "#fff",
|
<div style={{ display: "flex", alignItems: "center", gap: 7, fontSize: 14, color: "#6c6150", marginTop: 8, flexWrap: "wrap" }}>
|
||||||
border: "1px solid #ece3d0",
|
<span style={{ width: 8, height: 8, borderRadius: "50%", background: accent }} />
|
||||||
borderRadius: 18,
|
{[fullAddress, member.memberSince ? `Adhérent depuis ${member.memberSince}` : null].filter(Boolean).join(" · ")}
|
||||||
overflow: "hidden",
|
</div>
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
position: "relative",
|
|
||||||
height: 168,
|
|
||||||
background: p.imageUrl ? `url(${p.imageUrl})` : STRIPE_WARM,
|
|
||||||
backgroundSize: "cover",
|
|
||||||
backgroundPosition: "center",
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{!p.imageUrl && (
|
|
||||||
<span style={{ fontSize: 10.5, letterSpacing: "0.12em", color: "#a99c82", textTransform: "uppercase" }}>
|
|
||||||
photo de l'offre
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{p.badge && (
|
|
||||||
<span
|
|
||||||
className="font-display"
|
|
||||||
style={{
|
|
||||||
position: "absolute",
|
|
||||||
top: 0,
|
|
||||||
right: 0,
|
|
||||||
background: badgeColor(p.badge),
|
|
||||||
color: "#fff",
|
|
||||||
fontWeight: 800,
|
|
||||||
fontSize: 18,
|
|
||||||
padding: "8px 16px",
|
|
||||||
borderRadius: "0 0 0 16px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{p.badge}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{p.category && (
|
|
||||||
<span style={{ position: "absolute", top: 12, left: 12, background: "#9a6638", color: "#fff", borderRadius: 999, padding: "5px 12px", fontSize: 11, fontWeight: 700 }}>
|
|
||||||
{p.category}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div style={{ padding: "17px 18px 18px", display: "flex", flexDirection: "column", flex: 1 }}>
|
|
||||||
<h3 className="font-display" style={{ fontWeight: 700, fontSize: 18, margin: "0 0 5px", color: "#26201a" }}>
|
|
||||||
{p.title}
|
|
||||||
</h3>
|
|
||||||
<p style={{ margin: "0 0 14px", fontSize: 13.5, color: "#8c8068", lineHeight: 1.5, flex: 1 }}>
|
|
||||||
{p.text}
|
|
||||||
</p>
|
|
||||||
{p.validUntil && (
|
|
||||||
<div style={{ fontSize: 11.5, color: "#a99c82", borderTop: "1px solid #f0e8d6", paddingTop: 12 }}>
|
|
||||||
{p.validUntil}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
<div style={{ display: "flex", gap: 10, paddingBottom: 4, flexWrap: "wrap" }}>
|
||||||
|
{member.phone && (
|
||||||
|
<a href={`tel:${member.phone.replace(/\s/g, "")}`} className="font-display" style={{ textDecoration: "none", background: "#2C6FB3", color: "#fff", fontWeight: 700, fontSize: 14, padding: "11px 18px", borderRadius: 11 }}>Appeler</a>
|
||||||
|
)}
|
||||||
|
{mapQuery && (
|
||||||
|
<a href={`https://www.google.com/maps/dir/?api=1&destination=${mapQuery}`} target="_blank" rel="noopener" className="font-display" style={{ textDecoration: "none", background: "#fff", border: "1px solid #d8cdb4", color: "#3c3322", fontWeight: 700, fontSize: 14, padding: "11px 18px", borderRadius: 11 }}>Itinéraire</a>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{/* two columns */}
|
||||||
|
<div className="grid fiche-split" style={{ marginTop: 26 }}>
|
||||||
|
{/* LEFT */}
|
||||||
|
<div style={{ display: "flex", flexDirection: "column", gap: 22 }}>
|
||||||
|
{/* about */}
|
||||||
|
<section style={sectionCard}>
|
||||||
|
<h2 className="font-display" style={h2}>À propos de l'établissement</h2>
|
||||||
|
{paragraphs.length > 0 ? (
|
||||||
|
paragraphs.map((p, i) => (
|
||||||
|
<p key={i} style={{ margin: i === paragraphs.length - 1 ? 0 : "0 0 12px", fontSize: 15, lineHeight: 1.65, color: "#5e5444" }}>{p}</p>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<p style={{ margin: 0, fontSize: 15, color: "#8c8068" }}>Présentation à venir.</p>
|
||||||
|
)}
|
||||||
|
{tags.length > 0 && (
|
||||||
|
<div style={{ display: "flex", flexWrap: "wrap", gap: 9, marginTop: 18 }}>
|
||||||
|
{tags.map((t, i) => {
|
||||||
|
const c = TAG_COLORS[i % TAG_COLORS.length];
|
||||||
|
return (
|
||||||
|
<span key={t} style={{ background: c.bg, color: c.color, fontSize: 12.5, fontWeight: 600, padding: "7px 13px", borderRadius: 999 }}>{t}</span>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* practical info */}
|
||||||
|
<section style={sectionCard}>
|
||||||
|
<h2 className="font-display" style={{ ...h2, marginBottom: 16 }}>Informations pratiques</h2>
|
||||||
|
<div className="grid grid-2" style={{ gap: 22 }}>
|
||||||
|
<div>
|
||||||
|
<div style={{ fontSize: 12.5, letterSpacing: "0.06em", textTransform: "uppercase", color: "#a99c82", fontWeight: 700, marginBottom: 10 }}>Horaires</div>
|
||||||
|
{hours.length > 0 ? (
|
||||||
|
<div style={{ display: "flex", flexDirection: "column", gap: 7, fontSize: 14 }}>
|
||||||
|
{hours.map(([label, range], i) => {
|
||||||
|
const closed = (range ?? "").toLowerCase().includes("ferm");
|
||||||
|
return (
|
||||||
|
<div key={i} style={{ display: "flex", justifyContent: "space-between", gap: 12 }}>
|
||||||
|
<span style={{ color: "#6c6150" }}>{label}</span>
|
||||||
|
<span style={{ color: closed ? "#c0392b" : "#3c3322", fontWeight: 600 }}>{range}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div style={{ fontSize: 14, color: "#a99c82" }}>Horaires non communiqués.</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div style={{ fontSize: 12.5, letterSpacing: "0.06em", textTransform: "uppercase", color: "#a99c82", fontWeight: 700, marginBottom: 10 }}>Contact</div>
|
||||||
|
<div style={{ display: "flex", flexDirection: "column", gap: 9, fontSize: 14, color: "#5e5444" }}>
|
||||||
|
{member.phone && (
|
||||||
|
<div style={{ display: "flex", alignItems: "center", gap: 9 }}><span style={{ width: 7, height: 7, borderRadius: "50%", background: "#E0A63C" }} />{member.phone}</div>
|
||||||
|
)}
|
||||||
|
{member.email && (
|
||||||
|
<a href={`mailto:${member.email}`} style={{ display: "flex", alignItems: "center", gap: 9, color: "#5e5444", textDecoration: "none" }}><span style={{ width: 7, height: 7, borderRadius: "50%", background: "#6FB0C6" }} />{member.email}</a>
|
||||||
|
)}
|
||||||
|
{member.website && (
|
||||||
|
<a href={member.website} target="_blank" rel="noopener" style={{ display: "flex", alignItems: "center", gap: 9, color: "#2C6FB3", textDecoration: "none", fontWeight: 600 }}><span style={{ width: 7, height: 7, borderRadius: "50%", background: "#2C6FB3" }} />{member.website.replace(/^https?:\/\//, "")}</a>
|
||||||
|
)}
|
||||||
|
{!member.phone && !member.email && !member.website && (
|
||||||
|
<div style={{ color: "#a99c82" }}>Coordonnées non communiquées.</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* map */}
|
||||||
|
{mapQuery && (
|
||||||
|
<section style={{ background: "#fff", border: "1px solid #e6dcc6", borderRadius: 18, overflow: "hidden" }}>
|
||||||
|
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", padding: "20px 28px 14px", gap: 12, flexWrap: "wrap" }}>
|
||||||
|
<h2 className="font-display" style={{ ...h2, margin: 0 }}>Nous situer</h2>
|
||||||
|
<a href={`https://www.google.com/maps/search/?api=1&query=${mapQuery}`} target="_blank" rel="noopener" style={{ textDecoration: "none", color: "#2C6FB3", fontWeight: 700, fontSize: 13.5 }}>Ouvrir dans Google Maps →</a>
|
||||||
|
</div>
|
||||||
|
<iframe
|
||||||
|
title={`Carte — ${member.name}`}
|
||||||
|
src={`https://www.google.com/maps?q=${mapQuery}&output=embed`}
|
||||||
|
style={{ width: "100%", height: 320, border: 0, display: "block", filter: "saturate(0.92)" }}
|
||||||
|
loading="lazy"
|
||||||
|
referrerPolicy="no-referrer-when-downgrade"
|
||||||
|
/>
|
||||||
|
{fullAddress && (
|
||||||
|
<div style={{ display: "flex", alignItems: "center", gap: 9, padding: "14px 28px", fontSize: 13.5, color: "#6c6150", borderTop: "1px solid #f0e8d6" }}>
|
||||||
|
<span style={{ width: 8, height: 8, borderRadius: "50%", background: "#9a6638" }} />{fullAddress}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* RIGHT */}
|
||||||
|
<aside style={{ display: "flex", flexDirection: "column", gap: 18, position: "sticky", top: 18 }}>
|
||||||
|
<section style={{ position: "relative", background: "#fff", border: "1px solid #e6dcc6", borderRadius: 18, padding: "22px 22px 24px", overflow: "hidden" }}>
|
||||||
|
<Sparkle color="#E0A63C" size={16} style={{ top: 18, right: 20 }} duration={3} />
|
||||||
|
<div style={{ display: "inline-flex", alignItems: "center", gap: 8, background: "#fdeccd", color: "#9a6638", fontSize: 11.5, letterSpacing: "0.1em", textTransform: "uppercase", fontWeight: 800, padding: "5px 12px", borderRadius: 999, marginBottom: 14 }}>
|
||||||
|
<span style={{ width: 7, height: 7, borderRadius: "50%", background: "#E0A63C" }} />Ses bons plans
|
||||||
|
</div>
|
||||||
|
<h2 className="font-display" style={{ fontWeight: 800, fontSize: 21, margin: "0 0 4px", color: "#26201a" }}>Promotions en cours</h2>
|
||||||
|
<p style={{ margin: "0 0 18px", fontSize: 13.5, color: "#8c8068" }}>Réservées aux clients & adhérents Plein R.</p>
|
||||||
|
|
||||||
|
{promos.length === 0 ? (
|
||||||
|
<div style={{ border: "1px dashed #d8cdb4", borderRadius: 14, padding: "20px 16px", fontSize: 13.5, color: "#8c8068", textAlign: "center" }}>
|
||||||
|
Pas de promotion en cours.
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
promos.map((p, idx) => (
|
||||||
|
<article key={p.id} className="lift" style={{ border: "1px solid #ece3d0", borderRadius: 16, overflow: "hidden", marginBottom: idx === promos.length - 1 ? 0 : 14 }}>
|
||||||
|
<div style={{ position: "relative", height: 120, background: p.imageUrl ? `url(${p.imageUrl})` : STRIPE_WARM, backgroundSize: "cover", backgroundPosition: "center", display: "flex", alignItems: "center", justifyContent: "center" }}>
|
||||||
|
{!p.imageUrl && (<span style={{ fontSize: 10, letterSpacing: "0.12em", color: "#a99c82", textTransform: "uppercase" }}>photo de l'offre</span>)}
|
||||||
|
{p.badge && (
|
||||||
|
<span className="font-display" style={{ position: "absolute", top: 0, right: 0, background: badgeColor(p.badge), color: "#fff", fontWeight: 800, fontSize: 16, padding: "6px 13px", borderRadius: "0 0 0 14px" }}>{p.badge}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div style={{ padding: "14px 15px 15px" }}>
|
||||||
|
<h3 className="font-display" style={{ fontWeight: 700, fontSize: 16, margin: "0 0 5px", color: "#26201a" }}>{p.title}</h3>
|
||||||
|
<p style={{ margin: "0 0 11px", fontSize: 13, color: "#8c8068", lineHeight: 1.5 }}>{p.text}</p>
|
||||||
|
{p.validUntil && (
|
||||||
|
<div style={{ borderTop: "1px solid #f0e8d6", paddingTop: 10, fontSize: 12, color: "#a99c82" }}>{p.validUntil}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* contact CTA */}
|
||||||
|
<section style={{ background: "linear-gradient(120deg,#13324F,#1d4a72)", borderRadius: 18, padding: 22, color: "#fff" }}>
|
||||||
|
<h3 className="font-display" style={{ fontWeight: 700, fontSize: 17, margin: "0 0 6px" }}>Une question pour ce commerçant ?</h3>
|
||||||
|
<p style={{ margin: "0 0 14px", fontSize: 13.5, color: "#bcd3e6" }}>Contactez {member.name} directement.</p>
|
||||||
|
<a href={member.email ? `mailto:${member.email}` : "#"} className="font-display" style={{ textDecoration: "none", display: "block", textAlign: "center", background: "#E0A63C", color: "#33291D", fontWeight: 700, fontSize: 14.5, padding: 12, borderRadius: 11 }}>Envoyer un message</a>
|
||||||
|
</section>
|
||||||
|
</aside>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* footer */}
|
{/* footer */}
|
||||||
<footer style={{ background: "#EFE9DA", borderTop: "1px solid #e2d6bd", padding: "28px 56px" }}>
|
<footer style={{ background: "#EFE9DA", borderTop: "1px solid #e2d6bd", padding: "28px 56px" }}>
|
||||||
<div
|
<div className="container" style={{ display: "flex", justifyContent: "space-between", alignItems: "center", gap: 16, flexWrap: "wrap" }}>
|
||||||
className="container"
|
|
||||||
style={{ display: "flex", justifyContent: "space-between", alignItems: "center", gap: 16, flexWrap: "wrap" }}
|
|
||||||
>
|
|
||||||
<div style={{ display: "flex", alignItems: "center", gap: 10 }}>
|
<div style={{ display: "flex", alignItems: "center", gap: 10 }}>
|
||||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||||
<img src="/assets/logo.png" alt="Plein R" style={{ height: 36, width: "auto" }} />
|
<img src="/assets/logo.png" alt="Plein R" style={{ height: 36, width: "auto" }} />
|
||||||
<span className="font-display" style={{ fontWeight: 800, fontSize: 16, color: "#13324F" }}>
|
<span className="font-display" style={{ fontWeight: 800, fontSize: 16, color: "#13324F" }}>Plein R</span>
|
||||||
Plein R
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<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
|
contact@plein-r.fr · Bassin de Pompey<br />Réseau · Rencontre · Réussite
|
||||||
<br />
|
|
||||||
Réseau · Rencontre · Réussite
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|||||||
@@ -115,6 +115,12 @@ export async function updateMember(formData: FormData) {
|
|||||||
city: String(formData.get("city") ?? "").trim() || null,
|
city: String(formData.get("city") ?? "").trim() || null,
|
||||||
address: String(formData.get("address") ?? "").trim() || null,
|
address: String(formData.get("address") ?? "").trim() || null,
|
||||||
description: String(formData.get("description") ?? "").trim() || null,
|
description: String(formData.get("description") ?? "").trim() || null,
|
||||||
|
postalCode: String(formData.get("postalCode") ?? "").trim() || null,
|
||||||
|
phone: String(formData.get("phone") ?? "").trim() || null,
|
||||||
|
website: String(formData.get("website") ?? "").trim() || null,
|
||||||
|
memberSince: formData.get("memberSince") ? Number(formData.get("memberSince")) : null,
|
||||||
|
tags: String(formData.get("tags") ?? "").trim() || null,
|
||||||
|
hours: String(formData.get("hours") ?? "").trim() || null,
|
||||||
status: String(formData.get("status") ?? "pending") as "active" | "pending",
|
status: String(formData.get("status") ?? "pending") as "active" | "pending",
|
||||||
highlighted: formData.get("highlighted") === "on",
|
highlighted: formData.get("highlighted") === "on",
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -85,6 +85,22 @@ export default async function EditMemberPage({
|
|||||||
<label className="field-label">Adresse</label>
|
<label className="field-label">Adresse</label>
|
||||||
<input name="address" defaultValue={member.address ?? ""} className="field" />
|
<input name="address" defaultValue={member.address ?? ""} className="field" />
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="field-label">Code postal</label>
|
||||||
|
<input name="postalCode" defaultValue={member.postalCode ?? ""} className="field" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="field-label">Téléphone</label>
|
||||||
|
<input name="phone" defaultValue={member.phone ?? ""} className="field" placeholder="03 83 ..." />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="field-label">Site web</label>
|
||||||
|
<input name="website" defaultValue={member.website ?? ""} className="field" placeholder="https://..." />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="field-label">Adhérent depuis (année)</label>
|
||||||
|
<input name="memberSince" type="number" defaultValue={member.memberSince ?? ""} className="field" placeholder="2021" />
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="field-label">Statut</label>
|
<label className="field-label">Statut</label>
|
||||||
<select name="status" className="field" defaultValue={member.status}>
|
<select name="status" className="field" defaultValue={member.status}>
|
||||||
@@ -95,8 +111,18 @@ export default async function EditMemberPage({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ marginTop: 16 }}>
|
<div style={{ marginTop: 16 }}>
|
||||||
<label className="field-label">Description</label>
|
<label className="field-label">Description (une ligne vide sépare les paragraphes)</label>
|
||||||
<textarea name="description" rows={3} defaultValue={member.description ?? ""} className="field" style={{ resize: "vertical" }} />
|
<textarea name="description" rows={4} defaultValue={member.description ?? ""} className="field" style={{ resize: "vertical" }} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ marginTop: 16 }}>
|
||||||
|
<label className="field-label">Tags / spécialités (séparés par des virgules)</label>
|
||||||
|
<input name="tags" defaultValue={member.tags ?? ""} className="field" placeholder="Levain naturel, Produits locaux, Fait maison" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ marginTop: 16 }}>
|
||||||
|
<label className="field-label">Horaires (une ligne par jour : « Jour|plage », ex : Mardi – Vendredi|7h – 19h30)</label>
|
||||||
|
<textarea name="hours" rows={4} defaultValue={member.hours ?? ""} className="field" style={{ resize: "vertical" }} placeholder={"Mardi – Vendredi|7h – 19h30\nSamedi|7h – 19h\nDimanche|7h – 13h\nLundi|Fermé"} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<label style={{ display: "flex", alignItems: "center", gap: 9, marginTop: 16, fontSize: 14, color: "#3c3322", cursor: "pointer" }}>
|
<label style={{ display: "flex", alignItems: "center", gap: 9, marginTop: 16, fontSize: 14, color: "#3c3322", cursor: "pointer" }}>
|
||||||
|
|||||||
+7
-1
@@ -160,6 +160,11 @@ button {
|
|||||||
gap: 18px;
|
gap: 18px;
|
||||||
align-items: start;
|
align-items: start;
|
||||||
}
|
}
|
||||||
|
.fiche-split {
|
||||||
|
grid-template-columns: 1.65fr 1fr;
|
||||||
|
gap: 26px;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 1024px) {
|
@media (max-width: 1024px) {
|
||||||
.grid-6 {
|
.grid-6 {
|
||||||
@@ -172,7 +177,8 @@ button {
|
|||||||
grid-template-columns: repeat(2, 1fr);
|
grid-template-columns: repeat(2, 1fr);
|
||||||
}
|
}
|
||||||
.dash-split,
|
.dash-split,
|
||||||
.admins-split {
|
.admins-split,
|
||||||
|
.fiche-split {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,6 +48,13 @@ export const members = pgTable("members", {
|
|||||||
status: memberStatusEnum("status").notNull().default("pending"),
|
status: memberStatusEnum("status").notNull().default("pending"),
|
||||||
highlighted: boolean("highlighted").notNull().default(false),
|
highlighted: boolean("highlighted").notNull().default(false),
|
||||||
logoUrl: text("logo_url"),
|
logoUrl: text("logo_url"),
|
||||||
|
coverUrl: text("cover_url"),
|
||||||
|
phone: varchar("phone", { length: 40 }),
|
||||||
|
website: varchar("website", { length: 200 }),
|
||||||
|
postalCode: varchar("postal_code", { length: 20 }),
|
||||||
|
memberSince: integer("member_since"),
|
||||||
|
hours: text("hours"),
|
||||||
|
tags: text("tags"),
|
||||||
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+13
-1
@@ -48,7 +48,14 @@ async function main() {
|
|||||||
categoryId: catId("boulangerie"),
|
categoryId: catId("boulangerie"),
|
||||||
city: "Frouard",
|
city: "Frouard",
|
||||||
address: "12 rue de la République",
|
address: "12 rue de la République",
|
||||||
description: "Pains au levain & viennoiseries maison, tous les matins.",
|
postalCode: "54390",
|
||||||
|
phone: "03 83 49 00 00",
|
||||||
|
website: "https://www.aubonpain-frouard.fr",
|
||||||
|
memberSince: 2021,
|
||||||
|
tags: "Levain naturel, Produits locaux, Fait maison, Sans conservateur",
|
||||||
|
hours: "Mardi – Vendredi|7h – 19h30\nSamedi|7h – 19h\nDimanche|7h – 13h\nLundi|Fermé",
|
||||||
|
description:
|
||||||
|
"Boulangerie artisanale familiale installée au cœur de Frouard depuis trois générations. Nous travaillons chaque jour des farines locales et un levain naturel pour vous proposer pains, viennoiseries et pâtisseries faits maison.\nNotre équipe vous accueille du mardi au dimanche matin. Spécialités : pain au levain, baguette de tradition, kouglof et tartes aux fruits de saison.",
|
||||||
status: "active" as const,
|
status: "active" as const,
|
||||||
highlighted: true,
|
highlighted: true,
|
||||||
},
|
},
|
||||||
@@ -58,6 +65,11 @@ async function main() {
|
|||||||
categoryId: catId("restauration"),
|
categoryId: catId("restauration"),
|
||||||
city: "Pompey",
|
city: "Pompey",
|
||||||
address: "3 place Stanislas",
|
address: "3 place Stanislas",
|
||||||
|
postalCode: "54340",
|
||||||
|
phone: "03 83 24 00 00",
|
||||||
|
memberSince: 2019,
|
||||||
|
tags: "Cuisine de saison, Brunch, Terrasse, Produits frais",
|
||||||
|
hours: "Lundi – Vendredi|9h – 18h\nSamedi|9h – 19h\nDimanche|10h – 15h",
|
||||||
description: "Cuisine de saison, brunch le dimanche, terrasse au cœur du bourg.",
|
description: "Cuisine de saison, brunch le dimanche, terrasse au cœur du bourg.",
|
||||||
status: "active" as const,
|
status: "active" as const,
|
||||||
highlighted: true,
|
highlighted: true,
|
||||||
|
|||||||
@@ -86,6 +86,13 @@ export async function getPublicMember(id: number) {
|
|||||||
address: members.address,
|
address: members.address,
|
||||||
status: members.status,
|
status: members.status,
|
||||||
logoUrl: members.logoUrl,
|
logoUrl: members.logoUrl,
|
||||||
|
coverUrl: members.coverUrl,
|
||||||
|
phone: members.phone,
|
||||||
|
website: members.website,
|
||||||
|
postalCode: members.postalCode,
|
||||||
|
memberSince: members.memberSince,
|
||||||
|
hours: members.hours,
|
||||||
|
tags: members.tags,
|
||||||
categoryLabel: categories.label,
|
categoryLabel: categories.label,
|
||||||
accent: categories.accent,
|
accent: categories.accent,
|
||||||
tint: categories.tint,
|
tint: categories.tint,
|
||||||
|
|||||||
Reference in new issue
Block a user