diff --git a/app/accueil/page.tsx b/app/accueil/page.tsx index e9de1f1..8514abc 100644 --- a/app/accueil/page.tsx +++ b/app/accueil/page.tsx @@ -1,8 +1,10 @@ +"use client"; + import dynamic from "next/dynamic"; import Layout from "@/components/layout/Layout"; -import { config } from "@/lib/config"; import WifiCard from "@/components/accueil/WifiCard"; import Logo from "@/components/Logo"; +import { useClientData } from "@/lib/hooks/useClientData"; const WeatherWidget = dynamic(() => import("@/components/accueil/WeatherWidget"), { loading: () =>
, @@ -10,6 +12,18 @@ const WeatherWidget = dynamic(() => import("@/components/accueil/WeatherWidget") }); export default function AccueilPage() { + const { bungalowNumber, gerantMessage, loading } = useClientData(); + + if (loading) { + return ( + +
+
+
+ + ); + } + return (
@@ -19,7 +33,7 @@ export default function AccueilPage() { Ia Ora Na

- Bienvenue au Bungalow {config.bungalowNumber} + Bienvenue au Bungalow {bungalowNumber}

@@ -32,7 +46,7 @@ export default function AccueilPage() { Le mot du gérant

- {config.gerantMessage} + {gerantMessage}

diff --git a/app/admin/login/page.tsx b/app/admin/login/page.tsx new file mode 100644 index 0000000..6f9443a --- /dev/null +++ b/app/admin/login/page.tsx @@ -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 ( +
+ + +
+ +
+ Administration +
+ +
+
+ + 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 + /> +
+ + {error && ( +
+ {error} +
+ )} + + +
+
+
+
+ ); +} + diff --git a/app/admin/page.tsx b/app/admin/page.tsx new file mode 100644 index 0000000..2be6b23 --- /dev/null +++ b/app/admin/page.tsx @@ -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(); + 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 ( + +
+
+

Gestion des clients

+ {!showForm && ( + + )} +
+ + {showForm ? ( + + ) : ( + setRefreshKey((k) => k + 1)} /> + )} +
+
+ ); +} + diff --git a/app/api/admin/clients/[id]/route.ts b/app/api/admin/clients/[id]/route.ts new file mode 100644 index 0000000..0536f4f --- /dev/null +++ b/app/api/admin/clients/[id]/route.ts @@ -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 = 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 }); +} + diff --git a/app/api/admin/clients/route.ts b/app/api/admin/clients/route.ts new file mode 100644 index 0000000..c2a592c --- /dev/null +++ b/app/api/admin/clients/route.ts @@ -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 } + ); + } +} + diff --git a/app/api/client/[token]/route.ts b/app/api/client/[token]/route.ts new file mode 100644 index 0000000..4bc8360 --- /dev/null +++ b/app/api/client/[token]/route.ts @@ -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, + }); +} + diff --git a/components/accueil/WifiCard.tsx b/components/accueil/WifiCard.tsx index edcfa8a..1277386 100644 --- a/components/accueil/WifiCard.tsx +++ b/components/accueil/WifiCard.tsx @@ -1,24 +1,75 @@ "use client"; -import { useState } from "react"; -import { Wifi, Copy, Check } from "lucide-react"; +import { useState, useEffect } from "react"; +import { Wifi, Copy, Check, AlertCircle } from "lucide-react"; import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; -import { config } from "@/lib/config"; +import { useClientData } from "@/lib/hooks/useClientData"; export default function WifiCard() { + const { wifiName, wifiPassword, loading } = useClientData(); const [copied, setCopied] = useState(false); + const [error, setError] = useState(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 => { + // 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 { - await navigator.clipboard.writeText(config.wifiPassword); - setCopied(true); - setTimeout(() => setCopied(false), 2000); + const textArea = document.createElement("textarea"); + textArea.value = text; + 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) { - 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 ( @@ -30,14 +81,36 @@ export default function WifiCard() {

Nom du réseau

-

{config.wifiName}

+

{wifiName || "Chargement..."}

+ + {showPasswordFallback && wifiPassword && ( +
+

+ {wifiPassword} +

+
+ )} + + {error && !showPasswordFallback && ( +
+ +

{error}

+
+ )} + +
+ +
{children}
+
+ ); +} + diff --git a/components/admin/ClientForm.tsx b/components/admin/ClientForm.tsx new file mode 100644 index 0000000..94bc278 --- /dev/null +++ b/components/admin/ClientForm.tsx @@ -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({ + 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(null); + const [createdClient, setCreatedClient] = useState(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 ( + + + {client ? "Modifier le client" : "Nouveau client"} + + +
+
+ + 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" + /> +
+ +
+ + + 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" + /> +
+ +
+ + + 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" + /> +
+ +
+ + + 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" + /> +
+ +
+ +