Implémente le site Plein R (Accueil + Backend) sur Next.js + Postgres + Docker

Application unique Next.js (App Router, TypeScript) servant :
- Accueil public : héro, recherche, métiers, promotions, mises à l'honneur
  (données issues de Postgres)
- Backend authentifié avec RBAC : tableau de bord, adhérents (CRUD), modération
  des promotions, administrateurs, espace adhérent (publication avec aperçu)

Technique :
- Drizzle ORM + drizzle-kit (migrations versionnées) sur driver pg
- Auth.js v5 (credentials, sessions JWT), rôles admin/moderator/editor/member
- Middleware de protection de /backend + gardes par rôle dans chaque vue
- Server actions pour toutes les mutations (vérif auth + capacité + revalidate)
- Design fidèle aux maquettes (design-system CSS, animations, responsive)

Conteneurisation :
- 1 conteneur applicatif (sortie standalone) + 1 conteneur Postgres externe
- docker-compose avec healthcheck ; migrations + seed au démarrage
- scripts migrate/seed bundlés (esbuild) pour un runtime sans devDeps

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XwfhYHzC9hQWPDnQ2p53GL
This commit is contained in:
Claude committed 2026-06-26 12:06:21 +00:00
commit 7e22ec50d4
45 files changed
+12013

No files matched your search

+131
View File
@@ -0,0 +1,131 @@
import {
pgTable,
pgEnum,
serial,
integer,
text,
varchar,
boolean,
timestamp,
uniqueIndex,
} from "drizzle-orm/pg-core";
import { relations } from "drizzle-orm";
// ---- Enums ----
export const roleEnum = pgEnum("role", ["admin", "moderator", "editor", "member"]);
export const memberStatusEnum = pgEnum("member_status", ["active", "pending"]);
export const promoStatusEnum = pgEnum("promo_status", [
"pending",
"live",
"expired",
"rejected",
]);
export const requestStatusEnum = pgEnum("request_status", [
"new",
"approved",
"rejected",
]);
// ---- Categories (métiers) ----
export const categories = pgTable("categories", {
id: serial("id").primaryKey(),
slug: varchar("slug", { length: 80 }).notNull().unique(),
label: varchar("label", { length: 120 }).notNull(),
accent: varchar("accent", { length: 16 }).notNull().default("#E0A63C"),
tint: varchar("tint", { length: 16 }).notNull().default("#f6efdc"),
sort: integer("sort").notNull().default(0),
});
// ---- Members (adhérents / businesses) ----
export const members = pgTable("members", {
id: serial("id").primaryKey(),
name: varchar("name", { length: 200 }).notNull(),
email: varchar("email", { length: 200 }).notNull(),
categoryId: integer("category_id").references(() => categories.id),
city: varchar("city", { length: 120 }),
address: varchar("address", { length: 240 }),
description: text("description"),
status: memberStatusEnum("status").notNull().default("pending"),
highlighted: boolean("highlighted").notNull().default(false),
logoUrl: text("logo_url"),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
});
// ---- Users (admins + member accounts) ----
export const users = pgTable(
"users",
{
id: serial("id").primaryKey(),
email: varchar("email", { length: 200 }).notNull(),
name: varchar("name", { length: 200 }).notNull(),
passwordHash: varchar("password_hash", { length: 255 }).notNull(),
role: roleEnum("role").notNull().default("member"),
memberId: integer("member_id").references(() => members.id),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
},
(t) => ({
emailIdx: uniqueIndex("users_email_idx").on(t.email),
})
);
// ---- Promotions ----
export const promotions = pgTable("promotions", {
id: serial("id").primaryKey(),
title: varchar("title", { length: 200 }).notNull(),
text: text("text"),
category: varchar("category", { length: 120 }),
badge: varchar("badge", { length: 40 }),
imageUrl: text("image_url"),
memberId: integer("member_id").references(() => members.id),
status: promoStatusEnum("status").notNull().default("pending"),
validUntil: varchar("valid_until", { length: 120 }),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
});
// ---- Membership requests (demandes d'adhésion) ----
export const membershipRequests = pgTable("membership_requests", {
id: serial("id").primaryKey(),
name: varchar("name", { length: 200 }).notNull(),
email: varchar("email", { length: 200 }),
message: text("message"),
status: requestStatusEnum("status").notNull().default("new"),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
});
// ---- Activity log (activité récente) ----
export const activityLog = pgTable("activity_log", {
id: serial("id").primaryKey(),
dot: varchar("dot", { length: 16 }).notNull().default("#2C6FB3"),
message: text("message").notNull(),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
});
// ---- Relations ----
export const membersRelations = relations(members, ({ one, many }) => ({
category: one(categories, {
fields: [members.categoryId],
references: [categories.id],
}),
promotions: many(promotions),
}));
export const promotionsRelations = relations(promotions, ({ one }) => ({
member: one(members, {
fields: [promotions.memberId],
references: [members.id],
}),
}));
export const usersRelations = relations(users, ({ one }) => ({
member: one(members, {
fields: [users.memberId],
references: [members.id],
}),
}));
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 MembershipRequest = typeof membershipRequests.$inferSelect;
export type ActivityEntry = typeof activityLog.$inferSelect;