lunedì 27 luglio 2026

Corso Pensiero Computazionale e Coding per Tutti: 1 Cos'è il pensiero computazionale

Il pensiero computazionale:
una grammatica invisibile del mondo contemporaneo

Nel dibattito pubblico sull’intelligenza artificiale e sulla trasformazione digitale, si parla spesso di algoritmi, automazione e macchine “intelligenti”. Più raramente, però, ci si sofferma su ciò che rende possibile tutto questo: una particolare forma di razionalità, discreta ma pervasiva, che precede i computer e attraversa la vita quotidiana. È ciò che chiamiamo pensiero computazionale.

Contrariamente a quanto si potrebbe immaginare, non si tratta di una competenza riservata a programmatori, ingegneri o informatici. Il pensiero computazionale è, prima di tutto, un modo di affrontare i problemi: una capacità di scomporre la complessità, individuare strutture ricorrenti, isolare ciò che conta davvero e trasformarlo in una sequenza ordinata di azioni. In altre parole, è una forma di intelligenza pratica che utilizziamo continuamente, spesso senza esserne consapevoli.

Un concetto nato prima dei computer

Il termine nasce negli ambienti della ricerca educativa. Fu Seymour Papert, matematico e pedagogista, a intuire per primo che il contatto con il pensiero computazionale potesse trasformare il modo in cui i bambini apprendono e ragionano. Decenni dopo, Jeannette Wing ne ha sistematizzato il significato, proponendolo come una competenza chiave del XXI secolo: non una tecnica, ma una vera e propria alfabetizzazione cognitiva, paragonabile alla lettura o al calcolo.

La sua forza sta proprio qui: il pensiero computazionale non insegna cosa pensare, ma come pensare problemi complessi in modo efficace.

La logica delle azioni quotidiane

Organizzare una gita, pianificare una cena, gestire un progetto di lavoro: tutte queste attività implicano decisioni, priorità, vincoli di tempo e risorse. Chi le affronta in modo efficace, senza perdersi nei dettagli o farsi paralizzare dalla complessità, sta applicando — consapevolmente o meno — i principi del pensiero computazionale.

Il processo è sempre simile: si parte da un problema indistinto, lo si scompone in parti più piccole, si eliminano gli elementi superflui, si riconoscono situazioni già note e si costruisce una sequenza di azioni coerente. È una logica che ricorda quella dei computer, ma che in realtà precede la macchina ed è profondamente umana.

Le quattro operazioni fondamentali del ragionamento computazionale

Alla base di questo approccio si trovano quattro operazioni chiave.

La prima è la decomposizione: la capacità di frammentare un problema complesso in sottoproblemi più semplici e gestibili. È il passaggio che trasforma l’opacità in chiarezza.

Segue l’astrazione, ovvero l’arte di distinguere ciò che è essenziale da ciò che è irrilevante. Astrarre non significa semplificare superficialmente, ma isolare le variabili che contano davvero, riducendo il rumore cognitivo.

Il terzo elemento è il riconoscimento di schemi. Individuare somiglianze tra situazioni diverse permette di riutilizzare soluzioni già sperimentate, risparmiando tempo ed energia mentale. È uno dei meccanismi fondamentali dell’apprendimento umano.

Infine, vi sono gli algoritmi: sequenze ordinate di istruzioni che guidano l’azione passo dopo passo. Una ricetta di cucina, un piano di lavoro, una checklist: tutti esempi di algoritmi informali che strutturano il nostro agire quotidiano.

Pensiero computazionale e formazione del cittadino

Ridurre il pensiero computazionale a una competenza tecnica sarebbe un errore. In realtà, esso ha profonde implicazioni culturali e civiche. In un mondo governato da sistemi automatici, piattaforme digitali e decisioni algoritmiche, comprendere la logica dei processi diventa una forma di consapevolezza critica.

Chi possiede questa forma di pensiero è meno incline a subire la tecnologia come una scatola nera e più capace di interrogarsi sui suoi limiti, sulle sue regole implicite e sulle sue conseguenze sociali. In questo senso, il pensiero computazionale non è solo uno strumento operativo, ma anche un antidoto all’analfabetismo digitale.

Dalla scuola alla vita adulta

Non sorprende che oggi il pensiero computazionale sia al centro di molte riforme educative. Tuttavia, il suo valore non si esaurisce nei contesti scolastici. È una competenza trasversale, applicabile alle scienze come alle discipline umanistiche, alla gestione aziendale come alle scelte personali.

Imparare a pensare in modo computazionale significa allenare la mente alla chiarezza, alla progettualità e alla responsabilità delle decisioni. In un’epoca segnata dall’eccesso di informazioni e dalla complessità sistemica, questa forma di razionalità sobria e strutturata appare sempre meno opzionale e sempre più necessaria.

In definitiva, il pensiero computazionale non ci rende simili alle macchine. Al contrario, ci aiuta a essere più consapevolmente umani in un mondo di macchine.


🧠 1 Cos'è il Pensiero Computazionale

📘 Contenuti Didattici

Il pensiero computazionale non è un superpotere riservato ai programmatori: è una modalità di ragionamento che tutti, ogni giorno, applichiamo senza rendercene conto. È la capacità di affrontare un problema, grande o piccolo, in modo logico, strutturato e strategico, un po’ come farebbe un computer… ma con la nostra creatività umana.

Il termine fu coniato da Seymour Papert e successivamente ripreso da Jeannette Wing, che lo ha reso famoso nel mondo accademico come competenza fondamentale del XXI secolo.

Un esempio quotidiano

Immagina di dover organizzare una gita con gli amici. Devi decidere la destinazione, verificare chi viene, controllare i costi, prenotare i biglietti, e infine comunicare a tutti il programma. Anche senza saperlo, hai applicato pensiero computazionale: hai scomposto il problema, ignorato dettagli irrilevanti, trovato schemi comuni e creato un “algoritmo” di azioni.

Le quattro componenti fondamentali

  • 🔹 Decomposizione: Scomporre un problema in parti più piccole e gestibili. Esempio svolto: “Preparare una torta” diventa: comprare ingredienti → preparare impasto → infornare → decorare.
  • 🔹 Astrazione: Concentrarsi sugli elementi essenziali, ignorando ciò che non è rilevante. Esempio: Nel preparare la torta, non serve considerare il colore del grembiule, ma sì la temperatura del forno.
  • 🔹 Riconoscimento di schemi: Identificare modelli ricorrenti che possono semplificare il lavoro. Esempio: Notare che molte ricette prevedono “mescolare ingredienti secchi” e poi “aggiungere quelli liquidi”.
  • 🔹 Algoritmi: Creare una sequenza di istruzioni precise per risolvere un problema. Esempio: La ricetta scritta è un algoritmo: passo 1, passo 2, passo 3…

Questi strumenti non servono solo in informatica: sono utili in matematica, scienze, gestione aziendale, e perfino nelle decisioni personali.

🛠️ Attività ed Esercizi Svolti

Per imparare il pensiero computazionale bisogna “sporcarsi le mani” con esercizi concreti.

Esercizio 1 – Cena per 10 persone

Problema: Devi preparare una cena per 10 ospiti.

Soluzione con pensiero computazionale:

  1. Decomposizione: scegliere il menù, comprare ingredienti, cucinare, apparecchiare, servire.
  2. Astrazione: ignorare dettagli estetici non necessari (es. forma dei piatti) e concentrarsi su quantità e tempi.
  3. Riconoscimento di schemi: ricordare che ogni volta serve organizzare le cotture in modo da servire caldo.
  4. Algoritmo: redigere una tabella con orari di preparazione per ogni portata.

Esercizio 2 – Labirinto logico

Problema: Come uscire da un labirinto seguendo sempre la mano destra appoggiata al muro.

Risultato: Hai creato un algoritmo “meccanico” che funziona senza pensare, basato su una regola ricorrente.

📝 Test di Verifica

  1. Quale delle seguenti NON è una componente del pensiero computazionale?
    A. Decomposizione
    B. Intuizione
    C. Riconoscimento di schemi
    D. Algoritmi
    ✅ Soluzione

    B. Intuizione

  2. A cosa serve la decomposizione?
    A. Ignorare i dettagli
    B. Scomporre un problema complesso in parti più semplici
    C. Creare una lista di spesa
    D. Fare un disegno
    ✅ Soluzione

    B. Scomporre un problema complesso in parti più semplici

  3. Se segui una ricetta passo passo, stai usando:
    A. Un algoritmo
    B. Una mappa concettuale
    C. Una previsione
    D. Un ragionamento astratto
    ✅ Soluzione

    A. Un algoritmo

  4. Perché l’astrazione è utile?
    A. Per fare più calcoli
    B. Per evitare di concentrarsi su dettagli irrilevanti
    C. Per ricordare i passaggi
    D. Per aumentare la complessità
    ✅ Soluzione

    B. Per evitare di concentrarsi su dettagli irrilevanti

  5. Quale attività richiede maggiormente il riconoscimento di schemi?
    A. Risolvere un sudoku
    B. Fare la spesa
    C. Scrivere un tema
    D. Disegnare un ritratto
    ✅ Soluzione

    A. Risolvere un sudoku

📌 Suggerimenti per l’autovalutazione

Al termine della lezione, chiediti:

  • ✏️ Riesco a spiegare cos’è il pensiero computazionale a un amico senza usare parole difficili?
  • 🔍 Posso individuare le 4 componenti in una situazione reale della mia giornata?
  • 📋 Sono in grado di scrivere un algoritmo per risolvere un problema quotidiano?
  • 🎯 Posso applicare questo metodo in discipline diverse, come storia o scienze?

martedì 31 marzo 2026

Corso di JavaScript & Programmazione Web: 11 – Codice del sito di e-commerce

11 – Codice del sito di e-commerce

merchant-backend/
├─ package.json
├─ .env.example
└─ src/
   ├─ index.js
   ├─ config/
   │  └─ db.js
   ├─ models/
   │  ├─ User.js
   │  └─ Product.js
   ├─ controllers/
   │  ├─ authController.js
   │  └─ productController.js
   └─ routes/
      ├─ auth.js
      └─ products.js

{
  "name": "merchant-backend",
  "version": "1.0.0",
  "description": "Backend per sito commerciante con e-commerce minimale (Node/Express/MongoDB/JWT)",
  "main": "src/index.js",
  "scripts": {
    "start": "node src/index.js",
    "dev": "nodemon src/index.js",
    "lint": "eslint . --ext .js"
  },
  "author": "Tuonome",
  "license": "MIT",
  "dependencies": {
    "bcryptjs": "^2.4.3",
    "cors": "^2.8.5",
    "dotenv": "^16.1.4",
    "express": "^4.18.2",
    "express-async-errors": "^3.1.1",
    "jsonwebtoken": "^9.0.0",
    "mongoose": "^7.5.0",
    "morgan": "^1.10.0",
    "multer": "^1.4.5-lts.1"
  },
  "devDependencies": {
    "nodemon": "^2.0.22",
    "eslint": "^8.50.0"
  }
}

# Rename to .env and fill values
PORT=4000
MONGO_URI=mongodb://localhost:27017/merchantdb
JWT_SECRET=changeme_super_secret_jwt_key
JWT_EXPIRES_IN=7d
ADMIN_EMAIL=admin@example.com
ADMIN_PASSWORD=ChangeMe123!
UPLOADS_DIR=uploads


/**
 * Entry point for the backend server
 * - express app
 * - basic middlewares (cors, json, morgan)
 * - routes: /api/auth, /api/products
 * - global error handler
 */

require('dotenv').config();
require('express-async-errors'); // to allow throwing errors in async routes
const express = require('express');
const morgan = require('morgan');
const cors = require('cors');
const path = require('path');

const connectDB = require('./config/db');
const authRoutes = require('./routes/auth');
const productRoutes = require('./routes/products');

const app = express();

// Connect DB
connectDB();

// Middlewares
app.use(cors());
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true }));
app.use(morgan('dev'));

// Serve uploaded images (if using local storage)
const uploadsDir = process.env.UPLOADS_DIR || 'uploads';
app.use(`/static/${uploadsDir}`, express.static(path.join(__dirname, '..', uploadsDir)));

// API Routes
app.use('/api/auth', authRoutes);
app.use('/api/products', productRoutes);

// Health check
app.get('/api/health', (req, res) => res.json({ status: 'ok', time: new Date().toISOString() }));

// Global error handler
// eslint-disable-next-line no-unused-vars
app.use((err, req, res, next) => {
  console.error(err);
  const status = err.status || 500;
  const message = err.message || 'Internal Server Error';
  res.status(status).json({ error: message, stack: process.env.NODE_ENV === 'production' ? undefined : err.stack });
});

// Start server
const PORT = process.env.PORT || 4000;
app.listen(PORT, () => {
  // create admin user on startup? optional, can be implemented
  console.log(`Server running on port ${PORT}`);
});


/**
 * MongoDB connection via mongoose
 */

const mongoose = require('mongoose');

const connectDB = async () => {
  const uri = process.env.MONGO_URI;
  if (!uri) {
    console.error('MONGO_URI not set in environment');
    process.exit(1);
  }
  try {
    await mongoose.connect(uri, {
      // options (mongoose 7+ has sensible defaults)
    });
    console.log('MongoDB connected');
  } catch (err) {
    console.error('MongoDB connection error:', err);
    process.exit(1);
  }
};

module.exports = connectDB;

/**
 * User model (basic for authentication)
 * Roles: user, admin
 */

const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');

const UserSchema = new mongoose.Schema({
  name: { type: String, required: true, trim: true, maxlength: 100 },
  email: { type: String, required: true, unique: true, lowercase: true, trim: true },
  password: { type: String, required: true },
  role: { type: String, enum: ['user', 'admin'], default: 'user' },
  createdAt: { type: Date, default: Date.now }
});

// Hash password before save
UserSchema.pre('save', async function (next) {
  if (!this.isModified('password')) return next();
  const salt = await bcrypt.genSalt(10);
  this.password = await bcrypt.hash(this.password, salt);
  next();
});

// Instance method to compare password
UserSchema.methods.comparePassword = async function (candidate) {
  return bcrypt.compare(candidate, this.password);
};

module.exports = mongoose.model('User', UserSchema);



/**
 * Product model for e-commerce items
 * Images: store relative path or URL
 */

const mongoose = require('mongoose');

const ProductSchema = new mongoose.Schema({
  title: { type: String, required: true, trim: true, maxlength: 200 },
  slug: { type: String, required: true, unique: true }, // url-friendly
  description: { type: String, default: '' },
  price: { type: Number, required: true, min: 0 },
  currency: { type: String, default: 'EUR' },
  stock: { type: Number, default: 0 },
  category: { type: String, default: 'general' },
  images: [{ type: String }], // store URLs or local paths (/static/uploads/...)
  featured: { type: Boolean, default: false },
  createdAt: { type: Date, default: Date.now },
  updatedAt: { type: Date, default: Date.now }
});

ProductSchema.pre('save', function (next) {
  this.updatedAt = Date.now();
  next();
});

module.exports = mongoose.model('Product', ProductSchema);


/**
 * Auth controller: register, login, profile
 */

const jwt = require('jsonwebtoken');
const User = require('../models/User');

const JWT_SECRET = process.env.JWT_SECRET || 'changeme';
const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN || '7d';

const signToken = (user) => {
  return jwt.sign({ id: user._id, role: user.role, email: user.email }, JWT_SECRET, { expiresIn: JWT_EXPIRES_IN });
};

exports.register = async (req, res) => {
  const { name, email, password } = req.body;
  if (!name || !email || !password) return res.status(400).json({ error: 'name, email, password required' });

  const existing = await User.findOne({ email });
  if (existing) return res.status(409).json({ error: 'Email already registered' });

  const user = new User({ name, email, password });
  await user.save();

  const token = signToken(user);
  res.status(201).json({ token, user: { id: user._id, name: user.name, email: user.email, role: user.role } });
};

exports.login = async (req, res) => {
  const { email, password } = req.body;
  if (!email || !password) return res.status(400).json({ error: 'email and password required' });

  const user = await User.findOne({ email });
  if (!user) return res.status(401).json({ error: 'Invalid credentials' });

  const match = await user.comparePassword(password);
  if (!match) return res.status(401).json({ error: 'Invalid credentials' });

  const token = signToken(user);
  res.json({ token, user: { id: user._id, name: user.name, email: user.email, role: user.role } });
};

exports.me = async (req, res) => {
  // expects req.user set by auth middleware
  const user = await User.findById(req.user.id).select('-password');
  res.json({ user });
};

/**
 * Product controller: CRUD operations
 * - list with pagination / filters
 * - get by slug / id
 * - create (admin)
 * - update (admin)
 * - delete (admin)
 *
 * Note: For image upload we will accept a multipart/form-data route in the route file using multer.
 */

const Product = require('../models/Product');
const slugify = (s) => s.toLowerCase().trim().replace(/\s+/g, '-').replace(/[^\w\-]+/g, '');

exports.list = async (req, res) => {
  const page = Math.max(1, parseInt(req.query.page) || 1);
  const limit = Math.min(100, parseInt(req.query.limit) || 12);
  const skip = (page - 1) * limit;

  const filter = {};
  if (req.query.category) filter.category = req.query.category;
  if (req.query.q) filter.$or = [
    { title: { $regex: req.query.q, $options: 'i' } },
    { description: { $regex: req.query.q, $options: 'i' } }
  ];

  const [items, total] = await Promise.all([
    Product.find(filter).sort({ featured: -1, createdAt: -1 }).skip(skip).limit(limit),
    Product.countDocuments(filter)
  ]);

  res.json({ items, meta: { page, limit, total, pages: Math.ceil(total / limit) } });
};

exports.getBySlug = async (req, res) => {
  const product = await Product.findOne({ slug: req.params.slug });
  if (!product) return res.status(404).json({ error: 'Product not found' });
  res.json({ product });
};

exports.create = async (req, res) => {
  const { title, description, price, currency = 'EUR', stock = 0, category = 'general', images = [] } = req.body;
  if (!title || price == null) return res.status(400).json({ error: 'title and price required' });

  const slug = slugify(title);
  const existing = await Product.findOne({ slug });
  let finalSlug = slug;
  if (existing) finalSlug = `${slug}-${Date.now().toString().slice(-4)}`;

  const p = new Product({
    title, slug: finalSlug, description, price: parseFloat(price), currency, stock: parseInt(stock, 10), category, images
  });
  await p.save();
  res.status(201).json({ product: p });
};

exports.update = async (req, res) => {
  const updates = req.body;
  if (updates.title) updates.slug = slugify(updates.title);
  const product = await Product.findByIdAndUpdate(req.params.id, updates, { new: true });
  if (!product) return res.status(404).json({ error: 'Not found' });
  res.json({ product });
};

exports.remove = async (req, res) => {
  const product = await Product.findByIdAndDelete(req.params.id);
  if (!product) return res.status(404).json({ error: 'Not found' });
  res.json({ success: true });
};


/**
 * Auth routes
 */

const express = require('express');
const router = express.Router();
const authController = require('../controllers/authController');
const { authMiddleware } = require('../utils/middleware'); // file middleware che aggiungeremo dopo

// Public
router.post('/register', authController.register);
router.post('/login', authController.login);

// Protected
router.get('/me', authMiddleware, authController.me);

module.exports = router;

/**
 * Product routes
 * - GET /      -> list, with query params
 * - GET /:slug -> get by slug
 * - POST /     -> create (admin)
 * - PUT /:id   -> update (admin)
 * - DELETE /:id -> delete (admin)
 *
 * Image uploads: optionally POST /:id/images (handled separately)
 */

const express = require('express');
const router = express.Router();
const productController = require('../controllers/productController');
const { authMiddleware, adminOnly } = require('../utils/middleware');

// Public
router.get('/', productController.list);
router.get('/slug/:slug', productController.getBySlug);

// Protected admin
router.post('/', authMiddleware, adminOnly, productController.create);
router.put('/:id', authMiddleware, adminOnly, productController.update);
router.delete('/:id', authMiddleware, adminOnly, productController.remove);

module.exports = router;

/**
 * Utility middleware: authMiddleware (JWT), adminOnly
 */

const jwt = require('jsonwebtoken');
const User = require('../models/User');

const JWT_SECRET = process.env.JWT_SECRET || 'changeme';

exports.authMiddleware = async (req, res, next) => {
  const auth = req.headers.authorization;
  if (!auth || !auth.startsWith('Bearer ')) return res.status(401).json({ error: 'Missing token' });
  const token = auth.split(' ')[1];
  try {
    const payload = jwt.verify(token, JWT_SECRET);
    // attach user minimal info
    req.user = { id: payload.id, role: payload.role, email: payload.email };
    // optionally fetch full user if needed
    // req.userFull = await User.findById(payload.id).select('-password');
    next();
  } catch (err) {
    return res.status(401).json({ error: 'Invalid token' });
  }
};

exports.adminOnly = (req, res, next) => {
  if (!req.user) return res.status(401).json({ error: 'Not authenticated' });
  if (req.user.role !== 'admin') return res.status(403).json({ error: 'Admin only' });
  next();
};


"dependencies": {
  "...": "...",
  "stripe": "^11.0.0",
  "express-validator": "^7.0.1",
  "nodemailer": "^6.9.4"
}


(esegui npm install stripe express-validator nodemailer)

.env.example — aggiornamento

Aggiungi questi parametri:

# Stripe
STRIPE_SECRET_KEY=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...

# Email (opzionale)
SMTP_HOST=smtp.example.com
SMTP_PORT=587
SMTP_USER=your@email
SMTP_PASS=secret

# Admin seed
ADMIN_NAME=Admin
ADMIN_EMAIL=admin@example.com
ADMIN_PASSWORD=ChangeMe123!

1) Upload immagini — src/utils/upload.js

Crea src/utils/upload.js per gestire multer e storage locale (cartella uploads/).

const path = require('path');
const multer = require('multer');
const fs = require('fs');

const uploadsDir = process.env.UPLOADS_DIR || path.join(__dirname, '..', '..', 'uploads');

// assicurati che la cartella esista
if (!fs.existsSync(uploadsDir)) fs.mkdirSync(uploadsDir, { recursive: true });

const storage = multer.diskStorage({
  destination: function (req, file, cb) {
    cb(null, uploadsDir);
  },
  filename: function (req, file, cb) {
    const ext = path.extname(file.originalname);
    const name = path.basename(file.originalname, ext).replace(/\s+/g, '-').toLowerCase();
    const unique = Date.now() + '-' + Math.round(Math.random() * 1e9);
    cb(null, `${name}-${unique}${ext}`);
  }
});

const fileFilter = (req, file, cb) => {
  // accetta solo immagini
  if (/image\/(jpeg|png|gif|webp)/.test(file.mimetype)) cb(null, true);
  else cb(new Error('Only image files are allowed!'), false);
};

const upload = multer({ storage, fileFilter, limits: { fileSize: 5 * 1024 * 1024 } }); // 5MB

module.exports = { upload, uploadsDir };

2) Route per upload immagini — src/routes/uploads.js
const express = require('express');
const router = express.Router();
const { upload } = require('../utils/upload');
const { authMiddleware, adminOnly } = require('../utils/middleware');
const path = require('path');

router.post('/single', authMiddleware, adminOnly, upload.single('image'), (req, res) => {
  if (!req.file) return res.status(400).json({ error: 'No file uploaded' });
  // restituisci percorso pubblico relativo
  const publicPath = `/static/${process.env.UPLOADS_DIR || 'uploads'}/${req.file.filename}`;
  res.status(201).json({ url: publicPath, filename: req.file.filename });
});

router.post('/multiple', authMiddleware, adminOnly, upload.array('images', 8), (req, res) => {
  if (!req.files || req.files.length === 0) return res.status(400).json({ error: 'No files uploaded' });
  const urls = req.files.map(f => `/static/${process.env.UPLOADS_DIR || 'uploads'}/${f.filename}`);
  res.status(201).json({ urls });
});

module.exports = router;


Aggiungi la route in src/index.js:

const uploadRoutes = require('./routes/uploads');
app.use('/api/uploads', uploadRoutes);

3) Modello Cart e Order — src/models/Cart.js e src/models/Order.js
src/models/Cart.js

(Semplice rappresentazione lato backend — cart può essere memorizzata in DB oppure solo client-side; qui implementiamo una versione server-side associata a user)

const mongoose = require('mongoose');

const CartItemSchema = new mongoose.Schema({
  product: { type: mongoose.Schema.Types.ObjectId, ref: 'Product', required: true },
  title: { type: String },
  price: { type: Number, required: true },
  qty: { type: Number, default: 1 },
  image: { type: String }
}, { _id: false });

const CartSchema = new mongoose.Schema({
  user: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true, unique: true },
  items: [CartItemSchema],
  updatedAt: { type: Date, default: Date.now }
});

CartSchema.pre('save', function (next) {
  this.updatedAt = Date.now();
  next();
});

module.exports = mongoose.model('Cart', CartSchema);

src/models/Order.js
const mongoose = require('mongoose');

const OrderItemSchema = new mongoose.Schema({
  product: { type: mongoose.Schema.Types.ObjectId, ref: 'Product' },
  title: String,
  price: Number,
  qty: Number,
  image: String
}, { _id: false });

const OrderSchema = new mongoose.Schema({
  user: { type: mongoose.Schema.Types.ObjectId, ref: 'User' },
  items: [OrderItemSchema],
  shipping: {
    name: String,
    address: String,
    city: String,
    postalCode: String,
    country: String
  },
  totalAmount: { type: Number, required: true },
  currency: { type: String, default: 'EUR' },
  payment: {
    provider: { type: String, default: 'stripe' },
    status: { type: String, enum: ['pending', 'paid', 'failed', 'refunded'], default: 'pending' },
    stripePaymentIntent: String
  },
  status: { type: String, enum: ['created','processing','shipped','completed','cancelled'], default: 'created' },
  createdAt: { type: Date, default: Date.now },
  updatedAt: { type: Date, default: Date.now }
});

OrderSchema.pre('save', function (next) {
  this.updatedAt = Date.now();
  next();
});

module.exports = mongoose.model('Order', OrderSchema);

4) Controller Carrello / Ordini — src/controllers/cartController.js e src/controllers/orderController.js
src/controllers/cartController.js
const Cart = require('../models/Cart');
const Product = require('../models/Product');

/**
 * API:
 * GET /api/cart        -> get cart for user
 * POST /api/cart/add   -> add product { productId, qty }
 * POST /api/cart/update -> update item qty { productId, qty }
 * POST /api/cart/remove -> remove item { productId }
 * POST /api/cart/clear -> clear cart
 */

exports.getCart = async (req, res) => {
  let cart = await Cart.findOne({ user: req.user.id }).populate('items.product');
  if (!cart) {
    cart = new Cart({ user: req.user.id, items: [] });
    await cart.save();
  }
  res.json({ cart });
};

exports.addToCart = async (req, res) => {
  const { productId, qty = 1 } = req.body;
  if (!productId) return res.status(400).json({ error: 'productId required' });
  const product = await Product.findById(productId);
  if (!product) return res.status(404).json({ error: 'Product not found' });

  let cart = await Cart.findOne({ user: req.user.id });
  if (!cart) cart = new Cart({ user: req.user.id, items: [] });

  const idx = cart.items.findIndex(i => i.product.toString() === productId);
  if (idx >= 0) {
    cart.items[idx].qty = Math.max(1, cart.items[idx].qty + Number(qty));
  } else {
    cart.items.push({
      product: product._id,
      title: product.title,
      price: product.price,
      qty: Number(qty),
      image: product.images && product.images[0] ? product.images[0] : ''
    });
  }
  await cart.save();
  res.json({ cart });
};

exports.updateItem = async (req, res) => {
  const { productId, qty } = req.body;
  if (!productId) return res.status(400).json({ error: 'productId required' });
  let cart = await Cart.findOne({ user: req.user.id });
  if (!cart) return res.status(404).json({ error: 'Cart not found' });

  const idx = cart.items.findIndex(i => i.product.toString() === productId);
  if (idx === -1) return res.status(404).json({ error: 'Item not in cart' });
  cart.items[idx].qty = Math.max(0, Number(qty));
  if (cart.items[idx].qty === 0) cart.items.splice(idx, 1);
  await cart.save();
  res.json({ cart });
};

exports.removeItem = async (req, res) => {
  const { productId } = req.body;
  if (!productId) return res.status(400).json({ error: 'productId required' });
  let cart = await Cart.findOne({ user: req.user.id });
  if (!cart) return res.status(404).json({ error: 'Cart not found' });

  cart.items = cart.items.filter(i => i.product.toString() !== productId);
  await cart.save();
  res.json({ cart });
};

exports.clearCart = async (req, res) => {
  await Cart.findOneAndUpdate({ user: req.user.id }, { $set: { items: [] } }, { upsert: true });
  res.json({ success: true });
};

src/controllers/orderController.js
const Order = require('../models/Order');
const Cart = require('../models/Cart');
const Product = require('../models/Product');
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);

/**
 * API:
 * POST /api/orders/create -> crea ordine a partire dal carrello (può anche avviare sessione Stripe)
 * GET  /api/orders/:id    -> dettagli ordine (user o admin)
 * GET  /api/orders        -> lista ordini (admin: tutti, user: propri)
 *
 * POST /api/orders/checkout-session -> crea sessione Stripe dalla cartella/ordine
 */

exports.createOrderFromCart = async (req, res) => {
  const cart = await Cart.findOne({ user: req.user.id }).populate('items.product');
  if (!cart || cart.items.length === 0) return res.status(400).json({ error: 'Cart empty' });

  // calcola totale
  const total = cart.items.reduce((s, it) => s + (it.price * it.qty), 0);

  const order = new Order({
    user: req.user.id,
    items: cart.items.map(i => ({
      product: i.product._id,
      title: i.title,
      price: i.price,
      qty: i.qty,
      image: i.image
    })),
    totalAmount: total,
    currency: 'EUR',
    payment: { provider: 'stripe', status: 'pending' },
    status: 'created'
  });

  await order.save();
  // opzionale: svuota carrello (o lascia al checkout)
  // await Cart.findOneAndUpdate({ user: req.user.id }, { items: [] });

  res.status(201).json({ order });
};

exports.getOrder = async (req, res) => {
  const order = await Order.findById(req.params.id).populate('user', 'name email');
  if (!order) return res.status(404).json({ error: 'Order not found' });
  // autorizzazione: user può vedere solo i suoi ordini
  if (req.user.role !== 'admin' && order.user._id.toString() !== req.user.id) {
    return res.status(403).json({ error: 'Forbidden' });
  }
  res.json({ order });
};

exports.listOrders = async (req, res) => {
  const filter = {};
  if (req.user.role !== 'admin') filter.user = req.user.id;
  const items = await Order.find(filter).sort({ createdAt: -1 }).limit(200);
  res.json({ items });
};

/**
 * Crea sessione di checkout Stripe con items dal carrello o da ordine
 * POST /api/orders/checkout-session
 * body: { orderId? , successUrl, cancelUrl }
 */
exports.createCheckoutSession = async (req, res) => {
  const { orderId, successUrl, cancelUrl } = req.body;
  let order;
  if (orderId) {
    order = await Order.findById(orderId);
    if (!order) return res.status(404).json({ error: 'Order not found' });
  } else {
    // crea ordine provvisorio dal carrello
    const cart = await Cart.findOne({ user: req.user.id }).populate('items.product');
    if (!cart || cart.items.length === 0) return res.status(400).json({ error: 'Cart empty' });
    const total = cart.items.reduce((s, it) => s + (it.price * it.qty), 0);
    order = new Order({
      user: req.user.id,
      items: cart.items.map(i => ({
        product: i.product._id,
        title: i.title,
        price: i.price,
        qty: i.qty,
        image: i.image
      })),
      totalAmount: total,
      currency: 'EUR',
      payment: { provider: 'stripe', status: 'pending' },
      status: 'created'
    });
    await order.save();
  }

  // trasforma items in line_items per stripe
  const line_items = order.items.map(it => ({
    price_data: {
      currency: order.currency || 'EUR',
      product_data: { name: it.title, images: it.image ? [ `${req.protocol}://${req.get('host')}${it.image}` ] : [] },
      unit_amount: Math.round(it.price * 100)
    },
    quantity: it.qty
  }));

  // crea sessione
  const session = await stripe.checkout.sessions.create({
    payment_method_types: ['card'],
    mode: 'payment',
    line_items,
    success_url: successUrl || `${req.protocol}://${req.get('host')}/checkout/success?session_id={CHECKOUT_SESSION_ID}`,
    cancel_url: cancelUrl || `${req.protocol}://${req.get('host')}/checkout/cancel`,
    metadata: { orderId: order._id.toString(), userId: req.user.id }
  });

  // salva intent id / session id sull'ordine (opzionale)
  order.payment.stripePaymentIntent = session.payment_intent || session.id;
  await order.save();

  res.json({ sessionId: session.id, url: session.url });
};

5) Route ordini e carrello — src/routes/cart.js e src/routes/orders.js
src/routes/cart.js
const express = require('express');
const router = express.Router();
const cartController = require('../controllers/cartController');
const { authMiddleware } = require('../utils/middleware');

router.get('/', authMiddleware, cartController.getCart);
router.post('/add', authMiddleware, cartController.addToCart);
router.post('/update', authMiddleware, cartController.updateItem);
router.post('/remove', authMiddleware, cartController.removeItem);
router.post('/clear', authMiddleware, cartController.clearCart);

module.exports = router;


Aggiungi in src/index.js:

const cartRoutes = require('./routes/cart');
app.use('/api/cart', cartRoutes);

src/routes/orders.js
const express = require('express');
const router = express.Router();
const orderController = require('../controllers/orderController');
const { authMiddleware, adminOnly } = require('../utils/middleware');

router.post('/create', authMiddleware, orderController.createOrderFromCart);
router.post('/checkout-session', authMiddleware, orderController.createCheckoutSession);
router.get('/:id', authMiddleware, orderController.getOrder);
router.get('/', authMiddleware, orderController.listOrders);

module.exports = router;


Aggiungi in src/index.js:

const orderRoutes = require('./routes/orders');
app.use('/api/orders', orderRoutes);

6) Stripe Webhook — src/routes/webhooks.js e handler

Per ricevere eventi Stripe (pagamento riuscito ecc.) è meglio esporre endpoint separato e verificare firma.

src/routes/webhooks.js
const express = require('express');
const router = express.Router();
const Order = require('../models/Order');
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
const endpointSecret = process.env.STRIPE_WEBHOOK_SECRET;

router.post('/stripe', express.raw({ type: 'application/json' }), async (req, res) => {
  const sig = req.headers['stripe-signature'];
  let event;
  try {
    event = stripe.webhooks.constructEvent(req.body, sig, endpointSecret);
  } catch (err) {
    console.error('Webhook signature verification failed.', err.message);
    return res.status(400).send(`Webhook Error: ${err.message}`);
  }

  // handle the event
  switch (event.type) {
    case 'checkout.session.completed':
      {
        const session = event.data.object;
        // metadata contains orderId if we set it
        const orderId = session.metadata && session.metadata.orderId;
        if (orderId) {
          await Order.findByIdAndUpdate(orderId, {
            'payment.status': 'paid',
            'payment.stripePaymentIntent': session.payment_intent || session.id,
            status: 'processing'
          });
        }
      }
      break;
    case 'payment_intent.payment_failed':
      {
        const intent = event.data.object;
        // qui si può aggiornare l'ordine attraverso metadata se presente
        console.warn('Payment failed', intent);
      }
      break;
    default:
      // console.log(`Unhandled event type ${event.type}`);
  }

  res.json({ received: true });
});

module.exports = router;


Aggiungi in src/index.js prima di app.use(express.json()) o gestisci solo per la route webhook come fatto:

const webhookRoutes = require('./routes/webhooks');
app.use('/api/webhooks', webhookRoutes);


Importante: express.raw() è necessario per la verifica della firma Stripe. Quando testi in locale con stripe-cli imposta il webhook secret.

7) Seed admin — src/utils/seedAdmin.js

Script che crea admin all'avvio se non esiste (opzionale).

const User = require('../models/User');

module.exports = async function seedAdmin() {
  try {
    const adminEmail = process.env.ADMIN_EMAIL;
    const adminPassword = process.env.ADMIN_PASSWORD;
    const adminName = process.env.ADMIN_NAME || 'Admin';

    if (!adminEmail || !adminPassword) {
      console.warn('ADMIN_EMAIL or ADMIN_PASSWORD not set; skipping admin seed');
      return;
    }

    const existing = await User.findOne({ email: adminEmail });
    if (existing) {
      console.log('Admin already exists');
      return;
    }

    const user = new User({
      name: adminName,
      email: adminEmail,
      password: adminPassword,
      role: 'admin'
    });
    await user.save();
    console.log('Admin user created:', adminEmail);
  } catch (err) {
    console.error('Error seeding admin:', err);
  }
};


Richiamalo in src/index.js subito dopo connectDB():

const seedAdmin = require('./utils/seedAdmin');
...
connectDB().then(() => seedAdmin());


(se hai connectDB sincrono, adegua la chiamata)


<html lang="it">
<head>
  <meta charset="UTF-8"></meta>
  <meta content="width=device-width, initial-scale=1.0" name="viewport"></meta>
  <title>Carrello - Negozio Online</title>
  <link href="style.css" rel="stylesheet"></link>
</head>
<body>
  <header>
    <div class="logo">🛍️ Il Mio Negozio</div>
    <nav>
      <ul>
        <li><a href="index.html">Home</a></li>
        <li><a href="shop.html">Prodotti</a></li>
        <li><a class="active" href="cart.html">Carrello 🛒</a></li>
        <li><a href="checkout.html">Checkout</a></li>
      </ul>
    </nav>
  </header>

  <main>
    <h1>🛒 Il tuo Carrello</h1>
    <table id="cart-table">
      <thead>
        <tr>
          <th>Prodotto</th>
          <th>Prezzo</th>
          <th>Quantità</th>
          <th>Totale</th>
          <th>Azioni</th>
        </tr>
      </thead>
      <tbody id="cart-items">
        <!-- Qui vengono inseriti i prodotti dal JS -->
      </tbody>
    </table>

    <div class="cart-summary">
      <h3>Totale Carrello: &lt;

</h3></div></main></body></html>

lunedì 30 marzo 2026

Corso di JavaScript & Programmazione Web: 10 – Progetto completo di un sito di e-commerce

 10 – Progetto completo di un sito di e-commerce

1) Visione generale e requisito minima

Obiettivo: sito commerciale per commerciante locale con catalogo, search, carrello, checkout, gestione ordini, pannello amministrativo per inventario e analisi vendite. Interazione ricca in client con JavaScript (React) e backend REST/GraphQL in Node.js. Pagamenti con Stripe Checkout (semplice e sicuro). Database: MongoDB (flessibile, JSON-like), caching Redis opzionale.

Principali caratteristiche:

  • Catalogo prodotti (categoria, tagging, filtri)

  • Carrello persistente (localStorage + server-side per utenti loggati)

  • Checkout con Stripe (Checkout Session + webhook per conferma ordine)

  • Account cliente (registro, login JWT, storico ordini)

  • Area admin (login, CRUD prodotti, gestione ordini, report)

  • SEO friendly (SSR / pre-rendering per pagine prodotto) — usare SSG/SSR su Vercel o prerender per SEO

  • PWA (manifest + service worker) per offline caching e push notifications opzionale

  • Accessibilità (a11y), performance (images optim, lazy loading), security (rate-limit, helmet, CORS, CSP)

  • Internazionalizzazione (i18n) e multi-currency opzionali

2) Stack tecnico consigliato

  • Frontend: React (Vite), React Router, Context/Redux (Context + useReducer sufficiente), Fetch/Axios, Stripe JS (@stripe/stripe-js), Tailwind CSS o CSS modulare

  • Backend: Node.js + Express, TypeScript consigliato ma qui fornisco JS per leggibilità

  • DB: MongoDB Atlas (o locale), Redis opzionale per sessioni/caching

  • Pagamenti: Stripe Checkout + Webhook per conferma

  • Autenticazione: JWT + refresh tokens (o Auth0 se vuoi esternalizzare)

  • Hosting: Frontend su Vercel o Netlify; backend su Render/Heroku/AWS Elastic Beanstalk o container Docker su cloud

  • CI/CD: GitHub Actions per build/test/deploy

  • Monitoraggio: Sentry (error tracking), Prometheus/Grafana o NewRelic

  • Search: ElasticSearch se grande catalogo, altrimenti filtri MongoDB + indexing

3) Struttura progetto (sintesi)

merchant-site/
├─ backend/
│  ├─ src/
│  │  ├─ index.js           # Express app entry
│  │  ├─ routes/
│  │  │  ├─ products.js
│  │  │  ├─ auth.js
│  │  │  ├─ orders.js
│  │  │  └─ stripeWebhook.js
│  │  ├─ controllers/
│  │  ├─ models/
│  │  │  ├─ Product.js
│  │  │  ├─ User.js
│  │  │  └─ Order.js
│  │  └─ utils/
│  ├─ .env
│  └─ Dockerfile
├─ frontend/
│  ├─ src/
│  │  ├─ main.jsx
│  │  ├─ App.jsx
│  │  ├─ pages/
│  │  │  ├─ Home.jsx
│  │  │  ├─ Product.jsx
│  │  │  ├─ Cart.jsx
│  │  │  ├─ Checkout.jsx
│  │  │  └─ Admin/
│  │  ├─ components/
│  │  └─ hooks/
│  ├─ index.html
│  └─ vite.config.js
├─ docker-compose.yml
└─ README.md

4) Codice di riferimento — Backend essenziale (Express + Stripe webhook)

Nota: sostituisci le chiavi process.env.* e gli URL con i tuoi valori. Testa con Stripe test keys. Verifica webhook signing secret con stripe.webhooks.constructEvent.

backend/src/index.js

// index.js (Express minimal)
const express = require('express');
const helmet = require('helmet');
const cors = require('cors');
const rateLimit = require('express-rate-limit');
const mongoose = require('mongoose');
require('dotenv').config();

const productsRouter = require('./routes/products');
const ordersRouter = require('./routes/orders');
const stripeWebhook = require('./routes/stripeWebhook');

const app = express();

// security
app.use(helmet());
app.use(cors({ origin: process.env.FRONTEND_URL }));
app.use(express.json({ limit: '10kb' }));

// rate limit
const limiter = rateLimit({ windowMs: 60*1000, max: 100 });
app.use(limiter);

// connect DB
mongoose.connect(process.env.MONGODB_URI, { useNewUrlParser:true, useUnifiedTopology:true })
  .then(()=> console.log('MongoDB connected'))
  .catch(err => { console.error(err); process.exit(1); });

// routes
app.use('/api/products', productsRouter);
app.use('/api/orders', ordersRouter);

// stripe webhook: must use raw body, so mount separately
app.post('/webhook', express.raw({ type: 'application/json' }), stripeWebhook);

// global error
app.use((err, req, res, next) => {
  console.error(err);
  res.status(500).json({ error: 'internal error' });
});

const PORT = process.env.PORT || 4000;
app.listen(PORT, ()=> console.log(`API listening ${PORT}`));

backend/src/routes/products.js

const express = require('express');
const Product = require('../models/Product');
const router = express.Router();

// GET /api/products?search=&category=&page=
router.get('/', async (req, res, next) => {
  try {
    const { search, category, page = 1, limit = 20 } = req.query;
    const q = {};
    if (search) q.$text = { $search: search }; // requires text index
    if (category) q.category = category;
    const products = await Product.find(q)
      .skip((page-1)*limit).limit(Number(limit)).lean();
    res.json(products);
  } catch (e) { next(e); }
});

// GET single
router.get('/:id', async (req, res, next) => {
  try {
    const p = await Product.findById(req.params.id).lean();
    if (!p) return res.status(404).json({error:'not found'});
    res.json(p);
  } catch (e) { next(e); }
});

module.exports = router;

backend/src/routes/orders.js (estratto creazione ordine + Stripe Checkout session)

const express = require('express');
const router = express.Router();
const Stripe = require('stripe');
const stripe = Stripe(process.env.STRIPE_SECRET);
const Order = require('../models/Order');
const Product = require('../models/Product');

// Create Checkout Session
router.post('/create-checkout-session', async (req, res, next) => {
  try {
    const { items, customerEmail, successUrl, cancelUrl } = req.body;
    // items = [{ productId, qty }]
    const line_items = [];
    for (const it of items) {
      const p = await Product.findById(it.productId).lean();
      if (!p) return res.status(400).json({ error: 'invalid product' });
      line_items.push({
        price_data: {
          currency: 'eur',
          product_data: { name: p.title, images: [p.imageUrl] },
          unit_amount: Math.round(p.price*100),
        },
        quantity: it.qty,
      });
    }

    const session = await stripe.checkout.sessions.create({
      payment_method_types: ['card'],
      mode: 'payment',
      line_items,
      customer_email: customerEmail,
      success_url: successUrl,
      cancel_url: cancelUrl,
      metadata: { merchantOrderRef: 'tempRef' }
    });

    // optionally create pending order in DB
    const order = await Order.create({
      status: 'pending',
      stripeSessionId: session.id,
      items,
      total: line_items.reduce((s,l)=> s + (l.price_data.unit_amount/100)*l.quantity, 0)
    });

    res.json({ id: session.id, url: session.url, orderId: order._id });
  } catch (e) { next(e); }
});

module.exports = router;

backend/src/routes/stripeWebhook.js

// stripe webhook handler
const Stripe = require('stripe');
const stripe = Stripe(process.env.STRIPE_SECRET);
const Order = require('../models/Order');

module.exports = async (req, res) => {
  const sig = req.headers['stripe-signature'];
  let event;
  try {
    event = stripe.webhooks.constructEvent(req.body, sig, process.env.STRIPE_WEBHOOK_SECRET);
  } catch (err) {
    console.error('Webhook signature verification failed.', err.message);
    return res.status(400).send(`Webhook Error: ${err.message}`);
  }

  if (event.type === 'checkout.session.completed') {
    const session = event.data.object;
    // fulfill order
    const order = await Order.findOne({ stripeSessionId: session.id });
    if (order) {
      order.status = 'paid';
      order.paidAt = new Date();
      order.paymentIntent = session.payment_intent;
      await order.save();
      // send confirmation email (enqueue job)
    }
  }
  res.json({received: true});
};

Sicurezza webhook: mantieni il STRIPE_WEBHOOK_SECRET segreto; non usa body parser JSON su quella rotta (devi usare express.raw come sopra).

5) Frontend (React + Vite) — esempi chiave

frontend/src/main.jsx

import React from 'react';
import { createRoot } from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import App from './App';
import './styles.css';

createRoot(document.getElementById('root')).render(
  <BrowserRouter>
    <App />
  </BrowserRouter>
);

frontend/src/App.jsx (router, CartContext)

import React from 'react';
import { Routes, Route } from 'react-router-dom';
import Home from './pages/Home';
import Product from './pages/Product';
import Cart from './pages/Cart';
import Checkout from './pages/Checkout';
import { CartProvider } from './contexts/CartContext';

export default function App(){
  return (
    <CartProvider>
      <Routes>
        <Route path="/" element={<Home/>}/>
        <Route path="/product/:id" element={<Product/>}/>
        <Route path="/cart" element={<Cart/>}/>
        <Route path="/checkout" element={<Checkout/>}/>
      </Routes>
    </CartProvider>
  );
}

frontend/src/contexts/CartContext.jsx

import React, { createContext, useReducer, useContext, useEffect } from 'react';

const CartStateContext = createContext();
const CartDispatchContext = createContext();

function reducer(state, action){
  switch(action.type){
    case 'INIT': return action.payload;
    case 'ADD': {
      const exists = state.find(i => i.productId === action.payload.productId);
      if (exists) return state.map(i => i.productId === action.payload.productId ? { ...i, qty: i.qty + action.payload.qty } : i);
      return [...state, action.payload];
    }
    case 'REMOVE':
      return state.filter(i => i.productId !== action.payload.productId);
    case 'UPDATE':
      return state.map(i => i.productId === action.payload.productId ? { ...i, qty: action.payload.qty } : i);
    case 'CLEAR':
      return [];
    default: throw new Error('Unknown action');
  }
}

export function CartProvider({children}){
  const [state, dispatch] = useReducer(reducer, [], () => {
    try { return JSON.parse(localStorage.getItem('cart')||'[]'); } catch(e){ return []; }
  });

  useEffect(()=> { localStorage.setItem('cart', JSON.stringify(state)); }, [state]);

  return (
    <CartStateContext.Provider value={state}>
      <CartDispatchContext.Provider value={dispatch}>
        {children}
      </CartDispatchContext.Provider>
    </CartStateContext.Provider>
  );
}

export const useCart = () => useContext(CartStateContext);
export const useCartDispatch = () => useContext(CartDispatchContext);

frontend/src/pages/Checkout.jsx (crea sessione Stripe)

import React from 'react';
import { useCart } from '../contexts/CartContext';
import { loadStripe } from '@stripe/stripe-js';
const stripePromise = loadStripe(import.meta.env.VITE_STRIPE_PUBLISHABLE);

export default function Checkout(){
  const cart = useCart();

  async function handleCheckout(){
    const res = await fetch(`${import.meta.env.VITE_API_URL}/api/orders/create-checkout-session`, {
      method:'POST',
      headers: {'Content-Type':'application/json'},
      body: JSON.stringify({
        items: cart,
        customerEmail: '', // if logged in use user email
        successUrl: `${window.location.origin}/success`,
        cancelUrl: `${window.location.origin}/cart`
      })
    });
    const data = await res.json();
    if (data.url) {
      // redirect to stripe
      window.location.href = data.url;
    } else {
      alert('Errore checkout');
    }
  }

  return (
    <div>
      <h1>Checkout</h1>
      <button onClick={handleCheckout}>Paga con carta (Stripe)</button>
    </div>
  );
}

Questo flusso usa Stripe Checkout (redirect). Più complesso: integrazione con Payment Intents e elements per pagamento embedded.

6) Modelli Mongoose (esempi)

backend/src/models/Product.js

const mongoose = require('mongoose');
const schema = new mongoose.Schema({
  title: { type: String, required: true, text: true },
  description: String,
  price: { type: Number, required: true },
  imageUrl: String,
  sku: String,
  stock: { type: Number, default: 0 },
  category: String,
  tags: [String],
  createdAt: { type: Date, default: Date.now }
});
module.exports = mongoose.model('Product', schema);

backend/src/models/Order.js

const mongoose = require('mongoose');
const schema = new mongoose.Schema({
  items: [{ productId: String, qty: Number, price: Number }],
  total: Number,
  customerEmail: String,
  status: { type: String, enum: ['pending','paid','shipped','cancelled'], default: 'pending' },
  stripeSessionId: String,
  paymentIntent: String,
  createdAt: { type: Date, default: Date.now },
  paidAt: Date
});
module.exports = mongoose.model('Order', schema);

7) Docker & deploy (sintesi)

Dockerfile (backend)

FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
ENV NODE_ENV=production
EXPOSE 4000
CMD ["node", "src/index.js"]

docker-compose.yml (sviluppo locale)

version: '3.8'
services:
  db:
    image: mongo:6
    restart: unless-stopped
    volumes: - dbdata:/data/db
    ports: - "27017:27017"
  backend:
    build: ./backend
    environment:
      - MONGODB_URI=mongodb://db:27017/merchant
      - STRIPE_SECRET=${STRIPE_SECRET}
      - STRIPE_WEBHOOK_SECRET=${STRIPE_WEBHOOK_SECRET}
    ports:
      - "4000:4000"
    depends_on: [db]
volumes:
  dbdata:

Deploy:

  • Frontend: Vercel/Netlify (ottimo per React + prerender SEO)

  • Backend: Render, Heroku, Railway, o container su AWS ECS / Google Cloud Run

  • Use TLS (Let's Encrypt), CDN per immagini (Cloudflare, DigitalOcean Spaces, AWS S3 + CloudFront)

8) Sicurezza & compliance (obbligatorio)

  • Non memorizzare dati sensibili della carta (usa Stripe).

  • HTTPS obbligatorio.

  • Proteggi webhook (firmatura).

  • Helmet (security headers), CSP (content-security-policy) attenuato per script esterni (Stripe).

  • Rate limiting, input validation (Joi o express-validator), logging.

  • GDPR: policy cookie, consenso, gestione richieste utenti (data export / delete).

  • Password hashing: bcrypt + salting; usa 2FA per admin se possibile.

9) SEO, accessibilità, performance

  • Pagine prodotto prerenderate / SSR (Next.js) o generazione statica (SSG) per SEO.

  • Schema.org JSON-LD per prodotti (price, availability, sku).

  • Metadati OpenGraph per condivisione social.

  • WL: immagini ottimizzate (WebP), lazy loading loading="lazy".

  • Lighthouse: mira a 90+ mobile/desktop.

  • A11y: aria labels, semantic HTML, contrast ratios.

10) Testing, monitoraggio, analytics

  • Unit tests backend (Jest + supertest), frontend (Vitest / React Testing Library).

  • E2E: Playwright / Cypress per flusso checkout.

  • Monitor: Sentry per error tracking; Google Analytics / Plausible per traffico.

  • Backup DB giornaliero, disaster recovery doc.

11) Funzionalità avanzate JavaScript che puoi sfruttare

  • Realtime inventory con WebSocket / Socket.io (aggiorna stock in tempo reale)

  • Client-side search avanzata con Fuse.js per fuzzy search istantanea

  • Progressive Web App: service worker, offline caching del catalogo + carrello

  • Client-side rendering + streaming: caricamento progressivo immagini/skeletons per UX

  • Dynamic imports e code splitting con React.lazy per performance

  • Headless CMS (Strapi/Sanity) come backend per contenuti non tecnici

  • Personalizzazione JS: recommendations (collaborative filtering) eseguite in Node o client-side con cached models

  • Machine Learning inference: raccomandazioni prodotto con endpoint Python/TF o semplice scoring JS


domenica 29 marzo 2026

Corso di JavaScript & Programmazione Web: 9 – Animazioni e interattività avanzata

Animazioni e giochi 2D con JavaScript

Obiettivi didattici:

  • Imparare a usare setInterval e setTimeout per eseguire codice in ritardo o a intervalli regolari.

  • Animare elementi HTML in modo semplice.

  • Introdurre l’uso di Canvas per creare giochi 2D.

  • Creare un esercizio pratico: gioco “Indovina il numero”.

1. setTimeout e setInterval

Questi metodi servono a eseguire codice dopo un certo ritardo o a intervalli regolari.

Sintassi di setTimeout:

setTimeout(function() {
  console.log("Eseguito dopo 2 secondi");
}, 2000); // 2000 millisecondi = 2 secondi

Sintassi di setInterval:

let timer = setInterval(function() {
  console.log("Questo si ripete ogni secondo");
}, 1000);

// Per fermare l'intervallo:
clearInterval(timer);

Uso pratico: animazioni, countdown, aggiornamento dati dinamici.

2. Manipolazione animata di elementi HTML

Possiamo muovere un elemento usando style e setInterval.

Esempio: box che scorre a destra

<div id="box" style="width:50px;height:50px;background:red;position:absolute;left:0;"></div>
<script>
let pos = 0;
let box = document.getElementById('box');

let interval = setInterval(function() {
  if(pos >= 300) clearInterval(interval); // ferma animazione
  pos += 2;
  box.style.left = pos + 'px';
}, 20);
</script>

Spiegazione:

  • L’elemento si muove di 2px ogni 20ms.

  • Quando raggiunge 300px, l’animazione si ferma.

3. Introduzione a Canvas

Canvas permette di disegnare grafica 2D direttamente con JavaScript.

Esempio base: cerchio animato

<canvas id="myCanvas" width="400" height="200" style="border:1px solid black;"></canvas>
<script>
let canvas = document.getElementById('myCanvas');
let ctx = canvas.getContext('2d');

let x = 50;
let dx = 2;

function draw() {
  ctx.clearRect(0,0,canvas.width, canvas.height); // pulisce il canvas
  ctx.beginPath();
  ctx.arc(x, 100, 20, 0, Math.PI*2); // cerchio
  ctx.fillStyle = "blue";
  ctx.fill();
  x += dx;
  if(x > canvas.width-20 || x < 20) dx = -dx; // rimbalzo
  requestAnimationFrame(draw);
}

draw();
</script>

Spiegazione:

  • requestAnimationFrame crea animazioni più fluide di setInterval.

  • clearRect cancella il frame precedente.

  • Il cerchio rimbalza sui bordi del canvas.

4. Esercizio: Gioco “Indovina il numero”

Obiettivo: il computer sceglie un numero casuale tra 1 e 100 e l’utente deve indovinarlo.

HTML + JavaScript completo:

<h2>Indovina il numero!</h2>
<p>Inserisci un numero tra 1 e 100:</p>
<input type="number" id="guess" min="1" max="100">
<button id="check">Prova</button>
<p id="message"></p>

<script>
let secret = Math.floor(Math.random()*100) + 1; // numero casuale
let attempts = 0;

document.getElementById('check').addEventListener('click', function() {
  let guess = parseInt(document.getElementById('guess').value);
  attempts++;
  let msg = '';
  
  if(guess === secret) {
    msg = `Complimenti! Hai indovinato in ${attempts} tentativi.`;
  } else if(guess < secret) {
    msg = 'Troppo basso!';
  } else {
    msg = 'Troppo alto!';
  }

  document.getElementById('message').textContent = msg;
});
</script>

Spiegazione:

  • Math.random() genera un numero casuale.

  • L’utente inserisce il numero e clicca “Prova”.

  • Lo script confronta con il numero segreto e mostra un messaggio.

  • Si contano i tentativi con la variabile attempts.

5. Approfondimenti

  • Per animazioni più complesse si usa requestAnimationFrame invece di setInterval.

  • Canvas può essere combinato con immagini, sprite e collisioni per creare giochi 2D più elaborati.

  • Si possono aggiungere timer, punteggio e livelli usando variabili e setInterval.

6. Test rapido

  1. Differenza tra setTimeout e setInterval?
    Risposta: setTimeout esegue il codice una volta dopo un ritardo; setInterval ripete il codice a intervalli regolari.

  2. Come si pulisce un canvas prima di ridisegnare?
    Risposta: ctx.clearRect(0,0,canvas.width,canvas.height)

  3. Come generare un numero casuale tra 1 e 100?
    Risposta: Math.floor(Math.random()*100)+1

  4. Quale metodo è consigliato per animazioni fluide?
    Risposta: requestAnimationFrame.

sabato 28 marzo 2026

Corso di JavaScript & Programmazione Web: 8 – LocalStorage e persistenza

LocalStorage e persistenza dei dati


Obiettivi didattici:

  • Introdurre le API localStorage e sessionStorage per salvare dati lato client.

  • Mostrare come mantenere preferenze dell’utente, come il tema dark/light, tra diverse visite al sito.

  • Svolgere un esempio pratico di dark mode persistente.

1. Introduzione a localStorage e sessionStorage

JavaScript fornisce due principali sistemi di storage locale nel browser:

API Durata dati Scope Uso tipico
localStorage permanente (finché l’utente non cancella) stesso dominio preferenze utente, dati persistenti
sessionStorage durata sessione (chiusura tab) stesso tab dati temporanei, sessione utente

Proprietà principali:

  • Si lavora sempre con coppie chiave/valore (key/value) in formato stringa.

  • Metodi principali:

localStorage.setItem('chiave', 'valore');   // salva un dato
localStorage.getItem('chiave');             // legge un dato
localStorage.removeItem('chiave');          // elimina un dato
localStorage.clear();                        // cancella tutto

Per sessionStorage si usa la stessa sintassi.

2. Salvare dati dell’utente

Esempio: memorizzare la preferenza tema (light/dark) del visitatore.

Logica:

  1. L’utente seleziona il tema.

  2. Lo script salva la scelta in localStorage.

  3. Alla visita successiva, lo script legge il valore e applica il tema automaticamente.

3. Esempio pratico: Dark Mode persistente

HTML base:

<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dark Mode Persistente</title>
<style>
  body.light { background-color: white; color: black; }
  body.dark  { background-color: #121212; color: white; }
  button { margin: 20px; padding: 10px; }
</style>
</head>
<body>
  <h1>Dark Mode Persistente</h1>
  <button id="toggleTheme">Cambia Tema</button>

  <script src="script.js"></script>
</body>
</html>

JavaScript (script.js):

// Recupera il tema salvato, se esiste
let savedTheme = localStorage.getItem('theme') || 'light';
document.body.className = savedTheme;

// Funzione per cambiare tema
function toggleTheme() {
  if(document.body.className === 'light') {
    document.body.className = 'dark';
    localStorage.setItem('theme', 'dark');
  } else {
    document.body.className = 'light';
    localStorage.setItem('theme', 'light');
  }
}

// Event listener sul pulsante
document.getElementById('toggleTheme').addEventListener('click', toggleTheme);

Cosa succede:

  • Alla prima visita il tema di default è light.

  • L’utente clicca su “Cambia Tema”, la classe del body cambia e il nuovo tema viene salvato in localStorage.

  • Alla visita successiva, lo script legge il tema salvato e lo applica automaticamente.

4. Approfondimenti

  • localStorage salva solo stringhe. Per oggetti complessi si può usare JSON:

let settings = { theme: 'dark', fontSize: '16px' };
localStorage.setItem('settings', JSON.stringify(settings));

let loadedSettings = JSON.parse(localStorage.getItem('settings'));
console.log(loadedSettings.theme); // "dark"
  • Differenza con cookies: localStorage è più semplice e non viene inviato al server ad ogni richiesta.

5. Esercizio guidato

Obiettivo: creare un sito con scelta del tema che rimane salvata tra le visite.

Passaggi:

  1. Creare HTML con pulsante toggle.

  2. Definire due classi CSS light e dark.

  3. Leggere da localStorage il tema salvato e applicarlo al body.

  4. Al click sul pulsante, cambiare tema e salvare la nuova scelta.

Suggerimento avanzato: aggiungere anche la dimensione del font e salvare entrambi i dati in un oggetto JSON nel localStorage.

6. Test rapido

  1. Differenza tra localStorage e sessionStorage?
    Risposta: localStorage dura finché l’utente non cancella; sessionStorage dura solo fino a chiusura del tab.

  2. Come si legge un dato da localStorage?
    Risposta: localStorage.getItem('chiave')

  3. Come salvare un oggetto complesso?
    Risposta: Serializzare con JSON.stringify(obj) e leggere con JSON.parse(...)

  4. Perché localStorage è utile per dark mode?
    Risposta: Permette di ricordare la scelta dell’utente tra visite senza server.


venerdì 27 marzo 2026

Corso di JavaScript & Programmazione Web: 7 – Eventi e interattività

7 – Eventi e interattività

Obiettivi

  • Capire come funzionano gli event listeners (click, mouseover, keyup…).

  • Imparare a validare form in tempo reale.

  • Conoscere event bubbling e event delegation per codice più efficiente.

  • Sviluppare un esercizio pratico: form di login con validazione in tempo reale.

1) Event listeners: concetti e API base

  • Un evento è un’azione dell’utente o del browser (click, submit, keyup, mouseover, resize…).

  • Aggiungere un listener:

element.addEventListener('click', function(event){
  // comportamento quando avviene il click
});
  • Rimuovere un listener:

element.removeEventListener('click', listenerFn);
  • Proprietà utili nell’oggetto event: event.target (elemento che ha generato l’evento), event.currentTarget (elemento che gestisce l’evento), event.preventDefault() per bloccare il comportamento predefinito (es. submit), event.stopPropagation() per fermare la propagazione.

2) Tipi di eventi più comuni (esempi)

  • click — click del mouse su un elemento.

  • mouseover / mouseout — passaggio del cursore su/da un elemento.

  • keyup / keydown — tastiera (utile per validazione in tempo reale).

  • submit — invio di un form (si può intercettare per validare).

  • input — si attiva ad ogni modifica del valore di input (raccomandato per validazione live).

3) Event bubbling e event delegation

  • Event bubbling: un evento si propaga dal nodo target verso l’alto nella gerarchia DOM (bubble phase). Esiste anche la capture phase (dall’alto verso il basso) se lo si usa.

  • Event delegation: invece di aggiungere un listener su ogni child, si aggiunge un unico listener al parent e si usa event.target per capire quale child ha scatenato l’evento. Ripulisce il codice e funziona anche con elementi dinamici aggiunti dopo il binding.

Esempio di delega:

parentElement.addEventListener('click', (e) => {
  const btn = e.target.closest('.btn');
  if (!btn) return;
  // gestisci click sul bottone dentro parent
});

4) Form e validazione input — principi

  • Validazione lato client: migliora UX e riduce richieste al server, ma non sostituisce la validazione lato server (per sicurezza).

  • Strategie:

    • Validazione in tempo reale (usare input/keyup).

    • Validazione al submit (usare submit e preventDefault() se ci sono errori).

    • Fornire messaggi chiari, icone di successo/errore e focus sugli errori.

    • Usare constraint HTML5 (required, type="email", minlength) + JS per regole custom (es. controllo password).

5) Esercizio completo: Form di login con validazione in tempo reale

Snippet pronto per Blogger: HTML + CSS minimo + JS. Copia e incolla dentro un post in modalità HTML. Il codice è commentato e compatto, senza dipendenze esterne.

<!-- LOGIN FORM: copialo nella modalità HTML del tuo post Blogger -->
<div style="font-family: Arial, Helvetica, sans-serif; max-width:420px; margin:16px auto;">
  <style>
    .card { border:1px solid #e0e0e0; padding:18px; border-radius:8px; box-shadow:0 1px 3px rgba(0,0,0,0.04); background:#fff; }
    label { display:block; margin-bottom:6px; font-weight:600; font-size:14px; }
    input { width:100%; padding:10px 12px; margin-bottom:8px; border:1px solid #ccc; border-radius:6px; font-size:14px; box-sizing:border-box; }
    .input-group { margin-bottom:12px; position:relative; }
    .hint { font-size:12px; color:#666; margin-top:4px; }
    .error { color:#b00020; font-size:13px; margin-top:4px; display:none; }
    .success { color:#006400; font-size:13px; margin-top:4px; display:none; }
    button { padding:10px 14px; font-size:15px; border-radius:6px; border:0; background:#0077cc; color:#fff; cursor:pointer; width:100%; }
    button:disabled { background:#9ecae1; cursor:not-allowed; }
    .small { font-size:13px; color:#444; margin-top:8px; text-align:center; }
    .field-valid { border-color:#2e8b57 !important; box-shadow:0 0 0 3px rgba(46,139,87,0.06); }
    .field-invalid { border-color:#b00020 !important; box-shadow:0 0 0 3px rgba(176,0,32,0.06); }
  </style>

  <div class="card" id="loginCard">
    <h3 style="margin-top:0; margin-bottom:8px;">Login</h3>

    <form id="loginForm" novalidate>
      <div class="input-group">
        <label for="email">Email</label>
        <input id="email" name="email" type="email" placeholder="esempio@dominio.com" required>
        <div class="hint">Inserisci la tua email.</div>
        <div class="error" id="emailError">Email non valida.</div>
        <div class="success" id="emailOk">Email valida ✓</div>
      </div>

      <div class="input-group">
        <label for="password">Password</label>
        <input id="password" name="password" type="password" placeholder="Minimo 8 caratteri" required minlength="8">
        <div class="hint">Minimo 8 caratteri, almeno 1 lettera e 1 numero.</div>
        <div class="error" id="passError">Password debole.</div>
        <div class="success" id="passOk">Password valida ✓</div>
      </div>

      <div style="display:flex; gap:8px; margin-bottom:10px;">
        <label style="font-weight:400; font-size:13px;"><input id="remember" type="checkbox"> Ricordami</label>
      </div>

      <button id="submitBtn" type="submit" disabled>Accedi</button>
      <div class="small" aria-live="polite" id="statusText">Compila i campi per abilitare il pulsante.</div>
    </form>
  </div>

  <script>
    (function(){
      // Elementi
      const form = document.getElementById('loginForm');
      const email = document.getElementById('email');
      const pass  = document.getElementById('password');
      const submitBtn = document.getElementById('submitBtn');
      const statusText = document.getElementById('statusText');

      const emailError = document.getElementById('emailError');
      const emailOk = document.getElementById('emailOk');
      const passError = document.getElementById('passError');
      const passOk = document.getElementById('passOk');

      // Regole di validazione
      function validateEmail(v){
        // controllo semplice RFC-ish (non perfetto ma efficace)
        return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v.trim());
      }
      function validatePassword(v){
        // almeno 8 char, almeno una lettera e un numero
        return /^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d@$!%*?&]{8,}$/.test(v);
      }

      // Aggiorna stato visivo di un campo
      function setFieldState(input, okElement, errElement, valid){
        if(valid){
          input.classList.remove('field-invalid'); input.classList.add('field-valid');
          errElement.style.display = 'none'; okElement.style.display = 'block';
        } else {
          input.classList.remove('field-valid'); input.classList.add('field-invalid');
          errElement.style.display = 'block'; okElement.style.display = 'none';
        }
      }

      // Controllo live (input event)
      function updateState(){
        const emailVal = email.value;
        const passVal = pass.value;

        const emailIsValid = validateEmail(emailVal);
        const passIsValid  = validatePassword(passVal);

        // visual feedback
        if(emailVal.length === 0){
          email.classList.remove('field-valid','field-invalid');
          emailError.style.display = 'none'; emailOk.style.display = 'none';
        } else {
          setFieldState(email, emailOk, emailError, emailIsValid);
        }

        if(passVal.length === 0){
          pass.classList.remove('field-valid','field-invalid');
          passError.style.display = 'none'; passOk.style.display = 'none';
        } else {
          setFieldState(pass, passOk, passError, passIsValid);
        }

        // abilita submit solo se entrambi validi
        if(emailIsValid && passIsValid){
          submitBtn.disabled = false;
          statusText.textContent = 'Pronto per effettuare il login.';
        } else {
          submitBtn.disabled = true;
          statusText.textContent = 'Compila correttamente tutti i campi per abilitare il pulsante.';
        }
      }

      // debounce semplice per input/keyup (bottleneck UX)
      function debounce(fn, wait=150){
        let t;
        return (...args) => {
          clearTimeout(t);
          t = setTimeout(()=> fn(...args), wait);
        };
      }
      const debouncedUpdate = debounce(updateState, 120);

      // Event listeners: input in tempo reale
      email.addEventListener('input', debouncedUpdate);
      pass.addEventListener('input', debouncedUpdate);

      // Submit: validazione finale e prevenzione default
      form.addEventListener('submit', function(e){
        e.preventDefault(); // non inviare realmente
        // controllo di sicurezza
        if(!validateEmail(email.value) || !validatePassword(pass.value)){
          statusText.textContent = 'Errore: campi non validi. Correggi prima di inviare.';
          return;
        }
        // Simulazione login: qui chiameresti fetch/AJAX
        submitBtn.disabled = true;
        submitBtn.textContent = 'Accedendo...';
        statusText.textContent = 'Effettuo l\'autenticazione (simulata)...';

        // simulazione asincrona
        setTimeout(()=> {
          submitBtn.disabled = false;
          submitBtn.textContent = 'Accedi';
          statusText.textContent = 'Login riuscito (simulazione). Reindirizzamento...';
          // qui potresti fare window.location = '/dashboard' o simili
        }, 1100);
      });

      // Usare event delegation: esempio dimostrativo (gestire click su elementi che possono essere aggiunti dinamicamente)
      const card = document.getElementById('loginCard');
      card.addEventListener('click', (e) => {
        // se clicchi su un elemento con data-action, gestiscilo genericamente
        const el = e.target.closest('[data-action]');
        if(!el) return;
        const action = el.getAttribute('data-action');
        if(action === 'help'){
          alert('Inserisci email e password. Password: almeno 8 caratteri, 1 lettera e 1 numero.');
        }
      });

      // inizializza stato
      updateState();
    })();
  </script>
</div>

6) Spiegazione del codice (punti chiave)

  • Usiamo input per validazione in tempo reale (migliore di keyup perché copre incollamenti e altri input).

  • Funzioni validateEmail e validatePassword usano regex; sono esempi pratici: regole di password si possono adattare.

  • debounce evita troppe esecuzioni successive migliorando UX e performance.

  • novalidate sul form disabilita i messaggi browser standard, così mostriamo i nostri.

  • Il submit usa preventDefault() e una simulazione setTimeout per dimostrare comportamento asincrono; in produzione qui fai fetch() verso API.

  • event delegation è mostrato con gestione generica su card per elementi dinamici (utile quando aggiungi bottoni dinamicamente).

7) Varianti e miglioramenti suggeriti

  • Mostrare icone SVG (✓ / ✕) vicino ai campi per un feedback visivo istantaneo.

  • Aggiungere suggerimenti di sicurezza (es. strength meter password).

  • Mostrare messaggi ARIA per accessibilità (aria-live="polite" già utile per statusText).

  • Implementare autenticazione reale via fetch() con rate-limiting, CSRF token e hashing sul server.

  • Usare IntersectionObserver se vuoi validare solo campi visibili in pagina molto lunga.

8) Mini-esempi addizionali rapidi (copy-paste)

Aggiungere un click listener con rimozione:

const btn = document.querySelector('.myBtn');
function handleClick(e){ console.log('clic!'); }
btn.addEventListener('click', handleClick);
// più tardi
btn.removeEventListener('click', handleClick);

Uso di event.capture (rare):

parent.addEventListener('click', (e)=>{ console.log('capture phase'); }, true);
// true => handler in capture phase (dall'alto verso il basso)


Corso Pensiero Computazionale e Coding per Tutti: 1 Cos'è il pensiero computazionale

Il pensiero computazionale: una grammatica invisibile del mondo contemporaneo Nel dibattito pubblico sull’intelligenza artificiale e sulla t...