creation backend fonction import img cloudinary

This commit is contained in:
sebvtl728
2025-07-31 16:39:14 +02:00
parent f43e285a9c
commit bfe25828fe
14 changed files with 1271 additions and 57 deletions
+1
View File
@@ -39,6 +39,7 @@
<link rel="preload" href="/assets/index-CfManBR6.css" as="style" />
<link rel="stylesheet" href="/assets/index-CfManBR6.css" />
<script src="https://unpkg.com/html2pdf.js@0.10.1/dist/html2pdf.bundle.min.js"></script>
<script src="https://media-library.cloudinary.com/global/all.js"></script>
</head>
<body>
<div id="root"></div>
+14
View File
@@ -13,6 +13,7 @@
"@mui/icons-material": "^6.3.0",
"@mui/material": "^6.3.0",
"axios": "^1.7.9",
"cloudinary": "^2.7.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-helmet": "^6.1.0",
@@ -3177,6 +3178,19 @@
"node": ">=0.8"
}
},
"node_modules/cloudinary": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/cloudinary/-/cloudinary-2.7.0.tgz",
"integrity": "sha512-qrqDn31+qkMCzKu1GfRpzPNAO86jchcNwEHCUiqvPHNSFqu7FTNF9FuAkBUyvM1CFFgFPu64NT0DyeREwLwK0w==",
"license": "MIT",
"dependencies": {
"lodash": "^4.17.21",
"q": "^1.5.1"
},
"engines": {
"node": ">=9"
}
},
"node_modules/clsx": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
+1
View File
@@ -15,6 +15,7 @@
"@mui/icons-material": "^6.3.0",
"@mui/material": "^6.3.0",
"axios": "^1.7.9",
"cloudinary": "^2.7.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-helmet": "^6.1.0",
@@ -0,0 +1,71 @@
import React, { useEffect, useState } from "react";
import axios from "axios";
import { Box, Typography, ImageList, ImageListItem } from "@mui/material";
const BACKEND_URL = "https://api.sveitl.synology.me/api/cloudinary-images"; // adapte pour prod
const CloudinaryGallerySelector = ({ onSelect }) => {
const [images, setImages] = useState([]);
const [error, setError] = useState(null);
const [loading, setLoading] = useState(true);
const BACKEND_URL = import.meta.env.VITE_CLOUDINARY_BACKEND_URL;
useEffect(() => {
const fetchImages = async () => {
try {
const response = await axios.get(`${BACKEND_URL}/api/cloudinary-images`);
const data = response.data;
// 🔐 sécurisation du format de réponse
const validImages = Array.isArray(data)
? data
: Array.isArray(data.resources)
? data.resources
: [];
if (validImages.length === 0) {
setError("Aucune image trouvée.");
}
setImages(validImages);
} catch (err) {
console.error("❌ Erreur Cloudinary complète :", JSON.stringify(err.response?.data || err.message, null, 2));
setError("Impossible de récupérer les images depuis le serveur.");
} finally {
setLoading(false);
}
};
fetchImages();
}, []);
if (loading) return <Typography>Chargement des images Cloudinary...</Typography>;
if (error) return <Typography color="error">{error}</Typography>;
return (
<Box sx={{ mt: 2 }}>
<Typography variant="body1" sx={{ fontWeight: "bold", mb: 1 }}>
Galerie Cloudinary :
</Typography>
<ImageList cols={3} gap={12}>
{images.map((img) => (
<ImageListItem key={img.public_id} onClick={() => onSelect(img.secure_url)}>
<img
src={img.secure_url.replace("/upload/", "/upload/f_webp,w_300/")}
alt={img.public_id}
loading="lazy"
style={{
cursor: "pointer",
borderRadius: 8,
boxShadow: "0 2px 6px rgba(0,0,0,0.2)",
}}
/>
</ImageListItem>
))}
</ImageList>
</Box>
);
};
export default CloudinaryGallerySelector;
@@ -0,0 +1,35 @@
import React from "react";
const CLOUD_NAME = "dh5qgexjo";
const UPLOAD_PRESET = "preset_articles"; // crée-le dans Cloudinary si ce nest pas encore fait
const FOLDER = "articles-octopus";
const ImageUploaderCloudinary = ({ onUploadSuccess }) => {
const handleUpload = async (event) => {
const file = event.target.files[0];
if (!file) return;
const formData = new FormData();
formData.append("file", file);
formData.append("upload_preset", UPLOAD_PRESET);
formData.append("folder", FOLDER);
const res = await fetch(`https://api.cloudinary.com/v1_1/${CLOUD_NAME}/image/upload`, {
method: "POST",
body: formData,
});
const data = await res.json();
if (data.secure_url) {
const transformedUrl = data.secure_url.replace("/upload/", "/upload/f_webp,w_800/");
onUploadSuccess(transformedUrl); // ← Ici on t'envoie lURL optimisée
} else {
alert("❌ Erreur lors de l'upload");
}
};
return <input type="file" accept="image/*" onChange={handleUpload} />;
};
export default ImageUploaderCloudinary;
+51 -33
View File
@@ -1,6 +1,6 @@
import { useState, useEffect } from "react";
import { useNavigate } from "react-router-dom";
import { uploadImage, createPost } from "../../wordpress";
import { uploadImageFromUrl, createPost } from "../../wordpress";
import { getToken } from "../../auth";
import {
Box,
@@ -9,18 +9,18 @@ import {
TextField,
Button,
Paper,
Input,
IconButton,
} from "@mui/material";
import { ArrowBack, Publish } from "@mui/icons-material";
import ImageUploaderCloudinary from "../ImageUploaderCloudinary";
import CloudinaryGallerySelector from "../CloudinaryGallerySelector";
function CreatePost() {
const [title, setTitle] = useState("");
const [content, setContent] = useState("");
const [file, setFile] = useState(null);
const [imageUrl, setImageUrl] = useState(null);
const navigate = useNavigate();
// Vérification de l'authentification : Redirection vers /login si l'utilisateur n'est pas connecté
// Vérification de l'authentification
useEffect(() => {
if (!getToken()) {
navigate("/admin/login");
@@ -31,17 +31,15 @@ function CreatePost() {
e.preventDefault();
try {
console.log("📢 Tentative d'upload de l'image...");
const imageId = await uploadImage(file);
console.log("📢 Création du post avec l'image mise en avant...");
const imageId = await uploadImageFromUrl(imageUrl);
await createPost(title, content, imageId);
alert("✅ Article publié !");
navigate("/admin/gestion-articles");
} catch (error) {
console.error("❌ Erreur création post :", error.response?.data || error.message);
alert("❌ Erreur lors de la création du post.");
}
alert("✅ Post créé avec succès !");
navigate("/admin/gestion-articles"); // Redirection après la création
} catch (error) {
alert("❌ Erreur lors de la création du post.");
}
};
return (
@@ -51,14 +49,14 @@ function CreatePost() {
display: "flex",
alignItems: "center",
justifyContent: "center",
backgroundImage: "url('https://source.unsplash.com/1600x900/?office,writing')",
backgroundImage:
"url('https://source.unsplash.com/1600x900/?office,writing')",
backgroundSize: "cover",
backgroundPosition: "center",
}}
>
<Container maxWidth="sm">
<Paper elevation={6} sx={{ padding: 4, borderRadius: 3 }}>
{/* ✅ Bouton Retour à la Gestion des Articles */}
<Button
startIcon={<ArrowBack />}
variant="outlined"
@@ -66,15 +64,16 @@ function CreatePost() {
onClick={() => navigate("/admin/gestion-articles")}
sx={{ mb: 2 }}
>
Retour à la gestion des articles
Retour
</Button>
{/* ✅ Titre de la page */}
<Typography variant="h4" sx={{ fontWeight: "bold", textAlign: "center", mb: 3 }}>
<Typography
variant="h4"
sx={{ fontWeight: "bold", textAlign: "center", mb: 3 }}
>
Créer un article
</Typography>
{/* ✅ Formulaire */}
<form onSubmit={handleSubmit}>
<TextField
label="Titre de l'article"
@@ -98,28 +97,47 @@ function CreatePost() {
required
/>
{/* Upload de l'image */}
<Box sx={{ mt: 2 }}>
{/* Upload via Cloudinary */}
<Box sx={{ mt: 3 }}>
<Typography variant="body1" sx={{ fontWeight: "bold", mb: 1 }}>
Image en vedette :
Importer une image depuis ton ordi :
</Typography>
<Input
type="file"
accept="image/*"
onChange={(e) => setFile(e.target.files[0])}
required
sx={{ display: "block", mb: 2 }}
/>
<ImageUploaderCloudinary onUploadSuccess={(url) => setImageUrl(url)} />
</Box>
{/* ✅ Bouton Publier */}
{/* Sélection galerie Cloudinary */}
<Box sx={{ mt: 4 }}>
<Typography variant="body1" sx={{ fontWeight: "bold", mb: 1 }}>
Ou choisir une image déjà envoyée :
</Typography>
<CloudinaryGallerySelector onSelect={(url) => setImageUrl(url)} />
</Box>
{/* Aperçu */}
{imageUrl && (
<Box sx={{ mt: 3 }}>
<Typography variant="body2">Aperçu de limage :</Typography>
<img
src={imageUrl}
alt="Image sélectionnée"
style={{
width: "100%",
maxHeight: 200,
objectFit: "cover",
borderRadius: 8,
marginTop: 8,
}}
/>
</Box>
)}
<Button
type="submit"
variant="contained"
color="primary"
fullWidth
startIcon={<Publish />}
sx={{ mt: 3 }}
sx={{ mt: 4 }}
>
Publier l'article
</Button>
@@ -130,4 +148,4 @@ function CreatePost() {
);
}
export default CreatePost;
export default CreatePost;
+52 -23
View File
@@ -37,6 +37,39 @@ export async function uploadImage(file) {
throw error;
}
}
export async function uploadImageFromUrl(imageUrl) {
const token = getToken();
if (!token) {
console.error("❌ Aucun token trouvé !");
throw new Error("Utilisateur non authentifié.");
}
try {
// On télécharge limage sous forme de blob
const imageBlob = await fetch(imageUrl).then(res => res.blob());
const file = new File([imageBlob], "image-cloudinary.webp", { type: "image/webp" });
const formData = new FormData();
formData.append("file", file);
formData.append("title", "Image importée");
formData.append("status", "publish");
const response = await axios.post(`${API_URL}/media`, formData, {
headers: {
"Authorization": `Basic ${token}`,
"Content-Type": "multipart/form-data"
}
});
console.log("✅ Image uploadée depuis Cloudinary :", response.data);
return response.data.id;
} catch (error) {
console.error("❌ Erreur upload depuis Cloudinary :", error.response?.data || error.message);
throw error;
}
}
export async function fetchPosts() {
const response = await fetch("https://preprod.octopusdesign.fr/api-octopus/server/wp-json/wp/v2/posts");
@@ -52,33 +85,29 @@ export async function fetchPosts() {
* @returns {Promise<Object>} - Le post créé
*/
export async function createPost(title, content, imageId) {
const token = getToken();
if (!token) {
console.error("❌ Aucun token trouvé !");
throw new Error("Utilisateur non authentifié.");
}
const token = getToken();
if (!token) throw new Error("Utilisateur non authentifié.");
try {
const response = await axios.post(`${API_URL}/posts`, {
title,
content,
status: "publish",
featured_media: imageId,
}, {
headers: {
"Authorization": `Basic ${token}`, // ✅ Garder `Basic` si ça fonctionnait avant
"Content-Type": "application/json"
}
});
console.log("✅ Post créé avec succès :", response.data);
return response.data;
} catch (error) {
console.error("❌ Erreur création post :", error.response?.data || error.message);
throw error;
const response = await axios.post(
`${API_URL}/posts`,
{
title,
content,
status: "publish",
featured_media: imageId, // 🔥 très important
},
{
headers: {
"Authorization": `Basic ${token}`,
"Content-Type": "application/json",
},
}
);
return response.data;
}
/**
* 🔹 Récupère un article WordPress par son ID
* @param {number} postId - L'ID du post à récupérer
+1 -1
View File
File diff suppressed because one or more lines are too long