mirror of
https://github.com/Ladebeze66/devsite.git
synced 2025-12-15 13:36:49 +01:00
comok
This commit is contained in:
parent
ae85204879
commit
7e1369b329
@ -1,63 +1,71 @@
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
||||||
async function getAllCompetences() {
|
// Fonction pour récupérer toutes les compétences depuis l'API Strapi
|
||||||
try {
|
async function getAllCompetences() {
|
||||||
const response = await fetch("http://localhost:1337/api/competences?populate=*");
|
try {
|
||||||
if (!response.ok) {
|
const response = await fetch("http://localhost:1337/api/competences?populate=*");
|
||||||
throw new Error("Failed to fetch competences");
|
if (!response.ok) {
|
||||||
}
|
throw new Error("Failed to fetch competences");
|
||||||
const competences = await response.json();
|
}
|
||||||
return competences.data;
|
const competences = await response.json();
|
||||||
} catch (error) {
|
return competences.data;
|
||||||
console.error("Error fetching competences:", error);
|
} catch (error) {
|
||||||
return [];
|
console.error("Error fetching competences:", error);
|
||||||
}
|
return [];
|
||||||
}
|
}
|
||||||
|
}
|
||||||
export default async function Page() {
|
|
||||||
const competences = await getAllCompetences();
|
// Composant principal de la page des compétences
|
||||||
|
export default async function Page() {
|
||||||
return (
|
const competences = await getAllCompetences();
|
||||||
<div>
|
|
||||||
<h1 className="text-3xl mb-6 font-bold text-grey-700">Mes Compétences</h1>
|
return (
|
||||||
<div className="grid grid-cols-2 gap-6">
|
<div>
|
||||||
{competences.map((competence) => {
|
{/* Titre de la page */}
|
||||||
const picture = competence.picture?.[0]; // Récupère la première image si elle existe
|
<h1 className="text-3xl mb-6 font-bold text-grey-700">Mes Compétences</h1>
|
||||||
const largeImageUrl = picture?.formats?.large?.url; // Vérifie que le format "large" existe
|
{/* Grille pour afficher les compétences */}
|
||||||
const originalImageUrl = picture?.url; // URL de l'image originale
|
<div className="grid grid-cols-2 gap-6">
|
||||||
|
{competences.map((competence) => {
|
||||||
// Utilisez l'URL de l'image originale si disponible, sinon l'URL de l'image large
|
const picture = competence.picture?.[0]; // Récupère la première image si elle existe
|
||||||
const imageUrl = originalImageUrl
|
const largeImageUrl = picture?.formats?.large?.url; // Vérifie que le format "large" existe
|
||||||
? `http://localhost:1337${originalImageUrl}`
|
const originalImageUrl = picture?.url; // URL de l'image originale
|
||||||
: `http://localhost:1337${largeImageUrl}`;
|
|
||||||
|
// Utilisez l'URL de l'image originale si disponible, sinon l'URL de l'image large
|
||||||
return (
|
const imageUrl = originalImageUrl
|
||||||
<div key={competence.id} className="bg-white rounded-lg shadow-md overflow-hidden transform transition-transform duration-300 hover:scale-105 hover:shadow-lg hover:bg-blue-100">
|
? `http://localhost:1337${originalImageUrl}`
|
||||||
<Link href={`/competences/${competence.slug}`}>
|
: `http://localhost:1337${largeImageUrl}`;
|
||||||
<div className="overflow-hidden">
|
|
||||||
{imageUrl ? (
|
return (
|
||||||
<img
|
<div key={competence.id} className="bg-white rounded-lg shadow-md overflow-hidden transform transition-transform duration-300 hover:scale-105 hover:shadow-lg hover:bg-blue-100">
|
||||||
src={imageUrl}
|
{/* Lien vers la page de détail de la compétence */}
|
||||||
alt={picture?.name || "Competence image"}
|
<Link href={`/competences/${competence.slug}`}>
|
||||||
className="w-full h-48 object-cover transform transition-transform duration-300 hover:scale-125 hover:rotate-12"
|
<div className="overflow-hidden">
|
||||||
/>
|
{/* Affichage de l'image de la compétence */}
|
||||||
) : (
|
{imageUrl ? (
|
||||||
<div className="bg-gray-200 text-gray-500 text-center rounded-md shadow-md p-4">
|
<img
|
||||||
Image indisponible
|
src={imageUrl}
|
||||||
</div>
|
alt={picture?.name || "Competence image"}
|
||||||
)}
|
className="w-full h-48 object-cover transform transition-transform duration-300 hover:scale-125 hover:rotate-12"
|
||||||
</div>
|
/>
|
||||||
<div className="p-4">
|
) : (
|
||||||
<p className="font-bold text-xl mb-2">{competence.name}</p>
|
<div className="bg-gray-200 text-gray-500 text-center rounded-md shadow-md p-4">
|
||||||
<p className="text-gray-700">{competence.description}</p>
|
Image indisponible
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
<div className="p-4">
|
||||||
})}
|
{/* Affichage du nom de la compétence */}
|
||||||
</div>
|
<p className="font-bold text-xl mb-2">{competence.name}</p>
|
||||||
</div>
|
{/* Affichage de la description de la compétence */}
|
||||||
);
|
<p className="text-gray-700">{competence.description}</p>
|
||||||
}
|
</div>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1,24 +1,32 @@
|
|||||||
export default async function MessagesPage() {
|
// Composant principal de la page des messages
|
||||||
const res = await fetch("http://localhost:1337/api/messages");
|
export default async function MessagesPage() {
|
||||||
const { data } = await res.json();
|
// Récupération des messages depuis l'API Strapi
|
||||||
|
const res = await fetch("http://localhost:1337/api/messages");
|
||||||
return (
|
const { data } = await res.json();
|
||||||
<div className="max-w-3xl mx-auto p-6">
|
|
||||||
<h1 className="text-3xl font-bold text-center mb-6">📬 Messages reçus</h1>
|
return (
|
||||||
{data.length === 0 ? (
|
<div className="max-w-3xl mx-auto p-6">
|
||||||
<p className="text-center text-gray-600">Aucun message reçu.</p>
|
{/* Titre de la page */}
|
||||||
) : (
|
<h1 className="text-3xl font-bold text-center mb-6">📬 Messages reçus</h1>
|
||||||
<ul className="space-y-4">
|
|
||||||
{data.map((msg: any) => (
|
{/* Affichage d'un message si aucun message n'est reçu */}
|
||||||
<li key={msg.id} className="p-4 border rounded shadow">
|
{data.length === 0 ? (
|
||||||
<p><strong>👤 {msg.name}</strong> ({msg.email})</p>
|
<p className="text-center text-gray-600">Aucun message reçu.</p>
|
||||||
<p>📅 {new Date(msg.createdAt).toLocaleString("fr-FR")}</p>
|
) : (
|
||||||
<p className="mt-2">{msg.message}</p>
|
<ul className="space-y-4">
|
||||||
</li>
|
{/* Boucle sur les messages pour les afficher */}
|
||||||
))}
|
{data.map((msg: any) => (
|
||||||
</ul>
|
<li key={msg.id} className="p-4 border rounded shadow">
|
||||||
)}
|
{/* Affichage du nom et de l'email de l'expéditeur */}
|
||||||
</div>
|
<p><strong>👤 {msg.name}</strong> ({msg.email})</p>
|
||||||
);
|
{/* Affichage de la date de réception du message */}
|
||||||
}
|
<p>📅 {new Date(msg.createdAt).toLocaleString("fr-FR")}</p>
|
||||||
|
{/* Affichage du contenu du message */}
|
||||||
|
<p className="mt-2">{msg.message}</p>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,66 +1,74 @@
|
|||||||
@import url('https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap');
|
/* Importation des polices Google Fonts */
|
||||||
@import url('https://fonts.googleapis.com/css2?family=Montserrat:wght@400;700&display=swap');
|
@import url('https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap');
|
||||||
@import url('https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700&display=swap');
|
@import url('https://fonts.googleapis.com/css2?family=Montserrat:wght@400;700&display=swap');
|
||||||
@import url('https://fonts.googleapis.com/css2?family=Exo+2:wght@400;700&display=swap');
|
@import url('https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700&display=swap');
|
||||||
@import url('https://fonts.googleapis.com/css2?family=Audiowide&display=swap');
|
@import url('https://fonts.googleapis.com/css2?family=Exo+2:wght@400;700&display=swap');
|
||||||
@import url('https://fonts.googleapis.com/css2?family=Bebas+Neue&display=swap');
|
@import url('https://fonts.googleapis.com/css2?family=Audiowide&display=swap');
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=Bebas+Neue&display=swap');
|
||||||
@tailwind base;
|
|
||||||
@tailwind components;
|
/* Importation des styles de base, des composants et des utilitaires de Tailwind CSS */
|
||||||
@tailwind utilities;
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
.bg-wallpaper {
|
@tailwind utilities;
|
||||||
background-image: url('./images/wallpapersite.png');
|
|
||||||
background-size: cover;
|
/* Classe pour définir l'image de fond */
|
||||||
background-position: center;
|
.bg-wallpaper {
|
||||||
background-repeat: no-repeat;
|
background-image: url('./images/wallpapersite.png'); /* Chemin de l'image de fond */
|
||||||
}
|
background-size: cover; /* L'image couvre toute la zone */
|
||||||
|
background-position: center; /* L'image est centrée */
|
||||||
.homepage-content {
|
background-repeat: no-repeat; /* L'image ne se répète pas */
|
||||||
min-height: 50vh; /* Hauteur minimale de 50% de la hauteur de la fenêtre */
|
}
|
||||||
max-height: 80vh; /* Hauteur maximale de 80% de la hauteur de la fenêtre */
|
|
||||||
}
|
/* Classe pour définir la hauteur minimale et maximale du contenu de la page d'accueil */
|
||||||
|
.homepage-content {
|
||||||
.circle-one {
|
min-height: 50vh; /* Hauteur minimale de 50% de la hauteur de la fenêtre */
|
||||||
animation: move1 10s linear infinite;
|
max-height: 80vh; /* Hauteur maximale de 80% de la hauteur de la fenêtre */
|
||||||
}
|
}
|
||||||
|
|
||||||
.circle-two {
|
/* Classe pour animer le premier cercle */
|
||||||
animation: move2 10s linear infinite;
|
.circle-one {
|
||||||
}
|
animation: move1 10s linear infinite; /* Animation infinie avec une durée de 10s */
|
||||||
|
}
|
||||||
@keyframes move1 {
|
|
||||||
0% {
|
/* Classe pour animer le deuxième cercle */
|
||||||
transform: translate(0, 0) scale 1;
|
.circle-two {
|
||||||
}
|
animation: move2 10s linear infinite; /* Animation infinie avec une durée de 10s */
|
||||||
25% {
|
}
|
||||||
transform: translate(200px, 200px) scale(1);
|
|
||||||
}
|
/* Définition de l'animation pour le premier cercle */
|
||||||
50%{
|
@keyframes move1 {
|
||||||
transform: translate(100px, 400px) scale(1.2);
|
0% {
|
||||||
}
|
transform: translate(0, 0) scale(1); /* Position et échelle initiales */
|
||||||
75%{
|
}
|
||||||
transform: translate(-100px, -200px) scale(1.1);
|
25% {
|
||||||
}
|
transform: translate(200px, 200px) scale(1); /* Déplacement et échelle */
|
||||||
100% {
|
}
|
||||||
transform: translate(0, 0) scale(1);
|
50% {
|
||||||
}
|
transform: translate(100px, 400px) scale(1.2); /* Déplacement et échelle */
|
||||||
}
|
}
|
||||||
|
75% {
|
||||||
@keyframes move2 {
|
transform: translate(-100px, -200px) scale(1.1); /* Déplacement et échelle */
|
||||||
0% {
|
}
|
||||||
transform: translate(0, 0) scale 1;
|
100% {
|
||||||
}
|
transform: translate(0, 0) scale(1); /* Retour à la position et échelle initiales */
|
||||||
25% {
|
}
|
||||||
transform: translate(-30px, -300px) scale(1);
|
}
|
||||||
}
|
|
||||||
50%{
|
/* Définition de l'animation pour le deuxième cercle */
|
||||||
transform: translate(-200px, -100px) scale(1.2);
|
@keyframes move2 {
|
||||||
}
|
0% {
|
||||||
75%{
|
transform: translate(0, 0) scale(1); /* Position et échelle initiales */
|
||||||
transform: translate(30px, 70px) scale(1.1);
|
}
|
||||||
}
|
25% {
|
||||||
100% {
|
transform: translate(-200px, -200px) scale(1); /* Déplacement et échelle */
|
||||||
transform: translate(0, 0) scale(1);
|
}
|
||||||
}
|
50% {
|
||||||
}
|
transform: translate(-100px, -400px) scale(1.2); /* Déplacement et échelle */
|
||||||
|
}
|
||||||
|
75% {
|
||||||
|
transform: translate(100px, 200px) scale(1.1); /* Déplacement et échelle */
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translate(0, 0) scale(1); /* Retour à la position et échelle initiales */
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,74 +1,75 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { createPortal } from "react-dom"; // 🟢 Import du Portal
|
import { createPortal } from "react-dom"; // Importation de createPortal pour les modals
|
||||||
import { Swiper, SwiperSlide } from "swiper/react";
|
import { Swiper, SwiperSlide } from "swiper/react";
|
||||||
import { Navigation, Pagination, Autoplay } from "swiper/modules";
|
import { Navigation, Pagination, Autoplay } from "swiper/modules";
|
||||||
import "swiper/css";
|
import "swiper/css";
|
||||||
import "swiper/css/navigation";
|
import "swiper/css/navigation";
|
||||||
import "swiper/css/pagination";
|
import "swiper/css/pagination";
|
||||||
|
|
||||||
interface CarouselProps {
|
interface CarouselProps {
|
||||||
images: Array<{ url: string; alt: string }>;
|
images: Array<{ url: string; alt: string }>; // Propriétés des images du carrousel
|
||||||
className?: string;
|
className?: string; // Classe CSS optionnelle pour personnaliser le style
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Carousel({ images, className }: CarouselProps) {
|
export default function Carousel({ images, className }: CarouselProps) {
|
||||||
const [selectedImage, setSelectedImage] = useState<string | null>(null);
|
const [selectedImage, setSelectedImage] = useState<string | null>(null); // État pour l'image sélectionnée
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* Carrousel principal */}
|
{/* Carrousel principal */}
|
||||||
<div className={`relative w-full ${className || "h-64"} rounded-md shadow-md`}>
|
<div className={`relative w-full ${className || "h-64"} rounded-md shadow-md`}>
|
||||||
<Swiper
|
<Swiper
|
||||||
modules={[Navigation, Pagination, Autoplay]}
|
modules={[Navigation, Pagination, Autoplay]} // Modules Swiper utilisés
|
||||||
spaceBetween={10}
|
spaceBetween={10} // Espace entre les slides
|
||||||
slidesPerView={1}
|
slidesPerView={1} // Nombre de slides visibles en même temps
|
||||||
navigation
|
navigation // Activation de la navigation
|
||||||
pagination={{ clickable: true }}
|
pagination={{ clickable: true }} // Activation de la pagination cliquable
|
||||||
autoplay={{ delay: 3000 }}
|
autoplay={{ delay: 3000 }} // Activation de l'autoplay avec un délai de 3 secondes
|
||||||
className={`w-full ${className || "h-64"}`}
|
className={`w-full ${className || "h-64"}`}
|
||||||
>
|
>
|
||||||
{images.map((img, index) => (
|
{/* Boucle sur les images pour les afficher dans le carrousel */}
|
||||||
<SwiperSlide key={index} className="flex items-center justify-center h-full">
|
{images.map((img, index) => (
|
||||||
{/* Image cliquable pour affichage en plein écran */}
|
<SwiperSlide key={index} className="flex items-center justify-center h-full">
|
||||||
<img
|
{/* Image cliquable pour affichage en plein écran */}
|
||||||
src={img.url}
|
<img
|
||||||
alt={img.alt}
|
src={img.url}
|
||||||
className="w-full h-full object-cover rounded-md cursor-pointer transition-transform duration-300 hover:scale-105"
|
alt={img.alt}
|
||||||
onClick={() => setSelectedImage(img.url)} // 🟢 Ouvre l’image en plein écran
|
className="w-full h-full object-cover rounded-md cursor-pointer transition-transform duration-300 hover:scale-105"
|
||||||
/>
|
onClick={() => setSelectedImage(img.url)} // Ouvre l’image en plein écran
|
||||||
</SwiperSlide>
|
/>
|
||||||
))}
|
</SwiperSlide>
|
||||||
</Swiper>
|
))}
|
||||||
</div>
|
</Swiper>
|
||||||
|
</div>
|
||||||
{/* 🟢 Modal plein écran inséré DANS `<body>` grâce à `createPortal` */}
|
|
||||||
{selectedImage &&
|
{/* Modal plein écran inséré dans <body> grâce à createPortal */}
|
||||||
createPortal(
|
{selectedImage &&
|
||||||
<div
|
createPortal(
|
||||||
className="fixed inset-0 flex items-center justify-center w-screen h-screen bg-black bg-opacity-10 backdrop-blur-2xl transition-opacity duration-300 z-[1000]"
|
<div
|
||||||
onClick={() => setSelectedImage(null)} // 🔴 Fermer au clic
|
className="fixed inset-0 flex items-center justify-center w-screen h-screen bg-black bg-opacity-10 backdrop-blur-2xl transition-opacity duration-300 z-[1000]"
|
||||||
>
|
onClick={() => setSelectedImage(null)} // Fermer au clic
|
||||||
<div className="relative w-full max-w-6xl p-6 bg-transparent">
|
>
|
||||||
{/* Bouton de fermeture */}
|
<div className="relative w-full max-w-6xl p-6 bg-transparent">
|
||||||
<button
|
{/* Bouton de fermeture */}
|
||||||
className="absolute top-6 right-6 text-white text-3xl bg-gray-900/70 p-2 rounded-full"
|
<button
|
||||||
onClick={() => setSelectedImage(null)}
|
className="absolute top-6 right-6 text-white text-3xl bg-gray-900/70 p-2 rounded-full"
|
||||||
>
|
onClick={() => setSelectedImage(null)} // Fermer au clic
|
||||||
✖
|
>
|
||||||
</button>
|
✖
|
||||||
|
</button>
|
||||||
{/* Image affichée en grand */}
|
|
||||||
<img
|
{/* Image affichée en grand */}
|
||||||
src={selectedImage}
|
<img
|
||||||
alt="Agrandissement"
|
src={selectedImage}
|
||||||
className="w-full max-h-[90vh] object-contain rounded-lg shadow-lg"
|
alt="Agrandissement"
|
||||||
/>
|
className="w-full h-full object-cover rounded-md"
|
||||||
</div>
|
/>
|
||||||
</div>,
|
</div>
|
||||||
document.body // 🟢 Place le `modal` en dehors de `<main>` dans `<body>`
|
</div>,
|
||||||
)}
|
document.body
|
||||||
</>
|
)}
|
||||||
);
|
</>
|
||||||
}
|
);
|
||||||
|
}
|
||||||
@ -1,70 +1,75 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { createPortal } from "react-dom";
|
import { createPortal } from "react-dom"; // Importation de createPortal pour les modals
|
||||||
import { Swiper, SwiperSlide } from "swiper/react";
|
import { Swiper, SwiperSlide } from "swiper/react";
|
||||||
import { Navigation, Pagination, Autoplay } from "swiper/modules";
|
import { Navigation, Pagination, Autoplay } from "swiper/modules";
|
||||||
import "swiper/css";
|
import "swiper/css";
|
||||||
import "swiper/css/navigation";
|
import "swiper/css/navigation";
|
||||||
import "swiper/css/pagination";
|
import "swiper/css/pagination";
|
||||||
|
|
||||||
interface CarouselProps {
|
interface CarouselProps {
|
||||||
images: Array<{ url: string; alt: string }>;
|
images: Array<{ url: string; alt: string }>; // Propriétés des images du carrousel
|
||||||
className?: string;
|
className?: string; // Classe CSS optionnelle pour personnaliser le style
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function CarouselCompetences({ images, className }: CarouselProps) {
|
export default function CarouselCompetences({ images, className }: CarouselProps) {
|
||||||
const [selectedImage, setSelectedImage] = useState<string | null>(null);
|
const [selectedImage, setSelectedImage] = useState<string | null>(null); // État pour l'image sélectionnée
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* Carrousel compétences */}
|
{/* Carrousel compétences */}
|
||||||
<div className={`relative w-full ${className || "h-64"} rounded-md shadow-md`}>
|
<div className={`relative w-full ${className || "h-64"} rounded-md shadow-md`}>
|
||||||
<Swiper
|
<Swiper
|
||||||
modules={[Navigation, Pagination, Autoplay]}
|
modules={[Navigation, Pagination, Autoplay]} // Modules Swiper utilisés
|
||||||
spaceBetween={10}
|
spaceBetween={10} // Espace entre les slides
|
||||||
slidesPerView={1}
|
slidesPerView={1} // Nombre de slides visibles en même temps
|
||||||
navigation
|
navigation // Activation de la navigation
|
||||||
pagination={{ clickable: true }}
|
pagination={{ clickable: true }} // Activation de la pagination cliquable
|
||||||
autoplay={{ delay: 3000 }}
|
autoplay={{ delay: 3000 }} // Activation de l'autoplay avec un délai de 3 secondes
|
||||||
className={`w-full ${className || "h-64"}`}
|
className={`w-full ${className || "h-64"}`}
|
||||||
>
|
>
|
||||||
{images.map((img, index) => (
|
{/* Boucle sur les images pour les afficher dans le carrousel */}
|
||||||
<SwiperSlide key={index} className="flex items-center justify-center h-full">
|
{images.map((img, index) => (
|
||||||
<img
|
<SwiperSlide key={index} className="flex items-center justify-center h-full">
|
||||||
src={img.url}
|
{/* Image cliquable pour affichage en plein écran */}
|
||||||
alt={img.alt}
|
<img
|
||||||
className="w-full h-full object-cover rounded-md cursor-pointer transition-transform duration-300 hover:scale-105"
|
src={img.url}
|
||||||
onClick={() => setSelectedImage(img.url)}
|
alt={img.alt}
|
||||||
/>
|
className="w-full h-full object-cover rounded-md cursor-pointer transition-transform duration-300 hover:scale-105"
|
||||||
</SwiperSlide>
|
onClick={() => setSelectedImage(img.url)} // Ouvre l’image en plein écran
|
||||||
))}
|
/>
|
||||||
</Swiper>
|
</SwiperSlide>
|
||||||
</div>
|
))}
|
||||||
|
</Swiper>
|
||||||
{/* Modal plein écran pour agrandir les images */}
|
</div>
|
||||||
{selectedImage &&
|
|
||||||
createPortal(
|
{/* Modal plein écran pour agrandir les images */}
|
||||||
<div
|
{selectedImage &&
|
||||||
className="fixed inset-0 flex items-center justify-center w-screen h-screen bg-black bg-opacity-10 backdrop-blur-2xl transition-opacity duration-300 z-[1000]"
|
createPortal(
|
||||||
onClick={() => setSelectedImage(null)}
|
<div
|
||||||
>
|
className="fixed inset-0 flex items-center justify-center w-screen h-screen bg-black bg-opacity-10 backdrop-blur-2xl transition-opacity duration-300 z-[1000]"
|
||||||
<div className="relative w-full max-w-6xl p-6 bg-transparent">
|
onClick={() => setSelectedImage(null)} // Fermer au clic
|
||||||
<button
|
>
|
||||||
className="absolute top-6 right-6 text-white text-3xl bg-gray-900/70 p-2 rounded-full"
|
<div className="relative w-full max-w-6xl p-6 bg-transparent">
|
||||||
onClick={() => setSelectedImage(null)}
|
{/* Bouton de fermeture */}
|
||||||
>
|
<button
|
||||||
✖
|
className="absolute top-6 right-6 text-white text-3xl bg-gray-900/70 p-2 rounded-full"
|
||||||
</button>
|
onClick={() => setSelectedImage(null)} // Fermer au clic
|
||||||
<img
|
>
|
||||||
src={selectedImage}
|
✖
|
||||||
alt="Agrandissement"
|
</button>
|
||||||
className="w-full max-h-[90vh] object-contain rounded-lg shadow-lg"
|
|
||||||
/>
|
{/* Image affichée en grand */}
|
||||||
</div>
|
<img
|
||||||
</div>,
|
src={selectedImage}
|
||||||
document.body
|
alt="Agrandissement"
|
||||||
)}
|
className="w-full h-full object-cover rounded-md"
|
||||||
</>
|
/>
|
||||||
);
|
</div>
|
||||||
}
|
</div>,
|
||||||
|
document.body
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,102 +1,103 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { sendMessage } from "../utils/sendMessage";
|
import { sendMessage } from "../utils/sendMessage";
|
||||||
|
|
||||||
export default function ContactForm() {
|
export default function ContactForm() {
|
||||||
const [name, setName] = useState("");
|
// États pour gérer les valeurs des champs de formulaire
|
||||||
const [email, setEmail] = useState("");
|
const [name, setName] = useState("");
|
||||||
const [message, setMessage] = useState("");
|
const [email, setEmail] = useState("");
|
||||||
const [status, setStatus] = useState("");
|
const [message, setMessage] = useState("");
|
||||||
const [isSuccess, setIsSuccess] = useState<boolean | null>(null);
|
const [status, setStatus] = useState("");
|
||||||
const [isLoading, setIsLoading] = useState(false); // ✅ Nouvel état pour désactiver le bouton
|
const [isSuccess, setIsSuccess] = useState<boolean | null>(null);
|
||||||
|
const [isLoading, setIsLoading] = useState(false); // ✅ Nouvel état pour désactiver le bouton
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
|
||||||
e.preventDefault();
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
if (!name.trim() || !email.trim() || !message.trim()) {
|
|
||||||
setStatus("❌ Tous les champs sont obligatoires.");
|
if (!name.trim() || !email.trim() || !message.trim()) {
|
||||||
setIsSuccess(false);
|
setStatus("❌ Tous les champs sont obligatoires.");
|
||||||
return;
|
setIsSuccess(false);
|
||||||
}
|
return;
|
||||||
|
}
|
||||||
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
|
||||||
setStatus("❌ Email invalide.");
|
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
||||||
setIsSuccess(false);
|
setStatus("❌ Email invalide.");
|
||||||
return;
|
setIsSuccess(false);
|
||||||
}
|
return;
|
||||||
|
}
|
||||||
setStatus("⏳ Envoi en cours...");
|
|
||||||
setIsSuccess(null);
|
setStatus("⏳ Envoi en cours...");
|
||||||
setIsLoading(true); // ✅ Désactive le bouton pendant l'envoi
|
setIsSuccess(null);
|
||||||
|
setIsLoading(true); // ✅ Désactive le bouton pendant l'envoi
|
||||||
try {
|
|
||||||
await sendMessage(name, email, message);
|
try {
|
||||||
setStatus("✅ Message envoyé avec succès !");
|
await sendMessage(name, email, message);
|
||||||
setIsSuccess(true);
|
setStatus("✅ Message envoyé avec succès !");
|
||||||
setName("");
|
setIsSuccess(true);
|
||||||
setEmail("");
|
setName("");
|
||||||
setMessage("");
|
setEmail("");
|
||||||
} catch (error) {
|
setMessage("");
|
||||||
setStatus("❌ Erreur lors de l'envoi du message.");
|
} catch (error) {
|
||||||
setIsSuccess(false);
|
setStatus("❌ Erreur lors de l'envoi du message.");
|
||||||
} finally {
|
setIsSuccess(false);
|
||||||
setIsLoading(false); // ✅ Réactive le bouton après l'envoi
|
} finally {
|
||||||
}
|
setIsLoading(false); // ✅ Réactive le bouton après l'envoi
|
||||||
};
|
}
|
||||||
|
};
|
||||||
return (
|
|
||||||
<form
|
return (
|
||||||
onSubmit={handleSubmit}
|
<form
|
||||||
className="max-w-lg mx-auto p-6 bg-white shadow-lg rounded-lg animate-fade-in"
|
onSubmit={handleSubmit}
|
||||||
>
|
className="max-w-lg mx-auto p-6 bg-white shadow-lg rounded-lg animate-fade-in"
|
||||||
<h2 className="text-2xl font-bold mb-4 text-center">📩 Contactez-moi</h2>
|
>
|
||||||
|
<h2 className="text-2xl font-bold mb-4 text-center">📩 Contactez-moi</h2>
|
||||||
<input
|
|
||||||
type="text"
|
<input
|
||||||
placeholder="Votre nom"
|
type="text"
|
||||||
value={name}
|
placeholder="Votre nom"
|
||||||
onChange={(e) => setName(e.target.value)}
|
value={name}
|
||||||
className="w-full p-3 border border-gray-300 rounded mb-3 focus:outline-none focus:ring-2 focus:ring-blue-400"
|
onChange={(e) => setName(e.target.value)}
|
||||||
required
|
className="w-full p-3 border border-gray-300 rounded mb-3 focus:outline-none focus:ring-2 focus:ring-blue-400"
|
||||||
/>
|
required
|
||||||
|
/>
|
||||||
<input
|
|
||||||
type="email"
|
<input
|
||||||
placeholder="Votre email"
|
type="email"
|
||||||
value={email}
|
placeholder="Votre email"
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
value={email}
|
||||||
className="w-full p-3 border border-gray-300 rounded mb-3 focus:outline-none focus:ring-2 focus:ring-blue-400"
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
required
|
className="w-full p-3 border border-gray-300 rounded mb-3 focus:outline-none focus:ring-2 focus:ring-blue-400"
|
||||||
/>
|
required
|
||||||
|
/>
|
||||||
<textarea
|
|
||||||
placeholder="Votre message"
|
<textarea
|
||||||
value={message}
|
placeholder="Votre message"
|
||||||
onChange={(e) => setMessage(e.target.value)}
|
value={message}
|
||||||
className="w-full p-3 border border-gray-300 rounded mb-3 focus:outline-none focus:ring-2 focus:ring-blue-400"
|
onChange={(e) => setMessage(e.target.value)}
|
||||||
required
|
className="w-full p-3 border border-gray-300 rounded mb-3 focus:outline-none focus:ring-2 focus:ring-blue-400"
|
||||||
/>
|
required
|
||||||
|
/>
|
||||||
<button
|
|
||||||
type="submit"
|
<button
|
||||||
disabled={isLoading} // ✅ Désactive le bouton pendant l'envoi
|
type="submit"
|
||||||
className={`w-full py-3 rounded transition ${
|
disabled={isLoading} // ✅ Désactive le bouton pendant l'envoi
|
||||||
isLoading ? "bg-gray-400 cursor-not-allowed" : "bg-blue-500 hover:bg-blue-600 text-white"
|
className={`w-full py-3 rounded transition ${
|
||||||
}`}
|
isLoading ? "bg-gray-400 cursor-not-allowed" : "bg-blue-500 hover:bg-blue-600 text-white"
|
||||||
>
|
}`}
|
||||||
{isLoading ? "⏳ Envoi..." : "Envoyer"}
|
>
|
||||||
</button>
|
{isLoading ? "⏳ Envoi..." : "Envoyer"}
|
||||||
|
</button>
|
||||||
{/* ✅ Affichage du message de confirmation */}
|
|
||||||
{status && (
|
{/* ✅ Affichage du message de confirmation */}
|
||||||
<p
|
{status && (
|
||||||
className={`mt-4 text-center ${isSuccess ? "text-green-600" : "text-red-600"}`}
|
<p
|
||||||
aria-live="polite" // ✅ Accessibilité pour les lecteurs d’écran
|
className={`mt-4 text-center ${isSuccess ? "text-green-600" : "text-red-600"}`}
|
||||||
>
|
aria-live="polite" // ✅ Accessibilité pour les lecteurs d’écran
|
||||||
{status}
|
>
|
||||||
</p>
|
{status}
|
||||||
)}
|
</p>
|
||||||
</form>
|
)}
|
||||||
);
|
</form>
|
||||||
}
|
);
|
||||||
|
}
|
||||||
|
|||||||
@ -1,54 +1,60 @@
|
|||||||
import { fetchData } from "../utils/fetchData";
|
import { fetchData } from "../utils/fetchData"; // Importation de la fonction fetchData pour récupérer les données depuis l'API
|
||||||
import Carousel from "./Carousel";
|
import Carousel from "./Carousel"; // Importation du composant Carousel pour afficher les images
|
||||||
import ReactMarkdown from "react-markdown";
|
import ReactMarkdown from "react-markdown"; // Importation de ReactMarkdown pour rendre le texte riche en Markdown
|
||||||
|
|
||||||
interface ContentSectionProps {
|
// Définition des propriétés du composant ContentSection
|
||||||
collection: string; // Nom de la collection (projects, events, blog, etc.)
|
interface ContentSectionProps {
|
||||||
slug: string;
|
collection: string; // Nom de la collection (projects, events, blog, etc.)
|
||||||
titleClass?: string; // Permet de modifier le style du titre
|
slug: string; // Identifiant unique pour récupérer les données spécifiques
|
||||||
contentClass?: string; // Permet de modifier le style du contenu
|
titleClass?: string; // Permet de modifier le style du titre
|
||||||
}
|
contentClass?: string; // Permet de modifier le style du contenu
|
||||||
|
}
|
||||||
export default async function ContentSection({ collection, slug, titleClass, contentClass }: ContentSectionProps) {
|
|
||||||
const data = await fetchData(collection, slug);
|
// Composant principal ContentSection
|
||||||
|
export default async function ContentSection({ collection, slug, titleClass, contentClass }: ContentSectionProps) {
|
||||||
if (!data) {
|
// Récupération des données depuis l'API en utilisant la fonction fetchData
|
||||||
return <div>Contenu introuvable.</div>;
|
const data = await fetchData(collection, slug);
|
||||||
}
|
|
||||||
|
// Affichage d'un message si les données ne sont pas disponibles
|
||||||
const { name, Resum: richText, picture, link, linkText } = data;
|
if (!data) {
|
||||||
|
return <div>Contenu introuvable.</div>;
|
||||||
// Transformer les images de Strapi en format attendu par le carrousel
|
}
|
||||||
const images = picture?.map((img: any) => ({
|
|
||||||
url: `http://localhost:1337${img?.formats?.large?.url || img?.url}`,
|
// Déstructuration des données récupérées
|
||||||
alt: img.name || "Image",
|
const { name, Resum: richText, picture, link, linkText } = data;
|
||||||
})) || [];
|
|
||||||
|
// Transformation des images de Strapi en format attendu par le carrousel
|
||||||
return (
|
const images = picture?.map((img: any) => ({
|
||||||
<div className="max-w-3xl mx-auto p-6">
|
url: `http://localhost:1337${img?.formats?.large?.url || img?.url}`, // Utilisation de l'URL de l'image en format large ou originale
|
||||||
<h1 className={titleClass || "text-3xl mb-6 font-bold text-gray-700"}>{name}</h1>
|
alt: img.name || "Image", // Texte alternatif pour l'image
|
||||||
|
})) || [];
|
||||||
{/* Carrousel réutilisable */}
|
|
||||||
<Carousel images={images} className="w-full h-64" />
|
return (
|
||||||
|
<div className="max-w-3xl mx-auto p-6">
|
||||||
{/* Contenu en Markdown */}
|
{/* Titre de la section */}
|
||||||
<div className={contentClass || "bg-gray-100 rounded-md p-4 shadow-md mt-6"}>
|
<h1 className={titleClass || "text-3xl mb-6 font-bold text-gray-700"}>{name}</h1>
|
||||||
<ReactMarkdown>{richText}</ReactMarkdown>
|
|
||||||
</div>
|
{/* Carrousel réutilisable pour afficher les images */}
|
||||||
|
<Carousel images={images} className="w-full h-64" />
|
||||||
{/* Lien externe */}
|
|
||||||
{link && (
|
{/* Contenu en Markdown */}
|
||||||
<div className="mt-6">
|
<div className={contentClass || "bg-gray-100 rounded-md p-4 shadow-md mt-6"}>
|
||||||
<a
|
<ReactMarkdown>{richText}</ReactMarkdown>
|
||||||
href={link}
|
</div>
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
{/* Lien externe */}
|
||||||
className="text-blue-500 hover:underline transition duration-300 ease-in-out transform hover:scale-105 hover:text-blue-700"
|
{link && (
|
||||||
>
|
<div className="mt-6">
|
||||||
{linkText || "Voir plus/lien externe"}
|
<a
|
||||||
</a>
|
href={link}
|
||||||
</div>
|
target="_blank"
|
||||||
)}
|
rel="noopener noreferrer"
|
||||||
</div>
|
className="text-blue-500 hover:underline transition duration-300 ease-in-out transform hover:scale-105 hover:text-blue-700"
|
||||||
);
|
>
|
||||||
|
{linkText || "Voir plus/lien externe"}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
@ -1,91 +1,98 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import CarouselCompetences from "./CarouselCompetences";
|
import CarouselCompetences from "./CarouselCompetences";
|
||||||
import ReactMarkdown from "react-markdown";
|
import ReactMarkdown from "react-markdown";
|
||||||
import rehypeRaw from "rehype-raw"; // ✅ Permet d'interpréter le HTML dans ReactMarkdown
|
import rehypeRaw from "rehype-raw"; // ✅ Permet d'interpréter le HTML dans ReactMarkdown
|
||||||
import ModalGlossaire from "./ModalGlossaire";
|
import ModalGlossaire from "./ModalGlossaire";
|
||||||
|
|
||||||
interface ContentSectionProps {
|
// Définition des propriétés du composant ContentSectionCompetences
|
||||||
competenceData: any;
|
interface ContentSectionProps {
|
||||||
glossaireData: any[];
|
competenceData: any;
|
||||||
titleClass?: string;
|
glossaireData: any[];
|
||||||
contentClass?: string;
|
titleClass?: string;
|
||||||
}
|
contentClass?: string;
|
||||||
|
}
|
||||||
// ✅ Définition du type Glossaire
|
|
||||||
interface GlossaireItem {
|
// ✅ Définition du type Glossaire
|
||||||
mot_clef: string;
|
interface GlossaireItem {
|
||||||
slug: string;
|
mot_clef: string;
|
||||||
variantes: string[];
|
slug: string;
|
||||||
description: string;
|
variantes: string[];
|
||||||
images?: any[];
|
description: string;
|
||||||
}
|
images?: any[];
|
||||||
|
}
|
||||||
export default function ContentSectionCompetences({ competenceData, glossaireData, titleClass, contentClass }: ContentSectionProps) {
|
|
||||||
const [selectedMot, setSelectedMot] = useState<GlossaireItem | null>(null);
|
// Composant principal ContentSectionCompetences
|
||||||
|
export default function ContentSectionCompetences({ competenceData, glossaireData, titleClass, contentClass }: ContentSectionProps) {
|
||||||
if (!competenceData) {
|
const [selectedMot, setSelectedMot] = useState<GlossaireItem | null>(null);
|
||||||
return <div className="text-red-500 text-center">❌ Compétence introuvable.</div>;
|
|
||||||
}
|
if (!competenceData) {
|
||||||
|
return <div className="text-red-500 text-center">❌ Compétence introuvable.</div>;
|
||||||
const { name, content, picture } = competenceData;
|
}
|
||||||
|
|
||||||
const images = picture?.map((img: any) => ({
|
// Déstructuration des données de la compétence
|
||||||
url: `http://localhost:1337${img?.formats?.large?.url || img?.url}`,
|
const { name, content, picture } = competenceData;
|
||||||
alt: img.name || "Image de compétence",
|
|
||||||
})) || [];
|
// Transformation des images de Strapi en format attendu par le carrousel
|
||||||
|
const images = picture?.map((img: any) => ({
|
||||||
// 🔥 Transformation du texte riche avec des <span> cliquables
|
url: `http://localhost:1337${img?.formats?.large?.url || img?.url}`,
|
||||||
function transformMarkdownWithKeywords(text: string) {
|
alt: img.name || "Image de compétence",
|
||||||
if (!glossaireData || glossaireData.length === 0) return text;
|
})) || [];
|
||||||
|
|
||||||
let modifiedText = text;
|
// 🔥 Transformation du texte riche avec des <span> cliquables
|
||||||
|
function transformMarkdownWithKeywords(text: string) {
|
||||||
glossaireData.forEach(({ mot_clef, variantes }) => {
|
if (!glossaireData || glossaireData.length === 0) return text;
|
||||||
const regexVariants = (variantes || []).map((v: string) => v.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|");
|
|
||||||
const regex = new RegExp(`\\b(${mot_clef}|${regexVariants})\\b`, "gi");
|
let modifiedText = text;
|
||||||
|
|
||||||
modifiedText = modifiedText.replace(regex, (match) => {
|
glossaireData.forEach(({ mot_clef, variantes }) => {
|
||||||
return `<span class="keyword" data-mot="${mot_clef}" style="color: blue; cursor: pointer;">${match}</span>`; // ✅ Span cliquable
|
const regexVariants = (variantes || []).map((v: string) => v.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|");
|
||||||
});
|
const regex = new RegExp(`\\b(${mot_clef}|${regexVariants})\\b`, "gi");
|
||||||
});
|
|
||||||
|
modifiedText = modifiedText.replace(regex, (match) => {
|
||||||
return modifiedText;
|
return `<span class="keyword" data-mot="${mot_clef}" style="color: blue; cursor: pointer;">${match}</span>`; // ✅ Span cliquable
|
||||||
}
|
});
|
||||||
|
});
|
||||||
const contentWithLinks = transformMarkdownWithKeywords(content);
|
|
||||||
|
return modifiedText;
|
||||||
// ✅ Gestion des clics sur les mots-clés
|
}
|
||||||
useEffect(() => {
|
|
||||||
function handleKeywordClick(event: any) {
|
const contentWithLinks = transformMarkdownWithKeywords(content);
|
||||||
const target = event.target as HTMLElement;
|
|
||||||
if (target.classList.contains("keyword")) {
|
// ✅ Gestion des clics sur les mots-clés
|
||||||
const mot = target.getAttribute("data-mot");
|
useEffect(() => {
|
||||||
if (mot) {
|
function handleKeywordClick(event: any) {
|
||||||
const glossaireMot = glossaireData.find((g) => g.mot_clef === mot);
|
const target = event.target as HTMLElement;
|
||||||
setSelectedMot(glossaireMot || null);
|
if (target.classList.contains("keyword")) {
|
||||||
}
|
const mot = target.getAttribute("data-mot");
|
||||||
}
|
if (mot) {
|
||||||
}
|
const glossaireMot = glossaireData.find((g) => g.mot_clef === mot);
|
||||||
|
setSelectedMot(glossaireMot || null);
|
||||||
document.addEventListener("click", handleKeywordClick);
|
}
|
||||||
return () => document.removeEventListener("click", handleKeywordClick);
|
}
|
||||||
}, [glossaireData]);
|
}
|
||||||
|
|
||||||
return (
|
document.addEventListener("click", handleKeywordClick);
|
||||||
<div className="max-w-3xl mx-auto p-6">
|
return () => document.removeEventListener("click", handleKeywordClick);
|
||||||
<h1 className={titleClass || "text-3xl mb-6 font-bold text-gray-700"}>{name}</h1>
|
}, [glossaireData]);
|
||||||
|
|
||||||
<CarouselCompetences images={images} className="w-full h-64" />
|
return (
|
||||||
|
// ✅ Affichage de la compétence
|
||||||
{/* 🔥 Affichage du texte riche avec mots-clés cliquables */}
|
<div className="max-w-3xl mx-auto p-6">
|
||||||
<div className={contentClass || "mt-6 text-lg text-black-700"}>
|
{/* Titre de la section */}
|
||||||
<ReactMarkdown rehypePlugins={[rehypeRaw]}>{contentWithLinks}</ReactMarkdown> {/* ✅ Permet d'interpréter le HTML */}
|
<h1 className={titleClass || "text-3xl mb-6 font-bold text-gray-700"}>{name}</h1>
|
||||||
</div>
|
|
||||||
|
{/* Carrousel pour afficher les images */}
|
||||||
{/* 🚀 Modale pour afficher les infos des mots-clés */}
|
<CarouselCompetences images={images} className="w-full h-64" />
|
||||||
{selectedMot && <ModalGlossaire mot={selectedMot} onClose={() => setSelectedMot(null)} />}
|
|
||||||
</div>
|
{/* 🔥 Affichage du texte riche avec mots-clés cliquables */}
|
||||||
);
|
<div className={contentClass || "mt-6 text-lg text-black-700"}>
|
||||||
}
|
<ReactMarkdown rehypePlugins={[rehypeRaw]}>{contentWithLinks}</ReactMarkdown> {/* ✅ Permet d'interpréter le HTML */}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 🚀 Modale pour afficher les infos des mots-clés */}
|
||||||
|
{selectedMot && <ModalGlossaire mot={selectedMot} onClose={() => setSelectedMot(null)} />}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@ -1,23 +1,25 @@
|
|||||||
import { fetchDataCompetences, fetchDataGlossaire } from "../utils/fetchDataCompetences";
|
import { fetchDataCompetences, fetchDataGlossaire } from "../utils/fetchDataCompetences";
|
||||||
import ContentSectionCompetences from "./ContentSectionCompetences";
|
import ContentSectionCompetences from "./ContentSectionCompetences";
|
||||||
|
|
||||||
interface ContentSectionProps {
|
// Définition des propriétés du composant ContentSection
|
||||||
collection: string;
|
interface ContentSectionProps {
|
||||||
slug: string;
|
collection: string;
|
||||||
titleClass?: string;
|
slug: string;
|
||||||
contentClass?: string;
|
titleClass?: string;
|
||||||
}
|
contentClass?: string;
|
||||||
|
}
|
||||||
export default async function ContentSectionCompetencesContainer({ collection, slug, titleClass, contentClass }: ContentSectionProps) {
|
|
||||||
const competenceData = await fetchDataCompetences(collection, slug);
|
// Composant principal ContentSection
|
||||||
const glossaireData = await fetchDataGlossaire();
|
export default async function ContentSectionCompetencesContainer({ collection, slug, titleClass, contentClass }: ContentSectionProps) {
|
||||||
|
const competenceData = await fetchDataCompetences(collection, slug);
|
||||||
return (
|
const glossaireData = await fetchDataGlossaire();
|
||||||
<ContentSectionCompetences
|
|
||||||
competenceData={competenceData}
|
return (
|
||||||
glossaireData={glossaireData}
|
<ContentSectionCompetences
|
||||||
titleClass={titleClass}
|
competenceData={competenceData}
|
||||||
contentClass={contentClass}
|
glossaireData={glossaireData}
|
||||||
/>
|
titleClass={titleClass}
|
||||||
);
|
contentClass={contentClass}
|
||||||
}
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@ -1,18 +1,26 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import {useState} from "react"
|
import { useState } from "react"; // Importation du hook useState pour gérer l'état
|
||||||
|
|
||||||
export default function Footer() {
|
export default function Footer() {
|
||||||
const [count, setCount] = useState(0)
|
const [count, setCount] = useState(0); // État pour suivre le nombre de clics
|
||||||
function handleClick() {
|
|
||||||
setCount(count + 1)}
|
// Fonction pour gérer les clics sur le bouton
|
||||||
|
function handleClick() {
|
||||||
return (
|
setCount(count + 1); // Incrémente le compteur de clics
|
||||||
<footer className="bg-white/50 backdrop-blur rounded-lg">
|
}
|
||||||
<div className="max-w-4xl mx-auto flex flex-col items-center py-6 text-sm text-gray-400">
|
|
||||||
<p>© {new Date().getFullYear()} Our Company.</p>
|
return (
|
||||||
<p>Vous avez cliqué {count} fois sur le boutton.<button onClick={handleClick}>Click Me</button></p>
|
<footer className="bg-white/50 backdrop-blur rounded-lg">
|
||||||
</div>
|
<div className="max-w-4xl mx-auto flex flex-col items-center py-6 text-sm text-gray-400">
|
||||||
</footer>
|
{/* Affichage de l'année actuelle */}
|
||||||
)
|
<p>© {new Date().getFullYear()} Our Company.</p>
|
||||||
}
|
{/* Affichage du compteur de clics et du bouton */}
|
||||||
|
<p>
|
||||||
|
Vous avez cliqué {count} fois sur le bouton.
|
||||||
|
<button onClick={handleClick}>Click Me</button>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,58 +1,60 @@
|
|||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { createPortal } from "react-dom"; // ✅ Insère la modale dans <body>
|
import { createPortal } from "react-dom"; // Insère la modale dans <body>
|
||||||
import CarouselCompetences from "./CarouselCompetences";
|
import CarouselCompetences from "./CarouselCompetences"; // Importation du composant CarouselCompetences pour afficher les images
|
||||||
|
|
||||||
interface ModalGlossaireProps {
|
// Définition des propriétés du composant ModalGlossaire
|
||||||
mot: {
|
interface ModalGlossaireProps {
|
||||||
mot_clef: string;
|
mot: {
|
||||||
description: string;
|
mot_clef: string; // Mot-clé du glossaire
|
||||||
images?: any[];
|
description: string; // Description du mot-clé
|
||||||
};
|
images?: any[]; // Images associées au mot-clé
|
||||||
onClose: () => void;
|
};
|
||||||
}
|
onClose: () => void; // Fonction pour fermer la modale
|
||||||
|
}
|
||||||
export default function ModalGlossaire({ mot, onClose }: ModalGlossaireProps) {
|
|
||||||
// 🔥 Désactiver le scroll du `body` quand la modale est ouverte
|
// Composant principal ModalGlossaire
|
||||||
useEffect(() => {
|
export default function ModalGlossaire({ mot, onClose }: ModalGlossaireProps) {
|
||||||
document.body.classList.add("overflow-hidden");
|
// Désactiver le scroll du `body` quand la modale est ouverte
|
||||||
return () => {
|
useEffect(() => {
|
||||||
document.body.classList.remove("overflow-hidden");
|
document.body.classList.add("overflow-hidden");
|
||||||
};
|
return () => {
|
||||||
}, []);
|
document.body.classList.remove("overflow-hidden");
|
||||||
|
};
|
||||||
// ✅ Debug : Vérifier les images reçues
|
}, []);
|
||||||
console.log("🖼️ Images reçues dans la modale :", mot.images);
|
|
||||||
|
// Debug : Vérifier les images reçues
|
||||||
// ✅ Vérifier si `mot.images` est bien un tableau et contient des images
|
console.log("🖼️ Images reçues dans la modale :", mot.images);
|
||||||
const images = mot.images?.map((img: any) => {
|
|
||||||
return {
|
// Vérifier si `mot.images` est bien un tableau et contient des images
|
||||||
url: `http://localhost:1337${img.formats?.large?.url || img.url}`,
|
const images = mot.images?.map((img: any) => {
|
||||||
alt: img.name || "Illustration",
|
return {
|
||||||
};
|
url: `http://localhost:1337${img.formats?.large?.url || img.url}`,
|
||||||
}) || [];
|
alt: img.name || "Illustration",
|
||||||
|
};
|
||||||
return createPortal(
|
}) || [];
|
||||||
<div className="fixed inset-0 w-screen h-screen bg-black bg-opacity-75 flex items-center justify-center z-[1000]">
|
|
||||||
<div className="bg-white p-6 rounded-lg shadow-lg w-[90vw] max-w-4xl relative">
|
return createPortal(
|
||||||
{/* Bouton de fermeture */}
|
<div className="fixed inset-0 w-screen h-screen bg-black bg-opacity-75 flex items-center justify-center z-[1000]">
|
||||||
<button className="absolute top-3 right-3 text-gray-700 text-2xl" onClick={onClose}>
|
<div className="bg-white p-6 rounded-lg shadow-lg w-[90vw] max-w-4xl relative">
|
||||||
✖
|
{/* Bouton de fermeture */}
|
||||||
</button>
|
<button className="absolute top-3 right-3 text-gray-700 text-2xl" onClick={onClose}>
|
||||||
|
✖
|
||||||
{/* Titre */}
|
</button>
|
||||||
<h2 className="text-3xl font-bold mb-4">{mot.mot_clef}</h2>
|
|
||||||
|
{/* Titre */}
|
||||||
{/* Description */}
|
<h2 className="text-3xl font-bold mb-4">{mot.mot_clef}</h2>
|
||||||
<p className="text-gray-700 mb-6">{mot.description}</p>
|
|
||||||
|
{/* Description */}
|
||||||
{/* 🚀 Carrousel d'images si disponible */}
|
<p className="text-gray-700 mb-6">{mot.description}</p>
|
||||||
{images.length > 0 ? (
|
|
||||||
<CarouselCompetences images={images} className="w-full h-80" />
|
{/* Carrousel d'images si disponible */}
|
||||||
) : (
|
{images.length > 0 ? (
|
||||||
<p className="text-gray-500 text-center">Aucune image disponible</p>
|
<CarouselCompetences images={images} className="w-full h-80" />
|
||||||
)}
|
) : (
|
||||||
</div>
|
<p className="text-gray-500">Aucune image disponible.</p>
|
||||||
</div>,
|
)}
|
||||||
document.body // ✅ Fixe la modale au `body` pour qu'elle couvre toute la page
|
</div>
|
||||||
);
|
</div>,
|
||||||
}
|
document.body
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,14 +1,18 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import Link from "next/link"
|
import Link from "next/link"; // Importation du composant Link de Next.js pour la navigation
|
||||||
import {usePathname} from "next/navigation"
|
import { usePathname } from "next/navigation"; // Importation du hook usePathname pour obtenir le chemin actuel
|
||||||
|
|
||||||
export default function NavLink(props){
|
export default function NavLink(props) {
|
||||||
const pathname = usePathname()
|
const pathname = usePathname(); // Obtention du chemin actuel
|
||||||
const active = pathname === props.path
|
const active = pathname === props.path; // Vérification si le lien est actif
|
||||||
return(
|
|
||||||
<Link className={active ? "opacity-100" : "opacity-50 hover:opacity-65"} href={props.path}>
|
return (
|
||||||
{props.text}
|
<Link
|
||||||
</Link>
|
className={active ? "opacity-100" : "opacity-50 hover:opacity-65"} // Classes CSS pour le style du lien
|
||||||
)
|
href={props.path} // Chemin de navigation
|
||||||
|
>
|
||||||
|
{props.text} {/* Texte du lien */}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
@ -1,22 +1,25 @@
|
|||||||
import ContactForm from "../components/ContactForm";
|
import ContactForm from "../components/ContactForm"; // Importation du composant ContactForm
|
||||||
|
|
||||||
export default function ContactPage() {
|
export default function ContactPage() {
|
||||||
return (
|
return (
|
||||||
<div className="max-w-3xl mx-auto p-6">
|
<div className="max-w-3xl mx-auto p-6">
|
||||||
<h1 className="text-3xl font-bold text-center mb-6">Contactez-moi</h1>
|
{/* Titre de la page */}
|
||||||
<p className="text-lg text-center mb-4">
|
<h1 className="text-3xl font-bold text-center mb-6">Contactez-moi</h1>
|
||||||
Vous pouvez me contacter via ce formulaire ou sur mes réseaux sociaux.
|
|
||||||
</p>
|
{/* Texte d'introduction */}
|
||||||
|
<p className="text-lg text-center mb-4">
|
||||||
{/* Liens vers les réseaux sociaux */}
|
Vous pouvez me contacter via ce formulaire ou sur mes réseaux sociaux.
|
||||||
<div className="flex justify-center space-x-4 mb-6">
|
</p>
|
||||||
<a href="https://linkedin.com/in/votreprofil" className="text-blue-500">LinkedIn</a>
|
|
||||||
<a href="https://twitter.com/votreprofil" className="text-blue-500">Twitter</a>
|
{/* Liens vers les réseaux sociaux */}
|
||||||
<a href="mailto:votre@email.com" className="text-blue-500">Email</a>
|
<div className="flex justify-center space-x-4 mb-6">
|
||||||
</div>
|
<a href="https://linkedin.com/in/votreprofil" className="text-blue-500">LinkedIn</a>
|
||||||
|
<a href="https://twitter.com/votreprofil" className="text-blue-500">Twitter</a>
|
||||||
{/* Formulaire de contact */}
|
<a href="mailto:votre@email.com" className="text-blue-500">Email</a>
|
||||||
<ContactForm />
|
</div>
|
||||||
</div>
|
|
||||||
);
|
{/* Formulaire de contact */}
|
||||||
}
|
<ContactForm />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,37 +1,42 @@
|
|||||||
|
/* Importation des styles de base, des composants et des utilitaires de Tailwind CSS */
|
||||||
@tailwind base;
|
@tailwind base;
|
||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
|
/* Définition des variables CSS pour les couleurs de fond et de premier plan */
|
||||||
:root {
|
:root {
|
||||||
--background: #ffffff;
|
--background: #ffffff; /* Couleur de fond par défaut (clair) */
|
||||||
--foreground: #171717;
|
--foreground: #171717; /* Couleur de premier plan par défaut (foncé) */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Définition des variables CSS pour le mode sombre */
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
:root {
|
:root {
|
||||||
--background: #0a0a0a;
|
--background: #0a0a0a; /* Couleur de fond pour le mode sombre */
|
||||||
--foreground: #ededed;
|
--foreground: #ededed; /* Couleur de premier plan pour le mode sombre */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Styles globaux pour le corps de la page */
|
||||||
body {
|
body {
|
||||||
color: var(--foreground);
|
color: var(--foreground); /* Utilisation de la couleur de premier plan définie */
|
||||||
background: var(--background);
|
background: var(--background); /* Utilisation de la couleur de fond définie */
|
||||||
font-family: Arial, Helvetica, sans-serif;
|
font-family: Arial, Helvetica, sans-serif; /* Police de caractères par défaut */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Définition d'une animation de fondu en entrée */
|
||||||
@keyframes fade-in {
|
@keyframes fade-in {
|
||||||
from {
|
from {
|
||||||
opacity: 0;
|
opacity: 0; /* Opacité initiale à 0 (invisible) */
|
||||||
transform: translateY(-10px);
|
transform: translateY(-10px); /* Déplacement initial vers le haut */
|
||||||
}
|
}
|
||||||
to {
|
to {
|
||||||
opacity: 1;
|
opacity: 1; /* Opacité finale à 1 (visible) */
|
||||||
transform: translateY(0);
|
transform: translateY(0); /* Position finale */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Classe utilitaire pour appliquer l'animation de fondu en entrée */
|
||||||
.animate-fade-in {
|
.animate-fade-in {
|
||||||
animation: fade-in 0.5s ease-out;
|
animation: fade-in 0.5s ease-out; /* Animation de 0.5s avec une courbe de transition */
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -6,16 +6,17 @@ import "./assets/main.css";
|
|||||||
import NavLink from "./components/NavLink";
|
import NavLink from "./components/NavLink";
|
||||||
|
|
||||||
export default function RootLayout({ children }) {
|
export default function RootLayout({ children }) {
|
||||||
|
// États pour gérer la largeur et la hauteur du conteneur principal
|
||||||
const [numElements, setNumElements] = useState(0);
|
const [numElements, setNumElements] = useState(0);
|
||||||
const [containerWidth, setContainerWidth] = useState("max-w-4xl");
|
const [containerWidth, setContainerWidth] = useState("max-w-4xl");
|
||||||
const [containerHeight, setContainerHeight] = useState("min-h-[50vh]");
|
const [containerHeight, setContainerHeight] = useState("min-h-[50vh]");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Supposons que children soit un tableau d'éléments
|
// Compter le nombre d'éléments enfants
|
||||||
const elementsCount = React.Children.count(children);
|
const elementsCount = React.Children.count(children);
|
||||||
setNumElements(elementsCount);
|
setNumElements(elementsCount);
|
||||||
|
|
||||||
// Ajustez la largeur en fonction du nombre d'éléments
|
// Ajuster la largeur et la hauteur en fonction du nombre d'éléments
|
||||||
if (elementsCount > 5) {
|
if (elementsCount > 5) {
|
||||||
setContainerWidth("max-w-6xl");
|
setContainerWidth("max-w-6xl");
|
||||||
setContainerHeight("min-h-[80vh]");
|
setContainerHeight("min-h-[80vh]");
|
||||||
@ -31,11 +32,14 @@ export default function RootLayout({ children }) {
|
|||||||
return (
|
return (
|
||||||
<html lang="fr">
|
<html lang="fr">
|
||||||
<body>
|
<body>
|
||||||
|
{/* Conteneur principal avec image de fond */}
|
||||||
<div className="bg-wallpaper min-h-[100dvh] grid grid-rows-[auto_1fr_auto]">
|
<div className="bg-wallpaper min-h-[100dvh] grid grid-rows-[auto_1fr_auto]">
|
||||||
|
{/* Cercles de fond pour l'effet visuel */}
|
||||||
<div className="absolute z-0 inset-0 overflow-hidden">
|
<div className="absolute z-0 inset-0 overflow-hidden">
|
||||||
<div className="circle-one blur-3xl w-64 h-64 rounded-full bg-rose-400/60 top-0 right-28 absolute"></div>
|
<div className="circle-one blur-3xl w-64 h-64 rounded-full bg-rose-400/60 top-0 right-28 absolute"></div>
|
||||||
<div className="circle-two blur-3xl w-64 h-64 rounded-full bg-indigo-400/60 bottom-0 left-28 absolute"></div>
|
<div className="circle-two blur-3xl w-64 h-64 rounded-full bg-indigo-400/60 bottom-0 left-28 absolute"></div>
|
||||||
</div>
|
</div>
|
||||||
|
{/* En-tête avec navigation */}
|
||||||
<header className="z-10 bg-white/50 backdrop-blur rounded-lg border-2 border-gray-500">
|
<header className="z-10 bg-white/50 backdrop-blur rounded-lg border-2 border-gray-500">
|
||||||
<div className="max-w-4xl mx-auto flex items-center justify-between p-4">
|
<div className="max-w-4xl mx-auto flex items-center justify-between p-4">
|
||||||
<h2 className="text-2xl font-bold">Portofolio Gras-Calvet Fernand</h2>
|
<h2 className="text-2xl font-bold">Portofolio Gras-Calvet Fernand</h2>
|
||||||
@ -57,9 +61,11 @@ export default function RootLayout({ children }) {
|
|||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
{/* Conteneur principal pour le contenu */}
|
||||||
<main className={`backdrop-blur z-10 ${containerWidth} ${containerHeight} mx-auto bg-white/20 rounded-xl py-7 px-8 m-6 overflow-hidden`}>
|
<main className={`backdrop-blur z-10 ${containerWidth} ${containerHeight} mx-auto bg-white/20 rounded-xl py-7 px-8 m-6 overflow-hidden`}>
|
||||||
{children}
|
{children}
|
||||||
</main>
|
</main>
|
||||||
|
{/* Pied de page */}
|
||||||
<Footer />
|
<Footer />
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
import ContentSection from "../../components/ContentSection";
|
import ContentSection from "../../components/ContentSection"; // Importation du composant ContentSection
|
||||||
|
|
||||||
export default function Page({ params }: { params: { slug: string } }) {
|
// Composant principal de la page de détail du projet
|
||||||
return <ContentSection collection="projects" slug={params.slug} />;
|
export default function Page({ params }: { params: { slug: string } }) {
|
||||||
}
|
// Rendu du composant ContentSection avec les paramètres de la collection et du slug
|
||||||
|
return <ContentSection collection="projects" slug={params.slug} />;
|
||||||
|
}
|
||||||
@ -1,59 +1,69 @@
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
||||||
async function getAllprojects() {
|
// Fonction pour récupérer tous les projets depuis l'API Strapi
|
||||||
try {
|
async function getAllprojects() {
|
||||||
const response = await fetch("http://localhost:1337/api/projects?populate=*");
|
try {
|
||||||
if (!response.ok) {
|
const response = await fetch("http://localhost:1337/api/projects?populate=*");
|
||||||
throw new Error("Failed to fetch projects");
|
if (!response.ok) {
|
||||||
}
|
throw new Error("Failed to fetch projects");
|
||||||
const projects = await response.json();
|
}
|
||||||
return projects.data;
|
const projects = await response.json();
|
||||||
} catch (error) {
|
return projects.data;
|
||||||
console.error("Error fetching projects:", error);
|
} catch (error) {
|
||||||
return [];
|
console.error("Error fetching projects:", error);
|
||||||
}
|
return [];
|
||||||
}
|
}
|
||||||
|
}
|
||||||
export default async function Page() {
|
|
||||||
const projects = await getAllprojects();
|
// Composant principal de la page des projets
|
||||||
|
export default async function Page() {
|
||||||
return (
|
const projects = await getAllprojects();
|
||||||
<div>
|
|
||||||
<h1 className="text-3xl mb-6 font-bold text-grey-700">Portfolio formation 42</h1>
|
return (
|
||||||
<div className="grid grid-cols-2 gap-6">
|
<div>
|
||||||
{projects.map((project) => {
|
{/* Titre de la page */}
|
||||||
const picture = project.picture?.[0]; // Récupère la première image si elle existe
|
<h1 className="text-3xl mb-6 font-bold text-grey-700">Portfolio formation 42</h1>
|
||||||
const largeImageUrl = picture?.formats?.large?.url; // Vérifie que le format "large" existe
|
|
||||||
const originalImageUrl = picture?.url; // URL de l'image originale
|
{/* Grille pour afficher les projets */}
|
||||||
|
<div className="grid grid-cols-2 gap-6">
|
||||||
// Utilisez l'URL de l'image originale si disponible, sinon l'URL de l'image large
|
{/* Boucle sur les projets pour les afficher */}
|
||||||
const imageUrl = originalImageUrl ? `http://localhost:1337${originalImageUrl}` : `http://localhost:1337${largeImageUrl}`;
|
{projects.map((project) => {
|
||||||
|
const picture = project.picture?.[0]; // Récupère la première image si elle existe
|
||||||
return (
|
const largeImageUrl = picture?.formats?.large?.url; // Vérifie que le format "large" existe
|
||||||
<div key={project.id} className="bg-white rounded-lg shadow-md overflow-hidden transform transition-transform duration-300 hover:scale-105 hover:shadow-lg hover:bg-blue-100">
|
const originalImageUrl = picture?.url; // URL de l'image originale
|
||||||
<Link href={`/portfolio/${project.slug}`}>
|
|
||||||
<div className="overflow-hidden">
|
// Utilisez l'URL de l'image originale si disponible, sinon l'URL de l'image large
|
||||||
{imageUrl ? (
|
const imageUrl = originalImageUrl ? `http://localhost:1337${originalImageUrl}` : `http://localhost:1337${largeImageUrl}`;
|
||||||
<img
|
|
||||||
src={imageUrl}
|
return (
|
||||||
alt={picture?.name || "Project image"}
|
<div key={project.id} className="bg-white rounded-lg shadow-md overflow-hidden transform transition-transform duration-300 hover:scale-105 hover:shadow-lg hover:bg-blue-100">
|
||||||
className="w-full h-48 object-cover transform transition-transform duration-300 hover:scale-125 hover:rotate-12"
|
{/* Lien vers la page de détail du projet */}
|
||||||
/>
|
<Link href={`/portfolio/${project.slug}`}>
|
||||||
) : (
|
<div className="overflow-hidden">
|
||||||
<div className="bg-gray-200 text-gray-500 text-center rounded-md shadow-md p-4">
|
{/* Affichage de l'image du projet */}
|
||||||
Image indisponible
|
{imageUrl ? (
|
||||||
</div>
|
<img
|
||||||
)}
|
src={imageUrl}
|
||||||
</div>
|
alt={picture?.name || "Project image"}
|
||||||
<div className="p-4">
|
className="w-full h-48 object-cover transform transition-transform duration-300 hover:scale-125 hover:rotate-12"
|
||||||
<p className="font-bold text-xl mb-2">{project.name}</p>
|
/>
|
||||||
<p className="text-gray-700">{project.description}</p>
|
) : (
|
||||||
</div>
|
<div className="bg-gray-200 text-gray-500 text-center rounded-md shadow-md p-4">
|
||||||
</Link>
|
Image indisponible
|
||||||
</div>
|
</div>
|
||||||
);
|
)}
|
||||||
})}
|
</div>
|
||||||
</div>
|
<div className="p-4">
|
||||||
</div>
|
{/* Affichage du nom du projet */}
|
||||||
);
|
<p className="font-bold text-xl mb-2">{project.name}</p>
|
||||||
|
{/* Affichage de la description du projet */}
|
||||||
|
<p className="text-gray-700">{project.description}</p>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
@ -1,24 +1,30 @@
|
|||||||
import qs from "qs";
|
import qs from "qs"; // Importation de qs pour construire des requêtes de chaîne de requête
|
||||||
|
|
||||||
export async function fetchData(collection: string, slug: string) {
|
// Fonction pour récupérer des données spécifiques depuis l'API Strapi
|
||||||
const query = qs.stringify({
|
export async function fetchData(collection: string, slug: string) {
|
||||||
filters: { slug },
|
// Construction de la requête avec des filtres et des relations à peupler
|
||||||
populate: "picture",
|
const query = qs.stringify({
|
||||||
});
|
filters: { slug }, // Filtre basé sur le slug
|
||||||
|
populate: "picture", // On garde les images associées
|
||||||
try {
|
});
|
||||||
const response = await fetch(`http://localhost:1337/api/${collection}?${query}`, {
|
|
||||||
cache: "no-store",
|
try {
|
||||||
});
|
// Envoi de la requête à l'API Strapi
|
||||||
|
const response = await fetch(`http://localhost:1337/api/${collection}?${query}`, {
|
||||||
if (!response.ok) {
|
cache: "no-store", // Désactivation du cache pour obtenir les données les plus récentes
|
||||||
throw new Error("Failed to fetch data");
|
});
|
||||||
}
|
|
||||||
|
// Vérification de la réponse de l'API
|
||||||
const data = await response.json();
|
if (!response.ok) {
|
||||||
return data.data[0] || null;
|
throw new Error("Failed to fetch data");
|
||||||
} catch (error) {
|
}
|
||||||
console.error(`Error fetching ${collection} data:`, error);
|
|
||||||
return null;
|
// Récupération des données de la réponse
|
||||||
}
|
const data = await response.json();
|
||||||
}
|
return data.data[0] || null; // Retourne la première entrée ou null si aucune donnée n'est trouvée
|
||||||
|
} catch (error) {
|
||||||
|
// Gestion des erreurs et log des erreurs
|
||||||
|
console.error(`Error fetching ${collection} data:`, error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,54 +1,67 @@
|
|||||||
import qs from "qs";
|
import qs from "qs"; // Importation de qs pour construire des requêtes de chaîne de requête
|
||||||
|
|
||||||
// ✅ Fonction pour récupérer une compétence spécifique
|
// Fonction pour récupérer une compétence spécifique
|
||||||
export async function fetchDataCompetences(collection: string, slug: string) {
|
export async function fetchDataCompetences(collection: string, slug: string) {
|
||||||
const query = qs.stringify({
|
// Construction de la requête avec des filtres et des relations à peupler
|
||||||
filters: {
|
const query = qs.stringify({
|
||||||
slug: { $eq: slug },
|
filters: {
|
||||||
},
|
slug: { $eq: slug },
|
||||||
populate: "picture", // On garde les images des compétences
|
},
|
||||||
});
|
populate: "picture", // On garde les images des compétences
|
||||||
|
});
|
||||||
console.log(`🛠️ Requête API Compétence : http://localhost:1337/api/${collection}?${query}`);
|
|
||||||
|
// Log de la requête API pour le débogage
|
||||||
try {
|
console.log(`🛠️ Requête API Compétence : http://localhost:1337/api/${collection}?${query}`);
|
||||||
const response = await fetch(`http://localhost:1337/api/${collection}?${query}`, {
|
|
||||||
cache: "no-store",
|
try {
|
||||||
});
|
// Envoi de la requête à l'API Strapi
|
||||||
|
const response = await fetch(`http://localhost:1337/api/${collection}?${query}`, {
|
||||||
if (!response.ok) {
|
cache: "no-store",
|
||||||
throw new Error(`Failed to fetch competences data: ${response.status}`);
|
});
|
||||||
}
|
|
||||||
|
// Vérification de la réponse de l'API
|
||||||
const data = await response.json();
|
if (!response.ok) {
|
||||||
console.log("✅ Données reçues (Compétence) :", data);
|
throw new Error(`Failed to fetch competences data: ${response.status}`);
|
||||||
|
}
|
||||||
return data.data[0] || null;
|
|
||||||
} catch (error) {
|
// Récupération des données de la réponse
|
||||||
console.error("❌ Erreur lors de la récupération des compétences :", error);
|
const data = await response.json();
|
||||||
return null;
|
console.log("✅ Données reçues (Compétence) :", data);
|
||||||
}
|
|
||||||
}
|
// Retourne la première compétence ou null si aucune donnée n'est trouvée
|
||||||
|
return data.data[0] || null;
|
||||||
export async function fetchDataGlossaire() {
|
} catch (error) {
|
||||||
try {
|
// Gestion des erreurs et log des erreurs
|
||||||
console.log("🛠️ Requête API Glossaire : http://localhost:1337/api/glossaires?populate=images");
|
console.error("❌ Erreur lors de la récupération des compétences :", error);
|
||||||
|
return null;
|
||||||
const response = await fetch("http://localhost:1337/api/glossaires?populate=images", {
|
}
|
||||||
cache: "no-store",
|
}
|
||||||
});
|
|
||||||
|
// Fonction pour récupérer les données du glossaire
|
||||||
if (!response.ok) {
|
export async function fetchDataGlossaire() {
|
||||||
throw new Error(`Failed to fetch glossaire data: ${response.status}`);
|
try {
|
||||||
}
|
// Log de la requête API pour le débogage
|
||||||
|
console.log("🛠️ Requête API Glossaire : http://localhost:1337/api/glossaires?populate=images");
|
||||||
const data = await response.json();
|
|
||||||
console.log("✅ Données reçues (Glossaire) :", data);
|
// Envoi de la requête à l'API Strapi
|
||||||
|
const response = await fetch("http://localhost:1337/api/glossaires?populate=images", {
|
||||||
return data.data || [];
|
cache: "no-store",
|
||||||
} catch (error) {
|
});
|
||||||
console.error("❌ Erreur lors de la récupération du glossaire :", error);
|
|
||||||
return [];
|
// Vérification de la réponse de l'API
|
||||||
}
|
if (!response.ok) {
|
||||||
}
|
throw new Error(`Failed to fetch glossaire data: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Récupération des données de la réponse
|
||||||
|
const data = await response.json();
|
||||||
|
console.log("✅ Données reçues (Glossaire) :", data);
|
||||||
|
|
||||||
|
// Retourne les données du glossaire ou un tableau vide si aucune donnée n'est trouvée
|
||||||
|
return data.data || [];
|
||||||
|
} catch (error) {
|
||||||
|
// Gestion des erreurs et log des erreurs
|
||||||
|
console.error("❌ Erreur lors de la récupération du glossaire :", error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,32 +1,39 @@
|
|||||||
export async function sendMessage(name: string, email: string, message: string) {
|
// Fonction pour envoyer un message à l'API Strapi
|
||||||
const dateTime = new Date().toLocaleString("fr-FR", { timeZone: "Europe/Paris" }); // ✅ Date formatée en français
|
export async function sendMessage(name: string, email: string, message: string) {
|
||||||
|
// Formatage de la date et de l'heure en français
|
||||||
const messageWithDate = `${message}\n\n📅 Envoyé le : ${dateTime}`; // ✅ Ajout de la date à la fin du message
|
const dateTime = new Date().toLocaleString("fr-FR", { timeZone: "Europe/Paris" }); // ✅ Date formatée en français
|
||||||
|
|
||||||
console.log("📨 Envoi du message...", { name, email, messageWithDate });
|
// Ajout de la date à la fin du message
|
||||||
|
const messageWithDate = `${message}\n\n📅 Envoyé le : ${dateTime}`; // ✅ Ajout de la date à la fin du message
|
||||||
const res = await fetch("http://localhost:1337/api/messages", {
|
|
||||||
method: "POST",
|
// Log des informations du message avant l'envoi
|
||||||
headers: {
|
console.log("📨 Envoi du message...", { name, email, messageWithDate });
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
// Envoi du message à l'API Strapi
|
||||||
body: JSON.stringify({
|
const res = await fetch("http://localhost:1337/api/messages", {
|
||||||
data: {
|
method: "POST",
|
||||||
name: name,
|
headers: {
|
||||||
email: email,
|
"Content-Type": "application/json",
|
||||||
message: messageWithDate, // ✅ Message modifié avec la date
|
},
|
||||||
},
|
body: JSON.stringify({
|
||||||
}),
|
data: {
|
||||||
});
|
name: name,
|
||||||
|
email: email,
|
||||||
const responseData = await res.json();
|
message: messageWithDate, // ✅ Message modifié avec la date
|
||||||
|
},
|
||||||
if (!res.ok) {
|
}),
|
||||||
console.error("❌ Erreur API Strapi :", responseData);
|
});
|
||||||
throw new Error(`Échec de l'envoi du message: ${responseData.error.message}`);
|
|
||||||
}
|
// Récupération de la réponse de l'API
|
||||||
|
const responseData = await res.json();
|
||||||
console.log("✅ Message envoyé avec succès !", responseData);
|
|
||||||
return responseData;
|
// Gestion des erreurs de l'API
|
||||||
}
|
if (!res.ok) {
|
||||||
|
console.error("❌ Erreur API Strapi :", responseData);
|
||||||
|
throw new Error(`Échec de l'envoi du message: ${responseData.error.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log de la réussite de l'envoi du message
|
||||||
|
console.log("✅ Message envoyé avec succès !", responseData);
|
||||||
|
return responseData;
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user