Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Dependencies
**/node_modules/

# Next.js build output
**/.next/
**/out/

# Lock files (optional — retirer si tu veux les versionner)
**/package-lock.json

# Next.js type declarations (auto-générés)
**/next-env.d.ts

# Previews screenshots (local only)
previews/

# Env files
**/.env
**/.env.local
**/.env*.local

# OS
.DS_Store
Thumbs.db
22 changes: 22 additions & 0 deletions templates/admin-dashboard/app/(dashboard)/contacts/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import type { Metadata } from "next";
import ContactsTable from "@/components/ui/ContactsTable";
import { MOCK_CONTACTS } from "@/lib/mock-data";

export const metadata: Metadata = { title: "Contacts" };

export default function ContactsPage() {
return (
<div className="space-y-6 max-w-7xl mx-auto">
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-slate-900">Contacts</h1>
<p className="text-sm text-slate-400 mt-1">{MOCK_CONTACTS.length} demandes au total</p>
</div>
<button className="inline-flex items-center gap-2 rounded-xl bg-brand-600 px-4 py-2.5 text-sm font-semibold text-white hover:bg-brand-700 transition-colors">
Exporter CSV
</button>
</div>
<ContactsTable contacts={MOCK_CONTACTS} />
</div>
);
}
14 changes: 14 additions & 0 deletions templates/admin-dashboard/app/(dashboard)/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import Sidebar from "@/components/layout/Sidebar";
import Topbar from "@/components/layout/Topbar";

export default function DashboardLayout({ children }: { children: React.ReactNode }) {
return (
<div className="min-h-screen bg-slate-50">
<Sidebar />
<div className="lg:pl-64 flex flex-col min-h-screen">
<Topbar />
<main className="flex-1 p-6">{children}</main>
</div>
</div>
);
}
62 changes: 62 additions & 0 deletions templates/admin-dashboard/app/(dashboard)/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import StatsCards from "@/components/ui/StatsCards";
import WeeklyChart from "@/components/ui/WeeklyChart";
import ContactsTable from "@/components/ui/ContactsTable";
import { MOCK_CONTACTS, MOCK_STATS } from "@/lib/mock-data";

export default function DashboardPage() {
const urgent = MOCK_CONTACTS.filter((c) => c.urgency && c.status !== "done" && c.status !== "archived");

return (
<div className="space-y-6 max-w-7xl mx-auto">
<div>
<h1 className="text-2xl font-bold text-slate-900">Tableau de bord</h1>
<p className="text-slate-500 text-sm mt-1">Bienvenue, voici un résumé de l'activité.</p>
</div>

{/* Urgent alert */}
{urgent.length > 0 && (
<div className="flex items-center gap-3 rounded-2xl bg-orange-50 border border-orange-100 p-4">
<span className="flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-xl bg-orange-100 text-orange-600">
</span>
<div>
<p className="text-sm font-semibold text-orange-800">
{urgent.length} demande{urgent.length > 1 ? "s" : ""} urgente{urgent.length > 1 ? "s" : ""} en attente
</p>
<p className="text-xs text-orange-600">Traiter en priorité</p>
</div>
</div>
)}

<StatsCards stats={MOCK_STATS} />

<div className="grid lg:grid-cols-3 gap-6">
<div className="lg:col-span-2">
<WeeklyChart />
</div>
<div className="stat-card">
<h3 className="font-semibold text-slate-900 mb-4">Sources de contact</h3>
<div className="space-y-3">
{[
{ label: "Formulaire contact", pct: 52, color: "bg-brand-600" },
{ label: "Google / SEO", pct: 28, color: "bg-green-500" },
{ label: "Bouche à oreille", pct: 12, color: "bg-purple-500" },
{ label: "Réseaux sociaux", pct: 8, color: "bg-orange-500" },
].map((s) => (
<div key={s.label}>
<div className="flex justify-between text-xs text-slate-500 mb-1">
<span>{s.label}</span><span>{s.pct}%</span>
</div>
<div className="h-2 w-full rounded-full bg-slate-100 overflow-hidden">
<div className={`h-full rounded-full ${s.color}`} style={{ width: `${s.pct}%` }} />
</div>
</div>
))}
</div>
</div>
</div>

<ContactsTable contacts={MOCK_CONTACTS} />
</div>
);
}
24 changes: 24 additions & 0 deletions templates/admin-dashboard/app/globals.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

@layer base {
html { -webkit-font-smoothing: antialiased; }
body { @apply bg-slate-50 text-slate-800; }
*:focus-visible { @apply outline-2 outline-offset-2 outline-brand-500; }
}

@layer components {
.stat-card {
@apply rounded-2xl bg-white border border-slate-100 p-6 shadow-sm;
}
.btn-sm {
@apply inline-flex items-center gap-1.5 rounded-lg px-3 py-1.5 text-xs font-medium transition-colors duration-150;
}
.sidebar-link {
@apply flex items-center gap-3 rounded-xl px-3 py-2.5 text-sm font-medium text-slate-400 transition-colors duration-150 hover:bg-slate-800 hover:text-white;
}
.sidebar-link-active {
@apply flex items-center gap-3 rounded-xl px-3 py-2.5 text-sm font-medium bg-brand-600 text-white;
}
}
20 changes: 20 additions & 0 deletions templates/admin-dashboard/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import type { Metadata, Viewport } from "next";
import { Inter } from "next/font/google";
import "./globals.css";

const inter = Inter({ subsets: ["latin"], display: "swap", variable: "--font-inter" });
export const viewport: Viewport = { themeColor: "#4f46e5" };

export const metadata: Metadata = {
title: { default: "Dashboard Admin", template: "%s | Admin" },
description: "Interface d'administration — gestion des contacts et demandes clients",
robots: { index: false, follow: false },
};

export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="fr" className={inter.variable}>
<body>{children}</body>
</html>
);
}
66 changes: 66 additions & 0 deletions templates/admin-dashboard/components/layout/Sidebar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
"use client";

import { usePathname } from "next/navigation";
import Link from "next/link";
import {
LayoutDashboard, Users, Settings, LogOut, Wrench, Bell, BarChart3,
} from "lucide-react";
import { cn } from "@/lib/utils";

const NAV = [
{ label: "Dashboard", href: "/", icon: LayoutDashboard },
{ label: "Contacts", href: "/contacts", icon: Users },
{ label: "Statistiques", href: "/stats", icon: BarChart3 },
{ label: "Paramètres", href: "/settings", icon: Settings },
];

export default function Sidebar() {
const path = usePathname();

return (
<aside className="hidden lg:flex flex-col w-64 min-h-screen bg-slate-900 px-4 py-6 fixed left-0 top-0 bottom-0 z-20">
{/* Logo */}
<div className="flex items-center gap-2.5 px-3 mb-8">
<span className="flex h-8 w-8 items-center justify-center rounded-xl bg-brand-600">
<Wrench className="h-4 w-4 text-white" />
</span>
<div>
<p className="text-sm font-bold text-white leading-none">Admin Panel</p>
<p className="text-xs text-slate-500 mt-0.5">Dupont Plomberie</p>
</div>
</div>

{/* Nav */}
<nav className="flex-1 space-y-1">
<p className="px-3 mb-2 text-xs font-semibold uppercase tracking-widest text-slate-600">Navigation</p>
{NAV.map(({ label, href, icon: Icon }) => {
const active = path === href;
return (
<Link
key={href}
href={href}
className={active ? "sidebar-link-active" : "sidebar-link"}
>
<Icon className="h-4 w-4 flex-shrink-0" />
{label}
</Link>
);
})}
</nav>

{/* Bottom */}
<div className="space-y-1 border-t border-slate-800 pt-4">
<div className="flex items-center gap-3 rounded-xl px-3 py-2.5">
<div className="h-8 w-8 rounded-full bg-brand-600 flex items-center justify-center text-white text-xs font-bold flex-shrink-0">JD</div>
<div className="min-w-0">
<p className="text-sm font-medium text-white truncate">Jean Dupont</p>
<p className="text-xs text-slate-500 truncate">Admin</p>
</div>
</div>
<button className="sidebar-link w-full text-red-400 hover:text-red-300 hover:bg-red-900/20">
<LogOut className="h-4 w-4" /> Déconnexion
</button>
</div>
</aside>
);
}
80 changes: 80 additions & 0 deletions templates/admin-dashboard/components/layout/Topbar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
"use client";

import { useState } from "react";
import { Bell, Search, Menu, X } from "lucide-react";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { cn } from "@/lib/utils";

const NAV = [
{ label: "Dashboard", href: "/" },
{ label: "Contacts", href: "/contacts" },
];

const BREADCRUMBS: Record<string, string> = {
"/": "Dashboard",
"/contacts": "Contacts",
"/stats": "Statistiques",
"/settings": "Paramètres",
};

type Props = { notifCount?: number };

export default function Topbar({ notifCount = 3 }: Props) {
const [mobileOpen, setMobileOpen] = useState(false);
const path = usePathname();

return (
<>
<header className="h-16 bg-white border-b border-slate-100 flex items-center justify-between px-6 gap-4">
{/* Mobile menu toggle */}
<button onClick={() => setMobileOpen((v) => !v)} className="lg:hidden p-1.5 rounded-lg text-slate-500 hover:bg-slate-100">
{mobileOpen ? <X className="h-5 w-5" /> : <Menu className="h-5 w-5" />}
</button>

{/* Breadcrumb */}
<div className="hidden lg:flex items-center gap-2 text-sm text-slate-400">
<span className="text-slate-600 font-medium">{BREADCRUMBS[path] ?? "—"}</span>
</div>

{/* Search */}
<div className="flex-1 max-w-sm hidden sm:block">
<div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-slate-400" />
<input
type="search"
placeholder="Rechercher un contact..."
className="w-full rounded-xl border border-slate-200 bg-slate-50 pl-9 pr-4 py-2 text-sm placeholder:text-slate-400 focus:border-brand-500 focus:outline-none focus:ring-2 focus:ring-brand-500/20"
/>
</div>
</div>

{/* Right */}
<div className="flex items-center gap-3">
<button className="relative p-2 rounded-xl text-slate-500 hover:bg-slate-100 transition-colors">
<Bell className="h-5 w-5" />
{notifCount > 0 && (
<span className="absolute top-1.5 right-1.5 h-2 w-2 rounded-full bg-red-500 ring-2 ring-white" />
)}
</button>
<div className="h-8 w-8 rounded-full bg-brand-600 flex items-center justify-center text-white text-xs font-bold">JD</div>
</div>
</header>

{/* Mobile nav drawer */}
{mobileOpen && (
<div className="lg:hidden fixed inset-0 z-30 bg-slate-900/50" onClick={() => setMobileOpen(false)}>
<nav className="absolute left-0 top-0 bottom-0 w-64 bg-slate-900 p-6" onClick={(e) => e.stopPropagation()}>
{NAV.map((l) => (
<Link key={l.href} href={l.href} onClick={() => setMobileOpen(false)}
className={cn("block rounded-xl px-4 py-3 text-sm font-medium mb-1", path === l.href ? "bg-brand-600 text-white" : "text-slate-400 hover:bg-slate-800 hover:text-white")}
>
{l.label}
</Link>
))}
</nav>
</div>
)}
</>
);
}
Loading