mirror of
https://github.com/R0m1k3/PleinR.git
synced 2026-10-16 19:56:57 +02:00
Accueil, pied de page et catégories : compteur réel, boutons alignés, référentiel complété
- Accueil : le bandeau « Faites partie de l'aventure » affiche le nombre d'adhérents validés (statut active) au lieu de « plus de 120 » codé en dur. - Pied de page : les boutons Facebook / LinkedIn ont la même largeur (grille à une colonne dimensionnée sur le plus long) et une icône à largeur fixe. - Catégories : référentiel unique dans src/db/categories.ts (104 métiers, dont assurance, bazar/discount, grande distribution, librairie-presse, tabac, vétérinaire, auto-école, etc.), inséré par la migration 0012 avec ON CONFLICT (slug) : les catégories manquantes apparaissent au prochain démarrage, sans écraser un libellé renommé depuis le backend. Le seed et l'action « Ajouter une catégorie » réutilisent ce référentiel et sa palette. - tests/categories.test.ts vérifie unicité des slugs, contraintes de la table et cohérence entre le référentiel et la migration. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HDbKbJrPWweXXYSxA8MXW7
This commit is contained in:
12 files changed
+1718
-108
No files matched your search
@@ -9,6 +9,7 @@ import bcrypt from "bcryptjs";
|
||||
import { signOut } from "@/auth";
|
||||
import { getSession } from "@/lib/session";
|
||||
import { db } from "@/db";
|
||||
import { CATEGORY_PALETTE } from "@/db/categories";
|
||||
import {
|
||||
activityLog,
|
||||
categories,
|
||||
@@ -43,16 +44,6 @@ import { normalizeWebsite } from "@/lib/member-profile";
|
||||
import { SITE_SETTING_DEFAULTS } from "@/lib/site-settings";
|
||||
import type { AppRole } from "@/types/next-auth";
|
||||
|
||||
const CATEGORY_PALETTE = [
|
||||
{ accent: "#E0A63C", tint: "#f6efdc" },
|
||||
{ accent: "#6FB0C6", tint: "#e7f0f3" },
|
||||
{ accent: "#9a6638", tint: "#f4ebda" },
|
||||
{ accent: "#2C6FB3", tint: "#eaf0f6" },
|
||||
{ accent: "#5a7a5a", tint: "#eef0ec" },
|
||||
{ accent: "#7a6f9c", tint: "#efe9f3" },
|
||||
{ accent: "#c98a2e", tint: "#f7efe0" },
|
||||
{ accent: "#3f8aa3", tint: "#e6eff2" },
|
||||
];
|
||||
|
||||
function slugify(input: string): string {
|
||||
return input
|
||||
|
||||
+6
-6
@@ -379,15 +379,15 @@ button {
|
||||
/* Boutons sociaux libellés : plus lisibles que deux icônes isolées, et ils
|
||||
donnent enfin du contenu à la quatrième colonne. */
|
||||
.footer-social {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
/* Sans cela les boutons s'étirent sur toute la colonne et deviennent des
|
||||
pavés vides. */
|
||||
align-items: flex-start;
|
||||
/* Grille à une colonne dimensionnée sur son contenu : tous les boutons
|
||||
prennent la largeur du plus long au lieu de s'étirer sur la colonne ou de
|
||||
former un escalier. */
|
||||
display: inline-grid;
|
||||
grid-template-columns: minmax(150px, max-content);
|
||||
gap: 9px;
|
||||
}
|
||||
.footer-social a {
|
||||
display: inline-flex;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 9px 13px;
|
||||
|
||||
+6
-4
@@ -9,7 +9,7 @@ import { MembersCarousel } from "@/components/MembersCarousel";
|
||||
import { StarField } from "@/components/StarField";
|
||||
import { PromoImage } from "@/components/PromoImage";
|
||||
import { SOCIAL_BRAND, SocialIcon } from "@/components/SocialIcons";
|
||||
import { getLivePromotions, getRotatingActiveMembers } from "@/lib/queries";
|
||||
import { getActiveMemberCount, getLivePromotions, getRotatingActiveMembers } from "@/lib/queries";
|
||||
import { getSiteSettings, socialLinks } from "@/lib/site-settings";
|
||||
import { SITE_DESCRIPTION, SITE_TITLE, pageMetadata } from "@/lib/seo";
|
||||
|
||||
@@ -35,10 +35,11 @@ function badgeColor(badge: string | null) {
|
||||
}
|
||||
|
||||
export default async function AccueilPage() {
|
||||
const [promos, rotatingMembers, settings] = await Promise.all([
|
||||
const [promos, rotatingMembers, settings, memberCount] = await Promise.all([
|
||||
getLivePromotions(6),
|
||||
getRotatingActiveMembers(),
|
||||
getSiteSettings(),
|
||||
getActiveMemberCount(),
|
||||
]);
|
||||
const socials = socialLinks(settings);
|
||||
|
||||
@@ -450,8 +451,9 @@ export default async function AccueilPage() {
|
||||
Faites partie de l'aventure Plein R
|
||||
</h2>
|
||||
<p style={{ margin: "0 auto 22px", fontSize: 15.5, color: "#f4e6d3", maxWidth: 480 }}>
|
||||
Rejoignez plus de 120 commerçants et entreprises qui font réseau, se rencontrent et réussissent
|
||||
ensemble.
|
||||
{memberCount > 0
|
||||
? `Rejoignez les ${memberCount} commerçants et entreprises adhérents qui font réseau, se rencontrent et réussissent ensemble.`
|
||||
: "Rejoignez les commerçants et entreprises qui font réseau, se rencontrent et réussissent ensemble."}
|
||||
</p>
|
||||
<MembershipModalButton
|
||||
label="Devenir adhérent"
|
||||
|
||||
@@ -61,7 +61,7 @@ export async function SiteFooter() {
|
||||
rel="noopener noreferrer"
|
||||
aria-label={`Plein R sur ${s.label}`}
|
||||
>
|
||||
<span style={{ color: SOCIAL_BRAND[s.network], display: "inline-flex" }}>
|
||||
<span style={{ color: SOCIAL_BRAND[s.network], display: "inline-flex", width: 17, flexShrink: 0 }}>
|
||||
<SocialIcon network={s.network} size={17} />
|
||||
</span>
|
||||
{s.label}
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
/**
|
||||
* Référentiel des catégories de métiers.
|
||||
*
|
||||
* Source unique pour le seed, la migration d'insertion (drizzle/0012_*) et le
|
||||
* bouton « Ajouter » du backend. Les slugs sont stables : ils servent de clé
|
||||
* d'unicité (`ON CONFLICT (slug)`) et ne doivent jamais être renommés, sinon la
|
||||
* catégorie serait dupliquée. Un libellé renommé depuis le backend est conservé :
|
||||
* la migration ne touche qu'à l'ordre d'affichage (`sort`).
|
||||
*/
|
||||
|
||||
export const CATEGORY_PALETTE = [
|
||||
{ accent: "#E0A63C", tint: "#f6efdc" },
|
||||
{ accent: "#6FB0C6", tint: "#e7f0f3" },
|
||||
{ accent: "#9a6638", tint: "#f4ebda" },
|
||||
{ accent: "#2C6FB3", tint: "#eaf0f6" },
|
||||
{ accent: "#5a7a5a", tint: "#eef0ec" },
|
||||
{ accent: "#7a6f9c", tint: "#efe9f3" },
|
||||
{ accent: "#c98a2e", tint: "#f7efe0" },
|
||||
{ accent: "#3f8aa3", tint: "#e6eff2" },
|
||||
] as const;
|
||||
|
||||
export type CategoryDefinition = { slug: string; label: string };
|
||||
|
||||
// NB : les slugs historiques (alimentation, boulangerie, restauration, mode-beaute,
|
||||
// artisanat, services, sante) sont conservés pour ne pas casser les liens existants.
|
||||
export const CATEGORY_REFERENTIAL: CategoryDefinition[] = [
|
||||
// ---- Bouche & alimentation ----
|
||||
{ slug: "alimentation", label: "Alimentation / Épicerie" },
|
||||
{ slug: "epicerie-fine", label: "Épicerie fine – Produits du terroir" },
|
||||
{ slug: "supermarche", label: "Supermarché – Grande distribution" },
|
||||
{ slug: "boulangerie", label: "Boulangerie – Pâtisserie" },
|
||||
{ slug: "boucherie-charcuterie", label: "Boucherie – Charcuterie" },
|
||||
{ slug: "poissonnerie", label: "Poissonnerie" },
|
||||
{ slug: "primeur", label: "Primeur – Fruits & Légumes" },
|
||||
{ slug: "fromagerie", label: "Fromagerie – Crèmerie" },
|
||||
{ slug: "cave", label: "Cave – Vins & Spiritueux" },
|
||||
{ slug: "brasserie-artisanale", label: "Brasserie artisanale – Boissons" },
|
||||
{ slug: "chocolaterie", label: "Chocolaterie – Confiserie" },
|
||||
{ slug: "glacier-salon-de-the", label: "Glacier – Salon de thé" },
|
||||
{ slug: "traiteur", label: "Traiteur" },
|
||||
{ slug: "restauration", label: "Restauration" },
|
||||
{ slug: "pizzeria", label: "Pizzeria" },
|
||||
{ slug: "restauration-rapide", label: "Restauration rapide – Food truck" },
|
||||
{ slug: "cafe-bar", label: "Café – Bar – Brasserie" },
|
||||
// ---- Mode, beauté & soin ----
|
||||
{ slug: "mode-beaute", label: "Mode & Beauté" },
|
||||
{ slug: "mode-vetements", label: "Mode & Vêtements" },
|
||||
{ slug: "lingerie", label: "Lingerie" },
|
||||
{ slug: "vetements-enfants", label: "Vêtements enfants" },
|
||||
{ slug: "chaussures-maroquinerie", label: "Chaussures – Maroquinerie" },
|
||||
{ slug: "bijouterie", label: "Bijouterie – Horlogerie" },
|
||||
{ slug: "coiffure", label: "Coiffure" },
|
||||
{ slug: "barbier", label: "Barbier" },
|
||||
{ slug: "esthetique", label: "Esthétique – Institut" },
|
||||
{ slug: "onglerie", label: "Onglerie – Prothésie ongulaire" },
|
||||
{ slug: "parfumerie", label: "Parfumerie" },
|
||||
{ slug: "bien-etre", label: "Bien-être – Spa – Massage" },
|
||||
{ slug: "optique", label: "Optique" },
|
||||
{ slug: "tatouage", label: "Tatouage – Piercing" },
|
||||
// ---- Maison, équipement & décoration ----
|
||||
{ slug: "bazar-discount", label: "Bazar – Discount – Équipement de la maison" },
|
||||
{ slug: "decoration", label: "Décoration – Ameublement" },
|
||||
{ slug: "cuisiniste", label: "Cuisines – Salles de bain" },
|
||||
{ slug: "literie", label: "Literie" },
|
||||
{ slug: "arts-de-la-table", label: "Arts de la table – Cadeaux" },
|
||||
{ slug: "electromenager-multimedia", label: "Électroménager – Multimédia" },
|
||||
{ slug: "fleuriste", label: "Fleuriste" },
|
||||
{ slug: "jardinerie", label: "Jardinerie – Motoculture" },
|
||||
{ slug: "bricolage-jardinage", label: "Bricolage – Jardinage" },
|
||||
{ slug: "animalerie", label: "Animalerie – Services animaliers" },
|
||||
// ---- Artisanat & bâtiment ----
|
||||
{ slug: "artisanat", label: "Artisanat" },
|
||||
{ slug: "batiment", label: "Bâtiment – Rénovation" },
|
||||
{ slug: "architecte", label: "Architecte – Maître d'œuvre" },
|
||||
{ slug: "maconnerie", label: "Maçonnerie – Gros œuvre" },
|
||||
{ slug: "couverture-toiture", label: "Couverture – Toiture – Zinguerie" },
|
||||
{ slug: "plomberie", label: "Plomberie – Chauffage" },
|
||||
{ slug: "electricite", label: "Électricité" },
|
||||
{ slug: "isolation-energie", label: "Isolation – Énergies renouvelables" },
|
||||
{ slug: "menuiserie", label: "Menuiserie – Ébénisterie" },
|
||||
{ slug: "serrurerie-metallerie", label: "Serrurerie – Métallerie" },
|
||||
{ slug: "carrelage-sols", label: "Carrelage – Revêtements de sols" },
|
||||
{ slug: "peinture", label: "Peinture – Décoration intérieure" },
|
||||
{ slug: "paysagiste", label: "Paysagiste – Espaces verts" },
|
||||
{ slug: "depannage-multiservices", label: "Dépannage – Multiservices" },
|
||||
// ---- Auto & mobilité ----
|
||||
{ slug: "automobile", label: "Automobile – Garage" },
|
||||
{ slug: "carrosserie", label: "Carrosserie" },
|
||||
{ slug: "controle-technique", label: "Contrôle technique" },
|
||||
{ slug: "station-service-lavage", label: "Station-service – Lavage auto" },
|
||||
{ slug: "location-vehicules", label: "Location de véhicules" },
|
||||
{ slug: "cycles-motos", label: "Cycles – Motos" },
|
||||
{ slug: "auto-ecole", label: "Auto-école" },
|
||||
{ slug: "taxi-vtc", label: "Taxi – VTC" },
|
||||
// ---- Santé ----
|
||||
{ slug: "sante", label: "Santé – Pharmacie" },
|
||||
{ slug: "medical", label: "Médical – Paramédical" },
|
||||
{ slug: "audioprothese", label: "Audioprothèse" },
|
||||
{ slug: "veterinaire", label: "Vétérinaire" },
|
||||
// ---- Banque, assurance & immobilier ----
|
||||
{ slug: "banque-assurance", label: "Banque – Assurance" },
|
||||
{ slug: "assurance", label: "Assurance – Mutuelle – Prévoyance" },
|
||||
{ slug: "courtier-credit", label: "Courtier – Crédit – Financement" },
|
||||
{ slug: "immobilier", label: "Immobilier" },
|
||||
{ slug: "diagnostic-immobilier", label: "Diagnostic immobilier – Expertise" },
|
||||
// ---- Services aux particuliers ----
|
||||
{ slug: "services", label: "Services" },
|
||||
{ slug: "services-a-la-personne", label: "Services à la personne – Aide à domicile" },
|
||||
{ slug: "pressing-retouches", label: "Pressing – Retouches – Cordonnerie" },
|
||||
{ slug: "photographe", label: "Photographe – Vidéo" },
|
||||
{ slug: "pompes-funebres", label: "Pompes funèbres" },
|
||||
{ slug: "agence-voyages", label: "Agence de voyages" },
|
||||
{ slug: "demenagement", label: "Déménagement – Garde-meubles" },
|
||||
// ---- Services aux entreprises ----
|
||||
{ slug: "comptabilite", label: "Comptabilité – Gestion" },
|
||||
{ slug: "juridique", label: "Juridique – Notaire" },
|
||||
{ slug: "conseil", label: "Conseil aux entreprises" },
|
||||
{ slug: "communication", label: "Communication – Web – Marketing" },
|
||||
{ slug: "imprimerie-enseignes", label: "Imprimerie – Enseignes – Signalétique" },
|
||||
{ slug: "informatique", label: "Informatique – Téléphonie" },
|
||||
{ slug: "nettoyage", label: "Nettoyage – Entretien" },
|
||||
{ slug: "transport", label: "Transport – Logistique" },
|
||||
{ slug: "securite", label: "Sécurité" },
|
||||
{ slug: "coworking", label: "Coworking – Bureaux" },
|
||||
{ slug: "interim-rh", label: "Intérim – Ressources humaines" },
|
||||
{ slug: "recyclage-dechets", label: "Recyclage – Gestion des déchets" },
|
||||
// ---- Culture, loisirs & formation ----
|
||||
{ slug: "librairie-papeterie", label: "Librairie – Papeterie – Presse" },
|
||||
{ slug: "tabac-presse", label: "Tabac – Presse – Loto" },
|
||||
{ slug: "jouets-puericulture", label: "Jouets – Puériculture" },
|
||||
{ slug: "sport-loisirs", label: "Sport – Loisirs" },
|
||||
{ slug: "articles-de-sport", label: "Articles de sport" },
|
||||
{ slug: "fitness", label: "Salle de sport – Fitness" },
|
||||
{ slug: "culture-arts", label: "Culture – Arts" },
|
||||
{ slug: "education-formation", label: "Éducation – Formation" },
|
||||
{ slug: "tourisme-hotellerie", label: "Tourisme – Hôtellerie" },
|
||||
{ slug: "evenementiel", label: "Événementiel" },
|
||||
// ---- Entreprise, production & institutions ----
|
||||
{ slug: "industrie", label: "Industrie – Production" },
|
||||
{ slug: "agriculture", label: "Agriculture – Producteur local" },
|
||||
{ slug: "partenaire-institutionnel", label: "Partenaire institutionnel – Collectivité" },
|
||||
];
|
||||
|
||||
/** Lignes prêtes à insérer : couleur tournante et ordre d'affichage. */
|
||||
export function categoryRows() {
|
||||
return CATEGORY_REFERENTIAL.map((c, i) => ({
|
||||
...c,
|
||||
...CATEGORY_PALETTE[i % CATEGORY_PALETTE.length],
|
||||
sort: i + 1,
|
||||
}));
|
||||
}
|
||||
+5
-87
@@ -5,6 +5,7 @@ import bcrypt from "bcryptjs";
|
||||
import { randomInt } from "node:crypto";
|
||||
import { eq } from "drizzle-orm";
|
||||
import * as schema from "./schema";
|
||||
import { categoryRows } from "./categories";
|
||||
import {
|
||||
DEMO_PASSWORD,
|
||||
demoActivity,
|
||||
@@ -40,93 +41,10 @@ async function main() {
|
||||
|
||||
console.log("Seeding database…");
|
||||
|
||||
// ---- Categories (métiers) — liste complète ----
|
||||
const PALETTE = [
|
||||
{ accent: "#E0A63C", tint: "#f6efdc" },
|
||||
{ accent: "#6FB0C6", tint: "#e7f0f3" },
|
||||
{ accent: "#9a6638", tint: "#f4ebda" },
|
||||
{ accent: "#2C6FB3", tint: "#eaf0f6" },
|
||||
{ accent: "#5a7a5a", tint: "#eef0ec" },
|
||||
{ accent: "#7a6f9c", tint: "#efe9f3" },
|
||||
{ accent: "#c98a2e", tint: "#f7efe0" },
|
||||
{ accent: "#3f8aa3", tint: "#e6eff2" },
|
||||
];
|
||||
// NB: les slugs existants (alimentation, boulangerie, restauration, mode-beaute,
|
||||
// artisanat, services, sante) sont conservés pour ne pas casser les liens existants.
|
||||
const categoryList: { slug: string; label: string }[] = [
|
||||
// Bouche & alimentation
|
||||
{ slug: "alimentation", label: "Alimentation / Épicerie" },
|
||||
{ slug: "boulangerie", label: "Boulangerie – Pâtisserie" },
|
||||
{ slug: "boucherie-charcuterie", label: "Boucherie – Charcuterie" },
|
||||
{ slug: "poissonnerie", label: "Poissonnerie" },
|
||||
{ slug: "primeur", label: "Primeur – Fruits & Légumes" },
|
||||
{ slug: "fromagerie", label: "Fromagerie – Crèmerie" },
|
||||
{ slug: "cave", label: "Cave – Vins & Spiritueux" },
|
||||
{ slug: "chocolaterie", label: "Chocolaterie – Confiserie" },
|
||||
{ slug: "traiteur", label: "Traiteur" },
|
||||
{ slug: "restauration", label: "Restauration" },
|
||||
{ slug: "restauration-rapide", label: "Restauration rapide" },
|
||||
{ slug: "cafe-bar", label: "Café – Bar – Brasserie" },
|
||||
// Mode, beauté & soin
|
||||
{ slug: "mode-beaute", label: "Mode & Beauté" },
|
||||
{ slug: "mode-vetements", label: "Mode & Vêtements" },
|
||||
{ slug: "chaussures-maroquinerie", label: "Chaussures – Maroquinerie" },
|
||||
{ slug: "bijouterie", label: "Bijouterie – Horlogerie" },
|
||||
{ slug: "coiffure", label: "Coiffure" },
|
||||
{ slug: "esthetique", label: "Esthétique – Institut" },
|
||||
{ slug: "parfumerie", label: "Parfumerie" },
|
||||
{ slug: "bien-etre", label: "Bien-être – Spa – Massage" },
|
||||
{ slug: "optique", label: "Optique" },
|
||||
{ slug: "tatouage", label: "Tatouage – Piercing" },
|
||||
// Maison, déco & artisanat / bâtiment
|
||||
{ slug: "artisanat", label: "Artisanat" },
|
||||
{ slug: "decoration", label: "Décoration – Ameublement" },
|
||||
{ slug: "fleuriste", label: "Fleuriste" },
|
||||
{ slug: "bricolage-jardinage", label: "Bricolage – Jardinage" },
|
||||
{ slug: "batiment", label: "Bâtiment – Rénovation" },
|
||||
{ slug: "plomberie", label: "Plomberie – Chauffage" },
|
||||
{ slug: "electricite", label: "Électricité" },
|
||||
{ slug: "menuiserie", label: "Menuiserie – Ébénisterie" },
|
||||
{ slug: "peinture", label: "Peinture – Décoration intérieure" },
|
||||
{ slug: "paysagiste", label: "Paysagiste – Espaces verts" },
|
||||
// Auto & mobilité
|
||||
{ slug: "automobile", label: "Automobile – Garage" },
|
||||
{ slug: "carrosserie", label: "Carrosserie" },
|
||||
{ slug: "cycles-motos", label: "Cycles – Motos" },
|
||||
// Santé
|
||||
{ slug: "sante", label: "Santé – Pharmacie" },
|
||||
{ slug: "medical", label: "Médical – Paramédical" },
|
||||
{ slug: "audioprothese", label: "Audioprothèse" },
|
||||
// Services aux particuliers & entreprises
|
||||
{ slug: "services", label: "Services" },
|
||||
{ slug: "banque-assurance", label: "Banque – Assurance" },
|
||||
{ slug: "immobilier", label: "Immobilier" },
|
||||
{ slug: "comptabilite", label: "Comptabilité – Gestion" },
|
||||
{ slug: "juridique", label: "Juridique – Notaire" },
|
||||
{ slug: "communication", label: "Communication – Web – Marketing" },
|
||||
{ slug: "informatique", label: "Informatique – Téléphonie" },
|
||||
{ slug: "nettoyage", label: "Nettoyage – Entretien" },
|
||||
{ slug: "transport", label: "Transport – Logistique" },
|
||||
{ slug: "securite", label: "Sécurité" },
|
||||
{ slug: "coworking", label: "Coworking – Bureaux" },
|
||||
{ slug: "interim-rh", label: "Intérim – Ressources humaines" },
|
||||
// Loisirs, culture & formation
|
||||
{ slug: "sport-loisirs", label: "Sport – Loisirs" },
|
||||
{ slug: "fitness", label: "Salle de sport – Fitness" },
|
||||
{ slug: "culture-arts", label: "Culture – Arts" },
|
||||
{ slug: "education-formation", label: "Éducation – Formation" },
|
||||
{ slug: "tourisme-hotellerie", label: "Tourisme – Hôtellerie" },
|
||||
{ slug: "evenementiel", label: "Événementiel" },
|
||||
{ slug: "animalerie", label: "Animalerie – Services animaliers" },
|
||||
// Entreprise & production
|
||||
{ slug: "industrie", label: "Industrie – Production" },
|
||||
{ slug: "agriculture", label: "Agriculture – Producteur local" },
|
||||
];
|
||||
const categoryData = categoryList.map((c, i) => ({
|
||||
...c,
|
||||
...PALETTE[i % PALETTE.length],
|
||||
sort: i + 1,
|
||||
}));
|
||||
// ---- Categories (métiers) : référentiel partagé (src/db/categories.ts) ----
|
||||
// La migration 0012 insère déjà ce référentiel ; le seed le rejoue par sécurité
|
||||
// pour une base créée sans migration, sans jamais écraser un libellé existant.
|
||||
const categoryData = categoryRows();
|
||||
|
||||
for (const c of categoryData) {
|
||||
await db.insert(categories).values(c).onConflictDoNothing({ target: categories.slug });
|
||||
|
||||
@@ -165,3 +165,12 @@ export async function getHighlightedMembers(limit = 3) {
|
||||
.limit(limit);
|
||||
return rows;
|
||||
}
|
||||
|
||||
/** Nombre d'adhérents validés (statut `active`) : affiché sur l'accueil. */
|
||||
export async function getActiveMemberCount(): Promise<number> {
|
||||
const [row] = await db
|
||||
.select({ total: count(members.id) })
|
||||
.from(members)
|
||||
.where(eq(members.status, "active"));
|
||||
return Number(row?.total ?? 0);
|
||||
}
|
||||
Reference in new issue
Block a user