mirror of
https://github.com/R0m1k3/PleinR.git
synced 2026-10-22 06:38:05 +02:00
- Layout : metadataBase, gabarit de titre « %s · Plein R », description, mots-clés, Open Graph / Twitter, robots, icônes, manifeste, viewport et couleur de thème ; JSON-LD Organization + WebSite (recherche annuaire). - Chaque page publique pose titre, description (≤ 160 caractères) et canonique via pageMetadata() ; l'annuaire ignore ainsi ?q=. Fiche adhérent : titre « Nom · Catégorie à Ville », description dérivée, LocalBusiness avec adresse, contacts et horaires (OpeningHoursSpecification), BreadcrumbList. Annuaire : ItemList des adhérents. Association : BusinessEvent par rencontre à venir. Rencontres passées : fil d'Ariane. - Nouvelles routes : robots.txt, sitemap.xml (pages statiques + adhérents actifs), manifest.webmanifest, image de partage 1200×630 générée avec le logo, page 404 dédiée (noindex). - /backend, /login, /inscription/* et les fiches inactives en noindex. - Landmarks <main> sur toutes les pages publiques, fil d'Ariane en <nav>. - serializeJsonLd() échappe <, > et & : les saisies des adhérents ne peuvent pas fermer le bloc <script>. Tests unitaires dans tests/seo.test.ts. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HDbKbJrPWweXXYSxA8MXW7
95 lines
3.7 KiB
TypeScript
95 lines
3.7 KiB
TypeScript
import type { Metadata } from "next";
|
|
import { redirect } from "next/navigation";
|
|
import { desc, eq } from "drizzle-orm";
|
|
import { getSession } from "@/lib/session";
|
|
import { db } from "@/db";
|
|
import { contactMessages, imageConsents, membershipRequests, promotions } from "@/db/schema";
|
|
import { ImageConsentForm } from "@/components/ImageConsentForm";
|
|
import { isStaff } from "@/lib/rbac";
|
|
import { saveImageConsent } from "./actions";
|
|
import { BackendShell } from "./BackendShell";
|
|
import { NOINDEX } from "@/lib/seo";
|
|
|
|
export const dynamic = "force-dynamic";
|
|
|
|
// Espace privé : jamais indexé, même si une page fuyait hors du middleware.
|
|
export const metadata: Metadata = {
|
|
title: "Espace adhérent",
|
|
robots: NOINDEX,
|
|
};
|
|
|
|
export default async function BackendLayout({
|
|
children,
|
|
}: {
|
|
children: React.ReactNode;
|
|
}) {
|
|
const session = await getSession();
|
|
if (!session?.user) {
|
|
redirect("/login");
|
|
}
|
|
|
|
if (
|
|
session.user.role === "member" &&
|
|
session.user.memberId &&
|
|
!session.user.mustChangePassword
|
|
) {
|
|
const latestConsent = await db
|
|
.select({ id: imageConsents.id })
|
|
.from(imageConsents)
|
|
.where(eq(imageConsents.memberId, session.user.memberId))
|
|
.orderBy(desc(imageConsents.createdAt))
|
|
.limit(1);
|
|
|
|
if (latestConsent.length === 0) {
|
|
return (
|
|
<div style={{ minHeight: "100vh", background: "#F6F2E8", color: "#33291D", fontFamily: "'Public Sans',sans-serif", padding: "clamp(22px, 5vw, 54px)" }}>
|
|
<main style={{ maxWidth: 860, margin: "0 auto" }}>
|
|
<div style={{ display: "flex", alignItems: "center", gap: 14, marginBottom: 24 }}>
|
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
|
<img src="/assets/logo.png" alt="Plein R" style={{ height: 50, width: "auto" }} />
|
|
<div>
|
|
<div className="font-display" style={{ fontSize: 24, fontWeight: 800, color: "#26201a" }}>
|
|
Consentement RGPD
|
|
</div>
|
|
<div style={{ color: "#8c8068", fontSize: 14 }}>Droit à l'image - première connexion adhérent</div>
|
|
</div>
|
|
</div>
|
|
<section style={{ background: "#fff", border: "1px solid #e6dcc6", borderRadius: 20, padding: "clamp(22px, 5vw, 36px)", boxShadow: "0 24px 55px -42px rgba(40,30,15,0.55)" }}>
|
|
<h1 className="font-display" style={{ margin: "0 0 10px", color: "#26201a", fontSize: "clamp(28px, 5vw, 40px)" }}>
|
|
Votre choix sur le droit à l'image
|
|
</h1>
|
|
<p style={{ margin: "0 0 20px", color: "#6c6150", lineHeight: 1.7 }}>
|
|
Avant d'accéder à votre espace, indiquez si Plein R peut utiliser votre image dans
|
|
les supports liés aux rencontres et à la vie de l'association.
|
|
</p>
|
|
<ImageConsentForm saveAction={saveImageConsent} defaultName={session.user.name ?? "Adhérent"} />
|
|
</section>
|
|
</main>
|
|
</div>
|
|
);
|
|
}
|
|
}
|
|
|
|
let pendingCount = 0;
|
|
let inboxCount = 0;
|
|
if (isStaff(session.user.role)) {
|
|
const [pendingRows, newReqs, newMsgs] = await Promise.all([
|
|
db.select({ id: promotions.id }).from(promotions).where(eq(promotions.status, "pending")),
|
|
db.select({ id: membershipRequests.id }).from(membershipRequests).where(eq(membershipRequests.status, "new")),
|
|
db.select({ id: contactMessages.id }).from(contactMessages).where(eq(contactMessages.status, "new")),
|
|
]);
|
|
pendingCount = pendingRows.length;
|
|
inboxCount = newReqs.length + newMsgs.length;
|
|
}
|
|
|
|
return (
|
|
<BackendShell
|
|
user={{ name: session.user.name ?? "Adhérent", role: session.user.role }}
|
|
pendingCount={pendingCount}
|
|
inboxCount={inboxCount}
|
|
>
|
|
{children}
|
|
</BackendShell>
|
|
);
|
|
}
|