first commit
This commit is contained in:
259
components/mana-tracker/ExcursionBooking.tsx
Normal file
259
components/mana-tracker/ExcursionBooking.tsx
Normal file
@ -0,0 +1,259 @@
|
||||
"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 "@/app/api/excursions/route";
|
||||
|
||||
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("/api/excursions");
|
||||
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);
|
||||
try {
|
||||
const response = await fetch("/api/excursions", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
excursionId: selectedExcursion.id,
|
||||
...formData,
|
||||
}),
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
setSuccess(true);
|
||||
setFormData({ name: "", email: "", phone: "", date: "", participants: 1 });
|
||||
setTimeout(() => {
|
||||
setSuccess(false);
|
||||
setSelectedExcursion(null);
|
||||
}, 3000);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Erreur lors de la réservation:", error);
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
231
components/mana-tracker/PushNotificationManager.tsx
Normal file
231
components/mana-tracker/PushNotificationManager.tsx
Normal file
@ -0,0 +1,231 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { Bell, BellOff, X } from "lucide-react";
|
||||
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Notification } from "@/app/api/notifications/route";
|
||||
|
||||
export default function PushNotificationManager() {
|
||||
const [notifications, setNotifications] = useState<Notification[]>([]);
|
||||
const [permission, setPermission] = useState<NotificationPermission>("default");
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window !== "undefined" && "Notification" in window && window.Notification) {
|
||||
setPermission(window.Notification.permission);
|
||||
}
|
||||
fetchNotifications();
|
||||
}, []);
|
||||
|
||||
const fetchNotifications = async () => {
|
||||
try {
|
||||
const response = await fetch("/api/notifications");
|
||||
const data = await response.json();
|
||||
setNotifications(data);
|
||||
} catch (error) {
|
||||
console.error("Erreur lors du chargement des notifications:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const requestPermission = async () => {
|
||||
if (typeof window === "undefined" || !("Notification" in window) || !window.Notification) {
|
||||
alert("Votre navigateur ne supporte pas les notifications");
|
||||
return;
|
||||
}
|
||||
|
||||
const permission = await window.Notification.requestPermission();
|
||||
setPermission(permission);
|
||||
|
||||
if (permission === "granted") {
|
||||
// Enregistrer le service worker pour les notifications push
|
||||
if ("serviceWorker" in navigator) {
|
||||
try {
|
||||
const registration = await navigator.serviceWorker.ready;
|
||||
|
||||
// Vérifier périodiquement les nouvelles notifications
|
||||
setInterval(async () => {
|
||||
try {
|
||||
const response = await fetch("/api/notifications");
|
||||
const newNotifications = await response.json();
|
||||
const unread = newNotifications.filter((n: Notification) => !n.read);
|
||||
|
||||
// Afficher une notification pour chaque nouvelle alerte non lue
|
||||
if (typeof window !== "undefined" && window.Notification) {
|
||||
unread.forEach((notification: Notification) => {
|
||||
if (window.Notification.permission === "granted") {
|
||||
new window.Notification(notification.title, {
|
||||
body: notification.message,
|
||||
icon: "/icon-192x192.png",
|
||||
badge: "/icon-192x192.png",
|
||||
tag: notification.id,
|
||||
requireInteraction: notification.type === "whale",
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Erreur lors de la vérification des notifications:", error);
|
||||
}
|
||||
}, 60000); // Vérifier toutes les minutes
|
||||
} catch (error) {
|
||||
console.error("Erreur lors de l'enregistrement du service worker:", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const showTestNotification = () => {
|
||||
if (permission === "granted" && typeof window !== "undefined" && window.Notification) {
|
||||
new window.Notification("Test de notification", {
|
||||
body: "Les notifications fonctionnent correctement !",
|
||||
icon: "/icon-192x192.png",
|
||||
badge: "/icon-192x192.png",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const markAsRead = async (id: string) => {
|
||||
try {
|
||||
await fetch("/api/notifications", {
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ id, read: true }),
|
||||
});
|
||||
setNotifications(
|
||||
notifications.map((n) => (n.id === id ? { ...n, read: true } : n))
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Erreur lors de la mise à jour:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const getNotificationIcon = (type: string) => {
|
||||
switch (type) {
|
||||
case "whale":
|
||||
return "🐋";
|
||||
case "weather":
|
||||
return "🌦️";
|
||||
case "excursion":
|
||||
return "🚤";
|
||||
default:
|
||||
return "ℹ️";
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="p-6">
|
||||
<div className="animate-pulse">Chargement...</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
const unreadCount = notifications.filter((n) => !n.read).length;
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center justify-between">
|
||||
<span className="flex items-center gap-2">
|
||||
<Bell className="h-6 w-6 text-primary" />
|
||||
Notifications
|
||||
</span>
|
||||
{unreadCount > 0 && (
|
||||
<span className="bg-primary text-white text-xs font-bold px-2 py-1 rounded-full">
|
||||
{unreadCount}
|
||||
</span>
|
||||
)}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{permission === "default" && (
|
||||
<div className="bg-secondary rounded-xl p-4">
|
||||
<p className="text-sm text-gray-700 mb-3">
|
||||
Activez les notifications pour recevoir des alertes importantes (baleines, météo, etc.)
|
||||
</p>
|
||||
<Button onClick={requestPermission} className="w-full">
|
||||
<Bell className="mr-2 h-4 w-4" />
|
||||
Activer les notifications
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{permission === "denied" && (
|
||||
<div className="bg-red-50 border border-red-200 rounded-xl p-4">
|
||||
<p className="text-sm text-red-700">
|
||||
Les notifications sont désactivées. Veuillez les activer dans les paramètres de votre navigateur.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{permission === "granted" && (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-sm text-gray-600">Notifications activées</p>
|
||||
<Button
|
||||
onClick={showTestNotification}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
>
|
||||
Tester
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-2 max-h-64 overflow-y-auto">
|
||||
{notifications.length === 0 ? (
|
||||
<p className="text-sm text-gray-500 text-center py-4">
|
||||
Aucune notification pour le moment
|
||||
</p>
|
||||
) : (
|
||||
notifications.map((notification) => (
|
||||
<div
|
||||
key={notification.id}
|
||||
className={`border rounded-xl p-3 ${
|
||||
notification.read
|
||||
? "bg-gray-50 border-gray-200"
|
||||
: "bg-secondary border-primary/30"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className="text-lg">
|
||||
{getNotificationIcon(notification.type)}
|
||||
</span>
|
||||
<h4 className="font-semibold text-sm">{notification.title}</h4>
|
||||
{!notification.read && (
|
||||
<span className="bg-primary text-white text-xs px-1.5 py-0.5 rounded-full">
|
||||
Nouveau
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm text-gray-700">{notification.message}</p>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
{new Date(notification.timestamp).toLocaleString("fr-FR")}
|
||||
</p>
|
||||
</div>
|
||||
{!notification.read && (
|
||||
<button
|
||||
onClick={() => markAsRead(notification.id)}
|
||||
className="text-gray-400 hover:text-gray-600"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
80
components/mana-tracker/SunTimesWidget.tsx
Normal file
80
components/mana-tracker/SunTimesWidget.tsx
Normal file
@ -0,0 +1,80 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { Sun, Sunrise, Sunset } from "lucide-react";
|
||||
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
|
||||
import { SunTimes } from "@/app/api/sun-times/route";
|
||||
|
||||
export default function SunTimesWidget() {
|
||||
const [sunTimes, setSunTimes] = useState<SunTimes | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchSunTimes = async () => {
|
||||
try {
|
||||
const response = await fetch("/api/sun-times");
|
||||
const data = await response.json();
|
||||
setSunTimes(data);
|
||||
} catch (error) {
|
||||
console.error("Erreur lors du chargement des heures du soleil:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchSunTimes();
|
||||
}, []);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="p-6">
|
||||
<div className="animate-pulse">Chargement...</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
if (!sunTimes) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const getCurrentTime = () => {
|
||||
const now = new Date();
|
||||
return `${now.getHours().toString().padStart(2, "0")}:${now.getMinutes().toString().padStart(2, "0")}`;
|
||||
};
|
||||
|
||||
const currentTime = getCurrentTime();
|
||||
const isDay = currentTime >= sunTimes.sunrise && currentTime < sunTimes.sunset;
|
||||
|
||||
return (
|
||||
<Card className="bg-gradient-to-br from-yellow-50 to-orange-50">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Sun className="h-6 w-6 text-yellow-500" />
|
||||
Lever / Coucher du Soleil
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="bg-white rounded-xl p-4 text-center">
|
||||
<Sunrise className="h-8 w-8 text-yellow-500 mx-auto mb-2" />
|
||||
<p className="text-xs text-gray-600 mb-1">Lever</p>
|
||||
<p className="text-2xl font-bold text-primary">{sunTimes.sunrise}</p>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl p-4 text-center">
|
||||
<Sunset className="h-8 w-8 text-orange-500 mx-auto mb-2" />
|
||||
<p className="text-xs text-gray-600 mb-1">Coucher</p>
|
||||
<p className="text-2xl font-bold text-primary">{sunTimes.sunset}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 text-center">
|
||||
<p className="text-sm text-gray-600">
|
||||
{isDay ? "☀️ Soleil actuellement visible" : "🌙 Nuit"}
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
100
components/mana-tracker/TideWidget.tsx
Normal file
100
components/mana-tracker/TideWidget.tsx
Normal file
@ -0,0 +1,100 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { Waves, TrendingUp, TrendingDown } from "lucide-react";
|
||||
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
|
||||
import { TideData } from "@/app/api/tides/route";
|
||||
|
||||
export default function TideWidget() {
|
||||
const [tides, setTides] = useState<TideData[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchTides = async () => {
|
||||
try {
|
||||
const response = await fetch("/api/tides");
|
||||
const data = await response.json();
|
||||
setTides(data);
|
||||
} catch (error) {
|
||||
console.error("Erreur lors du chargement des marées:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchTides();
|
||||
}, []);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="p-6">
|
||||
<div className="animate-pulse">Chargement des marées...</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
const todayTide = tides[0];
|
||||
const tomorrowTide = tides[1];
|
||||
|
||||
if (!todayTide) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleDateString("fr-FR", { weekday: "short", day: "numeric", month: "short" });
|
||||
};
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Waves className="h-6 w-6 text-primary" />
|
||||
Marées
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div>
|
||||
<p className="text-sm text-gray-600 mb-2">Aujourd'hui - {formatDate(todayTide.date)}</p>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="bg-secondary rounded-xl p-3">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<TrendingUp className="h-4 w-4 text-primary" />
|
||||
<span className="text-sm font-semibold text-primary">Haute mer</span>
|
||||
</div>
|
||||
<p className="text-lg font-bold">{todayTide.highTide.time}</p>
|
||||
<p className="text-xs text-gray-600">{todayTide.highTide.height.toFixed(1)}m</p>
|
||||
</div>
|
||||
<div className="bg-secondary rounded-xl p-3">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<TrendingDown className="h-4 w-4 text-primary" />
|
||||
<span className="text-sm font-semibold text-primary">Basse mer</span>
|
||||
</div>
|
||||
<p className="text-lg font-bold">{todayTide.lowTide.time}</p>
|
||||
<p className="text-xs text-gray-600">{todayTide.lowTide.height.toFixed(1)}m</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{tomorrowTide && (
|
||||
<div className="pt-4 border-t border-gray-200">
|
||||
<p className="text-sm text-gray-600 mb-2">Demain - {formatDate(tomorrowTide.date)}</p>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="bg-gray-50 rounded-xl p-3">
|
||||
<p className="text-xs text-gray-600 mb-1">Haute mer</p>
|
||||
<p className="text-base font-semibold">{tomorrowTide.highTide.time}</p>
|
||||
</div>
|
||||
<div className="bg-gray-50 rounded-xl p-3">
|
||||
<p className="text-xs text-gray-600 mb-1">Basse mer</p>
|
||||
<p className="text-base font-semibold">{tomorrowTide.lowTide.time}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user