This commit is contained in:
Ladebeze66 2025-02-09 15:43:50 +01:00
parent 8bf66f695d
commit d45d36465c
18 changed files with 349 additions and 245 deletions

View File

@ -1,58 +1,71 @@
"use client";
import { useEffect, useState } from "react";
import Link from "next/link"; import Link from "next/link";
import { getApiUrl } from "../utils/getApiUrl"; // 🔥 Import de l'URL dynamique
// Fonction pour récupérer toutes les compétences depuis l'API Strapi export default function Page() {
async function getAllCompetences() { const [competences, setCompetences] = useState([]); // 🔥 Stocker les compétences une seule fois
try { const apiUrl = getApiUrl(); // 🔥 Définition de l'URL API
const response = await fetch("http://localhost:1337/api/competences?populate=*");
if (!response.ok) { useEffect(() => {
throw new Error("Failed to fetch competences"); async function fetchCompetences() {
console.log("🔍 API utilisée pour les compétences :", apiUrl);
try {
const response = await fetch(`${apiUrl}/api/competences?populate=*`);
if (!response.ok) {
throw new Error(`Erreur de récupération des compétences : ${response.statusText}`);
}
const data = await response.json();
setCompetences(data.data ?? []);
} catch (error) {
console.error("❌ Erreur lors de la récupération des compétences :", error);
}
} }
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 fetchCompetences(); // 🔥 Exécuter une seule fois au montage du composant
export default async function Page() { }, [apiUrl]); // Exécuter `useEffect()` uniquement si `apiUrl` change
const competences = await getAllCompetences();
return ( return (
<main className="w-full p-6"> <main className="w-full p-3 mt-5 mb-5">
<h1 className="text-3xl mb-6 font-bold text-gray-700 text-center">Mes Compétences</h1> {/* Titre de la page */}
<h1 className="text-3xl mb-3 font-bold text-gray-700 text-center">Mes Compétences</h1>
{/* Grille améliorée avec une meilleure gestion de l'espace */} {/* Affichage d'un message si aucune compétence n'est trouvée */}
<div className="grid gap-4 grid-cols-[repeat(auto-fit,minmax(300px,1fr))] max-w-7xl mx-auto"> {competences.length === 0 ? (
{competences.map((competence) => { <p className="text-center text-gray-500">Aucune compétence disponible.</p>
const picture = competence.picture?.[0]; ) : (
const imageUrl = picture?.url ? `http://localhost:1337${picture.url}` : "/placeholder.jpg"; <div className="grid gap-7 grid-cols-[repeat(auto-fit,minmax(300px,1fr))] max-w-7xl mx-auto">
{competences.map((competence) => {
const picture = competence.picture?.[0];
const imageUrl = picture?.url ? `${apiUrl}${picture.url}` : "/placeholder.jpg";
return ( return (
<div <div
key={competence.id} key={competence.id}
className="bg-white rounded-lg shadow-md overflow-hidden w-80 h-96 flex flex-col transform transition-all duration-300 hover:scale-105 hover:shadow-xl" className="bg-white rounded-lg shadow-md overflow-hidden w-80 h-96 flex flex-col transform transition-all duration-300 hover:scale-105 hover:shadow-xl p-4"
> >
<Link href={`/competences/${competence.slug}`}> {/* Lien vers la page de détail de la compétence */}
<div className="overflow-hidden w-full h-48"> <Link href={`/competences/${competence.slug}`}>
<img <div className="overflow-hidden w-full h-48 mb-4">
src={imageUrl} <img
alt={picture?.name || "Competence image"} src={imageUrl}
className="w-full h-full object-cover" alt={picture?.name || "Competence image"}
/> className="w-full h-full object-cover"
</div> />
<div className="p-4 flex-grow"> </div>
<p className="font-bold text-xl mb-2">{competence.name}</p> <div className="flex-grow overflow-y-auto max-h-32 hide-scrollbar show-scrollbar">
<p className="text-gray-700 text-sm hover:text-base transition-all duration-200 ease-in-out"> <p className="font-bold text-xl mb-2">{competence.name}</p>
{competence.description} <p className="text-gray-700 text-sm hover:text-base transition-all duration-200 ease-in-out">
</p> {competence.description}
</div> </p>
</Link> </div>
</div> </Link>
); </div>
})} );
</div> })}
</div>
)}
</main> </main>
); );
} }

View File

@ -1,6 +1,30 @@
import { fetchData } from "../utils/fetchData"; // Importation de la fonction fetchData pour récupérer les données depuis l'API "use client";
import Carousel from "./Carousel"; // Importation du composant Carousel pour afficher les images
import ReactMarkdown from "react-markdown"; // Importation de ReactMarkdown pour rendre le texte riche en Markdown import { useEffect, useState } from "react";
import { fetchData } from "../utils/fetchData"; // Importation de la fonction fetchData
import { getApiUrl } from "../utils/getApiUrl"; // Importation de l'URL dynamique
import Carousel from "./Carousel"; // Importation du composant Carrousel
import ReactMarkdown from "react-markdown"; // Importation pour gérer le Markdown
// Définition du type pour une image
interface ImageData {
url: string;
formats?: {
large?: {
url: string;
};
};
name?: string;
}
// Définition du type pour les données récupérées
interface ContentData {
name: string;
Resum: string; // Texte en Markdown
picture?: ImageData[];
link?: string;
linkText?: string;
}
// Définition des propriétés du composant ContentSection // Définition des propriétés du composant ContentSection
interface ContentSectionProps { interface ContentSectionProps {
@ -11,21 +35,31 @@ interface ContentSectionProps {
} }
// Composant principal ContentSection // Composant principal ContentSection
export default async function ContentSection({ collection, slug, titleClass, contentClass }: ContentSectionProps) { export default function ContentSection({ collection, slug, titleClass, contentClass }: ContentSectionProps) {
// Récupération des données depuis l'API en utilisant la fonction fetchData const [data, setData] = useState<ContentData | null>(null);
const data = await fetchData(collection, slug); const apiUrl = getApiUrl(); // Détection automatique de l'URL de l'API
useEffect(() => {
async function fetchContent() {
console.log("🔍 API utilisée pour ContentSection :", apiUrl);
const result = await fetchData(collection, slug);
setData(result);
}
fetchContent();
}, [collection, slug, apiUrl]);
// Affichage d'un message si les données ne sont pas disponibles // Affichage d'un message si les données ne sont pas disponibles
if (!data) { if (!data) {
return <div>Contenu introuvable.</div>; return <div className="text-center text-gray-500">Contenu introuvable.</div>;
} }
// Déstructuration des données récupérées // Déstructuration des données récupérées
const { name, Resum: richText, picture, link, linkText } = data; const { name, Resum: richText, picture, link, linkText } = data;
// Transformation des images de Strapi en format attendu par le carrousel // Transformation des images de Strapi en format attendu par le carrousel
const images = picture?.map((img: any) => ({ const images = picture?.map((img: ImageData) => ({
url: `http://localhost:1337${img?.formats?.large?.url || img?.url}`, // Utilisation de l'URL de l'image en format large ou originale url: `${apiUrl}${img.formats?.large?.url || img.url}`, // 🔥 URL dynamique
alt: img.name || "Image", // Texte alternatif pour l'image alt: img.name || "Image", // Texte alternatif pour l'image
})) || []; })) || [];
@ -57,4 +91,4 @@ export default async function ContentSection({ collection, slug, titleClass, con
)} )}
</div> </div>
); );
} }

View File

@ -1,60 +1,82 @@
"use client"; "use client";
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { getApiUrl } from "../utils/getApiUrl"; // ✅ Importation de l'URL dynamique
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";
import ModalGlossaire from "./ModalGlossaire"; import ModalGlossaire from "./ModalGlossaire";
// Définition des propriétés du composant ContentSectionCompetences // ✅ Définition des types pour TypeScript
interface ContentSectionProps { interface ImageData {
competenceData: any; url: string;
glossaireData: any[]; formats?: {
titleClass?: string; large?: { url: string };
contentClass?: string; };
name?: string;
}
interface CompetenceData {
name: string;
content: string;
picture?: ImageData[];
} }
// ✅ Définition du type Glossaire
interface GlossaireItem { interface GlossaireItem {
mot_clef: string; mot_clef: string;
slug: string; slug: string;
variantes: string[]; variantes: string[];
description: string; description: string;
images?: any[]; images?: ImageData[];
} }
// Composant principal ContentSectionCompetences interface ContentSectionProps {
competenceData: CompetenceData | null;
glossaireData: GlossaireItem[];
titleClass?: string;
contentClass?: string;
}
// ✅ Composant principal
export default function ContentSectionCompetences({ competenceData, glossaireData, titleClass, contentClass }: ContentSectionProps) { export default function ContentSectionCompetences({ competenceData, glossaireData, titleClass, contentClass }: ContentSectionProps) {
console.log("🔍 [ContentSectionCompetences] Chargement du composant...");
console.log("📌 [ContentSectionCompetences] Données reçues - competenceData :", competenceData);
console.log("📌 [ContentSectionCompetences] Données reçues - glossaireData :", glossaireData);
const [selectedMot, setSelectedMot] = useState<GlossaireItem | null>(null); const [selectedMot, setSelectedMot] = useState<GlossaireItem | null>(null);
const apiUrl = getApiUrl(); // ✅ Détection automatique de l'URL API
if (!competenceData) { if (!competenceData) {
console.error("❌ [ContentSectionCompetences] Compétence introuvable !");
return <div className="text-red-500 text-center"> Compétence introuvable.</div>; return <div className="text-red-500 text-center"> Compétence introuvable.</div>;
} }
// Déstructuration des données de la compétence // Déstructuration des données de la compétence
const { name, content, picture } = competenceData; const { name, content, picture } = competenceData;
// Transformation des images de Strapi en format attendu par le carrousel // Transformation des images de Strapi en format attendu par le carrousel
const images = picture?.map((img: any) => ({ const images = picture?.map((img) => ({
url: `http://localhost:1337${img?.formats?.large?.url || img?.url}`, url: `${apiUrl}${img.formats?.large?.url || img.url}`, // ✅ Correction ici
alt: img.name || "Image de compétence", alt: img.name || "Image de compétence",
})) || []; })) || [];
// 🔥 Transformation du texte riche avec des <span> cliquables console.log("✅ [ContentSectionCompetences] Images préparées :", images);
// ✅ Transformation des mots-clés du glossaire
function transformMarkdownWithKeywords(text: string) { function transformMarkdownWithKeywords(text: string) {
if (!glossaireData || glossaireData.length === 0) return text; if (!glossaireData.length) return text;
let modifiedText = text; let modifiedText = text;
glossaireData.forEach(({ mot_clef, variantes }) => { glossaireData.forEach(({ mot_clef, variantes }) => {
const regexVariants = (variantes || []).map((v: string) => v.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|"); const regexVariants = variantes.map((v) => v.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|");
const regex = new RegExp(`\\b(${mot_clef}|${regexVariants})\\b`, "gi"); const regex = new RegExp(`\\b(${mot_clef}|${regexVariants})\\b`, "gi");
modifiedText = modifiedText.replace(regex, (match) => { modifiedText = modifiedText.replace(regex, (match) => {
return `<span class="keyword" data-mot="${mot_clef}" style="color: blue; cursor: pointer;">${match}</span>`; // ✅ Span cliquable return `<span class="keyword" data-mot="${mot_clef}" style="color: blue; cursor: pointer;">${match}</span>`;
}); });
}); });
console.log("🔄 [ContentSectionCompetences] Contenu transformé avec mots-clés :", modifiedText);
return modifiedText; return modifiedText;
} }
@ -62,7 +84,7 @@ export default function ContentSectionCompetences({ competenceData, glossaireDat
// ✅ Gestion des clics sur les mots-clés // ✅ Gestion des clics sur les mots-clés
useEffect(() => { useEffect(() => {
function handleKeywordClick(event: any) { function handleKeywordClick(event: MouseEvent) {
const target = event.target as HTMLElement; const target = event.target as HTMLElement;
if (target.classList.contains("keyword")) { if (target.classList.contains("keyword")) {
const mot = target.getAttribute("data-mot"); const mot = target.getAttribute("data-mot");
@ -73,25 +95,18 @@ export default function ContentSectionCompetences({ competenceData, glossaireDat
} }
} }
document.addEventListener("click", handleKeywordClick); document.body.addEventListener("click", handleKeywordClick);
return () => document.removeEventListener("click", handleKeywordClick); return () => document.body.removeEventListener("click", handleKeywordClick);
}, [glossaireData]); }, [glossaireData]);
return ( return (
// ✅ Affichage de la compétence
<div className="max-w-3xl mx-auto p-6"> <div className="max-w-3xl mx-auto p-6">
{/* Titre de la section */} {/* ✅ Ajout de logs visuels dans le rendu */}
<h1 className={titleClass || "text-3xl mb-6 font-bold text-gray-700"}>{name}</h1> <h1 className={titleClass || "text-3xl mb-6 font-bold text-gray-700"}>{name}</h1>
{/* Carrousel pour afficher les images */}
<CarouselCompetences images={images} className="w-full h-64" /> <CarouselCompetences images={images} className="w-full h-64" />
{/* 🔥 Affichage du texte riche avec mots-clés cliquables */}
<div className={contentClass || "mt-6 text-lg text-black-700"}> <div className={contentClass || "mt-6 text-lg text-black-700"}>
<ReactMarkdown rehypePlugins={[rehypeRaw]}>{contentWithLinks}</ReactMarkdown> {/* ✅ Permet d'interpréter le HTML */} <ReactMarkdown rehypePlugins={[rehypeRaw]}>{contentWithLinks}</ReactMarkdown>
</div> </div>
{/* 🚀 Modale pour afficher les infos des mots-clés */}
{selectedMot && <ModalGlossaire mot={selectedMot} onClose={() => setSelectedMot(null)} />} {selectedMot && <ModalGlossaire mot={selectedMot} onClose={() => setSelectedMot(null)} />}
</div> </div>
); );

View File

@ -1,7 +1,6 @@
import { fetchDataCompetences, fetchDataGlossaire } from "../utils/fetchDataCompetences"; import { fetchDataCompetences, fetchDataGlossaire } from "../utils/fetchDataCompetences";
import ContentSectionCompetences from "./ContentSectionCompetences"; import ContentSectionCompetences from "./ContentSectionCompetences";
// Définition des propriétés du composant ContentSection
interface ContentSectionProps { interface ContentSectionProps {
collection: string; collection: string;
slug: string; slug: string;
@ -9,11 +8,15 @@ interface ContentSectionProps {
contentClass?: string; contentClass?: string;
} }
// Composant principal ContentSection
export default async function ContentSectionCompetencesContainer({ collection, slug, titleClass, contentClass }: ContentSectionProps) { export default async function ContentSectionCompetencesContainer({ collection, slug, titleClass, contentClass }: ContentSectionProps) {
const competenceData = await fetchDataCompetences(collection, slug); console.log("🔍 [ContentSectionCompetencesContainer] Chargement des données...");
const glossaireData = await fetchDataGlossaire();
const competenceData = await fetchDataCompetences(collection, slug);
console.log("✅ [ContentSectionCompetencesContainer] Données compétences :", JSON.stringify(competenceData, null, 2));
const glossaireData = await fetchDataGlossaire();
console.log("✅ [ContentSectionCompetencesContainer] Données glossaire :", JSON.stringify(glossaireData, null, 2));
return ( return (
<ContentSectionCompetences <ContentSectionCompetences
competenceData={competenceData} competenceData={competenceData}

View File

@ -1,19 +1,32 @@
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"; // Importation du composant CarouselCompetences pour afficher les images import { getApiUrl } from "../utils/getApiUrl"; // ✅ Import de l'URL dynamique
import CarouselCompetences from "./CarouselCompetences"; // Importation du composant CarouselCompetences
// Définition des propriétés du composant ModalGlossaire // ✅ Définition des propriétés du composant ModalGlossaire
interface ModalGlossaireProps { interface ImageData {
mot: { url: string;
mot_clef: string; // Mot-clé du glossaire formats?: {
description: string; // Description du mot-clé large?: { url: string };
images?: any[]; // Images associées au mot-clé
}; };
onClose: () => void; // Fonction pour fermer la modale name?: string;
} }
// Composant principal ModalGlossaire interface GlossaireMot {
mot_clef: string; // Mot-clé du glossaire
description: string; // Description du mot-clé
images?: ImageData[]; // Images associées au mot-clé
}
interface ModalGlossaireProps {
mot: GlossaireMot;
onClose: () => void;
}
// ✅ Composant principal ModalGlossaire
export default function ModalGlossaire({ mot, onClose }: ModalGlossaireProps) { export default function ModalGlossaire({ mot, onClose }: ModalGlossaireProps) {
const apiUrl = getApiUrl(); // 🔥 Détection automatique de l'URL API
// Désactiver le scroll du `body` quand la modale est ouverte // Désactiver le scroll du `body` quand la modale est ouverte
useEffect(() => { useEffect(() => {
document.body.classList.add("overflow-hidden"); document.body.classList.add("overflow-hidden");
@ -25,13 +38,11 @@ export default function ModalGlossaire({ mot, onClose }: ModalGlossaireProps) {
// Debug : Vérifier les images reçues // Debug : Vérifier les images reçues
console.log("🖼️ Images reçues dans la modale :", mot.images); console.log("🖼️ Images reçues dans la modale :", mot.images);
// Vérifier si `mot.images` est bien un tableau et contient des images // ✅ Vérification et mise à jour des URLs d'image avec `getApiUrl()`
const images = mot.images?.map((img: any) => { const images = mot.images?.map((img) => ({
return { url: `${apiUrl}${img.formats?.large?.url || img.url}`,
url: `http://localhost:1337${img.formats?.large?.url || img.url}`, alt: img.name || "Illustration",
alt: img.name || "Illustration", })) || [];
};
}) || [];
return createPortal( 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="fixed inset-0 w-screen h-screen bg-black bg-opacity-75 flex items-center justify-center z-[1000]">
@ -57,4 +68,4 @@ export default function ModalGlossaire({ mot, onClose }: ModalGlossaireProps) {
</div>, </div>,
document.body document.body
); );
} }

View File

@ -1,40 +1,43 @@
import React from "react"; "use client";
import ReactMarkdown from "react-markdown"; // Importation de ReactMarkdown
import React, { useEffect, useState } from "react";
import ReactMarkdown from "react-markdown";
import "./assets/main.css"; import "./assets/main.css";
import { getApiUrl } from "./utils/getApiUrl"; // 🔥 Import de l'URL dynamique
async function getHomepageData() { async function getHomepageData() {
const apiUrl = getApiUrl(); // 🔥 Utilisation de l'URL centralisée
try { try {
const response = await fetch("http://localhost:1337/api/homepages?populate=*"); const response = await fetch(`${apiUrl}/api/homepages?populate=*`);
if (!response.ok) { if (!response.ok) {
throw new Error("Failed to fetch homepage content"); throw new Error("Failed to fetch homepage content");
} }
const homepage = await response.json(); const data = await response.json();
return homepage.data?.[0]; // On récupère la première entrée return data.data?.[0] ?? null;
} catch (error) { } catch (error) {
console.error("Error fetching homepage:", error); console.error("Error fetching homepage:", error);
return null; return null;
} }
} }
export default async function HomePage() { export default function HomePage() {
const homepage = await getHomepageData(); const [homepage, setHomepage] = useState<any>(null);
const apiUrl = getApiUrl();
useEffect(() => {
getHomepageData().then((data) => setHomepage(data));
}, []);
if (!homepage) return <p className="text-center text-red-500">Erreur lors du chargement du contenu.</p>; if (!homepage) return <p className="text-center text-red-500">Erreur lors du chargement du contenu.</p>;
// Récupération des données const title = homepage.title ?? "Titre par défaut";
const title = homepage?.title; const cv = homepage.cv ?? "";
const cv = homepage?.cv || ""; // Assurer que `cv` est une chaîne même si vide const imageUrl = homepage.photo?.url ? `${apiUrl}${homepage.photo.url}` : null;
const photo = homepage?.photo;
// Correction de l'URL de l'image
const baseUrl = "http://localhost:1337";
const imageUrl = photo?.url ? `${baseUrl}${photo.url}` : null;
return ( return (
<main className="max-w-3xl w-full mx-auto flex flex-col items-center justify-center p-6 bg-white/55 rounded-lg mt-12 mb-3"> <main className="max-w-3xl w-full mx-auto flex flex-col items-center justify-center p-6 bg-white/55 rounded-lg mt-12 mb-3">
{/* Texte court (title) */}
<h1 className="text-3xl font-bold text-gray-800 mb-4">{title}</h1> <h1 className="text-3xl font-bold text-gray-800 mb-4">{title}</h1>
{/* Photo en cadre ovale avec effet hover */}
{imageUrl ? ( {imageUrl ? (
<div className="relative w-64 h-64 rounded-full overflow-hidden shadow-lg border-4 border-gray-300 transition-transform duration-300 hover:scale-110 hover:rotate-3"> <div className="relative w-64 h-64 rounded-full overflow-hidden shadow-lg border-4 border-gray-300 transition-transform duration-300 hover:scale-110 hover:rotate-3">
<img src={imageUrl} alt="Photo de profil" className="w-full h-full object-cover object-center" /> <img src={imageUrl} alt="Photo de profil" className="w-full h-full object-cover object-center" />
@ -45,7 +48,6 @@ export default async function HomePage() {
</div> </div>
)} )}
{/* Texte riche en Markdown */}
<div className="mt-6 text-lg text-black-700 max-w-2xl px-6 text-center"> <div className="mt-6 text-lg text-black-700 max-w-2xl px-6 text-center">
<ReactMarkdown>{cv}</ReactMarkdown> <ReactMarkdown>{cv}</ReactMarkdown>
</div> </div>

View File

@ -1,51 +1,11 @@
"use client";
import { useParams } from "next/navigation";
import ContentSection from "../../components/ContentSection"; import ContentSection from "../../components/ContentSection";
import { useEffect, useState } from "react";
export default function Page() { export default function Page({ params }: { params: { slug: string } }) {
const params = useParams(); const slug = params.slug;
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null); // ✅ Ajout du typage string | null
const slug = typeof params.slug === "string" ? params.slug : "";
useEffect(() => { if (!slug) {
if (!params?.slug) return;
async function fetchData() {
try {
const response = await fetch(`http://localhost:1337/api/projects?filters[slug][$eq]=${params.slug}&populate=*`);
const jsonData = await response.json();
if (!jsonData?.data || jsonData.data.length === 0) {
setError("❌ Erreur : Projet introuvable.");
} else {
setData(jsonData.data);
}
} catch (err) {
setError("❌ Erreur de chargement des données.");
} finally {
setLoading(false);
}
}
fetchData();
}, [params.slug]);
if (!params?.slug) {
return <div className="text-red-500 text-center"> Erreur : Slug introuvable.</div>; return <div className="text-red-500 text-center"> Erreur : Slug introuvable.</div>;
} }
if (loading) { return <ContentSection collection="projects" slug={slug} />;
return <div className="text-blue-500 text-center"> Chargement...</div>;
}
if (error) {
return <div className="text-red-500 text-center">{error}</div>;
}
return params.slug ? <ContentSection collection="projects" slug={slug} /> : <div className="text-red-500 text-center"> Erreur : Slug introuvable.</div>;
} }

View File

@ -1,23 +1,30 @@
"use client";
import { useEffect, useState } from "react";
import Link from "next/link"; import Link from "next/link";
import { getApiUrl } from "../utils/getApiUrl"; // 🔥 Import de l'URL dynamique
// Fonction pour récupérer tous les projets depuis l'API Strapi export default function Page() {
async function getAllprojects() { const [projects, setProjects] = useState([]); // 🔥 Stocker les projets une seule fois
try { const apiUrl = getApiUrl(); // 🔥 Définition de l'URL API
const response = await fetch("http://localhost:1337/api/projects?populate=*");
if (!response.ok) { useEffect(() => {
throw new Error("Failed to fetch projects"); async function fetchProjects() {
console.log("🔍 API utilisée pour les projets :", apiUrl);
try {
const response = await fetch(`${apiUrl}/api/projects?populate=*`);
if (!response.ok) {
throw new Error(`Erreur de récupération des projets : ${response.statusText}`);
}
const data = await response.json();
setProjects(data.data ?? []);
} catch (error) {
console.error("❌ Erreur lors de la récupération des projets :", error);
}
} }
const projects = await response.json();
return projects.data;
} catch (error) {
console.error("Error fetching projects:", error);
return [];
}
}
// Composant principal de la page des projets fetchProjects(); // 🔥 Exécuter une seule fois au montage du composant
export default async function Page() { }, [apiUrl]); // Exécuter `useEffect()` uniquement si `apiUrl` change
const projects = await getAllprojects();
return ( return (
<main className="w-full p-3 mt-5 mb-5"> <main className="w-full p-3 mt-5 mb-5">
@ -28,7 +35,7 @@ export default async function Page() {
<div className="grid gap-7 grid-cols-[repeat(auto-fit,minmax(300px,1fr))] max-w-7xl mx-auto"> <div className="grid gap-7 grid-cols-[repeat(auto-fit,minmax(300px,1fr))] max-w-7xl mx-auto">
{projects.map((project) => { {projects.map((project) => {
const picture = project.picture?.[0]; const picture = project.picture?.[0];
const imageUrl = picture?.url ? `http://localhost:1337${picture.url}` : "/placeholder.jpg"; const imageUrl = picture?.url ? `${apiUrl}${picture.url}` : "/placeholder.jpg";
return ( return (
<div <div
@ -57,4 +64,4 @@ export default async function Page() {
</div> </div>
</main> </main>
); );
} }

3
app/utils/config.ts Normal file
View File

@ -0,0 +1,3 @@
// utils/config.ts
export const API_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:1337";

View File

@ -1,7 +1,10 @@
import qs from "qs"; // Importation de qs pour construire des requêtes de chaîne de requête import qs from "qs"; // Importation de qs pour construire des requêtes de chaîne de requête
import { getApiUrl } from "./getApiUrl"; // 🔥 Import de l'URL dynamique
// Fonction pour récupérer des données spécifiques depuis l'API Strapi // Fonction pour récupérer des données spécifiques depuis l'API Strapi
export async function fetchData(collection: string, slug: string) { export async function fetchData(collection: string, slug: string) {
const apiUrl = getApiUrl(); // 🔥 Détection automatique de l'URL (local ou HTTPS)
// Construction de la requête avec des filtres et des relations à peupler // Construction de la requête avec des filtres et des relations à peupler
const query = qs.stringify({ const query = qs.stringify({
filters: { slug }, // Filtre basé sur le slug filters: { slug }, // Filtre basé sur le slug
@ -9,14 +12,17 @@ export async function fetchData(collection: string, slug: string) {
}); });
try { try {
const fullUrl = `${apiUrl}/api/${collection}?${query}`; // 🔥 URL finale
console.log(`🔍 Requête API vers : ${fullUrl}`); // Log pour vérifier l'URL
// Envoi de la requête à l'API Strapi // Envoi de la requête à l'API Strapi
const response = await fetch(`http://localhost:1337/api/${collection}?${query}`, { const response = await fetch(fullUrl, {
cache: "no-store", // Désactivation du cache pour obtenir les données les plus récentes cache: "no-store", // Désactivation du cache pour obtenir les données les plus récentes
}); });
// Vérification de la réponse de l'API // Vérification de la réponse de l'API
if (!response.ok) { if (!response.ok) {
throw new Error("Failed to fetch data"); throw new Error(`Erreur HTTP ${response.status} : ${response.statusText}`);
} }
// Récupération des données de la réponse // Récupération des données de la réponse
@ -24,7 +30,7 @@ export async function fetchData(collection: string, slug: string) {
return data.data[0] || null; // Retourne la première entrée ou null si aucune donnée n'est trouvée return data.data[0] || null; // Retourne la première entrée ou null si aucune donnée n'est trouvée
} catch (error) { } catch (error) {
// Gestion des erreurs et log des erreurs // Gestion des erreurs et log des erreurs
console.error(`Error fetching ${collection} data:`, error); console.error(`❌ Erreur lors de la récupération des données (${collection}):`, error);
return null; return null;
} }
} }

View File

@ -1,67 +1,56 @@
import qs from "qs"; // Importation de qs pour construire des requêtes de chaîne de requête import qs from "qs";
import { getApiUrl } from "./getApiUrl";
// 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) {
// Construction de la requête avec des filtres et des relations à peupler const apiUrl = getApiUrl();
const query = qs.stringify({ const query = qs.stringify({
filters: { filters: { slug: { $eq: slug } },
slug: { $eq: slug }, populate: "picture",
},
populate: "picture", // On garde les images des compétences
}); });
// Log de la requête API pour le débogage const fullUrl = `${apiUrl}/api/${collection}?${query}`;
console.log(`🛠️ Requête API Compétence : http://localhost:1337/api/${collection}?${query}`); console.log("🔍 [fetchDataCompetences] Requête API :", fullUrl);
try { try {
// Envoi de la requête à l'API Strapi const response = await fetch(fullUrl, { cache: "no-store" });
const response = await fetch(`http://localhost:1337/api/${collection}?${query}`, {
cache: "no-store", console.log(`📡 [fetchDataCompetences] Réponse HTTP : ${response.status} ${response.statusText}`);
});
// Vérification de la réponse de l'API
if (!response.ok) { if (!response.ok) {
throw new Error(`Failed to fetch competences data: ${response.status}`); console.error(`❌ [fetchDataCompetences] Erreur HTTP ${response.status} : ${response.statusText}`);
return null;
} }
// Récupération des données de la réponse
const data = await response.json(); const data = await response.json();
console.log("✅ Données reçues (Compétence) :", data); console.log("✅ [fetchDataCompetences] Données reçues :", JSON.stringify(data, null, 2));
return data.data?.[0] ?? null;
// Retourne la première compétence ou null si aucune donnée n'est trouvée
return data.data[0] || null;
} catch (error) { } catch (error) {
// Gestion des erreurs et log des erreurs console.error("❌ [fetchDataCompetences] Erreur lors de la récupération des compétences :", error);
console.error("❌ Erreur lors de la récupération des compétences :", error);
return null; return null;
} }
} }
// Fonction pour récupérer les données du glossaire
export async function fetchDataGlossaire() { export async function fetchDataGlossaire() {
const apiUrl = getApiUrl();
const fullUrl = `${apiUrl}/api/glossaires?populate=images`;
console.log("🔍 [fetchDataGlossaire] Requête API :", fullUrl);
try { try {
// Log de la requête API pour le débogage const response = await fetch(fullUrl, { cache: "no-store" });
console.log("🛠️ Requête API Glossaire : http://localhost:1337/api/glossaires?populate=images");
// Envoi de la requête à l'API Strapi console.log(`📡 [fetchDataGlossaire] Réponse HTTP : ${response.status} ${response.statusText}`);
const response = await fetch("http://localhost:1337/api/glossaires?populate=images", {
cache: "no-store",
});
// Vérification de la réponse de l'API
if (!response.ok) { if (!response.ok) {
throw new Error(`Failed to fetch glossaire data: ${response.status}`); console.error(`❌ [fetchDataGlossaire] Erreur HTTP ${response.status} : ${response.statusText}`);
return [];
} }
// Récupération des données de la réponse
const data = await response.json(); const data = await response.json();
console.log("✅ Données reçues (Glossaire) :", data); console.log("✅ [fetchDataGlossaire] Données reçues :", JSON.stringify(data, null, 2));
return data.data ?? [];
// Retourne les données du glossaire ou un tableau vide si aucune donnée n'est trouvée
return data.data || [];
} catch (error) { } catch (error) {
// Gestion des erreurs et log des erreurs console.error("❌ [fetchDataGlossaire] Erreur lors de la récupération du glossaire :", error);
console.error("❌ Erreur lors de la récupération du glossaire :", error);
return []; return [];
} }
} }

19
app/utils/getApiUrl.ts Normal file
View File

@ -0,0 +1,19 @@
export function getApiUrl() {
if (typeof window !== "undefined") {
// 🔥 Détection du mode local côté client
const isLocalhost =
window.location.hostname === "localhost" ||
window.location.hostname === "127.0.0.1" ||
window.location.hostname.startsWith("192.168.") ||
window.location.hostname.endsWith(".local");
console.log("🌍 [getApiUrl] Mode CLIENT détecté - URL :", isLocalhost ? "http://localhost:1337" : "https://api.fernandgrascalvet.com");
return isLocalhost ? "http://localhost:1337" : "https://api.fernandgrascalvet.com";
}
// 🔥 Côté serveur (SSR), on utilise une variable d'environnement
const apiUrl = process.env.NEXT_PUBLIC_API_URL || "https://api.fernandgrascalvet.com";
console.log("🌍 [getApiUrl] Mode SERVEUR détecté - URL :", apiUrl);
return apiUrl;
}

View File

@ -1,26 +1,28 @@
/** @type {import('next').NextConfig} */ /** @type {import('next').NextConfig} */
const nextConfig = {
// Active le mode strict de React pour signaler des erreurs potentielles
reactStrictMode: true,
experimental: {
appDir: true, // ✅ Assurez-vous que cette ligne est bien présente
},
// Gestion des réécritures d'URL pour proxy local vers le backend require("dotenv").config();
console.log("🔍 Vérification NEXT_PUBLIC_API_URL:", process.env.NEXT_PUBLIC_API_URL);
const nextConfig = {
reactStrictMode: true,
compress: false, // ❌ Désactive la compression Gzip pour éviter les erreurs IIS
trailingSlash: true,
// Utilisation de l'API URL dynamique pour Strapi
async rewrites() { async rewrites() {
return [ return [
{ {
source: "/api/:path*", // Toute URL commençant par /api source: "/api/:path*",
destination: "http://localhost:1337/api/:path*", // Redirige vers votre backend Strapi destination: process.env.NEXT_PUBLIC_API_URL + "/api/:path*",
}, },
]; ];
}, },
// Optimisation des fichiers statiques
images: { images: {
domains: ["localhost"], // Permet de charger les images provenant de "localhost" si nécessaire domains: ["localhost", "api.fernandgrascalvet.com"], // ✅ Autorise aussi l'API en HTTPS
}, },
}; };
export default nextConfig; module.exports = nextConfig;

13
package-lock.json generated
View File

@ -10,6 +10,7 @@
"dependencies": { "dependencies": {
"@strapi/blocks-react-renderer": "^1.0.1", "@strapi/blocks-react-renderer": "^1.0.1",
"@tailwindcss/typography": "^0.5.16", "@tailwindcss/typography": "^0.5.16",
"dotenv": "^16.4.7",
"husky": "^9.1.7", "husky": "^9.1.7",
"next": "^15.1.6", "next": "^15.1.6",
"qs": "^6.14.0", "qs": "^6.14.0",
@ -1243,6 +1244,18 @@
"integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/dotenv": {
"version": "16.4.7",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz",
"integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://dotenvx.com"
}
},
"node_modules/dunder-proto": { "node_modules/dunder-proto": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",

View File

@ -11,6 +11,7 @@
"dependencies": { "dependencies": {
"@strapi/blocks-react-renderer": "^1.0.1", "@strapi/blocks-react-renderer": "^1.0.1",
"@tailwindcss/typography": "^0.5.16", "@tailwindcss/typography": "^0.5.16",
"dotenv": "^16.4.7",
"husky": "^9.1.7", "husky": "^9.1.7",
"next": "^15.1.6", "next": "^15.1.6",
"qs": "^6.14.0", "qs": "^6.14.0",

Binary file not shown.

Binary file not shown.

26
web.config Normal file
View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<system.webServer>
<rewrite>
<outboundRules>
<rule name="ReverseProxyOutboundRule1" preCondition="ResponseIsHtml1">
<match filterByTags="A, Form, Img" pattern="^http(s)?://http://localhost:3000/(.*)" />
<action type="Rewrite" value="http{R:1}://localhost/{R:2}" />
</rule>
<preConditions>
<preCondition name="ResponseIsHtml1">
<add input="{RESPONSE_CONTENT_TYPE}" pattern="^text/html" />
</preCondition>
</preConditions>
</outboundRules>
<rules>
<rule name="ReverseProxyInboundRule1" stopProcessing="true">
<match url="(.*)" />
<action type="Rewrite" url="http://localhost:3000/{R:1}" />
</rule>
</rules>
</rewrite>
<directoryBrowse enabled="true" />
<caching enableKernelCache="true" />
</system.webServer>
</configuration>