- Création des fichiers JSON dans public/data/ - Modification de tous les composants pour fetch depuis /data/*.json - PlaceList, FAQ, Lexique, Tides, SunTimes, Excursions, Notifications - Données complètes pour Fakarava (plages, restaurants, épiceries) - Fix docker-compose.build.yml (suppression volume node_modules)
248 lines
8.5 KiB
TypeScript
248 lines
8.5 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useEffect } from "react";
|
|
import { Calendar, Users, CheckCircle } from "lucide-react";
|
|
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Excursion } from "@/lib/types/excursions";
|
|
|
|
export default function ExcursionBooking() {
|
|
const [excursions, setExcursions] = useState<Excursion[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [selectedExcursion, setSelectedExcursion] = useState<Excursion | null>(null);
|
|
const [formData, setFormData] = useState({
|
|
name: "",
|
|
email: "",
|
|
phone: "",
|
|
date: "",
|
|
participants: 1,
|
|
});
|
|
const [submitting, setSubmitting] = useState(false);
|
|
const [success, setSuccess] = useState(false);
|
|
|
|
useEffect(() => {
|
|
const fetchExcursions = async () => {
|
|
try {
|
|
const response = await fetch("/data/excursions.json");
|
|
const data = await response.json();
|
|
setExcursions(data);
|
|
} catch (error) {
|
|
console.error("Erreur lors du chargement des excursions:", error);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
fetchExcursions();
|
|
}, []);
|
|
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
if (!selectedExcursion) return;
|
|
|
|
setSubmitting(true);
|
|
|
|
// Simuler l'envoi de réservation (sans backend)
|
|
// Dans une version production, cela devrait appeler une API ou envoyer un email
|
|
setTimeout(() => {
|
|
setSuccess(true);
|
|
setFormData({ name: "", email: "", phone: "", date: "", participants: 1 });
|
|
setTimeout(() => {
|
|
setSuccess(false);
|
|
setSelectedExcursion(null);
|
|
}, 3000);
|
|
setSubmitting(false);
|
|
}, 1000);
|
|
};
|
|
|
|
const getExcursionTypeLabel = (type: string) => {
|
|
switch (type) {
|
|
case "tour-lagon":
|
|
return "Tour Lagon";
|
|
case "plongee":
|
|
return "Plongée";
|
|
case "4x4":
|
|
return "4x4";
|
|
default:
|
|
return type;
|
|
}
|
|
};
|
|
|
|
if (loading) {
|
|
return (
|
|
<Card>
|
|
<CardContent className="p-6">
|
|
<div className="animate-pulse">Chargement des excursions...</div>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
if (success) {
|
|
return (
|
|
<Card className="bg-secondary">
|
|
<CardContent className="p-6 text-center">
|
|
<CheckCircle className="h-12 w-12 text-primary mx-auto mb-4" />
|
|
<h3 className="text-xl font-semibold text-primary mb-2">
|
|
Réservation confirmée !
|
|
</h3>
|
|
<p className="text-gray-700">
|
|
Votre demande de réservation a été enregistrée. Nous vous contacterons bientôt.
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
if (selectedExcursion) {
|
|
return (
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Réserver : {selectedExcursion.name}</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<form onSubmit={handleSubmit} className="space-y-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Nom complet
|
|
</label>
|
|
<input
|
|
type="text"
|
|
required
|
|
value={formData.name}
|
|
onChange={(e) => setFormData({ ...formData, name: 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">
|
|
Email
|
|
</label>
|
|
<input
|
|
type="email"
|
|
required
|
|
value={formData.email}
|
|
onChange={(e) => setFormData({ ...formData, email: 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">
|
|
Téléphone
|
|
</label>
|
|
<input
|
|
type="tel"
|
|
required
|
|
value={formData.phone}
|
|
onChange={(e) => setFormData({ ...formData, phone: 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 className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Date
|
|
</label>
|
|
<input
|
|
type="date"
|
|
required
|
|
min={new Date().toISOString().split("T")[0]}
|
|
value={formData.date}
|
|
onChange={(e) => setFormData({ ...formData, date: 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">
|
|
Participants
|
|
</label>
|
|
<input
|
|
type="number"
|
|
min="1"
|
|
max="10"
|
|
required
|
|
value={formData.participants}
|
|
onChange={(e) =>
|
|
setFormData({ ...formData, participants: parseInt(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>
|
|
<div className="bg-secondary rounded-xl p-4">
|
|
<p className="text-sm text-gray-600 mb-1">Total</p>
|
|
<p className="text-2xl font-bold text-primary">
|
|
{selectedExcursion.price * formData.participants} XPF
|
|
</p>
|
|
</div>
|
|
<div className="flex gap-3">
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
onClick={() => setSelectedExcursion(null)}
|
|
className="flex-1"
|
|
>
|
|
Annuler
|
|
</Button>
|
|
<Button type="submit" disabled={submitting} className="flex-1">
|
|
{submitting ? "Envoi..." : "Réserver"}
|
|
</Button>
|
|
</div>
|
|
</form>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Réservation d'excursions</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="space-y-4">
|
|
{excursions.map((excursion) => (
|
|
<div
|
|
key={excursion.id}
|
|
className="border border-gray-200 rounded-xl p-4 hover:border-primary transition-colors"
|
|
>
|
|
<div className="flex items-start justify-between mb-2">
|
|
<div className="flex-1">
|
|
<h3 className="font-semibold text-lg text-primary mb-1">
|
|
{excursion.name}
|
|
</h3>
|
|
<span className="inline-block px-3 py-1 bg-secondary text-primary text-xs font-medium rounded-lg mb-2">
|
|
{getExcursionTypeLabel(excursion.type)}
|
|
</span>
|
|
<p className="text-sm text-gray-600 mb-2">{excursion.description}</p>
|
|
<div className="flex items-center gap-4 text-xs text-gray-500">
|
|
<span className="flex items-center gap-1">
|
|
<Calendar className="h-4 w-4" />
|
|
{excursion.duration}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<div className="text-right ml-4">
|
|
<p className="text-2xl font-bold text-primary">
|
|
{excursion.price.toLocaleString()} XPF
|
|
</p>
|
|
<p className="text-xs text-gray-500">par personne</p>
|
|
</div>
|
|
</div>
|
|
<Button
|
|
onClick={() => setSelectedExcursion(excursion)}
|
|
disabled={!excursion.available}
|
|
className="w-full mt-3"
|
|
variant={excursion.available ? "default" : "outline"}
|
|
>
|
|
{excursion.available ? "Réserver" : "Indisponible"}
|
|
</Button>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|
|
|