mirror of
https://github.com/R0m1k3/PleinR.git
synced 2026-10-15 11:20:06 +02:00
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
77 lines
2.7 KiB
TypeScript
77 lines
2.7 KiB
TypeScript
"use client";
|
||
|
||
import { useState } from "react";
|
||
import { addMember } from "../actions";
|
||
|
||
export function AddMemberPanel({
|
||
categories,
|
||
}: {
|
||
categories: { id: number; label: string }[];
|
||
}) {
|
||
const [open, setOpen] = useState(false);
|
||
|
||
return (
|
||
<div style={{ display: "flex", flexDirection: "column", gap: 0 }}>
|
||
<div style={{ display: "flex", justifyContent: "flex-end" }}>
|
||
<button
|
||
onClick={() => setOpen((o) => !o)}
|
||
className="font-display"
|
||
style={{ border: "none", background: "#13324F", color: "#fff", fontWeight: 700, fontSize: 14, padding: "11px 18px", borderRadius: 10, cursor: "pointer" }}
|
||
>
|
||
{open ? "× Fermer" : "+ Ajouter un adhérent"}
|
||
</button>
|
||
</div>
|
||
|
||
{open && (
|
||
<form
|
||
action={async (fd) => {
|
||
await addMember(fd);
|
||
setOpen(false);
|
||
}}
|
||
style={{ background: "#fff", border: "1px solid #e6dcc6", borderRadius: 16, padding: 22, marginTop: 14 }}
|
||
>
|
||
<div className="grid grid-2" style={{ gap: 16 }}>
|
||
<div>
|
||
<label className="field-label">Nom de l'adhérent</label>
|
||
<input name="name" required placeholder="ex : Au Bon Pain" className="field" />
|
||
</div>
|
||
<div>
|
||
<label className="field-label">E-mail</label>
|
||
<input name="email" type="email" placeholder="contact@exemple.fr" className="field" />
|
||
</div>
|
||
<div>
|
||
<label className="field-label">Catégorie</label>
|
||
<select name="categoryId" className="field" defaultValue="">
|
||
<option value="">—</option>
|
||
{categories.map((c) => (
|
||
<option key={c.id} value={c.id}>
|
||
{c.label}
|
||
</option>
|
||
))}
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<label className="field-label">Commune</label>
|
||
<input name="city" placeholder="ex : Pompey" className="field" />
|
||
</div>
|
||
<div>
|
||
<label className="field-label">Statut</label>
|
||
<select name="status" className="field" defaultValue="pending">
|
||
<option value="pending">En attente</option>
|
||
<option value="active">Actif</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<button
|
||
type="submit"
|
||
className="font-display"
|
||
style={{ marginTop: 18, border: "none", background: "#9a6638", color: "#fff", fontWeight: 700, fontSize: 14.5, padding: "12px 22px", borderRadius: 11, cursor: "pointer" }}
|
||
>
|
||
Enregistrer l'adhérent
|
||
</button>
|
||
</form>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|