Afficher les projets¶
L'essentiel en 3 points
main.jsattend les données dechargerProjets(), puis génère une carte par projet. Ce code est le même pour toutes les sources (JSON local, Google Sheets, Airtable).- Une fonction
creerCarteProjet(projet)transforme un projet en HTML. On l'applique à tout le tableau avecmap(). - Pour une structure multipages,
projet.html?id=cafe-du-coinlit le paramètreidavecURLSearchParams, puis retrouve le bon projet avecfind().
Avant de commencer
Votre chargerProjets() doit déjà fonctionner : le test console.log(projets[0].titre) affiche un titre dans la console. Sinon, retournez à la page de votre source.
1. Le conteneur dans le HTML¶
Dans index.html, un conteneur vide là où les cartes doivent apparaître. Les cartes que vous aviez codées à la main en HTML/CSS servent maintenant de modèle : on les retire du HTML, et on reproduit leur structure en JavaScript.
<section class="projets">
<h2 class="projets__titre">Projets</h2>
<div class="projets__grille">
<!-- les cartes sont générées par js/main.js -->
</div>
</section>
Gardez votre CSS tel quel
Si le HTML généré par JavaScript utilise exactement les mêmes classes que vos cartes codées à la main, votre CSS s'applique sans aucune modification. Copiez la structure de votre carte actuelle avant de la retirer du HTML.
2. Une carte : js/composants/carte-projet.js¶
Une fonction qui reçoit un projet et retourne le HTML de sa carte, avec un gabarit littéral (les accents graves `, et ${...} pour insérer une valeur).
// js/composants/carte-projet.js
function creerCarteProjet(projet) {
return `
<article class="carte-projet">
<img class="carte-projet__image" src="${projet.image}" alt="${projet.titre}">
<div class="carte-projet__contenu">
<h3 class="carte-projet__titre">${projet.titre}</h3>
<p class="carte-projet__meta">${projet.categorie} · ${projet.annee}</p>
<p class="carte-projet__description">${projet.description}</p>
<a class="carte-projet__lien" href="projet.html?id=${projet.id}">Voir le projet</a>
</div>
</article>
`;
}
Adaptez les classes et la structure à votre design : c'est votre composant, pas celui-ci.
Selon votre structure de navigation
- Multipages : le lien pointe vers
projet.html?id=${projet.id}(section 5). - One-pager avec modale : un bouton plutôt qu'un lien, qui garde l'
iddu projet dans un attributdata-(section 4). - One-pager avec carrousel : même principe de carte, c'est votre composant carrousel qui les fait défiler.
Les propriétés optionnelles¶
Un projet sans lien afficherait href="undefined". On n'affiche un élément que si la propriété existe, avec l'opérateur ternaire :
${projet.lien ? `<a class="carte-projet__externe" href="${projet.lien}" target="_blank">Voir en ligne</a>` : ''}
Lu à voix haute : « s'il y a un lien, affiche la balise <a>, sinon rien ».
3. Toutes les cartes : js/main.js¶
Avec async / await
// js/main.js
async function init() {
const grille = document.querySelector('.projets__grille');
try {
const projets = await chargerProjets();
grille.innerHTML = projets.map(creerCarteProjet).join('');
} catch (erreur) {
console.error(erreur);
grille.innerHTML = '<p>Les projets n’ont pas pu être chargés.</p>';
}
}
init();
Avec .then()
// js/main.js
function init() {
const grille = document.querySelector('.projets__grille');
chargerProjets()
.then(projets => {
grille.innerHTML = projets.map(creerCarteProjet).join('');
})
.catch(erreur => {
console.error(erreur);
grille.innerHTML = '<p>Les projets n’ont pas pu être chargés.</p>';
});
}
init();
Utilisez la même syntaxe que dans votre data.js. Les deux fonctionnent avec n'importe quelle version de chargerProjets() : une fonction async et une chaîne .then() retournent toutes deux une promesse.
Ligne par ligne :
await chargerProjets()ouchargerProjets().then(...): on attend que les données arrivent, peu importe leur source.projets.map(creerCarteProjet): chaque projet du tableau devient une chaîne de HTML. Résultat : un tableau de cartes..join(''): on colle toutes ces chaînes ensemble en une seule.grille.innerHTML = ...: on insère le tout dans le conteneur, en une seule opération.try/catch, ou.catch(): si le chargement échoue (fichier introuvable, jeton invalide, pas de réseau), l'utilisateur voit un message plutôt qu'une section vide, et l'erreur détaillée reste dans la console pour vous.
Pourquoi un seul innerHTML plutôt qu'une boucle qui ajoute les cartes une à une?
Modifier le DOM une seule fois est plus rapide que le modifier à chaque tour de boucle. C'est aussi ce que vise l'indicateur de la grille d'évaluation sur le chargement efficace des données.
Gabarits et innerHTML : seulement avec vos propres données
Insérer du texte dans innerHTML est sans danger ici parce que les données sont les vôtres. Avec du texte saisi par des visiteurs (un formulaire, des commentaires), ce serait une faille XSS, que vous avez vue dans vos cours précédents.
4. One-pager avec modale¶
Dans la carte, un bouton qui garde l'identifiant du projet :
<button class="carte-projet__bouton" data-id="${projet.id}">Voir le projet</button>
Dans index.html, une seule modale, vide, réutilisée pour tous les projets :
<dialog class="modale">
<div class="modale__contenu"></div>
<button class="modale__fermer">Fermer</button>
</dialog>
Dans main.js, après avoir inséré les cartes :
const modale = document.querySelector('.modale');
grille.addEventListener('click', (evenement) => {
const bouton = evenement.target.closest('.carte-projet__bouton');
if (!bouton) return; // le clic n'était pas sur un bouton de carte
const projet = projets.find(p => p.id === bouton.dataset.id);
modale.querySelector('.modale__contenu').innerHTML = `
<h2>${projet.titre}</h2>
<img src="${projet.image}" alt="${projet.titre}">
<p>${projet.description}</p>
`;
modale.showModal();
});
document.querySelector('.modale__fermer')
.addEventListener('click', () => modale.close());
- Un seul écouteur sur la grille, plutôt qu'un par carte :
closest()retrouve le bouton cliqué. C'est la délégation d'événements. dataset.idlit l'attributdata-iddu bouton.find()retrouve, dans le tableau déjà chargé, le projet qui a cetid. Aucun nouveaufetch().<dialog>etshowModal(): la modale native du navigateur, qui gère déjà le fond assombri et la touche Échap.
Ce code doit être placé à l'intérieur de init(), après la ligne grille.innerHTML = ... (dans le try avec async / await, ou dans le .then()), pour avoir accès à la variable projets.
5. Multipages : projet.html et les paramètres d'URL¶
Le principe¶
Chaque carte pointe vers la même page, avec un paramètre différent :
projet.html?id=cafe-du-coin
projet.html?id=biome
Tout ce qui suit le ? s'appelle la chaîne de requête (query string). Elle est composée de paires nom=valeur. projet.html est un gabarit vide : il lit l'id dans l'adresse, recharge les données, et affiche seulement le projet correspondant. Une seule page HTML pour tous vos projets, pas de routeur, pas de cadriciel.
Le HTML de projet.html¶
Mêmes <head>, en-tête et pied de page que index.html, avec un conteneur vide pour le détail. Les scripts :
<script src="js/data.js" defer></script>
<script src="js/projet.js" defer></script>
data.js est le même fichier que pour la page d'accueil : on réutilise chargerProjets() sans rien changer.
Le code de js/projet.js¶
Avec async / await
// js/projet.js
async function afficherProjet() {
const conteneur = document.querySelector('.projet');
// 1. Lire l'id dans l'adresse : projet.html?id=cafe-du-coin
const parametres = new URLSearchParams(window.location.search);
const idProjet = parametres.get('id'); // "cafe-du-coin"
try {
// 2. Recharger les données (même fonction que sur la page d'accueil)
const projets = await chargerProjets();
// 3. Retrouver le bon projet
const projet = projets.find(p => p.id === idProjet);
if (!projet) {
conteneur.innerHTML = '<p>Ce projet est introuvable.</p> <a href="index.html">Retour aux projets</a>';
return;
}
// 4. L'afficher
document.title = `${projet.titre} | Portfolio`;
conteneur.innerHTML = `
<h1 class="projet__titre">${projet.titre}</h1>
<p class="projet__meta">${projet.categorie} · ${projet.annee}</p>
<img class="projet__image" src="${projet.image}" alt="${projet.titre}">
<p class="projet__description">${projet.description}</p>
${projet.video ? `<iframe class="projet__video" src="${projet.video}" title="Vidéo : ${projet.titre}" allowfullscreen></iframe>` : ''}
`;
} catch (erreur) {
console.error(erreur);
conteneur.innerHTML = '<p>Le projet n’a pas pu être chargé.</p>';
}
}
afficherProjet();
Avec .then()
// js/projet.js
function afficherProjet() {
const conteneur = document.querySelector('.projet');
// 1. Lire l'id dans l'adresse : projet.html?id=cafe-du-coin
const parametres = new URLSearchParams(window.location.search);
const idProjet = parametres.get('id'); // "cafe-du-coin"
// 2. Recharger les données (même fonction que sur la page d'accueil)
chargerProjets()
.then(projets => {
// 3. Retrouver le bon projet
const projet = projets.find(p => p.id === idProjet);
if (!projet) {
conteneur.innerHTML = '<p>Ce projet est introuvable.</p> <a href="index.html">Retour aux projets</a>';
return;
}
// 4. L'afficher
document.title = `${projet.titre} | Portfolio`;
conteneur.innerHTML = `
<h1 class="projet__titre">${projet.titre}</h1>
<p class="projet__meta">${projet.categorie} · ${projet.annee}</p>
<img class="projet__image" src="${projet.image}" alt="${projet.titre}">
<p class="projet__description">${projet.description}</p>
${projet.video ? `<iframe class="projet__video" src="${projet.video}" title="Vidéo : ${projet.titre}" allowfullscreen></iframe>` : ''}
`;
})
.catch(erreur => {
console.error(erreur);
conteneur.innerHTML = '<p>Le projet n’a pas pu être chargé.</p>';
});
}
afficherProjet();
window.location.search: la partie de l'adresse qui commence au?, soit"?id=cafe-du-coin".new URLSearchParams(...): un objet qui sait lire cette chaîne..get('id')retourne la valeur, ounullsi le paramètre est absent.find()retourne le premier projet dont l'idcorrespond, ouundefineds'il n'y en a aucun. D'où le cas « introuvable » : quelqu'un peut toujours taper une adresse erronée.document.title: le titre de l'onglet change selon le projet, un détail qui fait professionnel.
Chaque id doit être unique
Deux projets avec le même id, et find() affichera toujours le premier. Pas d'espaces ni d'accents non plus : un id se retrouve dans une adresse web.
Bonus : afficher la galerie¶
${projet.galerie && projet.galerie.length ? `
<div class="projet__galerie">
${projet.galerie.map(url => `<img src="${url}" alt="${projet.titre}">`).join('')}
</div>
` : ''}
Le même patron que pour les cartes : map() puis join(''), cette fois à l'intérieur d'un gabarit.
6. Pièges fréquents¶
undefinedaffiché dans la page : une propriété est mal nommée (projet.Titreau lieu deprojet.titre) ou absente de vos données. Comparez avec unconsole.log(projet).chargerProjets is not defined:data.jsn'est pas lié dans cette page, ou il est lié après le script qui l'utilise.- Les cartes s'affichent mais sans style : les classes générées en JS ne correspondent pas exactement à celles de votre CSS.
- Rien ne s'affiche, aucune erreur : vérifiez que le sélecteur de
querySelector('.projets__grille')existe vraiment dans le HTML de cette page. - Texte alternatif :
alt="${projet.titre}"est un minimum. Pour de meilleures descriptions, ajoutez une propriétéaltà vos données.
7. Avant de partir¶
- Les cartes de la page d'accueil sont générées à partir de vos données, plus aucune n'est écrite à la main dans le HTML.
- Le détail d'un projet s'affiche (modale, carrousel ou
projet.html). - Un projet sans
lienou sansvideon'affiche pasundefined. - Commit avec un message clair, ex.
Chargement des projets depuis projets.json. - Entrée dans
JOURNAL.mdpour chaque prompt Copilot délibéré (pas les complétions en ligne).