From 12e020138c90fd6a0e057f78224858b138c826e0 Mon Sep 17 00:00:00 2001 From: Nonimart Date: Mon, 9 Feb 2026 16:37:57 +0100 Subject: [PATCH] init commit --- README.md | 0 app/components/Nav.jsx | 26 ++ app/components/PortfolioGridServerSide.jsx | 35 +++ app/components/PostGridClientSide.jsx | 66 +++++ app/components/PostGridServerSide.jsx | 66 +++++ app/contact/page.jsx | 30 +++ app/globals.css | 42 ++- app/page.tsx | 112 ++++---- app/projects-clients-side/page.jsx | 13 + app/projects-server-side/page.jsx | 13 + app/projects/[slug]/page.jsx | 74 ++++++ app/projects/not-found.jsx | 21 ++ app/projects/page.jsx | 12 + app/utils/useWordpress.js | 281 +++++++++++++++++++++ eslint.config.mjs | 0 next.config.ts | 20 +- package.json | 2 +- pnpm-lock.yaml | 0 pnpm-workspace.yaml | 0 postcss.config.mjs | 0 public/icon-switch.svg | 4 + tsconfig.json | 0 22 files changed, 741 insertions(+), 76 deletions(-) mode change 100644 => 100755 README.md create mode 100644 app/components/Nav.jsx create mode 100644 app/components/PortfolioGridServerSide.jsx create mode 100644 app/components/PostGridClientSide.jsx create mode 100644 app/components/PostGridServerSide.jsx create mode 100644 app/contact/page.jsx create mode 100644 app/projects-clients-side/page.jsx create mode 100644 app/projects-server-side/page.jsx create mode 100644 app/projects/[slug]/page.jsx create mode 100644 app/projects/not-found.jsx create mode 100644 app/projects/page.jsx create mode 100644 app/utils/useWordpress.js mode change 100644 => 100755 eslint.config.mjs mode change 100644 => 100755 next.config.ts mode change 100644 => 100755 package.json mode change 100644 => 100755 pnpm-lock.yaml mode change 100644 => 100755 pnpm-workspace.yaml mode change 100644 => 100755 postcss.config.mjs create mode 100644 public/icon-switch.svg mode change 100644 => 100755 tsconfig.json diff --git a/README.md b/README.md old mode 100644 new mode 100755 diff --git a/app/components/Nav.jsx b/app/components/Nav.jsx new file mode 100644 index 0000000..f33aa04 --- /dev/null +++ b/app/components/Nav.jsx @@ -0,0 +1,26 @@ +import Link from "next/link"; + +export default function Nav() { + return ( +
+

+ Deligraph +

+ + +
+ ); +} diff --git a/app/components/PortfolioGridServerSide.jsx b/app/components/PortfolioGridServerSide.jsx new file mode 100644 index 0000000..71f79e9 --- /dev/null +++ b/app/components/PortfolioGridServerSide.jsx @@ -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 ( +
+

Erreur: {error}

+
+ ); + } + + return ( +
+ {posts.map((post) => ( +
+

+ {post.title.rendered} +

+

+ {post.content.rendered} +

+
+ ))} +
+ ); +} diff --git a/app/components/PostGridClientSide.jsx b/app/components/PostGridClientSide.jsx new file mode 100644 index 0000000..f63c7e8 --- /dev/null +++ b/app/components/PostGridClientSide.jsx @@ -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 ( +
+

Chargement des posts...

+
+ ); + } + + if (error) { + return ( +
+

Erreur: {error}

+
+ ); + } + console.log(posts); + + return ( +
+ {posts.map((post) => ( +
+

+ {post.title.rendered} +

+

+ {post.content.rendered} +

+
+ ))} +
+ ); +} diff --git a/app/components/PostGridServerSide.jsx b/app/components/PostGridServerSide.jsx new file mode 100644 index 0000000..6560daf --- /dev/null +++ b/app/components/PostGridServerSide.jsx @@ -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 ( +
+

Erreur: {error}

+
+ ); + } + + return ( +
+ {posts.map((post) => { + return ( + +
+ {post.featuredImageUrl && ( +
+ {post.title.rendered +
+ )} +
+

+ {post.title.rendered} +

+
+
+
+ + ); + })} +
+ ); +} diff --git a/app/contact/page.jsx b/app/contact/page.jsx new file mode 100644 index 0000000..295087c --- /dev/null +++ b/app/contact/page.jsx @@ -0,0 +1,30 @@ +"use client"; + +import Link from "next/link"; +import { useState } from "react"; + +export default function Home() { + return ( +
+
+

+ {" "} + Deligraph +

+ +
    +
  • + Projets Clients Side +
  • +
  • + Projets Server Side +
  • +
  • + Contact +
  • +
+
+

prooooojets

+
+ ); +} diff --git a/app/globals.css b/app/globals.css index a2dc41e..3e3ad80 100644 --- a/app/globals.css +++ b/app/globals.css @@ -1,26 +1,42 @@ @import "tailwindcss"; :root { - --background: #ffffff; - --foreground: #171717; + --background: #ffffff; + --foreground: #171717; } @theme inline { - --color-background: var(--background); - --color-foreground: var(--foreground); - --font-sans: var(--font-geist-sans); - --font-mono: var(--font-geist-mono); + --color-background: var(--background); + --color-foreground: var(--foreground); + --font-sans: var(--font-geist-sans); + --font-mono: var(--font-geist-mono); } @media (prefers-color-scheme: dark) { - :root { - --background: #0a0a0a; - --foreground: #ededed; - } + :root { + --background: #0a0a0a; + --foreground: #ededed; + } } body { - background: var(--background); - color: var(--foreground); - font-family: Arial, Helvetica, sans-serif; + background: var(--background); + color: var(--foreground); + 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; } diff --git a/app/page.tsx b/app/page.tsx index 295f8fd..df8a0e5 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,65 +1,55 @@ +"use client"; +import Link from "next/link"; +import { useState, useEffect } from "react"; import Image from "next/image"; export default function Home() { - return ( -
-
- Next.js logo -
-

- To get started, edit the page.tsx file. -

-

- Looking for a starting point or more instructions? Head over to{" "} - - Templates - {" "} - or the{" "} - - Learning - {" "} - center. -

-
-
- - Vercel logomark - Deploy Now - - - Documentation - -
-
-
- ); + const [theme, setTheme] = useState<"light" | "dark">("light"); + + useEffect(() => { + // Vérifier les préférences système uniquement côté client + const isDarkMode = window.matchMedia("(prefers-color-scheme: dark)").matches; + const initialTheme = isDarkMode ? "dark" : "light"; + setTheme(initialTheme); + document.documentElement.classList.toggle("dark", initialTheme === "dark"); + }, []); + + const toggleTheme = () => { + const newTheme = theme === "light" ? "dark" : "light"; + setTheme(newTheme); + document.documentElement.classList.toggle("dark", newTheme === "dark"); + }; + return ( +
+
+

+ Deligraph +

+ +
    +
  • + Projets Clients Side +
  • +
  • + Projets Server Side +
  • +
  • + Contact +
  • + +
+
+
+ ); } diff --git a/app/projects-clients-side/page.jsx b/app/projects-clients-side/page.jsx new file mode 100644 index 0000000..2972e63 --- /dev/null +++ b/app/projects-clients-side/page.jsx @@ -0,0 +1,13 @@ +import Link from "next/link"; +import PostGridClientSide from "../components/PostGridClientSide"; +import Nav from "../components/Nav"; + +export default function Projects() { + return ( +
+
+ ); +} diff --git a/app/projects-server-side/page.jsx b/app/projects-server-side/page.jsx new file mode 100644 index 0000000..f7fa55d --- /dev/null +++ b/app/projects-server-side/page.jsx @@ -0,0 +1,13 @@ +import Link from "next/link"; +import PostGridServerSide from "../components/PostGridServerSide"; +import Nav from "../components/Nav"; + +export default function Projects() { + return ( +
+
+ ); +} diff --git a/app/projects/[slug]/page.jsx b/app/projects/[slug]/page.jsx new file mode 100644 index 0000000..fc3745d --- /dev/null +++ b/app/projects/[slug]/page.jsx @@ -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 ( +
+
+ ); +} diff --git a/app/projects/not-found.jsx b/app/projects/not-found.jsx new file mode 100644 index 0000000..eab033e --- /dev/null +++ b/app/projects/not-found.jsx @@ -0,0 +1,21 @@ +import Link from "next/link"; + +export default function NotFound() { + return ( +
+
+

+ 404 - Projet non trouvé +

+

+ Le projet que vous recherchez n'existe pas ou a été supprimé. +

+ + Retour aux projets + +
+
+ ); +} diff --git a/app/projects/page.jsx b/app/projects/page.jsx new file mode 100644 index 0000000..654a1f2 --- /dev/null +++ b/app/projects/page.jsx @@ -0,0 +1,12 @@ +import PortfolioGridServerSide from "../components/PostGridServerSide"; +import Nav from "../components/Nav"; + +export default function Projects() { + return ( +
+
+ ); +} diff --git a/app/utils/useWordpress.js b/app/utils/useWordpress.js new file mode 100644 index 0000000..6b8b3f7 --- /dev/null +++ b/app/utils/useWordpress.js @@ -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} 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} 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} 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} 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} 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} 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; +} diff --git a/eslint.config.mjs b/eslint.config.mjs old mode 100644 new mode 100755 diff --git a/next.config.ts b/next.config.ts old mode 100644 new mode 100755 index e9ffa30..e272b20 --- a/next.config.ts +++ b/next.config.ts @@ -1,7 +1,25 @@ import type { NextConfig } from "next"; 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; diff --git a/package.json b/package.json old mode 100644 new mode 100755 index 9a925b5..a9bb505 --- a/package.json +++ b/package.json @@ -24,4 +24,4 @@ "typescript": "^5" }, "packageManager": "pnpm@10.22.0+sha512.bf049efe995b28f527fd2b41ae0474ce29186f7edcb3bf545087bd61fbbebb2bf75362d1307fda09c2d288e1e499787ac12d4fcb617a974718a6051f2eee741c" -} +} \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml old mode 100644 new mode 100755 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml old mode 100644 new mode 100755 diff --git a/postcss.config.mjs b/postcss.config.mjs old mode 100644 new mode 100755 diff --git a/public/icon-switch.svg b/public/icon-switch.svg new file mode 100644 index 0000000..7033783 --- /dev/null +++ b/public/icon-switch.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json old mode 100644 new mode 100755