rinomina esercizi js

This commit is contained in:
2026-02-12 18:36:35 +01:00
parent f0b6b85b36
commit 60878cf770
150 changed files with 16 additions and 0 deletions

View File

@@ -0,0 +1,36 @@
<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Esercizio 1 - GET Singolo Utente</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<a href="../index.html" style="position: absolute; top: 20px; left: 20px; text-decoration: none; color: #555; font-weight: bold;">← Dashboard</a>
<div class="app-container">
<h1>🔍 GET Singolo Utente</h1>
<p class="subtitle">Recupera un utente dal server</p>
<!-- SEZIONE CONFIGURAZIONE -->
<div class="config-box">
<label for="userId">ID Utente (1-40):</label>
<div class="input-group">
<input type="number" id="userId" min="1" max="40" value="1">
<button id="btnFetch">Carica</button>
</div>
</div>
<!-- LOADING SPINNER -->
<div id="loading" class="loading nascosto">
⏳ Caricamento...
</div>
<!-- RISULTATO -->
<div id="result" class="result-container"></div>
</div>
<script src="script.js"></script>
</body>
</html>

View File

@@ -0,0 +1,98 @@
// ⚠️ COMPILARE E CONTROLLARE PRIMA DI INIZIARE ⚠️
const BASE_URL = 'http://localhost:5000/api';
const userId = document.querySelector('#userId');
const loading = document.querySelector('#loading');
const result = document.querySelector('#result');
const btnFetch = document.querySelector('#btnFetch');
/**
* FUNZIONE: Crea utente card
*
* Crea la card completa dell'utente e la inserisce nell'elemento result
* Funzione già fatta - non modificare
* L'oggetto user ha questa struttura:
* {
* id: number,
* nome: string,
* cognome: string,
* email: string,
* avatar: string (url),
* dataNascita: string (formato YYYY-MM-DD),
* comune: string,
* attivo: boolean
* }
*/
function creaCardUtente(user) {
result.innerHTML = `
<div class="user-card">
<div class="card-header">
<img src="${user.avatar}" alt="Avatar" class="avatar">
<div class="user-info">
<h2>${user.nome} ${user.cognome}</h2>
<p class="email">${user.email}</p>
<p class="comune">${user.comune}</p>
</div>
</div>
<div class="card-body">
<div class="detail">
<strong>Data Nascita:</strong>
<span>${user.dataNascita}</span>
</div>
<div class="detail">
<strong class="status ${user.attivo ? 'attivo' : 'inattivo'}">
Status:
</strong>
<span>${user.attivo ? 'Attivo' : 'Inattivo'}</span>
</div>
</div>
</div>
`;
}
/**
* FUNZIONE: Gestione errori
*
* Mostra un messaggio di errore nell'elemento result
* e logga l'errore in console (per debug)
* Funzione già fatta - non modificare
*/
function handleError(message) {
console.error('Errore:', message);
result.innerHTML = `
<div class="error">
<strong>❌ ${message}</strong>
</div>
`;
}
/**
* FUNZIONE: Fetch singolo utente
*
* Questa funzione deve recuperare l'ID utente dall'input,
* fare una chiamata GET a BASE_URL + "/users/" + id e mostrare i dati
*
* Passi:
* 1. Leggi l'ID utente dall'input
* 2. Controlla che l'ID sia valido, ovvero un numero tra 1 e 40
* In caso contrario, mostra un messaggio di errore (usa handleError()) e return
* 3. Mostra lo spinner di caricamento (rimuovi la classe nascosto)
* 4. Apri un blocco try/catch
* 5. Fai una fetch GET a /users/{id}
* 6. Se la risposta non è OK, usa handleError() per mostrare un messaggio e return
* 7. Converti la risposta in JSON
* 8. Mostra i dati dell'utente chiamando createUserCard(user)
* 9. Nascondi lo spinner di caricamento (aggiungi la classe nascosto)
*/
async function fetchUser() {
// TODO Rimuovi questa riga e completa la funzione
handleError('Codice non implementato - Completa la funzione fetchUser()');
}
// COLLEGA IL BOTTONE AL CLICK
btnFetch.addEventListener('click', fetchUser);

View File

@@ -0,0 +1,264 @@
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
margin: 0;
padding: 20px;
}
.app-container {
background: white;
width: 100%;
max-width: 450px;
padding: 30px;
border-radius: 15px;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
}
h1 {
color: #333;
text-align: center;
margin: 0 0 10px 0;
font-size: 1.8rem;
}
.subtitle {
color: #666;
text-align: center;
margin: 0 0 30px 0;
font-size: 1rem;
}
.config-box {
background: #f9f9f9;
padding: 20px;
border-radius: 10px;
margin-bottom: 25px;
}
.config-box label {
display: block;
color: #555;
font-weight: 600;
margin-bottom: 10px;
font-size: 0.95rem;
}
.input-group {
display: flex;
gap: 10px;
}
.input-group input {
flex: 1;
padding: 10px;
border: 2px solid #ddd;
border-radius: 6px;
font-size: 15px;
font-family: inherit;
}
.input-group input:focus {
outline: none;
border-color: #007bff;
box-shadow: 0 0 5px rgba(0, 123, 255, 0.2);
}
.input-group button {
padding: 10px 20px;
background: #007bff;
color: white;
border: none;
border-radius: 6px;
font-weight: bold;
cursor: pointer;
transition: background 0.2s;
font-size: 15px;
}
.input-group button:hover {
background: #0056b3;
}
.loading {
text-align: center;
color: #666;
font-size: 1.1rem;
padding: 30px;
animation: pulse 1s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.6; }
}
.result-container {
margin-bottom: 30px;
}
.error {
background: #fee;
color: #c00;
padding: 15px;
border-radius: 6px;
border-left: 4px solid #c00;
font-weight: 500;
}
.user-card {
background: #f9f9f9;
border-radius: 10px;
overflow: hidden;
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.1);
animation: slideIn 0.3s ease-out;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.card-header {
background: linear-gradient(135deg, #007bff 0%, #0056b3 100%);
color: white;
padding: 20px;
display: flex;
gap: 15px;
align-items: center;
}
.avatar {
width: 70px;
height: 70px;
border-radius: 50%;
border: 3px solid white;
object-fit: cover;
flex-shrink: 0;
}
.user-info {
flex: 1;
}
.user-info h2 {
font-size: 1.3em;
margin: 0 0 5px 0;
}
.user-info p {
margin: 3px 0;
opacity: 0.95;
font-size: 0.9em;
}
.card-body {
padding: 20px;
}
.detail {
display: flex;
justify-content: space-between;
padding: 10px 0;
border-bottom: 1px solid #eee;
}
.detail:last-child {
border-bottom: none;
}
.detail strong {
color: #333;
}
.detail span {
color: #666;
text-align: right;
}
.status {
display: inline-block;
padding: 4px 10px;
border-radius: 15px;
font-size: 0.85em;
}
.status.attivo {
background: #d4edda;
color: #155724;
}
.status.inattivo {
background: #f8d7da;
color: #721c24;
}
.instructions-box {
background: #f9f9f9;
padding: 20px;
border-radius: 10px;
border-left: 4px solid #007bff;
margin-top: 30px;
}
.instructions-box h2 {
color: #333;
margin: 0 0 15px 0;
font-size: 1.1rem;
}
.instructions-box ol {
margin-left: 20px;
color: #555;
line-height: 1.7;
}
.instructions-box li {
margin-bottom: 10px;
}
.instructions-box code {
background: white;
padding: 2px 5px;
border-radius: 3px;
font-family: 'Courier New', monospace;
color: #d63384;
font-size: 0.9em;
}
.hint {
background: white;
border: 1px solid #ddd;
padding: 15px;
border-radius: 6px;
margin-top: 15px;
}
.hint strong {
color: #007bff;
display: block;
margin-bottom: 10px;
}
.hint pre {
background: #f0f0f0;
padding: 10px;
border-radius: 4px;
overflow-x: auto;
font-size: 0.85em;
color: #333;
margin: 0;
}
.nascosto {
display: none;
}

View File

@@ -0,0 +1,38 @@
<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Esercizio 2 - GET Lista Utenti</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<a href="../index.html" style="position: absolute; top: 20px; left: 20px; text-decoration: none; color: #555; font-weight: bold;">← Dashboard</a>
<div class="app-container">
<h1>👥 GET Lista Utenti</h1>
<p class="subtitle">Crea card dinamiche da un array</p>
<!-- BOTTONE CARICA -->
<div style="text-align: center; margin-bottom: 25px;">
<button id="btnLoadUsers" style="padding: 10px 25px; background: #007bff; color: white; border: none; border-radius: 6px; font-weight: bold; cursor: pointer; font-size: 15px;">Carica Utenti</button>
</div>
<!-- LOADING SPINNER -->
<div id="loading" class="loading nascosto">
⏳ Caricamento...
</div>
<!-- COUNTER -->
<div id="counter" style="display: none; text-align: center; color: #666; margin-bottom: 20px; font-weight: bold;"></div>
<!-- GRIGLIA UTENTI -->
<div id="users-container" class="users-grid"></div>
<!-- RISULTATO -->
<div id="result" class=""></div>
</div>
<script src="script.js"></script>
</body>
</html>

View File

@@ -0,0 +1,91 @@
// ⚠️ COMPILARE E CONTROLLARE PRIMA DI INIZIARE ⚠️
const BASE_URL = 'http://localhost:5000/api';
const loading = document.querySelector('#loading');
const counter = document.querySelector('#counter');
const result = document.querySelector('#result');
const btnFetch = document.querySelector('#btnLoadUsers');
/**
* FUNZIONE che crea la card HTML per un singolo utente
*
* Passi:
* 1. Crea un template literal con la struttura HTML della card
* 2. Usa i dati dell'oggetto user per popolare i campi
* 3. La struttura della card deve essere simile a questa:
* <div class="user-card">
* <img src="avatar_url" alt="Avatar" class="card-avatar">
* <div class="card-content">
* <h3>Nome Cognome</h3>
* <p class="email">📧 email</p>
* <p class="location">📍 comune</p>
* <div class="status attivo/inattivo"> Attivo/Inattivo </div>
* </div>
* </div>
*/
function creaCardUtente(user) {
return ``;
}
/**
* FUNZIONE: Gestione errori
*
* Mostra un messaggio di errore nell'elemento result
* e logga l'errore in console (per debug)
* Funzione già fatta - non modificare
*/
function handleError(message) {
result.innerHTML = '';
let div = document.createElement('div');
div.className = 'error';
let strong = document.createElement('strong');
strong.textContent = `${message}`;
div.appendChild(strong);
result.appendChild(div);
console.error('Errore:', message);
}
/**
* FUNZIONE: Fetch lista utenti
*
* Passi:
* 1. Mostra lo spinner di caricamento (rimuovi la classe nascosto)
* 2. Apri un blocco try/catch
* 3. Fai una fetch GET a /users
* 4. Se la risposta non è OK, usa handleError() per mostrare un messaggio e return
* 5. Converti la risposta da stringa JSON a oggetto JavaScript
* 6. Mostra i dati degli utenti chiamando mostraUtenti(users)
* 7. Nascondi lo spinner di caricamento (aggiungi la classe nascosto)
*/
async function fetchAllUsers() {
// TODO Rimuovi questa riga e completa la funzione
handleError('Codice non implementato - Completa la funzione fetchAllUsers()');
}
/**
* Visualizza gli utenti in una griglia di card
* Funzione già fatta - non modificare
*/
function mostraUtenti(users) {
if (!Array.isArray(users) || users.length === 0) {
handleError('Nessun utente trovato');
return;
}
// CREA CARD PER OGNI UTENTE
const cardsHTML = users.map(user => creaCardUtente(user)).join('');
result.innerHTML = cardsHTML;
// MOSTRA CONTATORE
counter.innerHTML = `📊 Totale: <strong>${users.length} utenti</strong>`;
counter.style.display = 'block';
}
// COLLEGA IL BOTTONE
btnFetch.addEventListener('click', fetchAllUsers);

View File

@@ -0,0 +1,193 @@
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
display: flex;
justify-content: center;
align-items: flex-start;
min-height: 100vh;
margin: 0;
padding: 20px;
}
.app-container {
background: white;
width: 100%;
max-width: 600px;
padding: 30px;
border-radius: 15px;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
margin-top: 20px;
}
h1 {
color: #333;
text-align: center;
margin: 0 0 10px 0;
font-size: 1.8rem;
}
.subtitle {
color: #666;
text-align: center;
margin: 0 0 30px 0;
font-size: 1rem;
}
.loading {
text-align: center;
color: #666;
font-size: 1rem;
padding: 30px;
animation: pulse 1s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.6; }
}
.users-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
gap: 15px;
margin-bottom: 30px;
}
.user-card {
background: #f9f9f9;
border-radius: 10px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
transition: transform 0.2s, box-shadow 0.2s;
animation: slideIn 0.3s ease-out;
}
.user-card:hover {
transform: translateY(-3px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.card-avatar {
width: 100%;
height: 120px;
object-fit: cover;
}
.card-content {
padding: 12px;
}
.card-content h3 {
font-size: 0.95rem;
color: #333;
margin: 0 0 5px 0;
}
.card-content p {
color: #666;
font-size: 0.8rem;
margin: 3px 0;
}
.email {
font-weight: 500;
}
.status {
display: inline-block;
padding: 3px 8px;
border-radius: 12px;
font-size: 0.75rem;
font-weight: 600;
margin-top: 8px;
}
.status.attivo {
background: #d4edda;
color: #155724;
}
.status.inattivo {
background: #f8d7da;
color: #721c24;
}
.error {
background: #fee;
color: #c00;
padding: 15px;
border-radius: 6px;
border-left: 4px solid #c00;
font-weight: 500;
}
.instructions-box {
background: #f9f9f9;
padding: 20px;
border-radius: 10px;
border-left: 4px solid #007bff;
}
.instructions-box h2 {
color: #333;
margin: 0 0 15px 0;
font-size: 1.1rem;
}
.instructions-box ol {
margin-left: 20px;
color: #555;
line-height: 1.7;
}
.instructions-box li {
margin-bottom: 10px;
}
.instructions-box code {
background: white;
padding: 2px 5px;
border-radius: 3px;
font-family: 'Courier New', monospace;
color: #d63384;
font-size: 0.9em;
}
.hint {
background: white;
border: 1px solid #ddd;
padding: 15px;
border-radius: 6px;
margin-top: 15px;
}
.hint strong {
color: #007bff;
display: block;
margin-bottom: 10px;
}
.hint pre {
background: #f0f0f0;
padding: 10px;
border-radius: 4px;
overflow-x: auto;
font-size: 0.8em;
color: #333;
margin: 0;
}
.nascosto {
display: none;
}

View File

@@ -0,0 +1,39 @@
<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Esercizio 3 - Utente + Post</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<a href="../index.html" style="position: absolute; top: 20px; left: 20px; text-decoration: none; color: #555; font-weight: bold;">← Dashboard</a>
<div class="app-container">
<h1>📝 Utente + Post</h1>
<p class="subtitle">Fetch multipli e relazioni dati</p>
<!-- SEZIONE CONFIGURAZIONE -->
<div class="config-box">
<label for="userId">ID Utente (1-40):</label>
<div class="input-group">
<input type="number" id="userId" min="1" max="40" value="1">
<button id="btnFetch">Carica</button>
</div>
</div>
<!-- LOADING -->
<div id="loading" class="loading nascosto">
⏳ Caricamento...
</div>
<!-- PROFILO UTENTE -->
<div id="userProfile" class="user-profile"></div>
<!-- POST -->
<div id="postsContainer" class="posts-container"></div>
</div>
<script src="script.js"></script>
</body>
</html>

View File

@@ -0,0 +1,131 @@
// ⚠️ COMPILARE E CONTROLLARE PRIMA DI INIZIARE ⚠️
const BASE_URL = 'http://localhost:5000/api';
const userId = document.getElementById('userId');
const loading = document.getElementById('loading');
const userProfile = document.getElementById('userProfile');
const postsContainer = document.getElementById('postsContainer');
const btnFetch = document.getElementById('btnFetch');
/**
* FUNZIONE: Crea un utente card
*
* Crea la card completa dell'utente e la inserisce nell'elemento userProfile
* L'oggetto user ha questa struttura:
* {
* id: number,
* nome: string,
* cognome: string,
* email: string,
* avatar: string (url),
* dataNascita: string (formato YYYY-MM-DD),
* comune: string,
* attivo: boolean
* }
*/
function creaCardUtente(user) {
userProfile.innerHTML = `
<div class="user-card">
<div class="card-header">
<img src="${user.avatar}" alt="Avatar" class="avatar">
<div class="user-info">
<h2>${user.nome} ${user.cognome}</h2>
<p class="email">📧 ${user.email}</p>
<p class="location">📍 ${user.comune}</p>
</div>
</div>
</div>
`;
}
/**
* FUNZIONE: Crea le card dei post
*
* Visualizza i post dell'utente nel contenitore postsContainer
* Se non ci sono post, mostra un messaggio di vuoto
* posts è un array di oggetti con questa struttura:
* {
* id: number,
* userId: number,
* titolo: string,
* contenuto: string,
* data: string (formato YYYY-MM-DD),
* likes: number
* }
*/
function creaCardPosts(postList) {
let allPosts = postList.map((post) => `
<div class="post-card">
<div class="post-header">
<h3>${post.titolo}</h3>
<span class="post-date">📅 ${post.data}</span>
</div>
<p class="post-content">${post.contenuto}</p>
<div class="post-footer">
<span class="likes">❤️ ${post.likes} likes</span>
</div>
</div>
`).join('')
if (postList.length === 0) {
allPosts = `
<div class="empty">
Questo utente non ha scritto nessun post
</div>
`;
}
postsContainer.innerHTML = `
<div class="posts-section">
<h2>📄 Post (${postList.length})</h2>
${allPosts}
</div>
`;
}
/**
* FUNZIONE: Gestione errori
*
* Mostra un messaggio di errore e logga in console
*/
function handleError(message) {
userProfile.innerHTML = `
<div class="error">
<strong>❌ ${message}</strong>
</div>
`;
console.error('Errore:', message);
}
/**
* ESERCIZIO 3: Recupera un utente E tutti i suoi post
*
* Devi completare questa funzione:
* 1. Leggi l'ID dell'utente
* 2. Valida che sia un numero tra 1 e 40
* 3. Mostra lo spinner di caricamento
* 4. Apri un blocco try/catch
* 5. Fai DUE fetch:
* - GET /users/{id}
* - GET /posts
* 6. Filtra i post per trovare solo quelli di questo utente (usando userId)
* - Il filtro va fatto in JS, ma puoi anche usare un endpoint come /posts?userId={id}
* 7. Mostra i risultati chiamando prima creaCardUtente(user) e poi creaCardPosts(posts)
* 8. Gestisci gli errori con handleError()
*/
async function fetchUserAndPosts() {
// TODO Rimuovi questa riga e completa la funzione
handleError('Codice non implementato - Completa la funzione fetchUserAndPosts()');
}
// COLLEGA IL BOTTONE
btnFetch.addEventListener('click', fetchUserAndPosts);
// PERMETTI ENTER
userId.addEventListener('keypress', (e) => {
if (e.key === 'Enter') fetchUserAndPosts();
});

View File

@@ -0,0 +1,253 @@
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
display: flex;
justify-content: center;
align-items: flex-start;
min-height: 100vh;
margin: 0;
padding: 20px;
}
.app-container {
background: white;
width: 100%;
max-width: 600px;
padding: 30px;
border-radius: 15px;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
margin-top: 20px;
}
h1 {
color: #333;
text-align: center;
margin: 0 0 10px 0;
font-size: 1.8rem;
}
.subtitle {
color: #666;
text-align: center;
margin: 0 0 30px 0;
font-size: 1rem;
}
.config-box {
background: #f9f9f9;
padding: 20px;
border-radius: 10px;
margin-bottom: 25px;
}
.config-box label {
display: block;
color: #555;
font-weight: 600;
margin-bottom: 10px;
font-size: 0.95rem;
}
.input-group {
display: flex;
gap: 10px;
}
.input-group input {
flex: 1;
padding: 10px;
border: 2px solid #ddd;
border-radius: 6px;
font-size: 15px;
}
.input-group input:focus {
outline: none;
border-color: #007bff;
box-shadow: 0 0 5px rgba(0, 123, 255, 0.2);
}
.input-group button {
padding: 10px 20px;
background: #007bff;
color: white;
border: none;
border-radius: 6px;
font-weight: bold;
cursor: pointer;
transition: background 0.2s;
}
.input-group button:hover {
background: #0056b3;
}
.loading {
text-align: center;
color: #666;
font-size: 1rem;
padding: 30px;
animation: pulse 1s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.6; }
}
.error {
background: #fee;
color: #c00;
padding: 15px;
border-radius: 6px;
border-left: 4px solid #c00;
}
.user-card {
background: #f9f9f9;
border-radius: 10px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
margin-bottom: 20px;
}
.card-header {
background: linear-gradient(135deg, #007bff 0%, #0056b3 100%);
color: white;
padding: 20px;
display: flex;
gap: 15px;
}
.avatar {
width: 70px;
height: 70px;
border-radius: 50%;
border: 2px solid white;
object-fit: cover;
flex-shrink: 0;
}
.user-info h2 {
margin: 0 0 5px 0;
font-size: 1.2rem;
}
.user-info p {
margin: 3px 0;
opacity: 0.95;
font-size: 0.9rem;
}
.posts-section h2 {
color: #333;
margin: 25px 0 15px 0;
font-size: 1.2rem;
border-bottom: 2px solid #007bff;
padding-bottom: 10px;
}
.post-card {
background: #f9f9f9;
padding: 15px;
border-radius: 8px;
border-left: 4px solid #007bff;
margin-bottom: 12px;
}
.post-header {
display: flex;
justify-content: space-between;
margin-bottom: 10px;
}
.post-header h3 {
color: #333;
margin: 0;
font-size: 1rem;
}
.post-date {
color: #999;
font-size: 0.8rem;
}
.post-content {
color: #555;
line-height: 1.5;
font-size: 0.95rem;
margin-bottom: 10px;
}
.post-footer {
color: #d63384;
font-weight: 600;
font-size: 0.85rem;
}
.empty {
color: #999;
text-align: center;
padding: 20px;
}
.instructions-box {
background: #f9f9f9;
padding: 20px;
border-radius: 10px;
border-left: 4px solid #007bff;
margin-top: 30px;
}
.instructions-box h2 {
color: #333;
margin: 0 0 15px 0;
font-size: 1.1rem;
}
.instructions-box ol {
margin-left: 20px;
color: #555;
line-height: 1.7;
}
.instructions-box li {
margin-bottom: 10px;
}
.instructions-box code {
background: white;
padding: 2px 5px;
border-radius: 3px;
font-family: 'Courier New', monospace;
color: #d63384;
font-size: 0.9em;
}
.hint {
background: white;
border: 1px solid #ddd;
padding: 15px;
border-radius: 6px;
margin-top: 15px;
}
.hint strong {
color: #007bff;
display: block;
margin-bottom: 10px;
}
.hint pre {
background: #f0f0f0;
padding: 10px;
border-radius: 4px;
overflow-x: auto;
font-size: 0.85em;
color: #333;
margin: 0;
}
.nascosto {
display: none;
}

View File

@@ -0,0 +1,36 @@
<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Esercizio 4 - Ricerca Post</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<a href="../index.html" style="position: absolute; top: 20px; left: 20px; text-decoration: none; color: #555; font-weight: bold;">← Dashboard</a>
<div class="app-container">
<h1>🔍 Ricerca Post</h1>
<p class="subtitle">Fetch multipli con filtri incrociati</p>
<!-- SEZIONE CONFIGURAZIONE -->
<div class="config-box">
<label for="keyword">Ricerca per parola chiave:</label>
<div class="input-group">
<input type="text" id="keyword" placeholder="Es: JavaScript, CSS, React...">
<button id="btnSearch">Cerca</button>
</div>
</div>
<!-- LOADING -->
<div id="loading" class="loading nascosto">
⏳ Caricamento...
</div>
<!-- RISULTATI -->
<div id="results" class="results-container"></div>
</div>
<script src="script.js"></script>
</body>
</html>

View File

@@ -0,0 +1,83 @@
// ⚠️ COMPILARE E CONTROLLARE PRIMA DI INIZIARE ⚠️
const BASE_URL = 'http://localhost:5000/api';
const keyword = document.querySelector('#keyword');
const loading = document.querySelector('#loading');
const results = document.querySelector('#results');
const btnSearch = document.querySelector('#btnSearch');
/**
* FUNZIONE: Crea la card HTML per un singolo post con autore a fianco
*
* Funzione già fatta - non modificare
*/
function creaElementoPost(post, autore) {
results.innerHTML += `
<div class="post-row">
<div class="row-author">
<img src="${autore.avatar}" alt="Avatar" class="row-avatar">
<div class="row-author-info">
<strong>${autore.nome} ${autore.cognome}</strong>
<small>${autore.email}</small>
</div>
</div>
<div class="row-post">
<div class="post-title">${post.titolo}</div>
<div class="post-preview">${post.contenuto}</div>
<div class="post-meta">
<span>📅 ${post.data}</span>
<span>❤️ ${post.likes} likes</span>
</div>
</div>
</div>
`;
}
/**
* FUNZIONE: Gestione errori
*
* Mostra un messaggio di errore e logga in console
* Funzione già fatta - non modificare
*/
function handleError(message) {
results.innerHTML = `
<div class="error">
<strong>❌ ${message}</strong>
</div>
`;
console.error('Errore:', message);
}
/**
* ESERCIZIO 5: Ricerca Post con Autori
*
* Devi completare questa funzione:
* 1. Leggi la parola chiave dall'input
* 2. Svuota il contenuto dei risultati (results.innerHTML = '')
* 3. Valida che non sia vuota (usa trim)
* 4. Mostra lo spinner di caricamento
* 5. Apri un blocco try/catch
* 6. Fai una GET a /posts
* 7. Filtra i post dove titolo O contenuto contiene la keyword
* (ricorda minuscole per la ricerca)
* 8. Se nessun post trovato, usa handleError() e return
* 9. Per ogni post trovato:
* - Prendi l'id autore e fai una GET a /users?id=[id]
* - Crea un nuovo elemento della lista dei risultati con creaElementoPost(post, autore)
* 10. Gestisci gli errori con handleError()
* 11. Nascondi lo spinner di caricamento
*/
async function fetchPostsByKeyword() {
//TODO
}
// COLLEGA IL BOTTONE
btnSearch.addEventListener('click', fetchPostsByKeyword);
// PERMETTI ENTER
keyword.addEventListener('keypress', (e) => {
if (e.key === 'Enter') fetchPostsByKeyword();
});

View File

@@ -0,0 +1,268 @@
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
display: flex;
justify-content: center;
align-items: flex-start;
min-height: 100vh;
margin: 0;
padding: 20px;
}
.app-container {
background: white;
width: 100%;
max-width: 700px;
padding: 30px;
border-radius: 15px;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
margin-top: 20px;
}
h1 {
color: #333;
text-align: center;
margin: 0 0 10px 0;
font-size: 1.8rem;
}
.subtitle {
color: #666;
text-align: center;
margin: 0 0 30px 0;
font-size: 1rem;
}
.config-box {
background: #f9f9f9;
padding: 20px;
border-radius: 10px;
margin-bottom: 25px;
}
.config-box label {
display: block;
color: #333;
font-weight: 600;
margin-bottom: 12px;
}
.input-group {
display: flex;
gap: 10px;
}
.input-group input,
.input-group button {
padding: 12px 15px;
border-radius: 6px;
border: 1px solid #ddd;
font-size: 1rem;
}
.input-group input {
flex: 1;
border-color: #ddd;
}
.input-group input:focus {
outline: none;
border-color: #007bff;
box-shadow: 0 0 5px rgba(0, 123, 255, 0.2);
}
.input-group button {
background: #007bff;
color: white;
border: none;
font-weight: bold;
cursor: pointer;
padding: 12px 25px;
}
.input-group button:hover {
background: #0056b3;
}
.loading {
text-align: center;
color: #666;
font-size: 1rem;
padding: 30px;
animation: pulse 1s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.6; }
}
.results-container {
display: flex;
flex-direction: column;
gap: 15px;
}
.post-row {
background: white;
border-radius: 10px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
display: flex;
animation: slideIn 0.3s ease-out;
min-height: 100px;
border: 1px solid #f0f0f0;
}
.row-author {
background: #f5f5f5;
color: #333;
padding: 15px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 130px;
flex-shrink: 0;
gap: 8px;
border-right: 1px solid #e0e0e0;
}
.row-avatar {
width: 48px;
height: 48px;
border-radius: 50%;
object-fit: cover;
border: 2px solid #ddd;
}
.row-author-info {
text-align: center;
width: 100%;
}
.row-author-info strong {
display: block;
font-size: 0.85rem;
margin-bottom: 3px;
word-break: break-word;
color: #333;
font-weight: 600;
}
.row-author-info small {
font-size: 0.7rem;
color: #999;
word-break: break-word;
}
.row-post {
flex: 1;
padding: 15px;
display: flex;
flex-direction: column;
justify-content: center;
}
.author-section {
background: #f9f9f9;
border-radius: 10px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
animation: slideIn 0.3s ease-out;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.author-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 20px;
display: flex;
align-items: center;
gap: 15px;
}
.author-avatar {
width: 60px;
height: 60px;
border-radius: 50%;
object-fit: cover;
border: 3px solid white;
}
.author-info h3 {
margin: 0 0 5px 0;
font-size: 1.2rem;
}
.author-info p {
margin: 0;
font-size: 0.9rem;
opacity: 0.9;
}
.posts-list {
padding: 15px;
}
.post-item {
background: white;
padding: 12px;
margin-bottom: 10px;
border-radius: 6px;
border-left: 3px solid #667eea;
}
.post-item:last-child {
margin-bottom: 0;
}
.post-title {
font-weight: 600;
color: #333;
margin: 0 0 5px 0;
}
.post-preview {
color: #666;
font-size: 0.9rem;
margin: 0 0 8px 0;
line-height: 1.4;
}
.post-meta {
display: flex;
gap: 15px;
font-size: 0.8rem;
color: #999;
}
.empty {
text-align: center;
color: #999;
padding: 40px 20px;
font-size: 1.1rem;
}
.error {
background: #fee;
color: #c00;
padding: 15px;
border-radius: 6px;
border-left: 4px solid #c00;
font-weight: 500;
}
.nascosto {
display: none;
}

View File

@@ -0,0 +1,48 @@
<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Esercizio 4 - Todo App CRUD</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<a href="../index.html" style="position: absolute; top: 20px; left: 20px; text-decoration: none; color: #555; font-weight: bold;">← Dashboard</a>
<div class="app-container">
<h1>✅ Todo App CRUD</h1>
<p class="subtitle">GET, POST, PUT, DELETE - App completa</p>
<!-- SEZIONE SELEZIONE UTENTE -->
<div class="config-box">
<h2>👤 Seleziona Utente</h2>
<div class="input-group">
<input type="number" id="userId" min="1" max="40" value="1" placeholder="ID Utente (1-40)">
<button id="btnLoadTodos">Carica TODO</button>
</div>
</div>
<!-- SEZIONE AGGIUNTA TODO -->
<div id="addTodoSection" class="add-todo-box nascosto">
<h2> Aggiungi Nuovo TODO</h2>
<div class="input-group">
<input type="text" id="todoInput" placeholder="Scrivi un nuovo TODO...">
<button id="btnAddTodo">Aggiungi</button>
</div>
</div>
<!-- LOADING -->
<div id="loading" class="loading nascosto">
⏳ Caricamento...
</div>
<!-- COUNTER -->
<div id="counter" class="counter nascosto"></div>
<!-- LISTA TODO -->
<div id="todosContainer" class="todos-container"></div>
</div>
<script src="script.js"></script>
</body>
</html>

View File

@@ -0,0 +1,176 @@
// ⚠️ COMPILARE E CONTROLLARE PRIMA DI INIZIARE ⚠️
const BASE_URL = 'http://localhost:5000/api';
const userId = document.querySelector('#userId');
const todoInput = document.querySelector('#todoInput');
const btnAddTodo = document.querySelector('#btnAddTodo');
const addTodoSection = document.querySelector('#addTodoSection');
const btnLoadTodos = document.querySelector('#btnLoadTodos');
const todosContainer = document.querySelector('#todosContainer');
const loading = document.querySelector('#loading');
const counter = document.querySelector('#counter');
// ===== VARIABILE DI STATO =====
// Tiene traccia dell'ID utente attualmente caricato
// Viene usata per sapere a quale utente associare i nuovi TODO
// DA COMPLETARE: Carica questa variabile dal localStorage (se presente)
let currentUserId = null;
/**
* FUNZIONE: Crea la card HTML per tutti i TODO passati come parametro
*/
function creaCardTodos(todoList) {
allTodos = todoList.map((todo) => `
<div class="todo-item ${todo.completato ? 'completed' : ''}">
<div class="todo-checkbox">
<input type="checkbox"
${todo.completato ? 'checked' : ''}
onchange="toggleTodo(${todo.id}, ${todo.completato})">
</div>
<div class="todo-content">
<div class="todo-title">${todo.titolo}</div>
<div class="todo-id">ID: ${todo.id}</div>
</div>
<button class="btn-delete" onclick="deleteTodo(${todo.id})">🗑️ Elimina</button>
</div>
`).join('');
todosContainer.innerHTML = allTodos;
}
/**
* FUNZIONE: Gestione errori
*
* Mostra un messaggio di errore e logga in console
*/
function handleError(message) {
todosContainer.innerHTML = `
<div class="error">
<strong>❌ ${message}</strong>
</div>
`;
console.error('Errore:', message);
}
/**
* ESERCIZIO 4: Todo App CRUD Completa
*
* Devi implementare 4 funzioni:
* 1. fetchTodosUtente() - GET /todos?userId={id} (READ)
* 2. addTodo() - POST /todos (CREATE)
* 3. toggleTodo() - PUT /todos/{id} (UPDATE)
* 4. deleteTodo() - DELETE /todos/{id} (DELETE)
*/
/**
* FUNZIONE 1⃣: Carica TODO (GET)
*
* Passi:
* 1. Leggi l'ID utente dall'input
* 2. Valida che sia un numero tra 1 e 40
* 3. Mostra lo spinner di caricamento
* 4. Apri un blocco try/catch
* 5. Fai una GET a /todos?userId={id}
* 6. Se non OK, mostra errore e return
* 7. Converti la risposta in JSON
* 8. Chiama displayTodos() per visualizzare
* 9. Nascondi lo spinner e mostra addTodoSection
* 10. Salva l'ID utente in currentUserId (e localStorage)
*/
async function fetchTodosUtente() {
// TODO Rimuovi questa riga e completa la funzione
handleError('Codice non implementato - Completa la funzione fetchTodosUtente()');
}
/**
* FUNZIONE 2⃣: Aggiungi TODO (POST)
*
* Passi:
* 1. Verifica che un utente sia stato caricato (currentUserId)
* 2. Leggi il testo dal campo di input del nuovo TODO
* 3. Valida con trim che non sia vuoto
* 4. Mostra lo spinner di caricamento
* 5. Apri un blocco try/catch
* 6. Fai una POST a /todos con body: {userId, titolo, completato: false}
* 7. Se non OK, mostra errore
* 8. Se OK, svuota l'input
* 9. Ricarica la lista chiamando fetchTodosUtente()
*/
async function addTodo() {
}
/**
* FUNZIONE 3⃣: Modifica TODO (PUT)
*
* Passi:
* 1. Ricevi id e currentCompleted come parametri
* 2. Fai una PUT a /todos/{id} con body: {completato: !currentCompleted}
* 3. Se non OK, mostra errore
* 4. Se OK, ricarica la lista chiamando fetchTodosUtente()
*/
async function toggleTodo(id, currentCompleted) {
}
/**
* FUNZIONE 4⃣: Elimina TODO (DELETE)
*
* Passi:
* 1. Chiedi conferma con confirm("Sicuro?")
* 2. Se l'utente cancella, return
* 3. Fai una DELETE a /todos/{id}
* 4. Se non OK, mostra errore
* 5. Se OK, ricarica la lista chiamando fetchTodosUtente()
*/
async function deleteTodo(id) {
}
/**
* FUNZIONE: Visualizza i TODO
* Funzione già fatta - non modificare
*/
function displayTodos(todos) {
if (!Array.isArray(todos) || todos.length === 0) {
todosContainer.innerHTML = '<div class="empty">Nessun TODO. Creane uno!</div>';
counter.classList.add('nascosto');
return;
}
// CONTA COMPLETATI E NON
const completed = todos.filter(t => t.completato).length;
const pending = todos.length - completed;
// MOSTRA COUNTER
counter.innerHTML = `
📊 Totale: <strong>${todos.length}</strong> |
✅ Completati: <strong>${completed}</strong> |
⏳ In Sospeso: <strong>${pending}</strong>
`;
counter.classList.remove('nascosto');
// CREA CARD TODO
const todosHTML = todos.map(todo => creaCardTodos(todo)).join('');
todosContainer.innerHTML = todosHTML;
}
// ===== COLLEGA GLI EVENTI =====
btnLoadTodos.addEventListener('click', fetchTodosUtente);
btnAddTodo.addEventListener('click', addTodo);
// PERMETTI ENTER per aggiungere TODO
todoInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
addTodo();
}
});

View File

@@ -0,0 +1,264 @@
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
display: flex;
justify-content: center;
align-items: flex-start;
min-height: 100vh;
margin: 0;
padding: 20px;
}
.app-container {
background: white;
width: 100%;
max-width: 550px;
padding: 30px;
border-radius: 15px;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
margin-top: 20px;
}
h1 {
color: #333;
text-align: center;
margin: 0 0 10px 0;
font-size: 1.8rem;
}
.subtitle {
color: #666;
text-align: center;
margin: 0 0 30px 0;
font-size: 1rem;
}
.config-box,
.add-todo-box {
background: #f9f9f9;
padding: 20px;
border-radius: 10px;
margin-bottom: 25px;
}
.config-box h2,
.add-todo-box h2 {
color: #333;
margin: 0 0 15px 0;
font-size: 1.1rem;
}
.config-box label,
.add-todo-box label {
display: block;
color: #555;
font-weight: 600;
margin-bottom: 10px;
font-size: 0.95rem;
}
.input-group {
display: flex;
gap: 10px;
}
.input-group input {
flex: 1;
padding: 10px;
border: 2px solid #ddd;
border-radius: 6px;
font-size: 15px;
font-family: inherit;
}
.input-group input:focus {
outline: none;
border-color: #007bff;
box-shadow: 0 0 5px rgba(0, 123, 255, 0.2);
}
.input-group button {
padding: 10px 20px;
background: #007bff;
color: white;
border: none;
border-radius: 6px;
font-weight: bold;
cursor: pointer;
transition: background 0.2s;
font-size: 15px;
}
.input-group button:hover {
background: #0056b3;
}
.loading {
text-align: center;
color: #666;
font-size: 1rem;
padding: 30px;
animation: pulse 1s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.6; }
}
.counter {
background: #f0f8ff;
color: #333;
padding: 15px;
border-radius: 6px;
margin-bottom: 20px;
font-weight: 500;
text-align: center;
border-left: 4px solid #007bff;
}
.todos-container {
display: flex;
flex-direction: column;
gap: 12px;
margin-bottom: 30px;
}
.todo-item {
background: white;
padding: 15px;
border-radius: 6px;
display: flex;
gap: 12px;
align-items: center;
border-left: 4px solid #007bff;
animation: slideIn 0.2s ease-out;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}
.todo-item.completed {
background: #f5f5f5;
border-left-color: #28a745;
opacity: 0.7;
}
.todo-item.completed .todo-title {
text-decoration: line-through;
color: #999;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateX(-20px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
.todo-checkbox {
flex-shrink: 0;
}
.todo-checkbox input {
width: 20px;
height: 20px;
cursor: pointer;
}
.todo-content {
flex: 1;
}
.todo-title {
color: #333;
font-weight: 500;
font-size: 1rem;
margin: 0 0 4px 0;
}
.todo-id {
color: #999;
font-size: 0.8rem;
margin: 0;
}
.btn-delete {
padding: 6px 12px;
background: #fee;
color: #c00;
border: 1px solid #fcc;
border-radius: 4px;
cursor: pointer;
font-weight: 500;
transition: background 0.2s;
}
.btn-delete:hover {
background: #fcc;
color: #800;
}
.empty {
background: white;
color: #999;
padding: 30px;
text-align: center;
border-radius: 6px;
border: 1px solid #eee;
}
.error {
background: #fee;
color: #c00;
padding: 15px;
border-radius: 6px;
border-left: 4px solid #c00;
font-weight: 500;
}
.instructions {
background: #f9f9f9;
padding: 20px;
border-radius: 10px;
border-left: 4px solid #007bff;
margin-top: 30px;
}
.instructions h2 {
color: #333;
margin: 0 0 15px 0;
font-size: 1.1rem;
}
.instructions h3 {
color: #555;
margin: 15px 0 10px 0;
font-size: 1rem;
}
.instructions ol,
.instructions ul {
margin-left: 20px;
color: #555;
line-height: 1.7;
}
.instructions li {
margin-bottom: 8px;
}
.instructions code {
background: white;
padding: 2px 5px;
border-radius: 3px;
font-family: 'Courier New', monospace;
color: #d63384;
font-size: 0.9em;
}
.nascosto {
display: none;
}

View File

@@ -0,0 +1,92 @@
<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Extra 1 - App Meteo</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<a href="../index.html" style="position: absolute; top: 20px; left: 20px; text-decoration: none; color: #555; font-weight: bold;">← Dashboard</a>
<div class="app-container">
<h1>🌤️ App Meteo</h1>
<p class="subtitle">API pubblica Open-Meteo</p>
<!-- SEZIONE RICERCA -->
<div class="search-box">
<h2>🔍 Ricerca Città</h2>
<label>Latitudine e Longitudine:</label>
<div class="input-group">
<input type="number" id="latitude" placeholder="Lat" step="0.01" value="45.4642">
<input type="number" id="longitude" placeholder="Lon" step="0.01" value="9.1900">
<button id="btnSearch">Cerca Meteo</button>
</div>
<p class="hint-text">💡 Esempi: Milano (45.46, 9.19) | Roma (41.90, 12.50) | Napoli (40.85, 14.27)</p>
</div>
<!-- LOADING -->
<div id="loading" class="loading nascosto">
⏳ Caricamento meteo...
</div>
<!-- RISULTATO -->
<div id="weatherContainer" class="weather-container"></div>
<!-- ISTRUZIONI -->
<div class="instructions">
<h2>📝 Cosa Devi Fare</h2>
<ol>
<li>Leggi latitudine e longitudine dagli input</li>
<li>Costruisci l'URL della richiesta con i parametri query
<ul style="margin-top: 10px;">
<li>URL base: <code>https://api.open-meteo.com/v1/forecast</code></li>
<li>Parametri: <code>latitude</code>, <code>longitude</code>, <code>current</code>, <code>timezone</code></li>
</ul>
</li>
<li>Fai una GET all'URL che hai costruito</li>
<li>Crea l'oggetto dalla risposta JSON</li>
<li>
Prendi i dati da dentro <code>response.current</code><br>
L'oggetto è più complesso, ma ci servono solo i dati attuali.
</li>
<li>Visualizza temperatura, umidità, descrizione meteo</li>
</ol>
<div class="hint">
<strong>💡 Come costruire l'URL:</strong>
<p>I parametri query iniziano con <code>?</code> e sono separati da <code>&amp;</code></p>
<pre>const url = `https://api.open-meteo.com/v1/forecast?latitude=${lat}&amp;longitude=${lon}&amp;current=temperature_2m,relative_humidity_2m,weather_code&amp;timezone=auto`;</pre>
</div>
<div class="hint">
<strong>💡 URL Completo (esempio con Milano):</strong>
<pre>https://api.open-meteo.com/v1/forecast?latitude=45.46&amp;longitude=9.19&amp;current=temperature_2m,relative_humidity_2m,weather_code&amp;timezone=auto</pre>
<p>Puoi testare questo URL direttamente nel browser per vedere la struttura della risposta.</p>
<p>
Sarà in formato JSON, copiala e mettila in un visualizzatore JSON online per esplorarla meglio.<br>
Oppure mettila in un file (es. response.json) e aprilo con VSCode, premi CTRL+SHIFT+F per formattarlo.
</p>
</div>
<div class="hint">
<strong>💡 Struttura Risposta:</strong>
<pre>response.current = {
time: "2024-06-01T12:00:00Z",
interval: 900,
temperature_2m: 22.5,
relative_humidity_2m: 65,
weather_code: 0 // 0=soleggiato, 1=nuvoloso, 2=coperto, 3=pioggia, ecc
}</pre>
</div>
<div class="challenge">
<strong>🎯 Bonus Challenge:</strong>
<p>Converti il codice meteo in emoji (0=☀️, 1=⛅, 2=☁️, 3=🌧️, ecc.) usando una mappa di conversione</p>
</div>
</div>
</div>
<script src="script.js"></script>
</body>
</html>

View File

@@ -0,0 +1,157 @@
/**
* EXTRA 1: App Meteo con Open-Meteo
*
* Open-Meteo è un'API PUBBLICA e GRATUITA che NON richiede autenticazione!
* Puoi fare centinaia di richieste al giorno senza problemi.
*
* API Base: https://api.open-meteo.com/v1/forecast
*/
// ===== VARIABILI DEL DOM =====
const latitude = document.getElementById('latitude');
const longitude = document.getElementById('longitude');
const btnSearch = document.getElementById('btnSearch');
const loading = document.getElementById('loading');
const weatherContainer = document.getElementById('weatherContainer');
/**
* FUNZIONE: Gestione errori
*
* Mostra un messaggio di errore e logga in console
*/
function handleError(message) {
weatherContainer.innerHTML = `
<div class="error">
<strong>❌ ${message}</strong>
</div>
`;
console.error('Errore:', message);
}
/**
* FUNZIONE: Ricerca il meteo per latitudine e longitudine
*
* Parametri obbligatori dell'API:
* - latitude: numero decimale
* - longitude: numero decimale
* - current: variabili da ottenere (separati da virgola)
* - timezone: 'auto' oppure fuso orario specifico
*
* Passi:
* 1. Leggi latitudine e longitudine dagli input
* 2. Valida che siano compilati
* 3. Mostra lo spinner di caricamento
* 4. Costruisci l'URL con i parametri corretti
* 5. Fai una GET a https://api.open-meteo.com/v1/forecast
* 6. Se non OK, mostra errore e return
* 7. Converti in JSON
* 8. Estrai data.current
* 9. Chiama displayWeather() per visualizzare
* 10. Nascondi lo spinner
*/
async function searchWeather() {
const lat = latitude.value;
const lon = longitude.value;
// VALIDAZIONE
if (!lat || !lon) {
handleError('Inserisci latitudine e longitudine');
return;
}
loading.classList.remove('nascosto');
weatherContainer.innerHTML = '';
try {
// 👇 SCRIVI QUI IL TUO CODICE 👇
// 1. Costruisci l'URL con i parametri corretti:
// const url = 'https://api.open-meteo.com/v1/forecast' +
// '?latitude=' + lat +
// '&longitude=' + lon +
// '&current=temperature_2m,relative_humidity_2m,weather_code' +
// '&timezone=auto';
// 2. Fai la fetch
// const response = await fetch(url);
// 3. Se non OK, mostra errore e return
// if (!response.ok) {
// throw new Error('Errore nel caricamento dei dati meteo');
// }
// 4. Converti in JSON
// const data = await response.json();
// 5. Estrai i dati meteo e visualizza
// displayWeather(data.current, lat, lon);
throw new Error('Codice non implementato - Completa searchWeather()');
} catch (error) {
handleError(error.message);
} finally {
loading.classList.add('nascosto');
}
}
/**
* FUNZIONE: Visualizza i dati meteo
* (Questa funzione è già fatta - non modificare)
*/
function displayWeather(current, lat, lon) {
// Converti codice meteo in descrizione
const weatherDescriptions = {
0: { emoji: '☀️', descrizione: 'Sereno' },
1: { emoji: '🌤️', descrizione: 'Poco nuvoloso' },
2: { emoji: '⛅', descrizione: 'Nuvoloso' },
3: { emoji: '☁️', descrizione: 'Molto nuvoloso' },
45: { emoji: '🌫️', descrizione: 'Nebbia' },
48: { emoji: '🌫️', descrizione: 'Nebbia con brina' },
51: { emoji: '🌧️', descrizione: 'Pioggia leggera' },
53: { emoji: '🌧️', descrizione: 'Pioggia' },
55: { emoji: '⛈️', descrizione: 'Pioggia forte' },
80: { emoji: '🌧️', descrizione: 'Pioggia leggera' },
81: { emoji: '🌧️', descrizione: 'Pioggia' },
82: { emoji: '⛈️', descrizione: 'Pioggia forte' },
95: { emoji: '⛈️', descrizione: 'Temporale' },
};
const weather = weatherDescriptions[current.weather_code] || { emoji: '❓', descrizione: 'Sconosciuto' };
const html = `
<div class="weather-card">
<div class="location">
📍 ${lat.toFixed(2)}°N, ${lon.toFixed(2)}°E
</div>
<div class="weather-main">
<div class="emoji">${weather.emoji}</div>
<div class="temp">${current.temperature_2m}°C</div>
</div>
<div class="weather-details">
<p><strong>Condizione:</strong> ${weather.descrizione}</p>
<p><strong>Umidità:</strong> ${current.relative_humidity_2m}%</p>
<p><strong>Codice Meteo:</strong> ${current.weather_code}</p>
</div>
</div>
`;
weatherContainer.innerHTML = html;
}
// ===== COLLEGA GLI EVENTI =====
btnSearch.addEventListener('click', searchWeather);
// PERMETTI ENTER
latitude.addEventListener('keypress', (e) => {
if (e.key === 'Enter') searchWeather();
});
longitude.addEventListener('keypress', (e) => {
if (e.key === 'Enter') searchWeather();
});

View File

@@ -0,0 +1,259 @@
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
display: flex;
justify-content: center;
align-items: flex-start;
min-height: 100vh;
margin: 0;
padding: 20px;
}
.app-container {
background: white;
width: 100%;
max-width: 550px;
padding: 30px;
border-radius: 15px;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
margin-top: 20px;
}
h1 {
color: #333;
text-align: center;
margin: 0 0 10px 0;
font-size: 1.8rem;
}
.subtitle {
color: #666;
text-align: center;
margin: 0 0 30px 0;
font-size: 1rem;
}
.search-box {
background: #f9f9f9;
padding: 20px;
border-radius: 10px;
margin-bottom: 25px;
}
.search-box h2 {
color: #333;
margin: 0 0 15px 0;
font-size: 1.1rem;
}
.search-box label {
display: block;
color: #555;
font-weight: 600;
margin-bottom: 10px;
font-size: 0.95rem;
}
.input-group {
display: flex;
gap: 10px;
margin-bottom: 10px;
}
.input-group input {
flex: 1;
padding: 10px;
border: 2px solid #ddd;
border-radius: 6px;
font-size: 15px;
font-family: inherit;
}
.input-group input:focus {
outline: none;
border-color: #007bff;
box-shadow: 0 0 5px rgba(0, 123, 255, 0.2);
}
.input-group button {
padding: 10px 20px;
background: #007bff;
color: white;
border: none;
border-radius: 6px;
font-weight: bold;
cursor: pointer;
transition: background 0.2s;
}
.input-group button:hover {
background: #0056b3;
}
.hint-text {
color: #666;
font-size: 0.9rem;
margin-top: 10px;
}
.loading {
text-align: center;
color: #666;
font-size: 1rem;
padding: 30px;
animation: pulse 1s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.6; }
}
.weather-container {
margin-bottom: 50px;
}
.weather-card {
background: #f9f9f9;
border-radius: 10px;
padding: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
animation: slideIn 0.3s ease-out;
}
.location {
text-align: center;
color: #666;
margin-bottom: 20px;
font-size: 0.95rem;
}
.weather-main {
text-align: center;
margin: 20px 0;
}
.emoji {
font-size: 3.5rem;
margin-bottom: 10px;
}
.temp {
font-size: 2.2rem;
font-weight: bold;
color: #333;
}
.weather-details {
background: white;
padding: 15px;
border-radius: 6px;
margin-top: 15px;
}
.weather-details p {
margin: 8px 0;
color: #555;
font-size: 0.95rem;
}
.error {
background: #fee;
color: #c00;
padding: 15px;
border-radius: 6px;
border-left: 4px solid #c00;
font-weight: 500;
}
.instructions {
background: #f9f9f9;
padding: 20px;
border-radius: 10px;
border-left: 4px solid #007bff;
margin-top: 30px;
}
.instructions h2 {
color: #333;
margin: 0 0 15px 0;
font-size: 1.1rem;
}
.instructions ol,
.instructions ul {
margin-left: 20px;
color: #555;
line-height: 1.7;
}
.instructions li {
margin-bottom: 8px;
}
.instructions code {
background: white;
padding: 2px 5px;
border-radius: 3px;
font-family: 'Courier New', monospace;
color: #d63384;
font-size: 0.9rem;
}
.hint {
background: white;
border: 1px solid #ddd;
padding: 15px;
border-radius: 6px;
margin-top: 15px;
}
.hint strong {
color: #007bff;
display: block;
margin-bottom: 10px;
}
.hint pre {
background: #f0f0f0;
padding: 10px;
border-radius: 4px;
overflow-x: auto;
font-size: 0.85rem;
color: #333;
margin: 0;
}
.challenge {
background: white;
border: 1px solid #ddd;
padding: 15px;
border-radius: 6px;
margin-top: 15px;
}
.challenge strong {
color: #28a745;
display: block;
margin-bottom: 10px;
}
.challenge p {
margin: 0;
color: #555;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.nascosto {
display: none;
}

View File

@@ -0,0 +1,99 @@
<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Extra 2 - Pokédex</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<a href="../index.html" style="position: absolute; top: 20px; left: 20px; text-decoration: none; color: #555; font-weight: bold;">← Dashboard</a>
<div class="app-container">
<h1>🔴 Pokédex</h1>
<p class="subtitle">API pubblica PokéAPI</p>
<!-- SEZIONE RICERCA -->
<div class="search-box">
<h2>🔍 Cerca Pokémon</h2>
<label>Nome o Numero:</label>
<div class="input-group">
<input type="text" id="pokemonInput" placeholder="Pikachu oppure 25" value="pikachu">
<button id="btnSearch">Cerca</button>
</div>
<p class="hint-text">💡 Esempi: pikachu, charizard, 1 (Bulbasaur), 6 (Charizard)</p>
</div>
<!-- LOADING -->
<div id="loading" class="loading nascosto">
⏳ Cercando Pokémon...
</div>
<!-- RISULTATO -->
<div id="pokemonContainer" class="pokemon-container"></div>
<!-- ISTRUZIONI -->
<div class="instructions">
<h2>📝 Cosa Devi Fare</h2>
<ol>
<li>Leggi il nome/numero dal campo input</li>
<li>Fai una GET a <code>https://pokeapi.co/api/v2/pokemon/{name_or_id}</code></li>
<li>Verifica che la risposta sia OK (gestisci errori 404)
<ul style="margin-top: 10px;">
<li>Se <code>!response.ok</code>, mostra un errore</li>
</ul>
</li>
<li>Estrai i dati da <code>response</code>:
<ul style="margin-top: 10px;">
<li><code>name</code> - Nome</li>
<li><code>sprites.front_default</code> - Immagine</li>
<li><code>height</code> - Altezza (in decimetri)</li>
<li><code>weight</code> - Peso (in ettogrammi)</li>
<li><code>types[].type.name</code> - Tipi (è un ARRAY!)</li>
</ul>
</li>
<li>Visualizza le informazioni in una card</li>
</ol>
<div class="hint">
<strong>💡 URL Completo (esempio Pikachu):</strong>
<pre>https://pokeapi.co/api/v2/pokemon/pikachu</pre>
</div>
<div class="hint">
<strong>💡 Array Annidati - Come Accedere a <code>types</code>:</strong>
<p><code>types</code> è un ARRAY di oggetti. Ogni elemento ha <code>type.name</code>:</p>
<pre>// response.types = [{type: {name: "electric"}}, {type: {name: "flying"}}]
// Per estrarre i nomi, usa map():
const typeNames = response.types.map(t => t.type.name);
// Risultato: ["electric", "flying"]</pre>
</div>
<div class="hint">
<strong>💡 Struttura Card da Visualizzare:</strong>
<pre>&lt;div class="pokemon-card"&gt;
&lt;img src="sprites.front_default" alt="name"&gt;
&lt;h3&gt;name&lt;/h3&gt;
&lt;p&gt;Altezza: height dm&lt;/p&gt;
&lt;p&gt;Peso: weight hg&lt;/p&gt;
&lt;p&gt;Tipi: typeNames.join(", ")&lt;/p&gt;
&lt;/div&gt;</pre>
</div>
<div class="hint">
<strong>💡 Gestire Errori:</strong>
<pre>if (!response.ok) {
throw new Error('Pokémon non trovato');
}</pre>
</div>
<div class="challenge">
<strong>🎯 Bonus Challenge:</strong>
<p>Aggiungi una funzione che carica 6 Pokémon casuali al caricamento della pagina (usa numeri random tra 1 e 151 per Pokémon validi)</p>
</div>
</div>
</div>
<script src="script.js"></script>
</body>
</html>

View File

@@ -0,0 +1,144 @@
/**
* EXTRA 2: Pokédex con PokéAPI
*
* PokéAPI è un'API PUBBLICA e GRATUITA con migliaia di dati su Pokémon!
* NON richiede autenticazione, puoi fare tante richieste quante vuoi.
*
* API Base: https://pokeapi.co/api/v2/
*/
// ===== VARIABILI DEL DOM =====
const pokemonInput = document.getElementById('pokemonInput');
const btnSearch = document.getElementById('btnSearch');
const loading = document.getElementById('loading');
const pokemonContainer = document.getElementById('pokemonContainer');
/**
* FUNZIONE: Gestione errori
*
* Mostra un messaggio di errore e logga in console
*/
function handleError(message) {
pokemonContainer.innerHTML = `
<div class="error">
<strong>❌ ${message}</strong>
</div>
`;
console.error('Errore:', message);
}
/**
* FUNZIONE: Ricerca un Pokémon per nome o numero
*
* Parametri:
* - Accetta nome (in minuscolo) o numero ID
* - Ritorna oggetto con tutte le info del Pokémon
*
* Passi:
* 1. Leggi il valore dall'input
* 2. Converti a minuscolo e trim()
* 3. Valida che non sia vuoto
* 4. Mostra lo spinner di caricamento
* 5. Fai una GET a https://pokeapi.co/api/v2/pokemon/{input}
* 6. Se la risposta non è OK (404), mostra "Pokémon non trovato" e return
* 7. Converti in JSON
* 8. Chiama displayPokemon() per visualizzare
* 9. Nascondi lo spinner
*/
async function searchPokemon() {
const input = pokemonInput.value.trim().toLowerCase();
// VALIDAZIONE
if (!input) {
handleError('Inserisci il nome o numero di un Pokémon');
return;
}
loading.classList.remove('nascosto');
pokemonContainer.innerHTML = '';
try {
// 👇 SCRIVI QUI IL TUO CODICE 👇
// 1. Fai una fetch GET a: https://pokeapi.co/api/v2/pokemon/ + input
// const response = await fetch('https://pokeapi.co/api/v2/pokemon/' + input);
// 2. Controlla se la risposta è OK (non 404)
// if (!response.ok) {
// throw new Error('Pokémon non trovato');
// }
// 3. Converti in JSON
// const pokemon = await response.json();
// 4. Visualizza
// displayPokemon(pokemon);
throw new Error('Codice non implementato - Completa searchPokemon()');
} catch (error) {
handleError(error.message);
} finally {
loading.classList.add('nascosto');
}
}
/**
* FUNZIONE: Visualizza le informazioni del Pokémon
* (Questa funzione è già fatta - non modificare)
*/
function displayPokemon(pokemon) {
// ESTRAI I DATI
const name = pokemon.name.charAt(0).toUpperCase() + pokemon.name.slice(1);
const id = pokemon.id;
const image = pokemon.sprites.front_default || 'https://placehold.co/200';
const height = (pokemon.height / 10).toFixed(1); // Converti da dm a metri
const weight = (pokemon.weight / 10).toFixed(1); // Converti da hg a kg
const types = pokemon.types.map(t => t.type.name).join(', ');
const html = `
<div class="pokemon-card">
<div class="card-header">
<h2>#${id} - ${name}</h2>
</div>
<div class="card-image">
<img src="${image}" alt="${name}">
</div>
<div class="card-details">
<div class="detail-row">
<strong>Tipo:</strong>
<span>${types}</span>
</div>
<div class="detail-row">
<strong>Altezza:</strong>
<span>${height} m</span>
</div>
<div class="detail-row">
<strong>Peso:</strong>
<span>${weight} kg</span>
</div>
</div>
<div class="card-footer">
<small>Dati da <a href="https://pokeapi.co/" target="_blank">PokéAPI</a></small>
</div>
</div>
`;
pokemonContainer.innerHTML = html;
}
// ===== COLLEGA GLI EVENTI =====
btnSearch.addEventListener('click', searchPokemon);
// PERMETTI ENTER
pokemonInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
searchPokemon();
}
});

View File

@@ -0,0 +1,290 @@
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
display: flex;
justify-content: center;
align-items: flex-start;
min-height: 100vh;
margin: 0;
padding: 20px;
}
.app-container {
background: white;
width: 100%;
max-width: 550px;
padding: 30px;
border-radius: 15px;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
margin-top: 20px;
}
h1 {
color: #333;
text-align: center;
margin: 0 0 10px 0;
font-size: 1.8rem;
}
.subtitle {
color: #666;
text-align: center;
margin: 0 0 30px 0;
font-size: 1rem;
}
.search-box {
background: #f9f9f9;
padding: 20px;
border-radius: 10px;
margin-bottom: 25px;
}
.search-box h2 {
color: #333;
margin: 0 0 15px 0;
font-size: 1.1rem;
}
.search-box label {
display: block;
color: #555;
font-weight: 600;
margin-bottom: 10px;
font-size: 0.95rem;
}
.input-group {
display: flex;
gap: 10px;
margin-bottom: 10px;
}
.input-group input {
flex: 1;
padding: 10px;
border: 2px solid #ddd;
border-radius: 6px;
font-size: 15px;
font-family: inherit;
}
.input-group input:focus {
outline: none;
border-color: #007bff;
box-shadow: 0 0 5px rgba(0, 123, 255, 0.2);
}
.input-group button {
padding: 10px 20px;
background: #007bff;
color: white;
border: none;
border-radius: 6px;
font-weight: bold;
cursor: pointer;
transition: background 0.2s;
}
.input-group button:hover {
background: #0056b3;
}
.hint-text {
color: #666;
font-size: 0.9rem;
margin-top: 10px;
}
.loading {
text-align: center;
color: #666;
font-size: 1rem;
padding: 30px;
animation: pulse 1s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.6; }
}
.pokemon-container {
margin-bottom: 50px;
}
.pokemon-card {
background: #f9f9f9;
border-radius: 10px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
animation: slideIn 0.3s ease-out;
}
.card-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 20px;
text-align: center;
}
.card-header h2 {
margin: 0;
font-size: 1.3rem;
}
.card-image {
background: white;
padding: 30px;
text-align: center;
border-bottom: 1px solid #f0f0f0;
}
.card-image img {
max-width: 180px;
height: auto;
}
.card-details {
padding: 20px;
}
.detail-row {
display: flex;
justify-content: space-between;
padding: 10px 0;
border-bottom: 1px solid #eee;
color: #555;
font-size: 0.95rem;
}
.detail-row:last-child {
border-bottom: none;
}
.detail-row strong {
color: #333;
font-weight: 600;
}
.card-footer {
background: #f9f9f9;
padding: 12px 20px;
text-align: center;
border-top: 1px solid #eee;
}
.card-footer small {
color: #999;
font-size: 0.8rem;
}
.card-footer a {
color: #007bff;
text-decoration: none;
}
.card-footer a:hover {
text-decoration: underline;
}
.error {
background: #fee;
color: #c00;
padding: 15px;
border-radius: 6px;
border-left: 4px solid #c00;
font-weight: 500;
}
.instructions {
background: #f9f9f9;
padding: 20px;
border-radius: 10px;
border-left: 4px solid #007bff;
margin-top: 30px;
}
.instructions h2 {
color: #333;
margin: 0 0 15px 0;
font-size: 1.1rem;
}
.instructions ol,
.instructions ul {
margin-left: 20px;
color: #555;
line-height: 1.7;
}
.instructions li {
margin-bottom: 8px;
}
.instructions code {
background: white;
padding: 2px 5px;
border-radius: 3px;
font-family: 'Courier New', monospace;
color: #d63384;
font-size: 0.9rem;
}
.hint {
background: white;
border: 1px solid #ddd;
padding: 15px;
border-radius: 6px;
margin-top: 15px;
}
.hint strong {
color: #007bff;
display: block;
margin-bottom: 10px;
}
.hint pre {
background: #f0f0f0;
padding: 10px;
border-radius: 4px;
overflow-x: auto;
font-size: 0.85rem;
color: #333;
margin: 0;
}
.challenge {
background: white;
border: 1px solid #ddd;
padding: 15px;
border-radius: 6px;
margin-top: 15px;
}
.challenge strong {
color: #28a745;
display: block;
margin-bottom: 10px;
}
.challenge p {
margin: 0;
color: #555;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.nascosto {
display: none;
}

View File

@@ -0,0 +1,259 @@
<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hub Esercizi API</title>
<style>
/* RESET & BASE */
* {
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
margin: 0;
padding: 40px;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
/* CONTENITORE PRINCIPALE */
.hub-container {
background: white;
width: 100%;
max-width: 750px;
padding: 40px;
border-radius: 20px;
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.1);
}
h1 {
text-align: center;
color: #333;
margin-bottom: 5px;
font-size: 2.5rem;
}
p.subtitle {
text-align: center;
color: #666;
margin-bottom: 40px;
font-size: 1.1rem;
}
/* TITOLI SEZIONI */
h2 {
color: #333;
font-size: 1.3rem;
margin-top: 35px;
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 2px solid #007bff;
}
h2:first-of-type {
margin-top: 0;
}
/* LISTA CARD */
.exercise-list {
display: flex;
flex-direction: column;
gap: 15px;
margin-bottom: 30px;
}
/* CARD ESERCIZIO */
.card {
display: flex;
align-items: center;
text-decoration: none;
background: #fff;
border: 2px solid #f0f0f0;
border-radius: 12px;
padding: 20px;
transition: all 0.2s ease;
}
.card:hover {
transform: translateY(-3px);
border-color: #007bff;
box-shadow: 0 5px 15px rgba(0, 123, 255, 0.1);
}
/* ICONA E TESTI */
.icon {
font-size: 2rem;
margin-right: 20px;
width: 40px;
text-align: center;
flex-shrink: 0;
}
.info {
flex: 1;
}
.info h3 {
margin: 0 0 5px 0;
color: #2c3e50;
font-size: 1.1rem;
display: flex;
align-items: center;
gap: 10px;
}
.info p {
margin: 0;
color: #7f8c8d;
font-size: 0.9rem;
}
/* TAG DIFFICOLTA' */
.difficulty {
display: inline-block;
padding: 4px 10px;
border-radius: 6px;
font-size: 0.75rem;
font-weight: bold;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.difficulty.tutorial {
background: #e3f2fd;
color: #1565c0;
}
.difficulty.easy {
background: #d4edda;
color: #155724;
}
.difficulty.medium {
background: #fff3cd;
color: #856404;
}
.difficulty.hard {
background: #f8d7da;
color: #721c24;
}
/* NOTA IMPORTANTE */
.important-note {
background: #fff3cd;
border-left: 4px solid #ffc107;
padding: 15px;
border-radius: 5px;
margin-top: 30px;
font-size: 0.95rem;
color: #856404;
}
.important-note strong {
color: #333;
}
.important-note code {
background: white;
padding: 2px 6px;
border-radius: 3px;
font-family: 'Courier New', monospace;
}
</style>
</head>
<body>
<div class="hub-container">
<h1>API REST</h1>
<p class="subtitle">Esercizi Da Base a CRUD</p>
<h2>Tutorial Introduttivo</h2>
<div class="exercise-list">
<a href="tutorial/index.html" class="card">
<div class="icon">🧪</div>
<div class="info">
<h3>Tutorial Interattivo <span class="difficulty tutorial">Tutorial</span></h3>
<p>Impara API, Fetch e Async/Await con 10 step</p>
</div>
</a>
</div>
<h2>Esercizi su Server Locale</h2>
<div class="exercise-list">
<a href="01_get_singolo_utente/index.html" class="card">
<div class="icon">📊</div>
<div class="info">
<h3>GET Singolo Utente <span class="difficulty easy">Facile</span></h3>
<p>Recupera e visualizza un utente dal server</p>
</div>
</a>
<a href="02_get_lista_utenti/index.html" class="card">
<div class="icon">👥</div>
<div class="info">
<h3>GET Lista Utenti <span class="difficulty easy">Facile</span></h3>
<p>Crea card dinamiche da un array di utenti</p>
</div>
</a>
<a href="03_utente_e_post/index.html" class="card">
<div class="icon">📝</div>
<div class="info">
<h3>Utente + Post <span class="difficulty medium">Medio</span></h3>
<p>Fetch multipli e relazioni tra dati</p>
</div>
</a>
<a href="04_ricerca_post/index.html" class="card">
<div class="icon">🔍</div>
<div class="info">
<h3>Ricerca Post <span class="difficulty medium">Medio</span></h3>
<p>Fetch multipli con filtri incrociati</p>
</div>
</a>
<a href="05_todo_app_crud/index.html" class="card">
<div class="icon"></div>
<div class="info">
<h3>Todo App CRUD <span class="difficulty hard">Difficile</span></h3>
<p>GET, POST, PUT, DELETE - App completa</p>
</div>
</a>
</div>
<h2>Esercizi su Server Esterno</h2>
<div class="exercise-list">
<a href="extra_meteo/index.html" class="card">
<div class="icon">🌤️</div>
<div class="info">
<h3>App Meteo <span class="difficulty medium">Medio</span></h3>
<p>API pubblica Open-Meteo - No server locale</p>
</div>
</a>
<a href="extra_pokedex/index.html" class="card">
<div class="icon">🔴</div>
<div class="info">
<h3>Pokédex <span class="difficulty medium">Medio</span></h3>
<p>API pubblica PokéAPI - No server locale</p>
</div>
</a>
</div>
<div class="important-note">
<strong>⚠️ Prima di Iniziare:</strong><br>
Per esercizi 1-5: avvia il server con <code>cd server-api</code> e poi <code>npm start</code><br>
L'insegnante dovrebbe averlo già avviato, ma potete farlo anche in locale.<br>
Controllare sempre che <code>BASE_URL</code> in ogni script punti al corretto URL.
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,93 @@
<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="styles.css">
<title>API e Asincronia</title>
</head>
<body>
<div class="controls">
<h1>Tutorial: API e Asincronia</h1>
<button id="btn-esegui">▶️ Esegui Codice</button>
<button id="btn-reset">🧹 Pulisci Console</button>
<a href="../index.html" style="position: absolute; top: 20px; left: 20px; text-decoration: none; color: #555; font-weight: bold;">← Dashboard</a>
</div>
<script>
function mostraOutput(stepNumero, messaggio) {
const elemento = document.getElementById(`output-${stepNumero}`);
if (elemento) {
messaggio = messaggio || "❌";
let isObject = (typeof messaggio === 'object' && messaggio !== null);
elemento.textContent = isObject ? JSON.stringify(messaggio, null, 2) : String(messaggio);
elemento.classList.remove("loading");
elemento.style.transition = "";
elemento.style.backgroundColor = "#666";
setTimeout(() => {
elemento.style.transition = "background-color 1s ease";
elemento.style.backgroundColor = "#2d3436";
}, 0);
}
}
[
{
title: "1. Il concetto di Attesa (Simulazione)",
description: "Le API sono lente. Simuliamo un ritardo di 2 secondi prima di mostrare il messaggio.",
outputLabel: "Output Step 1:"
},
{
title: "2. Prima chiamata Fetch (GET)",
description: "Recuperiamo un oggetto JSON reale da un server pubblico.",
outputLabel: "Dati ricevuti:"
},
{
title: "3. Estrazione Dati (JSON)",
description: "La risposta grezza non basta. Dobbiamo convertirla ed estrarre il titolo.",
outputLabel: "Titolo del Post:"
},
{
title: "4. Gestione Errori (Try/Catch)",
description: "Proviamo a chiamare un sito che non esiste per vedere se il codice sopravvive.",
outputLabel: "Stato operazione:"
},
{
title: "5. Lavorare con le Liste (Array)",
description: "Scarichiamo 10 utenti e mostriamo solo i loro nomi.",
outputLabel: "Elenco Utenti:"
},
{
title: "6. Invio Dati (POST)",
description: "Simuliamo l'invio di un nuovo post al server.",
outputLabel: "Risposta Server:"
}
].forEach((step, i) => {
document.write(`
<div class="step-card">
<div class="step-header">
<div class="step-title">${step.title}</div>
</div>
<div class="step-desc">${step.description}</div>
<span class="label">${step.outputLabel}</span>
<div id="output-${i + 1}" class="output-box"></div>
</div>
`);
});
</script>
<script src="script.js"></script>
<script>
document.getElementById('btn-reset').addEventListener('click', () => {
localStorage.clear();
location.reload();
});
document.getElementById('btn-esegui').addEventListener('click', () => {
const el = document.querySelectorAll('[id^="output-"]');
el.forEach(box => box.className = "output-box loading");
eseguiTutorial();
});
</script>
</body>
</html>

View File

@@ -0,0 +1,115 @@
// TUTORIAL INTERATTIVO: API, Fetch e Async/Await
// NON TOCCARE
// Funzioni di supporto per il tutorial
const wait = (ms) => new Promise(resolve => setTimeout(resolve, ms)); // Simula server lento
// Funzione pricipale del tutorial
async function eseguiTutorial() {
/**
* ===========================================
* === 1. Il Concetto di Attesa ===
* JavaScript di solito corre veloce. Con i server, deve aspettare.
* La keyword 'await' serve a dire "Fermati qui finché l'operazione non finisce".
*/
// Questa funzione simula un server che ci mette 2 secondi a rispondere
// TODO: Aggiungi 'await' prima di wait(2000) per far funzionare l'attesa.
wait(2000);
mostraOutput(1, "Operazione completata!");
/**
* ===========================================
* === 2. Fetch Base (GET) ===
* Usiamo `fetch(url)` per chiamare un server vero.
*/
// TODO: crea una costante 'urlBase' con l'indirizzo dettato a lezione
// TODO: Esegui una fetch a quell'URL.
// Ricorda di mettere 'await' davanti a fetch!
const urlBase = "https://sito-finto.com";
const rispostaGrezza = null;
mostraOutput(2, rispostaGrezza ? "Risposta ricevuta (Response Object)" : "");
/**
* ===========================================
* === 3. Estrarre il JSON ===
* La risposta grezza contiene status code, headers, ecc.
* A noi servono i dati. Dobbiamo usare il metodo .json().
* ANCHE .json() è asincrono e vuole 'await'.
*/
let nomeUtente = "";
if (rispostaGrezza) {
// TODO: Estrai i dati usando: await rispostaGrezza.json()
const dati = {};
// TODO: Assegna a nomeUtente il valore di dati.nome
nomeUtente = "NOME MANCANTE";
}
mostraOutput(3, nomeUtente);
/**
* ===========================================
* === 4. Gestione Errori (Try / Catch) ===
* Se il server è giù o l'URL è sbagliato, fetch esplode.
* Usiamo try/catch per gestire il problema.
*/
try {
// TODO: Prova a fare una fetch a un URL sbagliato (es. 'https://sito-finto.com')
// Ricorda l'await!
mostraOutput(4);
} catch (errore) {
mostraOutput(4, errore.message);
}
/**
* ===========================================
* === 5. Liste di Dati (Array) ===
* Spesso riceviamo un array di oggetti. Dobbiamo ciclarlo.
*/
// Scarichiamo 5 utenti
const responseUtenti = await fetch(`${urlBase}/users?_limit=5`);
const listaUtenti = await responseUtenti.json();
// TODO: Usa .map() o un ciclo for per creare un array contenente SOLO i nomi degli utenti.
// Esempio: const nomi = listaUtenti.map(u => u.name);
const soloNomi = [];
mostraOutput(5, soloNomi);
/**
* ===========================================
* === 6. Inviare Dati (POST) ===
* Per inviare dati, fetch accetta un secondo parametro di opzioni.
*/
// TODO: Completa l'oggetto con i dati mancanti
const nuovoUtente = {
nome: "",
cognome: "",
dataNascita: "", // Formato: 'YYYY-MM-DD'
comune: "",
email: "",
attivo: true,
avatar: "https://ui-avatars.com/api/?name=Nome+Cognome"
};
// TODO: Completa la fetch.
// Aggiungi method: 'POST'
// Aggiungi headers: { 'Content-Type': 'application/json' }
// Aggiungi body: JSON.stringify(nuovoUtente)
const rispInvio = await fetch(`${urlBase}/posts`, {
});
const risultatoInvio = await rispInvio.json();
mostraOutput(6, risultatoInvio);
}

View File

@@ -0,0 +1,118 @@
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: #f0f2f5;
color: #333;
margin: 0 auto;
}
h1 {
text-align: center;
color: #1a73e8;
margin-top: 0px;
}
.controls {
text-align: center;
margin-bottom: 30px;
position: sticky;
top: 0px;
z-index: 100;
background-color: white;
box-shadow: 0 10px 15px rgba(0, 0, 0, 0.1);
padding-top: 50px;
}
button {
padding: 12px 24px;
font-size: 16px;
cursor: pointer;
background-color: #1a73e8;
color: white;
border: none;
border-radius: 10px;
margin: 5px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
transition: transform 0.1s;
}
button:hover {
transform: scale(1.05);
}
button:active {
transform: scale(0.95);
}
button#btn-reset {
background-color: #ea4335;
}
.step-card {
background: white;
border-radius: 12px;
padding: 20px;
margin-bottom: 25px;
max-width: 800px;
margin: 0 auto 25px auto;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
border-left: 6px solid #1a73e8;
}
.step-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.step-title {
font-weight: bold;
font-size: 1.2em;
color: #1557b0;
}
.step-desc {
font-size: 0.95em;
color: #5f6368;
margin-bottom: 15px;
line-height: 1.5;
}
.output-box {
background: #2d3436;
color: #00c900;
padding: 10px;
font-family: 'Courier New', monospace;
border-radius: 6px;
min-height: 20px;
white-space: pre-wrap;
overflow-x: auto;
position: relative;
}
.output-box.loading::after {
content: "⏳ Attesa server...";
position: absolute;
top: 5px;
right: 5px;
font-size: 0.8em;
color: #e5c07b;
}
.output-box.error {
border: 2px solid #e06c75;
color: #e06c75;
}
.output-box.success {
border-left: 4px solid #98c379;
}
.label {
font-size: 0.8em;
color: #bbbbbb;
margin-bottom: 5px;
display: block;
font-weight: bold;
letter-spacing: 0.5px;
}