Ajout du système d'administration avec token unique et QR code
- Implémentation complète du système d'administration (/admin) - Gestion des clients avec base de données JSON - Génération de token unique et QR code pour chaque client - Intégration des données client dans l'application (bungalow, WiFi, message) - Amélioration du composant WifiCard avec fallback de copie - Optimisation du hook useClientData pour chargement immédiat - Ajout de la variable d'environnement ADMIN_PASSWORD
This commit is contained in:
@ -1,8 +1,10 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
import Layout from "@/components/layout/Layout";
|
import Layout from "@/components/layout/Layout";
|
||||||
import { config } from "@/lib/config";
|
|
||||||
import WifiCard from "@/components/accueil/WifiCard";
|
import WifiCard from "@/components/accueil/WifiCard";
|
||||||
import Logo from "@/components/Logo";
|
import Logo from "@/components/Logo";
|
||||||
|
import { useClientData } from "@/lib/hooks/useClientData";
|
||||||
|
|
||||||
const WeatherWidget = dynamic(() => import("@/components/accueil/WeatherWidget"), {
|
const WeatherWidget = dynamic(() => import("@/components/accueil/WeatherWidget"), {
|
||||||
loading: () => <div className="h-32 bg-gray-100 rounded-2xl animate-pulse" />,
|
loading: () => <div className="h-32 bg-gray-100 rounded-2xl animate-pulse" />,
|
||||||
@ -10,6 +12,18 @@ const WeatherWidget = dynamic(() => import("@/components/accueil/WeatherWidget")
|
|||||||
});
|
});
|
||||||
|
|
||||||
export default function AccueilPage() {
|
export default function AccueilPage() {
|
||||||
|
const { bungalowNumber, gerantMessage, loading } = useClientData();
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<Layout>
|
||||||
|
<div className="px-4 py-6 space-y-6">
|
||||||
|
<div className="h-32 bg-gray-100 rounded-2xl animate-pulse" />
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<Layout>
|
||||||
<div className="px-4 py-6 space-y-6">
|
<div className="px-4 py-6 space-y-6">
|
||||||
@ -19,7 +33,7 @@ export default function AccueilPage() {
|
|||||||
Ia Ora Na
|
Ia Ora Na
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-lg text-gray-700">
|
<p className="text-lg text-gray-700">
|
||||||
Bienvenue au Bungalow {config.bungalowNumber}
|
Bienvenue au Bungalow {bungalowNumber}
|
||||||
</p>
|
</p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@ -32,7 +46,7 @@ export default function AccueilPage() {
|
|||||||
Le mot du gérant
|
Le mot du gérant
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-gray-700 leading-relaxed">
|
<p className="text-gray-700 leading-relaxed">
|
||||||
{config.gerantMessage}
|
{gerantMessage}
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
85
app/admin/login/page.tsx
Normal file
85
app/admin/login/page.tsx
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
|
||||||
|
import Logo from "@/components/Logo";
|
||||||
|
|
||||||
|
export default function AdminLoginPage() {
|
||||||
|
const [password, setPassword] = useState("");
|
||||||
|
const [error, setError] = useState("");
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setError("");
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
// Vérification simple côté client (la vraie vérification se fait côté serveur)
|
||||||
|
// Pour l'instant, on stocke le mot de passe dans localStorage
|
||||||
|
localStorage.setItem("adminPassword", password);
|
||||||
|
|
||||||
|
// Test avec une requête API
|
||||||
|
try {
|
||||||
|
const response = await fetch("/api/admin/clients", {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${password}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
router.push("/admin");
|
||||||
|
} else {
|
||||||
|
setError("Mot de passe incorrect");
|
||||||
|
localStorage.removeItem("adminPassword");
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setError("Erreur de connexion");
|
||||||
|
localStorage.removeItem("adminPassword");
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-background flex items-center justify-center px-4">
|
||||||
|
<Card className="w-full max-w-md">
|
||||||
|
<CardHeader className="text-center">
|
||||||
|
<div className="flex justify-center mb-4">
|
||||||
|
<Logo size={100} />
|
||||||
|
</div>
|
||||||
|
<CardTitle>Administration</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Mot de passe
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
className="w-full px-4 py-2 border border-gray-300 rounded-xl focus:ring-2 focus:ring-primary focus:border-transparent"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-xl text-sm">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Button type="submit" disabled={loading} className="w-full">
|
||||||
|
{loading ? "Connexion..." : "Se connecter"}
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
73
app/admin/page.tsx
Normal file
73
app/admin/page.tsx
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import AdminLayout from "@/components/admin/AdminLayout";
|
||||||
|
import ClientForm from "@/components/admin/ClientForm";
|
||||||
|
import ClientList from "@/components/admin/ClientList";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Plus } from "lucide-react";
|
||||||
|
import { Client } from "@/lib/types/client";
|
||||||
|
|
||||||
|
export default function AdminPage() {
|
||||||
|
const [showForm, setShowForm] = useState(false);
|
||||||
|
const [editingClient, setEditingClient] = useState<Client | undefined>();
|
||||||
|
const [refreshKey, setRefreshKey] = useState(0);
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Vérifier si l'admin est connecté
|
||||||
|
const adminPassword = localStorage.getItem("adminPassword");
|
||||||
|
if (!adminPassword) {
|
||||||
|
router.push("/admin/login");
|
||||||
|
}
|
||||||
|
}, [router]);
|
||||||
|
|
||||||
|
const handleNewClient = () => {
|
||||||
|
setEditingClient(undefined);
|
||||||
|
setShowForm(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEdit = (client: Client) => {
|
||||||
|
setEditingClient(client);
|
||||||
|
setShowForm(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSuccess = () => {
|
||||||
|
setShowForm(false);
|
||||||
|
setEditingClient(undefined);
|
||||||
|
setRefreshKey((k) => k + 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
setShowForm(false);
|
||||||
|
setEditingClient(undefined);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AdminLayout>
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h2 className="text-2xl font-bold text-primary">Gestion des clients</h2>
|
||||||
|
{!showForm && (
|
||||||
|
<Button onClick={handleNewClient}>
|
||||||
|
<Plus className="h-4 w-4 mr-2" />
|
||||||
|
Nouveau client
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{showForm ? (
|
||||||
|
<ClientForm
|
||||||
|
client={editingClient}
|
||||||
|
onSuccess={handleSuccess}
|
||||||
|
onCancel={handleCancel}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<ClientList onEdit={handleEdit} onRefresh={() => setRefreshKey((k) => k + 1)} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</AdminLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
55
app/api/admin/clients/[id]/route.ts
Normal file
55
app/api/admin/clients/[id]/route.ts
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import { NextResponse } from "next/server";
|
||||||
|
import { updateClient, deleteClient, loadClients } from "@/lib/admin/client-utils";
|
||||||
|
import { requireAdminAuth } from "@/lib/admin/auth";
|
||||||
|
import { ClientInput } from "@/lib/types/client";
|
||||||
|
|
||||||
|
export async function PUT(
|
||||||
|
request: Request,
|
||||||
|
{ params }: { params: Promise<{ id: string }> }
|
||||||
|
) {
|
||||||
|
if (!requireAdminAuth(request)) {
|
||||||
|
return NextResponse.json({ error: "Non autorisé" }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { id } = await params;
|
||||||
|
const body: Partial<ClientInput> = await request.json();
|
||||||
|
const client = updateClient(id, body);
|
||||||
|
|
||||||
|
if (!client) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Client non trouvé" },
|
||||||
|
{ status: 404 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json(client);
|
||||||
|
} catch (error: any) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: error.message || "Erreur lors de la mise à jour" },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function DELETE(
|
||||||
|
request: Request,
|
||||||
|
{ params }: { params: Promise<{ id: string }> }
|
||||||
|
) {
|
||||||
|
if (!requireAdminAuth(request)) {
|
||||||
|
return NextResponse.json({ error: "Non autorisé" }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { id } = await params;
|
||||||
|
const deleted = deleteClient(id);
|
||||||
|
|
||||||
|
if (!deleted) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Client non trouvé" },
|
||||||
|
{ status: 404 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({ success: true });
|
||||||
|
}
|
||||||
|
|
||||||
60
app/api/admin/clients/route.ts
Normal file
60
app/api/admin/clients/route.ts
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import { NextResponse } from "next/server";
|
||||||
|
import {
|
||||||
|
createClient,
|
||||||
|
loadClients,
|
||||||
|
updateClient,
|
||||||
|
deleteClient,
|
||||||
|
validateEmail,
|
||||||
|
} from "@/lib/admin/client-utils";
|
||||||
|
import { requireAdminAuth } from "@/lib/admin/auth";
|
||||||
|
import { ClientInput } from "@/lib/types/client";
|
||||||
|
|
||||||
|
export async function GET(request: Request) {
|
||||||
|
if (!requireAdminAuth(request)) {
|
||||||
|
return NextResponse.json({ error: "Non autorisé" }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const clients = loadClients();
|
||||||
|
return NextResponse.json(clients);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function POST(request: Request) {
|
||||||
|
if (!requireAdminAuth(request)) {
|
||||||
|
return NextResponse.json({ error: "Non autorisé" }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const body: ClientInput = await request.json();
|
||||||
|
|
||||||
|
// Validation
|
||||||
|
if (!body.email || !validateEmail(body.email)) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Email invalide" },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!body.bungalowNumber) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Numéro de bungalow requis" },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const client = createClient({
|
||||||
|
email: body.email,
|
||||||
|
bungalowNumber: body.bungalowNumber,
|
||||||
|
wifiName: body.wifiName || "Lagon-WiFi",
|
||||||
|
wifiPassword: body.wifiPassword || "",
|
||||||
|
gerantMessage: body.gerantMessage || "Bienvenue dans notre pension de famille !",
|
||||||
|
});
|
||||||
|
|
||||||
|
return NextResponse.json(client, { status: 201 });
|
||||||
|
} catch (error: any) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: error.message || "Erreur lors de la création du client" },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
26
app/api/client/[token]/route.ts
Normal file
26
app/api/client/[token]/route.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { NextResponse } from "next/server";
|
||||||
|
import { getClientByToken } from "@/lib/admin/client-utils";
|
||||||
|
|
||||||
|
export async function GET(
|
||||||
|
request: Request,
|
||||||
|
{ params }: { params: Promise<{ token: string }> }
|
||||||
|
) {
|
||||||
|
const { token } = await params;
|
||||||
|
const client = getClientByToken(token);
|
||||||
|
|
||||||
|
if (!client) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Token invalide ou expiré" },
|
||||||
|
{ status: 404 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retourner uniquement les informations nécessaires (sans le token)
|
||||||
|
return NextResponse.json({
|
||||||
|
bungalowNumber: client.bungalowNumber,
|
||||||
|
wifiName: client.wifiName,
|
||||||
|
wifiPassword: client.wifiPassword,
|
||||||
|
gerantMessage: client.gerantMessage,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@ -1,24 +1,75 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { Wifi, Copy, Check } from "lucide-react";
|
import { Wifi, Copy, Check, AlertCircle } from "lucide-react";
|
||||||
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
|
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { config } from "@/lib/config";
|
import { useClientData } from "@/lib/hooks/useClientData";
|
||||||
|
|
||||||
export default function WifiCard() {
|
export default function WifiCard() {
|
||||||
|
const { wifiName, wifiPassword, loading } = useClientData();
|
||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
const handleCopyPassword = async () => {
|
// Fonction de copie avec fallback pour les navigateurs qui ne supportent pas l'API Clipboard
|
||||||
|
const copyToClipboard = async (text: string): Promise<boolean> => {
|
||||||
|
// Méthode moderne (nécessite HTTPS ou localhost)
|
||||||
|
if (navigator.clipboard && window.isSecureContext) {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(text);
|
||||||
|
return true;
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Erreur avec l'API Clipboard:", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback pour les navigateurs plus anciens ou contextes non sécurisés
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(config.wifiPassword);
|
const textArea = document.createElement("textarea");
|
||||||
setCopied(true);
|
textArea.value = text;
|
||||||
setTimeout(() => setCopied(false), 2000);
|
textArea.style.position = "fixed";
|
||||||
|
textArea.style.left = "-999999px";
|
||||||
|
textArea.style.top = "-999999px";
|
||||||
|
document.body.appendChild(textArea);
|
||||||
|
textArea.focus();
|
||||||
|
textArea.select();
|
||||||
|
|
||||||
|
const successful = document.execCommand("copy");
|
||||||
|
document.body.removeChild(textArea);
|
||||||
|
|
||||||
|
if (successful) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
throw new Error("La commande copy a échoué");
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Erreur lors de la copie:", err);
|
console.error("Erreur avec la méthode fallback:", err);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleCopyPassword = async () => {
|
||||||
|
if (!wifiPassword || wifiPassword.trim() === "") {
|
||||||
|
setError("Le mot de passe WiFi n'est pas disponible");
|
||||||
|
setTimeout(() => setError(null), 3000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setError(null);
|
||||||
|
const success = await copyToClipboard(wifiPassword);
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
setCopied(true);
|
||||||
|
setTimeout(() => setCopied(false), 2000);
|
||||||
|
} else {
|
||||||
|
setError("Impossible de copier. Veuillez sélectionner manuellement le mot de passe ci-dessous.");
|
||||||
|
setTimeout(() => setError(null), 5000);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Afficher le mot de passe en cas d'échec de la copie
|
||||||
|
const showPasswordFallback = error && error.includes("sélectionner manuellement");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="bg-white">
|
<Card className="bg-white">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
@ -30,14 +81,36 @@ export default function WifiCard() {
|
|||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-gray-600 mb-1">Nom du réseau</p>
|
<p className="text-sm text-gray-600 mb-1">Nom du réseau</p>
|
||||||
<p className="text-lg font-semibold text-primary">{config.wifiName}</p>
|
<p className="text-lg font-semibold text-primary">{wifiName || "Chargement..."}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{showPasswordFallback && wifiPassword && (
|
||||||
|
<div className="bg-yellow-50 border border-yellow-200 rounded-xl p-3">
|
||||||
|
<p className="text-sm text-yellow-800 font-mono select-all">
|
||||||
|
{wifiPassword}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{error && !showPasswordFallback && (
|
||||||
|
<div className="bg-red-50 border border-red-200 rounded-xl p-3 flex items-start gap-2">
|
||||||
|
<AlertCircle className="h-5 w-5 text-red-600 flex-shrink-0 mt-0.5" />
|
||||||
|
<p className="text-sm text-red-800">{error}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
onClick={handleCopyPassword}
|
onClick={handleCopyPassword}
|
||||||
|
disabled={loading || !wifiPassword}
|
||||||
className="w-full h-14 text-lg"
|
className="w-full h-14 text-lg"
|
||||||
size="lg"
|
size="lg"
|
||||||
>
|
>
|
||||||
{copied ? (
|
{loading ? (
|
||||||
|
<>
|
||||||
|
<Copy className="mr-2 h-5 w-5" />
|
||||||
|
Chargement...
|
||||||
|
</>
|
||||||
|
) : copied ? (
|
||||||
<>
|
<>
|
||||||
<Check className="mr-2 h-5 w-5" />
|
<Check className="mr-2 h-5 w-5" />
|
||||||
Mot de passe copié !
|
Mot de passe copié !
|
||||||
|
|||||||
30
components/admin/AdminLayout.tsx
Normal file
30
components/admin/AdminLayout.tsx
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { LogOut } from "lucide-react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
|
||||||
|
export default function AdminLayout({ children }: { children: React.ReactNode }) {
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const handleLogout = () => {
|
||||||
|
localStorage.removeItem("adminPassword");
|
||||||
|
router.push("/admin/login");
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-background">
|
||||||
|
<header className="bg-white border-b border-gray-200 shadow-sm">
|
||||||
|
<div className="max-w-4xl mx-auto px-4 py-4 flex items-center justify-between">
|
||||||
|
<h1 className="text-xl font-bold text-primary">Administration</h1>
|
||||||
|
<Button variant="outline" size="sm" onClick={handleLogout}>
|
||||||
|
<LogOut className="h-4 w-4 mr-2" />
|
||||||
|
Déconnexion
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<main className="max-w-4xl mx-auto px-4 py-6">{children}</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
195
components/admin/ClientForm.tsx
Normal file
195
components/admin/ClientForm.tsx
Normal file
@ -0,0 +1,195 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
|
||||||
|
import { Client, ClientInput } from "@/lib/types/client";
|
||||||
|
import QRCodeDisplay from "./QRCodeDisplay";
|
||||||
|
|
||||||
|
interface ClientFormProps {
|
||||||
|
client?: Client;
|
||||||
|
onSuccess: () => void;
|
||||||
|
onCancel: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ClientForm({ client, onSuccess, onCancel }: ClientFormProps) {
|
||||||
|
const [formData, setFormData] = useState<ClientInput>({
|
||||||
|
email: client?.email || "",
|
||||||
|
bungalowNumber: client?.bungalowNumber || "",
|
||||||
|
wifiName: client?.wifiName || "Lagon-WiFi",
|
||||||
|
wifiPassword: client?.wifiPassword || "",
|
||||||
|
gerantMessage: client?.gerantMessage || "Bienvenue dans notre pension de famille !",
|
||||||
|
});
|
||||||
|
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [createdClient, setCreatedClient] = useState<Client | null>(client || null);
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const adminPassword = localStorage.getItem("adminPassword") || "";
|
||||||
|
const url = client
|
||||||
|
? `/api/admin/clients/${client.id}`
|
||||||
|
: "/api/admin/clients";
|
||||||
|
|
||||||
|
const method = client ? "PUT" : "POST";
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer ${adminPassword}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify(formData),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
throw new Error(data.error || "Erreur lors de la sauvegarde");
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
setCreatedClient(data);
|
||||||
|
onSuccess();
|
||||||
|
} catch (err: any) {
|
||||||
|
setError(err.message);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getClientUrl = () => {
|
||||||
|
if (!createdClient) return "";
|
||||||
|
const baseUrl = typeof window !== "undefined" ? window.location.origin : "";
|
||||||
|
return `${baseUrl}/accueil?token=${createdClient.token}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>{client ? "Modifier le client" : "Nouveau client"}</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Email *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
required
|
||||||
|
value={formData.email}
|
||||||
|
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
|
||||||
|
disabled={!!client}
|
||||||
|
className="w-full px-4 py-2 border border-gray-300 rounded-xl focus:ring-2 focus:ring-primary focus:border-transparent disabled:bg-gray-100"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Numéro de bungalow *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
value={formData.bungalowNumber}
|
||||||
|
onChange={(e) =>
|
||||||
|
setFormData({ ...formData, bungalowNumber: e.target.value })
|
||||||
|
}
|
||||||
|
className="w-full px-4 py-2 border border-gray-300 rounded-xl focus:ring-2 focus:ring-primary focus:border-transparent"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Nom du WiFi
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={formData.wifiName}
|
||||||
|
onChange={(e) =>
|
||||||
|
setFormData({ ...formData, wifiName: e.target.value })
|
||||||
|
}
|
||||||
|
className="w-full px-4 py-2 border border-gray-300 rounded-xl focus:ring-2 focus:ring-primary focus:border-transparent"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Mot de passe WiFi
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={formData.wifiPassword}
|
||||||
|
onChange={(e) =>
|
||||||
|
setFormData({ ...formData, wifiPassword: e.target.value })
|
||||||
|
}
|
||||||
|
className="w-full px-4 py-2 border border-gray-300 rounded-xl focus:ring-2 focus:ring-primary focus:border-transparent"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Message du gérant
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
value={formData.gerantMessage}
|
||||||
|
onChange={(e) =>
|
||||||
|
setFormData({ ...formData, gerantMessage: e.target.value })
|
||||||
|
}
|
||||||
|
rows={3}
|
||||||
|
className="w-full px-4 py-2 border border-gray-300 rounded-xl focus:ring-2 focus:ring-primary focus:border-transparent"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-xl text-sm">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<Button type="button" variant="outline" onClick={onCancel} className="flex-1">
|
||||||
|
Annuler
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" disabled={loading} className="flex-1">
|
||||||
|
{loading ? "Enregistrement..." : client ? "Modifier" : "Créer"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{createdClient && !client && (
|
||||||
|
<div className="mt-6 pt-6 border-t border-gray-200">
|
||||||
|
<h3 className="font-semibold text-primary mb-3">Client créé avec succès !</h3>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-gray-600 mb-2">Lien unique :</p>
|
||||||
|
<div className="bg-secondary rounded-xl p-3 flex items-center justify-between gap-2">
|
||||||
|
<code className="text-xs break-all flex-1">{getClientUrl()}</code>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => {
|
||||||
|
navigator.clipboard.writeText(getClientUrl());
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Copier
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-gray-600 mb-2">QR Code :</p>
|
||||||
|
<QRCodeDisplay url={getClientUrl()} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
165
components/admin/ClientList.tsx
Normal file
165
components/admin/ClientList.tsx
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { Trash2, Edit, Copy, QrCode } from "lucide-react";
|
||||||
|
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Client } from "@/lib/types/client";
|
||||||
|
import QRCodeDisplay from "./QRCodeDisplay";
|
||||||
|
|
||||||
|
interface ClientListProps {
|
||||||
|
onEdit: (client: Client) => void;
|
||||||
|
onRefresh: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ClientList({ onEdit, onRefresh }: ClientListProps) {
|
||||||
|
const [clients, setClients] = useState<Client[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [selectedClient, setSelectedClient] = useState<Client | null>(null);
|
||||||
|
const [showQR, setShowQR] = useState<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchClients();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const fetchClients = async () => {
|
||||||
|
try {
|
||||||
|
const adminPassword = localStorage.getItem("adminPassword") || "";
|
||||||
|
const response = await fetch("/api/admin/clients", {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${adminPassword}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
setClients(data);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erreur lors du chargement des clients:", error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = async (id: string) => {
|
||||||
|
if (!confirm("Êtes-vous sûr de vouloir supprimer ce client ?")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const adminPassword = localStorage.getItem("adminPassword") || "";
|
||||||
|
const response = await fetch(`/api/admin/clients/${id}`, {
|
||||||
|
method: "DELETE",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${adminPassword}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
fetchClients();
|
||||||
|
onRefresh();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erreur lors de la suppression:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getClientUrl = (token: string) => {
|
||||||
|
const baseUrl = typeof window !== "undefined" ? window.location.origin : "";
|
||||||
|
return `${baseUrl}/accueil?token=${token}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center py-8">
|
||||||
|
<p className="text-gray-600">Chargement...</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (clients.length === 0) {
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardContent className="py-8 text-center">
|
||||||
|
<p className="text-gray-600">Aucun client pour le moment.</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{clients.map((client) => (
|
||||||
|
<Card key={client.id}>
|
||||||
|
<CardHeader>
|
||||||
|
<div className="flex items-start justify-between">
|
||||||
|
<div className="flex-1">
|
||||||
|
<CardTitle className="text-lg">{client.email}</CardTitle>
|
||||||
|
<p className="text-sm text-gray-600 mt-1">
|
||||||
|
Bungalow {client.bungalowNumber}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => {
|
||||||
|
const url = getClientUrl(client.token);
|
||||||
|
navigator.clipboard.writeText(url);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Copy className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() =>
|
||||||
|
setShowQR(showQR === client.id ? null : client.id)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<QrCode className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => onEdit(client)}
|
||||||
|
>
|
||||||
|
<Edit className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => handleDelete(client.id)}
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-2 text-sm">
|
||||||
|
<div>
|
||||||
|
<span className="font-medium">WiFi:</span> {client.wifiName}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="font-medium">Message:</span>{" "}
|
||||||
|
{client.gerantMessage.substring(0, 50)}
|
||||||
|
{client.gerantMessage.length > 50 ? "..." : ""}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-gray-500">
|
||||||
|
Créé le {new Date(client.createdAt).toLocaleDateString("fr-FR")}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{showQR === client.id && (
|
||||||
|
<div className="mt-4 pt-4 border-t border-gray-200">
|
||||||
|
<QRCodeDisplay url={getClientUrl(client.token)} size={150} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
20
components/admin/QRCodeDisplay.tsx
Normal file
20
components/admin/QRCodeDisplay.tsx
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { QRCodeSVG } from "qrcode.react";
|
||||||
|
|
||||||
|
interface QRCodeDisplayProps {
|
||||||
|
url: string;
|
||||||
|
size?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function QRCodeDisplay({ url, size = 200 }: QRCodeDisplayProps) {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center gap-4 p-4 bg-white rounded-2xl border border-gray-200">
|
||||||
|
<QRCodeSVG value={url} size={size} level="H" />
|
||||||
|
<p className="text-xs text-gray-600 text-center break-all max-w-xs">
|
||||||
|
{url}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@ -10,3 +10,6 @@ NEXT_PUBLIC_CONTACT_WHATSAPP=+689 XX XX XX XX
|
|||||||
NEXT_PUBLIC_CONTACT_ADDRESS=Rotoava, Fakarava
|
NEXT_PUBLIC_CONTACT_ADDRESS=Rotoava, Fakarava
|
||||||
NEXT_PUBLIC_CONTACT_HOURS=Disponible 24/7 pour les urgences
|
NEXT_PUBLIC_CONTACT_HOURS=Disponible 24/7 pour les urgences
|
||||||
|
|
||||||
|
# Administration
|
||||||
|
ADMIN_PASSWORD=admin123
|
||||||
|
|
||||||
|
|||||||
15
lib/admin/auth.ts
Normal file
15
lib/admin/auth.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
export function verifyAdminPassword(password: string): boolean {
|
||||||
|
const adminPassword = process.env.ADMIN_PASSWORD || "admin123";
|
||||||
|
return password === adminPassword;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function requireAdminAuth(request: Request): boolean {
|
||||||
|
const authHeader = request.headers.get("authorization");
|
||||||
|
if (!authHeader) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = authHeader.replace("Bearer ", "");
|
||||||
|
return verifyAdminPassword(token);
|
||||||
|
}
|
||||||
|
|
||||||
101
lib/admin/client-utils.ts
Normal file
101
lib/admin/client-utils.ts
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
import { randomBytes } from "crypto";
|
||||||
|
import { Client, ClientInput } from "@/lib/types/client";
|
||||||
|
import { readFileSync, writeFileSync } from "fs";
|
||||||
|
import { join } from "path";
|
||||||
|
|
||||||
|
const CLIENTS_FILE = join(process.cwd(), "lib/data/clients.json");
|
||||||
|
|
||||||
|
export function generateToken(): string {
|
||||||
|
return randomBytes(32).toString("hex");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateEmail(email: string): boolean {
|
||||||
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||||
|
return emailRegex.test(email);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function loadClients(): Client[] {
|
||||||
|
try {
|
||||||
|
const data = readFileSync(CLIENTS_FILE, "utf-8");
|
||||||
|
return JSON.parse(data);
|
||||||
|
} catch (error) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function saveClients(clients: Client[]): void {
|
||||||
|
try {
|
||||||
|
writeFileSync(CLIENTS_FILE, JSON.stringify(clients, null, 2), "utf-8");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erreur lors de la sauvegarde des clients:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createClient(input: ClientInput): Client {
|
||||||
|
const clients = loadClients();
|
||||||
|
|
||||||
|
// Vérifier si l'email existe déjà
|
||||||
|
if (clients.some((c) => c.email === input.email)) {
|
||||||
|
throw new Error("Un client avec cet email existe déjà");
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
const client: Client = {
|
||||||
|
id: `client-${Date.now()}`,
|
||||||
|
email: input.email,
|
||||||
|
token: generateToken(),
|
||||||
|
bungalowNumber: input.bungalowNumber,
|
||||||
|
wifiName: input.wifiName,
|
||||||
|
wifiPassword: input.wifiPassword,
|
||||||
|
gerantMessage: input.gerantMessage,
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
};
|
||||||
|
|
||||||
|
clients.push(client);
|
||||||
|
saveClients(clients);
|
||||||
|
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getClientByToken(token: string): Client | null {
|
||||||
|
const clients = loadClients();
|
||||||
|
return clients.find((c) => c.token === token) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getClientByEmail(email: string): Client | null {
|
||||||
|
const clients = loadClients();
|
||||||
|
return clients.find((c) => c.email === email) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateClient(id: string, input: Partial<ClientInput>): Client | null {
|
||||||
|
const clients = loadClients();
|
||||||
|
const index = clients.findIndex((c) => c.id === id);
|
||||||
|
|
||||||
|
if (index === -1) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
clients[index] = {
|
||||||
|
...clients[index],
|
||||||
|
...input,
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
saveClients(clients);
|
||||||
|
return clients[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteClient(id: string): boolean {
|
||||||
|
const clients = loadClients();
|
||||||
|
const filtered = clients.filter((c) => c.id !== id);
|
||||||
|
|
||||||
|
if (filtered.length === clients.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
saveClients(filtered);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
13
lib/data/clients.json
Normal file
13
lib/data/clients.json
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "client-1763884046464",
|
||||||
|
"email": "sylvestre@anuanua.fr",
|
||||||
|
"token": "017e529de30383c9dba62705fc92df7062f1bcc8872985bc55a8f7dcb7407bd4",
|
||||||
|
"bungalowNumber": "4",
|
||||||
|
"wifiName": "Lagon-WiFi",
|
||||||
|
"wifiPassword": "le motepasseduwifi987",
|
||||||
|
"gerantMessage": "Bienvenue dans notre pension de famille !",
|
||||||
|
"createdAt": "2025-11-23T07:47:26.464Z",
|
||||||
|
"updatedAt": "2025-11-23T07:47:26.464Z"
|
||||||
|
}
|
||||||
|
]
|
||||||
96
lib/hooks/useClientData.ts
Normal file
96
lib/hooks/useClientData.ts
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useEffect, useMemo } from "react";
|
||||||
|
import { useSearchParams } from "next/navigation";
|
||||||
|
import { config } from "@/lib/config";
|
||||||
|
|
||||||
|
export interface ClientData {
|
||||||
|
bungalowNumber: string;
|
||||||
|
wifiName: string;
|
||||||
|
wifiPassword: string;
|
||||||
|
gerantMessage: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const STORAGE_KEY = "clientData";
|
||||||
|
|
||||||
|
function loadFromStorage(): ClientData | null {
|
||||||
|
if (typeof window === "undefined") return null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const stored = localStorage.getItem(STORAGE_KEY);
|
||||||
|
if (stored) {
|
||||||
|
return JSON.parse(stored);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erreur lors du chargement depuis localStorage:", error);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useClientData() {
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
// Charger immédiatement depuis localStorage pour éviter le délai
|
||||||
|
const [clientData, setClientData] = useState<ClientData | null>(() => loadFromStorage());
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const loadClientData = async () => {
|
||||||
|
// 1. Charger d'abord depuis localStorage pour un affichage immédiat
|
||||||
|
const storedData = loadFromStorage();
|
||||||
|
if (storedData) {
|
||||||
|
setClientData(storedData);
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Vérifier s'il y a un token dans l'URL
|
||||||
|
const token = searchParams.get("token");
|
||||||
|
|
||||||
|
if (token) {
|
||||||
|
try {
|
||||||
|
// 3. Charger les données depuis l'API pour mettre à jour
|
||||||
|
const response = await fetch(`/api/client/${token}`);
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
const clientInfo: ClientData = {
|
||||||
|
bungalowNumber: data.bungalowNumber,
|
||||||
|
wifiName: data.wifiName,
|
||||||
|
wifiPassword: data.wifiPassword,
|
||||||
|
gerantMessage: data.gerantMessage,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 4. Sauvegarder dans localStorage
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(clientInfo));
|
||||||
|
}
|
||||||
|
setClientData(clientInfo);
|
||||||
|
} else {
|
||||||
|
// Token invalide, garder les données sauvegardées si disponibles
|
||||||
|
if (!storedData) {
|
||||||
|
setClientData(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erreur lors du chargement des données client:", error);
|
||||||
|
// En cas d'erreur, garder les données sauvegardées si disponibles
|
||||||
|
if (!storedData) {
|
||||||
|
setClientData(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
loadClientData();
|
||||||
|
}, [searchParams]);
|
||||||
|
|
||||||
|
// Retourner les données client ou les valeurs par défaut
|
||||||
|
return useMemo(() => ({
|
||||||
|
bungalowNumber: clientData?.bungalowNumber || config.bungalowNumber,
|
||||||
|
wifiName: clientData?.wifiName || config.wifiName,
|
||||||
|
wifiPassword: clientData?.wifiPassword || config.wifiPassword,
|
||||||
|
gerantMessage: clientData?.gerantMessage || config.gerantMessage,
|
||||||
|
loading,
|
||||||
|
}), [clientData, loading]);
|
||||||
|
}
|
||||||
|
|
||||||
20
lib/types/client.ts
Normal file
20
lib/types/client.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
export interface Client {
|
||||||
|
id: string;
|
||||||
|
email: string;
|
||||||
|
token: string;
|
||||||
|
bungalowNumber: string;
|
||||||
|
wifiName: string;
|
||||||
|
wifiPassword: string;
|
||||||
|
gerantMessage: string;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ClientInput {
|
||||||
|
email: string;
|
||||||
|
bungalowNumber: string;
|
||||||
|
wifiName: string;
|
||||||
|
wifiPassword: string;
|
||||||
|
gerantMessage: string;
|
||||||
|
}
|
||||||
|
|
||||||
10
package-lock.json
generated
10
package-lock.json
generated
@ -12,6 +12,7 @@
|
|||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"lucide-react": "^0.460.0",
|
"lucide-react": "^0.460.0",
|
||||||
"next": "^14.2.33",
|
"next": "^14.2.33",
|
||||||
|
"qrcode.react": "^4.2.0",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"tailwind-merge": "^2.5.5"
|
"tailwind-merge": "^2.5.5"
|
||||||
@ -4701,6 +4702,15 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/qrcode.react": {
|
||||||
|
"version": "4.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/qrcode.react/-/qrcode.react-4.2.0.tgz",
|
||||||
|
"integrity": "sha512-QpgqWi8rD9DsS9EP3z7BT+5lY5SFhsqGjpgW5DY/i3mK4M9DTBNz3ErMi8BWYEfI3L0d8GIbGmcdFAS1uIRGjA==",
|
||||||
|
"license": "ISC",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/queue-microtask": {
|
"node_modules/queue-microtask": {
|
||||||
"version": "1.2.3",
|
"version": "1.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
|
||||||
|
|||||||
18
package.json
18
package.json
@ -9,24 +9,24 @@
|
|||||||
"lint": "next lint"
|
"lint": "next lint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"class-variance-authority": "^0.7.1",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"lucide-react": "^0.460.0",
|
||||||
|
"next": "^14.2.33",
|
||||||
|
"qrcode.react": "^4.2.0",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"next": "^14.2.33",
|
"tailwind-merge": "^2.5.5"
|
||||||
"lucide-react": "^0.460.0",
|
|
||||||
"clsx": "^2.1.1",
|
|
||||||
"tailwind-merge": "^2.5.5",
|
|
||||||
"class-variance-authority": "^0.7.1"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"typescript": "^5.6.3",
|
|
||||||
"@types/node": "^22.10.1",
|
"@types/node": "^22.10.1",
|
||||||
"@types/react": "^18.3.12",
|
"@types/react": "^18.3.12",
|
||||||
"@types/react-dom": "^18.3.1",
|
"@types/react-dom": "^18.3.1",
|
||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.20",
|
||||||
|
"eslint": "^8.57.1",
|
||||||
|
"eslint-config-next": "^14.2.33",
|
||||||
"postcss": "^8.4.47",
|
"postcss": "^8.4.47",
|
||||||
"tailwindcss": "^3.4.14",
|
"tailwindcss": "^3.4.14",
|
||||||
"eslint": "^8.57.1",
|
"typescript": "^5.6.3"
|
||||||
"eslint-config-next": "^14.2.33"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user