You are currently viewing Développement d’un site de location de drones (HTML / CSS / JS)

Développement d’un site de location de drones (HTML / CSS / JS)

Ma formation universitaire au sein de la LP1 Métiers de l’Informatique – Applications Web est fortement axée sur le développement informatique pur. Ce projet m’a permis de m’éloigner des créateurs de sites visuels (No-Code) afin de coder intégralement une interface pour une entreprise de location de drones.

J’ai utilisé trois languages de programation pour réaliser ce projet :

  • HTML – Pour la création d’une architecture sémantique.
  • CSS – Pour la création d’un design moderne.
  • Javascript – Pour l’intégration de scripts personnalisés pour la gestion interactive de la page.

Voici un extrait de code :

      <!doctype html>
<html lang="fr">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>SKY VIEW</title>
    <link rel="stylesheet" href="style.css" />
  </head>
  <body>
    <h1>Sky View</h1>
    <p>Locations de drones professionnels</p>
    <p></p>

    <h2>À propos</h2>
    <p>
      Sky View propose à la location des drones de haute précision destinés aux
      professionnels du <strong>cinéma</strong>, de la
      <strong>publicité</strong> et de la <strong>cartographie</strong>. Notre
      flotte est entièrement certifiée. Location à la journée ou à la semaine,
      retrait en agence.
    </p>
    <h2>Nos drones</h2>

    <div class="container">
      <div class="slide">
        <div class="item" style="background-image: url(images/Inspire-3.png)">
          <div class="content">
            <div class="name">DJI Matrice 350 RTK</div>
            <div class="des">
              Très précis grâce au GPS RTK, idéal pour relevés et modélisation
              3D
            </div>
            <a
              class="seeMore"
              target="_blank"
              href="https://enterprise.dji.com/matrice-350-rtk"
              ><button>voir plus</button></a
            >
          </div>
        </div>

        <div
          class="item"
          style="background-image: url(images/dji-matrice-350-rtk.png)"
        >
          <div class="content">
            <div class="name">DJI Mini 2 SE</div>
            <div class="des">
              Simple, léger, facile à piloter, parfait pour commencer
            </div>
            <a
              class="seeMore"
              target="_blank"
              href="https://www.dji.com/fr/mini-2-se"
              ><button>voir plus</button></a
            >
          </div>
        </div>

        <div
          class="item"
          style="background-image: url(images/DJI-mini-2-se.png)"
        >
          <div class="content">
            <div class="name">DJI Mavic 3 Pro</div>
            <div class="des">
              Excellent compromis entre qualité vidéo, autonomie et portabilité
            </div>
            <a
              class="seeMore"
              target="_blank"
              href="https://www.dji.com/fr/mavic-3-pro"
              ><button>voir plus</button></a
            >
          </div>
        </div>

        <div
          class="item"
          style="background-image: url(images/DJI_Mavic_3_Pro.png)"
        >
          <div class="content">
            <div class="name">DJI Inspire 3</div>
            <div class="des">
              Utilisé pour des tournages pros, qualité cinéma avec capteur plein
              format
            </div>
            <a
              class="seeMore"
              target="_blank"
              href="https://www.dji.com/fr/inspire-3"
              ><button>voir plus</button></a
            >
          </div>
        </div>
      </div>
      <div class="button">
        <button class="prev">◁</button>
        <button class="next">▷</button>
      </div>
    </div>
    <script src="carrousel.js"></script>

    <h2>Formulaire de réservation</h2>
    <form action="#" method="post" enctype="multipart/form-data">
      <fieldset>
        <legend>Informations client</legend>
        Nom / Entreprise * <input type="text" name="nom" required /><br />
        E-mail * <input type="email" name="email" required /><br />
        Téléphone * <input type="tel" name="telephone" required />
      </fieldset>

      <fieldset>
        <legend>Configuration du pack</legend>
        Modèle de drone * :
        <select name="drone" required>
          <option value="" disabled selected>— Choisir —</option>
          <option value="inspire3">
            DJI Inspire 3 — Cinéma ultra haute qualité
          </option>
          <option value="matrice350">
            DJI Matrice 350 RTK — Cartographie / topographie
          </option>
          <option value="mini2se">DJI Mini 2 SE — Loisir / débutant</option>
          <option value="mavic3pro">
            DJI Mavic 3 Pro — Pro polyvalent
          </option></select
        ><br /><br />
        Date de début * <input type="date" name="date_debut" required /><br />
        Date de fin * <input type="date" name="date_fin" required />
      </fieldset>

      <fieldset>
        <legend>Détails mission et sécurité</legend>
        Localisation du tournage (ville ou coordonnées) *
        <input type="text" name="localisation" required /><br /><br />
        Précisions sur la mission (filtres ND, objectifs, etc.)<br />
        <textarea
          name="precisions"
          rows="3"
          cols="50"
          placeholder="Options souhaitées, contraintes particulières…"
        ></textarea
        ><br /><br />
        Attestation d'assurance RC drone * <small>(PDF)</small><br />

        <input
          type="file"
          name="assurance"
          accept=".pdf,.jpg,.jpeg,.png"
          required
        /><br /><br />
        Certificat de compétence télépilote <small>(optionnel, PDF)</small
        ><br />
        <input type="file" name="brevet" accept=".pdf,.jpg,.jpeg,.png" />
      </fieldset>
      <br />
      <small>* Champs obligatoires — confirmation sous 24 h ouvrées</small
      ><br /><br />
      <input type="submit" value="Envoyer ma demande" />
    </form>

    <hr />
    <small id="footer-credits">Sky View — contact@skyview-drones.fr</small>
    <script src="animations.js"></script>
    <script src="validation.js"></script>
    <script>
      // L'année se met à jour automatiquement chaque année
      var annee = new Date().getFullYear();
      document.getElementById("footer-credits").textContent =
        "© " + annee + " Sky View — contact@skyview-drones.fr";
    </script>
  </body>
</html>

    
      "use strict";

// Empêche la sélection de dates antérieures à aujourd'hui
const aujourdhui = new Date().toISOString().split("T")[0];
document.querySelector('[name="date_debut"]').min = aujourdhui;
document.querySelector('[name="date_fin"]').min = aujourdhui;

// affiche une erreur sous le champ avec un liseré rouge
function afficherErreur(champ, message) {
  champ.style.border = "2px solid red";

  let msg = champ.nextElementSibling;
  if (!msg || !msg.classList.contains("erreur-msg")) {
    msg = document.createElement("span");
    msg.classList.add("erreur-msg");
    msg.style.color = "red";
    msg.style.fontSize = "0.85em";
    msg.style.display = "block";
    msg.style.marginBottom = "8px";
    champ.insertAdjacentElement("afterend", msg);
  }
  msg.textContent = message;
}

// efface l'erreur
function effacerErreur(champ) {
  champ.style.border = "";
  let msg = champ.nextElementSibling;
  if (msg && msg.classList.contains("erreur-msg")) {
    msg.remove();
  }
}

function validerNom(champ) {
  if (champ.value.trim().length < 2) {
    afficherErreur(champ, "Veuillez saisir un nom ou une entreprise.");
    return false;
  }
  effacerErreur(champ);
  return true;
}

function validerEmail(champ) {
  // verifie qu'il y a bien un @ et un point
  if (!champ.value.includes("@") || !champ.value.includes(".")) {
    afficherErreur(champ, "Adresse e-mail invalide.");
    return false;
  }
  effacerErreur(champ);
  return true;
}

function validerTelephone(champ) {
  const numero = champ.value.replace(/\s/g, ""); // on enleve les espaces
  if (!/^(0|\+33)[1-9]\d{8}$/.test(numero)) {
    afficherErreur(champ, "Numero invalide (ex: 06 12 34 56 78).");
    return false;
  }
  effacerErreur(champ);
  return true;
}

function validerDrone(champ) {
  if (champ.value == "") {
    afficherErreur(champ, "Veuillez choisir un drone.");
    return false;
  }
  effacerErreur(champ);
  return true;
}

function validerDates() {
  const debut = document.querySelector('[name="date_debut"]');
  const fin = document.querySelector('[name="date_fin"]');
  let ok = true;

  if (debut.value == "") {
    afficherErreur(debut, "Date de debut requise.");
    ok = false;
  } else {
    effacerErreur(debut);
  }

  if (fin.value == "") {
    afficherErreur(fin, "Date de fin requise.");
    ok = false;
  } else if (debut.value != "" && fin.value < debut.value) {
    afficherErreur(fin, "La date de fin doit etre apres la date de debut.");
    ok = false;
  } else {
    effacerErreur(fin);
  }

  return ok;
}

function validerLocalisation(champ) {
  if (champ.value.trim().length < 2) {
    afficherErreur(champ, "Veuillez indiquer la localisation.");
    return false;
  }
  effacerErreur(champ);
  return true;
}

function validerFichier(champ) {
  if (champ.files.length == 0) {
    afficherErreur(champ, "Ce fichier est obligatoire.");
    return false;
  }
  effacerErreur(champ);
  return true;
}

// validation quand on clique sur envoyer
const form = document.querySelector("form");

form.addEventListener("submit", function (e) {
  e.preventDefault();

  const nom = form.querySelector('[name="nom"]');
  const email = form.querySelector('[name="email"]');
  const tel = form.querySelector('[name="telephone"]');
  const drone = form.querySelector('[name="drone"]');
  const local = form.querySelector('[name="localisation"]');
  const assurance = form.querySelector('[name="assurance"]');

  let toutOk = true;

  if (!validerNom(nom)) toutOk = false;
  if (!validerEmail(email)) toutOk = false;
  if (!validerTelephone(tel)) toutOk = false;
  if (!validerDrone(drone)) toutOk = false;
  if (!validerDates()) toutOk = false;
  if (!validerLocalisation(local)) toutOk = false;
  if (!validerFichier(assurance)) toutOk = false;

  if (toutOk) {
    form.submit();
  } else {
    // scroll vers la premiere erreur
    const premiereErreur = form.querySelector(".erreur-msg");
    if (premiereErreur) {
      premiereErreur.scrollIntoView({ behavior: "smooth", block: "center" });
    }
  }
});

// verification en direct quand l'utilisateur quitte un champ
form.querySelector('[name="nom"]').addEventListener("blur", function () {
  validerNom(this);
});
form.querySelector('[name="email"]').addEventListener("blur", function () {
  validerEmail(this);
});
form.querySelector('[name="telephone"]').addEventListener("blur", function () {
  validerTelephone(this);
});
form.querySelector('[name="drone"]').addEventListener("change", function () {
  validerDrone(this);
});
form
  .querySelector('[name="date_debut"]')
  .addEventListener("change", validerDates);
form
  .querySelector('[name="date_fin"]')
  .addEventListener("change", validerDates);
form
  .querySelector('[name="localisation"]')
  .addEventListener("blur", function () {
    validerLocalisation(this);
  });
form
  .querySelector('[name="assurance"]')
  .addEventListener("change", function () {
    validerFichier(this);
  });

    
      /* ============================================================
   STYLE GLOBAL & RESET
   ============================================================ */
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  background-color: #121212;
  font-family: "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  color: #e0e0e0;
  line-height: 1.6;
  overflow-y: auto;
  overflow-x: hidden;
  padding-bottom: 80px;
}

/* ============================================================
   ENTÊTE (TITRES)
   ============================================================ */
h1 {
  text-align: center;
  font-size: 50px;
  margin-top: 40px;
  letter-spacing: 4px;
  text-transform: uppercase;
  color: #ffffff;
  text-shadow: 0 0 15px rgba(255, 126, 95, 0.3);
}

body > p:first-of-type {
  text-align: center;
  font-size: 20px;
  color: #ff7e5f;
  margin-bottom: 40px;
  font-style: italic;
}

h2 {
  margin: 60px auto 20px;
  max-width: 1000px;
  font-size: 28px;
  border-left: 6px solid #feb47b;
  padding-left: 15px;
  color: #ffffff;
}

p {
  max-width: 1000px;
  margin: 15px auto;
  padding: 0 15px;
  font-size: 1.1em;
}

strong {
  color: #feb47b;
}

/* ============================================================
   CARROUSEL (CONTAINER)
   ============================================================ */
.container {
  position: relative;
  width: 100%;
  max-width: 1000px;
  height: 550px;
  margin: 20px auto;
  background: #1a1a1a;
  box-shadow: 0 40px 100px rgba(0, 0, 0, 0.8);
  border-radius: 30px;
  border: 1px solid rgba(255, 255, 255, 0.05);
}

.container .slide .item {
  width: 220px;
  height: 320px;
  position: absolute;
  top: 50%;
  transform: translate(0, -50%);
  border-radius: 20px;
  background-position: center;
  background-size: cover;
  display: inline-block;
  transition: 0.6s ease-in-out;
  background-image: linear-gradient(
    to top,
    rgba(0, 0, 0, 0.85) 0%,
    rgba(0, 0, 0, 0) 60%
  );
}

.slide .item:nth-child(1),
.slide .item:nth-child(2) {
  top: 0;
  left: 0;
  transform: translate(0, 0);
  width: 100%;
  height: 100%;
  border-radius: 30px;
}

.slide .item:nth-child(3) {
  left: 55%;
}
.slide .item:nth-child(4) {
  left: calc(55% + 240px);
}
.slide .item:nth-child(5) {
  left: calc(55% + 480px);
}
.slide .item:nth-child(n + 6) {
  left: calc(55% + 720px);
  opacity: 0;
}

.item .content {
  position: absolute;
  top: 50%;
  left: 60px;
  width: 400px;
  text-align: left;
  color: #fff;
  transform: translate(0, -50%);
  display: none;
}

.slide .item:nth-child(2) .content {
  display: block;
  padding: 30px;
  background: rgba(0, 0, 0, 0.2);
  backdrop-filter: blur(10px);
  border-radius: 20px;
  border: 1px solid rgba(255, 255, 255, 0.1);
}

.content .name {
  font-size: 35px;
  text-transform: uppercase;
  font-weight: bold;
  color: #feb47b;
  opacity: 0;
  animation: animate 0.8s ease-in-out 1 forwards;
}

.content .des {
  margin: 15px 0 25px;
  opacity: 0;
  animation: animate 0.8s ease-in-out 0.3s 1 forwards;
}

.content button {
  padding: 12px 25px;
  border: none;
  cursor: pointer;
  border-radius: 8px;
  background: #ffffff;
  color: #000;
  font-weight: bold;
  text-transform: uppercase;
  transition: 0.3s;
  opacity: 0;
  animation: animate 0.8s ease-in-out 0.6s 1 forwards;
}

.content button:hover {
  background: #feb47b;
  color: #fff;
}

@keyframes animate {
  from {
    opacity: 0;
    transform: translate(0, 50px);
    filter: blur(15px);
  }
  to {
    opacity: 1;
    transform: translate(0);
    filter: blur(0);
  }
}

.button {
  width: 100%;
  text-align: center;
  position: absolute;
  bottom: 30px;
  display: flex;
  justify-content: center;
  gap: 20px;
  z-index: 10;
}

.button button {
  width: 50px;
  height: 50px;
  border-radius: 50%;
  border: 1px solid rgba(255, 255, 255, 0.4);
  cursor: pointer;
  background: rgba(255, 255, 255, 0.1);
  color: white;
  font-size: 20px;
  transition: 0.3s;
}

.button button:hover {
  background: #fff;
  color: #000;
}

/* ============================================================
   FORMULAIRE
   ============================================================ */
form {
  max-width: 800px;
  margin: 40px auto;
  padding: 40px;
  background: #1e1e1e;
  border-radius: 25px;
  box-shadow: 0 20px 50px rgba(0, 0, 0, 0.4);
  border: 1px solid rgba(255, 255, 255, 0.05);
}

fieldset {
  border: none;
  margin-bottom: 30px;
  padding: 20px;
  background: #252525;
  border-radius: 15px;
}

legend {
  font-weight: bold;
  color: #feb47b;
  font-size: 1.2em;
  padding-bottom: 10px;
}

input,
select,
textarea {
  background: #2c2c2c;
  color: #fff;
  border: 1px solid #444;
  padding: 12px;
  margin: 10px 0 20px;
  border-radius: 10px;
  width: 100%;
}

input:focus,
select:focus,
textarea:focus {
  border-color: #ff7e5f;
  outline: none;
  box-shadow: 0 0 10px rgba(255, 126, 95, 0.2);
}

input[type="submit"] {
  background: linear-gradient(135deg, #ff7e5f, #feb47b);
  color: white;
  font-size: 18px;
  font-weight: bold;
  border: none;
  cursor: pointer;
  transition: 0.4s;
  margin-top: 10px;
}

input[type="submit"]:hover {
  transform: translateY(-3px);
  box-shadow: 0 10px 25px rgba(255, 126, 95, 0.4);
}

/* ============================================================
   FOOTER & RESPONSIVE
   ============================================================ */
hr {
  margin: 60px auto 20px;
  max-width: 1000px;
  border: 0;
  border-top: 1px solid #333;
}

body > small {
  display: block;
  text-align: center;
  color: #777;
  font-size: 0.9em;
}

@media (max-width: 800px) {
  .container {
    height: 400px;
  }
  .item .content {
    left: 20px;
    width: 80%;
  }
  .content .name {
    font-size: 24px;
  }
}

@keyframes shake {
  0%,
  100% {
    transform: translateX(0);
  }
  25% {
    transform: translateX(-5px);
  }
  75% {
    transform: translateX(5px);
  }
}