Compare commits
3 Commits
e33cf17426
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 402706496a | |||
| cbd0b76074 | |||
| ca008f66bb |
@@ -166,6 +166,28 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@layer utilities {
|
@layer utilities {
|
||||||
|
.grid-cols-2 {
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 640px) {
|
||||||
|
.sm\:grid-cols-2 {
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.md\:grid-cols-2 {
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
.lg\:grid-cols-3 {
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.animate-fade-in {
|
.animate-fade-in {
|
||||||
animation: fadeIn 0.6s ease-out forwards;
|
animation: fadeIn 0.6s ease-out forwards;
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
assets/img/projects/app-gestion-screen-1.jpg
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
assets/img/projects/app-gestion-thumb.jpg
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
assets/img/projects/ecommerce-xyz-screen-1.jpg
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
assets/img/projects/ecommerce-xyz-screen-2.jpg
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
assets/img/projects/ecommerce-xyz-screen-3.jpg
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
assets/img/projects/ecommerce-xyz-thumb.jpg
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
assets/img/projects/restaurant-thumb.jpg
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
assets/img/testimonials/marie-dupont.jpg
Normal file
|
After Width: | Height: | Size: 10 KiB |
@@ -71,6 +71,9 @@ GPT-5 Codex
|
|||||||
- Constantes SMTP ajoutées via .env / config.php
|
- Constantes SMTP ajoutées via .env / config.php
|
||||||
- Endpoint contact charge l'autoload vendor
|
- Endpoint contact charge l'autoload vendor
|
||||||
- Tests locaux OK ; test production confirmé
|
- Tests locaux OK ; test production confirmé
|
||||||
|
- SMTP requis (MAIL_HOST obligatoire) pour éviter fallback mail()
|
||||||
|
- Sanitation CRLF sur champs sensibles (nom/prenom/objet/email)
|
||||||
|
- reCAPTCHA tolérant si Google indisponible (seuil appliqué)
|
||||||
|
|
||||||
### Debug Log References
|
### Debug Log References
|
||||||
|
|
||||||
|
|||||||
@@ -228,7 +228,7 @@ function verifyRecaptcha(string $token): float
|
|||||||
|
|
||||||
if ($response === false) {
|
if ($response === false) {
|
||||||
error_log('reCAPTCHA: impossible de contacter Google');
|
error_log('reCAPTCHA: impossible de contacter Google');
|
||||||
return 0.3;
|
return RECAPTCHA_THRESHOLD;
|
||||||
}
|
}
|
||||||
|
|
||||||
$result = json_decode($response, true);
|
$result = json_decode($response, true);
|
||||||
@@ -256,8 +256,19 @@ function validateContactData(array $input): array
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$nomRaw = trim($input['nom'] ?? '');
|
||||||
|
$prenomRaw = trim($input['prenom'] ?? '');
|
||||||
$emailRaw = trim($input['email'] ?? '');
|
$emailRaw = trim($input['email'] ?? '');
|
||||||
|
$entrepriseRaw = trim($input['entreprise'] ?? '');
|
||||||
|
$objetRaw = trim($input['objet'] ?? '');
|
||||||
|
$messageRaw = trim($input['message'] ?? '');
|
||||||
|
|
||||||
|
$nom = str_replace(["\r", "\n"], '', $nomRaw);
|
||||||
|
$prenom = str_replace(["\r", "\n"], '', $prenomRaw);
|
||||||
$email = str_replace(["\r", "\n"], '', $emailRaw);
|
$email = str_replace(["\r", "\n"], '', $emailRaw);
|
||||||
|
$entreprise = str_replace(["\r", "\n"], '', $entrepriseRaw);
|
||||||
|
$objet = str_replace(["\r", "\n"], '', $objetRaw);
|
||||||
|
$message = $messageRaw;
|
||||||
if ($email && !filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
if ($email && !filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||||
$errors[] = "L'adresse email n'est pas valide";
|
$errors[] = "L'adresse email n'est pas valide";
|
||||||
}
|
}
|
||||||
@@ -268,22 +279,22 @@ function validateContactData(array $input): array
|
|||||||
$errors[] = 'Catégorie invalide';
|
$errors[] = 'Catégorie invalide';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (strlen($input['nom'] ?? '') > 100) {
|
if (strlen($nom) > 100) {
|
||||||
$errors[] = 'Le nom est trop long (max 100 caractères)';
|
$errors[] = 'Le nom est trop long (max 100 caractères)';
|
||||||
}
|
}
|
||||||
if (strlen($input['prenom'] ?? '') > 100) {
|
if (strlen($prenom) > 100) {
|
||||||
$errors[] = 'Le prénom est trop long (max 100 caractères)';
|
$errors[] = 'Le prénom est trop long (max 100 caractères)';
|
||||||
}
|
}
|
||||||
if (strlen($input['objet'] ?? '') > 200) {
|
if (strlen($objet) > 200) {
|
||||||
$errors[] = "L'objet est trop long (max 200 caractères)";
|
$errors[] = "L'objet est trop long (max 200 caractères)";
|
||||||
}
|
}
|
||||||
if (strlen($input['message'] ?? '') > 5000) {
|
if (strlen($message) > 5000) {
|
||||||
$errors[] = 'Le message est trop long (max 5000 caractères)';
|
$errors[] = 'Le message est trop long (max 5000 caractères)';
|
||||||
}
|
}
|
||||||
if (strlen($input['objet'] ?? '') > 0 && strlen($input['objet']) < 5) {
|
if (strlen($objet) > 0 && strlen($objet) < 5) {
|
||||||
$errors[] = "L'objet est trop court (min 5 caractères)";
|
$errors[] = "L'objet est trop court (min 5 caractères)";
|
||||||
}
|
}
|
||||||
if (strlen($input['message'] ?? '') > 0 && strlen($input['message']) < 20) {
|
if (strlen($message) > 0 && strlen($message) < 20) {
|
||||||
$errors[] = 'Le message est trop court (min 20 caractères)';
|
$errors[] = 'Le message est trop court (min 20 caractères)';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -292,13 +303,13 @@ function validateContactData(array $input): array
|
|||||||
}
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'nom' => htmlspecialchars(trim($input['nom'] ?? ''), ENT_QUOTES, 'UTF-8'),
|
'nom' => htmlspecialchars($nom, ENT_QUOTES, 'UTF-8'),
|
||||||
'prenom' => htmlspecialchars(trim($input['prenom'] ?? ''), ENT_QUOTES, 'UTF-8'),
|
'prenom' => htmlspecialchars($prenom, ENT_QUOTES, 'UTF-8'),
|
||||||
'email' => filter_var($email, FILTER_SANITIZE_EMAIL),
|
'email' => filter_var($email, FILTER_SANITIZE_EMAIL),
|
||||||
'entreprise' => htmlspecialchars(trim($input['entreprise'] ?? ''), ENT_QUOTES, 'UTF-8'),
|
'entreprise' => htmlspecialchars($entreprise, ENT_QUOTES, 'UTF-8'),
|
||||||
'categorie' => $categorie,
|
'categorie' => $categorie,
|
||||||
'objet' => htmlspecialchars(trim($input['objet'] ?? ''), ENT_QUOTES, 'UTF-8'),
|
'objet' => htmlspecialchars($objet, ENT_QUOTES, 'UTF-8'),
|
||||||
'message' => htmlspecialchars(trim($input['message'] ?? ''), ENT_QUOTES, 'UTF-8'),
|
'message' => htmlspecialchars($message, ENT_QUOTES, 'UTF-8'),
|
||||||
'ip' => $_SERVER['REMOTE_ADDR'] ?? 'inconnue',
|
'ip' => $_SERVER['REMOTE_ADDR'] ?? 'inconnue',
|
||||||
'date' => date('d/m/Y à H:i:s'),
|
'date' => date('d/m/Y à H:i:s'),
|
||||||
];
|
];
|
||||||
@@ -341,20 +352,21 @@ IP: {$data['ip']}
|
|||||||
============================================
|
============================================
|
||||||
EMAIL;
|
EMAIL;
|
||||||
|
|
||||||
|
if (!MAIL_HOST) {
|
||||||
|
error_log('Échec envoi email contact: MAIL_HOST manquant');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
$mail = new \PHPMailer\PHPMailer\PHPMailer(true);
|
$mail = new \PHPMailer\PHPMailer\PHPMailer(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (MAIL_HOST) {
|
$mail->isSMTP();
|
||||||
$mail->isSMTP();
|
$mail->Host = MAIL_HOST;
|
||||||
$mail->Host = MAIL_HOST;
|
$mail->SMTPAuth = true;
|
||||||
$mail->SMTPAuth = true;
|
$mail->Username = MAIL_USERNAME;
|
||||||
$mail->Username = MAIL_USERNAME;
|
$mail->Password = MAIL_PASSWORD;
|
||||||
$mail->Password = MAIL_PASSWORD;
|
$mail->SMTPSecure = MAIL_ENCRYPTION;
|
||||||
$mail->SMTPSecure = MAIL_ENCRYPTION;
|
$mail->Port = MAIL_PORT;
|
||||||
$mail->Port = MAIL_PORT;
|
|
||||||
} else {
|
|
||||||
$mail->isMail();
|
|
||||||
}
|
|
||||||
|
|
||||||
$mail->CharSet = 'UTF-8';
|
$mail->CharSet = 'UTF-8';
|
||||||
$mail->setFrom(MAIL_FROM, MAIL_FROM_NAME);
|
$mail->setFrom(MAIL_FROM, MAIL_FROM_NAME);
|
||||||
|
|||||||
@@ -198,7 +198,7 @@ include_template('navbar', compact('currentPage'));
|
|||||||
|
|
||||||
<?php $testimonials = getTestimonials(); ?>
|
<?php $testimonials = getTestimonials(); ?>
|
||||||
<?php if (!empty($testimonials)): ?>
|
<?php if (!empty($testimonials)): ?>
|
||||||
<section class="section bg-surface">
|
<section class="section">
|
||||||
<div class="container-content">
|
<div class="container-content">
|
||||||
<div class="section-header">
|
<div class="section-header">
|
||||||
<h2 class="section-title">Ce Qu'ils Disent</h2>
|
<h2 class="section-title">Ce Qu'ils Disent</h2>
|
||||||
|
|||||||
@@ -21,6 +21,13 @@ include_template('navbar', compact('currentPage'));
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<?php if (!empty($featuredProjects)): ?>
|
<?php if (!empty($featuredProjects)): ?>
|
||||||
|
<div class="mt-10 mb-16">
|
||||||
|
<h2 class="text-heading mb-3">Projets vedettes</h2>
|
||||||
|
<p class="text-text-secondary max-w-2xl">
|
||||||
|
Une sélection de projets qui montrent le mieux mon approche produit et technique.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 lg:gap-8">
|
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 lg:gap-8">
|
||||||
<?php foreach ($featuredProjects as $project): ?>
|
<?php foreach ($featuredProjects as $project): ?>
|
||||||
<?php include_template('project-card', ['project' => $project]); ?>
|
<?php include_template('project-card', ['project' => $project]); ?>
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ $authorName = $testimonial['author_name'] ?? 'Anonyme';
|
|||||||
$authorRole = $testimonial['author_role'] ?? '';
|
$authorRole = $testimonial['author_role'] ?? '';
|
||||||
$authorCompany = $testimonial['author_company'] ?? '';
|
$authorCompany = $testimonial['author_company'] ?? '';
|
||||||
$authorPhoto = $testimonial['author_photo'] ?? null;
|
$authorPhoto = $testimonial['author_photo'] ?? null;
|
||||||
|
$authorPhotoFallback = $authorPhoto ? str_replace('.webp', '.jpg', $authorPhoto) : null;
|
||||||
$projectSlug = $testimonial['project_slug'] ?? null;
|
$projectSlug = $testimonial['project_slug'] ?? null;
|
||||||
$showProjectLink = $showProjectLink ?? true;
|
$showProjectLink = $showProjectLink ?? true;
|
||||||
?>
|
?>
|
||||||
@@ -25,12 +26,15 @@ $showProjectLink = $showProjectLink ?? true;
|
|||||||
|
|
||||||
<footer class="flex items-center gap-4">
|
<footer class="flex items-center gap-4">
|
||||||
<?php if ($authorPhoto): ?>
|
<?php if ($authorPhoto): ?>
|
||||||
<img
|
<picture>
|
||||||
src="/assets/img/testimonials/<?= htmlspecialchars($authorPhoto, ENT_QUOTES, 'UTF-8') ?>"
|
<source srcset="/assets/img/testimonials/<?= htmlspecialchars($authorPhoto, ENT_QUOTES, 'UTF-8') ?>" type="image/webp">
|
||||||
alt="<?= htmlspecialchars($authorName, ENT_QUOTES, 'UTF-8') ?>"
|
<img
|
||||||
class="w-12 h-12 rounded-full object-cover"
|
src="/assets/img/testimonials/<?= htmlspecialchars($authorPhotoFallback, ENT_QUOTES, 'UTF-8') ?>"
|
||||||
loading="lazy"
|
alt="<?= htmlspecialchars($authorName, ENT_QUOTES, 'UTF-8') ?>"
|
||||||
>
|
class="w-12 h-12 rounded-full object-cover"
|
||||||
|
loading="lazy"
|
||||||
|
>
|
||||||
|
</picture>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<div class="w-12 h-12 rounded-full bg-primary/20 flex items-center justify-center">
|
<div class="w-12 h-12 rounded-full bg-primary/20 flex items-center justify-center">
|
||||||
<span class="text-primary font-semibold text-lg">
|
<span class="text-primary font-semibold text-lg">
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ assertTrue(strpos($composer, 'phpmailer/phpmailer') !== false, 'missing phpmaile
|
|||||||
|
|
||||||
$functions = file_get_contents(__DIR__ . '/../includes/functions.php');
|
$functions = file_get_contents(__DIR__ . '/../includes/functions.php');
|
||||||
assertTrue(strpos($functions, 'PHPMailer') !== false, 'missing PHPMailer usage');
|
assertTrue(strpos($functions, 'PHPMailer') !== false, 'missing PHPMailer usage');
|
||||||
|
assertTrue(strpos($functions, 'MAIL_HOST') !== false, 'missing MAIL_HOST usage');
|
||||||
|
assertTrue(strpos($functions, 'isSMTP') !== false, 'missing SMTP usage');
|
||||||
|
|
||||||
$config = file_get_contents(__DIR__ . '/../includes/config.php');
|
$config = file_get_contents(__DIR__ . '/../includes/config.php');
|
||||||
assertTrue(strpos($config, 'MAIL_HOST') !== false, 'missing mail constants');
|
assertTrue(strpos($config, 'MAIL_HOST') !== false, 'missing mail constants');
|
||||||
|
|||||||