added project blog
This commit is contained in:
190
javascript/12_Progetti/blog/README.md
Normal file
190
javascript/12_Progetti/blog/README.md
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
# Blog Dashboard
|
||||||
|
|
||||||
|
Questo è il progetto finale del corso JavaScript.
|
||||||
|
Dovrai creare un mini-blog con **due pagine**: una per visualizzare i post e una per crearne di nuovi, usando l'API locale (`server-api`).
|
||||||
|
|
||||||
|
## Funzionalità da implementare
|
||||||
|
|
||||||
|
Le funzionalità sono divise in 3 fasi:
|
||||||
|
|
||||||
|
### Fase 1 — Visualizzare i Post
|
||||||
|
1. **Recuperare i post** — `GET /api/posts` per ottenere tutti i post
|
||||||
|
2. **Recuperare gli utenti** — `GET /api/users` per mostrare il nome dell'autore accanto a ogni post
|
||||||
|
3. **Mostrare i post in una tabella** — con autore, titolo, contenuto (troncato), likes, data
|
||||||
|
|
||||||
|
### Fase 2 — Filtri
|
||||||
|
4. **Ricerca per titolo** — un input per cercare post per titolo
|
||||||
|
5. **Filtro per autore** — un dropdown per mostrare solo i post di un certo autore
|
||||||
|
6. I filtri devono lavorare **insieme**
|
||||||
|
|
||||||
|
### Fase 3 — Creare ed Eliminare
|
||||||
|
7. **Pagina "Nuovo Post"** — Form con titolo, contenuto e autore (dropdown) + validazione
|
||||||
|
8. **Creare un post** — `POST /api/posts` per salvare il nuovo post
|
||||||
|
9. **Eliminare un post** — `DELETE /api/posts/:id` con conferma
|
||||||
|
|
||||||
|
|
||||||
|
## Struttura del Progetto
|
||||||
|
|
||||||
|
```
|
||||||
|
blog/
|
||||||
|
├── index.html ← Pagina lista post (con filtri e pulsante elimina)
|
||||||
|
├── index.js ← JS per la pagina lista (GET + DELETE + filtri)
|
||||||
|
├── nuovo.html ← Pagina creazione nuovo post (form)
|
||||||
|
├── nuovo.js ← JS per la pagina creazione (GET utenti + POST)
|
||||||
|
├── style.css ← CSS condiviso tra le due pagine
|
||||||
|
└── README.md ← Questo file
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Come Iniziare
|
||||||
|
|
||||||
|
### Prerequisiti
|
||||||
|
1. Assicurati che il `server-api` sia in esecuzione: dalla cartella `server-api`, esegui `npm start`
|
||||||
|
2. Oppure chiedi al docente di avviare il server
|
||||||
|
3. Verifica che l'API risponda: apri `http://localhost:5000/api` o il link del docente nel browser
|
||||||
|
|
||||||
|
### Setup del Progetto
|
||||||
|
1. Crea una nuova repository `BlogDashboard` su GitHub
|
||||||
|
2. Clona la repo e apri la cartella in VSCode
|
||||||
|
3. Copia tutti i file del progetto nella repo
|
||||||
|
4. Fai il primo commit: `"Setup iniziale progetto Blog Dashboard"`
|
||||||
|
|
||||||
|
Oppure fai una fork del progetto già creato dal docente e clonalo, così avrai già tutto pronto e potrai concentrarti solo sul codice.
|
||||||
|
|
||||||
|
### Workflow Git
|
||||||
|
Dopo **ogni funzionalità**, fai un commit:
|
||||||
|
```
|
||||||
|
"Fase 1: caricamento e visualizzazione post in tabella"
|
||||||
|
"Fase 1: aggiunto nome autore con join utenti"
|
||||||
|
"Fase 2: aggiunto filtro ricerca per titolo"
|
||||||
|
"Fase 2: aggiunto filtro dropdown per autore"
|
||||||
|
"Fase 3: form creazione nuovo post con validazione"
|
||||||
|
"Fase 3: implementata eliminazione post"
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## API Reference
|
||||||
|
|
||||||
|
### URL Base
|
||||||
|
```
|
||||||
|
http://localhost:5000/api
|
||||||
|
```
|
||||||
|
|
||||||
|
### Posts — `/api/posts`
|
||||||
|
| Metodo | URL | Descrizione |
|
||||||
|
|--------|-----|-------------|
|
||||||
|
| `GET` | `/api/posts` | Tutti i post |
|
||||||
|
| `POST` | `/api/posts` | Crea un nuovo post |
|
||||||
|
| `DELETE` | `/api/posts/:id` | Elimina un post |
|
||||||
|
|
||||||
|
**Struttura di un post:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"userId": 3,
|
||||||
|
"titolo": "Titolo del Post",
|
||||||
|
"contenuto": "Il contenuto completo del post...",
|
||||||
|
"likes": 12,
|
||||||
|
"data": "2024-01-15"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Users — `/api/users`
|
||||||
|
| Metodo | URL | Descrizione |
|
||||||
|
|--------|-----|-------------|
|
||||||
|
| `GET` | `/api/users` | Tutti gli utenti |
|
||||||
|
|
||||||
|
**Struttura di un utente:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"nome": "Mario",
|
||||||
|
"cognome": "Rossi",
|
||||||
|
"avatar": "https://ui-avatars.com/api/?name=Mario+Rossi&..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Esempi di Codice
|
||||||
|
|
||||||
|
### Fetch GET (leggere dati)
|
||||||
|
```javascript
|
||||||
|
const response = await fetch("http://localhost:5000/api/posts");
|
||||||
|
const posts = await response.json();
|
||||||
|
console.log(posts); // array di oggetti post
|
||||||
|
```
|
||||||
|
|
||||||
|
### Fetch POST (creare un post)
|
||||||
|
```javascript
|
||||||
|
const nuovoPost = {
|
||||||
|
userId: 3,
|
||||||
|
titolo: "Il mio primo post",
|
||||||
|
contenuto: "Ciao mondo!",
|
||||||
|
likes: 0,
|
||||||
|
data: new Date().toISOString().split("T")[0]
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await fetch("http://localhost:5000/api/posts", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(nuovoPost)
|
||||||
|
});
|
||||||
|
|
||||||
|
const postCreato = await response.json();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Fetch DELETE (eliminare un post)
|
||||||
|
```javascript
|
||||||
|
const response = await fetch("http://localhost:5000/api/posts/1", {
|
||||||
|
method: "DELETE"
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Trovare il nome dell'autore (join)
|
||||||
|
```javascript
|
||||||
|
const autore = utenti.find(u => u.id === post.userId);
|
||||||
|
const nomeAutore = autore ? autore.nome + " " + autore.cognome : "Sconosciuto";
|
||||||
|
```
|
||||||
|
|
||||||
|
### Troncare il testo
|
||||||
|
```javascript
|
||||||
|
const testoCorto = post.contenuto.substring(0, 50) + "...";
|
||||||
|
```
|
||||||
|
|
||||||
|
### Filtrare un array
|
||||||
|
```javascript
|
||||||
|
const risultati = posts.filter(post => {
|
||||||
|
return post.titolo.toLowerCase().includes(testoCercato.toLowerCase());
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## 💡 Suggerimenti
|
||||||
|
|
||||||
|
1. **Ordine di lavoro nella Fase 1**: prima carica gli utenti, poi i post. Ti servono gli utenti già pronti in memoria per poter mostrare il nome dell'autore quando crei le righe della tabella.
|
||||||
|
|
||||||
|
2. **Variabili globali**: salva utenti e post in variabili `let` globali. Così puoi riusarli nelle funzioni di filtro senza ricaricarli.
|
||||||
|
|
||||||
|
3. **Il dropdown autore (Fase 2)**: il `value` di una `<option>` è sempre una **stringa**. Ma `userId` nel post è un **numero**. Per confrontarli devi convertire uno dei due (es. `Number(dropdown.value)` oppure `post.userId.toString()`).
|
||||||
|
|
||||||
|
4. **Validazione form (Fase 3)**: controlla i campi PRIMA di fare il fetch. Non mandare dati vuoti all'API.
|
||||||
|
|
||||||
|
5. **Dopo la POST**: resetta il form con `form.reset()` e mostra un messaggio di conferma per far capire all'utente che il post è stato creato.
|
||||||
|
|
||||||
|
|
||||||
|
## 🐛 Debug
|
||||||
|
|
||||||
|
### I post non si vedono
|
||||||
|
- Apri la Console (F12) e cerca errori
|
||||||
|
- Prova l'URL nel browser: `http://localhost:5000/api/posts`
|
||||||
|
- Controlla che il server sia avviato
|
||||||
|
|
||||||
|
### Il nome autore non appare
|
||||||
|
- Hai caricato gli utenti PRIMA dei post?
|
||||||
|
- Controlla che `userId` del post corrisponda a un `id` nella lista utenti
|
||||||
|
|
||||||
|
### La POST non funziona
|
||||||
|
- Hai messo `method: "POST"`?
|
||||||
|
- Hai l'header `"Content-Type": "application/json"`?
|
||||||
|
- Hai usato `JSON.stringify()` nel body?
|
||||||
|
- Controlla la tab **Network** in DevTools per vedere la richiesta
|
||||||
46
javascript/12_Progetti/blog/index.html
Normal file
46
javascript/12_Progetti/blog/index.html
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="it">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Blog Dashboard</title>
|
||||||
|
<link rel="stylesheet" href="style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<header>
|
||||||
|
<h1>📝 Blog Dashboard</h1>
|
||||||
|
<p class="subtitle">Visualizza e gestisci i post del blog</p>
|
||||||
|
<nav class="navigation">
|
||||||
|
<a href="index.html" class="nav-link active">📋 Lista Post</a>
|
||||||
|
<a href="nuovo.html" class="nav-link">➕ Nuovo Post</a>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- FILTRI -->
|
||||||
|
<div class="filters-section">
|
||||||
|
<input type="text" id="searchInput" class="search-bar" placeholder="Cerca per titolo...">
|
||||||
|
<select id="authorFilter" class="filter-select">
|
||||||
|
<option value="">Tutti gli autori</option>
|
||||||
|
<!-- Opzioni generate dinamicamente -->
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- TABELLA POST -->
|
||||||
|
<!--
|
||||||
|
Qui manca la tabella.
|
||||||
|
La struttura sarà:
|
||||||
|
- Un div con classe "table-section"
|
||||||
|
- Un h2 con il titolo "Post del Blog"
|
||||||
|
- Una tabella con id "postsTable" che contiene:
|
||||||
|
- Thead con le intestazioni: Autore, Titolo, Contenuto, Likes, Data, Azioni
|
||||||
|
- Tbody con id "postsTableBody" dove verranno inserite le righe dinamicamente
|
||||||
|
-->
|
||||||
|
|
||||||
|
<!-- MESSAGGIO (per stato vuoto o errori) -->
|
||||||
|
<div id="message" class="message nascosto"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="index.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
89
javascript/12_Progetti/blog/index.js
Normal file
89
javascript/12_Progetti/blog/index.js
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
/**
|
||||||
|
* Progetto JS - Blog Dashboard (Pagina Lista Post)
|
||||||
|
*
|
||||||
|
* API Base URL: http://localhost:5000/api
|
||||||
|
* Risorse usate: /posts, /users
|
||||||
|
*
|
||||||
|
* In questa pagina si usano:
|
||||||
|
* - GET → Leggere i post e gli utenti
|
||||||
|
* - DELETE → Eliminare un post
|
||||||
|
*
|
||||||
|
* =============================================
|
||||||
|
* FASE 1 — VISUALIZZARE I POST
|
||||||
|
* =============================================
|
||||||
|
*
|
||||||
|
* 1. Metti a posto il file index.html con la struttura base (manca qualcosa)
|
||||||
|
* 2. Al caricamento della pagina, recupera i post e gli utenti dall'API
|
||||||
|
* 3. Mostra i post in una tabella con le colonne indicate nel file HTML
|
||||||
|
* 4. Per ogni post, mostra il nome dell'autore (recuperato dagli utenti)
|
||||||
|
* e tronca il contenuto a 50 caratteri + "..." se è più lungo
|
||||||
|
* 5. Aggiungi un pulsante "Elimina" per ogni post (colonna Azioni) al click
|
||||||
|
*
|
||||||
|
* Suggerimenti per l'implementazione:
|
||||||
|
* - Crea una funzione async "caricaPosts()" che fa un fetch GET a /api/posts
|
||||||
|
* e salva i post in una variabile globale (es. "posts")
|
||||||
|
* - Crea una funzione async "caricaUtenti()" che fa un fetch GET a /api/users
|
||||||
|
* e salva gli utenti in una variabile globale (es. "utenti")
|
||||||
|
* - Crea una funzione "mostraPosts(postsDaMostrare)" che mostra i post nella tabella
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* =============================================
|
||||||
|
* FASE 2 — FILTRI
|
||||||
|
* =============================================
|
||||||
|
*
|
||||||
|
* 1. Popola il dropdown di filtro autore (#authorFilter) con i nomi degli utenti già recuperati
|
||||||
|
* 2. Fai in modo che quando l'utente scrive qualcosa nell'input di ricerca o seleziona un autore dal dropdown, i post mostrati si filtrino di conseguenza
|
||||||
|
* 3. Il filtro per titolo deve cercare i post il cui titolo contiene il testo inserito (non deve essere una corrispondenza esatta)
|
||||||
|
*
|
||||||
|
* Suggerimenti per l'implementazione:
|
||||||
|
* - Crea una funzione "popolaFiltroAutori()" che aggiunge un <option> per ogni autore al dropdown
|
||||||
|
* - Crea una funzione "filtraPosts()" che legge i valori dei filtri e filtra l'array completo dei post (la variabile globale) in base a quei valori, poi chiama mostraPosts() con il NUOVO array filtrato
|
||||||
|
* - Aggiungi gli event listener per i filtri: sull'input di ricerca (evento "input") e sul dropdown autore (evento "change") → entrambi chiamano filtraPosts()
|
||||||
|
* - Per il filtro autore, ricorda che il value del dropdown è una stringa, mentre l'userId nei post è un numero → usa parseInt() per confrontarli correttamente
|
||||||
|
* - I due filtri devono lavorare insieme, quindi filtraPosts() deve applicare entrambi i filtri all'array completo dei post
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* =============================================
|
||||||
|
* FASE 3 — ELIMINARE UN POST
|
||||||
|
* =============================================
|
||||||
|
*
|
||||||
|
* 1. Aggiungi un pulsante "Elimina" per ogni post nella tabella (colonna Azioni) se non fatto già prima
|
||||||
|
* 2. Quando l'utente clicca su "Elimina", chiedi conferma con confirm("Sei sicuro?") o simile
|
||||||
|
* 3. Se l'utente conferma, fai un fetch DELETE a /api/posts/{id} per eliminare il post
|
||||||
|
* 4. Se l'eliminazione ha successo, ricarica i post
|
||||||
|
*
|
||||||
|
* Suggerimenti per l'implementazione:
|
||||||
|
* - Crea una funzione "eliminaPost(postId)" che fa un fetch DELETE a /api/posts/{postId} e ricarica tutti i post
|
||||||
|
* - Usa confirm() per chiedere conferma all'utente prima di eliminare (ovvero all'inizio della funzione)
|
||||||
|
* - Usa try/catch per gestire eventuali errori di rete durante l'eliminazione
|
||||||
|
* - MODIFICA la funzione mostraPosts() per aggiungere un event listener al nuovo pulsante "Elimina" di ogni post, che chiama eliminaPost() con l'id del post da eliminare
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
52
javascript/12_Progetti/blog/nuovo.html
Normal file
52
javascript/12_Progetti/blog/nuovo.html
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="it">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Nuovo Post - Blog Dashboard</title>
|
||||||
|
<link rel="stylesheet" href="style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<header>
|
||||||
|
<h1>📝 Blog Dashboard</h1>
|
||||||
|
<p class="subtitle">Crea un nuovo post per il blog</p>
|
||||||
|
<nav class="navigation">
|
||||||
|
<a href="index.html" class="nav-link">📋 Lista Post</a>
|
||||||
|
<a href="nuovo.html" class="nav-link active">➕ Nuovo Post</a>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- FORM NUOVO POST -->
|
||||||
|
<div class="form-section">
|
||||||
|
<h2>Crea un Nuovo Post</h2>
|
||||||
|
<form id="postForm">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="postTitolo">Titolo</label>
|
||||||
|
<input type="text" id="postTitolo" placeholder="Titolo del post...">
|
||||||
|
<span class="error-msg" id="titoloError"></span>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="postContenuto">Contenuto</label>
|
||||||
|
<textarea id="postContenuto" rows="5" placeholder="Scrivi il contenuto del post..."></textarea>
|
||||||
|
<span class="error-msg" id="contenutoError"></span>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="postAutore">Autore</label>
|
||||||
|
<select id="postAutore">
|
||||||
|
<option value="">Seleziona un autore...</option>
|
||||||
|
<!-- Opzioni generate dinamicamente -->
|
||||||
|
</select>
|
||||||
|
<span class="error-msg" id="autoreError"></span>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary">Pubblica Post</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- MESSAGGIO di conferma o errore -->
|
||||||
|
<div id="message" class="message nascosto"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="nuovo.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
34
javascript/12_Progetti/blog/nuovo.js
Normal file
34
javascript/12_Progetti/blog/nuovo.js
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
/**
|
||||||
|
* Progetto JS - Blog Dashboard (Pagina Nuovo Post)
|
||||||
|
*
|
||||||
|
* API Base URL: http://localhost:5000/api
|
||||||
|
* Risorse usate: /posts, /users
|
||||||
|
*
|
||||||
|
* In questa pagina si usa:
|
||||||
|
* - GET → Caricare la lista utenti per il dropdown autore
|
||||||
|
* - POST → Creare un nuovo post
|
||||||
|
*
|
||||||
|
* =============================================
|
||||||
|
* FASE 3 — CREARE UN POST
|
||||||
|
* =============================================
|
||||||
|
*
|
||||||
|
* 1. Al caricamento della pagina, recupera gli utenti dall'API e popola il dropdown autore (#postAutore)
|
||||||
|
* con un <option> per ogni utente (value = id, testo = nome + cognome)
|
||||||
|
* 2. Quando l'utente invia il form (#postForm), valida i campi:
|
||||||
|
* - Il titolo deve avere almeno 3 caratteri
|
||||||
|
* - Il contenuto non deve essere vuoto
|
||||||
|
* - Deve essere selezionato un autore
|
||||||
|
* Se un campo non è valido, mostra un messaggio di errore nello <span> corrispondente
|
||||||
|
* 3. Se il form è valido, crea un nuovo post con una fetch POST a /api/posts
|
||||||
|
* Il body della richiesta deve contenere: userId, titolo, contenuto, likes (0) e data (oggi)
|
||||||
|
* 4. Se la creazione va a buon fine, resetta il form e mostra un messaggio di successo
|
||||||
|
*
|
||||||
|
* Suggerimenti per l'implementazione:
|
||||||
|
* - Ricordati di usare e.preventDefault() nel submit del form per evitare il ricaricamento della pagina
|
||||||
|
* - Per la fetch POST servono: method "POST", header "Content-Type: application/json", e body con JSON.stringify()
|
||||||
|
* - Per la data di oggi: new Date().toISOString().split("T")[0]
|
||||||
|
* - Il value del dropdown autore è una stringa, ma userId deve essere un numero → usa Number() o parseInt()
|
||||||
|
* - Usa try/catch per gestire eventuali errori di rete
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
290
javascript/12_Progetti/blog/style.css
Normal file
290
javascript/12_Progetti/blog/style.css
Normal file
@@ -0,0 +1,290 @@
|
|||||||
|
/* ===== RESET & BASE ===== */
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
|
background-color: #f0f2f5;
|
||||||
|
color: #1a1a2e;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== CONTAINER ===== */
|
||||||
|
.container {
|
||||||
|
max-width: 960px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== HEADER ===== */
|
||||||
|
header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
padding: 20px;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
border-radius: 12px;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
header h1 {
|
||||||
|
font-size: 2rem;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
opacity: 0.9;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== NAVIGAZIONE ===== */
|
||||||
|
.navigation {
|
||||||
|
margin-top: 15px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link {
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
padding: 8px 20px;
|
||||||
|
border-radius: 20px;
|
||||||
|
border: 2px solid rgba(255, 255, 255, 0.4);
|
||||||
|
transition: all 0.3s;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
border-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link.active {
|
||||||
|
background: rgba(255, 255, 255, 0.25);
|
||||||
|
border-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== FILTRI ===== */
|
||||||
|
.filters-section {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-bar {
|
||||||
|
flex: 1;
|
||||||
|
padding: 10px 16px;
|
||||||
|
border: 2px solid #ddd;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 1rem;
|
||||||
|
transition: border-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-bar:focus {
|
||||||
|
border-color: #667eea;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-select {
|
||||||
|
padding: 10px 16px;
|
||||||
|
border: 2px solid #ddd;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 1rem;
|
||||||
|
background: white;
|
||||||
|
cursor: pointer;
|
||||||
|
min-width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-select:focus {
|
||||||
|
border-color: #667eea;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== FORM ===== */
|
||||||
|
.form-section {
|
||||||
|
background: white;
|
||||||
|
padding: 24px;
|
||||||
|
border-radius: 12px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-section h2 {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #555;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input,
|
||||||
|
.form-group textarea,
|
||||||
|
.form-group select {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px 14px;
|
||||||
|
border: 2px solid #ddd;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-family: inherit;
|
||||||
|
transition: border-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input:focus,
|
||||||
|
.form-group textarea:focus,
|
||||||
|
.form-group select:focus {
|
||||||
|
border-color: #667eea;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-msg {
|
||||||
|
color: #e74c3c;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
margin-top: 4px;
|
||||||
|
display: block;
|
||||||
|
min-height: 1.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== BOTTONI ===== */
|
||||||
|
.btn {
|
||||||
|
padding: 10px 20px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
opacity: 0.9;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-delete {
|
||||||
|
background: #e74c3c;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 6px 14px;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-delete:hover {
|
||||||
|
background: #c0392b;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== TABELLA ===== */
|
||||||
|
.table-section {
|
||||||
|
background: white;
|
||||||
|
padding: 24px;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-section h2 {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
thead th {
|
||||||
|
background: #f8f9fa;
|
||||||
|
padding: 12px 10px;
|
||||||
|
text-align: left;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #555;
|
||||||
|
border-bottom: 2px solid #e0e0e0;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody tr {
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody tr:hover {
|
||||||
|
background: #f8f9ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody td {
|
||||||
|
padding: 12px 10px;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.truncated {
|
||||||
|
color: #777;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== MESSAGGI ===== */
|
||||||
|
.message {
|
||||||
|
padding: 14px 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
text-align: center;
|
||||||
|
font-weight: 500;
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.success {
|
||||||
|
background: #d4edda;
|
||||||
|
color: #155724;
|
||||||
|
border: 1px solid #c3e6cb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.error {
|
||||||
|
background: #f8d7da;
|
||||||
|
color: #721c24;
|
||||||
|
border: 1px solid #f5c6cb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nascosto {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== RESPONSIVE ===== */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.filters-section {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-select {
|
||||||
|
min-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
thead th,
|
||||||
|
tbody td {
|
||||||
|
padding: 8px 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navigation {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user