fCC Forum Leaderboard
Il Progetto
fCC Forum Leaderboard sviluppato con JavaScript vanilla, Fetch API e programmazione asincrona, combinando manipolazione del DOM, destrutturazione avanzata e formattazione dei dati in una tabella responsive. Un progetto che rappresenta il passaggio naturale dalla gestione delle Promises con .then() a una sintassi più espressiva e leggibile con async/await.
Source Code
- index.html
- styles.css
- script.js
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>fCC Forum Leaderboard</title>
<link rel="stylesheet" href="./styles.css" />
</head>
<body>
<header>
<nav>
<img
class="fcc-logo"
src="https://cdn.freecodecamp.org/platform/universal/fcc_primary.svg"
alt="freeCodeCamp logo"
/>
</nav>
<h1 class="title">Latest Topics</h1>
</header>
<main>
<div class="table-wrapper">
<table>
<thead>
<tr>
<th id="topics">Topics</th>
<th id="avatars">Avatars</th>
<th id="replies">Replies</th>
<th id="views">Views</th>
<th id="activity">Activity</th>
</tr>
</thead>
<tbody id="posts-container"></tbody>
</table>
</div>
</main>
<script src="./script.js"></script>
</body>
</html>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
:root {
--main-bg-color: #2a2a40;
--black: #000;
--dark-navy: #0a0a23;
--dark-grey: #d0d0d5;
--medium-grey: #dfdfe2;
--light-grey: #f5f6f7;
--peach: #f28373;
--salmon-color: #f0aea9;
--light-blue: #8bd9f6;
--light-orange: #f8b172;
--light-green: #93cb5b;
--golden-yellow: #f1ba33;
--gold: #f9aa23;
--green: #6bca6b;
}
body {
background-color: var(--main-bg-color);
}
nav {
background-color: var(--dark-navy);
padding: 10px 0;
}
.fcc-logo {
width: 210px;
display: block;
margin: auto;
}
.title {
margin: 25px 0;
text-align: center;
color: var(--light-grey);
}
.table-wrapper {
padding: 0 25px;
overflow-x: auto;
}
table {
width: 100%;
color: var(--dark-grey);
margin: auto;
table-layout: fixed;
border-collapse: collapse;
overflow-x: scroll;
}
#topics {
text-align: start;
width: 60%;
}
th {
border-bottom: 2px solid var(--dark-grey);
padding-bottom: 10px;
font-size: 1.3rem;
}
td:not(:first-child) {
text-align: center;
}
td {
border-bottom: 1px solid var(--dark-grey);
padding: 20px 0;
}
.post-title {
font-size: 1.2rem;
color: var(--medium-grey);
text-decoration: none;
}
.category {
padding: 3px;
color: var(--black);
text-decoration: none;
display: block;
width: fit-content;
margin: 10px 0 10px;
}
.career {
background-color: var(--salmon-color);
}
.feedback,
.html-css {
background-color: var(--light-blue);
}
.support {
background-color: var(--light-orange);
}
.general {
background-color: var(--light-green);
}
.javascript {
background-color: var(--golden-yellow);
}
.backend {
background-color: var(--gold);
}
.python {
background-color: var(--green);
}
.motivation {
background-color: var(--peach);
}
.avatar-container {
display: flex;
justify-content: center;
gap: 10px;
flex-wrap: wrap;
}
.avatar-container img {
width: 30px;
height: 30px;
}
@media (max-width: 750px) {
.table-wrapper {
padding: 0 15px;
}
table {
width: 700px;
}
th {
font-size: 1.2rem;
}
.post-title {
font-size: 1.1rem;
}
}
const forumLatest = "https://cdn.freecodecamp.org/curriculum/forum-latest/latest.json";
const forumTopicUrl = "https://forum.freecodecamp.org/t/";
const forumCategoryUrl = "https://forum.freecodecamp.org/c/";
const avatarUrl = "https://sea1.discourse-cdn.com/freecodecamp";
const postsContainer = document.getElementById("posts-container");
const allCategories = {
299: { category: "Career Advice", className: "career" },
409: { category: "Project Feedback", className: "feedback" },
417: { category: "freeCodeCamp Support", className: "support" },
421: { category: "JavaScript", className: "javascript" },
423: { category: "HTML - CSS", className: "html-css" },
424: { category: "Python", className: "python" },
432: { category: "You Can Do This!", className: "motivation" },
560: { category: "Backend Development", className: "backend" },
};
const forumCategory = (id) => {
let selectedCategory = {};
if (allCategories.hasOwnProperty(id)) {
const { className, category } = allCategories[id];
selectedCategory.className = className;
selectedCategory.category = category;
} else {
selectedCategory.className = "general";
selectedCategory.category = "General";
selectedCategory.id = 1;
}
const url = `${forumCategoryUrl}${selectedCategory.className}/${id}`;
const linkText = selectedCategory.category;
const linkClass = `category ${selectedCategory.className}`;
return `<a href="${url}" class="${linkClass}" target="_blank">
${linkText}
</a>`;
};
const timeAgo = (time) => {
const currentTime = new Date();
const lastPost = new Date(time);
const timeDifference = currentTime - lastPost;
const msPerMinute = 1000 * 60;
const minutesAgo = Math.floor(timeDifference / msPerMinute);
const hoursAgo = Math.floor(minutesAgo / 60);
const daysAgo = Math.floor(hoursAgo / 24);
if (minutesAgo < 60) {
return `${minutesAgo}m ago`;
}
if (hoursAgo < 24) {
return `${hoursAgo}h ago`;
}
return `${daysAgo}d ago`;
};
const viewCount = (views) => {
const thousands = Math.floor(views / 1000);
if (views >= 1000) {
return `${thousands}k`;
}
return views;
};
const avatars = (posters, users) => {
return posters
.map((poster) => {
const user = users.find((user) => user.id === poster.user_id);
if (user) {
const avatar = user.avatar_template.replace(/{size}/, 30);
const userAvatarUrl = avatar.startsWith("/user_avatar/")
? avatarUrl.concat(avatar)
: avatar;
return `<img src="${userAvatarUrl}" alt="${user.name}" />`;
}
})
.join("");
};
const fetchData = async () => {
try {
const res = await fetch(forumLatest);
const data = await res.json();
showLatestPosts(data);
} catch (err) {
console.log(err);
}
};
fetchData();
const showLatestPosts = (data) => {
const { topic_list, users } = data;
const { topics } = topic_list;
postsContainer.innerHTML = topics.map((item) => {
const {
id,
title,
views,
posts_count,
slug,
posters,
category_id,
bumped_at,
} = item;
return `
<tr>
<td>
<a class="post-title" target="_blank" href="${forumTopicUrl}${slug}/${id}">${title}</a>
${forumCategory(category_id)}
</td>
<td>
<div class="avatar-container">
${avatars(posters, users)}
</div>
</td>
<td>${posts_count - 1}</td>
<td>${viewCount(views)}</td>
<td>${timeAgo(bumped_at)}</td>
</tr>`;
}).join("");
};
Il Progetto e la "Sensazione Giusta"
È stato un progetto davvero interessante, e ho capito il perché.
Mi sono reso conto che gli esercizi che mi piacciono di più sono quelli in cui posso mischiare JavaScript con HTML: percepisco l’esercizio come bellissimo, non lo faccio apposta ma ad ogni innerHTML sorrido. Credo che non ci sia modo migliore per concludere questa parte di JavaScript, dato che React è esattamente questo, ovvero, JavaScript che abbraccia l’HTML (o meglio, JSX). Ad ogni passo successivo sento sempre di più di essere nel posto giusto, e questa sensazione mi riempie di gioia.
Il "Level Up": Da .then() a async/await
In questo progetto ho fatto un vero salto di qualità nel modo di pensare l’asincrono.
Il progetto precedente (fCC News Authors Page) utilizzava la catena .then().catch(), che è come il cambio manuale di un’auto, quindi potente, ma più verboso, con tanti passaggi da esplicitare al fine di gestirli. Con questo progetto, invece, ho iniziato a usare async/await, ovvero lo stesso motore (le Promises) ma con cambio automatico: il codice scorre dall’alto verso il basso come se fosse sincrono, ma resta pienamente asincrono sotto il cofano.
Nel codice sincrono ogni istruzione aspetta che la precedente abbia finito prima di partire, ho capito appieno il concetto con questa analogia: il codice sincrono è come un cuoco che mette il pane nel tostapane e resta fermo a fissarlo finché non scatta, bloccando tutto il resto.
Nel codice asincrono, invece, avvii un’operazione lenta (come un fetch verso il server, il “tostapane”), e mentre quella operazione procede in background il programma continua ad eseguire altre istruzioni, esattamente come un cuoco efficiente che mette il pane a tostare e nel frattempo prende il latte, prepara la tazza e torna al tostapane solo quando suona.
Ho imparato un concetto estremamente utile, perché grazie all’asincronia, puoi far partire il fetch, mostrare un indicatore di caricamento, mantenere l’interfaccia reattiva e aggiornare la UI solo quando la Promise è risolta, offrendo un’esperienza fluida anche quando il server impiega diverso tempo a rispondere.
Tornando al concetto di async/await l’aspetto fondamentale è che non introducono un nuovo modello mentale: è l'ennesimo caso di zucchero sintattico ma questa volta per le Promises. Quello che prima scrivevo come catena di .then() e .catch(), ora lo esprimo con:
async functionper "marcare" la funzione come asincronaawaitper aspettare il risultato delle Promises (es.fetch,res.json())try...catchper gestire gli errori in modo molto simile al codice sincrono (come facevo con.catch((err) => {})
Risultato? Stessa logica, ma molto più leggibile.
Una delle cose che avrei voluto fare diversamente è l'uso del metodo .concat() per unire le stringhe. Penso che con l'utilizzo dei Template Literals (${...}) il codice sarebbe stato più leggibile, evitando quella sintassi soggetto-verbo-oggetto che complica inutilmente operazioni semplici come questa.
Ecco nello specifico cosa intendo:
trasformare:
// Soluzione richiesta dal tutorial (Metodo .concat)
const userAvatarUrl = avatar.startsWith("/user_avatar/")
? avatarUrl.concat(avatar)
: avatar;
in:
// Soluzione con Template Literal (Più leggibile)
const userAvatarUrl = avatar.startsWith("/user_avatar/")
? `${avatarUrl}${avatar}`
: avatar;
Cosa Ho Imparato
Async/Await e try/catch:
- Dichiarare una funzione con
asyncpermette di usareawaital suo interno per "mettere in pausa" l’esecuzione finché una Promise non si risolve, pur restando non bloccante a livello di thread principale. await fetch(url)e poiawait res.json()sostituiscono la catena di.then(), rendendo il flusso molto più simile al codice sincrono e quindi più leggibile.- Il blocco
try { ... } catch (err) { ... }diventa il nuovo modo idiomatico di gestire errori asincroni, unificando in un solo punto errori di rete, di parsing e logici.
Destructuring su oggetti annidati:
- La destrutturazione multipla
const { topic_list, users } = data; const { topics } = topic_list;aiuta a navigare risposte JSON complesse senza accedere continuamente con notazione a punti annidata. - Destrutturare direttamente gli oggetti
topicinshowLatestPostsrende più chiaro quali campi vengono effettivamente usati e riduce rumore visivo.const { id, title, views, posts_count, slug, posters, category_id, bumped_at } = item;
Metodi Array per trasformare dati in UI:
.map()è stato centrale per trasformare l’arraytopicsin righe HTML della tabella, restituendo una stringa conjoin("")da assegnare una sola volta ainnerHTML..find()è stato usato per incrociarepostersconuserse ricavare i dati necessari per gli avatar, mostrando come sia possibile combinare più collezioni in un unico passaggio..join("")dopo.map()evita concatenazioni ripetute con+=e risulta più efficiente e leggibile.
Funzioni helper pure per formattare i dati:
timeAgo(bumped_at)incapsula la logica di differenza temporale in minuti, ore e giorni, trasformando timestamp grezzi in stringhe leggibili come10m ago,3h agoo2d ago.viewCount(views)introduce la logica di formattazione compatta (1500→1k), mostrando come separare responsabilità di presentazione dal resto del codice.- Queste funzioni pure sono facili da testare e riutilizzare in contesti diversi.
Uso pratico di Map e oggetti di configurazione:
- L’oggetto
allCategoriesfunziona come una piccola mappa di configurazione per tradurrecategory_idincategoryeclassName, dimostrando come centralizzare le regole di mapping fra dati API e UI. - La funzione
forumCategory(category_id)genera il markup del link di categoria combinando questi metadati in modo dinamico, invece di avere logica sparsa nel template.
Template literal per HTML dinamico:
- L’uso di template literal multi-linea per generare
<tr>...</tr>e<a>...</a>ha reso naturale interpolare variabili JavaScript dentro l’HTML. - Questo approccio avvicina molto al modo di pensare di librerie come React, dove si mappa direttamente da dati strutturati a componenti UI.
Pattern di rendering “One-shot”:
- Assegnare
postsContainer.innerHTML = topics.map(...).join("");in un unico passaggio evita continui accessi e ricalcoli del layout rispetto ad aggiornare il DOM dentro un loop. - Questo pattern è stato un passo importante verso un modo più dichiarativo di ragionare sul rendering, piuttosto che imperativo.
Prossimo Progetto: Costruire un RPG Creature Search App (Certification Project)