Build APK Android fonctionnel - Corrections finales

- Ajout de Java 21 dans Dockerfile pour compatibilité Capacitor
- Création de fichiers de types séparés (lib/types/) pour éviter dépendances API routes
- Configuration next.config.export.js pour export statique
- Exclusion temporaire des routes API pendant le build
- Correction configuration Gradle (Java 17/21)
- Script build-apk.sh amélioré avec gestion des routes API
- APK généré avec succès (4.5MB) dans dist/compagnon-admin-debug.apk

Fichiers de types créés:
- lib/types/place.ts
- lib/types/infos.ts
- lib/types/tides.ts
- lib/types/excursions.ts
- lib/types/sun-times.ts
- lib/types/notifications.ts

Tous les imports mis à jour pour utiliser les nouveaux fichiers de types.
This commit is contained in:
2025-11-23 10:07:34 +01:00
parent 51a74342f4
commit 115d8c05a7
83 changed files with 1143 additions and 679 deletions

View File

@ -1,55 +0,0 @@
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 });
}

View File

@ -1,60 +0,0 @@
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 }
);
}
}

View File

@ -1,26 +0,0 @@
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,
});
}

View File

@ -1,67 +0,0 @@
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 }
);
}
}

View File

@ -1,191 +0,0 @@
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: "🍽️",
},
// 💰 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 });
}

View File

@ -1,76 +0,0 @@
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 }
);
}
}

View File

@ -1,85 +0,0 @@
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);
}

View File

@ -1,40 +0,0 @@
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);
}

View File

@ -1,44 +0,0 @@
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);
}