mirror of
https://github.com/Ladebeze66/devsite.git
synced 2025-12-13 04:36:49 +01:00
test
This commit is contained in:
parent
8bf66f695d
commit
d45d36465c
@ -1,58 +1,71 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
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
|
||||
async function getAllCompetences() {
|
||||
try {
|
||||
const response = await fetch("http://localhost:1337/api/competences?populate=*");
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to fetch competences");
|
||||
export default function Page() {
|
||||
const [competences, setCompetences] = useState([]); // 🔥 Stocker les compétences une seule fois
|
||||
const apiUrl = getApiUrl(); // 🔥 Définition de l'URL API
|
||||
|
||||
useEffect(() => {
|
||||
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
|
||||
export default async function Page() {
|
||||
const competences = await getAllCompetences();
|
||||
fetchCompetences(); // 🔥 Exécuter une seule fois au montage du composant
|
||||
}, [apiUrl]); // ✅ Exécuter `useEffect()` uniquement si `apiUrl` change
|
||||
|
||||
return (
|
||||
<main className="w-full p-6">
|
||||
<h1 className="text-3xl mb-6 font-bold text-gray-700 text-center">Mes Compétences</h1>
|
||||
<main className="w-full p-3 mt-5 mb-5">
|
||||
{/* 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 */}
|
||||
<div className="grid gap-4 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 ? `http://localhost:1337${picture.url}` : "/placeholder.jpg";
|
||||
{/* Affichage d'un message si aucune compétence n'est trouvée */}
|
||||
{competences.length === 0 ? (
|
||||
<p className="text-center text-gray-500">Aucune compétence disponible.</p>
|
||||
) : (
|
||||
<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 (
|
||||
<div
|
||||
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"
|
||||
>
|
||||
<Link href={`/competences/${competence.slug}`}>
|
||||
<div className="overflow-hidden w-full h-48">
|
||||
<img
|
||||
src={imageUrl}
|
||||
alt={picture?.name || "Competence image"}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
<div className="p-4 flex-grow">
|
||||
<p className="font-bold text-xl mb-2">{competence.name}</p>
|
||||
<p className="text-gray-700 text-sm hover:text-base transition-all duration-200 ease-in-out">
|
||||
{competence.description}
|
||||
</p>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
return (
|
||||
<div
|
||||
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 p-4"
|
||||
>
|
||||
{/* Lien vers la page de détail de la compétence */}
|
||||
<Link href={`/competences/${competence.slug}`}>
|
||||
<div className="overflow-hidden w-full h-48 mb-4">
|
||||
<img
|
||||
src={imageUrl}
|
||||
alt={picture?.name || "Competence image"}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-grow overflow-y-auto max-h-32 hide-scrollbar show-scrollbar">
|
||||
<p className="font-bold text-xl mb-2">{competence.name}</p>
|
||||
<p className="text-gray-700 text-sm hover:text-base transition-all duration-200 ease-in-out">
|
||||
{competence.description}
|
||||
</p>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,6 +1,30 @@
|
||||
import { fetchData } from "../utils/fetchData"; // Importation de la fonction fetchData pour récupérer les données depuis l'API
|
||||
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
|
||||
"use client";
|
||||
|
||||
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
|
||||
interface ContentSectionProps {
|
||||
@ -11,21 +35,31 @@ interface ContentSectionProps {
|
||||
}
|
||||
|
||||
// Composant principal ContentSection
|
||||
export default async function ContentSection({ collection, slug, titleClass, contentClass }: ContentSectionProps) {
|
||||
// Récupération des données depuis l'API en utilisant la fonction fetchData
|
||||
const data = await fetchData(collection, slug);
|
||||
export default function ContentSection({ collection, slug, titleClass, contentClass }: ContentSectionProps) {
|
||||
const [data, setData] = useState<ContentData | null>(null);
|
||||
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
|
||||
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
|
||||
const { name, Resum: richText, picture, link, linkText } = data;
|
||||
|
||||
// Transformation des 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}`, // Utilisation de l'URL de l'image en format large ou originale
|
||||
const images = picture?.map((img: ImageData) => ({
|
||||
url: `${apiUrl}${img.formats?.large?.url || img.url}`, // 🔥 URL dynamique
|
||||
alt: img.name || "Image", // Texte alternatif pour l'image
|
||||
})) || [];
|
||||
|
||||
@ -57,4 +91,4 @@ export default async function ContentSection({ collection, slug, titleClass, con
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,60 +1,82 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { getApiUrl } from "../utils/getApiUrl"; // ✅ Importation de l'URL dynamique
|
||||
import CarouselCompetences from "./CarouselCompetences";
|
||||
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";
|
||||
|
||||
// Définition des propriétés du composant ContentSectionCompetences
|
||||
interface ContentSectionProps {
|
||||
competenceData: any;
|
||||
glossaireData: any[];
|
||||
titleClass?: string;
|
||||
contentClass?: string;
|
||||
// ✅ Définition des types pour TypeScript
|
||||
interface ImageData {
|
||||
url: string;
|
||||
formats?: {
|
||||
large?: { url: string };
|
||||
};
|
||||
name?: string;
|
||||
}
|
||||
|
||||
interface CompetenceData {
|
||||
name: string;
|
||||
content: string;
|
||||
picture?: ImageData[];
|
||||
}
|
||||
|
||||
// ✅ Définition du type Glossaire
|
||||
interface GlossaireItem {
|
||||
mot_clef: string;
|
||||
slug: string;
|
||||
variantes: 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) {
|
||||
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 apiUrl = getApiUrl(); // ✅ Détection automatique de l'URL API
|
||||
|
||||
if (!competenceData) {
|
||||
console.error("❌ [ContentSectionCompetences] Compétence introuvable !");
|
||||
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;
|
||||
|
||||
// Transformation des 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}`,
|
||||
// ✅ Transformation des images de Strapi en format attendu par le carrousel
|
||||
const images = picture?.map((img) => ({
|
||||
url: `${apiUrl}${img.formats?.large?.url || img.url}`, // ✅ Correction ici
|
||||
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) {
|
||||
if (!glossaireData || glossaireData.length === 0) return text;
|
||||
if (!glossaireData.length) return text;
|
||||
|
||||
let modifiedText = text;
|
||||
|
||||
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");
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@ -62,7 +84,7 @@ export default function ContentSectionCompetences({ competenceData, glossaireDat
|
||||
|
||||
// ✅ Gestion des clics sur les mots-clés
|
||||
useEffect(() => {
|
||||
function handleKeywordClick(event: any) {
|
||||
function handleKeywordClick(event: MouseEvent) {
|
||||
const target = event.target as HTMLElement;
|
||||
if (target.classList.contains("keyword")) {
|
||||
const mot = target.getAttribute("data-mot");
|
||||
@ -73,25 +95,18 @@ export default function ContentSectionCompetences({ competenceData, glossaireDat
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("click", handleKeywordClick);
|
||||
return () => document.removeEventListener("click", handleKeywordClick);
|
||||
document.body.addEventListener("click", handleKeywordClick);
|
||||
return () => document.body.removeEventListener("click", handleKeywordClick);
|
||||
}, [glossaireData]);
|
||||
|
||||
return (
|
||||
// ✅ Affichage de la compétence
|
||||
<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>
|
||||
|
||||
{/* Carrousel pour afficher les images */}
|
||||
<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"}>
|
||||
<ReactMarkdown rehypePlugins={[rehypeRaw]}>{contentWithLinks}</ReactMarkdown> {/* ✅ Permet d'interpréter le HTML */}
|
||||
<ReactMarkdown rehypePlugins={[rehypeRaw]}>{contentWithLinks}</ReactMarkdown>
|
||||
</div>
|
||||
|
||||
{/* 🚀 Modale pour afficher les infos des mots-clés */}
|
||||
{selectedMot && <ModalGlossaire mot={selectedMot} onClose={() => setSelectedMot(null)} />}
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import { fetchDataCompetences, fetchDataGlossaire } from "../utils/fetchDataCompetences";
|
||||
import ContentSectionCompetences from "./ContentSectionCompetences";
|
||||
|
||||
// Définition des propriétés du composant ContentSection
|
||||
interface ContentSectionProps {
|
||||
collection: string;
|
||||
slug: string;
|
||||
@ -9,11 +8,15 @@ interface ContentSectionProps {
|
||||
contentClass?: string;
|
||||
}
|
||||
|
||||
// Composant principal ContentSection
|
||||
export default async function ContentSectionCompetencesContainer({ collection, slug, titleClass, contentClass }: ContentSectionProps) {
|
||||
const competenceData = await fetchDataCompetences(collection, slug);
|
||||
const glossaireData = await fetchDataGlossaire();
|
||||
console.log("🔍 [ContentSectionCompetencesContainer] Chargement des données...");
|
||||
|
||||
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 (
|
||||
<ContentSectionCompetences
|
||||
competenceData={competenceData}
|
||||
|
||||
@ -1,19 +1,32 @@
|
||||
import { useEffect } from "react";
|
||||
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
|
||||
interface ModalGlossaireProps {
|
||||
mot: {
|
||||
mot_clef: string; // Mot-clé du glossaire
|
||||
description: string; // Description du mot-clé
|
||||
images?: any[]; // Images associées au mot-clé
|
||||
// ✅ Définition des propriétés du composant ModalGlossaire
|
||||
interface ImageData {
|
||||
url: string;
|
||||
formats?: {
|
||||
large?: { url: string };
|
||||
};
|
||||
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) {
|
||||
const apiUrl = getApiUrl(); // 🔥 Détection automatique de l'URL API
|
||||
|
||||
// Désactiver le scroll du `body` quand la modale est ouverte
|
||||
useEffect(() => {
|
||||
document.body.classList.add("overflow-hidden");
|
||||
@ -25,13 +38,11 @@ export default function ModalGlossaire({ mot, onClose }: ModalGlossaireProps) {
|
||||
// Debug : Vérifier les images reçues
|
||||
console.log("🖼️ Images reçues dans la modale :", mot.images);
|
||||
|
||||
// Vérifier si `mot.images` est bien un tableau et contient des images
|
||||
const images = mot.images?.map((img: any) => {
|
||||
return {
|
||||
url: `http://localhost:1337${img.formats?.large?.url || img.url}`,
|
||||
alt: img.name || "Illustration",
|
||||
};
|
||||
}) || [];
|
||||
// ✅ Vérification et mise à jour des URLs d'image avec `getApiUrl()`
|
||||
const images = mot.images?.map((img) => ({
|
||||
url: `${apiUrl}${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]">
|
||||
@ -57,4 +68,4 @@ export default function ModalGlossaire({ mot, onClose }: ModalGlossaireProps) {
|
||||
</div>,
|
||||
document.body
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
38
app/page.tsx
38
app/page.tsx
@ -1,40 +1,43 @@
|
||||
import React from "react";
|
||||
import ReactMarkdown from "react-markdown"; // Importation de ReactMarkdown
|
||||
"use client";
|
||||
|
||||
import React, { useEffect, useState } from "react";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import "./assets/main.css";
|
||||
import { getApiUrl } from "./utils/getApiUrl"; // 🔥 Import de l'URL dynamique
|
||||
|
||||
async function getHomepageData() {
|
||||
const apiUrl = getApiUrl(); // 🔥 Utilisation de l'URL centralisée
|
||||
try {
|
||||
const response = await fetch("http://localhost:1337/api/homepages?populate=*");
|
||||
const response = await fetch(`${apiUrl}/api/homepages?populate=*`);
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to fetch homepage content");
|
||||
}
|
||||
const homepage = await response.json();
|
||||
return homepage.data?.[0]; // On récupère la première entrée
|
||||
const data = await response.json();
|
||||
return data.data?.[0] ?? null;
|
||||
} catch (error) {
|
||||
console.error("Error fetching homepage:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export default async function HomePage() {
|
||||
const homepage = await getHomepageData();
|
||||
export default function HomePage() {
|
||||
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>;
|
||||
|
||||
// Récupération des données
|
||||
const title = homepage?.title;
|
||||
const cv = homepage?.cv || ""; // Assurer que `cv` est une chaîne même si vide
|
||||
const photo = homepage?.photo;
|
||||
|
||||
// Correction de l'URL de l'image
|
||||
const baseUrl = "http://localhost:1337";
|
||||
const imageUrl = photo?.url ? `${baseUrl}${photo.url}` : null;
|
||||
const title = homepage.title ?? "Titre par défaut";
|
||||
const cv = homepage.cv ?? "";
|
||||
const imageUrl = homepage.photo?.url ? `${apiUrl}${homepage.photo.url}` : null;
|
||||
|
||||
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">
|
||||
{/* Texte court (title) */}
|
||||
<h1 className="text-3xl font-bold text-gray-800 mb-4">{title}</h1>
|
||||
|
||||
{/* Photo en cadre ovale avec effet hover */}
|
||||
{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">
|
||||
<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>
|
||||
)}
|
||||
|
||||
{/* Texte riche en Markdown */}
|
||||
<div className="mt-6 text-lg text-black-700 max-w-2xl px-6 text-center">
|
||||
<ReactMarkdown>{cv}</ReactMarkdown>
|
||||
</div>
|
||||
|
||||
@ -1,51 +1,11 @@
|
||||
"use client";
|
||||
|
||||
import { useParams } from "next/navigation";
|
||||
import ContentSection from "../../components/ContentSection";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export default function Page() {
|
||||
const params = useParams();
|
||||
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 : "";
|
||||
export default function Page({ params }: { params: { slug: string } }) {
|
||||
const slug = params.slug;
|
||||
|
||||
useEffect(() => {
|
||||
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) {
|
||||
if (!slug) {
|
||||
return <div className="text-red-500 text-center">❌ Erreur : Slug introuvable.</div>;
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
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>;
|
||||
|
||||
return <ContentSection collection="projects" slug={slug} />;
|
||||
}
|
||||
|
||||
@ -1,23 +1,30 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
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
|
||||
async function getAllprojects() {
|
||||
try {
|
||||
const response = await fetch("http://localhost:1337/api/projects?populate=*");
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to fetch projects");
|
||||
export default function Page() {
|
||||
const [projects, setProjects] = useState([]); // 🔥 Stocker les projets une seule fois
|
||||
const apiUrl = getApiUrl(); // 🔥 Définition de l'URL API
|
||||
|
||||
useEffect(() => {
|
||||
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
|
||||
export default async function Page() {
|
||||
const projects = await getAllprojects();
|
||||
fetchProjects(); // 🔥 Exécuter une seule fois au montage du composant
|
||||
}, [apiUrl]); // ✅ Exécuter `useEffect()` uniquement si `apiUrl` change
|
||||
|
||||
return (
|
||||
<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">
|
||||
{projects.map((project) => {
|
||||
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 (
|
||||
<div
|
||||
@ -57,4 +64,4 @@ export default async function Page() {
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
3
app/utils/config.ts
Normal file
3
app/utils/config.ts
Normal file
@ -0,0 +1,3 @@
|
||||
// utils/config.ts
|
||||
|
||||
export const API_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:1337";
|
||||
@ -1,7 +1,10 @@
|
||||
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
|
||||
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
|
||||
const query = qs.stringify({
|
||||
filters: { slug }, // Filtre basé sur le slug
|
||||
@ -9,14 +12,17 @@ export async function fetchData(collection: string, slug: string) {
|
||||
});
|
||||
|
||||
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
|
||||
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
|
||||
});
|
||||
|
||||
// Vérification de la réponse de l'API
|
||||
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
|
||||
@ -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
|
||||
} catch (error) {
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
// Construction de la requête avec des filtres et des relations à peupler
|
||||
const apiUrl = getApiUrl();
|
||||
const query = qs.stringify({
|
||||
filters: {
|
||||
slug: { $eq: slug },
|
||||
},
|
||||
populate: "picture", // On garde les images des compétences
|
||||
filters: { slug: { $eq: slug } },
|
||||
populate: "picture",
|
||||
});
|
||||
|
||||
// Log de la requête API pour le débogage
|
||||
console.log(`🛠️ Requête API Compétence : http://localhost:1337/api/${collection}?${query}`);
|
||||
const fullUrl = `${apiUrl}/api/${collection}?${query}`;
|
||||
console.log("🔍 [fetchDataCompetences] Requête API :", fullUrl);
|
||||
|
||||
try {
|
||||
// Envoi de la requête à l'API Strapi
|
||||
const response = await fetch(`http://localhost:1337/api/${collection}?${query}`, {
|
||||
cache: "no-store",
|
||||
});
|
||||
const response = await fetch(fullUrl, { 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) {
|
||||
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();
|
||||
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;
|
||||
console.log("✅ [fetchDataCompetences] Données reçues :", JSON.stringify(data, null, 2));
|
||||
return data.data?.[0] ?? null;
|
||||
} catch (error) {
|
||||
// Gestion des erreurs et log des erreurs
|
||||
console.error("❌ Erreur lors de la récupération des compétences :", error);
|
||||
console.error("❌ [fetchDataCompetences] Erreur lors de la récupération des compétences :", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Fonction pour récupérer les données du glossaire
|
||||
export async function fetchDataGlossaire() {
|
||||
const apiUrl = getApiUrl();
|
||||
const fullUrl = `${apiUrl}/api/glossaires?populate=images`;
|
||||
|
||||
console.log("🔍 [fetchDataGlossaire] Requête API :", fullUrl);
|
||||
|
||||
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 response = await fetch(fullUrl, { cache: "no-store" });
|
||||
|
||||
// Envoi de la requête à l'API Strapi
|
||||
const response = await fetch("http://localhost:1337/api/glossaires?populate=images", {
|
||||
cache: "no-store",
|
||||
});
|
||||
console.log(`📡 [fetchDataGlossaire] Réponse HTTP : ${response.status} ${response.statusText}`);
|
||||
|
||||
// Vérification de la réponse de l'API
|
||||
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();
|
||||
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 || [];
|
||||
console.log("✅ [fetchDataGlossaire] Données reçues :", JSON.stringify(data, null, 2));
|
||||
return data.data ?? [];
|
||||
} catch (error) {
|
||||
// Gestion des erreurs et log des erreurs
|
||||
console.error("❌ Erreur lors de la récupération du glossaire :", error);
|
||||
console.error("❌ [fetchDataGlossaire] Erreur lors de la récupération du glossaire :", error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
19
app/utils/getApiUrl.ts
Normal file
19
app/utils/getApiUrl.ts
Normal 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;
|
||||
}
|
||||
|
||||
@ -1,26 +1,28 @@
|
||||
/** @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() {
|
||||
return [
|
||||
{
|
||||
source: "/api/:path*", // Toute URL commençant par /api
|
||||
destination: "http://localhost:1337/api/:path*", // Redirige vers votre backend Strapi
|
||||
source: "/api/:path*",
|
||||
destination: process.env.NEXT_PUBLIC_API_URL + "/api/:path*",
|
||||
},
|
||||
];
|
||||
},
|
||||
|
||||
// Optimisation des fichiers statiques
|
||||
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
13
package-lock.json
generated
@ -10,6 +10,7 @@
|
||||
"dependencies": {
|
||||
"@strapi/blocks-react-renderer": "^1.0.1",
|
||||
"@tailwindcss/typography": "^0.5.16",
|
||||
"dotenv": "^16.4.7",
|
||||
"husky": "^9.1.7",
|
||||
"next": "^15.1.6",
|
||||
"qs": "^6.14.0",
|
||||
@ -1243,6 +1244,18 @@
|
||||
"integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
|
||||
"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": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
|
||||
@ -11,6 +11,7 @@
|
||||
"dependencies": {
|
||||
"@strapi/blocks-react-renderer": "^1.0.1",
|
||||
"@tailwindcss/typography": "^0.5.16",
|
||||
"dotenv": "^16.4.7",
|
||||
"husky": "^9.1.7",
|
||||
"next": "^15.1.6",
|
||||
"qs": "^6.14.0",
|
||||
|
||||
BIN
public/.well-known/acme-challenge/test-www.txt
Normal file
BIN
public/.well-known/acme-challenge/test-www.txt
Normal file
Binary file not shown.
BIN
public/.well-known/acme-challenge/test.txt
Normal file
BIN
public/.well-known/acme-challenge/test.txt
Normal file
Binary file not shown.
26
web.config
Normal file
26
web.config
Normal 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>
|
||||
Loading…
x
Reference in New Issue
Block a user