✨ feat(composer.lock): Met à jour les dépendances Composer et ajoute php-whois.
🎨 style(templates/order/process.twig): Améliore l'affichage de la commande et la gestion des options. 🐛 fix(src/Controller/Order/HomeController.php): Ajoute une route pour vérifier la disponibilité d'un nom de domaine (WHOIS).
This commit is contained in:
@@ -22,6 +22,7 @@
|
||||
"fpdf/fpdf": "*",
|
||||
"google/cloud": "^0.296.0",
|
||||
"imagine/imagine": "^1.5",
|
||||
"io-developer/php-whois": "*",
|
||||
"knplabs/knp-paginator-bundle": "^6.8",
|
||||
"lasserafn/php-initial-avatar-generator": "^4.4",
|
||||
"league/flysystem-aws-s3-v3": "^3.29",
|
||||
|
||||
64
composer.lock
generated
64
composer.lock
generated
@@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "c30269ac9b27fa9af3a5958c1033684b",
|
||||
"content-hash": "95e96b2eef6b5c2b6179fc5b8ba9ef6e",
|
||||
"packages": [
|
||||
{
|
||||
"name": "async-aws/core",
|
||||
@@ -3799,6 +3799,68 @@
|
||||
],
|
||||
"time": "2022-05-21T17:30:32+00:00"
|
||||
},
|
||||
{
|
||||
"name": "io-developer/php-whois",
|
||||
"version": "4.1.10",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/io-developer/php-whois.git",
|
||||
"reference": "ea4cf52832fe8ff56fc4937138467819aa2829c1"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/io-developer/php-whois/zipball/ea4cf52832fe8ff56fc4937138467819aa2829c1",
|
||||
"reference": "ea4cf52832fe8ff56fc4937138467819aa2829c1",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-curl": "*",
|
||||
"ext-json": "*",
|
||||
"ext-mbstring": "*",
|
||||
"php": ">=7.2",
|
||||
"symfony/polyfill-intl-idn": "^1.27"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^8.0"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Iodev\\": "src/Iodev/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Sergey Sedyshev",
|
||||
"email": "i.o.developer@gmail.com",
|
||||
"homepage": "https://github.com/io-developer"
|
||||
}
|
||||
],
|
||||
"description": "PHP WHOIS provides parsed and raw whois lookup of domains and ASN routes. PHP 5.4+ and 7+ compatible ",
|
||||
"homepage": "https://github.com/io-developer/php-whois",
|
||||
"keywords": [
|
||||
"asn",
|
||||
"domain",
|
||||
"info",
|
||||
"lookup",
|
||||
"parser",
|
||||
"php",
|
||||
"query",
|
||||
"routes",
|
||||
"tld",
|
||||
"whois",
|
||||
"црщшы"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/io-developer/php-whois/issues",
|
||||
"source": "https://github.com/io-developer/php-whois/tree/4.1.10"
|
||||
},
|
||||
"time": "2023-01-25T14:42:45+00:00"
|
||||
},
|
||||
{
|
||||
"name": "jean85/pretty-package-versions",
|
||||
"version": "2.1.1",
|
||||
|
||||
@@ -4,7 +4,9 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Controller\Order;
|
||||
|
||||
use Iodev\Whois\Factory;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
@@ -467,4 +469,38 @@ class HomeController extends AbstractController
|
||||
'formules' => self::formules,
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/commande/check', name: 'commande_check')]
|
||||
public function check(Request $request): JsonResponse
|
||||
{
|
||||
$ndd = $request->get('ndd');
|
||||
if (empty($ndd)) {
|
||||
return new JsonResponse([
|
||||
'isAvailable' => false,
|
||||
'message' => 'Nom de domaine manquant.',
|
||||
], Response::HTTP_BAD_REQUEST);
|
||||
}
|
||||
|
||||
// 2. Validation de format simple (côté serveur)
|
||||
// C'est une vérification de sécurité et de propreté des données
|
||||
if (!preg_match('/^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$/i', $ndd)) {
|
||||
return new JsonResponse([
|
||||
'isAvailable' => false,
|
||||
'message' => 'Format de domaine invalide.',
|
||||
], Response::HTTP_OK); // HTTP 200 car c'est une réponse de l'API, pas une erreur serveur
|
||||
}
|
||||
|
||||
$whois = Factory::get()->createWhois();
|
||||
if($whois->isDomainAvailable($ndd)) {
|
||||
return new JsonResponse([
|
||||
'isAvailable' => true,
|
||||
'message' => 'Félicitations ! Ce domaine est disponible.',
|
||||
]);
|
||||
} else {
|
||||
return new JsonResponse([
|
||||
'isAvailable' => false,
|
||||
'message' => 'Désolé, ce domaine est déjà enregistré.',
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,20 +18,8 @@
|
||||
</div>
|
||||
|
||||
{% set options = {
|
||||
'domaine': {'price': 15.00, 'period': '/ an', 'description': 'Enregistrement et gestion de votre nom de domaine (.fr, .com, .net, etc.)', 'is_annual': true},
|
||||
'email': {'price': 5.00, 'period': '/ mois', 'description': 'Ajout d\'une boîte email professionnelle personnalisée (par compte)'},
|
||||
|
||||
'esycreator': {'price': 50.00, 'period': '/ mois', 'description': 'Accompagnement et coaching dans la conception et l\'évolution de votre site.'},
|
||||
'esyseo_pro': {'price': 75.00, 'period': '/ mois', 'description': 'Optimisation avancée de votre référencement naturel par nos experts.'},
|
||||
'esydefender_pro': {'price': 30.00, 'period': '/ mois', 'description': 'Protection et surveillance avancées (niveau Pro) contre les menaces.'},
|
||||
|
||||
'esytranslate': {'price': 25.00, 'period': '/ mois', 'description': 'Traduction automatisée de votre site en plus de 100 langues.'},
|
||||
'esymailer': {'price': 40.00, 'period': '/ mois', 'description': 'Plateforme pour l\'envoi de campagnes marketing et newsletters professionnelles.'},
|
||||
'esyvisio': {'price': 15.00, 'period': '/ mois', 'description': 'Solution de visioconférence gratuite et illimitée intégrée.'},
|
||||
'esymeet': {'price': 20.00, 'period': '/ mois', 'description': 'Module de prise de rendez-vous en ligne et gestion de calendrier.'},
|
||||
'esytchat': {'price': 15.00, 'period': '/ mois', 'description': 'Ajout d\'un module de chat en direct pour interagir instantanément avec vos clients.'},
|
||||
'esysignature': {'price': 25.00, 'period': '/ mois', 'description': 'Outil de signature électronique à valeur juridique intégré.'}
|
||||
} %}
|
||||
'domaine': {'price': 50.00, 'period': '/ an', 'description': 'Enregistrement et gestion de votre nom de domaine (.fr, .com, .net, etc.)', 'is_annual': true},
|
||||
} %}
|
||||
|
||||
{# Vérification pour s'assurer qu'une formule est bien présente #}
|
||||
{% if currentFormule is defined and currentFormule is not empty %}
|
||||
@@ -44,23 +32,54 @@
|
||||
{# Colonne de gauche (Formulaire et Détails) - 8/12 = 2/3 #}
|
||||
<div class="lg:col-span-8">
|
||||
|
||||
<div class="card flex flex-col p-8 md:p-10 border-4 border-{{ currentFormule.color }}-600 shadow-2xl bg-white rounded-xl mb-12">
|
||||
<div class="mb-8 border-b pb-4 border-gray-200">
|
||||
<h2 class="text-4xl font-extrabold text-gray-900 mb-2">{{ currentFormule.name }}</h2>
|
||||
<p class="text-lg font-semibold text-{{ currentFormule.color }}-600">{{ currentFormule.audience }}</p>
|
||||
{# 💡 BLOC AMÉLIORÉ : FORMULE SÉLECTIONNÉE #}
|
||||
<div class="bg-white p-8 md:p-10 border-4 border-{{ currentFormule.color }}-600 shadow-2xl rounded-xl mb-12">
|
||||
|
||||
{# En-tête de la formule #}
|
||||
<div class="border-b pb-4 mb-6 border-gray-200">
|
||||
<h2 class="text-4xl font-extrabold text-gray-900 mb-1">
|
||||
{{ currentFormule.name }}
|
||||
</h2>
|
||||
<p class="text-lg font-semibold text-{{ currentFormule.color }}-600">
|
||||
Pour les **{{ currentFormule.audience }}**
|
||||
</p>
|
||||
</div>
|
||||
{# Les détails du prix ici sont statiques, le prix dynamique est dans le panneau flottant #}
|
||||
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-end mb-8">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-gray-500 mb-1">Prix de la formule</p>
|
||||
<p class="text-6xl font-extrabold text-gray-900">
|
||||
<span class="text-7xl font-extrabold text-{{ currentFormule.color }}-600">{{ currentFormule.price | number_format(2, ',', ' ') }}</span>
|
||||
<span class="text-2xl font-medium text-gray-500">{{ currentFormule.period }}</span>
|
||||
|
||||
{# Détails de la tarification et engagement #}
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 items-center">
|
||||
|
||||
{# Prix de la formule #}
|
||||
<div class="border-r border-gray-100 md:pr-6">
|
||||
<p class="text-base font-medium text-gray-500 mb-1">Tarif mensuel</p>
|
||||
<p class="text-5xl font-extrabold text-gray-900">
|
||||
<span class="text-6xl font-extrabold text-{{ currentFormule.color }}-600">
|
||||
{{ currentFormule.price | number_format(2, ',', ' ') }}
|
||||
</span>
|
||||
<span class="text-xl font-medium text-gray-500">{{ currentFormule.period }}</span>
|
||||
</p>
|
||||
</div>
|
||||
<p class="text-base text-gray-700 mt-4 sm:mt-0 sm:text-right font-semibold">
|
||||
{{ currentFormule.commitment }}
|
||||
</p>
|
||||
|
||||
{# Engagement #}
|
||||
<div class="md:pr-6 md:pl-6 border-r border-gray-100">
|
||||
<h5 class="text-base font-medium text-gray-500 mb-1">Engagement</h5>
|
||||
<p class="text-xl text-gray-700 font-semibold mt-2">
|
||||
{{ currentFormule.commitment }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{# Frais de mise en service #}
|
||||
<div class="md:pl-6">
|
||||
<h5 class="text-base font-medium text-gray-500 mb-1">Frais de mise en service</h5>
|
||||
{% if currentFormule.priceUp is defined and currentFormule.priceUp > 0 %}
|
||||
<p class="text-xl text-gray-700 font-semibold mt-2">
|
||||
{{ currentFormule.priceUp | number_format(2, ',', ' ') }} € (HT)
|
||||
</p>
|
||||
{% else %}
|
||||
<p class="text-xl text-green-600 font-bold mt-2">
|
||||
OFFERTS
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -88,57 +107,10 @@
|
||||
|
||||
<div id="domaine-input" class="pl-10 hidden">
|
||||
<label for="domaine_name" class="block text-sm font-medium text-gray-700 mb-2">Nom de domaine souhaité</label>
|
||||
<input type="text" id="domaine_name" name="options[domaine][name]" placeholder="Entrez le nom de domaine"
|
||||
<input type="text" id="domaine_name" name="options[domaine][name]" placeholder="Entrez le nom de domaine (ex:monsite.fr)"
|
||||
class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm p-2 focus:ring-indigo-500 focus:border-indigo-500">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4 border-b pb-4 mb-4" data-option-id="email" data-option-price="{{ options.email.price }}" data-option-period="monthly">
|
||||
<div class="flex items-center justify-between">
|
||||
<label for="option_email" class="flex items-center cursor-pointer w-full">
|
||||
<input type="checkbox" id="option_email" name="options[email][activate]" value="1" class="option-checkbox h-6 w-6 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded mr-4" data-option-input-target="#email-input" data-qty-target="#email_count">
|
||||
<div>
|
||||
<span class="text-lg font-bold text-gray-900">Boîtes Email Professionnelles</span>
|
||||
<p class="text-sm text-gray-500">{{ options.email.description }}</p>
|
||||
</div>
|
||||
</label>
|
||||
<span class="text-lg font-semibold text-gray-700 whitespace-nowrap">
|
||||
+{{ options.email.price | number_format(2, ',', ' ') }} €{{ options.email.period }} / boîte
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div id="email-input" class="pl-10 hidden">
|
||||
<label for="email_count" class="block text-sm font-medium text-gray-700 mb-2">Nombre de boîtes email souhaitées</label>
|
||||
<input type="number" id="email_count" name="options[email][count]" min="1" value="1"
|
||||
class="mt-1 block w-32 border border-gray-300 rounded-md shadow-sm p-2 focus:ring-indigo-500 focus:border-indigo-500 option-qty-input">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 class="text-xl font-bold text-gray-800 mt-8 mb-4">Modules Communication & Sécurité</h3>
|
||||
|
||||
{% for key, option in options %}
|
||||
{% if key not in ['domaine', 'email'] %}
|
||||
<div class="flex items-center justify-between py-3 border-b {% if loop.last %}border-b-0 pb-0{% endif %}"
|
||||
data-option-id="{{ key }}" data-option-price="{{ option.price }}" data-option-period="monthly">
|
||||
<label for="option_{{ key }}" class="flex items-center cursor-pointer w-full">
|
||||
<input type="checkbox" id="option_{{ key }}" name="options[{{ key }}][activate]" value="1" class="option-checkbox h-6 w-6 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded mr-4">
|
||||
<div>
|
||||
<span class="text-lg font-bold text-gray-900">
|
||||
{% if key == 'esyseo_pro' %}ESY-SEO Pro
|
||||
{% elseif key == 'esydefender_pro' %}ESY-Defender Pro
|
||||
{% else %}ESY-{{ key | capitalize | replace({'Esy': ''}) }}
|
||||
{% endif %}
|
||||
</span>
|
||||
<p class="text-sm text-gray-500">{{ option.description }}</p>
|
||||
</div>
|
||||
</label>
|
||||
<span class="text-lg font-semibold text-gray-700 whitespace-nowrap">
|
||||
+{{ option.price | number_format(2, ',', ' ') }} €{{ option.period }}
|
||||
</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
</div>
|
||||
|
||||
{# BLOC : FONCTIONNALITÉS (Incluses) #}
|
||||
@@ -172,45 +144,88 @@
|
||||
</div>
|
||||
|
||||
{# Colonne de droite (Sous-total flottant) - 4/12 = 1/3 #}
|
||||
{# Colonne de droite (Récapitulatif - Sticky) #}
|
||||
<div class="lg:col-span-4 mt-10 lg:mt-0">
|
||||
<div class="sticky top-8 bg-white p-6 shadow-2xl rounded-xl border border-gray-200">
|
||||
<h3 class="text-2xl font-bold text-gray-900 border-b pb-3 mb-4">
|
||||
<div class="sticky top-8 bg-white p-6 shadow-2xl rounded-xl border border-gray-200 divide-y divide-gray-200">
|
||||
|
||||
<h3 class="text-2xl font-bold text-gray-900 pb-3 mb-4">
|
||||
Votre Commande
|
||||
</h3>
|
||||
|
||||
<div class="mb-4 flex justify-between items-center text-gray-800">
|
||||
<span class="font-semibold text-lg">{{ currentFormule.name }}</span>
|
||||
<span class="font-bold text-lg text-{{ currentFormule.color }}-600">{{ currentFormule.price | number_format(2, ',', ' ') }} € / mois</span>
|
||||
{# SECTION 1: DÉTAILS DE LA FORMULE DE BASE #}
|
||||
<div class="py-4">
|
||||
<h4 class="text-lg font-bold text-gray-800 mb-3">Formule sélectionnée :</h4>
|
||||
|
||||
{# Nom et Prix Mensuel de la Formule #}
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<span class="font-semibold text-lg text-gray-900">{{ currentFormule.name }}</span>
|
||||
<span class="font-bold text-lg text-{{ currentFormule.color }}-600">
|
||||
{{ currentFormule.price | number_format(2, ',', ' ') }} € / mois
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{# Frais de mise en service (Si applicables) #}
|
||||
{% if currentFormule.priceUp is defined and currentFormule.priceUp > 0 %}
|
||||
<div class="flex justify-between items-center text-base">
|
||||
<span class="font-medium text-gray-600">Frais de mise en service (Unique)</span>
|
||||
<span class="font-bold text-gray-800">
|
||||
{{ currentFormule.priceUp | number_format(2, ',', ' ') }} €
|
||||
</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="border-t pt-4 mt-4">
|
||||
<h4 class="font-bold text-gray-700 mb-3">Options sélectionnées :</h4>
|
||||
<ul id="selected-options-list" class="space-y-2 text-sm text-gray-600 min-h-[50px]">
|
||||
{# SECTION 2: OPTIONS SÉLECTIONNÉES (Dynamique via JS) #}
|
||||
<div class="py-4">
|
||||
<h4 class="font-bold text-gray-800 mb-3">Options & Services :</h4>
|
||||
<ul id="selected-options-list" class="space-y-2 text-sm text-gray-600 min-h-[40px]">
|
||||
<li id="no-options-message" class="italic">Aucune option supplémentaire choisie.</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="border-t border-gray-300 pt-4 mt-4">
|
||||
{# SECTION 3: RÉCAPITULATIF DES TOTAUX #}
|
||||
<div class="pt-4">
|
||||
|
||||
<h4 class="font-bold text-gray-700 mb-3 text-xl">Total Mensuel Estimé :</h4>
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<span class="text-3xl font-extrabold text-gray-900">TOTAL HT/mois</span>
|
||||
<span id="monthly-total" class="text-3xl font-extrabold text-indigo-600">
|
||||
{{ currentFormule.price | number_format(2, ',', ' ') }} €
|
||||
</span>
|
||||
{% set initialUpPrice = currentFormule.priceUp is defined ? currentFormule.priceUp : 0.00 %}
|
||||
{% set firstMonthPrice = currentFormule.price is defined ? currentFormule.price : 0.00 %}
|
||||
|
||||
{# BLOC TOTAL À PAYER AUJOURD'HUI (Mise en avant) #}
|
||||
<div class="bg-indigo-50 p-3 rounded-lg border-2 border-indigo-200 mb-4">
|
||||
<h4 class="font-bold text-indigo-800 mb-2 text-xl">Total à payer aujourd'hui (HT) :</h4>
|
||||
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-2xl font-extrabold text-indigo-900">Total Initial</span>
|
||||
<span id="initial-total-static" class="text-3xl font-extrabold text-indigo-600">
|
||||
{{ (firstMonthPrice + initialUpPrice) | number_format(2, ',', ' ') }} €
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-xs text-indigo-600 text-right mt-1">
|
||||
(1er mois + Frais de mise en service + Options Annuelles)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<h4 class="font-bold text-gray-700 mb-3 text-xl">Frais Annuels (Domaine) :</h4>
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<span class="text-xl font-bold text-gray-700">TOTAL HT/an</span>
|
||||
<span id="annual-total" class="text-xl font-bold text-indigo-600">
|
||||
0,00 €
|
||||
</span>
|
||||
<h4 class="font-bold text-gray-700 mb-3 text-lg">Synthèse des frais récurrents :</h4>
|
||||
|
||||
{# Total Mensuel Estimé #}
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<span class="text-lg font-bold text-gray-700">TOTAL HT / mois</span>
|
||||
<span id="monthly-total" class="text-2xl font-bold text-gray-900">
|
||||
{{ currentFormule.price | number_format(2, ',', ' ') }} €
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{# Frais Annuels (Options) #}
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<span class="text-base font-medium text-gray-600">TOTAL HT / an (Options)</span>
|
||||
<span id="annual-total" class="text-lg font-semibold text-gray-800">
|
||||
0,00 €
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{# Bouton de Soumission #}
|
||||
<button type="submit"
|
||||
class="w-full py-3 mt-4 rounded-lg text-white font-bold text-lg shadow-md
|
||||
bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-4 focus:ring-indigo-300 transition duration-150 hidden lg:block">
|
||||
class="w-full py-3 mt-2 rounded-lg text-white font-bold text-lg shadow-md
|
||||
bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-4 focus:ring-indigo-300 transition duration-150 hidden lg:block">
|
||||
Passer à l'étape suivante
|
||||
</button>
|
||||
</div>
|
||||
@@ -235,25 +250,34 @@
|
||||
const form = document.getElementById('order-form');
|
||||
const checkboxes = form.querySelectorAll('.option-checkbox');
|
||||
const qtyInputs = form.querySelectorAll('.option-qty-input');
|
||||
|
||||
// Cibles pour l'affichage des totaux
|
||||
const monthlyTotalEl = document.getElementById('monthly-total');
|
||||
const annualTotalEl = document.getElementById('annual-total');
|
||||
const initialTotalEl = document.getElementById('initial-total-static'); // NOUVEAU ID pour le total initial
|
||||
const selectedOptionsList = document.getElementById('selected-options-list');
|
||||
const noOptionsMessage = document.getElementById('no-options-message');
|
||||
|
||||
// Prix de base de la formule (mensuel)
|
||||
// Prix de base
|
||||
const baseMonthlyPrice = {{ currentFormule.price | json_encode() | raw }};
|
||||
// Frais fixes initiaux (Mise en service), s'ils existent
|
||||
const initialFixedUpPrice = parseFloat({{ currentFormule.priceUp is defined ? currentFormule.priceUp : 0.00 | json_encode() | raw }});
|
||||
|
||||
// Fonction utilitaire pour formater le prix
|
||||
// Fonction utilitaire pour formater le prix
|
||||
const formatPrice = (price) => {
|
||||
price = parseFloat(price);
|
||||
// On vérifie si le prix est un nombre valide avant de formater
|
||||
if (isNaN(price)) return '0,00 €';
|
||||
return price.toFixed(2).replace('.', ',').replace(/\B(?=(\d{3})+(?!\d))/g, ' ') + ' €';
|
||||
}; // <--- L'erreur se produit ici si 'price' n'est pas un nombre.
|
||||
};
|
||||
|
||||
// Fonction de calcul du total
|
||||
const calculateTotal = () => {
|
||||
let currentMonthlyTotal = baseMonthlyPrice;
|
||||
let currentAnnualTotal = 0;
|
||||
// Démarrer le total initial avec le prix du 1er mois + frais de mise en service
|
||||
let currentInitialTotal = parseFloat(baseMonthlyPrice) + parseFloat(initialFixedUpPrice);
|
||||
|
||||
let selectedOptionsHtml = '';
|
||||
let optionsCount = 0;
|
||||
|
||||
@@ -265,6 +289,7 @@
|
||||
const price = parseFloat(parentDiv.dataset.optionPrice);
|
||||
const period = parentDiv.dataset.optionPeriod || 'monthly';
|
||||
|
||||
// Gestion de la quantité (bien que non utilisé pour 'domaine' ici, on le garde)
|
||||
let quantity = 1;
|
||||
const qtyTarget = parentDiv.dataset.qtyTarget;
|
||||
if (qtyTarget) {
|
||||
@@ -274,7 +299,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Affiche/Masque les champs de saisie pour domaine/email
|
||||
// Affiche/Masque les champs de saisie
|
||||
const inputTarget = checkbox.dataset.optionInputTarget;
|
||||
if (inputTarget) {
|
||||
const inputDiv = document.querySelector(inputTarget);
|
||||
@@ -287,7 +312,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (checkbox.checked) {
|
||||
optionsCount++;
|
||||
|
||||
@@ -299,6 +323,7 @@
|
||||
|
||||
if (period === 'annual') {
|
||||
currentAnnualTotal += optionPriceTotal;
|
||||
currentInitialTotal += parseFloat(optionPriceTotal); // AJOUT : L'option annuelle est facturée immédiatement
|
||||
displayPeriod = '/ an';
|
||||
priceText = formatPrice(optionPriceTotal) + displayPeriod;
|
||||
} else {
|
||||
@@ -313,16 +338,16 @@
|
||||
}
|
||||
|
||||
selectedOptionsHtml += `
|
||||
<li class="flex justify-between items-start">
|
||||
<span class="flex-1">${optionTitle}</span>
|
||||
<span class="font-medium text-right text-gray-800 whitespace-nowrap">+${priceText}</span>
|
||||
</li>
|
||||
`;
|
||||
<li class="flex justify-between items-start">
|
||||
<span class="flex-1">${optionTitle}</span>
|
||||
<span class="font-medium text-right text-gray-800 whitespace-nowrap">+${priceText}</span>
|
||||
</li>
|
||||
`;
|
||||
}
|
||||
});
|
||||
|
||||
// Mise à jour de l'affichage
|
||||
|
||||
initialTotalEl.textContent = formatPrice(currentInitialTotal); // MISE À JOUR DU TOTAL INITIAL
|
||||
monthlyTotalEl.textContent = formatPrice(currentMonthlyTotal);
|
||||
annualTotalEl.textContent = formatPrice(currentAnnualTotal);
|
||||
selectedOptionsList.innerHTML = selectedOptionsHtml;
|
||||
@@ -330,7 +355,9 @@
|
||||
if (optionsCount === 0) {
|
||||
selectedOptionsList.innerHTML = `<li id="no-options-message" class="italic">Aucune option supplémentaire choisie.</li>`;
|
||||
} else {
|
||||
noOptionsMessage && noOptionsMessage.remove(); // Supprime le message s'il est là
|
||||
// S'assurer que le message "Aucune option" est retiré si des options sont sélectionnées
|
||||
const existingNoMessage = document.getElementById('no-options-message');
|
||||
existingNoMessage && existingNoMessage.remove();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -345,6 +372,125 @@
|
||||
|
||||
// Lancement du calcul initial
|
||||
calculateTotal();
|
||||
|
||||
document.querySelector('#domaine_name').addEventListener('keyup', async (e) => {
|
||||
const inputElement = e.target;
|
||||
let ndd = inputElement.value.trim().toLowerCase();
|
||||
|
||||
// Retirer tout message précédent avant de commencer
|
||||
removeErrorMessage(inputElement);
|
||||
removeLoadingMessage(inputElement);
|
||||
|
||||
// Validation de format côté client
|
||||
const nddRegex = /^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$/i;
|
||||
|
||||
if (ndd === '') {
|
||||
displayErrorMessage(inputElement, 'Veuillez entrer le nom de domaine.');
|
||||
return;
|
||||
} else if (!nddRegex.test(ndd)) {
|
||||
displayErrorMessage(inputElement, 'Format invalide. Utilisez uniquement lettres, chiffres et tirets, avec une extension valide (.fr, .com, etc.).');
|
||||
return;
|
||||
}
|
||||
|
||||
// Si le format est valide, lancer la vérification de disponibilité asynchrone
|
||||
await checkDomainAvailability(ndd, inputElement);
|
||||
});
|
||||
|
||||
|
||||
function displayLoadingMessage(inputElement) {
|
||||
let loadingEl = inputElement.nextElementSibling;
|
||||
if (!loadingEl || !loadingEl.classList.contains('ndd-loading-message')) {
|
||||
loadingEl = document.createElement('p');
|
||||
loadingEl.classList.add('ndd-loading-message', 'text-sm', 'text-indigo-600', 'mt-1', 'flex', 'items-center');
|
||||
inputElement.parentNode.insertBefore(loadingEl, inputElement.nextSibling);
|
||||
}
|
||||
loadingEl.innerHTML = `<svg class="animate-spin -ml-1 mr-3 h-5 w-5 text-indigo-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg><span>Vérification de la disponibilité...</span>`;
|
||||
|
||||
inputElement.setAttribute('disabled', 'true');
|
||||
inputElement.classList.add('opacity-50');
|
||||
inputElement.classList.remove('border-red-500', 'border-green-500');
|
||||
}
|
||||
function removeLoadingMessage(inputElement) {
|
||||
const loadingEl = inputElement.nextElementSibling;
|
||||
if (loadingEl && loadingEl.classList.contains('ndd-loading-message')) {
|
||||
loadingEl.remove();
|
||||
}
|
||||
inputElement.removeAttribute('disabled');
|
||||
inputElement.classList.remove('opacity-50');
|
||||
}
|
||||
function displaySuccessMessage(inputElement) {
|
||||
let successEl = inputElement.nextElementSibling;
|
||||
if (!successEl || !successEl.classList.contains('ndd-success-message')) {
|
||||
successEl = document.createElement('p');
|
||||
successEl.classList.add('ndd-success-message', 'text-sm', 'text-green-600', 'mt-1', 'font-semibold', 'flex', 'items-center');
|
||||
inputElement.parentNode.insertBefore(successEl, inputElement.nextSibling);
|
||||
}
|
||||
successEl.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-1 flex-shrink-0" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" /></svg><span>Félicitations ! Ce nom de domaine est DISPONIBLE.</span>`;
|
||||
|
||||
inputElement.classList.add('border-green-500', 'ring-green-500');
|
||||
inputElement.classList.remove('border-red-500', 'ring-red-500');
|
||||
}
|
||||
|
||||
|
||||
const checkDomainAvailability = async (ndd, inputElement) => {
|
||||
// 1. Initialiser l'état de chargement
|
||||
removeErrorMessage(inputElement);
|
||||
displayLoadingMessage(inputElement);
|
||||
|
||||
// Endpoint où le serveur vérifiera le domaine
|
||||
|
||||
try {
|
||||
// SIMULATION : Remplacez ce bloc par votre appel réel à l'API
|
||||
const response = await fetch("/commande/check?ndd="+ndd, {
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
// 2. Traitement de la réponse
|
||||
if (data.isAvailable) { // data.isAvailable
|
||||
removeLoadingMessage(inputElement)
|
||||
displaySuccessMessage(inputElement);
|
||||
return true;
|
||||
} else {
|
||||
removeLoadingMessage(inputElement)
|
||||
displayErrorMessage(inputElement, `Ce nom de domaine (${ndd}) n'est PAS disponible.`);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de la vérification du domaine:', error);
|
||||
displayErrorMessage(inputElement, 'Erreur de connexion. Veuillez réessayer.');
|
||||
return false;
|
||||
} finally {
|
||||
// 3. Retirer l'état de chargement
|
||||
removeLoadingMessage(inputElement);
|
||||
// Si la simulation a créé un message de succès, le laisser
|
||||
if (!inputElement.classList.contains('border-green-500') && !inputElement.classList.contains('border-red-500')) {
|
||||
// Si aucune bordure n'a été ajoutée, cela signifie une erreur sans message
|
||||
removeErrorMessage(inputElement);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function displayErrorMessage(inputElement, message) {
|
||||
// Créer ou mettre à jour un élément d'erreur juste après l'input
|
||||
let errorEl = inputElement.nextElementSibling
|
||||
|
||||
if (!errorEl || !errorEl.classList.contains('ndd-error-message')) {
|
||||
errorEl = document.createElement('p');
|
||||
errorEl.classList.add('ndd-error-message', 'text-sm', 'text-red-600', 'mt-1');
|
||||
inputElement.parentNode.insertBefore(errorEl, inputElement.nextSibling);
|
||||
}
|
||||
errorEl.textContent = '❌ ' + message;
|
||||
inputElement.classList.add('border-green-500', 'ring-green-500');
|
||||
inputElement.classList.remove('border-red-500', 'ring-red-500');
|
||||
}
|
||||
|
||||
function removeErrorMessage(inputElement) {
|
||||
const errorEl = inputElement.nextElementSibling;
|
||||
if (errorEl && errorEl.classList.contains('ndd-error-message')) {
|
||||
errorEl.remove();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user