mirror of
https://github.com/R0m1k3/PleinR.git
synced 2026-10-20 21:58:09 +02:00
Refonte du bloc infos pratiques, suspension des promos et publication réseaux
Bloc « Informations pratiques » (fiche adhérent) - Suppression des dégradés hors palette (radial doré de la section, dégradés crème/bleu des deux sous-cartes) au profit du vocabulaire visuel du site : carte blanche bordée, sous-cartes plates sur le fond de champ, variables CSS. - La pastille « Préparer votre visite » devient une puce arrondie identique aux badges du reste du site ; le chip d'ouverture reprend les teintes de statut. - Les glyphes texte ◷ et ↗ sont remplacés par des icônes SVG cohérentes. Suspension des promotions - Nouveau statut `suspended` + colonnes `suspended_by` / `suspended_by_id` / `suspended_at`. - L'adhérent suspend et réactive ses propres promotions depuis Mon espace ; l'association suspend et réactive n'importe laquelle depuis le backoffice. - Une suspension décidée par l'association ne peut pas être levée par l'adhérent : le motif est affiché dans son espace. - Les lectures publiques filtrant déjà sur `live`, une promo suspendue quitte immédiatement l'accueil, l'annuaire et la fiche. Publication Facebook / LinkedIn - `src/lib/social.ts` : publication d'une promo en ligne sur la page Facebook (Graph API, upload multipart car les images sont en data-URI) et sur la page LinkedIn (Posts API + Images API en trois étapes). - Jetons d'accès exclusivement en variables d'environnement ; un réseau non configuré masque son bouton. - Table `social_posts` : chaque tentative est tracée, un échec est rattrapé et affiché sur la carte de la promo sans interrompre le backoffice. - Nouvelle capacité RBAC `publishSocial` (admin + modérateur). Liens sociaux publics - Réglages `association_facebook` / `association_linkedin` éditables dans Paramètres, préremplis avec les pages de l'association. - Section « Plein R sur les réseaux » sur l'accueil et icônes dans le pied de page, masquées si le réglage est vide. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QXNXRC4j5VLfKvpyyisrnb
This commit is contained in:
20 files changed
+2370
-109
No files matched your search
+41
-1
@@ -20,7 +20,13 @@ export const promoStatusEnum = pgEnum("promo_status", [
|
||||
"live",
|
||||
"expired",
|
||||
"rejected",
|
||||
"suspended",
|
||||
]);
|
||||
// Qui a suspendu la promotion : son créateur (l'adhérent) ou l'association.
|
||||
// Une promotion suspendue par l'association ne peut être réactivée que par elle.
|
||||
export const promoSuspendedByEnum = pgEnum("promo_suspended_by", ["member", "staff"]);
|
||||
export const socialNetworkEnum = pgEnum("social_network", ["facebook", "linkedin"]);
|
||||
export const socialPostStatusEnum = pgEnum("social_post_status", ["posted", "failed"]);
|
||||
export const requestStatusEnum = pgEnum("request_status", [
|
||||
"new",
|
||||
"approved",
|
||||
@@ -94,9 +100,33 @@ export const promotions = pgTable("promotions", {
|
||||
memberId: integer("member_id").references(() => members.id),
|
||||
status: promoStatusEnum("status").notNull().default("pending"),
|
||||
validUntil: varchar("valid_until", { length: 120 }),
|
||||
suspendedBy: promoSuspendedByEnum("suspended_by"),
|
||||
suspendedById: integer("suspended_by_id").references(() => users.id, { onDelete: "set null" }),
|
||||
suspendedAt: timestamp("suspended_at", { withTimezone: true }),
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
});
|
||||
|
||||
// ---- Publications réseaux sociaux (Facebook / LinkedIn) ----
|
||||
export const socialPosts = pgTable(
|
||||
"social_posts",
|
||||
{
|
||||
id: serial("id").primaryKey(),
|
||||
promotionId: integer("promotion_id")
|
||||
.notNull()
|
||||
.references(() => promotions.id, { onDelete: "cascade" }),
|
||||
network: socialNetworkEnum("network").notNull(),
|
||||
status: socialPostStatusEnum("status").notNull(),
|
||||
externalId: varchar("external_id", { length: 200 }),
|
||||
url: text("url"),
|
||||
error: text("error"),
|
||||
postedById: integer("posted_by_id").references(() => users.id, { onDelete: "set null" }),
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
},
|
||||
(t) => ({
|
||||
promoIdx: index("social_posts_promotion_idx").on(t.promotionId),
|
||||
})
|
||||
);
|
||||
|
||||
// ---- Membership requests (demandes d'adhésion) ----
|
||||
export const membershipRequests = pgTable("membership_requests", {
|
||||
id: serial("id").primaryKey(),
|
||||
@@ -218,11 +248,19 @@ export const membersRelations = relations(members, ({ one, many }) => ({
|
||||
imageConsents: many(imageConsents),
|
||||
}));
|
||||
|
||||
export const promotionsRelations = relations(promotions, ({ one }) => ({
|
||||
export const promotionsRelations = relations(promotions, ({ one, many }) => ({
|
||||
member: one(members, {
|
||||
fields: [promotions.memberId],
|
||||
references: [members.id],
|
||||
}),
|
||||
socialPosts: many(socialPosts),
|
||||
}));
|
||||
|
||||
export const socialPostsRelations = relations(socialPosts, ({ one }) => ({
|
||||
promotion: one(promotions, {
|
||||
fields: [socialPosts.promotionId],
|
||||
references: [promotions.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
export const usersRelations = relations(users, ({ one }) => ({
|
||||
@@ -273,6 +311,8 @@ export type Category = typeof categories.$inferSelect;
|
||||
export type Member = typeof members.$inferSelect;
|
||||
export type User = typeof users.$inferSelect;
|
||||
export type Promotion = typeof promotions.$inferSelect;
|
||||
export type SocialPost = typeof socialPosts.$inferSelect;
|
||||
export type SocialNetwork = (typeof socialNetworkEnum.enumValues)[number];
|
||||
export type MembershipRequest = typeof membershipRequests.$inferSelect;
|
||||
export type ContactMessage = typeof contactMessages.$inferSelect;
|
||||
export type ActivityEntry = typeof activityLog.$inferSelect;
|
||||
|
||||
Reference in new issue
Block a user