init commit
This commit is contained in:
parent
6c8f86e804
commit
12e020138c
26
app/components/Nav.jsx
Normal file
26
app/components/Nav.jsx
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
export default function Nav() {
|
||||||
|
return (
|
||||||
|
<div className='heading menu flex justify-between items-center px-8 py-8'>
|
||||||
|
<h1 className='text-3xl font-bold'>
|
||||||
|
<Link href='/'>Deligraph</Link>
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<ul className='flex gap-4 items-center'>
|
||||||
|
<li>
|
||||||
|
<Link href='/projects'>Portfolio</Link>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Link href='/projects-clients-side'>Projets Clients Side</Link>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Link href='/projects-server-side'>Projets Server Side</Link>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Link href='/contact'>Contact</Link>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
35
app/components/PortfolioGridServerSide.jsx
Normal file
35
app/components/PortfolioGridServerSide.jsx
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
export default async function PortfolioGridServerSide({ apiUrl = "https://deligraph.com/wp-json/wp/v2/portfolio" }) {
|
||||||
|
let posts = [];
|
||||||
|
let error = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
posts = await fetchPortfolioPosts();
|
||||||
|
} catch (err) {
|
||||||
|
error = err.message;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<div className='flex justify-center items-center py-12'>
|
||||||
|
<p className='text-red-500 text-lg'>Erreur: {error}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 px-8 py-8'>
|
||||||
|
{posts.map((post) => (
|
||||||
|
<article
|
||||||
|
key={post.id}
|
||||||
|
className='bg-white dark:bg-neutral-800 rounded-lg p-6 shadow-md hover:shadow-lg transition-shadow'>
|
||||||
|
<h3 className='text-xl font-bold mb-2 text-neutral-800 dark:text-white'>
|
||||||
|
{post.title.rendered}
|
||||||
|
</h3>
|
||||||
|
<p className='text-neutral-600 dark:text-neutral-300 line-clamp-3'>
|
||||||
|
{post.content.rendered}
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
66
app/components/PostGridClientSide.jsx
Normal file
66
app/components/PostGridClientSide.jsx
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
"use client";
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
|
||||||
|
export default function PostGridClientSide({ apiUrl = "https://deligraph.com/wp-json/wp/v2/portfolio" }) {
|
||||||
|
const [posts, setPosts] = useState([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchPosts = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
const response = await fetch(apiUrl);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Erreur HTTP: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
setPosts(data);
|
||||||
|
} catch (err) {
|
||||||
|
setError(err.message);
|
||||||
|
console.error("Erreur lors du fetch:", err);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchPosts();
|
||||||
|
}, [apiUrl]);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className='flex justify-center items-center py-12'>
|
||||||
|
<p className='text-lg'>Chargement des posts...</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<div className='flex justify-center items-center py-12'>
|
||||||
|
<p className='text-red-500 text-lg'>Erreur: {error}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
console.log(posts);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 px-8 py-8'>
|
||||||
|
{posts.map((post) => (
|
||||||
|
<article
|
||||||
|
key={post.id}
|
||||||
|
className='bg-white dark:bg-neutral-800 rounded-lg p-6 shadow-md hover:shadow-lg transition-shadow'>
|
||||||
|
<h3 className='text-xl font-bold mb-2 text-neutral-800 dark:text-white'>
|
||||||
|
{post.title.rendered}
|
||||||
|
</h3>
|
||||||
|
<p className='text-neutral-600 dark:text-neutral-300 line-clamp-3'>
|
||||||
|
{post.content.rendered}
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
66
app/components/PostGridServerSide.jsx
Normal file
66
app/components/PostGridServerSide.jsx
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
import Link from "next/link";
|
||||||
|
import Image from "next/image";
|
||||||
|
import { fetchPortfolioPosts } from "../utils/useWordpress";
|
||||||
|
|
||||||
|
export default async function PortfolioGridServerSide() {
|
||||||
|
let posts = [];
|
||||||
|
let error = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// fetchPortfolioPosts récupère automatiquement les images de couverture
|
||||||
|
const result = await fetchPortfolioPosts({
|
||||||
|
perPage: 10,
|
||||||
|
fetchOptions: {
|
||||||
|
next: { revalidate: 3600 }, // Revalide toutes les heures
|
||||||
|
},
|
||||||
|
});
|
||||||
|
posts = result.posts;
|
||||||
|
} catch (err) {
|
||||||
|
error = err.message;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<div className='flex justify-center items-center py-12'>
|
||||||
|
<p className='text-red-500 text-lg'>Erreur: {error}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 px-8 py-8'>
|
||||||
|
{posts.map((post) => {
|
||||||
|
return (
|
||||||
|
<Link href={`/projects/${post.slug}`} key={post.id}>
|
||||||
|
<article className='bg-white dark:bg-neutral-800 rounded-lg overflow-hidden shadow-md hover:shadow-lg transition-shadow'>
|
||||||
|
{post.featuredImageUrl && (
|
||||||
|
<div className='relative w-full h-48 overflow-hidden'>
|
||||||
|
<Image
|
||||||
|
src={post.featuredImageUrl}
|
||||||
|
alt={post.title.rendered || "Image du projet"}
|
||||||
|
fill
|
||||||
|
className='object-cover'
|
||||||
|
sizes='(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className='p-6'>
|
||||||
|
<h3 className='text-xl font-bold mb-2 text-neutral-800 dark:text-white'>
|
||||||
|
{post.title.rendered}
|
||||||
|
</h3>
|
||||||
|
<div
|
||||||
|
className='text-neutral-600 dark:text-neutral-300 line-clamp-3'
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html:
|
||||||
|
post.excerpt?.rendered ||
|
||||||
|
post.content.rendered?.substring(0, 150) + "...",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
30
app/contact/page.jsx
Normal file
30
app/contact/page.jsx
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
export default function Home() {
|
||||||
|
return (
|
||||||
|
<main>
|
||||||
|
<div className='heading menu flex justify-between items-center px-8 py-8'>
|
||||||
|
<h1 className='text-3xl font-bold'>
|
||||||
|
{" "}
|
||||||
|
<Link href='/'>Deligraph</Link>
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<ul className='flex gap-4 items-center'>
|
||||||
|
<li>
|
||||||
|
<Link href='/projects-clients-side'>Projets Clients Side</Link>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Link href='/projects-server-side'>Projets Server Side</Link>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Link href='/contact'>Contact</Link>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<h2>prooooojets</h2>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,26 +1,42 @@
|
||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--background: #ffffff;
|
--background: #ffffff;
|
||||||
--foreground: #171717;
|
--foreground: #171717;
|
||||||
}
|
}
|
||||||
|
|
||||||
@theme inline {
|
@theme inline {
|
||||||
--color-background: var(--background);
|
--color-background: var(--background);
|
||||||
--color-foreground: var(--foreground);
|
--color-foreground: var(--foreground);
|
||||||
--font-sans: var(--font-geist-sans);
|
--font-sans: var(--font-geist-sans);
|
||||||
--font-mono: var(--font-geist-mono);
|
--font-mono: var(--font-geist-mono);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
:root {
|
:root {
|
||||||
--background: #0a0a0a;
|
--background: #0a0a0a;
|
||||||
--foreground: #ededed;
|
--foreground: #ededed;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
background: var(--background);
|
background: var(--background);
|
||||||
color: var(--foreground);
|
color: var(--foreground);
|
||||||
font-family: Arial, Helvetica, sans-serif;
|
font-family: Arial, Helvetica, sans-serif;
|
||||||
|
|
||||||
|
&:has(main[data-theme="dark"]) {
|
||||||
|
background: var(--background);
|
||||||
|
color: var(--foreground);
|
||||||
|
font-family: Arial, Helvetica, sans-serif;
|
||||||
|
}
|
||||||
|
&:has(main[data-theme="light"]) {
|
||||||
|
--background: white;
|
||||||
|
--foreground: #000;
|
||||||
|
|
||||||
|
background: var(--background);
|
||||||
|
color: var(--foreground);
|
||||||
|
font-family: Arial, Helvetica, sans-serif;
|
||||||
|
}
|
||||||
|
background: white;
|
||||||
|
color: #000;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
112
app/page.tsx
112
app/page.tsx
|
|
@ -1,65 +1,55 @@
|
||||||
|
"use client";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
return (
|
const [theme, setTheme] = useState<"light" | "dark">("light");
|
||||||
<div className="flex min-h-screen items-center justify-center bg-zinc-50 font-sans dark:bg-black">
|
|
||||||
<main className="flex min-h-screen w-full max-w-3xl flex-col items-center justify-between py-32 px-16 bg-white dark:bg-black sm:items-start">
|
useEffect(() => {
|
||||||
<Image
|
// Vérifier les préférences système uniquement côté client
|
||||||
className="dark:invert"
|
const isDarkMode = window.matchMedia("(prefers-color-scheme: dark)").matches;
|
||||||
src="/next.svg"
|
const initialTheme = isDarkMode ? "dark" : "light";
|
||||||
alt="Next.js logo"
|
setTheme(initialTheme);
|
||||||
width={100}
|
document.documentElement.classList.toggle("dark", initialTheme === "dark");
|
||||||
height={20}
|
}, []);
|
||||||
priority
|
|
||||||
/>
|
const toggleTheme = () => {
|
||||||
<div className="flex flex-col items-center gap-6 text-center sm:items-start sm:text-left">
|
const newTheme = theme === "light" ? "dark" : "light";
|
||||||
<h1 className="max-w-xs text-3xl font-semibold leading-10 tracking-tight text-black dark:text-zinc-50">
|
setTheme(newTheme);
|
||||||
To get started, edit the page.tsx file.
|
document.documentElement.classList.toggle("dark", newTheme === "dark");
|
||||||
</h1>
|
};
|
||||||
<p className="max-w-md text-lg leading-8 text-zinc-600 dark:text-zinc-400">
|
return (
|
||||||
Looking for a starting point or more instructions? Head over to{" "}
|
<main data-theme={theme === "dark" ? "dark" : "light"}>
|
||||||
<a
|
<div className='heading menu flex justify-between items-center px-8 py-8'>
|
||||||
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
<h1 className='text-3xl font-bold '>
|
||||||
className="font-medium text-zinc-950 dark:text-zinc-50"
|
<Link href='/'>Deligraph</Link>
|
||||||
>
|
</h1>
|
||||||
Templates
|
|
||||||
</a>{" "}
|
<ul className='flex gap-4 items-center'>
|
||||||
or the{" "}
|
<li>
|
||||||
<a
|
<Link href='/projects-clients-side'>Projets Clients Side</Link>
|
||||||
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
</li>
|
||||||
className="font-medium text-zinc-950 dark:text-zinc-50"
|
<li>
|
||||||
>
|
<Link href='/projects-server-side'>Projets Server Side</Link>
|
||||||
Learning
|
</li>
|
||||||
</a>{" "}
|
<li>
|
||||||
center.
|
<Link href='/contact'>Contact</Link>
|
||||||
</p>
|
</li>
|
||||||
</div>
|
<button
|
||||||
<div className="flex flex-col gap-4 text-base font-medium sm:flex-row">
|
className={`px-4 py-2 rounded-2xl flex items-center gap-2 ${theme === "light" ? "bg-neutral-800 text-white" : "bg-white text-neutral-800"} cursor-pointer`}
|
||||||
<a
|
onClick={toggleTheme}>
|
||||||
className="flex h-12 w-full items-center justify-center gap-2 rounded-full bg-foreground px-5 text-background transition-colors hover:bg-[#383838] dark:hover:bg-[#ccc] md:w-[158px]"
|
<Image
|
||||||
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
src={"/icon-switch.svg"}
|
||||||
target="_blank"
|
className={theme === "light" ? "invert" : ""}
|
||||||
rel="noopener noreferrer"
|
alt='sun'
|
||||||
>
|
width={20}
|
||||||
<Image
|
height={20}
|
||||||
className="dark:invert"
|
/>
|
||||||
src="/vercel.svg"
|
Switch to {theme === "light" ? "dark" : "light"}
|
||||||
alt="Vercel logomark"
|
</button>
|
||||||
width={16}
|
</ul>
|
||||||
height={16}
|
</div>
|
||||||
/>
|
</main>
|
||||||
Deploy Now
|
);
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
className="flex h-12 w-full items-center justify-center rounded-full border border-solid border-black/[.08] px-5 transition-colors hover:border-transparent hover:bg-black/[.04] dark:border-white/[.145] dark:hover:bg-[#1a1a1a] md:w-[158px]"
|
|
||||||
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
Documentation
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
13
app/projects-clients-side/page.jsx
Normal file
13
app/projects-clients-side/page.jsx
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
import Link from "next/link";
|
||||||
|
import PostGridClientSide from "../components/PostGridClientSide";
|
||||||
|
import Nav from "../components/Nav";
|
||||||
|
|
||||||
|
export default function Projects() {
|
||||||
|
return (
|
||||||
|
<main>
|
||||||
|
<Nav />
|
||||||
|
<h2 className='px-8 text-2xl font-bold mb-4'>Projets</h2>
|
||||||
|
<PostGridClientSide />
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
13
app/projects-server-side/page.jsx
Normal file
13
app/projects-server-side/page.jsx
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
import Link from "next/link";
|
||||||
|
import PostGridServerSide from "../components/PostGridServerSide";
|
||||||
|
import Nav from "../components/Nav";
|
||||||
|
|
||||||
|
export default function Projects() {
|
||||||
|
return (
|
||||||
|
<main>
|
||||||
|
<Nav />
|
||||||
|
<h2 className='px-8 text-2xl font-bold mb-4'>Projets</h2>
|
||||||
|
<PostGridServerSide />
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
74
app/projects/[slug]/page.jsx
Normal file
74
app/projects/[slug]/page.jsx
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
import { notFound } from "next/navigation";
|
||||||
|
import Image from "next/image";
|
||||||
|
import Link from "next/link";
|
||||||
|
import Nav from "../../components/Nav";
|
||||||
|
import { fetchPortfolioPostBySlug, fetchMedia } from "../../utils/useWordpress";
|
||||||
|
|
||||||
|
export default async function ProjectDetail({ params }) {
|
||||||
|
const { slug } = params;
|
||||||
|
|
||||||
|
if (!slug) {
|
||||||
|
notFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
let post = null;
|
||||||
|
let featuredImageUrl = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Récupérer le post par son slug
|
||||||
|
post = await fetchPortfolioPostBySlug(slug, {
|
||||||
|
next: { revalidate: 3600 }, // Revalide toutes les heures
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!post) {
|
||||||
|
notFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Récupérer l'image featured si elle existe
|
||||||
|
if (post.featured_media) {
|
||||||
|
featuredImageUrl = await fetchMedia(post.featured_media, {
|
||||||
|
next: { revalidate: 3600 },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erreur lors de la récupération du projet:", error);
|
||||||
|
notFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main>
|
||||||
|
<Nav />
|
||||||
|
<article className='max-w-4xl mx-auto px-8 py-8'>
|
||||||
|
<Link
|
||||||
|
href='/projects'
|
||||||
|
className='inline-block mb-6 text-blue-600 dark:text-blue-400 hover:underline'>
|
||||||
|
← Retour aux projets
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
{featuredImageUrl && (
|
||||||
|
<div className='relative w-full h-96 mb-8 rounded-lg overflow-hidden'>
|
||||||
|
<Image
|
||||||
|
src={featuredImageUrl}
|
||||||
|
alt={post.title?.rendered || "Image du projet"}
|
||||||
|
fill
|
||||||
|
className='object-cover'
|
||||||
|
priority
|
||||||
|
sizes='(max-width: 768px) 100vw, 896px'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<h1 className='text-4xl font-bold mb-6 text-neutral-800 dark:text-white'>
|
||||||
|
{post.title?.rendered || "Sans titre"}
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className='prose prose-lg dark:prose-invert max-w-none text-neutral-700 dark:text-neutral-300'
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: post.content?.rendered || "",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</article>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
21
app/projects/not-found.jsx
Normal file
21
app/projects/not-found.jsx
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
export default function NotFound() {
|
||||||
|
return (
|
||||||
|
<main className='px-8 py-8'>
|
||||||
|
<div className='max-w-4xl mx-auto text-center'>
|
||||||
|
<h1 className='text-4xl font-bold mb-4 text-neutral-800 dark:text-white'>
|
||||||
|
404 - Projet non trouvé
|
||||||
|
</h1>
|
||||||
|
<p className='text-neutral-600 dark:text-neutral-300 mb-6'>
|
||||||
|
Le projet que vous recherchez n'existe pas ou a été supprimé.
|
||||||
|
</p>
|
||||||
|
<Link
|
||||||
|
href='/projects'
|
||||||
|
className='inline-block px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors'>
|
||||||
|
Retour aux projets
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
12
app/projects/page.jsx
Normal file
12
app/projects/page.jsx
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
import PortfolioGridServerSide from "../components/PostGridServerSide";
|
||||||
|
import Nav from "../components/Nav";
|
||||||
|
|
||||||
|
export default function Projects() {
|
||||||
|
return (
|
||||||
|
<main>
|
||||||
|
<Nav />
|
||||||
|
<h2 className='px-8 text-2xl font-bold mb-4'>Portfolio</h2>
|
||||||
|
<PortfolioGridServerSide />
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
281
app/utils/useWordpress.js
Normal file
281
app/utils/useWordpress.js
Normal file
|
|
@ -0,0 +1,281 @@
|
||||||
|
/**
|
||||||
|
* Fonctions utilitaires pour récupérer des données depuis WordPress REST API
|
||||||
|
* Compatible avec Next.js (client et serveur)
|
||||||
|
*/
|
||||||
|
|
||||||
|
const WORDPRESS_API_BASE = "https://deligraph.com/wp-json/wp/v2";
|
||||||
|
const PORTFOLIO_ENDPOINT = `${WORDPRESS_API_BASE}/portfolio`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construit une URL avec des paramètres de requête
|
||||||
|
* @param {string} baseUrl - URL de base
|
||||||
|
* @param {Object} params - Paramètres de requête
|
||||||
|
* @returns {string} URL complète avec paramètres
|
||||||
|
*/
|
||||||
|
function buildUrl(baseUrl, params = {}) {
|
||||||
|
const url = new URL(baseUrl);
|
||||||
|
Object.keys(params).forEach((key) => {
|
||||||
|
if (params[key] !== undefined && params[key] !== null) {
|
||||||
|
url.searchParams.append(key, params[key]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return url.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère les informations d'un média WordPress par son ID
|
||||||
|
* @param {number} mediaId - ID du média WordPress
|
||||||
|
* @param {Object} fetchOptions - Options supplémentaires pour fetch
|
||||||
|
* @returns {Promise<string|null>} URL de l'image ou null si non trouvé
|
||||||
|
*/
|
||||||
|
export async function fetchMedia(mediaId, fetchOptions = {}) {
|
||||||
|
if (!mediaId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = `${WORDPRESS_API_BASE}/media/${mediaId}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(url, {
|
||||||
|
...fetchOptions,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
if (response.status === 404) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
throw new Error(`Erreur HTTP: ${response.status} - ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
// Retourner l'URL source de l'image
|
||||||
|
return data.source_url || null;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Erreur lors de la récupération du média ID "${mediaId}":`, error);
|
||||||
|
return null; // Retourner null au lieu de throw pour éviter de casser le rendu
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère les posts de type portfolio depuis WordPress
|
||||||
|
* @param {Object} options - Options de récupération
|
||||||
|
* @param {number} options.perPage - Nombre de posts par page (défaut: 10)
|
||||||
|
* @param {number} options.page - Numéro de page (défaut: 1)
|
||||||
|
* @param {string} options.search - Terme de recherche
|
||||||
|
* @param {number} options.categories - ID de catégorie
|
||||||
|
* @param {string} options.order - Ordre de tri (asc/desc, défaut: desc)
|
||||||
|
* @param {string} options.orderby - Champ de tri (date, title, etc., défaut: date)
|
||||||
|
* @param {Object} options.fetchOptions - Options supplémentaires pour fetch (utile pour revalidate côté serveur)
|
||||||
|
* @returns {Promise<Array>} Tableau de posts portfolio
|
||||||
|
*/
|
||||||
|
export async function fetchPortfolioPosts(options = {}) {
|
||||||
|
const {
|
||||||
|
perPage = 10,
|
||||||
|
page = 1,
|
||||||
|
search,
|
||||||
|
categories,
|
||||||
|
order = "desc",
|
||||||
|
orderby = "date",
|
||||||
|
fetchOptions = {},
|
||||||
|
} = options;
|
||||||
|
|
||||||
|
const params = {
|
||||||
|
per_page: perPage,
|
||||||
|
page: page,
|
||||||
|
order: order,
|
||||||
|
orderby: orderby,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (search) {
|
||||||
|
params.search = search;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (categories) {
|
||||||
|
params.categories = categories;
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = buildUrl(PORTFOLIO_ENDPOINT, params);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(url, {
|
||||||
|
...fetchOptions,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Erreur HTTP: ${response.status} - ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
// Récupérer les images de couverture pour chaque post en parallèle
|
||||||
|
const postsWithImages = await Promise.all(
|
||||||
|
data.map(async (post) => {
|
||||||
|
if (post.featured_media) {
|
||||||
|
try {
|
||||||
|
const mediaUrl = await fetchMedia(post.featured_media, fetchOptions);
|
||||||
|
return { ...post, featuredImageUrl: mediaUrl };
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Erreur lors de la récupération de l'image pour le post ${post.id}:`, error);
|
||||||
|
return { ...post, featuredImageUrl: null };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { ...post, featuredImageUrl: null };
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
posts: postsWithImages,
|
||||||
|
totalPages: parseInt(response.headers.get("X-WP-TotalPages") || "1", 10),
|
||||||
|
total: parseInt(response.headers.get("X-WP-Total") || "0", 10),
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erreur lors de la récupération des posts portfolio:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère un post portfolio spécifique par son slug
|
||||||
|
* @param {string} slug - Slug du post
|
||||||
|
* @param {Object} fetchOptions - Options supplémentaires pour fetch
|
||||||
|
* @returns {Promise<Object|null>} Post portfolio ou null si non trouvé
|
||||||
|
*/
|
||||||
|
export async function fetchPortfolioPostBySlug(slug, fetchOptions = {}) {
|
||||||
|
if (!slug) {
|
||||||
|
throw new Error("Le slug est requis");
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = buildUrl(PORTFOLIO_ENDPOINT, { slug });
|
||||||
|
console.log("URL de recherche:", url);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(url, {
|
||||||
|
...fetchOptions,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("Statut de la réponse:", response.status, response.statusText);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
if (response.status === 404) {
|
||||||
|
console.log("Aucun post trouvé avec le paramètre slug, tentative avec fallback...");
|
||||||
|
// Fallback: récupérer tous les posts et filtrer par slug
|
||||||
|
return await fetchPortfolioPostBySlugFallback(slug, fetchOptions);
|
||||||
|
}
|
||||||
|
throw new Error(`Erreur HTTP: ${response.status} - ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
console.log(
|
||||||
|
"Données reçues:",
|
||||||
|
Array.isArray(data) ? `${data.length} résultat(s)` : "Format inattendu",
|
||||||
|
data,
|
||||||
|
);
|
||||||
|
|
||||||
|
// L'API WordPress retourne un tableau même pour un seul résultat
|
||||||
|
if (Array.isArray(data)) {
|
||||||
|
if (data.length > 0) {
|
||||||
|
return data[0];
|
||||||
|
}
|
||||||
|
// Si le tableau est vide, essayer le fallback
|
||||||
|
console.log("Tableau vide, tentative avec fallback...");
|
||||||
|
return await fetchPortfolioPostBySlugFallback(slug, fetchOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Si ce n'est pas un tableau, retourner directement (cas où l'API retourne un objet unique)
|
||||||
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Erreur lors de la récupération du post portfolio "${slug}":`, error);
|
||||||
|
// En cas d'erreur, essayer le fallback
|
||||||
|
try {
|
||||||
|
return await fetchPortfolioPostBySlugFallback(slug, fetchOptions);
|
||||||
|
} catch {
|
||||||
|
throw error; // Relancer l'erreur originale
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fallback: récupère tous les posts et filtre par slug
|
||||||
|
* @param {string} slug - Slug du post
|
||||||
|
* @param {Object} fetchOptions - Options supplémentaires pour fetch
|
||||||
|
* @returns {Promise<Object|null>} Post portfolio ou null si non trouvé
|
||||||
|
*/
|
||||||
|
async function fetchPortfolioPostBySlugFallback(slug, fetchOptions = {}) {
|
||||||
|
console.log("Utilisation du fallback pour le slug:", slug);
|
||||||
|
try {
|
||||||
|
const allPosts = await fetchAllPortfolioPosts({ fetchOptions });
|
||||||
|
const post = allPosts.find((p) => p.slug === slug);
|
||||||
|
|
||||||
|
if (post) {
|
||||||
|
console.log("Post trouvé via fallback");
|
||||||
|
return post;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Aucun post trouvé avec le slug:", slug);
|
||||||
|
return null;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erreur dans le fallback:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère un post portfolio par son ID
|
||||||
|
* @param {number} id - ID du post
|
||||||
|
* @param {Object} fetchOptions - Options supplémentaires pour fetch
|
||||||
|
* @returns {Promise<Object|null>} Post portfolio ou null si non trouvé
|
||||||
|
*/
|
||||||
|
export async function fetchPortfolioPostById(id, fetchOptions = {}) {
|
||||||
|
if (!id) {
|
||||||
|
throw new Error("L'ID est requis");
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = `${PORTFOLIO_ENDPOINT}/${id}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(url, {
|
||||||
|
...fetchOptions,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
if (response.status === 404) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
throw new Error(`Erreur HTTP: ${response.status} - ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await response.json();
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Erreur lors de la récupération du post portfolio ID "${id}":`, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère tous les posts portfolio (sans pagination)
|
||||||
|
* @param {Object} options - Options de récupération (mêmes que fetchPortfolioPosts)
|
||||||
|
* @returns {Promise<Array>} Tableau de tous les posts portfolio
|
||||||
|
*/
|
||||||
|
export async function fetchAllPortfolioPosts(options = {}) {
|
||||||
|
const allPosts = [];
|
||||||
|
let page = 1;
|
||||||
|
let hasMore = true;
|
||||||
|
|
||||||
|
while (hasMore) {
|
||||||
|
const result = await fetchPortfolioPosts({
|
||||||
|
...options,
|
||||||
|
page,
|
||||||
|
perPage: 100, // Maximum recommandé par WordPress REST API
|
||||||
|
});
|
||||||
|
|
||||||
|
allPosts.push(...result.posts);
|
||||||
|
|
||||||
|
if (page >= result.totalPages) {
|
||||||
|
hasMore = false;
|
||||||
|
} else {
|
||||||
|
page++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return allPosts;
|
||||||
|
}
|
||||||
0
eslint.config.mjs
Normal file → Executable file
0
eslint.config.mjs
Normal file → Executable file
20
next.config.ts
Normal file → Executable file
20
next.config.ts
Normal file → Executable file
|
|
@ -1,7 +1,25 @@
|
||||||
import type { NextConfig } from "next";
|
import type { NextConfig } from "next";
|
||||||
|
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
/* config options here */
|
// Configuration pour l'export statique
|
||||||
|
output: "export",
|
||||||
|
|
||||||
|
images: {
|
||||||
|
|
||||||
|
remotePatterns: [
|
||||||
|
{
|
||||||
|
protocol: "https",
|
||||||
|
hostname: "deligraph.com",
|
||||||
|
pathname: "/wp-content/**",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
// Pour l'export statique, désactiver l'optimisation d'images Next.js
|
||||||
|
// ou utiliser unloader externe
|
||||||
|
unoptimized: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Désactiver les fonctionnalités qui ne fonctionnent pas en mode statique
|
||||||
|
trailingSlash: true, // Optionnel : ajoute un slash à la fin des URLs
|
||||||
};
|
};
|
||||||
|
|
||||||
export default nextConfig;
|
export default nextConfig;
|
||||||
|
|
|
||||||
2
package.json
Normal file → Executable file
2
package.json
Normal file → Executable file
|
|
@ -24,4 +24,4 @@
|
||||||
"typescript": "^5"
|
"typescript": "^5"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@10.22.0+sha512.bf049efe995b28f527fd2b41ae0474ce29186f7edcb3bf545087bd61fbbebb2bf75362d1307fda09c2d288e1e499787ac12d4fcb617a974718a6051f2eee741c"
|
"packageManager": "pnpm@10.22.0+sha512.bf049efe995b28f527fd2b41ae0474ce29186f7edcb3bf545087bd61fbbebb2bf75362d1307fda09c2d288e1e499787ac12d4fcb617a974718a6051f2eee741c"
|
||||||
}
|
}
|
||||||
0
pnpm-lock.yaml
Normal file → Executable file
0
pnpm-lock.yaml
Normal file → Executable file
0
pnpm-workspace.yaml
Normal file → Executable file
0
pnpm-workspace.yaml
Normal file → Executable file
0
postcss.config.mjs
Normal file → Executable file
0
postcss.config.mjs
Normal file → Executable file
4
public/icon-switch.svg
Normal file
4
public/icon-switch.svg
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg id="Calque_1" data-name="Calque 1" xmlns="http://www.w3.org/2000/svg" width="87.5" height="87.5" viewBox="0 0 87.5 87.5">
|
||||||
|
<path d="M43.8,0C19.6,0,0,19.6,0,43.8s19.6,43.8,43.8,43.8,43.8-19.6,43.8-43.8S67.9,0,43.8,0ZM43.8,81.2c-20.7,0-37.5-16.8-37.5-37.5S23.1,6.2,43.8,6.2s37.5,16.8,37.5,37.5-16.8,37.5-37.5,37.5ZM78.1,43.8c0,9.2-3.6,17.8-10.1,24.3-5.6,5.6-13,9.1-20.9,9.9l-3.4.3V9.2l3.4.3c7.8.8,15.3,4.3,20.9,9.9,6.5,6.5,10.1,15.1,10.1,24.3h0Z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 498 B |
0
tsconfig.json
Normal file → Executable file
0
tsconfig.json
Normal file → Executable file
Loading…
Reference in New Issue
Block a user