first commit
This commit is contained in:
40
app/accueil/page.tsx
Normal file
40
app/accueil/page.tsx
Normal file
@ -0,0 +1,40 @@
|
||||
import dynamic from "next/dynamic";
|
||||
import Layout from "@/components/layout/Layout";
|
||||
import { config } from "@/lib/config";
|
||||
import WifiCard from "@/components/accueil/WifiCard";
|
||||
|
||||
const WeatherWidget = dynamic(() => import("@/components/accueil/WeatherWidget"), {
|
||||
loading: () => <div className="h-32 bg-gray-100 rounded-2xl animate-pulse" />,
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
export default function AccueilPage() {
|
||||
return (
|
||||
<Layout>
|
||||
<div className="px-4 py-6 space-y-6">
|
||||
<header className="text-center py-4">
|
||||
<h1 className="text-2xl font-bold text-primary mb-2">
|
||||
Ia Ora Na
|
||||
</h1>
|
||||
<p className="text-lg text-gray-700">
|
||||
Bienvenue au Bungalow {config.bungalowNumber}
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<WifiCard />
|
||||
|
||||
<WeatherWidget />
|
||||
|
||||
<section className="bg-secondary rounded-2xl p-6">
|
||||
<h2 className="text-xl font-semibold text-primary mb-3">
|
||||
Le mot du gérant
|
||||
</h2>
|
||||
<p className="text-gray-700 leading-relaxed">
|
||||
{config.gerantMessage}
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
67
app/api/excursions/route.ts
Normal file
67
app/api/excursions/route.ts
Normal file
@ -0,0 +1,67 @@
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
export interface Excursion {
|
||||
id: string;
|
||||
name: string;
|
||||
type: "tour-lagon" | "plongee" | "4x4";
|
||||
description: string;
|
||||
duration: string;
|
||||
price: number;
|
||||
available: boolean;
|
||||
}
|
||||
|
||||
const excursions: Excursion[] = [
|
||||
{
|
||||
id: "1",
|
||||
name: "Tour du Lagon de Fakarava",
|
||||
type: "tour-lagon",
|
||||
description: "Découvrez les merveilles du lagon de Fakarava avec arrêts snorkeling aux raies et requins. Visite des motus et des spots de plongée exceptionnels.",
|
||||
duration: "4 heures",
|
||||
price: 12000,
|
||||
available: true,
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
name: "Plongée à la Passe Sud (Tumakohua)",
|
||||
type: "plongee",
|
||||
description: "Expérience unique de plongée à la Passe Sud de Fakarava, réputée pour ses raies mantas et sa faune exceptionnelle. Accessible uniquement par bateau.",
|
||||
duration: "Journée complète",
|
||||
price: 15000,
|
||||
available: true,
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
name: "Excursion en vélo vers le Sud",
|
||||
type: "4x4",
|
||||
description: "Exploration de l'atoll de Fakarava en vélo le long de la route principale. Découvrez les villages et les points de vue sur le lagon.",
|
||||
duration: "3 heures",
|
||||
price: 8000,
|
||||
available: true,
|
||||
},
|
||||
];
|
||||
|
||||
export async function GET() {
|
||||
return NextResponse.json(excursions);
|
||||
}
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { excursionId, name, email, phone, date, participants } = body;
|
||||
|
||||
// Ici, en production, vous sauvegarderiez la réservation dans une base de données
|
||||
// Pour l'instant, on simule juste une réponse de succès
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: "Réservation enregistrée avec succès",
|
||||
reservationId: `RES-${Date.now()}`,
|
||||
});
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{ success: false, message: "Erreur lors de la réservation" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
198
app/api/infos/route.ts
Normal file
198
app/api/infos/route.ts
Normal file
@ -0,0 +1,198 @@
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
export interface FAQItem {
|
||||
id: string;
|
||||
question: string;
|
||||
answer: string;
|
||||
category?: string;
|
||||
icon?: string;
|
||||
}
|
||||
|
||||
export interface LexiqueItem {
|
||||
id: string;
|
||||
mot: string;
|
||||
traduction: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
const faq: FAQItem[] = [
|
||||
// Pension - Informations générales
|
||||
{
|
||||
id: "pension-1",
|
||||
question: "Heures de Check-out",
|
||||
answer: "Le check-out se fait avant 11h00. Merci de libérer votre bungalow à l'heure prévue pour permettre la préparation pour les prochains clients.",
|
||||
category: "Pension",
|
||||
icon: "🏠",
|
||||
},
|
||||
{
|
||||
id: "pension-2",
|
||||
question: "Petit-déjeuner",
|
||||
answer: "Le petit-déjeuner est servi de 7h30 à 9h30 dans la salle commune. Il comprend des fruits frais, du pain, des confitures locales et des boissons chaudes.",
|
||||
category: "Pension",
|
||||
icon: "🍽️",
|
||||
},
|
||||
{
|
||||
id: "pension-3",
|
||||
question: "Comment utiliser la climatisation",
|
||||
answer: "La télécommande de la climatisation se trouve sur la table de chevet. Appuyez sur le bouton ON/OFF pour l'activer. La température recommandée est de 24°C pour un confort optimal et une consommation raisonnable.",
|
||||
category: "Pension",
|
||||
icon: "❄️",
|
||||
},
|
||||
|
||||
// 💰 Argent, Banque & Paiement
|
||||
{
|
||||
id: "argent-1",
|
||||
question: "Où trouver un distributeur ?",
|
||||
answer: "Un seul distributeur automatique de billets (DAB) est disponible à Fakarava. Il est situé à l'entrée du Bureau de Poste (OPT) dans le village de Rotoava.",
|
||||
category: "💰 Argent, Banque & Paiement",
|
||||
icon: "💰",
|
||||
},
|
||||
{
|
||||
id: "argent-2",
|
||||
question: "Faut-il prévoir du liquide ?",
|
||||
answer: "OUI, le liquide (XPF) est ESSENTIEL. De nombreux snacks, petites pensions et activités n'acceptent pas la carte bancaire. Prévoyez de retirer assez à Tahiti ou Papeete avant d'arriver, ou dès votre arrivée à Rotoava.",
|
||||
category: "💰 Argent, Banque & Paiement",
|
||||
icon: "💰",
|
||||
},
|
||||
{
|
||||
id: "argent-3",
|
||||
question: "Les cartes bancaires sont-elles acceptées ?",
|
||||
answer: "Les hôtels, les grands prestataires de plongée et les magasins principaux acceptent la carte. Les petites roulottes et certains taxis exigent du liquide.",
|
||||
category: "💰 Argent, Banque & Paiement",
|
||||
icon: "💰",
|
||||
},
|
||||
|
||||
// 🌐 Communications & Internet
|
||||
{
|
||||
id: "internet-1",
|
||||
question: "Comment avoir du Wifi ?",
|
||||
answer: "Le Wifi est disponible exclusivement à la pension. Le code est accessible sur la page d'accueil de cette application (onglet Accueil). Attention : le débit est plus lent que sur les grandes îles.",
|
||||
category: "🌐 Communications & Internet",
|
||||
icon: "🌐",
|
||||
},
|
||||
{
|
||||
id: "internet-2",
|
||||
question: "Où acheter une carte SIM ?",
|
||||
answer: "Vous pouvez acheter une carte SIM locale (Vini ou Vodafone) au Bureau de Poste (OPT) de Rotoava, si vous souhaitez avoir la 4G sur l'atoll (coûteux).",
|
||||
category: "🌐 Communications & Internet",
|
||||
icon: "🌐",
|
||||
},
|
||||
{
|
||||
id: "internet-3",
|
||||
question: "Y a-t-il une couverture mobile ?",
|
||||
answer: "La couverture 4G est bonne et stable uniquement dans la zone de Rotoava (au Nord) et sur la route principale. Elle est quasi inexistante vers le Sud de l'atoll.",
|
||||
category: "🌐 Communications & Internet",
|
||||
icon: "🌐",
|
||||
},
|
||||
|
||||
// 🚲 Transport & Déplacements
|
||||
{
|
||||
id: "transport-1",
|
||||
question: "Quel est le meilleur moyen de se déplacer ?",
|
||||
answer: "Le vélo est le moyen de transport standard et le plus agréable. Le Nord de l'atoll (Rotoava) est plat et idéal pour le vélo.",
|
||||
category: "🚲 Transport & Déplacements",
|
||||
icon: "🚲",
|
||||
},
|
||||
{
|
||||
id: "transport-2",
|
||||
question: "Comment aller à la Passe Sud (Tumakohua) ?",
|
||||
answer: "La Passe Sud est à environ 60 km et est accessible uniquement par bateau. Les excursions sont organisées par les prestataires de plongée. Il est impossible d'y aller en voiture ou vélo depuis le Nord.",
|
||||
category: "🚲 Transport & Déplacements",
|
||||
icon: "🚲",
|
||||
},
|
||||
{
|
||||
id: "transport-3",
|
||||
question: "Où louer un vélo ou un scooter ?",
|
||||
answer: "Des vélos sont disponibles à la location directement à la pension. Contactez le gérant pour plus d'informations.",
|
||||
category: "🚲 Transport & Déplacements",
|
||||
icon: "🚲",
|
||||
},
|
||||
|
||||
// 🩹 Santé & Urgences
|
||||
{
|
||||
id: "sante-1",
|
||||
question: "Où trouver un médecin ?",
|
||||
answer: "Le dispensaire se trouve dans le village de Rotoava. En cas d'urgence grave, contactez le gérant immédiatement.",
|
||||
category: "🩹 Santé & Urgences",
|
||||
icon: "🩹",
|
||||
},
|
||||
{
|
||||
id: "sante-2",
|
||||
question: "Y a-t-il une pharmacie ?",
|
||||
answer: "Il n'y a pas de pharmacie à Fakarava. Seule l'infirmerie du dispensaire dispose d'une petite réserve de médicaments de base.",
|
||||
category: "🩹 Santé & Urgences",
|
||||
icon: "🩹",
|
||||
},
|
||||
{
|
||||
id: "sante-3",
|
||||
question: "Numéros d'urgence",
|
||||
answer: "Gérant : Contactez le gérant via l'application ou le numéro fourni à l'arrivée. Dispensaire : Situé à Rotoava. SAMU : 15. Police : 17.",
|
||||
category: "🩹 Santé & Urgences",
|
||||
icon: "🩹",
|
||||
},
|
||||
|
||||
// 🌿 Consignes Éco-Lagon
|
||||
{
|
||||
id: "eco-1",
|
||||
question: "Comment protéger le lagon ?",
|
||||
answer: "Fakarava est une Réserve de Biosphère UNESCO ! Nous vous demandons de ne jamais toucher les coraux (morts ou vivants), de ne pas nourrir les poissons, et de toujours emporter vos déchets avec vous.",
|
||||
category: "🌿 Consignes Éco-Lagon",
|
||||
icon: "🌿",
|
||||
},
|
||||
{
|
||||
id: "eco-2",
|
||||
question: "Consommation d'eau",
|
||||
answer: "L'eau potable est précieuse ici. Merci de limiter la durée de vos douches et de toujours fermer les robinets après usage.",
|
||||
category: "🌿 Consignes Éco-Lagon",
|
||||
icon: "🌿",
|
||||
},
|
||||
];
|
||||
|
||||
const lexique: LexiqueItem[] = [
|
||||
{
|
||||
id: "1",
|
||||
mot: "Ia Ora Na",
|
||||
traduction: "Bonjour / Salut",
|
||||
description: "Salutation traditionnelle tahitienne utilisée pour dire bonjour.",
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
mot: "Mauruuru",
|
||||
traduction: "Merci",
|
||||
description: "Expression de gratitude en tahitien.",
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
mot: "Maeva",
|
||||
traduction: "Bienvenue",
|
||||
description: "Mot utilisé pour souhaiter la bienvenue aux visiteurs.",
|
||||
},
|
||||
{
|
||||
id: "4",
|
||||
mot: "Aita",
|
||||
traduction: "Non",
|
||||
description: "Négation en tahitien.",
|
||||
},
|
||||
{
|
||||
id: "5",
|
||||
mot: "E",
|
||||
traduction: "Oui",
|
||||
description: "Affirmation en tahitien.",
|
||||
},
|
||||
];
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const type = searchParams.get("type");
|
||||
|
||||
if (type === "faq") {
|
||||
return NextResponse.json(faq);
|
||||
}
|
||||
|
||||
if (type === "lexique") {
|
||||
return NextResponse.json(lexique);
|
||||
}
|
||||
|
||||
return NextResponse.json({ faq, lexique });
|
||||
}
|
||||
|
||||
76
app/api/notifications/route.ts
Normal file
76
app/api/notifications/route.ts
Normal file
@ -0,0 +1,76 @@
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
export interface Notification {
|
||||
id: string;
|
||||
title: string;
|
||||
message: string;
|
||||
type: "whale" | "weather" | "excursion" | "info";
|
||||
timestamp: string;
|
||||
read: boolean;
|
||||
}
|
||||
|
||||
// Notifications stockées (en production, utiliser une base de données)
|
||||
let notifications: Notification[] = [
|
||||
{
|
||||
id: "1",
|
||||
title: "Observation de baleines",
|
||||
message: "Les baleines ont été vues au nord de l'île ce matin !",
|
||||
type: "whale",
|
||||
timestamp: new Date().toISOString(),
|
||||
read: false,
|
||||
},
|
||||
];
|
||||
|
||||
export async function GET() {
|
||||
return NextResponse.json(notifications);
|
||||
}
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { title, message, type } = body;
|
||||
|
||||
const notification: Notification = {
|
||||
id: `notif-${Date.now()}`,
|
||||
title,
|
||||
message,
|
||||
type: type || "info",
|
||||
timestamp: new Date().toISOString(),
|
||||
read: false,
|
||||
};
|
||||
|
||||
notifications.unshift(notification);
|
||||
|
||||
// Limiter à 50 notifications
|
||||
if (notifications.length > 50) {
|
||||
notifications = notifications.slice(0, 50);
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true, notification });
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{ success: false, message: "Erreur lors de la création de la notification" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function PATCH(request: Request) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { id, read } = body;
|
||||
|
||||
const notification = notifications.find((n) => n.id === id);
|
||||
if (notification) {
|
||||
notification.read = read;
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{ success: false, message: "Erreur lors de la mise à jour" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
85
app/api/places/route.ts
Normal file
85
app/api/places/route.ts
Normal file
@ -0,0 +1,85 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { FAKARAVA_SPOTS } from "@/lib/data/fakarava-spots";
|
||||
|
||||
export interface Place {
|
||||
id: string;
|
||||
name: string;
|
||||
category: string;
|
||||
description: string;
|
||||
image: string;
|
||||
location: {
|
||||
lat: number;
|
||||
lng: number;
|
||||
address: string;
|
||||
};
|
||||
type?: string;
|
||||
keywords?: string[];
|
||||
contact?: string;
|
||||
gmapLink?: string;
|
||||
conseil?: string;
|
||||
horaires?: string;
|
||||
}
|
||||
|
||||
// Coordonnées approximatives de Fakarava (Rotoava)
|
||||
const FAKARAVA_CENTER = {
|
||||
lat: -16.3167,
|
||||
lng: -145.6167,
|
||||
};
|
||||
|
||||
// Convertir les spots de Fakarava en format Place
|
||||
const convertFakaravaSpots = (): Place[] => {
|
||||
return FAKARAVA_SPOTS.map((spot, index) => {
|
||||
// Mapper la catégorie "Restauration" vers "restaurants"
|
||||
const categoryMap: Record<string, string> = {
|
||||
"Restauration": "restaurants",
|
||||
"Plages": "plages",
|
||||
"Epiceries": "epiceries",
|
||||
"Activités": "activites",
|
||||
};
|
||||
|
||||
// Déterminer l'image par défaut selon la catégorie
|
||||
const getDefaultImage = (category: string) => {
|
||||
if (category === "plages") return "/placeholder-beach.jpg";
|
||||
if (category === "epiceries") return "/placeholder-store.jpg";
|
||||
return "/placeholder-restaurant.jpg";
|
||||
};
|
||||
|
||||
return {
|
||||
id: `fakarava-${index + 1}`,
|
||||
name: spot.name,
|
||||
category: categoryMap[spot.category] || spot.category.toLowerCase(),
|
||||
type: spot.type,
|
||||
description: spot.description,
|
||||
keywords: spot.keywords,
|
||||
contact: spot.contact,
|
||||
conseil: spot.conseil,
|
||||
horaires: spot.horaires,
|
||||
image: getDefaultImage(categoryMap[spot.category] || spot.category.toLowerCase()),
|
||||
location: {
|
||||
// Coordonnées approximatives basées sur le PK
|
||||
lat: FAKARAVA_CENTER.lat + (index * 0.01),
|
||||
lng: FAKARAVA_CENTER.lng + (index * 0.01),
|
||||
address: spot.location,
|
||||
},
|
||||
gmapLink: spot.gmapLink,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const places: Place[] = [
|
||||
...convertFakaravaSpots(),
|
||||
// Ajoutez ici d'autres lieux spécifiques à Fakarava
|
||||
];
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const category = searchParams.get("category");
|
||||
|
||||
let filteredPlaces = places;
|
||||
if (category && category !== "all") {
|
||||
filteredPlaces = places.filter((place) => place.category === category);
|
||||
}
|
||||
|
||||
return NextResponse.json(filteredPlaces);
|
||||
}
|
||||
|
||||
40
app/api/sun-times/route.ts
Normal file
40
app/api/sun-times/route.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
export interface SunTimes {
|
||||
date: string;
|
||||
sunrise: string;
|
||||
sunset: string;
|
||||
}
|
||||
|
||||
// Calcul approximatif du lever/coucher du soleil pour Fakarava
|
||||
// Coordonnées de Fakarava (Rotoava) : -16.3167, -145.6167
|
||||
// En production, utiliser une API comme https://sunrise-sunset.org/api
|
||||
const calculateSunTimes = (date: Date, lat: number = -16.3167, lng: number = -145.6167): SunTimes => {
|
||||
// Calcul simplifié (formule approximative)
|
||||
const dayOfYear = Math.floor((date.getTime() - new Date(date.getFullYear(), 0, 0).getTime()) / 86400000);
|
||||
const declination = 23.45 * Math.sin((360 * (284 + dayOfYear) / 365) * Math.PI / 180);
|
||||
const hourAngle = Math.acos(-Math.tan(lat * Math.PI / 180) * Math.tan(declination * Math.PI / 180)) * 180 / Math.PI;
|
||||
|
||||
const sunriseHour = 12 - hourAngle / 15 - (lng / 15);
|
||||
const sunsetHour = 12 + hourAngle / 15 - (lng / 15);
|
||||
|
||||
const formatTime = (hour: number) => {
|
||||
const h = Math.floor(hour);
|
||||
const m = Math.floor((hour - h) * 60);
|
||||
return `${h.toString().padStart(2, "0")}:${m.toString().padStart(2, "0")}`;
|
||||
};
|
||||
|
||||
return {
|
||||
date: date.toISOString().split("T")[0],
|
||||
sunrise: formatTime(sunriseHour),
|
||||
sunset: formatTime(sunsetHour),
|
||||
};
|
||||
};
|
||||
|
||||
export async function GET() {
|
||||
const today = new Date();
|
||||
const sunTimes = calculateSunTimes(today);
|
||||
|
||||
return NextResponse.json(sunTimes);
|
||||
}
|
||||
|
||||
44
app/api/tides/route.ts
Normal file
44
app/api/tides/route.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
export interface TideData {
|
||||
date: string;
|
||||
highTide: { time: string; height: number };
|
||||
lowTide: { time: string; height: number };
|
||||
}
|
||||
|
||||
// Données de marées pour les 7 prochains jours
|
||||
// En production, utiliser une API réelle comme https://www.tide-forecast.com/api
|
||||
const generateTideData = (): TideData[] => {
|
||||
const tides: TideData[] = [];
|
||||
const today = new Date();
|
||||
|
||||
for (let i = 0; i < 7; i++) {
|
||||
const date = new Date(today);
|
||||
date.setDate(today.getDate() + i);
|
||||
|
||||
// Simulation de données de marées (à remplacer par une vraie API)
|
||||
const dayOffset = i;
|
||||
const highTideHour = (6 + dayOffset * 0.8) % 24;
|
||||
const lowTideHour = (12 + dayOffset * 0.8) % 24;
|
||||
|
||||
tides.push({
|
||||
date: date.toISOString().split("T")[0],
|
||||
highTide: {
|
||||
time: `${Math.floor(highTideHour).toString().padStart(2, "0")}:${Math.floor((highTideHour % 1) * 60).toString().padStart(2, "0")}`,
|
||||
height: 1.2 + Math.random() * 0.3,
|
||||
},
|
||||
lowTide: {
|
||||
time: `${Math.floor(lowTideHour).toString().padStart(2, "0")}:${Math.floor((lowTideHour % 1) * 60).toString().padStart(2, "0")}`,
|
||||
height: 0.3 + Math.random() * 0.2,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return tides;
|
||||
};
|
||||
|
||||
export async function GET() {
|
||||
const tides = generateTideData();
|
||||
return NextResponse.json(tides);
|
||||
}
|
||||
|
||||
39
app/explorer/page.tsx
Normal file
39
app/explorer/page.tsx
Normal file
@ -0,0 +1,39 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import dynamic from "next/dynamic";
|
||||
import Layout from "@/components/layout/Layout";
|
||||
import CategoryList from "@/components/explorer/CategoryList";
|
||||
|
||||
const PlaceList = dynamic(() => import("@/components/explorer/PlaceList"), {
|
||||
loading: () => (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<p className="text-gray-600">Chargement...</p>
|
||||
</div>
|
||||
),
|
||||
});
|
||||
|
||||
export default function ExplorerPage() {
|
||||
const [selectedCategory, setSelectedCategory] = useState("all");
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<div className="py-6">
|
||||
<header className="px-4 mb-6">
|
||||
<h1 className="text-2xl font-bold text-primary mb-2">Explorer</h1>
|
||||
<p className="text-gray-600">
|
||||
Découvrez les meilleurs endroits de Fakarava
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<CategoryList
|
||||
selectedCategory={selectedCategory}
|
||||
onCategoryChange={setSelectedCategory}
|
||||
/>
|
||||
|
||||
<PlaceList category={selectedCategory} />
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
19
app/globals.css
Normal file
19
app/globals.css
Normal file
@ -0,0 +1,19 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
body {
|
||||
@apply bg-background;
|
||||
font-size: 16px;
|
||||
min-height: 100vh;
|
||||
color: #1f2937;
|
||||
}
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
.text-balance {
|
||||
text-wrap: balance;
|
||||
}
|
||||
}
|
||||
|
||||
44
app/infos/page.tsx
Normal file
44
app/infos/page.tsx
Normal file
@ -0,0 +1,44 @@
|
||||
import dynamic from "next/dynamic";
|
||||
import Layout from "@/components/layout/Layout";
|
||||
import ContactSection from "@/components/infos/ContactSection";
|
||||
|
||||
const FAQAccordion = dynamic(() => import("@/components/infos/FAQAccordion"), {
|
||||
loading: () => <div className="h-64 bg-gray-100 rounded-2xl animate-pulse" />,
|
||||
});
|
||||
|
||||
const LexiqueSection = dynamic(() => import("@/components/infos/LexiqueSection"), {
|
||||
loading: () => <div className="h-48 bg-gray-100 rounded-2xl animate-pulse" />,
|
||||
});
|
||||
|
||||
export default function InfosPage() {
|
||||
return (
|
||||
<Layout>
|
||||
<div className="px-4 py-6 space-y-8">
|
||||
<header>
|
||||
<h1 className="text-2xl font-bold text-primary mb-2">
|
||||
Infos Pratiques
|
||||
</h1>
|
||||
<p className="text-gray-600">
|
||||
Tout ce que vous devez savoir pour votre séjour
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<section>
|
||||
<h2 className="text-xl font-semibold text-primary mb-4">
|
||||
Questions Fréquentes
|
||||
</h2>
|
||||
<FAQAccordion />
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<LexiqueSection />
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<ContactSection />
|
||||
</section>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
44
app/layout.tsx
Normal file
44
app/layout.tsx
Normal file
@ -0,0 +1,44 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Inter } from "next/font/google";
|
||||
import "./globals.css";
|
||||
import PWARegister from "@/components/PWARegister";
|
||||
|
||||
const inter = Inter({ subsets: ["latin"] });
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Compagnon du lagon - Pension Marama",
|
||||
description: "Votre guide numérique pour votre séjour à Fakarava",
|
||||
manifest: "/manifest.json",
|
||||
themeColor: "#0E7490",
|
||||
appleWebApp: {
|
||||
capable: true,
|
||||
statusBarStyle: "default",
|
||||
title: "Compagnon du lagon - Pension Marama",
|
||||
},
|
||||
viewport: {
|
||||
width: "device-width",
|
||||
initialScale: 1,
|
||||
maximumScale: 1,
|
||||
userScalable: false,
|
||||
},
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<link rel="apple-touch-icon" href="/icon-192x192.png" />
|
||||
</head>
|
||||
<body className={inter.className}>
|
||||
{children}
|
||||
<PWARegister />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
||||
44
app/mana-tracker/page.tsx
Normal file
44
app/mana-tracker/page.tsx
Normal file
@ -0,0 +1,44 @@
|
||||
import dynamic from "next/dynamic";
|
||||
import Layout from "@/components/layout/Layout";
|
||||
import TideWidget from "@/components/mana-tracker/TideWidget";
|
||||
import SunTimesWidget from "@/components/mana-tracker/SunTimesWidget";
|
||||
|
||||
const ExcursionBooking = dynamic(
|
||||
() => import("@/components/mana-tracker/ExcursionBooking"),
|
||||
{
|
||||
loading: () => <div className="h-64 bg-gray-100 rounded-2xl animate-pulse" />,
|
||||
}
|
||||
);
|
||||
|
||||
const PushNotificationManager = dynamic(
|
||||
() => import("@/components/mana-tracker/PushNotificationManager"),
|
||||
{
|
||||
loading: () => <div className="h-48 bg-gray-100 rounded-2xl animate-pulse" />,
|
||||
}
|
||||
);
|
||||
|
||||
export default function ManaTrackerPage() {
|
||||
return (
|
||||
<Layout>
|
||||
<div className="px-4 py-6 space-y-6">
|
||||
<header>
|
||||
<h1 className="text-2xl font-bold text-primary mb-2">
|
||||
Mana Tracker
|
||||
</h1>
|
||||
<p className="text-gray-600">
|
||||
Activités & Météo - Tout dépend de la mer et du soleil
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<TideWidget />
|
||||
|
||||
<SunTimesWidget />
|
||||
|
||||
<ExcursionBooking />
|
||||
|
||||
<PushNotificationManager />
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
20
app/offline/page.tsx
Normal file
20
app/offline/page.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
import Layout from "@/components/layout/Layout";
|
||||
|
||||
export default function OfflinePage() {
|
||||
return (
|
||||
<Layout>
|
||||
<div className="flex flex-col items-center justify-center min-h-[60vh] px-4 text-center">
|
||||
<h1 className="text-2xl font-bold text-primary mb-4">
|
||||
Mode hors ligne
|
||||
</h1>
|
||||
<p className="text-gray-600 mb-6">
|
||||
Vous êtes actuellement hors ligne. Certaines fonctionnalités peuvent être limitées.
|
||||
</p>
|
||||
<p className="text-sm text-gray-500">
|
||||
Les données mises en cache restent disponibles.
|
||||
</p>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
6
app/page.tsx
Normal file
6
app/page.tsx
Normal file
@ -0,0 +1,6 @@
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
export default function Home() {
|
||||
redirect("/accueil");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user