TypeScript 50.6%
JavaScript 46.1%
SQL 2.1%
Shell 0.6%
CSS 0.5%
1import Link from "next/link";2import { eq, or, sql, and } from "drizzle-orm";3import { requireUser } from "@/lib/auth/session";4import { db } from "@/db/client";5import { listings, inquiries, messages, viewingRequests, favorites } from "@/db/schema";6import { Stat, Notice } from "@/components/ui";7import { fmtInt } from "@/lib/format";8export const dynamic = "force-dynamic";9export default async function Dashboard() {10 const u = await requireUser(); const d = db();11 const [[l], [m], [v], [f]] = await Promise.all([12 d.select({ total: sql<number>`count(*)::int`, published: sql<number>`sum(case when status='PUBLISHED' then 1 else 0 end)::int`, views: sql<number>`coalesce(sum(views_count),0)::int`, drafts: sql<number>`sum(case when status='DRAFT' then 1 else 0 end)::int` }).from(listings).where(eq(listings.ownerUserId, u.id)),13 d.select({ unread: sql<number>`count(*)::int` }).from(messages).innerJoin(inquiries, eq(inquiries.id, messages.inquiryId)).where(and(or(eq(inquiries.senderUserId, u.id), eq(inquiries.recipientUserId, u.id)), sql`${messages.senderId} <> ${u.id}`, sql`${messages.readAt} is null`)),14 d.select({ pending: sql<number>`count(*)::int` }).from(viewingRequests).where(and(eq(viewingRequests.ownerId, u.id), eq(viewingRequests.status, "REQUESTED"))),15 d.select({ n: sql<number>`count(*)::int` }).from(favorites).where(eq(favorites.userId, u.id)),16 ]);17 return (18 <div className="space-y-6">19 <div><h1 className="text-2xl md:text-3xl font-extrabold tracking-tight">Tableau de bord</h1><p className="text-ink-2 text-sm mt-1">Une seule identité pour chercher, publier et échanger. Vous êtes à la fois visiteur, acheteur ou locataire potentiel et, si vous publiez, propriétaire : aucun profil à choisir.</p></div>20 <div className="grid grid-cols-2 md:grid-cols-4 gap-3">21 <Link href="/membre/annonces"><Stat label="Mes annonces" value={fmtInt(l.total)} sub={`${l.published} publiée${l.published > 1 ? "s" : ""} · ${l.drafts} brouillon${l.drafts > 1 ? "s" : ""}`} /></Link>22 <Link href="/membre/messages"><Stat label="Messages non lus" value={fmtInt(m.unread)} tone={m.unread ? "warn" : undefined} /></Link>23 <Link href="/membre/visites"><Stat label="Visites à confirmer" value={fmtInt(v.pending)} tone={v.pending ? "info" : undefined} /></Link>24 <Link href="/membre/favoris"><Stat label="Favoris" value={fmtInt(f.n)} /></Link>25 </div>26 <div className="grid md:grid-cols-2 gap-3">27 <Link href="/membre/annonces/nouvelle?type=RENT" className="tv-card p-5 hover:border-brand/40"><p className="tv-label">Publier</p><p className="font-bold text-lg mt-1">Un logement à louer</p><p className="text-sm text-ink-2">Appartement, condo, maison, chambre… Loyer, inclusions, disponibilité, photos.</p></Link>28 <Link href="/membre/annonces/nouvelle?type=SALE" className="tv-card p-5 hover:border-brand/40"><p className="tv-label">Publier</p><p className="font-bold text-lg mt-1">Une propriété à vendre</p><p className="text-sm text-ink-2">Maison, condo, plex, chalet, terrain, local commercial. Estimation Toveli affichée à titre indicatif.</p></Link>29 </div>30 <Notice>Vues totales de vos annonces : {fmtInt(l.views)} (comptage de pages, sans suivi des visiteurs). Les demandes et visites passent par la messagerie Toveli : votre courriel et votre téléphone ne sont jamais affichés publiquement.</Notice>31 </div>32 );33}34