diff --git a/app/Competences/page.jsx b/app/Competences/page.jsx index c94e744..f610107 100644 --- a/app/Competences/page.jsx +++ b/app/Competences/page.jsx @@ -1,63 +1,71 @@ -import Link from "next/link"; - -async function getAllCompetences() { - try { - const response = await fetch("http://localhost:1337/api/competences?populate=*"); - if (!response.ok) { - throw new Error("Failed to fetch competences"); - } - const competences = await response.json(); - return competences.data; - } catch (error) { - console.error("Error fetching competences:", error); - return []; - } -} - -export default async function Page() { - const competences = await getAllCompetences(); - - return ( -
-

Mes Compétences

-
- {competences.map((competence) => { - const picture = competence.picture?.[0]; // Récupère la première image si elle existe - const largeImageUrl = picture?.formats?.large?.url; // Vérifie que le format "large" existe - const originalImageUrl = picture?.url; // URL de l'image originale - - // Utilisez l'URL de l'image originale si disponible, sinon l'URL de l'image large - const imageUrl = originalImageUrl - ? `http://localhost:1337${originalImageUrl}` - : `http://localhost:1337${largeImageUrl}`; - - return ( -
- -
- {imageUrl ? ( - {picture?.name - ) : ( -
- Image indisponible -
- )} -
-
-

{competence.name}

-

{competence.description}

-
- -
- ); - })} -
-
- ); -} - - +import Link from "next/link"; + +// Fonction pour récupérer toutes les compétences depuis l'API Strapi +async function getAllCompetences() { + try { + const response = await fetch("http://localhost:1337/api/competences?populate=*"); + if (!response.ok) { + throw new Error("Failed to fetch competences"); + } + const competences = await response.json(); + return competences.data; + } catch (error) { + console.error("Error fetching competences:", error); + return []; + } +} + +// Composant principal de la page des compétences +export default async function Page() { + const competences = await getAllCompetences(); + + return ( +
+ {/* Titre de la page */} +

Mes Compétences

+ {/* Grille pour afficher les compétences */} +
+ {competences.map((competence) => { + const picture = competence.picture?.[0]; // Récupère la première image si elle existe + const largeImageUrl = picture?.formats?.large?.url; // Vérifie que le format "large" existe + const originalImageUrl = picture?.url; // URL de l'image originale + + // Utilisez l'URL de l'image originale si disponible, sinon l'URL de l'image large + const imageUrl = originalImageUrl + ? `http://localhost:1337${originalImageUrl}` + : `http://localhost:1337${largeImageUrl}`; + + return ( +
+ {/* Lien vers la page de détail de la compétence */} + +
+ {/* Affichage de l'image de la compétence */} + {imageUrl ? ( + {picture?.name + ) : ( +
+ Image indisponible +
+ )} +
+
+ {/* Affichage du nom de la compétence */} +

{competence.name}

+ {/* Affichage de la description de la compétence */} +

{competence.description}

+
+ +
+ ); + })} +
+
+ ); +} + + diff --git a/app/admin/messages/page.tsx b/app/admin/messages/page.tsx index 012c1a6..8a28499 100644 --- a/app/admin/messages/page.tsx +++ b/app/admin/messages/page.tsx @@ -1,24 +1,32 @@ -export default async function MessagesPage() { - const res = await fetch("http://localhost:1337/api/messages"); - const { data } = await res.json(); - - return ( -
-

📬 Messages reçus

- {data.length === 0 ? ( -

Aucun message reçu.

- ) : ( - - )} -
- ); - } - \ No newline at end of file +// Composant principal de la page des messages +export default async function MessagesPage() { + // Récupération des messages depuis l'API Strapi + const res = await fetch("http://localhost:1337/api/messages"); + const { data } = await res.json(); + + return ( +
+ {/* Titre de la page */} +

📬 Messages reçus

+ + {/* Affichage d'un message si aucun message n'est reçu */} + {data.length === 0 ? ( +

Aucun message reçu.

+ ) : ( + + )} +
+ ); + } \ No newline at end of file diff --git a/app/assets/main.css b/app/assets/main.css index 4e4ddb8..720d178 100644 --- a/app/assets/main.css +++ b/app/assets/main.css @@ -1,66 +1,74 @@ -@import url('https://fonts.googleapis.com/css2?family=Roboto: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=Orbitron: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=Audiowide&display=swap'); -@import url('https://fonts.googleapis.com/css2?family=Bebas+Neue&display=swap'); - -@tailwind base; -@tailwind components; -@tailwind utilities; - -.bg-wallpaper { - background-image: url('./images/wallpapersite.png'); - background-size: cover; - background-position: center; - background-repeat: no-repeat; -} - -.homepage-content { - 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 */ -} - -.circle-one { - animation: move1 10s linear infinite; -} - -.circle-two { - animation: move2 10s linear infinite; - } - -@keyframes move1 { - 0% { - transform: translate(0, 0) scale 1; - } - 25% { - transform: translate(200px, 200px) scale(1); - } - 50%{ - transform: translate(100px, 400px) scale(1.2); - } - 75%{ - transform: translate(-100px, -200px) scale(1.1); - } - 100% { - transform: translate(0, 0) scale(1); - } -} - -@keyframes move2 { - 0% { - transform: translate(0, 0) scale 1; - } - 25% { - transform: translate(-30px, -300px) scale(1); - } - 50%{ - transform: translate(-200px, -100px) scale(1.2); - } - 75%{ - transform: translate(30px, 70px) scale(1.1); - } - 100% { - transform: translate(0, 0) scale(1); - } - } \ No newline at end of file +/* Importation des polices Google Fonts */ +@import url('https://fonts.googleapis.com/css2?family=Roboto: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=Orbitron: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=Audiowide&display=swap'); +@import url('https://fonts.googleapis.com/css2?family=Bebas+Neue&display=swap'); + +/* Importation des styles de base, des composants et des utilitaires de Tailwind CSS */ +@tailwind base; +@tailwind components; +@tailwind utilities; + +/* Classe pour définir l'image de fond */ +.bg-wallpaper { + 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 */ + background-repeat: no-repeat; /* L'image ne se répète pas */ +} + +/* Classe pour définir la hauteur minimale et maximale du contenu de la page d'accueil */ +.homepage-content { + 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 animer le premier cercle */ +.circle-one { + animation: move1 10s linear infinite; /* Animation infinie avec une durée de 10s */ +} + +/* Classe pour animer le deuxième cercle */ +.circle-two { + animation: move2 10s linear infinite; /* Animation infinie avec une durée de 10s */ +} + +/* Définition de l'animation pour le premier cercle */ +@keyframes move1 { + 0% { + transform: translate(0, 0) scale(1); /* Position et échelle initiales */ + } + 25% { + transform: translate(200px, 200px) scale(1); /* Déplacement et échelle */ + } + 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 */ + } +} + +/* Définition de l'animation pour le deuxième cercle */ +@keyframes move2 { + 0% { + transform: translate(0, 0) scale(1); /* Position et échelle initiales */ + } + 25% { + transform: translate(-200px, -200px) scale(1); /* Déplacement et échelle */ + } + 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 */ + } +} \ No newline at end of file diff --git a/app/components/Carousel.tsx b/app/components/Carousel.tsx index 3a7c9d3..dd9dfcc 100644 --- a/app/components/Carousel.tsx +++ b/app/components/Carousel.tsx @@ -1,74 +1,75 @@ -"use client"; - -import { useState } from "react"; -import { createPortal } from "react-dom"; // 🟢 Import du Portal -import { Swiper, SwiperSlide } from "swiper/react"; -import { Navigation, Pagination, Autoplay } from "swiper/modules"; -import "swiper/css"; -import "swiper/css/navigation"; -import "swiper/css/pagination"; - -interface CarouselProps { - images: Array<{ url: string; alt: string }>; - className?: string; -} - -export default function Carousel({ images, className }: CarouselProps) { - const [selectedImage, setSelectedImage] = useState(null); - - return ( - <> - {/* Carrousel principal */} -
- - {images.map((img, index) => ( - - {/* Image cliquable pour affichage en plein écran */} - {img.alt} setSelectedImage(img.url)} // 🟢 Ouvre l’image en plein écran - /> - - ))} - -
- - {/* 🟢 Modal plein écran inséré DANS `` grâce à `createPortal` */} - {selectedImage && - createPortal( -
setSelectedImage(null)} // 🔴 Fermer au clic - > -
- {/* Bouton de fermeture */} - - - {/* Image affichée en grand */} - Agrandissement -
-
, - document.body // 🟢 Place le `modal` en dehors de `
` dans `` - )} - - ); -} +"use client"; + +import { useState } from "react"; +import { createPortal } from "react-dom"; // Importation de createPortal pour les modals +import { Swiper, SwiperSlide } from "swiper/react"; +import { Navigation, Pagination, Autoplay } from "swiper/modules"; +import "swiper/css"; +import "swiper/css/navigation"; +import "swiper/css/pagination"; + +interface CarouselProps { + images: Array<{ url: string; alt: string }>; // Propriétés des images du carrousel + className?: string; // Classe CSS optionnelle pour personnaliser le style +} + +export default function Carousel({ images, className }: CarouselProps) { + const [selectedImage, setSelectedImage] = useState(null); // État pour l'image sélectionnée + + return ( + <> + {/* Carrousel principal */} +
+ + {/* Boucle sur les images pour les afficher dans le carrousel */} + {images.map((img, index) => ( + + {/* Image cliquable pour affichage en plein écran */} + {img.alt} setSelectedImage(img.url)} // Ouvre l’image en plein écran + /> + + ))} + +
+ + {/* Modal plein écran inséré dans grâce à createPortal */} + {selectedImage && + createPortal( +
setSelectedImage(null)} // Fermer au clic + > +
+ {/* Bouton de fermeture */} + + + {/* Image affichée en grand */} + Agrandissement +
+
, + document.body + )} + + ); +} \ No newline at end of file diff --git a/app/components/CarouselCompetences.tsx b/app/components/CarouselCompetences.tsx index 913fcfc..ff77adf 100644 --- a/app/components/CarouselCompetences.tsx +++ b/app/components/CarouselCompetences.tsx @@ -1,70 +1,75 @@ -"use client"; - -import { useState } from "react"; -import { createPortal } from "react-dom"; -import { Swiper, SwiperSlide } from "swiper/react"; -import { Navigation, Pagination, Autoplay } from "swiper/modules"; -import "swiper/css"; -import "swiper/css/navigation"; -import "swiper/css/pagination"; - -interface CarouselProps { - images: Array<{ url: string; alt: string }>; - className?: string; -} - -export default function CarouselCompetences({ images, className }: CarouselProps) { - const [selectedImage, setSelectedImage] = useState(null); - - return ( - <> - {/* Carrousel compétences */} -
- - {images.map((img, index) => ( - - {img.alt} setSelectedImage(img.url)} - /> - - ))} - -
- - {/* Modal plein écran pour agrandir les images */} - {selectedImage && - createPortal( -
setSelectedImage(null)} - > -
- - Agrandissement -
-
, - document.body - )} - - ); -} +"use client"; + +import { useState } from "react"; +import { createPortal } from "react-dom"; // Importation de createPortal pour les modals +import { Swiper, SwiperSlide } from "swiper/react"; +import { Navigation, Pagination, Autoplay } from "swiper/modules"; +import "swiper/css"; +import "swiper/css/navigation"; +import "swiper/css/pagination"; + +interface CarouselProps { + images: Array<{ url: string; alt: string }>; // Propriétés des images du carrousel + className?: string; // Classe CSS optionnelle pour personnaliser le style +} + +export default function CarouselCompetences({ images, className }: CarouselProps) { + const [selectedImage, setSelectedImage] = useState(null); // État pour l'image sélectionnée + + return ( + <> + {/* Carrousel compétences */} +
+ + {/* Boucle sur les images pour les afficher dans le carrousel */} + {images.map((img, index) => ( + + {/* Image cliquable pour affichage en plein écran */} + {img.alt} setSelectedImage(img.url)} // Ouvre l’image en plein écran + /> + + ))} + +
+ + {/* Modal plein écran pour agrandir les images */} + {selectedImage && + createPortal( +
setSelectedImage(null)} // Fermer au clic + > +
+ {/* Bouton de fermeture */} + + + {/* Image affichée en grand */} + Agrandissement +
+
, + document.body + )} + + ); +} \ No newline at end of file diff --git a/app/components/ContactForm.tsx b/app/components/ContactForm.tsx index a768349..18d4aed 100644 --- a/app/components/ContactForm.tsx +++ b/app/components/ContactForm.tsx @@ -1,102 +1,103 @@ -"use client"; - -import { useState } from "react"; -import { sendMessage } from "../utils/sendMessage"; - -export default function ContactForm() { - const [name, setName] = useState(""); - const [email, setEmail] = useState(""); - const [message, setMessage] = useState(""); - const [status, setStatus] = useState(""); - const [isSuccess, setIsSuccess] = useState(null); - const [isLoading, setIsLoading] = useState(false); // ✅ Nouvel état pour désactiver le bouton - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - - if (!name.trim() || !email.trim() || !message.trim()) { - setStatus("❌ Tous les champs sont obligatoires."); - setIsSuccess(false); - return; - } - - if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) { - setStatus("❌ Email invalide."); - setIsSuccess(false); - return; - } - - setStatus("⏳ Envoi en cours..."); - setIsSuccess(null); - setIsLoading(true); // ✅ Désactive le bouton pendant l'envoi - - try { - await sendMessage(name, email, message); - setStatus("✅ Message envoyé avec succès !"); - setIsSuccess(true); - setName(""); - setEmail(""); - setMessage(""); - } catch (error) { - setStatus("❌ Erreur lors de l'envoi du message."); - setIsSuccess(false); - } finally { - setIsLoading(false); // ✅ Réactive le bouton après l'envoi - } - }; - - return ( -
-

📩 Contactez-moi

- - setName(e.target.value)} - className="w-full p-3 border border-gray-300 rounded mb-3 focus:outline-none focus:ring-2 focus:ring-blue-400" - required - /> - - setEmail(e.target.value)} - className="w-full p-3 border border-gray-300 rounded mb-3 focus:outline-none focus:ring-2 focus:ring-blue-400" - required - /> - -