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);
}
}
