messagerie
@ -3,6 +3,7 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { getApiUrl } from "../utils/getApiUrl"; // 🔥 Import de l'URL dynamique
|
||||
import CarouselCompetences from "../components/CarouselCompetences"; // 🔥 Import du composant CarouselCompetences
|
||||
|
||||
export default function Page() {
|
||||
const [competences, setCompetences] = useState([]); // 🔥 Stocker les compétences une seule fois
|
||||
@ -28,34 +29,31 @@ export default function Page() {
|
||||
|
||||
return (
|
||||
<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>
|
||||
|
||||
{/* 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">
|
||||
<div className="flex flex-col gap-7 max-w-7xl mx-auto">
|
||||
{competences.map((competence) => {
|
||||
const picture = competence.picture?.[0];
|
||||
const imageUrl = picture?.url ? `${apiUrl}${picture.url}` : "/placeholder.jpg";
|
||||
const pictures = competence.picture || [];
|
||||
const images = pictures.map(picture => ({
|
||||
url: picture.url ? `${apiUrl}${picture.url}` : "/placeholder.jpg",
|
||||
alt: picture.name || "Competence image"
|
||||
}));
|
||||
|
||||
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"
|
||||
className="bg-white/70 rounded-lg shadow-md overflow-hidden w-full h-auto 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 className="overflow-hidden w-full h-64 mb-4">
|
||||
<CarouselCompetences images={images} 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="font-orbitron-16-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>
|
||||
|
||||
@ -1,11 +1,13 @@
|
||||
// Composant principal de la page des messages
|
||||
import { getApiUrl } from "../../utils/getApiUrl"; // 🔥 Import de l'URL dynamique
|
||||
// // 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 apiUrl = getApiUrl();
|
||||
const res = await fetch(`${apiUrl}/api/messages`);
|
||||
const { data } = await res.json();
|
||||
|
||||
return (
|
||||
<div className="max-w-3xl mx-auto p-6">
|
||||
<div className="bg-white/70 rounded-md max-w-3xl mx-auto p-6">
|
||||
{/* Titre de la page */}
|
||||
<h1 className="text-3xl font-bold text-center mb-6">📬 Messages reçus</h1>
|
||||
|
||||
|
||||
|
After Width: | Height: | Size: 7.5 MiB |
|
After Width: | Height: | Size: 5.5 MiB |
|
After Width: | Height: | Size: 7.1 MiB |
|
After Width: | Height: | Size: 7.3 MiB |
|
After Width: | Height: | Size: 8.1 MiB |
|
After Width: | Height: | Size: 3.6 MiB |
|
After Width: | Height: | Size: 6.9 MiB |
|
After Width: | Height: | Size: 5.4 MiB |
|
After Width: | Height: | Size: 7.5 MiB |
|
After Width: | Height: | Size: 7.4 MiB |
|
After Width: | Height: | Size: 6.4 MiB |
|
After Width: | Height: | Size: 6.8 MiB |
|
After Width: | Height: | Size: 7.6 MiB |
|
After Width: | Height: | Size: 3.3 MiB |
|
After Width: | Height: | Size: 8.7 MiB |
|
After Width: | Height: | Size: 8.3 MiB |
|
After Width: | Height: | Size: 8.0 MiB |
|
After Width: | Height: | Size: 7.5 MiB |
|
After Width: | Height: | Size: 5.8 MiB |
|
After Width: | Height: | Size: 7.2 MiB |
|
After Width: | Height: | Size: 6.8 MiB |
|
After Width: | Height: | Size: 6.0 MiB |
|
After Width: | Height: | Size: 7.8 MiB |
|
After Width: | Height: | Size: 9.0 MiB |
|
After Width: | Height: | Size: 7.6 MiB |
|
After Width: | Height: | Size: 7.5 MiB |
|
After Width: | Height: | Size: 6.8 MiB |
|
After Width: | Height: | Size: 6.2 MiB |
|
After Width: | Height: | Size: 6.2 MiB |
|
After Width: | Height: | Size: 7.5 MiB |
|
After Width: | Height: | Size: 5.6 MiB |
|
After Width: | Height: | Size: 6.1 MiB |
|
After Width: | Height: | Size: 6.9 MiB |
|
After Width: | Height: | Size: 6.9 MiB |
|
After Width: | Height: | Size: 6.2 MiB |
|
After Width: | Height: | Size: 6.0 MiB |
|
After Width: | Height: | Size: 6.2 MiB |
|
After Width: | Height: | Size: 7.1 MiB |
BIN
app/assets/images/impressions/agencement.jpg
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
app/assets/images/impressions/agencement2.jpg
Normal file
|
After Width: | Height: | Size: 2.1 MiB |
BIN
app/assets/images/impressions/agencement3.jpg
Normal file
|
After Width: | Height: | Size: 2.7 MiB |
BIN
app/assets/images/impressions/agencement4.jpg
Normal file
|
After Width: | Height: | Size: 697 KiB |
BIN
app/assets/images/impressions/casque2.jpg
Normal file
|
After Width: | Height: | Size: 761 KiB |
BIN
app/assets/images/impressions/casque3.jpg
Normal file
|
After Width: | Height: | Size: 720 KiB |
BIN
app/assets/images/impressions/casque4.jpg
Normal file
|
After Width: | Height: | Size: 371 KiB |
BIN
app/assets/images/impressions/casque5.jpg
Normal file
|
After Width: | Height: | Size: 385 KiB |
BIN
app/assets/images/impressions/cybersabre.jpg
Normal file
|
After Width: | Height: | Size: 324 KiB |
BIN
app/assets/images/impressions/gravure.jpg
Normal file
|
After Width: | Height: | Size: 998 KiB |
BIN
app/assets/images/impressions/gravurevegeta.jpg
Normal file
|
After Width: | Height: | Size: 212 KiB |
BIN
app/assets/images/impressions/hautbureau.jpg
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
app/assets/images/impressions/hautbureau2.jpg
Normal file
|
After Width: | Height: | Size: 2.2 MiB |
BIN
app/assets/images/impressions/hautbureau3.jpg
Normal file
|
After Width: | Height: | Size: 2.4 MiB |
BIN
app/assets/images/impressions/pc.jpg
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
BIN
app/assets/images/impressions/ring.png
Normal file
|
After Width: | Height: | Size: 2.0 MiB |
BIN
app/assets/images/impressions/sauron.jpg
Normal file
|
After Width: | Height: | Size: 2.5 MiB |
BIN
app/assets/images/impressions/supportcasque.jpg
Normal file
|
After Width: | Height: | Size: 1.7 MiB |
BIN
app/assets/images/impressions/vase.png
Normal file
|
After Width: | Height: | Size: 8.9 MiB |
BIN
app/assets/images/impressions/vase2.png
Normal file
|
After Width: | Height: | Size: 4.7 MiB |
|
After Width: | Height: | Size: 24 KiB |
BIN
app/assets/images/imprimante/lampdeath.jpg
Normal file
|
After Width: | Height: | Size: 1.8 MiB |
BIN
app/assets/images/imprimante/sidewinderx2.png
Normal file
|
After Width: | Height: | Size: 4.6 MiB |
BIN
app/assets/images/imprimante/x1c.jpg
Normal file
|
After Width: | Height: | Size: 2.2 MiB |
BIN
app/assets/images/imprimante/x1c2.jpg
Normal file
|
After Width: | Height: | Size: 2.3 MiB |
BIN
app/assets/images/imprimante/x1c3.jpg
Normal file
|
After Width: | Height: | Size: 2.3 MiB |
BIN
app/assets/images/imprimante/x1c4.jpg
Normal file
|
After Width: | Height: | Size: 2.5 MiB |
BIN
app/assets/images/imprimante/x1c5.jpg
Normal file
|
After Width: | Height: | Size: 2.4 MiB |
BIN
app/assets/images/imprimante/x1cseche.jpg
Normal file
|
After Width: | Height: | Size: 2.3 MiB |
BIN
app/assets/images/photo2.jpg
Normal file
|
After Width: | Height: | Size: 872 KiB |
BIN
app/assets/images/photo3.png
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
@ -5,6 +5,102 @@
|
||||
@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');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Raleway:wght@400;700&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Open+Sans:wght@400;700&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Lato:wght@400;700&display=swap');
|
||||
|
||||
.font-orbitron-24 {
|
||||
font-family: 'Orbitron', sans-serif;
|
||||
font-size: 24px;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.font-orbitron-16 {
|
||||
font-family: 'Orbitron', sans-serif;
|
||||
font-size: 16px;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.font-orbitron-12 {
|
||||
font-family: 'Orbitron', sans-serif;
|
||||
font-size: 12px;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
/* Classe pour la police Orbitron avec taille 24px en gras */
|
||||
.font-orbitron-24-bold {
|
||||
font-family: 'Orbitron', sans-serif;
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
/* Classe pour la police Orbitron avec taille 24px en italique */
|
||||
.font-orbitron-24-italic {
|
||||
font-family: 'Orbitron', sans-serif;
|
||||
font-size: 24px;
|
||||
font-style: italic;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
/* Classe pour la police Orbitron avec taille 24px en gras et italique */
|
||||
.font-orbitron-24-bold-italic {
|
||||
font-family: 'Orbitron', sans-serif;
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
font-style: italic;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
/* Classe pour la police Orbitron avec taille 16px en gras */
|
||||
.font-orbitron-16-bold {
|
||||
font-family: 'Orbitron', sans-serif;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
/* Classe pour la police Orbitron avec taille 16px en italique */
|
||||
.font-orbitron-16-italic {
|
||||
font-family: 'Orbitron', sans-serif;
|
||||
font-size: 16px;
|
||||
font-style: italic;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
/* Classe pour la police Orbitron avec taille 16px en gras et italique */
|
||||
.font-orbitron-16-bold-italic {
|
||||
font-family: 'Orbitron', sans-serif;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
font-style: italic;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
/* Classe pour la police Orbitron avec taille 12px en gras */
|
||||
.font-orbitron-12-bold {
|
||||
font-family: 'Orbitron', sans-serif;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
/* Classe pour la police Orbitron avec taille 12px en italique */
|
||||
.font-orbitron-12-italic {
|
||||
font-family: 'Orbitron', sans-serif;
|
||||
font-size: 12px;
|
||||
font-style: italic;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
/* Classe pour la police Orbitron avec taille 12px en gras et italique */
|
||||
.font-orbitron-12-bold-italic {
|
||||
font-family: 'Orbitron', sans-serif;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
font-style: italic;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
/* Importation des styles de base, des composants et des utilitaires de Tailwind CSS */
|
||||
@tailwind base;
|
||||
|
||||
@ -54,7 +54,7 @@ export default function Carousel({ images, className }: CarouselProps) {
|
||||
<div className="relative w-full max-w-6xl p-6 bg-transparent">
|
||||
{/* Bouton de fermeture */}
|
||||
<button
|
||||
className="absolute top-6 right-6 text-white text-3xl bg-gray-900/70 p-2 rounded-full"
|
||||
className="absolute top-6 right-6 text-white text-l bg-gray-900/70 p-2 rounded-full"
|
||||
onClick={() => setSelectedImage(null)} // Fermer au clic
|
||||
>
|
||||
✖
|
||||
|
||||
@ -54,7 +54,7 @@ export default function CarouselCompetences({ images, className }: CarouselProps
|
||||
<div className="relative w-full max-w-6xl p-6 bg-transparent">
|
||||
{/* Bouton de fermeture */}
|
||||
<button
|
||||
className="absolute top-6 right-6 text-white text-3xl bg-gray-900/70 p-2 rounded-full"
|
||||
className="absolute top-6 right-6 text-white text-l bg-gray-900/70 p-2 rounded-full"
|
||||
onClick={() => setSelectedImage(null)} // Fermer au clic
|
||||
>
|
||||
✖
|
||||
|
||||
@ -51,14 +51,14 @@ export default function ContactForm() {
|
||||
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-orbitron-16-bold mb-4 text-center">📩 Contactez-moi</h2>
|
||||
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Votre nom"
|
||||
value={name}
|
||||
onChange={(e) => 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"
|
||||
className="w-full p-3 border border-gray-300 font-orbitron-16-bold rounded mb-3 focus:outline-none focus:ring-2 focus:ring-blue-400"
|
||||
required
|
||||
/>
|
||||
|
||||
@ -67,7 +67,7 @@ export default function ContactForm() {
|
||||
placeholder="Votre email"
|
||||
value={email}
|
||||
onChange={(e) => 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"
|
||||
className="w-full p-3 border border-gray-300 rounded font-orbitron-16-bold mb-3 focus:outline-none focus:ring-2 focus:ring-blue-400"
|
||||
required
|
||||
/>
|
||||
|
||||
@ -75,7 +75,7 @@ export default function ContactForm() {
|
||||
placeholder="Votre message"
|
||||
value={message}
|
||||
onChange={(e) => setMessage(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"
|
||||
className="w-full p-3 border border-gray-300 rounded mb-3 font-orbitron-16-bold focus:outline-none focus:ring-2 focus:ring-blue-400"
|
||||
required
|
||||
/>
|
||||
|
||||
@ -83,7 +83,7 @@ export default function ContactForm() {
|
||||
type="submit"
|
||||
disabled={isLoading} // ✅ Désactive le bouton pendant l'envoi
|
||||
className={`w-full py-3 rounded transition ${
|
||||
isLoading ? "bg-gray-400 cursor-not-allowed" : "bg-blue-500 hover:bg-blue-600 text-white"
|
||||
isLoading ? "bg-gray-400 cursor-not-allowed" : "bg-blue-500 hover:bg-blue-600 text-white font-orbitron-16-bold"
|
||||
}`}
|
||||
>
|
||||
{isLoading ? "⏳ Envoi..." : "Envoyer"}
|
||||
|
||||
@ -66,13 +66,13 @@ export default function ContentSection({ collection, slug, titleClass, contentCl
|
||||
return (
|
||||
<div className="max-w-3xl mx-auto p-6">
|
||||
{/* Titre de la section */}
|
||||
<h1 className={titleClass || "text-3xl mb-6 font-bold text-gray-700"}>{name}</h1>
|
||||
<h1 className={titleClass || "bg-white/50 rounded-md text-3xl mb-6 font-orbitron-24-bold p-2 text-blue-700"}>{name}</h1>
|
||||
|
||||
{/* Carrousel réutilisable pour afficher les images */}
|
||||
<Carousel images={images} className="w-full h-64" />
|
||||
|
||||
{/* Contenu en Markdown */}
|
||||
<div className={contentClass || "bg-white/55 rounded-md p-4 shadow-md mt-6"}>
|
||||
<div className={contentClass || "bg-white/80 rounded-md p-4 font-orbitron-16-bold shadow-md mt-6"}>
|
||||
<ReactMarkdown>{richText}</ReactMarkdown>
|
||||
</div>
|
||||
|
||||
@ -83,7 +83,7 @@ export default function ContentSection({ collection, slug, titleClass, contentCl
|
||||
href={link}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-blue-500 hover:underline transition duration-300 ease-in-out transform hover:scale-105 hover:text-blue-700"
|
||||
className="bg-white/65 rounded-md p-1 text-red-700 hover:underline transition duration-300 ease-in-out transform hover:scale-105 font-orbitron-16-bold hover:text-blue-700"
|
||||
>
|
||||
{linkText || "Voir plus/lien externe"}
|
||||
</a>
|
||||
|
||||
@ -117,9 +117,9 @@ export default function ContentSectionCompetences({
|
||||
|
||||
return (
|
||||
<div className="max-w-3xl mx-auto p-6">
|
||||
<h1 className={titleClass || "text-3xl mb-6 font-bold text-gray-700"}>{name}</h1>
|
||||
<h1 className={titleClass || "bg-white/60 rounded-md p-1 text-2xl mb-6 font-orbitron-16-bold text-blue-700"}>{name}</h1>
|
||||
<CarouselCompetences images={images} className="w-full h-64" />
|
||||
<div className={contentClass || "mt-6 text-lg text-black-700"}>
|
||||
<div className={contentClass || "bg-white/70 rounded-md p-4 mt-6 text-lg font-orbitron-16-bold text-black-700"}>
|
||||
<ReactMarkdown rehypePlugins={[rehypeRaw]}>{contentWithLinks}</ReactMarkdown>
|
||||
</div>
|
||||
{selectedMot && <ModalGlossaire mot={selectedMot} onClose={() => setSelectedMot(null)} />}
|
||||
|
||||
@ -12,14 +12,9 @@ export default function Footer() {
|
||||
|
||||
return (
|
||||
<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">
|
||||
<div className="max-w-4xl mx-auto flex flex-col items-center font-orbitron-12 py-6 text-sm text-gray-700">
|
||||
{/* 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>
|
||||
<p>© {new Date().getFullYear()} Gras-Calvet Fernand</p>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
|
||||
@ -1,25 +1,47 @@
|
||||
import ContactForm from "../components/ContactForm"; // Importation du composant ContactForm
|
||||
// Importation de ContactForm et getApiUrl
|
||||
import ContactForm from "../components/ContactForm";
|
||||
import { getApiUrl } from "../utils/getApiUrl";
|
||||
|
||||
export default function ContactPage() {
|
||||
// Définition de l'URL API dynamique
|
||||
const apiUrl = getApiUrl();
|
||||
|
||||
return (
|
||||
<div className="max-w-3xl mx-auto p-6">
|
||||
{/* Titre de la page */}
|
||||
<h1 className="text-3xl font-bold text-center mb-6">Contactez-moi</h1>
|
||||
{/* Titre avec un cadre */}
|
||||
<h1 className="bg-white/50 rounded-md text-3xl font-orbitron-24-bold text-center mb-6 border-b-4 border-blue-500 pb-2">
|
||||
📬 Correspondance
|
||||
</h1>
|
||||
|
||||
{/* Texte d'introduction */}
|
||||
<p className="text-lg text-center mb-4">
|
||||
<p className="bg-white/70 rounded-md font-orbitron-16 text-lg text-center border-b-4 border-blue-500 pb-2 mb-4">
|
||||
Vous pouvez me contacter via ce formulaire ou sur mes réseaux sociaux.
|
||||
</p>
|
||||
|
||||
{/* Liens vers les réseaux sociaux */}
|
||||
<div className="flex justify-center space-x-4 mb-6">
|
||||
<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>
|
||||
<a href="mailto:votre@email.com" className="text-blue-500">Email</a>
|
||||
{/* Liens vers les réseaux sociaux mis à jour */}
|
||||
<div className="bg-white/80 rounded-mt flex justify-center space-x-4 mb-6">
|
||||
<a href="https://linkedin.com/in/votreprofil"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-blue-500 hover:text-blue-700 font-orbitron-16-bold transition">
|
||||
LinkedIn
|
||||
</a>
|
||||
<a href="https://www.facebook.com/ton.profil"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-blue-500 hover:text-blue-700 font-orbitron-16-bold transition">
|
||||
Facebook
|
||||
</a>
|
||||
<a href="mailto:grascalvet.fernand@gmail.com"
|
||||
className="text-blue-500 hover:text-blue-700 font-orbitron-16-bold transition">
|
||||
Email
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{/* Formulaire de contact */}
|
||||
<ContactForm />
|
||||
{/* Formulaire de contact amélioré */}
|
||||
<div className="bg-white/50 p-6 rounded-lg border-b-4 border-blue-500 pb-2 shadow">
|
||||
<ContactForm apiUrl={apiUrl} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -12,9 +12,9 @@ export default function RootLayout({ children }) {
|
||||
<div className="bg-wallpaper min-h-[100dvh] grid grid-rows-[auto_1fr_auto]">
|
||||
<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">
|
||||
<h2 className="text-2xl font-bold">Portfolio Gras-Calvet Fernand</h2>
|
||||
<h2 className="text-2xl font-orbitron-24-bold-italic">Portfolio Gras-Calvet Fernand</h2>
|
||||
<nav>
|
||||
<ul className="flex gap-x-7 text-black-500 font-bold">
|
||||
<ul className="flex gap-x-7 text-black-500 font-orbitron-16-bold">
|
||||
<li><NavLink text="Accueil" path="/" /></li>
|
||||
<li><NavLink text="Portfolio" path="/portfolio" /></li>
|
||||
<li><NavLink text="Compétences" path="/competences" /></li>
|
||||
|
||||
@ -36,7 +36,7 @@ export default function HomePage() {
|
||||
|
||||
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">
|
||||
<h1 className="text-3xl font-bold text-gray-800 mb-4">{title}</h1>
|
||||
<h1 className="text-3xl font-orbitron-24-bold-italic text-gray-800 mb-4">{title}</h1>
|
||||
|
||||
{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">
|
||||
@ -48,7 +48,7 @@ export default function HomePage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<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 font-orbitron-16-bold px-6 text-center">
|
||||
<ReactMarkdown>{cv}</ReactMarkdown>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
@ -29,8 +29,7 @@ export default function Page() {
|
||||
|
||||
return (
|
||||
<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">Portfolio formation 42</h1>
|
||||
|
||||
|
||||
{/* Grille des projets */}
|
||||
<div className="grid gap-7 grid-cols-[repeat(auto-fit,minmax(300px,1fr))] max-w-7xl mx-auto">
|
||||
@ -44,7 +43,7 @@ export default function Page() {
|
||||
return (
|
||||
<div
|
||||
key={project.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"
|
||||
className="bg-white/80 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 du projet */}
|
||||
<Link href={`/portfolio/${project.slug}`}>
|
||||
@ -62,8 +61,8 @@ export default function Page() {
|
||||
</div>
|
||||
|
||||
<div className="flex-grow overflow-y-auto max-h-32 hide-scrollbar show-scrollbar">
|
||||
<p className="font-bold text-xl mb-2">{project.name}</p>
|
||||
<p className="text-gray-700 text-sm hover:text-base transition-all duration-200 ease-in-out">
|
||||
<p className="font-orbitron-16-bold text-xl mb-2">{project.name}</p>
|
||||
<p className="text-gray-700 text-sm font-orbitron-12 hover:text-base transition-all duration-200 ease-in-out">
|
||||
{project.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@ -4,24 +4,34 @@ require("dotenv").config();
|
||||
|
||||
console.log("🔍 Vérification NEXT_PUBLIC_API_URL:", process.env.NEXT_PUBLIC_API_URL);
|
||||
|
||||
const API_URL = process.env.NEXT_PUBLIC_API_URL || "https://api.fernandgrascalvet.com"; // ✅ Valeur par défaut sécurisée
|
||||
|
||||
const nextConfig = {
|
||||
reactStrictMode: true,
|
||||
compress: false, // ❌ Désactive la compression Gzip pour éviter les erreurs IIS
|
||||
trailingSlash: true,
|
||||
compress: false, // ❌ Désactive Gzip pour éviter les erreurs IIS
|
||||
trailingSlash: false, // ❌ Peut causer des erreurs avec Next.js App Router, on le désactive
|
||||
|
||||
// Utilisation de l'API URL dynamique pour Strapi
|
||||
// ✅ Empêche WebSocket HMR en HTTPS et force le polling pour éviter les erreurs
|
||||
webpackDevMiddleware: (config: any) => {
|
||||
config.watchOptions = {
|
||||
poll: 1000, // Vérifie les changements toutes les 1 seconde
|
||||
aggregateTimeout: 300,
|
||||
};
|
||||
return config;
|
||||
},
|
||||
|
||||
// ✅ Rewrites pour Strapi (évite d'écrire l'URL complète dans chaque requête)
|
||||
async rewrites() {
|
||||
return [
|
||||
{
|
||||
source: "/api/:path*",
|
||||
destination: process.env.NEXT_PUBLIC_API_URL + "/api/:path*",
|
||||
destination: `${API_URL}/api/:path*`, // ✅ Utilisation sécurisée de la variable d'API
|
||||
},
|
||||
];
|
||||
},
|
||||
|
||||
images: {
|
||||
domains: ["localhost", "api.fernandgrascalvet.com"], // ✅ Autorise aussi l'API en HTTPS
|
||||
domains: ["localhost", "api.fernandgrascalvet.com"], // ✅ Autorise les images locales et distantes
|
||||
},
|
||||
};
|
||||
|
||||
|
||||