Creiamo finalmente la Dashboard, la prima vera pagina Web consultabile !
mkdir -p /opt/pokemon/backend/app/static
nano /opt/pokemon/backend/app/static/dashboard.html
Inserire:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Pokemon Dashboard</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-datalabels@2"></script>
</head>
<body>
<h1>Pokemon Dashboard</h1>
<canvas id="statsChart" width="800" height="400"></canvas>
<script>
async function loadStats() {
const response = await fetch('/stats?user_id=1');
const stats = await response.json();
Chart.register(ChartDataLabels);
new Chart(document.getElementById('statsChart'), {
type: 'bar',
data: {
labels: [
'Carte Totali',
'Carte Diverse',
'Set Iniziati'
],
datasets: [{
label: 'Statistiche',
data: [
stats.total_cards,
stats.unique_cards,
stats.sets_started
],
backgroundColor: [
'#1976d2',
'#388e3c',
'#f57c00'
]
}]
},
options: {
responsive: true,
plugins: {
datalabels: {
anchor: 'end',
align: 'top',
color: '#000',
font: {
weight: 'bold',
size: 16
}
}
},
scales: {
y: {
beginAtZero: true
}
}
}
});
}
loadStats();
</script>
</body>
</html>
Salva e chiudi
Ricarica i file nel container:
cd /opt/pokemon
docker-compose down
docker-compose build --no-cache
docker-compose up -d
Verifichiamo aprendo la pagina web http://vm ip:8000/dashboard dovrebbe mostrare una pagina con un grafico
Creiamo finalmente la prima vera Home Page !
nano /opt/pokemon/backend/app/static/index.html
Inseriamo:
<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="utf-8">
<title>Pokemon DB</title>
<link rel="stylesheet" href="/static/style.css">
</head>
<body>
<div class="header">
<div class="logo">Pokemon DB</div>
<div class="user">Paolo ▼</div>
</div>
<div class="container">
<div class="set-info">
<label>Set:</label>
<select>
<option>Pitch Black</option>
</select>
</div>
<div class="toolbar">
<button>Ricerca</button>
<button>Aggiungi</button>
<button>Collezione</button>
<button>Statistiche</button>
</div>
<div class="set-info">
<h2>Pitch Black</h2>
<p>Tutte le carte del set</p>
</div>
<div id="card-grid" class="card-grid"></div>
</div>
<script>
async function loadCards() {
const response = await fetch('/set/me5/cards');
const cards = await response.json();
const grid = document.getElementById('card-grid');
cards.forEach(card => {
const div = document.createElement('div');
div.className = 'card';
div.innerHTML = `
<img src="${card.image_small}" alt="${card.name}">
<div class="card-body">
<div class="card-number">#${card.card_number}</div>
<div class="card-name">${card.name}</div>
<div class="card-rarity">${card.rarity || ''}</div>
</div>
<button>Aggiungi</button>
`;
grid.appendChild(div);
});
}
loadCards();
</script>
</body>
</html>
Salva ed esci poi
nano /opt/pokemon/backend/app/static/style.css
e inserisci:
body {
margin: 0;
padding: 0;
background: #eef2f5;
font-family: Arial, sans-serif;
}
.header {
background: white;
height: 70px;
padding: 0 30px;
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.logo {
font-size: 28px;
font-weight: bold;
color: #1976d2;
}
.user {
font-size: 18px;
}
.container {
padding: 20px;
}
.set-info {
background: white;
padding: 20px;
border-radius: 12px;
margin-bottom: 20px;
}
.toolbar {
background: white;
padding: 15px;
border-radius: 12px;
margin-bottom: 20px;
display: flex;
gap: 10px;
}
.toolbar button {
border: none;
border-radius: 8px;
padding: 10px 15px;
cursor: pointer;
}
.card-grid {
display: grid;
grid-template-columns: repeat(auto-fill, 180px);
gap: 20px;
}
.card {
width: 180px;
}
.card img {
width: 100%;
display: block;
}
.card-body {
padding: 10px;
}
.card-number {
font-weight: bold;
}
.card-name {
margin-top: 5px;
font-size: 18px;
}
.card-rarity {
color: #666;
margin-top: 5px;
}
.card button {
width: 100%;
border: none;
padding: 12px;
background: #1976d2;
color: white;
cursor: pointer;
}
Salva ed esci poi sostituisci il primo blocco di main.py con questo:
from fastapi.responses import FileResponse
from fastapi.staticfiles import StaticFiles
from fastapi import FastAPI
from pydantic import BaseModel
from sqlalchemy import text
from app.database import engine
app = FastAPI(
title="Pokemon Collection API"
)
app.mount(
"/static",
StaticFiles(directory="app/static"),
name="static"
)
Ricarica i file nel container:
cd /opt/pokemon
docker-compose down
docker-compose build --no-cache
docker-compose up -d
Verifichiamo aprendo la pagina web http://vm ip:8000/index dovrebbe mostrare una bozza della pagina simile a questa

Un miglioramento potrebbe essere che le carte possedute non si vedono a colori ma in bianco e nero, per farlo servono tre modifiche:
Modificare il file main.py in modo da sotiture il blocco @app.get("/set/{set_id}/cards") con questo:
@app.get("/set/{set_id}/cards")
def get_set_cards(set_id: str):
with engine.connect() as conn:
result = conn.execute(
text("""
SELECT
c.id,
c.name,
c.card_number,
c.rarity,
c.image_small,
EXISTS (
SELECT 1
FROM collection col
WHERE col.card_id = c.id
AND col.user_id = 1
) AS owned
FROM cards c
WHERE c.set_id = :set_id
ORDER BY
CASE
WHEN c.card_number ~ '^[0-9]+$'
THEN c.card_number::integer
END NULLS LAST,
c.card_number
"""),
{"set_id": set_id}
)
return result.mappings().all()
Modificare il file index.html in modo da sostiture la riga:
div.className = 'card';
con questa:
div.className = card.owned
? 'card'
: 'card not-owned';
Modificare il file css e aggiungere:
.not-owned img {
filter: grayscale(100%);
opacity: 0.30;
}
Ricompilare:
docker-compose down
docker-compose build --no-cache
docker-compose up -d
Adesso la pagina dovrebbe essere così perchè non abbiamo ancora inserito alcuna carta

La pagina adesso è di base perfettamente configurata. Per il SET scelto si ha il relativo Titolo e le relative carte, chiaramente al momento solo l’elenco delle carte attinge al DB, non è ancora possibile scegliere il SET ne tanto meno aggiungere, modificare etc..
Il prossimo passo importante è configurare il menù “SET” per la scelta del SET da DB.
Editare il file index.html
nano /opt/pokemon/backend/app/static/index.html
sostituire l'intero contenuto con:
<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="utf-8">
<title>Pokemon DB</title>
<link rel="stylesheet" href="/static/style.css">
</head>
<body>
<div class="header">
<div class="logo">Pokemon DB</div>
<div class="user">Paolo ▼</div>
</div>
<div class="container">
<div class="set-info">
<label>Set:</label>
<select id="setSelect"></select>
</div>
<div class="toolbar">
<button>Ricerca</button>
<button>Aggiungi</button>
<button>Collezione</button>
<button>Statistiche</button>
</div>
<div class="set-info">
<h2 id="setTitle">Pitch Black</h2>
<p id="setDescription">Tutte le carte del set</p>
</div>
<div id="card-grid" class="card-grid"></div>
</div>
<script>
async function loadSets() {
const response = await fetch('/sets');
const sets = await response.json();
const select = document.getElementById('setSelect');
sets.forEach(set => {
const option = document.createElement('option');
option.value = set.id;
option.textContent = set.name;
if (set.id === 'me5') {
option.selected = true;
}
select.appendChild(option);
});
select.addEventListener('change', () => {
const selectedText =
select.options[select.selectedIndex].text;
document.getElementById('setTitle').textContent =
selectedText;
loadCards(select.value);
});
}
async function loadCards(setId = 'me5') {
const response = await fetch(`/set/${setId}/cards`);
const cards = await response.json();
const grid = document.getElementById('card-grid');
grid.innerHTML = '';
cards.forEach(card => {
const div = document.createElement('div');
div.className = card.owned
? 'card'
: 'card not-owned';
div.innerHTML = `
<img src="${card.image_small}" alt="${card.name}">
<div class="card-body">
<div class="card-number">#${card.card_number}</div>
<div class="card-name">${card.name}</div>
<div class="card-rarity">${card.rarity || ''}</div>
</div>
<button>Aggiungi</button>
`;
grid.appendChild(div);
});
}
loadSets();
document.getElementById('setTitle').textContent =
'Pitch Black';
loadCards();
</script>
</body>
</html>
Ricarica il container con:
docker-compose down
docker-compose build --no-cache
docker-compose up -d
Adesso la pagina index.html ha il menù a tendina dei SET da cui so potrà scegliere il SET voluto e di conseguenza mostrerà le relative carte (vedi figura sottostante).

Il resto della guida nei prossimi giorni..
<—– Articolo Parte 5 | Articolo Parte 7 —–>
Lascia un commento
Devi essere connesso per inviare un commento.