- Projet basé uniquement sur React avec TypeScript (pas de backend pour l'instant, données stockées en état local).
- CSS natif (pas Tailwind ou Bootstrap).
- Fonctionnalités CRUD : Create (ajouter), Read (lister), Update (modifier), Delete (supprimer).
- Posts avec titre, description, et image (soit via URL directe d'une image sur le net, soit via sélection d'un fichier local depuis votre machine).
- Validation : L'URL doit pointer directement vers un fichier image (ex. terminant par .jpg, .png, etc.), et on vérifie si elle se charge. Messages d'erreur clairs pour guider.
- Images affichées avec dimensions uniformes (300x200 pixels) pour que tout soit propre.
- Gestion des fichiers locaux : Convertis en URL temporaire (blob:) pour l'affichage pendant la session.
- Structure modulaire avec composants réutilisables.
Je vais expliquer le rôle de chaque étape, de chaque fichier, et de chaque partie de code. Les codes seront écrits directement dans le texte (pas en artefacts) pour que vous puissiez facilement copier-coller. Copiez-les un par un dans vos fichiers dans VS Code.
Allons-y pas à pas, calmement. Si quelque chose n'est pas clair, arrêtez-vous et demandez-moi !
---
### Contexte
**Objectif du projet** :
Créer une application React simple pour un blog où vous pouvez ajouter, voir, modifier et supprimer des posts. Chaque post a un titre, une description, et une photo (image). Les données sont stockées en mémoire (état local de React), donc elles disparaissent si vous rechargez la page. On simule une "base de données" avec un tableau en état.
**Rôles des concepts clés en React (expliqués simplement car vous êtes débutant)** :
- **Composants** : Comme des blocs Lego. Chaque partie de l'app (ex. formulaire, post individuel) est un composant réutilisable.
- **État (useState)** : Pour stocker des données qui changent, comme la liste des posts ou les champs du formulaire. C'est comme une variable qui se met à jour automatiquement sur l'écran.
- **Props** : Des données passées d'un composant parent (ex. App) à un enfant (ex. Post). Comme des arguments de fonction.
- **useEffect** : Pour exécuter du code quand quelque chose change (ex. pré-remplir le formulaire lors de l'édition).
- **Événements** : Pour réagir aux clics ou soumissions (ex. onSubmit pour le formulaire).
- **Validation** : On vérifie que les entrées sont correctes avant d'ajouter un post (ex. URL valide pour l'image).
**Outils nécessaires** :
- VS Code (éditeur de code).
- Node.js installé (pour npm).
- Un navigateur (Chrome ou Firefox).
**Structure des fichiers** :
- `src/App.tsx` : Le composant principal, gère la liste des posts et l'état global.
- `src/components/Post.tsx` : Affiche un post individuel.
- `src/components/PostForm.tsx` : Le formulaire pour ajouter ou modifier un post, avec validation.
- `src/index.css` : Les styles CSS pour tout l'app.
- `index.html` : Version autonome pour tester sans npm.
Prêt ? On commence par créer le projet.
---
### Étape 1 : Créer le projet React avec TypeScript
**Rôle de cette étape** : Créer la base de l'application React. TypeScript ajoute des types pour éviter les erreurs (ex. s'assurer que "id" est un nombre). On n'installe pas Tailwind, car on utilise du CSS natif.
1. Ouvrez un terminal dans VS Code (Ctrl + ` sur Windows, ou menu Terminal > New Terminal).
2. Créez un dossier pour le projet et allez dedans :
```
mkdir photo-blog
cd photo-blog
```
3. Créez le projet React avec TypeScript :
```
npx create-react-app . --template typescript
```
(Le "." signifie "dans le dossier actuel". Cela installe React, TypeScript, et les dépendances basiques.)
4. Testez que ça marche :
```
npm start
```
Ouvrez http://localhost:3000 dans votre navigateur. Vous devriez voir la page par défaut de React (un logo qui tourne). Arrêtez le serveur avec Ctrl + C si besoin.
5. Nettoyez les fichiers inutiles :
- Supprimez `src/App.css`, `src/logo.svg`, `src/setupTests.ts` (pas besoin pour ce projet simple).
- Ouvrez `src/App.tsx` et remplacez tout son contenu par un squelette basique (on le remplira plus tard) :
```
import React from 'react';
import './index.css';
function App() {
return (
<div className="container">
<h1 className="title">Photo Blog</h1>
</div>
);
}
export default App;
```
6. Modifiez `src/index.tsx` pour importer `index.css` (déjà fait par défaut, mais confirmez) :
Le fichier devrait ressembler à ça (ne changez pas sauf si nécessaire) :
```
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
reportWebVitals();
```
**Vérification** : Relancez `npm start`. Vous devriez voir "Photo Blog" sur la page. Si erreur, vérifiez que vous avez bien supprimé les fichiers inutiles.
---
### Étape 2 : Créer le dossier components et les fichiers principaux
**Rôle de cette étape** : Diviser l'app en composants réutilisables. `components` est un dossier pour organiser les blocs (Post et PostForm).
1. Dans `src`, créez un dossier `components` (clic droit > New Folder).
2. Créez `src/components/Post.tsx` : Ce composant affiche un post individuel (titre, description, image, boutons modifier/supprimer).
Copiez ce code dedans :
```
import React from 'react';
interface PostProps {
id: number;
title: string;
description: string;
imageUrl: string;
onEdit: (id: number) => void;
onDelete: (id: number) => void;
}
const Post: React.FC<PostProps> = ({ id, title, description, imageUrl, onEdit, onDelete }) => {
return (
<div className="post-card">
<h2>{title}</h2>
<p>{description}</p>
<img src={imageUrl} alt={title} />
<div className="post-actions">
<button onClick={() => onEdit(id)} className="btn btn-edit">
Modifier
</button>
<button onClick={() => onDelete(id)} className="btn btn-delete">
Supprimer
</button>
</div>
</div>
);
};
export default Post;
```
**Explication détaillée** :
- `interface PostProps` : Définit les types des données reçues (props) : id (nombre), title (texte), etc. C'est TypeScript qui aide à éviter les erreurs.
- `const Post: React.FC<PostProps>` : Crée le composant. `FC` signifie Functional Component.
- Le return affiche le HTML : h2 pour titre, p pour description, img pour image, boutons pour actions.
- Les boutons appellent `onEdit` et `onDelete` (fonctions passées par le parent).
3. Créez `src/components/PostForm.tsx` : Ce composant gère le formulaire (ajout ou édition), avec validation pour l'image.
Copiez ce code dedans :
```
import React, { useState, useEffect } from 'react';
interface PostFormProps {
onAddPost: (post: { title: string; description: string; imageUrl: string }) => void;
onEditPost?: (post: { id: number; title: string; description: string; imageUrl: string }) => void;
postToEdit?: { id: number; title: string; description: string; imageUrl: string };
}
const PostForm: React.FC<PostFormProps> = ({ onAddPost, onEditPost, postToEdit }) => {
const [title, setTitle] = useState(postToEdit?.title || '');
const [description, setDescription] = useState(postToEdit?.description || '');
const [imageUrl, setImageUrl] = useState(postToEdit?.imageUrl || '');
const [file, setFile] = useState<File | null>(null);
const [error, setError] = useState<string>('');
useEffect(() => {
if (postToEdit) {
setTitle(postToEdit.title);
setDescription(postToEdit.description);
setImageUrl(postToEdit.imageUrl);
setFile(null);
setError('');
}
}, [postToEdit]);
const validImageExtensions = /\.(jpg|jpeg|png|gif|bmp|webp)$/i;
const validateImageUrl = (url: string): Promise<boolean> => {
return new Promise((resolve) => {
// Vérifier l'extension de l'URL
if (!validImageExtensions.test(url)) {
resolve(false);
return;
}
const img = new Image();
img.src = url;
img.onload = () => resolve(true);
img.onerror = () => resolve(false);
});
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError('');
if (!title || !description) {
setError('Le titre et la description sont requis.');
return;
}
let finalImageUrl = imageUrl;
if (file) {
finalImageUrl = URL.createObjectURL(file);
} else if (imageUrl) {
if (!validImageExtensions.test(imageUrl)) {
setError('L\'URL doit pointer vers un fichier image (ex. .jpg, .png, .gif). Exemple : https://images.pexels.com/photos/123/pexels-photo-123.jpeg');
return;
}
const isValid = await validateImageUrl(imageUrl);
if (!isValid) {
setError('L\'URL de l\'image est invalide ou ne peut pas être chargée. Vérifiez qu\'elle pointe directement vers une image.');
return;
}
} else {
setError('Veuillez fournir une URL d\'image valide ou sélectionner un fichier image.');
return;
}
if (postToEdit && onEditPost) {
onEditPost({ id: postToEdit.id, title, description, imageUrl: finalImageUrl });
} else {
onAddPost({ title, description, imageUrl: finalImageUrl });
}
setTitle('');
setDescription('');
setImageUrl('');
setFile(null);
setError('');
};
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const selectedFile = e.target.files?.[0] || null;
if (selectedFile && selectedFile.type.startsWith('image/')) {
setFile(selectedFile);
setImageUrl('');
setError('');
} else {
setError('Veuillez sélectionner un fichier image valide (ex. .jpg, .png, .gif).');
setFile(null);
}
};
return (
<div className="form-container">
<h2>{postToEdit ? 'Modifier un post' : 'Ajouter un post'}</h2>
<form onSubmit={handleSubmit}>
<div className="form-group">
<label>Titre</label>
<input
type="text"
value={title}
onChange={(e) => { setTitle(e.target.value); setError(''); }}
placeholder="Entrez le titre"
/>
</div>
<div className="form-group">
<label>Description</label>
<textarea
value={description}
onChange={(e) => { setDescription(e.target.value); setError(''); }}
placeholder="Entrez la description"
/>
</div>
<div className="form-group">
<label>URL de l'image</label>
<input
type="text"
value={imageUrl}
onChange={(e) => { setImageUrl(e.target.value); setFile(null); setError(''); }}
placeholder="Ex. https://images.pexels.com/photos/123/pexels-photo-123.jpeg"
disabled={!!file}
/>
</div>
<div className="form-group">
<label>Sélectionner une image locale</label>
<input
type="file"
accept="image/*"
onChange={handleFileChange}
disabled={!!imageUrl}
/>
</div>
{error && <div className="error-message">{error}</div>}
<button type="submit" className="btn btn-submit">
{postToEdit ? 'Mettre à jour' : 'Ajouter'}
</button>
</form>
</div>
);
};
export default PostForm;
```
**Explication détaillée** :
- `useState` : Pour stocker le titre, description, imageUrl, file (fichier local), et error (message d'erreur).
- `useEffect` : Pré-remplit le formulaire quand on édite un post.
- `validImageExtensions` : Regex pour vérifier si l'URL finit par .jpg, .png, etc.
- `validateImageUrl` : Vérifie l'extension, puis essaie de charger l'image avec new Image().
- `handleSubmit` : Vérifie tout avant d'ajouter/modifier : champs requis, image valide. Si fichier local, utilise URL.createObjectURL pour l'afficher.
- `handleFileChange` : Gère la sélection de fichier, vérifie que c'est une image.
- Return : Le formulaire avec inputs, disabled pour éviter conflits entre URL et fichier, et affichage d'erreur.
4. Mettez à jour `src/App.tsx` : Le cœur de l'app, gère la liste des posts, les fonctions add/edit/delete.
Copiez ce code dedans :
```
import React, { useState } from 'react';
import './index.css';
import Post from './components/Post';
import PostForm from './components/PostForm';
interface Post {
id: number;
title: string;
description: string;
imageUrl: string;
}
function App() {
const [posts, setPosts] = useState<Post[]>([
{
id: 1,
title: 'Premier post',
description: 'Une belle photo de montagne.',
imageUrl: 'https://images.pexels.com/photos/13798023/pexels-photo-13798023.jpeg',
},
{
id: 2,
title: 'Deuxième post',
description: 'Un coucher de soleil magnifique.',
imageUrl: 'https://via.placeholder.com/300x200',
},
]);
const [postToEdit, setPostToEdit] = useState<Post | undefined>(undefined);
const addPost = (newPost: { title: string; description: string; imageUrl: string }) => {
setPosts([...posts, { ...newPost, id: posts.length + 1 }]);
};
const editPost = (updatedPost: Post) => {
setPosts(posts.map((post) => (post.id === updatedPost.id ? updatedPost : post)));
setPostToEdit(undefined);
};
const deletePost = (id: number) => {
setPosts(posts.filter((post) => post.id !== id));
};
const startEditing = (id: number) => {
const post = posts.find((p) => p.id === id);
setPostToEdit(post);
};
return (
<div className="container">
<h1 className="title">Photo Blog</h1>
<PostForm onAddPost={addPost} onEditPost={editPost} postToEdit={postToEdit} />
<div className="post-grid">
{posts.map((post) => (
<Post
key={post.id}
id={post.id}
title={post.title}
description={post.description}
imageUrl={post.imageUrl}
onEdit={startEditing}
onDelete={deletePost}
/>
))}
</div>
</div>
);
}
export default App;
```
**Explication détaillée** :
- `interface Post` : Définit la structure d'un post.
- `useState` pour `posts` (liste) et `postToEdit` (post en édition).
- `addPost` : Ajoute un nouveau post avec ID auto-incrémenté.
- `editPost` : Met à jour un post existant.
- `deletePost` : Supprime un post par ID.
- `startEditing` : Charge le post à éditer dans le formulaire.
- Return : Affiche le titre, le formulaire, et la liste des posts via map (boucle).
5. Créez `src/index.css` : Les styles pour rendre tout joli et responsive.
Copiez ce code dedans :
```
/* Styles globaux */
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
background-color: #f4f4f4;
}
/* Conteneur principal */
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
/* Titre principal */
.title {
font-size: 2rem;
font-weight: bold;
text-align: center;
margin-bottom: 1.5rem;
}
/* Grille des posts */
.post-grid {
display: grid;
grid-template-columns: 1fr;
gap: 1rem;
}
@media (min-width: 768px) {
.post-grid {
grid-template-columns: repeat(2, 1fr);
}
}
/* Carte de post */
.post-card {
border: 1px solid #ddd;
border-radius: 8px;
padding: 1rem;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
background-color: #fff;
}
.post-card h2 {
font-size: 1.25rem;
font-weight: 600;
margin-bottom: 0.5rem;
}
.post-card p {
color: #666;
margin-bottom: 0.5rem;
}
.post-card img {
width: 300px;
height: 200px;
object-fit: cover;
border-radius: 4px;
margin-top: 0.5rem;
display: block;
margin-left: auto;
margin-right: auto;
}
/* Boutons dans le post */
.post-actions {
margin-top: 1rem;
display: flex;
gap: 0.5rem;
}
.btn {
padding: 0.5rem 1rem;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 1rem;
color: #fff;
}
.btn-edit {
background-color: #f59e0b;
}
.btn-edit:hover {
background-color: #d97706;
}
.btn-delete {
background-color: #ef4444;
}
.btn-delete:hover {
background-color: #dc2626;
}
/* Formulaire */
.form-container {
border: 1px solid #ddd;
border-radius: 8px;
padding: 1rem;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
background-color: #fff;
margin-bottom: 1.5rem;
}
.form-container h2 {
font-size: 1.25rem;
font-weight: 600;
margin-bottom: 1rem;
}
.form-group {
margin-bottom: 1rem;
}
.form-group label {
display: block;
font-size: 0.875rem;
font-weight: 500;
margin-bottom: 0.25rem;
}
.form-group input,
.form-group textarea {
width: 100%;
padding: 0.5rem;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 1rem;
}
.form-group textarea {
resize: vertical;
min-height: 100px;
}
.form-group input[type="file"] {
padding: 0.25rem;
}
.btn-submit {
background-color: #3b82f6;
}
.btn-submit:hover {
background-color: #2563eb;
}
/* Message d'erreur */
.error-message {
color: #ef4444;
font-size: 0.875rem;
margin-top: 0.25rem;
}
```
**Explication détaillée** :
- Styles globaux pour le body.
- .container : Centre le contenu.
- .title : Style pour le h1.
- .post-grid : Grille responsive (1 colonne sur mobile, 2 sur desktop).
- .post-card : Style pour chaque post (bordure, ombre).
- .post-card img : Dimensions uniformes (300x200), centrée.
- Boutons : Couleurs et hover.
- Formulaire : Styles pour champs, labels, et erreur.
**Vérification** : Relancez `npm start`. Vous devriez voir le titre, le formulaire, et deux posts initiaux. Testez l'ajout avec une URL valide comme `https://images.pexels.com/photos/13798023/pexels-photo-13798023.jpeg` ou un fichier local. Vérifiez les erreurs si URL invalide.
---
### Étape 3 : Ajouter le contrôle de version avec Git
**Rôle de cette étape** : Git permet de sauvegarder les versions de votre code, comme des sauvegardes. Utile si vous cassez quelque chose.
1. Dans le terminal :
```
git init
git add .
git commit -m "Version complète du projet Photo Blog avec CRUD, validation et CSS natif"
```
2. Si vous modifiez plus tard, répétez `git add .` et `git commit -m "Description des changements"`.
---
### Étape 4 : Créer la version autonome (index.html)
**Rôle de cette étape** : Une version tout-en-un dans un fichier HTML, pour tester sans npm (utile pour partager ou tester rapidement). Elle utilise React via CDN (liens internet pour charger React sans installation).
Créez un fichier `index.html` à la racine du projet (ou ailleurs) et copiez ce code dedans :
```
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Photo Blog</title>
<script src="https://cdn.jsdelivr.net/npm/react@18/umd/react.development.js"></script>
<script src="https://cdn.jsdelivr.net/npm/react-dom@18/umd/react-dom.development.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@babel/standalone/babel.min.js"></script>
<style>
/* Styles globaux */
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
background-color: #f4f4f4;
}
/* Conteneur principal */
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
/* Titre principal */
.title {
font-size: 2rem;
font-weight: bold;
text-align: center;
margin-bottom: 1.5rem;
}
/* Grille des posts */
.post-grid {
display: grid;
grid-template-columns: 1fr;
gap: 1rem;
}
@media (min-width: 768px) {
.post-grid {
grid-template-columns: repeat(2, 1fr);
}
}
/* Carte de post */
.post-card {
border: 1px solid #ddd;
border-radius: 8px;
padding: 1rem;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
background-color: #fff;
}
.post-card h2 {
font-size: 1.25rem;
font-weight: 600;
margin-bottom: 0.5rem;
}
.post-card p {
color: #666;
margin-bottom: 0.5rem;
}
.post-card img {
width: 300px;
height: 200px;
object-fit: cover;
border-radius: 4px;
margin-top: 0.5rem;
display: block;
margin-left: auto;
margin-right: auto;
}
/* Boutons dans le post */
.post-actions {
margin-top: 1rem;
display: flex;
gap: 0.5rem;
}
.btn {
padding: 0.5rem 1rem;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 1rem;
color: #fff;
}
.btn-edit {
background-color: #f59e0b;
}
.btn-edit:hover {
background-color: #d97706;
}
.btn-delete {
background-color: #ef4444;
}
.btn-delete:hover {
background-color: #dc2626;
}
/* Formulaire */
.form-container {
border: 1px solid #ddd;
border-radius: 8px;
padding: 1rem;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
background-color: #fff;
margin-bottom: 1.5rem;
}
.form-container h2 {
font-size: 1.25rem;
font-weight: 600;
margin-bottom: 1rem;
}
.form-group {
margin-bottom: 1rem;
}
.form-group label {
display: block;
font-size: 0.875rem;
font-weight: 500;
margin-bottom: 0.25rem;
}
.form-group input,
.form-group textarea {
width: 100%;
padding: 0.5rem;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 1rem;
}
.form-group input[type="file"] {
padding: 0.25rem;
}
.form-group textarea {
resize: vertical;
min-height: 100px;
}
.btn-submit {
background-color: #3b82f6;
}
.btn-submit:hover {
background-color: #2563eb;
}
/* Message d'erreur */
.error-message {
color: #ef4444;
font-size: 0.875rem;
margin-top: 0.25rem;
}
</style>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
const { useState, useEffect } = React;
const Post = ({ id, title, description, imageUrl, onEdit, onDelete }) => {
return (
<div className="post-card">
<h2>{title}</h2>
<p>{description}</p>
<img src={imageUrl} alt={title} />
<div className="post-actions">
<button onClick={() => onEdit(id)} className="btn btn-edit">
Modifier
</button>
<button onClick={() => onDelete(id)} className="btn btn-delete">
Supprimer
</button>
</div>
</div>
);
};
const PostForm = ({ onAddPost, onEditPost, postToEdit }) => {
const [title, setTitle] = useState(postToEdit?.title || '');
const [description, setDescription] = useState(postToEdit?.description || '');
const [imageUrl, setImageUrl] = useState(postToEdit?.imageUrl || '');
const [file, setFile] = useState(null);
const [error, setError] = useState('');
useEffect(() => {
if (postToEdit) {
setTitle(postToEdit.title);
setDescription(postToEdit.description);
setImageUrl(postToEdit.imageUrl);
setFile(null);
setError('');
}
}, [postToEdit]);
const validImageExtensions = /\.(jpg|jpeg|png|gif|bmp|webp)$/i;
const validateImageUrl = (url) => {
return new Promise((resolve) => {
if (!validImageExtensions.test(url)) {
resolve(false);
return;
}
const img = new Image();
img.src = url;
img.onload = () => resolve(true);
img.onerror = () => resolve(false);
});
};
const handleSubmit = async (e) => {
e.preventDefault();
setError('');
if (!title || !description) {
setError('Le titre et la description sont requis.');
return;
}
let finalImageUrl = imageUrl;
if (file) {
finalImageUrl = URL.createObjectURL(file);
} else if (imageUrl) {
if (!validImageExtensions.test(imageUrl)) {
setError('L\'URL doit pointer vers un fichier image (ex. .jpg, .png, .gif). Exemple : https://images.pexels.com/photos/123/pexels-photo-123.jpeg');
return;
}
const isValid = await validateImageUrl(imageUrl);
if (!isValid) {
setError('L\'URL de l\'image est invalide ou ne peut pas être chargée. Vérifiez qu\'elle pointe directement vers une image.');
return;
}
} else {
setError('Veuillez fournir une URL d\'image valide ou sélectionner un fichier image.');
return;
}
if (postToEdit && onEditPost) {
onEditPost({ id: postToEdit.id, title, description, imageUrl: finalImageUrl });
} else {
onAddPost({ title, description, imageUrl: finalImageUrl });
}
setTitle('');
setDescription('');
setImageUrl('');
setFile(null);
setError('');
};
const handleFileChange = (e) => {
const selectedFile = e.target.files?.[0] || null;
if (selectedFile && selectedFile.type.startsWith('image/')) {
setFile(selectedFile);
setImageUrl('');
setError('');
} else {
setError('Veuillez sélectionner un fichier image valide (ex. .jpg, .png, .gif).');
setFile(null);
}
};
return (
<div className="form-container">
<h2>{postToEdit ? 'Modifier un post' : 'Ajouter un post'}</h2>
<form onSubmit={handleSubmit}>
<div className="form-group">
<label>Titre</label>
<input
type="text"
value={title}
onChange={(e) => { setTitle(e.target.value); setError(''); }}
placeholder="Entrez le titre"
/>
</div>
<div className="form-group">
<label>Description</label>
<textarea
value={description}
onChange={(e) => { setDescription(e.target.value); setError(''); }}
placeholder="Entrez la description"
/>
</div>
<div className="form-group">
<label>URL de l'image</label>
<input
type="text"
value={imageUrl}
onChange={(e) => { setImageUrl(e.target.value); setFile(null); setError(''); }}
placeholder="Ex. https://images.pexels.com/photos/123/pexels-photo-123.jpeg"
disabled={!!file}
/>
</div>
<div className="form-group">
<label>Sélectionner une image locale</label>
<input
type="file"
accept="image/*"
onChange={handleFileChange}
disabled={!!imageUrl}
/>
</div>
{error && <div className="error-message">{error}</div>}
<button type="submit" className="btn btn-submit">
{postToEdit ? 'Mettre à jour' : 'Ajouter'}
</button>
</form>
</div>
);
};
const App = () => {
const [posts, setPosts] = useState([
{
id: 1,
title: 'Premier post',
description: 'Une belle photo de montagne.',
imageUrl: 'https://images.pexels.com/photos/13798023/pexels-photo-13798023.jpeg',
},
{
id: 2,
title: 'Deuxième post',
description: 'Un coucher de soleil magnifique.',
imageUrl: 'https://via.placeholder.com/300x200',
},
]);
const [postToEdit, setPostToEdit] = useState(undefined);
const addPost = (newPost) => {
setPosts([...posts, { ...newPost, id: posts.length + 1 }]);
};
const editPost = (updatedPost) => {
setPosts(posts.map((post) => (post.id === updatedPost.id ? updatedPost : post)));
setPostToEdit(undefined);
};
const deletePost = (id) => {
setPosts(posts.filter((post) => post.id !== id));
};
const startEditing = (id) => {
const post = posts.find((p) => p.id === id);
setPostToEdit(post);
};
return (
<div className="container">
<h1 className="title">Photo Blog</h1>
<PostForm onAddPost={addPost} onEditPost={editPost} postToEdit={postToEdit} />
<div className="post-grid">
{posts.map((post) => (
<Post
key={post.id}
id={post.id}
title={post.title}
description={post.description}
imageUrl={post.imageUrl}
onEdit={startEditing}
onDelete={deletePost}
/>
))}
</div>
</div>
);
};
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);
</script>
</body>
</html>