01 Hello World TypeScript

Exercice : Le typage dans TypeScript

Déduction de l'étape de transpilation

La transpilation permet de convertir du code TypeScript en JavaScript standard, compatible avec tous les navigateurs. Toutes les annotations de types sont supprimées et seule la logique JavaScript reste.

Utilité du typage fort dans TypeScript

  • Détecter les erreurs à la compilation plutôt qu'à l'exécution
  • Rendre le code plus lisible et auto-documenté
  • Aider à l'autocomplétion et la navigation dans l'IDE
  • Faciliter la maintenance et le refactoring à grande échelle

Le typage fort dans TypeScript apporte donc plus de robustesse et de sécurité au développement, contrairement à JavaScript qui est faiblement typé et dynamique.

02 Hello Astro TypeScript

Exercice : Créer une application Astro en TypeScript qui affiche dynamiquement "Bonjour, TypeScript!"


const my_hello_message: string = "Bonjour TYPESCRIPT !";

// Création d'un élément h1
const my_title: HTMLHeadingElement = document.createElement("h1");
my_title.textContent = my_hello_message;

// Création d'une div avec id="app"
const appDiv: HTMLDivElement = document.createElement("div");
appDiv.id = "app";
appDiv.appendChild(my_title);

// Insertion dans le body
document.body.appendChild(appDiv);
  

03 Modules

Exercice sur les modules TypeScript avec gestion de conflits de nommage entre deux fonctions identiques.


  import { calculerJoursRestants as calculerVacances } from "./moduleVacances.ts";
  import { calculerJoursRestants as calculerTravail } from "./moduleTravail.ts";
  
  const vacancesRestantes = calculerVacances();
  const travailRestant = calculerTravail();
  
  const resultatVacances = document.createElement("div");
  resultatVacances.textContent = \`la jours de vacances restant = \${vacancesRestantes}\`;
  
  const resultatTravail = document.createElement("div");
  resultatTravail.textContent = \`la jours de travail restant = \${travailRestant}\`;
  
  document.body.appendChild(resultatVacances);
  document.body.appendChild(resultatTravail);
    

// moduleTravail.ts
// Hypothèse : Un employé a droit à 30 jours de vacances par an.
export function calculerJoursRestants(): number {
    const joursTravaille = 180; // par exemple, un employee a travaille 180 jours
    return 250 - joursTravaille; // on suppose que un employee a 250 jours de travail par an
}
       

// moduleVacances.ts
// Hypothèse : Un employé a droit à 30 jours de vacances par an.

export function calculerJoursRestants(): number {
    const joursPris = 10; // par exemple, un employee a pris 10 jours de vacances
    return 30 - joursPris; // on suppose que un employee a 30 jours de vacances par an
}
          

04 Client ou Server Side

Réflexion sur l'exécution du code selon son emplacement dans le projet Astro.

Les messages console.log ajoutés dans les fonctions de moduleWeatherClientSide.ts s'affichent dans la console du navigateur, car ces fonctions s'exécutent côté client. En revanche, ceux dans moduleWeatherServerSide.ts apparaissent dans le terminal du serveur Astro, car ils sont exécutés lors du rendu côté serveur.

1. Code côté serveur (dans le dossier src/pages)

  • Les fichiers dans src/pages (comme index.astro et moduleWeatherServerSide.ts) sont exécutés sur le serveur au moment du rendu.
  • Les console.log de ces fonctions s'affichent donc dans le terminal serveur et non dans le navigateur.

2. Code côté client (dans le dossier src/scripts)

  • Les fichiers comme app.ts ou moduleWeatherClientSide.ts sont exécutés dans le navigateur une fois la page chargée.
  • Leurs console.log s'affichent dans la console DevTools du navigateur.
En résumé, la séparation des dossiers permet de distinguer clairement le code côté serveur et côté client.
Console serveur et navigateur

1. Visibilité des requêtes côté client

Les appels fetch() faits dans le navigateur sont visibles dans l'onglet Réseau des outils de développement, car le navigateur enregistre toutes les requêtes HTTP qu’il déclenche lui-même.

2. Requêtes côté serveur invisibles

Les requêtes faites sur le serveur (ex. dans le frontmatter d'un fichier Astro) ne passent pas par le navigateur, donc elles ne sont pas visibles dans l’onglet Réseau.

Si on voit deux requêtes fetch dans l'onglet Réseau, cela signifie que les deux ont été lancées côté client.

Console serveur et navigateur

05 HTML CSS Punto

Visiter la page Mini Projet - Punto pour découvrir le jeu:)

06 CSS Flexbox

Voici les 4 exercices réalisés à partir de la documentation MDN sur Flexbox :

Je les ai réalisé les fonctionalités par moi-même. Vous pouvez également les ouvrir dans un nouvel onglet pour regarder les résultats:

07 Fonctions Fléchées (Fat Arrow)

Pour tester les exercices du session 07, ouvrir le console pour voir les résultats apres cliquer les buttons.

Exercice 1 : Exécuter le fichier dans le navigateur

Faire le fichier app.ts soit exécuté dans le navigateur (côté client).

Ajouter une fonction fléchée, qui est utilisée comme callback dans addEventListener pour afficher les coordonnées de la souris dans la console à chaque clic.


  document.addEventListener("click", (event: MouseEvent) => {
    console.log(`x: ${event.clientX}, y: ${event.clientY}`);
  });
    

Exercice 2 : Calculatrice simple avec fonctions fléchées

Quatre fonctions fléchées sont définies pour effectuer des opérations, utilisées ensuite dans une fonction calculatrice avec un switch.


  const additionner = (a: number, b: number): number => a + b;
  const soustraire = (a: number, b: number): number => a - b;
  const multiplier = (a: number, b: number): number => a * b;
  const diviser = (a: number, b: number): number => a / b;
  
  // Fonction calculatrice avec une instruction pour déterminer l'opération
const calculatrice = (a: number, b: number, operation: string): void => {
    let resultat: number;

    switch (operation) {
        case "additionner":
            resultat = additionner(a, b);
            console.log(`La somme est: ${resultat}`);
            break;
        case "soustraire":
            resultat = soustraire(a, b);
            console.log(`La différence est: ${resultat}`);
            break;
        case "multiplier":
            resultat = multiplier(a, b);
            console.log(`Le produit est: ${resultat}`);
            break;
        case "diviser":
            resultat = diviser(a, b);
            console.log(`Le quotient est: ${resultat}`);
            break;
        default:
            console.log("Opération non reconnue.");
            break;
    }
};

// Exemples d'utilisation
calculatrice(5, 3, "additionner"); // Affiche "La somme est: 8"
calculatrice(5, 3, "soustraire");  // Affiche "La différence est: 2"
calculatrice(5, 3, "multiplier");  // Affiche "Le produit est: 15"
calculatrice(5, 3, "diviser");     // Affiche "Le quotient est: 1.6666666666666667"
    

Exercice 3 : Affichage continu des coordonnées

Un setInterval avec une fonction fléchée affiche les coordonnées de la souris toutes les secondes.


  document.addEventListener("mousemove", (event: MouseEvent) => {
    x = event.clientX; y = event.clientY;
  });
  setInterval(() => {
    console.log(`x: ${x}, y: ${y}`);
  }, 1000);
    

08 Map / Filter / Reduce / Find

Nous appliquons les méthodes map, filter, reduce et find à un tableau de fruits :


  const fruits = ["pomme", "kiwi", "banane", "cerise", "orange", "poire", "fraise", "prune", "ananas", "pêche"];
  
  const fruitsUpperCase = fruits.map(fruit => fruit.toUpperCase());
  // ["POMME", "KIWI", "BANANE", ...]
  
  const fruitsStartingWithP = fruits.filter(fruit => fruit.startsWith('p'));
  // ["pomme", "poire", "prune", "pêche"]
  
  const fruitsJoined = fruits.reduce((acc, fruit) => acc + (acc ? ', ' : '') + fruit, '');
  // "pomme, kiwi, banane, ..."
  
  const firstFruitWithMoreThanFiveLetters = fruits.find(fruit => fruit.length > 5);
  // "banane"
    

09 Classes et Interfaces - Véhicules

Une interface IVehicle définit deux méthodes. Une classe abstraite Vehicle fournit une implémentation de drive(). Les classes Car et Bicycle définissent leur propre méthode honk(). Le programme crée plusieurs instances et les parcourt.


  // Ici j'ai mis tout les messages apparaîts dans console affichés dessous le button

  interface IVehicle {
    drive(): void;
    honk(): void;
  }
  
  abstract class Vehicle implements IVehicle {
    constructor(protected speed: number) {}
    abstract honk(): void;
    drive(): void {
      console.log(\`Driving at \${this.speed} km/h\`);
    }
  }
  
  class Car extends Vehicle {
    honk(): void {
      console.log("Beep beep!");
    }
  }
  
  class Bicycle extends Vehicle {
    honk(): void {
      console.log("Ring Ring");
    }
  }

  const vehicles: IVehicle[] = [
    new Car(120),
    new Car(150),
    new Bicycle(20),
    new Bicycle(25),
    new Bicycle(30)
 ];

  for (const vehicle of vehicles) {
    vehicle.drive();
    vehicle.honk();
  }
    

10 json

Exercice sur la (dé)sérialisation JSON avec classes et interfaces.


    class User {
      constructor(
        public id: string,
        public name: string,
        public age: number,
        public scores: number[]
      ) {}
    
      getMaxScore(): number {
        return Math.max(...this.scores);
      }
    
      getAverageScore(): number {
        if (this.scores.length === 0) return 0;
        const sum = this.scores.reduce((a, b) => a + b, 0);
        return sum / this.scores.length;
      }
    }
    
    const users = [
      new User("1", "Alice", 25, [80, 90, 85]),
      new User("2", "Bob", 30, [70, 75, 80, 65]),
      new User("3", "Charlie", 22, [95, 85, 90]),
    ];
    
    const serialized = JSON.stringify(users);
    const plainObjects = JSON.parse(serialized);
    
    const revivedUsers = plainObjects.map(
      (data: any) => new User(data.id, data.name, data.age, data.scores)
    );
    
    const result = \`User: \${revivedUsers[1].name}, Max: \${revivedUsers[1].getMaxScore()}, Avg: \${revivedUsers[1].getAverageScore().toFixed(2)}\`;
    

Étapes expliquées :

  1. Nous avons défini une interface IUser avec les propriétés id, name, age et un tableau scores. Deux méthodes getMaxScore et getAverageScore sont aussi définies.
  2. La classe User implémente cette interface et fournit les méthodes nécessaires.
  3. Trois instances de User ont été créées avec des données fictives.
  4. Ces instances sont stockées dans un tableau de type IUser[].
  5. Une fonction serializeUsers utilise JSON.stringify() pour sérialiser le tableau IUser[] en une chaîne JSON.
  6. La fonction deserializeUsers utilise JSON.parse() pour transformer la chaîne JSON en un tableau d'objets simples.
  7. Après la désérialisation, on ne peut plus appeler getMaxScore ou getAverageScore, car les objets ne sont plus des instances de la classe User.
  8. Pour retrouver les méthodes, nous avons reconstruit des instances User à partir des objets désérialisés.
  9. Nous affichons alors le nom, le score max et le score moyen du 2ᵉ utilisateur restauré : c'est bien Bob.

11 Génériques

Implémentation d'une classe générique Queue<T> avec les méthodes enqueue, dequeue et size.


  class Queue<T> {
    private elements: T[] = [];
  
    enqueue(element: T): void {
      this.elements.push(element);
    }
  
    dequeue(): T | undefined {
      return this.elements.shift();
    }
  
    size(): number {
      return this.elements.length;
    }
  }

  // Testez votre code ici
    let numberQueue = new Queue();

    console.log("start numberQueue test:");
    console.log("origial size:", numberQueue.size()); 
    numberQueue.enqueue(10);
    numberQueue.enqueue(20);
    numberQueue.enqueue(30);
    console.log("size now:", numberQueue.size()); 
    console.log("dequeue", numberQueue.dequeue()); 
    console.log("size now:", numberQueue.size()); 
    console.log("dequeue:", numberQueue.dequeue()); 
    console.log("dequeue (from new empty Queue):", new Queue().dequeue()); 

    let stringQueue = new Queue();

    console.log("start stringQueue test:");
    console.log("origial size:", stringQueue.size()); 
    stringQueue.enqueue("a");
    stringQueue.enqueue('b');
    stringQueue.enqueue('c');
    console.log("size now:", stringQueue.size()); 
    console.log("dequeue", stringQueue.dequeue()); 
    console.log("size now:", stringQueue.size()); 
    console.log("dequeue:", stringQueue.dequeue()); 
    console.log("dequeue (from new empty Queue):", new Queue().dequeue());

    

12 Exceptions - Gestion d'erreurs

Une classe Rectangle vérifie les dimensions avant de calculer la surface. Les erreurs sont capturées via un bloc try-catch-finally.


  class Rectangle {
    constructor(private longueur: number, private largeur: number) {}
  
    calculerSurface(): number {
      if (this.longueur <= 0 || this.largeur <= 0) {
        throw new Error("Dimensions invalides !");
      }
      return this.longueur * this.largeur;
    }
  
    essayerCalculSurface(): void {
      try {
        const surface = this.calculerSurface();
        console.log(surface);
      } catch (e) {
        console.error(e.message);
      } finally {
        console.log("Calcul terminé");
      }
    }
  }

  // test
  const rectangles = [
      new Rectangle(5, 10),   
      new Rectangle(0, 8),     
      new Rectangle(4, -3),    
      new Rectangle(7, 12)     
  ];
  
  console.log("=== Test des rectangles ===");
  rectangles.forEach(rect => rect.essayerCalculSurface());
    

13 async/await et Promises

Trois exercices sur les Promesses et la syntaxe async/await :

  • Q1 : somme de deux nombres avec délai.
  • Q2 : vérification d'un utilisateur avec then/catch.
  • Q3 : version orientée objet avec des classes.

// Question 1
function calculateSumAsync(a: number, b: number): Promise {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve(a + b);
        }, 3000);
    });
}

async function printSum(a: number, b: number): Promise {
    console.log("Calcul en cours...");
    const sum = await calculateSumAsync(a, b);
    console.log(`Résultat: ${sum}`);
}

console.log("=== Test Question 1 ===");
printSum(5, 7);




// Question 2
function verifyUser(username: string, password: string): Promise {
    return new Promise((resolve, reject) => {
        const validUsername = "admin";
        const validPassword = "1234";
        
        if (username === validUsername && password === validPassword) {
            resolve();
        } else {
            reject("Nom d'utilisateur ou mot de passe incorrect");
        }
    });
}

// 2. use then/catch
console.log("=== Test Question 2 ===");
verifyUser("admin", "1234")
    .then(() => console.log("Bienvenue, admin!"))
    .catch(err => console.error("Erreur:", err));

verifyUser("user", "psjciwqw")
    .then(() => console.log("Bienvenue!"))
    .catch(err => console.error("Erreur:", err)); 




// Question 3 - Q1 (Version with Class)
class Calculator {
    async calculateSumAsync(a: number, b: number): Promise {
        return new Promise((resolve) => {
            setTimeout(() => {
                resolve(a + b);
            }, 3000);
        });
    }

    async printSum(a: number, b: number): Promise {
        console.log("Calcul en cours...");
        const sum = await this.calculateSumAsync(a, b);
        console.log(`Résultat: ${sum}`);
    }
}

// TEST
console.log("=== Test Question 3 - Q1 ===");
const calc = new Calculator();
calc.printSum(10, 15); 


// Question 3 - Q2 (Version with Class)
class Users {
    private validUsername = "admin";
    private validPassword = "1234";

    verifyUser(username: string, password: string): Promise {
        return new Promise((resolve, reject) => {
            if (username === this.validUsername && password === this.validPassword) {
                resolve();
            } else {
                reject("Nom d'utilisateur ou mot de passe incorrect");
            }
        });
    }

    login(username: string, password: string): void {
        this.verifyUser(username, password)
            .then(() => console.log(`Bienvenue, ${username}!`))
            .catch(err => console.error("Erreur:", err));
    }
}

// TEST
console.log("=== Test Question 3 - Q2 ===");
const userSystem = new Users();
userSystem.login("admin", "1234"); 
userSystem.login("guest", "030dwqxsa"); 

    

14 fetch

Exercice 1

Ce script utilise l'API de l'USGS pour afficher les tremblements de terre de magnitude ≥ 4 depuis hier.

  
interface Earthquake {
properties: {
    time: number;
    place: string;
    mag: number;
};
}

class EarthquakeService {
private async getEarthquakesFromUSGS(): Promise {
    const response = await fetch(
    "https://earthquake.usgs.gov/fdsnws/event/1/query?format=geojson&starttime=yesterday&endtime=today&minmagnitude=4"
    );
    const earthquakejson = await response.json();
    return earthquakejson.features;
}

async displayEarthquakes() {
    try {
    const earthquakes = await this.getEarthquakesFromUSGS();

    // ======= add line 1 =======
    const ul: HTMLUListElement = document.createElement("ul");
    

    earthquakes.forEach((earthquake) => {
        const date: Date = new Date(earthquake.properties.time);
        const options: Intl.DateTimeFormatOptions = {
        year: "numeric",
        month: "2-digit",
        day: "2-digit",
        hour: "2-digit",
        minute: "2-digit",
        timeZone: "Europe/Paris",
        };
        const dateString: string = date.toLocaleDateString("fr-FR", options);
        const finalString: string = `${dateString} - ${earthquake.properties.mag} - ${earthquake.properties.place}`;
        console.log(finalString);

        // ======= add line 2-4 =======
        const li: HTMLLIElement = document.createElement("li");
        li.textContent = finalString;
        ul.appendChild(li);
        
    });

    // ======= add line 5 =======
    // document.body.appendChild(ul);
    document.getElementById("ex1")?.appendChild(ul);
    

    } catch (error) {
    console.error('Une erreur est survenue lors de la récupération des données de tremblement de terre :', error);
    }
}
}

const earthquakeService = new EarthquakeService();
earthquakeService.displayEarthquakes();    
    

Exercice 2 : Recherche d'adresse en France

Recherche des adresses via l'API adresse.data.gouv.fr (exemple : 8 bd du port)

 
// app2.ts

interface Address {
    city: string;
    postcode: string;
    street: string;
    housenumber: string;
    context: string;
    lat: number;
    lon: number;
    }
    
    class AddressService {
    async searchAddress(query: string, limit: number): Promise {
        const encodedQuery = encodeURIComponent(query);
        const url = `https://api-adresse.data.gouv.fr/search/?q=${encodedQuery}&limit=${limit}`;
    
        try {
        const response = await fetch(url);
        const data = await response.json();
    
        const addresses: Address[] = data.features.map((feature: any) => {
            const props = feature.properties;
            return {
            city: props.city,
            postcode: props.postcode,
            street: props.street || '', 
            housenumber: props.housenumber || '',
            context: props.context,
            lat: feature.geometry.coordinates[1],
            lon: feature.geometry.coordinates[0],
            };
        });
    
        return addresses;
        } catch (error) {
        console.error("Erreur lors de la recherche d'adresse :", error);
        return [];
        }
    }
    }
    

    
    const addressService = new AddressService();
    
    addressService.searchAddress("8 bd du port", 5).then((results) => {
    const ul = document.createElement("ul");
    
    results.forEach((address) => {
        const li = document.createElement("li");
        li.textContent = `${address.city}, ${address.postcode}, ${address.street} ${address.housenumber} (${address.context}) [${address.lat}, ${address.lon}]`;
        ul.appendChild(li);
    });
    
    document.getElementById("ex2")?.appendChild(ul);
    });
    

15 CORS - Deezer Playlist

Affichage des morceaux de la playlist Top 100 France 2023 (depuis l'API de Deezer)


const container = document.createElement("ul");
fetch("https://api.deezer.com/playlist/11846226041")
  .then(res => res.json())
  .then(async playlist => {
    // track is a url
    const trackRes = await fetch(playlist.tracklist);
    const trackData = await trackRes.json();

    trackData.data.forEach((track: any, index: number) => {
      const li = document.createElement("li");
      li.innerHTML = `
        ...
      `;
      container.appendChild(li);
    });


document.body.appendChild(container);
  })
  .catch(error => {
    console.error("Erreur lors de la récupération des données :", error);
  });
  
Afficher le playlist
  • 1. Flowers

    Miley Cyrus

    Flowers

    cover
  • 2. Bolide allemand

    SDM

    Liens du 100

    cover
  • 3. Jolie (feat. Ninho)

    GAULOIS

    Jolie (feat. Ninho)

    cover
  • 4. Meuda

    Tiakola

    Mélo

    cover
  • 5. Another Love

    Tom Odell

    Long Way Down (Deluxe)

    cover
  • 6. Secret

    Louane

    Secret

    cover
  • 7. Saiyan

    Heuss L'enfoiré

    Saiyan

    cover
  • 8. Nocif

    Hamza

    Sincèrement

    cover
  • 9. Rush

    Ayra Starr

    Rush

    cover
  • 10. Tiki Taka

    Vacra

    Tiki Taka

    cover
  • 11. DIE

    Gazo

    KMT

    cover
  • 12. Mr. Ocho

    SDM

    Liens du 100

    cover
  • 13. Casanova

    Soolking

    Casanova

    cover
  • 14. THE LONELIEST

    Måneskin

    THE LONELIEST

    cover
  • 15. Decrescendo

    Lomepal

    Mauvais Ordre

    cover
  • 16. Calm Down

    Rema

    Calm Down

    cover
  • 17. Baby

    Aya Nakamura

    Baby

    cover
  • 18. AMBER

    Zola

    AMBER

    cover
  • 19. DESPECHÁ

    ROSALÍA

    DESPECHÁ

    cover
  • 20. I'm Good (Blue)

    David Guetta

    I'm Good (Blue)

    cover
  • 21. As It Was

    Harry Styles

    As It Was

    cover
  • 22. FLEURS (feat. Tiakola)

    Gazo

    KMT

    cover
  • 23. C'est carré le S

    naps

    En temps réel

    cover
  • 24. Creepin'

    Metro Boomin

    HEROES & VILLAINS

    cover
  • 25. Petit génie

    Jungeli

    Petit génie

    cover

16 - HTTP CRUD avec relations entre entités

Ce script gère la suppression, création, mise à jour de restaurants et la mise à jour des catégories et relations associées.

Afficher le code

interface AlloResto {
  restaurants: Restaurant[];
  categories: Category[];
  restaurantCategories: RestaurantCategory[];
}

interface Category {
  id?: string;
  name?: string;
  restaurantIds?: string[];
}

interface RestaurantCategory {
  restaurantId?: string;
  categoryId?: string;
}

interface Restaurant {
  id?: string;
  name?: string;
  description?: string;
  categoryIds?: string[];
}

abstract class HttpClient {
    protected url: string;
    protected options: RequestInit;
  
    constructor(url: string) {
      this.url = url;
      this.options = {
        headers: {
          "Content-Type": "application/json",
        },
      };
    }
  
    public async execute(): Promise {
      try {
        const response = await fetch(this.url, this.options);
        if (response.ok) {
        const data: T = await response.json(); //extraction du corps de la réponse au format JSON
        return data;
        }
      } catch (error) {
        console.error("There was a problem with the fetch operation: ", error);
      }
    }
  }


  class CreateClient extends HttpClient {
    constructor(url: string, data: T) {
      super(url);
      this.options.method = "POST";
      this.options.body = JSON.stringify(data);
    }
  }
  
  class ReadClient extends HttpClient {
    constructor(url: string) {
      super(url);
      this.options.method = "GET";
    }
  }
  
  class UpdateClient extends HttpClient {
    constructor(url: string, data: T) {
      super(url);
      this.options.method = "PATCH";
      this.options.body = JSON.stringify(data);
    }
  }
  
  class DeleteClient extends HttpClient {
    constructor(url: string) {
      super(url);
      this.options.method = "DELETE";
    }
  }
  const url = "http://localhost:3000/restaurants";
  const categoriesUrl = "http://localhost:3000/categories";
  const restaurantCategoriesUrl = "http://localhost:3000/restaurantCategories";

  async function deleteRestaurantAndUpdateRelations(restaurantId: string): Promise {
    const getClient = new ReadClient(`${url}/${restaurantId}`);
    const restaurant = await getClient.execute();
    if (!restaurant) return;
  
    const deleteClient = new DeleteClient(`${url}/${restaurantId}`);
    const deletedRestaurant = await deleteClient.execute();
    if (!deletedRestaurant) return;
  
    console.log(`DELETE id : ${deletedRestaurant.id} name : ${deletedRestaurant.name} `);

    const categoryIds = restaurant.categoryIds || [];
    for (const categoryId of categoryIds) {
      const getCategoryClient = new ReadClient(`${categoriesUrl}/${categoryId}`);
      const category = await getCategoryClient.execute();
      if (category) {
        const updatedRestaurantIds = category.restaurantIds?.filter(id => id !== restaurantId) || [];
        const updateCategoryClient = new UpdateClient(`${categoriesUrl}/${categoryId}`, {
          restaurantIds: updatedRestaurantIds
        });
        await updateCategoryClient.execute();
      }
    }
  
    const deleteRestaurantCategoriesClient = new DeleteClient(`${restaurantCategoriesUrl}?restaurantId=${restaurantId}`);
    await deleteRestaurantCategoriesClient.execute();
  }

  async function createRestaurantAndRelations(data: Restaurant): Promise {
    const createClient = new CreateClient(url, data);
    const createdRestaurant = await createClient.execute();
    if (createdRestaurant && createdRestaurant.id) {
      console.log(`CREATE id : ${createdRestaurant.id} name : ${createdRestaurant.name} `);
  
      const categoryIds = data.categoryIds || [];
      for (const categoryId of categoryIds) {
        const getCategoryClient = new ReadClient(`${categoriesUrl}/${categoryId}`);
        const category = await getCategoryClient.execute();
        if (category) {
          const updatedRestaurantIds = [...(category.restaurantIds || []), createdRestaurant.id];
          const updateCategoryClient = new UpdateClient(`${categoriesUrl}/${categoryId}`, {
            restaurantIds: updatedRestaurantIds
          });
          await updateCategoryClient.execute();

          const createRestaurantCategoryClient = new CreateClient(restaurantCategoriesUrl, {
            restaurantId: createdRestaurant.id,
            categoryId: categoryId
          });
          await createRestaurantCategoryClient.execute();
        }
      }
      return createdRestaurant;
    }
  }
  
  async function updateRestaurantAndRelations(restaurantId: string, updatedData: Restaurant): Promise {
    const getClient = new ReadClient(`${url}/${restaurantId}`);
    const originalRestaurant = await getClient.execute();
    if (!originalRestaurant) return;
  
    const updateClient = new UpdateClient(`${url}/${restaurantId}`, updatedData);
    const updatedRestaurant = await updateClient.execute();
    if (!updatedRestaurant) return;
  
    console.log(`UPDATE id : ${updatedRestaurant.id} name : ${updatedRestaurant.name} `);
  
    if (updatedData.categoryIds !== undefined) {
      const originalCategoryIds = originalRestaurant.categoryIds || [];
      const newCategoryIds = updatedData.categoryIds || [];
      const addedIds = newCategoryIds.filter(id => !originalCategoryIds.includes(id));
      const removedIds = originalCategoryIds.filter(id => !newCategoryIds.includes(id));
  
      for (const categoryId of addedIds) {
        const getCategoryClient = new ReadClient(`${categoriesUrl}/${categoryId}`);
        const category = await getCategoryClient.execute();
        if (category) {
          const updatedRestaurantIds = [...(category.restaurantIds || []), restaurantId];
          const updateCategoryClient = new UpdateClient(`${categoriesUrl}/${categoryId}`, {
            restaurantIds: updatedRestaurantIds
          });
          await updateCategoryClient.execute();
  
          const createRestaurantCategoryClient = new CreateClient(restaurantCategoriesUrl, {
            restaurantId: restaurantId,
            categoryId: categoryId
          });
          await createRestaurantCategoryClient.execute();
        }
      }

      for (const categoryId of removedIds) {
        const getCategoryClient = new ReadClient(`${categoriesUrl}/${categoryId}`);
        const category = await getCategoryClient.execute();
        if (category) {
          const updatedRestaurantIds = category.restaurantIds?.filter(id => id !== restaurantId) || [];
          const updateCategoryClient = new UpdateClient(`${categoriesUrl}/${categoryId}`, {
            restaurantIds: updatedRestaurantIds
          });
          await updateCategoryClient.execute();
  
          const deleteRestaurantCategoryClient = new DeleteClient(`${restaurantCategoriesUrl}?restaurantId=${restaurantId}&categoryId=${categoryId}`);
          await deleteRestaurantCategoryClient.execute();
        }
      }
    }
  
    return updatedRestaurant;
  }


// Exemple d'exécution : suppression, création, modification
async function main() {
  // suppression
  await deleteRestaurantAndUpdateRelations('3aa8');
  // création
  await createRestaurantAndRelations({
    name: "Le Restaurant de la Joie",
    description: "Un restaurant où la joie est au menu",
    categoryIds: ["71b2"],
  });
  // mise à jour
  await updateRestaurantAndRelations('12b3', {
    name: "Le Grill Super Marrant",
  });
}
  

17. Forms + API OpenAQ

Cet exercice vous montre comment interagir avec l'API OpenAQ via un formulaire.
Étant donné que l'API OpenAQ ne permet pas les requêtes directes côté navigateur (CORS), l'application utilise un serveur proxy déployé sur Vercel.

  • L'utilisateur entre un pays (ex: FR, US, CN…)
  • On récupère les locations disponibles via /api/locations
  • On sélectionne un site de mesure → les mesures s'affichent
  • Données rafraîchies toutes les 10 secondes automatiquement
  • Si le pays n'est pas trouvé → message d'erreur
Afficher le code

        import { serve } from "bun";

        const OPENAQ_API_KEY = "YOUR_API_KEY"; // 替换为你的API密钥
        const PORT = 3000;
        async function fetchOpenAQData(endpoint: string, params: Record) {
            const url = new URL(`https://api.openaq.org/v3/${endpoint}`);
            Object.entries(params).forEach(([key, value]) => 
                url.searchParams.append(key, value));
            
            const response = await fetch(url, {
                headers: {
                    "X-API-Key": OPENAQ_API_KEY,
                }
            });
        
            if (!response.ok) throw new Error(await response.text());
            return response.json();
        }

        serve({
            port: PORT,
            async fetch(req) {
                const url = new URL(req.url);
                
                // 返回前端页面
                if (url.pathname === "/") {
                    return new Response(HTML, {
                        headers: { "Content-Type": "text/html" }
                    });
                }

                if (url.pathname === "/locations") {
                    try {
                        const country = url.searchParams.get("country");
                        const data = await fetchOpenAQData("locations", {
                            country,
                            limit: "100"
                        });
                        
                        if (data.results.length === 0) {
                            return new Response("Country not found", { status: 404 });
                        }
                        
                        return Response.json(data.results.map(location => ({
                            id: location.id,
                            name: location.name
                        })));
                    } catch (error) {
                        return new Response(error.message, { status: 500 });
                    }
                }

                if (url.pathname === "/measurements") {
                    try {
                        const locationId = url.searchParams.get("locationId");
                        const data = await fetchOpenAQData("measurements", {
                            location_id: locationId,
                            limit: "10",
                            sort: "desc"
                        });
                        
                        return Response.json(data.results);
                    } catch (error) {
                        return new Response(error.message, { status: 500 });
                    }
                }
        
                return new Response("Not found", { status: 404 });
            }
        });
        
        console.log(`Server running at http://localhost:${PORT}`);
    

18. Web Components personnalisés

Cet exercice montre comment créer des éléments HTML personnalisés en utilisant l’API Web Components. Deux éléments ont été définis :

  • <date-time> : affiche la date et l’heure actuelles, mise à jour chaque seconde
  • <greet-custom> : affiche une salutation contextuelle (Bonjour, Bon après-midi, Bonsoir) selon l’heure de la journée. Il accepte un attribut name pour personnaliser le message.
Afficher le code de <greet-custom>

class HelloWorld extends HTMLElement {
    // le navigateur appelle cette méthode à chaque fois que l'élément est ajouté
    // au DOM (c'est à dire inséré dans la page)
    // vous pouvez aussi appeler cette méthode manuellement
    // pour mettre à jour l'élément
    connectedCallback() {
        this.textContent = "Hello World!";
    }
    }
    
    // ici on enregistre notre élément  dans le navigateur
    // pour qu'il soit reconnu et utilisable dans le HTML
    // le nom de la balise est le premier paramètre
    // le deuxième paramètre est la classe qui définit le comportement de l'élément
    customElements.define("hello-world", HelloWorld);
    
    class HelloName extends HTMLElement {
    name: string = "Remi";
    constructor() {
        super();
    }
    
    // le navigateur appelle cette méthode pour savoir
    // quels attributs observer (c'est à dire surveiller les changements de valeur)
    // et pour ensuite appeler attributeChangedCallback
    static get observedAttributes(): string[] {
        return ["name"];
    }
    
    // le navigateur appelle cette méthode à chaque fois qu'un attribut
    // observé change de valeur (voir observedAttributes)
    // puis il appelle connectedCallback pour mettre à jour l'élément
    attributeChangedCallback(name: string, oldValue: string, newValue: string) {
        if (name === "name") {
        console.log(`Attribute ${name} changed from ${oldValue} to ${newValue}`);
        this.name = newValue;
        }
    }
    
    // cette méthode est appelée à chaque fois que l'attribut name change
    // (voir ci-dessus)
    connectedCallback() {
        console.log(`Hello ${this.name}!`);
        this.textContent = `Hello ${this.name}!`;
    }
    }
    
    customElements.define("hello-name", HelloName);
    
    //ajouter un élément  programmatiquement
    const helloName : HelloName = document.createElement("hello-name") as HelloName;
    helloName.setAttribute("name", "Monsieur DEBUT");
    document.body.appendChild(helloName);

            
// ==================== date and time ====================
class DateTime extends HTMLElement {
    private timerId?: number;

    connectedCallback() {
    this.updateTime();

    this.timerId = setInterval(() => this.updateTime(), 1000);
    }

    disconnectedCallback() {
    if (this.timerId) {
        clearInterval(this.timerId);
    }
    }

    private updateTime() {
    this.textContent = new Date().toLocaleString();
    }
}

customElements.define('date-time', DateTime);

// ==================== Active Greeting ====================
class GreetCustom extends HTMLElement {

    static get observedAttributes() {
    return ['name'];
    }

    private name: string = '';

    attributeChangedCallback(name: string, oldVal: string, newVal: string) {
    if (name === 'name') {
        this.name = newVal;
        this.updateGreeting();
    }
    }

    connectedCallback() {
    this.updateGreeting();
    }

    private updateGreeting() {
    const hour = new Date().getHours();
    let greeting = 'Bonsoir'; // default bonsoir

    if (hour >= 5 && hour < 12) {
        greeting = 'Bonjour';
    } else if (hour >= 12 && hour < 18) {
        greeting = 'Bon après-midi';
    }

    this.innerHTML = `
        
${greeting}${this.name ? `, ${this.name}` : ''}!
`; } } customElements.define('greet-custom', GreetCustom); window.onload = () => { const style = document.createElement('style'); style.textContent = ` hello-world, hello-name, date-time, greet-custom { display: block; margin-bottom: 15px; } `; document.head.appendChild(style); const thirdHelloName: HelloName = document.getElementById("third") as HelloName; if (thirdHelloName) { thirdHelloName.setAttribute("name", "John"); thirdHelloName.connectedCallback(); } else { console.error("Element not found"); } const container = document.createElement('div'); const components = [ document.createElement('date-time'), helloName, // 原有的helloName document.createElement('greet-custom') ]; components[2].setAttribute('name', ''); components.forEach(el => { container.appendChild(el); container.appendChild(document.createElement('br')); }); document.body.appendChild(container); };