Init Git Prod Mars 2026

This commit is contained in:
Oleg Barykin committed 2026-03-05 10:25:39 +01:00
commit d81efe93b1
362 files changed
+86627

No files matched your search

+12
View File
@@ -0,0 +1,12 @@
# Interdit l'accès HTTP aux fichiers sensibles
<FilesMatch "\.(php|inc|phtml|phar|env|ini|json|ya?ml|sql|bak|zip)$">
Require all denied
</FilesMatch>
# Interdit l'accès aux fichiers cachés (.env, .git, etc.)
<FilesMatch "^\.">
Require all denied
</FilesMatch>
# Pas de listing de répertoire
Options -Indexes
+139
View File
@@ -0,0 +1,139 @@
<?php
// htdocs/includes/accepter_cookies.php — CookieConsent v3 (correctif)
// Si tu as des traductions dans lang/fr.php / lang/ru.php, on les lit (optionnel)
$lang = $_SESSION['lang'] ?? 'fr';
$langFile = __DIR__ . "/../lang/{$lang}.php";
if (is_file($langFile)) { include $langFile; }
$t = isset($translations) && is_array($translations) ? $translations : [];
function tt($k,$d=''){ global $t; return htmlspecialchars($t[$k]??$d, ENT_QUOTES, 'UTF-8'); }
?>
<!--<a id="cc-manage" href="#" style="display:none"><?= tt('cookies_manage_link','Paramètres cookies') ?></a>-->
<link id="cc-css" rel="stylesheet" href="/js/cookieconsent.css"
onerror="this.href='https://cdn.jsdelivr.net/gh/orestbida/cookieconsent@3.1.0/dist/cookieconsent.css'">
<script>
(function(){
// Textes (prend ceux de tes fichiers de langue si présents)
const TXT = {
title: <?= json_encode($t['cookies_title'] ?? 'Cookies & confidentialité') ?>,
desc: <?= json_encode($t['cookies_desc'] ?? "Nous utilisons des cookies essentiels. Avec votre accord, nous activons aussi l'analyse et la personnalisation.") ?>,
acceptAll: <?= json_encode($t['cookies_accept_all'] ?? 'Tout accepter') ?>,
rejectAll: <?= json_encode($t['cookies_reject_all'] ?? 'Tout refuser') ?>,
settings: <?= json_encode($t['cookies_settings'] ?? 'Personnaliser') ?>,
necessary: <?= json_encode($t['cookies_necessary'] ?? 'Essentiels (toujours actifs)') ?>,
analytics: <?= json_encode($t['cookies_analytics'] ?? 'Mesure d’audience') ?>,
personalization: <?= json_encode($t['cookies_personalization'] ?? 'Personnalisation') ?>,
save: <?= json_encode($t['cookies_save'] ?? 'Enregistrer') ?>,
};
const SRCS = [
'/js/cookieconsent.umd.js', // local d'abord
'https://cdn.jsdelivr.net/gh/orestbida/cookieconsent@3.1.0/dist/cookieconsent.umd.js',
'https://unpkg.com/vanilla-cookieconsent@3.1.0/dist/cookieconsent.umd.js'
];
function loadScriptSequential(i){
if(i >= SRCS.length){ console.error('[CC] Impossible de charger la lib'); return; }
const s = document.createElement('script');
s.src = SRCS[i];
s.onload = function(){
// v3 => CookieConsent.run(...)
if (window.CookieConsent && typeof CookieConsent.run === 'function'){ initCC(); }
else { console.warn('[CC] Script chargé mais API v3 absente → essai suivant'); loadScriptSequential(i+1); }
};
s.onerror = function(){ console.warn('[CC] Échec chargement', SRCS[i]); loadScriptSequential(i+1); };
document.head.appendChild(s);
}
function initCC(){
// Sécurité
if (!(window.CookieConsent && typeof CookieConsent.run === 'function')){
console.error('[CC] CookieConsent.run indisponible'); return;
}
CookieConsent.run({
cookie: {
name: 'cc_consent_v3',
path: '/',
sameSite: 'Lax',
expiresAfterDays: 180
},
guiOptions: {
consentModal: { layout: 'cloud', position: 'bottom center', equalWeightButtons: false, flipButtons: false },
preferencesModal: { layout: 'box', equalWeightButtons: true, flipButtons: false }
},
// v3: définir "categories" + "language" (obligatoires)
categories: {
necessary: { enabled: true, readOnly: true },
analytics: {},
personalization: {}
},
language: {
default: 'xx',
translations: {
xx: {
consentModal: {
title: TXT.title,
description: TXT.desc,
acceptAllBtn: TXT.acceptAll,
acceptNecessaryBtn: TXT.rejectAll,
showPreferencesBtn: TXT.settings
},
preferencesModal: {
title: TXT.title,
acceptAllBtn: TXT.acceptAll,
acceptNecessaryBtn: TXT.rejectAll,
savePreferencesBtn: TXT.save,
sections: [
{ title: TXT.necessary, description: '' },
{ title: TXT.analytics, description: '', linkedCategory: 'analytics' },
{ title: TXT.personalization, description: '', linkedCategory: 'personalization' }
]
}
}
}
},
// Hooks
onConsent: ({cookie}) => {
if (CookieConsent.acceptedCategory('analytics')) enableAnalytics();
},
onChange: ({changedCategories}) => {
if (changedCategories.includes('analytics')) {
CookieConsent.acceptedCategory('analytics') ? enableAnalytics() : disableAnalytics();
}
}
});
// Lien pour rouvrir
const manage = document.getElementById('cc-manage');
if (manage) {
manage.style.display = 'inline';
manage.addEventListener('click', (e)=>{ e.preventDefault(); CookieConsent.showPreferences(); });
}
}
// Tes hooks perso (ex: activer GA seulement si analytics accepté)
window.enableAnalytics = function(){
if (window.__gaLoaded) return; window.__gaLoaded = true;
// Exemple:
CookieConsent.loadScript('https://www.googletagmanager.com/gtag/js?id=G-XXXX').then(()=>{
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments)}; gtag('js', new Date()); gtag('config','G-XXXX');
});
};
window.disableAnalytics = function(){ /* éventuellement reset */ };
// Lance le chargement (local → CDN fallback)
loadScriptSequential(0);
})();
</script>
<style>
#cc--main .cm__btn, #cc--main .pm__btn { border-radius: 999px; }
#cc-manage { position: fixed; right: 12px; bottom: 12px; font-size: 12px; opacity: .7; z-index: 99999; }
</style>
+400
View File
@@ -0,0 +1,400 @@
/* ==========================================================================
admin_style.css — Mobile global (≤ 956px)
Charge APRÈS Bootstrap
========================================================================== */
/* --- Dropdown admin (utilisé aussi hors mobile) --- */
:root {
--header: #f1f1f1;
}
.menu-wrap {
position: relative;
display: inline-block;
}
.dropdown-menu-custom {
position: absolute;
top: calc(100% + 8px);
right: 0;
background: #fff;
border: 1px solid #dee2e6;
border-radius: 8px;
list-style: none;
padding: 0;
margin: 0;
width: 230px;
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.08);
display: none;
z-index: 1100;
}
.dropdown-menu-custom li {
padding: 12px 18px;
border-bottom: 1px solid #f1f1f1;
cursor: pointer;
font-size: 15px;
}
.dropdown-menu-custom li:last-child {
border-bottom: none;
}
.dropdown-menu-custom li:hover {
background: #f8f9fa;
}
/* ======================== Mobile-first (≤ 956px) ======================== */
@media (max-width: 956px) {
/* Garde-fous */
html,
body {
width: 100%;
max-width: 100%;
overflow-x: hidden;
-webkit-text-size-adjust: 100%;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
img,
video,
iframe {
max-width: 100%;
height: auto;
display: block;
}
body {
padding-left: env(safe-area-inset-left);
padding-right: env(safe-area-inset-right);
-webkit-tap-highlight-color: transparent;
}
/* Grille & conteneurs */
.row {
margin-left: 0 !important;
margin-right: 0 !important;
}
.container {
width: 100% !important;
max-width: 100% !important;
padding-left: 16px !important;
padding-right: 16px !important;
}
/* Layout principal */
main,
.main,
.main1,
.main2 {
display: block !important;
align-items: initial !important;
justify-content: initial !important;
background: transparent !important;
margin: 0 !important;
padding: 0 !important;
width: 100% !important;
max-width: none !important;
}
.btn-lg,
.btn-sm {
min-height: 38px !important;
padding: 0.35rem 0.7rem !important;
font-size: 0.92rem !important;
border-radius: 10px !important;
}
.btn-outline-primary,
.btn-outline-secondary,
.btn-outline-danger,
.btn-outline-dark {
border-width: 1px !important;
box-shadow: none !important;
}
/* groupes d’actions dans les tableaux → même largeur */
.d-flex.gap-2 .btn,
.row .btn {
flex: 1 1 0;
text-align: center;
}
.text-end {
text-align: initial !important;
}
.text-end .btn {
width: 100%;
}
.btn-group {
flex-wrap: wrap;
}
.btn-group .btn {
margin-bottom: 6px;
}
/* Cartes */
.card,
.card * {
overflow-wrap: anywhere;
word-break: break-word;
}
.card,
.card.shadow-sm,
.card.shadow-sm.p-3 {
border-radius: 16px;
overflow: hidden;
}
/* Formulaires */
.form-control,
.form-select,
.form-check-input,
.input-group-text,
.input-group > .btn {
min-height: 44px;
}
/* Tables: scroll horizontal “propre” */
.table-responsive {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
table.table {
font-size: 14px;
}
table.table th,
table.table td {
vertical-align: middle;
}
/* Listes → actions tap-friendly */
td.text-end .d-flex {
gap: 8px;
flex-wrap: wrap;
}
td.text-end .d-flex .btn {
flex: 1 1 calc(50% - 4px);
min-width: 140px;
}
/* Split list/detail */
.row.g-3 > [class*="col-"] .card {
margin-bottom: 0;
}
/* Autocomplete / suggestions */
.suggest,
#suggestions {
position: static !important;
max-height: none !important;
overflow: visible !important;
width: 100% !important;
}
/* Footer */
.site-footer {
font-size: 12px;
padding-bottom: 16px;
}
/* Menu hamburger */
.header-actions .btn {
min-width: 44px;
}
}
/* ====== Composants “pages admin” (tous écrans, pensés mobile) ====== */
.table th {
white-space: nowrap;
}
.table .minw-120 {
min-width: 120px;
}
.photo {
width: 40px;
height: 40px;
object-fit: cover;
border-radius: 8px;
}
.badge {
display: inline-block;
padding: 0.35rem 0.6rem;
border-radius: 9999px;
font-size: 0.85rem;
border: 1px solid transparent;
}
.badge-active {
background: #e8f6ee;
border-color: #27ae60;
color: #2e07a3ff;
}
.badge-expiring {
background: #fff8e6;
border-color: #f39c12;
color: #2e07a3ff;
}
.badge-expired {
background: #fdecea;
border-color: #c0392b;
color: #2e07a3ff;
}
.card pre {
font-size: 0.85rem;
line-height: 1.45;
}
.list-group-item > .badge {
margin-left: auto;
}
:focus-visible {
outline: 2px solid #6366f1;
outline-offset: 2px;
border-radius: 8px;
}
/* ====== Table -> cartes (opt-in via .mobile-cards) ====== */
@media (max-width: 768px) {
table.mobile-cards {
border-collapse: separate;
border-spacing: 0;
}
table.mobile-cards thead {
display: none;
}
table.mobile-cards tbody {
display: block;
}
table.mobile-cards tr {
display: block;
margin: 12px 0;
background: #fff;
border: 1px solid rgba(0, 0, 0, 0.06);
border-radius: 14px;
box-shadow: 0 6px 14px rgba(0, 0, 0, 0.08);
overflow: hidden;
}
table.mobile-cards tr::before {
content: "";
display: block;
height: 3px;
background: #38378b;
opacity: 0.9;
}
table.mobile-cards td {
display: block;
padding: 10px 14px !important;
border: none !important;
background: transparent !important;
}
table.mobile-cards td + td {
border-top: 1px dashed rgba(0, 0, 0, 0.08) !important;
}
table.mobile-cards td::before {
content: attr(data-label);
display: inline-block;
min-width: 120px;
margin-right: 8px;
font-weight: 600;
color: #666;
}
table.mobile-cards td::before:empty {
display: none;
}
}
@media (max-width: 956px) {
.d-flex.justify-content-end.gap-2 .btn {
flex: 1 1 0; /* même largeur */
min-width: 120px; /* évite les boutons trop petits */
text-align: center; /* centre le texte */
}
}
/* Header */
.site-header {
background: var(--header);
border-bottom: 1px solid rgba(168, 39, 39, 0.06);
}
.site-header .container {
margin: 0 auto;
padding: 10px 18px;
display: flex;
align-items: center;
justify-content: space-between;
}
.site-header .btn-circle {
border-radius: 8px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08);
}
.site-header .avatar {
width: 32px;
height: 32px;
border-radius: 999px;
display: inline-grid;
place-items: center;
font-weight: 600;
background: #eef2ff;
color: #3730a3;
}
/* ==== LOGO: mobile vs desktop (béton) ==== */
.brand {
letter-spacing: 0.5px;
font-weight: 700;
display: flex;
align-items: center;
line-height: 0; /* évite petits décalages baseline */
}
.header-left {
display: flex;
align-items: center;
gap: 8px; /* distance logo ↔ langue */
}
.brand .logo-mobile,
.brand .logo-desktop {
display: none !important; /* on cache tout par défaut */
height: 40px; /* taille unique (tu ajustes) */
width: auto;
}
/* Mobile: logo_u */
@media (max-width: 767.98px) {
.brand .logo-mobile {
display: block !important;
}
}
/* Desktop: logo.png */
@media (min-width: 768px) {
.brand .logo-desktop {
display: block !important;
}
}
/* Langue choix*/
/* Sélecteur de langue dans le header */
.lang-switch {
display: flex;
gap: 6px;
height: 42px;
background: #fff;
padding: 6px;
border-radius: 20px;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08);
}
.lang-btn {
display: inline-block;
padding: 4px 10px;
font-size: 13px;
font-weight: 700;
text-decoration: none;
color: #38378b; /* bleu foncé */
border-radius: 14px;
transition: all 0.2s ease-in-out;
}
.lang-btn:hover {
background: rgba(56, 55, 139, 0.08);
}
.lang-btn.active {
background: #38378b; /* bleu foncé */
color: #fff;
}
+71
View File
@@ -0,0 +1,71 @@
<?php
/**
* Prépare le contexte attendu par header.php (pushCfg, alerte_abonnement, avatar/initiales)
* SANS appeler init_praticien.php => pas de redirection abonnement.
*/
/* =========================
Helpers
========================= */
function is_logged_in(): bool {
$role = (string)($_SESSION['role'] ?? ($_SESSION['user_type'] ?? ''));
if (!empty($_SESSION['reference_id']) && $role !== '') return true;
if (!empty($_SESSION['user_id'])) return true;
if (!empty($_SESSION['client_id'])) return true;
if (!empty($_SESSION['praticien_id'])) return true;
return false;
}
/**
* Inclut /public/includes/header.php sans DOCTYPE/HTML/HEAD/BODY et sans recharger bootstrap/css.
*/
function include_header_clean(string $absPath): void {
global $translations, $lang;
ob_start();
include $absPath;
$buf = ob_get_clean() ?: '';
$buf = preg_replace('~<!doctype\s+html[^>]*>~i', '', $buf);
$buf = preg_replace('~</?html\b[^>]*>~i', '', $buf);
$buf = preg_replace('~<head\b[^>]*>.*?</head>~is', '', $buf);
$buf = preg_replace('~</?body\b[^>]*>~i', '', $buf);
// éviter d’écraser la home
$buf = preg_replace('~<link[^>]+href="[^"]*(bootstrap[^"]*\.css|mobil_style\.css|site_style\.css|fonts\.googleapis)[^"]*"[^>]*>~i', '', $buf);
$buf = preg_replace('~<script[^>]+src="[^"]*bootstrap[^"]*bundle[^"]*\.js[^"]*"[^>]*>\s*</script>~i', '', $buf);
echo $buf;
}
function prepare_secure_header_context(): void {
$pdo = $GLOBALS['pdo'] ?? null;
if (!($pdo instanceof PDO)) return;
$role = strtolower((string)($_SESSION['role'] ?? ''));
$refId = (int)($_SESSION['reference_id'] ?? 0);
// Initiales client/praticien
if ($refId > 0 && ($role === 'client' || $role === 'praticien')) {
try {
$table = ($role === 'client') ? 'client' : 'praticien';
$st = $pdo->prepare("SELECT nom, prenom FROM {$table} WHERE id = ? LIMIT 1");
$st->execute([$refId]);
$u = $st->fetch(PDO::FETCH_ASSOC) ?: [];
$nom = trim((string)($u['nom'] ?? ''));
$prenom = trim((string)($u['prenom'] ?? ''));
$ini = mb_strtoupper(
mb_substr($prenom, 0, 1, 'UTF-8') . mb_substr($nom, 0, 1, 'UTF-8'),
'UTF-8'
);
$GLOBALS['initiales'] = $ini !== '' ? $ini : 'U';
} catch (Throwable $e) {
$GLOBALS['initiales'] = 'U';
}
}
}
// Si connecté -> préparer le contexte header sécurisé (sans redirection)
$logged = is_logged_in();
if ($logged) prepare_secure_header_context();
+316
View File
@@ -0,0 +1,316 @@
<?php
/*
* /city_aliases_ru.php
*/
// RU exonyms -> Latin/French city names (lowercase, sans accents si possible)
return [
/* ================= Île-de-France ================= */
'париж' => 'paris',
'булонь-бийанкур' => 'boulogne-billancourt',
'сен-дени' => 'saint-denis',
'аржантёй' => 'argenteuil',
'монтрой' => 'montreuil',
'нантер' => 'nanterre',
'витри-сюр-сен' => 'vitry-sur-seine',
'курбевуа' => 'courbevoie',
'кольомб' => 'colombes',
'аньер-сюр-сен' => 'asnieres-sur-seine',
'олнэ-су-буа' => 'aulnay-sous-bois',
'рюэй-мальмезон' => 'rueil-malmaison',
'сен-мор-де-фоссе' => 'saint-maur-des-fosses',
'шампиньи-сюр-марн' => 'champigny-sur-marne',
'исси-ле-мулино' => 'issy-les-moulineaux',
'клиши' => 'clichy',
'нуази-ле-гран' => 'noisy-le-grand',
'сарсель' => 'sarcelles',
'леваллуа-перре' => 'levallois-perret',
'нёйи-сюр-сен' => 'neuilly-sur-seine',
'антони' => 'antony',
'мэзон-алфор' => 'maisons-alfort',
'кретей' => 'creteil',
'версаль' => 'versailles',
'иври-сюр-сен' => 'ivry-sur-seine',
'пантен' => 'pantin',
'севран' => 'sevran',
'дранси' => 'drancy',
'бобиньи' => 'bobigny',
'бонди' => 'bondy',
'обервилье' => 'aubervilliers',
'сен-уэн' => 'saint-ouen',
'масси' => 'massy',
'мо' => 'meaux',
'сен-жермен-ан-ле' => 'saint-germain-en-laye',
'пюто' => 'puteaux',
'мелён' => 'melun',
'эври' => 'evry',
'сержи' => 'cergy',
'фонтенбло' => 'fontainebleau',
'монруж' => 'montrouge',
'нанси-сюр- марн' => 'nancy',
'сарсель' => 'sarcelles',
'монтеро-фо-ивонн' => 'montereau-fault-yonne',
'сарсель' => 'sarcelles',
'нантер' => 'nanterre',
/* ================= Auvergne–Rhône–Alpes ================= */
'лион' => 'lyon',
'виллербанн' => 'villeurbanne',
'вок-ан-влен' => 'vaulx-en-velin',
'венисьё' => 'venissieux',
'кальюир-э-кюйр' => 'caluire-et-cuire',
'сент-фуа-ле-льон' => 'sainte-foy-les-lyon',
'гренобль' => 'grenoble',
'сен-этьен' => 'saint-etienne',
'клермон-ферран' => 'clermont-ferrand',
'ансии' => 'annecy',
'валанс' => 'valence',
'шамбери' => 'chambery',
'бур-ж-ан-бресс' => 'bourg-en-bresse',
'роан' => 'roanne',
'ойонна' => 'oyonnax',
'анномас' => 'annemasse',
'тнон-ле-бен' => 'thonon-les-bains',
'эвиан-ле-бен' => 'evian-les-bains',
'боннвиль' => 'bonneville',
'вирьё-шательон' => 'villefontaine', /* approx area alias */
'белле' => 'belley',
/* ================= Provence–Alpes–Côte d’Azur ================= */
'марсель' => 'marseille',
'ницца' => 'nice',
'тулон' => 'toulon',
'авиньон' => 'avignon',
'канны' => 'cannes',
'антиб' => 'antibes',
'грас' => 'grasse',
'экс-ан-прованс' => 'aix-en-provence',
'арль' => 'arles',
'фрежюс' => 'frejus',
'драгиньян' => 'draguignan',
'йер' => 'hyeres',
'ла-сейн-сюр-мер' => 'la seyne-sur-mer',
'ла-сьота' => 'la ciotat',
'мартг' => 'martigues',
'салон-де-прованс' => 'salon-de-provence',
'оранж' => 'orange',
'карпантра' => 'carpentras',
'маноск' => 'manosque',
'кавайон' => 'cavaillon',
'мантон' => 'menton',
'сен-рафаэль' => 'saint-raphael',
'канн-сюр-мер' => 'cagnes-sur-mer',
/* ================= Occitanie ================= */
'тулуза' => 'toulouse',
'монпелье' => 'montpellier',
'ним' => 'nimes',
'перпиньян' => 'perpignan',
'безье' => 'beziers',
'нарбон' => 'narbonne',
'каркасон' => 'carcassonne',
'альби' => 'albi',
'тарб' => 'tarbes',
'ош' => 'auch',
'каор' => 'cahors',
'рудез' => 'rodez',
'монто-бан' => 'montauban',
'сет' => 'sete',
'коломье' => 'colomiers',
'бланьяк' => 'blagnac',
'кастр' => 'castres',
/* ================= Nouvelle-Aquitaine ================= */
'бордо' => 'bordeaux',
'лимож' => 'limoges',
'пуа' => 'poitiers',
'пау' => 'pau',
'байонна' => 'bayonne',
'биарриц' => 'biarritz',
'ла-рошель' => 'la rochelle',
'ниор' => 'niort',
'ангулем' => 'angouleme',
'перигё' => 'perigueux',
'мон-де-марсан' => 'mont-de-marsan',
'брив-ла-гайард' => 'brive-la-gaillarde',
'бержерак' => 'bergerac',
'ажен' => 'agen',
'анксьон' => 'anglet',
/* ================= Grand Est ================= */
'страсбург' => 'strasbourg',
'реймс' => 'reims',
'мец' => 'metz',
'нанси' => 'nancy',
'мюлуз' => 'mulhouse',
'труа' => 'troyes',
'колмар' => 'colmar',
'эпиналь' => 'epinal',
'шалон-ан-шампань' => 'chalons-en-champagne',
'шарлевиль-мезьер' => 'charleville-mezieres',
'седан' => 'sedan',
'сент-дизье' => 'saint-dizier',
'саррегемин' => 'sarreguemines',
'илкирш-графенштаден' => 'illkirch-graffenstaden',
'сент-луи' => 'saint-louis',
'агно' => 'haguenau',
'томблен' => 'tomblaine',
/* ================= Hauts-de-France ================= */
'лилль' => 'lille',
'рубай' => 'roubaix',
'туркуэн' => 'tourcoing',
'дакс' => 'dax', /* ignore if not needed */
'амиен' => 'amiens',
'валансен' => 'valenciennes',
'дуэ' => 'douai',
'денкерк' => 'dunkerque',
'кале' => 'calais',
'компьень' => 'compiegne',
'сен-кантен' => 'saint-quentin',
'аррас' => 'arras',
'булонь-сюр-мер' => 'boulogne-sur-mer',
'абвиль' => 'abbeville',
'ламберсар' => 'lambersart',
'вилнев-д-аск' => 'villeneuve-d-ascq',
'курриер' => 'courrieres',
'ли-ленн' => 'lievin',
/* ================= Pays de la Loire ================= */
'нант' => 'nantes',
'анже' => 'angers',
'ле-ман' => 'le mans',
'сен-назер' => 'saint-nazaire',
'шоле' => 'cholet',
'ла-рош-сюр-йон' => 'la roche-sur-yon',
'лаваль' => 'laval',
/* ================= Bretagne ================= */
'ренн' => 'rennes',
'брест' => 'brest',
'квенпер' => 'quimper',
'ван' => 'vannes',
'сен-мало' => 'saint-malo',
'лориан' => 'lorient',
'сен-бриё' => 'saint-brieuc',
/* ================= Normandie ================= */
'руан' => 'rouen',
'ле-авр' => 'le havre',
'ле-аврр' => 'le havre',
'каен' => 'caen',
'шербур' => 'cherbourg',
'эвре' => 'evreux',
'сен-ло' => 'saint-lo',
'флер' => 'flers',
'алансон' => 'alencon',
/* ================= Centre–Val de Loire ================= */
'тур' => 'tours',
'орлеан' => 'orleans',
'шартр' => 'chartres',
'бурж' => 'bourges',
'блуа' => 'blois',
'шатору' => 'chateauroux',
/* ================= Bourgogne–Franche-Comté ================= */
'дижон' => 'dijon',
'безансон' => 'besancon',
'оссер' => 'auxerre',
'макон' => 'macon',
'невр' => 'nevers',
'шалон-сюр-сон' => 'chalon-sur-saone',
'дан-ле-золе' => 'dole',
/* ================= Grand Ouest divers ================= */
'ла-манш' => 'saint-lo', /* dept seat hint */
'пуатье' => 'poitiers',
'сан-назар' => 'saint-nazaire',
'сан-мало' => 'saint-malo',
'конкарно' => 'concarneau',
'морле' => 'morlaix',
'сен-брюк' => 'saint-brieuc',
'баянн' => 'bayonne', /* duplicate safe */
/* ================= Corse ================= */
'аджакьо' => 'ajaccio',
'бастия' => 'bastia',
/* ================= Outre-mer ================= */
'фор-де-франс' => 'fort-de-france',
'басс-тер' => 'basse-terre',
'каенн' => 'cayenne',
'сен-дени' => 'saint-denis',
'мамудзу' => 'mamoudzou',
'пуан-а-питр' => 'pointe-a-pitre',
/* ================= Autres communes fréquentes ================= */
'нанси' => 'nancy',
'реймс' => 'reims',
'амьен' => 'amiens',
'тур' => 'tours',
'ниш' => 'nice',
'монтабан' => 'montauban',
'канн' => 'cannes',
'анси' => 'annecy',
'везул' => 'vesoul',
'перига' => 'perigueux',
'лейон' => 'lyon',
/* ===== Variantes pour Saint-/Sainte- (дублеты «сен-» / «сент-») ===== */
'сент-этьен' => 'saint-etienne',
'сент-мало' => 'saint-malo',
'сент-назер' => 'saint-nazaire',
'сент-дени' => 'saint-denis',
'сент-ло' => 'saint-lo',
'сент-омер' => 'saint-omer',
'сент-рафаэль' => 'saint-raphael',
'сент-квентин' => 'saint-quentin',
'сент-жермен-ан-ле' => 'saint-germain-en-laye',
'сент-мор-де-фоссе' => 'saint-maur-des-fosses',
'сент-дизье' => 'saint-dizier',
'сент-луи' => 'saint-louis',
'сент-брие' => 'saint-brieuc',
/* ===== Variantes avec articles Le/La/Les ===== */
'ле-авр' => 'le havre',
'ле-ман' => 'le mans',
'ла-рош-сюр-йон' => 'la roche-sur-yon',
'ле-кане' => 'le cannet',
'ле-мюро' => 'les mureaux',
'ле-сабль-д-олон' => 'les sables-d-olonne',
/* ===== Agglo parisienne complémentaires ===== */
'аржантей' => 'argenteuil',
'монфермей' => 'montfermeil',
'вильжюиф' => 'villejuif',
'валь-де-репюблик' => 'val-de-reuil', /* approx */
'нежен-сюр-сен' => 'gennevilliers',
'оржи' => 'orges', /* ignore if not used */
/* ===== Sud-Ouest complémentaires ===== */
'байон' => 'bayonne',
'мон-де-марсан' => 'mont-de-marsan',
'песак' => 'pesac', /* if stored */
/* ===== Côte d’Azur complémentaires ===== */
'сан-лерен-дю-вар' => 'saint-laurent-du-var',
'вилльфранш-сюр-мер' => 'villefranche-sur-mer',
'ле-кане' => 'le cannet',
/* ===== Languedoc complémentaires ===== */
'безье' => 'beziers',
'альес' => 'ales',
'агд' => 'agde',
/* ===== Savoie / Haute-Savoie ===== */
'анси-ле-вье' => 'annecy',
'кранс-же' => 'cran-gevrier',
'анмас' => 'annemasse',
/* ===== Divers couverts par translit mais utiles en alias ===== */
'луара' => 'loire',
'исер' => 'isere',
'вар' => 'var',
'воклюз' => 'vaucluse',
'кукурон' => 'cucuron',
];
+912
View File
@@ -0,0 +1,912 @@
@import url("https://fonts.googleapis.com/css2?family=Montserrat:wght@100..900&display=swap");
:root {
--red: #e03c3f;
--white: #ffffff;
--blue: #38378b;
--mint: #06ffa8;
--hero: #8ca9ff; /* bleu clair fond hero (d’après maquette) */
--header: #f1f1f1;
--radius: 22px;
--c-bg: #8fa2ff; /* fond bleu/violet actuel de ton hero */
--c-dark: #38378b; /* bleu foncé de ta charte */
--c-white: #ffffff;
--c-shadow: 0 10px 24px rgba(0, 0, 0, 0.12);
}
.only-mobile {
display: none;
}
@media (max-width: 956px) {
.only-desktop {
display: none;
}
.only-mobile {
display: inline;
}
}
/*Boutons page accueil du site*/
.steps {
width: min(800px, 92vw);
margin: 40px auto 0;
position: relative;
display: grid;
gap: 36px;
}
.step {
position: relative;
display: inline-flex;
align-items: center;
gap: 0;
filter: drop-shadow(0 6px 10px rgba(0, 0, 0, 0.08));
}
.step-badge {
display: grid;
place-items: center;
width: 110px;
height: 110px;
border-radius: 50%;
background: var(--c-white);
color: var(--c-dark);
font-weight: 800;
font-size: clamp(28px, 5vw, 56px);
line-height: 1;
box-shadow: var(--c-shadow);
z-index: 1;
}
.step-label {
background: var(--c-white);
color: var(--c-dark);
font-weight: 700;
font-size: clamp(16px, 2.6vw, 26px);
padding: 14px 26px 14px 38px; /* pour chevauchement côté gauche */
border-radius: 999px;
white-space: nowrap;
box-shadow: var(--c-shadow);
margin-left: -30px;
z-index: 0;
}
/* Étape 3 inversée */
.step--reverse {
flex-direction: row-reverse; /* inverse ordre badge et label */
}
.step--reverse .step-label {
padding: 14px 38px 14px 26px; /* espace à droite */
margin-left: 0;
margin-right: -30px; /* chevauchement côté droit */
}
/* tailles différentes pour chaque étape */
.step:nth-child(1) .step-badge {
width: 110px;
height: 110px;
font-size: clamp(28px, 5vw, 56px);
}
.step:nth-child(2) .step-badge {
width: 132px;
height: 132px;
font-size: clamp(32px, 5.5vw, 64px);
}
.step:nth-child(3) .step-badge {
width: 158px;
height: 158px;
font-size: clamp(36px, 6vw, 72px);
}
/* placements (comme sur ta maquette) */
.step--left {
justify-self: start;
transform: translateX(0);
}
.step--center {
justify-self: center;
transform: translateY(8px);
padding-left: 230px;
}
.step--right {
justify-self: end;
transform: translateX(0);
}
/*Fin boutons rondes*/
* {
box-sizing: border-box;
}
html,
body {
height: 100%;
}
body {
margin: 0;
font-family:
"Montserrat",
system-ui,
-apple-system,
Segoe UI,
Roboto,
Arial,
sans-serif;
color: #111;
/*background: var(--hero);*/
display: grid;
grid-template-rows: auto 1fr auto; /* header, contenu, footer */
}
/* Header */
.site-header {
background: var(--header);
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
}
.site-header .container {
max-width: 1200px;
margin: 0 auto;
padding: 10px 18px;
display: flex;
align-items: center;
justify-content: space-between;
}
/* ==== LOGO: mobile vs desktop (béton) ==== */
.brand {
letter-spacing: 0.5px;
font-weight: 700;
display: flex;
align-items: center;
line-height: 0; /* évite petits décalages baseline */
}
.header-left {
display: flex;
align-items: center;
gap: 8px; /* distance logo ↔ langue */
}
.brand .logo-mobile,
.brand .logo-desktop {
display: none !important; /* on cache tout par défaut */
height: 40px; /* taille unique (tu ajustes) */
width: auto;
}
/* Mobile: logo_u */
@media (max-width: 767.98px) {
.brand .logo-mobile {
display: block !important;
}
}
/* Desktop: logo.png */
@media (min-width: 768px) {
.brand .logo-desktop {
display: block !important;
}
}
/* Actions header */
.header-actions {
display: flex;
align-items: center;
gap: 10px;
}
.icon-btn {
border: none;
background: #000;
color: #fff;
width: 28px;
height: 28px;
border-radius: 6px;
}
/* HERO */
.hero {
padding: 0 0 56px;
background: var(--hero);
}
.hero .container {
max-width: 1100px;
margin: 0 auto;
padding: 0 18px;
}
.hero-title {
color: #38378b;
text-align: center;
font-size: 48px;
line-height: 1.16;
font-weight: 800;
margin: 5px 0 26px;
}
/* Bouton LOGIN */
.btn-login {
background: var(--red);
color: #fff;
text-decoration: none;
padding: 8px 14px;
border-radius: 999px;
font-weight: 500;
box-shadow: 0 8px 22px rgba(0, 0, 0, 0.18);
/*font-size: clamp(12px, 2vw, 16px);*/
}
/* grille 3 au-dessus + 1 en dessous */
/* style des blocs ovales */
.hero .row {
display: flex;
justify-content: center;
gap: 20px;
margin: 15px 0;
flex-wrap: wrap; /* au cas où ; reste cohérent */
}
.chip {
appearance: none;
background: #fff;
cursor: pointer;
color: #000000;
width: 230px; /* largeur fixe */
text-align: center; /* texte centré */
padding: 12px 0; /* hauteur uniforme */
border-radius: 15px; /* ovale */
font-weight: 600;
font-size: 16px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.15);
border: 0;
}
/* Footer plus large (haut) et gris, toujours en bas */
footer {
background: var(--header);
border-top: 1px solid rgba(0, 0, 0, 0.06);
padding: 22px 20px; /* plus haut qu’avant */
}
footer p {
margin: 0;
text-align: center;
color: #222;
font-size: 13px;
}
/*===================================================================================================================*/
/*La partie affichage pour la page aprés la recherche*/
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 18px;
}
/* Liste : espace entre blocs */
.pro-list {
display: flex;
flex-direction: column;
gap: 40px; /* augmente l’écart vertical */
padding: 20px 0 40px;
}
/* Carte = 4 colonnes : photo | infos | vidéo | bouton */
.pro-card {
background: #fff;
border-radius: 28px;
box-shadow: 0 14px 30px rgba(0, 0, 0, 0.18);
display: grid;
/* NE PAS forcer start ici */
align-items: stretch; /* <— important */
gap: 20px;
padding: 20px;
grid-template-columns: 260px 1.5fr 240px 180px;
grid-template-areas: "photo info video cta";
}
/* 1) Photo portrait, non déformée (recadrage OK) */
.pro-photo {
grid-area: photo;
}
.pro-photo img {
width: 100%;
aspect-ratio: 3/4; /* portrait */
object-fit: cover; /* pas de déformation */
display: block;
border-radius: 22px;
}
/* 2) Infos */
.pro-info {
grid-area: info;
padding: 0 6px;
color: #1b1b1b;
}
.pro-name {
margin: 0 0 8px;
font-size: 26px;
font-weight: 800;
}
.pro-spec {
color: #38378b;
font-weight: 700;
margin-bottom: 10px;
}
.pro-desc {
color: #3f3f3f;
line-height: 1.45;
font-size: 1.05rem; /* augmente légèrement la taille */
max-width: 95%; /* laisse plus d’espace dans la colonne */
}
.pro-info .favori {
top: 10px;
right: 10px; /* passe à left:10px si tu le veux à gauche */
z-index: 2;
font-size: 16px;
line-height: 1;
cursor: pointer;
user-select: none;
filter: drop-shadow(0 2px 6px rgba(6, 6, 6, 0.25));
transition:
transform 0.15s ease,
opacity 0.15s ease;
}
.pro-info .favori:hover {
transform: scale(1.1);
}
.pro-info .favori.ajoute {
color: rgb(12, 12, 11);
transform: scale(1.15);
}
/* 3) Vidéo (vignette 16:9) */
/* même gabarit pour lien vidéo ET placeholder */
.pro-video,
.pro-video.placeholder {
position: relative;
display: block;
aspect-ratio: 4 / 3; /* <- ton format souhaité */
max-height: 180px;
border-radius: 18px;
overflow: hidden;
background: #000; /* évite les bordures blanches */
}
/* l'image occupe tout l'espace du cadre */
.pro-video img,
.pro-video.placeholder img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
/* optionnel: largeur de la colonne vidéo */
.pro-video-col {
display: flex;
align-items: center; /* centre verticalement dans la colonne */
justify-content: center;
width: clamp(240px, 32vw, 200px);
}
/* 4) Bouton dans sa colonne, centré verticalement et aligné à droite */
.pro-cta-col {
grid-area: cta;
display: flex;
align-items: center; /* centre verticalement */
justify-content: center; /* centre horizontalement */
}
.pro-cta {
background: #e03c3f;
color: #fff;
text-decoration: none;
font-weight: 600;
padding: 10px 18px;
border-radius: 999px;
box-shadow: 0 8px 22px rgba(0, 0, 0, 0.18);
white-space: nowrap;
/*font-size: 0.95rem;*/
}
/* Langue choix*/
/* Sélecteur de langue dans le header */
.lang-switch {
display: flex;
gap: 6px;
height: 42px;
background: #fff;
padding: 6px;
border-radius: 20px;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08);
}
.lang-btn {
display: inline-block;
padding: 4px 10px;
font-size: 13px;
font-weight: 700;
text-decoration: none;
color: #38378b; /* bleu foncé */
border-radius: 14px;
transition: all 0.2s ease-in-out;
}
.lang-btn:hover {
background: rgba(56, 55, 139, 0.08);
}
.lang-btn.active {
background: #38378b; /* bleu foncé */
color: #fff;
}
a {
text-decoration: none;
color: #000;
}
/* Compact */
.pro-card--sm {
--pro-card-pad: 12px;
--pro-card-gap: 10px;
--pro-avatar: 72px;
--pro-title: 16px;
--pro-lines: 2;
}
/* Large */
.pro-card--lg {
--pro-card-pad: 22px;
--pro-card-gap: 18px;
--pro-avatar: 100px;
--pro-title: 20px;
--pro-lines: 4;
}
.results-title,
.results-meta,
.result-count {
color: #fff;
text-align: center;
}
.btn-logo {
--bs-btn-bg: #38378b;
--bs-btn-border-color: #38378b;
--bs-btn-hover-bg: #2f2e77;
--bs-btn-hover-border-color: #2f2e77;
--bs-btn-active-bg: #24235e;
--bs-btn-active-border-color: #24235e;
--bs-btn-color: #fff; /* blanc par défaut */
--bs-btn-focus-shadow-rgb: 56, 55, 139; /* halo focus */
}
/* On force la couleur du texte à blanc dans tous les états */
.btn-logo,
.btn-logo:hover,
.btn-logo:active,
.btn-logo:focus,
.btn-logo:focus-visible {
color: #fff !important;
}
/* Menu compact, largeur auto comme dans ta maquette */
.help-menu {
min-width: auto; /* override Bootstrap */
width: max-content; /* s’adapte au contenu */
max-width: 92vw; /* borne mobile */
border-radius: 12px;
padding: 8px;
}
.help-menu .dropdown-item {
padding: 10px 14px;
white-space: nowrap;
}
.help-menu .dropdown-item + .dropdown-item {
margin-top: 6px;
} /* petits espacements */
@media (min-width: 576px) {
.help-menu {
max-width: 360px;
} /* borne desktop */
}
/* Taille tactile du bouton “?” */
.question-btn {
width: 38px;
height: 38px;
font-size: 22px;
font-weight: 700;
line-height: 1;
padding: 0;
}
/* Menu plus étroit (recommandé) */
.offcanvas-slim {
--bs-offcanvas-width: clamp(240px, 80vw, 320px);
}
/* Option: largeur calée au titre (plus “compacte”) */
/* .offcanvas-slim{ --bs-offcanvas-width: fit-content; max-width: min(90vw, 360px); } */
/* Fallback minimal sans Bootstrap */
.d-none {
display: none !important;
}
.d-sm-inline {
display: none !important;
}
.d-sm-none {
display: inline !important;
}
.text-nowrap {
white-space: nowrap !important;
}
@media (min-width: 576px) {
.d-sm-inline {
display: inline !important;
}
.d-sm-none {
display: none !important;
}
}
/* ==== VISUEL COMMUN POUR TOUS LES MENUS ==== */
.menu-panel {
background: #fff;
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 12px;
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.08);
min-width: 230px;
padding: 8px 0;
}
/* Items menu principal (li custom OU .dropdown-item Bootstrap) */
.menu-panel > li,
.menu-panel .dropdown-item {
padding: 12px 18px;
font-size: 16px;
line-height: 1.25;
white-space: nowrap;
cursor: pointer;
transition: background 0.2s ease;
}
.menu-panel > li + li,
.menu-panel .dropdown-item + .dropdown-item {
border-top: 1px solid #f1f1f1;
}
.menu-panel > li:hover,
.menu-panel .dropdown-item:hover {
background: #f8f9fa; /* juste fond gris clair */
}
.menu-panel a {
color: inherit;
text-decoration: none;
display: block;
cursor: pointer;
}
/* ==== MENU “BURGER” CUSTOM ==== */
.menu-wrap {
position: relative;
}
.dropdown-menu-custom {
position: absolute;
top: calc(100% + 8px);
right: 0;
list-style: none;
margin: 0;
display: none; /* fermé par défaut */
z-index: 1100;
}
.dropdown-menu-custom.is-open {
display: block;
}
/* Sous-menu */
.has-submenu .submenu-toggle {
width: 100%;
text-align: left;
background: none;
border: 0;
padding: 0.55rem 1rem;
font-weight: 400;
display: flex;
align-items: center;
justify-content: space-between;
cursor: pointer;
}
.submenu {
list-style: none;
margin: 0;
padding: 0 0 0.5rem 0;
}
.submenu > li {
padding: 0.4rem 1rem 0.4rem 1.5rem;
cursor: pointer;
transition: background 0.2s ease;
}
.submenu > li:hover {
background: #fff; /* juste fond gris clair */
}
.submenu[hidden] {
display: none;
}
/* ==== MENU “?” BOOTSTRAP ==== */
.dropdown-menu.menu-panel {
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 12px;
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.08);
min-width: 230px;
padding: 8px 0;
}
/* Sécurité : que rien ne coupe les menus */
.site-header,
.header-actions,
.menu-wrap,
.dropdown {
overflow: visible;
}
/* Bouton avatar (menu principal) */
#btnMenu {
background: none;
border: none;
padding: 0;
cursor: pointer;
-webkit-tap-highlight-color: transparent; /* enlève flash bleu sur mobile */
padding-left: 10px;
}
/* Supprime le halo/cadre bleu par défaut */
#btnMenu:focus,
#btnMenu:active {
outline: none;
box-shadow: none;
}
/* Si tu veux garder une indication focus clavier accessible */
#btnMenu:focus-visible {
outline: 2px solid var(--color-blue); /* ou une couleur douce */
outline-offset: 3px;
border-radius: 50%;
}
.footer-top-links {
color: #38378b;
font-weight: 600;
}
.footer-top-links a {
color: inherit; /* hérite du bleu foncé */
}
/* ===== Personnalisation bandeau cookies (CookieConsent v3) ===== */
#cc-main {
--cc-font-family: "Montserrat", sans-serif;
/* Couleurs de texte */
--cc-primary-color: #38378b; /* titres/links -> bleu foncé */
--cc-secondary-color: #38378b; /* paragraphe du bandeau */
/* Bouton principal (Accepter tout) -> rouge arrondi */
--cc-btn-border-radius: 22px;
--cc-btn-primary-bg: #e03c3f;
--cc-btn-primary-color: #ffffff;
--cc-btn-primary-border-color: #e03c3f;
--cc-btn-primary-hover-bg: #c92f33; /* rouge un peu plus foncé */
--cc-btn-primary-hover-color: #ffffff;
--cc-btn-primary-hover-border-color: #c92f33;
/* Boutons secondaires (Refuser / Paramétrer) -> fond blanc, bord gris, arrondi */
--cc-btn-secondary-bg: #ffffff;
--cc-btn-secondary-color: #38378b;
--cc-btn-secondary-border-color: #d9dee3;
--cc-btn-secondary-hover-bg: #f4f6f8;
--cc-btn-secondary-hover-color: #38378b;
--cc-btn-secondary-hover-border-color: #c9d0d6;
/* Séparateur & fond pieds/bords pour un rendu plus clean */
--cc-separator-border-color: #eef2f5;
--cc-footer-bg: #ffffff;
--cc-footer-color: #38378b;
/* Optionnel: arrondir la carte elle-même */
--cc-modal-border-radius: 22px;
}
/* Si tu veux forcer l’arrondi aussi quand des classes sont injectées */
#cc-main .cm__btn,
#cc-main .pm__btn {
border-radius: 22px;
}
/*===================================================================================================================*/
/* --- Container modal --- */
.vmodal {
position: fixed;
inset: 0;
display: none;
z-index: 99999;
}
.vmodal[aria-hidden="false"] {
display: block;
}
.vmodal__backdrop {
position: absolute;
inset: 0;
background: rgba(0, 0, 0, 0.6);
}
/* --- Boîte --- */
.vmodal__dialog {
position: relative;
z-index: 2;
max-width: 1000px;
width: 92%;
margin: 40px auto;
background: #fff;
border-radius: 16px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.25);
overflow: hidden; /* pour arrondis propres */
}
/* --- Header “Bootstrap-like” --- */
.vmodal__header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.85rem 1rem;
border-bottom: 1px solid rgba(0, 0, 0, 0.08);
background: #fff;
}
.vmodal__title {
margin: 0;
font-size: 1.1rem;
font-weight: 600;
color: #111;
}
/* --- Close visible (rond blanc) --- */
.vmodal__close {
border: 0;
background: #fff;
color: #111;
width: 36px;
height: 36px;
border-radius: 999px;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 22px;
line-height: 1;
cursor: pointer;
border: 1px solid rgba(0, 0, 0, 0.1);
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.18);
}
.vmodal__close:hover {
filter: brightness(0.95);
}
/* --- Body + ratio vidéo --- */
.vmodal__body {
padding: 1rem;
}
.vmodal__ratio {
position: relative;
width: 100%;
aspect-ratio: 16/9;
overflow: hidden;
border-radius: 12px;
background: #000;
}
.vmodal__ratio iframe {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
border: 0;
}
/* --- Footer --- */
.vmodal__footer {
display: flex;
justify-content: flex-end;
gap: 0.5rem;
padding: 0.85rem 1rem;
border-top: 1px solid rgba(0, 0, 0, 0.08);
background: #fff;
}
.btn.btn-secondary-like {
appearance: none;
border: 1px solid #ced4da;
background: #f8f9fa;
color: #212529;
padding: 0.5rem 0.9rem;
border-radius: 0.35rem;
cursor: pointer;
}
.btn.btn-secondary-like:hover {
background: #e9ecef;
}
.no-results {
text-align: center; /* centre le texte */
color: #fff; /* texte en blanc */
}
/* --- Mobile --- */
@media (max-width: 575.98px) {
.vmodal__dialog {
margin: 16px auto;
width: 96%;
}
.vmodal__title {
font-size: 1rem;
}
}
.pager {
display: flex;
justify-content: center;
align-items: center;
gap: 10px;
margin: 22px 0;
}
.pager a {
text-decoration: none;
font-weight: 700;
}
.pager-arrow {
padding: 10px 18px;
border-radius: 999px;
border: 1px solid #e0e3eb;
background: #fff;
color: #38378b;
box-shadow: 0 6px 14px rgba(0, 0, 0, 0.06);
transition:
background 0.2s,
transform 0.1s,
opacity 0.2s;
}
.pager-arrow:hover {
background: #f8f9ff;
transform: translateY(-1px);
}
.pager-arrow.is-disabled {
opacity: 0.4;
pointer-events: none;
}
.pager-page {
width: 40px;
height: 40px;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 50%;
border: 1px solid #e0e3eb;
background: #fff;
color: #38378b;
box-shadow: 0 6px 14px rgba(0, 0, 0, 0.06);
transition:
background 0.2s,
transform 0.1s,
color 0.2s,
border-color 0.2s;
}
.pager-page:hover {
background: #f8f9ff;
transform: translateY(-1px);
}
.pager-page.is-active {
background: #e03c3f;
color: #fff;
border-color: #e03c3f;
transform: none;
}
+58
View File
@@ -0,0 +1,58 @@
<?php
// Si quelqu'un essaie d'appeler directement /includes/db.php dans le navigateur, on coupe
if (isset($_SERVER['SCRIPT_FILENAME']) && realpath(__FILE__) === realpath($_SERVER['SCRIPT_FILENAME'])) {
http_response_code(403);
exit('
<!doctype html>
<html lang="ru"><head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>403 — Accès refusé</title>
<style>
html,body{height:100%;margin:0}
body{display:flex;align-items:center;justify-content:center;background:#f7f7f9;font-family:system-ui,Segoe UI,Roboto,Arial}
.card{max-width:560px;width:92%;background:#fff;border:1px solid #e5e7eb;border-radius:14px;box-shadow:0 8px 24px rgba(0,0,0,.08);padding:28px;text-align:center}
.emoji{font-size:48px;margin-bottom:8px}
h1{margin:6px 0 10px;font-size:22px}
p{margin:0 0 18px;color:#6b7280}
.row{display:flex;gap:10px;justify-content:center;flex-wrap:wrap}
.btn{padding:10px 14px;border-radius:999px;text-decoration:none;display:inline-block}
.btn-primary{background:#E03C3F;color:#fff}
.btn-outline{border:1px solid #d1d5db;color:#fff;background:#38378B}
</style>
</head><body>
<div class="card">
<div class="emoji"></div>
<h1>Accès refusé</h1>
<p>Vous n’êtes pas autorisé à consulter cette page.</p>
<div class="row">
<a class="btn btn-primary" href="/connexion.php">Se connecter</a>
<a class="btn btn-outline" href="/">Retour à l’accueil</a>
</div>
</div>
</body></html>');
}
$host = getenv('APP_DB_HOST') ?: 'localhost';
$dbname = getenv('APP_DB_NAME') ?: '';
$user = getenv('APP_DB_USER') ?: '';
$password = getenv('APP_DB_PASS') ?: '';
$charset = getenv('APP_DB_CHARSET') ?: 'utf8mb4';
try {
$pdo = new PDO(
"mysql:host=$host;dbname=$dbname;charset=$charset",
$user,
$password,
[
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
]
);
} catch (PDOException $e) {
error_log('DB connect error: ' . $e->getMessage());
http_response_code(500);
exit('Erreur de connexion à la base de données. racine');
}
?>
+507
View File
@@ -0,0 +1,507 @@
/* =========================================================
1) Liens / Chips
========================================================= */
.chip-link {
display: inline-flex;
align-items: center;
justify-content: center;
text-decoration: none;
}
/* =========================================================
2) Grand panneau blanc
========================================================= */
.home-panel {
max-width: 850px;
margin: 22px auto 0;
background: rgba(255, 255, 255, 0.92);
border: 1px solid rgba(255, 255, 255, 0.45);
border-radius: 22px;
/*box-shadow: 0 10px 26px rgba(0, 0, 0, 0.14);*/
overflow: hidden;
}
.home-panel__inner {
padding: 18px 16px;
}
.home-panel__divider {
height: 1px;
background: rgba(0, 0, 0, 0.06);
margin: 14px 0;
}
/* =========================================================
3) Checklist
========================================================= */
.home-checklist {
padding: 14px 14px 10px;
}
.home-checklist ul {
list-style: none;
padding: 0;
margin: 0;
display: grid;
gap: 10px;
}
.home-checklist li {
display: flex;
gap: 10px;
align-items: flex-start;
color: #1b1b1b;
}
.home-check {
width: 22px;
height: 22px;
border-radius: 999px;
background: rgba(6, 255, 168, 0.18);
display: grid;
place-items: center;
}
.home-check svg {
width: 14px;
height: 14px;
}
/* =========================================================
4) Titres
========================================================= */
.home-h2 {
margin: 0 0 12px;
font-size: 22px;
font-weight: 800;
color: #1b1b1b;
}
/* =========================================================
5) Liste praticiens
========================================================= */
.hp-list {
display: grid;
gap: 12px;
}
/* --- Carte --- */
.hp-card {
background: #fff;
border: 1px solid rgba(0, 0, 0, 0.06);
border-radius: 16px;
box-shadow: 0 8px 18px rgba(0, 0, 0, 0.08);
padding: 12px;
}
/* --- Ligne 1 : avatar | texte | rating --- */
.hp-row1 {
display: flex;
align-items: flex-start;
gap: 12px;
}
.hp-avatar {
width: 56px;
height: 56px;
border-radius: 14px;
object-fit: cover;
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12);
}
.hp-main {
flex: 1;
min-width: 0;
}
.hp-name {
margin: 0;
font-size: 16px;
font-weight: 800;
color: #111;
line-height: 1.15;
}
.hp-spec {
margin-top: 2px;
font-size: 13px;
font-weight: 700;
color: #6b7280;
}
/* --- Rating (étoiles base + fill) --- */
.hp-rating {
text-align: right;
min-width: 110px;
}
.hp-stars {
position: relative;
display: inline-block;
line-height: 1;
font-size: 13px;
letter-spacing: 1px;
}
.hp-stars .stars-base {
color: #ddd;
}
.hp-stars .stars-fill {
position: absolute;
inset: 0;
overflow: hidden;
white-space: nowrap;
color: #f5b301;
}
.hp-note {
margin-top: 2px;
font-size: 0.85rem;
color: #111;
}
/* --- Ligne 2 : Online (gauche) | bouton (droite) --- */
.hp-row2 {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 10px;
padding-left: 68px; /* aligné sous le texte (56 avatar + gap) */
}
.hp-online {
display: flex;
align-items: center;
gap: 6px;
font-size: 13px;
font-weight: 700;
color: #111;
opacity: 0.9;
white-space: nowrap;
}
/* --- Bouton --- */
.hp-btn {
background: var(--red, #e03c3f);
color: #fff;
text-decoration: none;
font-weight: 500;
font-size: 14px;
border-radius: 999px;
padding: 10px 14px;
min-width: 110px;
display: inline-flex;
align-items: center;
justify-content: center;
box-shadow: 0 8px 18px rgba(0, 0, 0, 0.16);
white-space: nowrap;
}
/* =========================================================
6) Mini steps
========================================================= */
.hp-steps {
background: #fff;
border: 1px solid rgba(0, 0, 0, 0.06);
border-radius: 16px;
box-shadow: 0 8px 18px rgba(0, 0, 0, 0.08);
padding: 12px 14px;
}
/* --- Nouveau : entête (sobre) --- */
.hp-steps__head {
text-align: center;
margin-bottom: 10px;
}
.hp-steps__title {
margin: 0;
font-size: 18px;
font-weight: 900;
color: #111;
}
.hp-steps__sub {
margin: 6px 0 0;
font-size: 13px;
font-weight: 600;
color: #6b7280;
}
/* --- Nouveau : grille steps propre (sans <br>) --- */
.hp-steps__grid {
display: grid;
grid-template-columns: 1fr;
gap: 10px;
padding-top: 6px;
}
.hp-steps__item {
display: flex;
align-items: center;
gap: 10px;
font-weight: 500;
font-size: 16px;
color: #111;
}
.hp-steps__arr {
color: #6b7280;
font-weight: 900;
}
/* =========================================================
7) Articles
========================================================= */
.hp-articles {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
.ha {
display: block;
text-decoration: none;
background: #fff;
border: 1px solid rgba(0, 0, 0, 0.06);
border-radius: 16px;
box-shadow: 0 8px 18px rgba(0, 0, 0, 0.08);
overflow: hidden;
color: #111;
}
.ha-img {
height: 92px;
background: #e9edf8;
}
.ha-body {
padding: 10px 12px 12px;
}
.ha-title {
margin: 0 0 6px;
font-size: 15px;
font-weight: 600;
}
.ha-sub {
margin: 0;
font-size: 12.5px;
font-weight: 400;
color: #6b7280;
}
.hp-all {
text-align: center;
padding-top: 10px;
}
.hp-all a {
font-weight: 600;
color: #1f3bb3;
text-decoration: none;
}
.spec-cta-wrap {
margin-top: 24px;
padding: 28px 22px;
border-radius: 12px;
background: #8ca9ff;
box-shadow: 0 20px 40px rgba(127, 91, 255, 0.2);
color: #fff;
/* ⬇️ IMPORTANT */
display: grid;
grid-template-columns: auto 1fr auto; /* gauche | milieu | droite */
align-items: center;
column-gap: 22px;
text-align: left;
}
/* Titre */
.spec-cta-wrap h3 {
margin: 0;
font-size: 34px; /* pour se rapprocher de la capture 2 */
font-weight: 700;
line-height: 1.1;
white-space: nowrap; /* optionnel: évite retour ligne */
}
.spec-cta-wrap p {
margin: 0;
font-size: 18px;
opacity: 0.95;
line-height: 1.35;
max-width: 520px; /* pour un look “milieu” */
}
/* Bouton blanc */
.spec-cta {
display: inline-flex;
align-items: center;
justify-content: center;
white-space: nowrap;
padding: 12px 24px;
border-radius: 999px;
background: #fff;
color: #8ca9ff;
font-weight: 600;
font-size: 15px;
text-decoration: none;
box-shadow: 0 8px 18px rgba(0, 0, 0, 0.12);
transition:
transform 0.18s ease,
box-shadow 0.18s ease;
}
/* Hover */
.spec-cta:hover {
transform: translateY(-2px);
box-shadow: 0 12px 22px rgba(0, 0, 0, 0.18);
}
/* Active */
.spec-cta:active {
transform: translateY(0);
}
/* Barre style "carte" */
.searchbar {
background: #fff;
border-radius: 12px;
display: flex;
align-items: center;
gap: 14px;
padding: 10px 14px;
max-width: 860px;
margin: 0 auto;
box-shadow: 0 12px 30px rgba(0, 0, 0, 0.1);
border: 1px solid rgba(0, 0, 0, 0.04);
}
.search-icon {
margin-left: 6px;
opacity: 0.7;
}
.input-wrap {
position: relative;
flex: 1 1 0;
min-width: 0; /* IMPORTANT: évite débordements */
}
/* Champs “plats” (comme sur ta capture) */
.searchbar input {
width: 100%;
border: none;
outline: none;
background: transparent;
font-size: 14px;
padding: 10px 8px;
color: #111;
}
.searchbar input::placeholder {
color: rgba(0, 0, 0, 0.45);
}
/* séparateur */
.divider {
width: 1px;
height: 34px;
background: rgba(0, 0, 0, 0.1);
}
/* Bouton intégré à droite */
.btn-find--inbar {
margin: 0;
border: none;
background: var(--red);
color: #fff;
font-weight: 500;
font-size: 14px;
padding: 10px 14px;
border-radius: 999px;
cursor: pointer;
box-shadow: 0 10px 24px rgba(0, 0, 0, 0.16);
white-space: nowrap;
}
.btn-find--inbar:hover {
transform: translateY(-1px);
}
.btn-find--inbar:active {
transform: translateY(0);
}
/* Suggestions dropdown */
.suggest {
position: absolute;
top: 100%;
left: 0;
right: 0;
background: #fff;
list-style: none;
margin: 6px 0 0;
padding: 6px 0;
border-radius: 12px;
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.14);
max-height: 260px;
overflow: auto;
z-index: 20;
}
.suggest li {
padding: 10px 12px;
cursor: pointer;
font-size: 15px;
}
.suggest li:hover {
background: #f6f6f6;
}
/* =========================================================
8) Mobile
========================================================= */
@media (max-width: 720px) {
.hp-btn {
min-width: 112px;
padding: 9px 12px;
}
.hp-row2 {
padding-left: 4px;
}
.ha-img {
height: 90px;
}
.hp-articles {
grid-template-columns: 1fr;
}
.hp-steps__title {
font-size: 17px;
}
.hp-steps__sub {
font-size: 12.5px;
}
.hp-steps__item {
font-size: 13.5px;
}
.spec-cta-wrap {
grid-template-columns: 1fr;
row-gap: 12px;
text-align: center;
justify-items: center;
padding: 22px 18px;
}
.spec-cta-wrap h3 {
font-size: 26px;
white-space: normal;
}
.spec-cta-wrap p {
font-size: 15px;
max-width: 34ch;
}
.spec-cta {
width: 50%;
max-width: 340px;
}
.hero {
background: #f3f4f8;
}
}
+17
View File
@@ -0,0 +1,17 @@
<?php
// NE PAS versionner ce fichier (ajoute-le au .gitignore)
return [
'smtp' => [
'host' => 'smtp-relay.brevo.com',
'port' => 587,
'secure' => 'tls', // 'tls' (STARTTLS) ou 'ssl' si 465
'auth' => true,
'username' => getenv('BREVO_USERNAME') ?: '', // boîte SMTP dédiée
'password' => getenv('BREVO_PASSWORD') ?: '', // ⚠️ remplace par le vrai mdp
// 'password' => getenv('SMTP_PASS'), // (option) via variable d'env
],
'from' => ['no-reply@uslugi.fr', 'USLUGI.FR'], // From par défaut
'reply_to' => ['contact@uslugi.fr', 'USLUGI.FR'], // Reply-To global
'bounce' => 'bounce@uslugi.fr', // Return-Path
'suppress_auto' => true, // Ajoute des entêtes pour réduire OOO/auto-replies
];
+80
View File
@@ -0,0 +1,80 @@
<?php
use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\Exception;
require_once __DIR__.'/../vendor/autoload.php';
$MAILCFG = require __DIR__.'/mail_config.php';
/**
* Envoie un mail HTML via SMTP Gandi.
*
* @param string $to Adresse destinataire
* @param string $subject Sujet (UTF-8)
* @param string $html Corps HTML
* @param string|null $text Alternative texte (sinon strip_tags)
* @param array $opts ['to_name','cc'=>[],'bcc'=>[],'attachments'=>[],'headers'=>[],'from'=>[email,name],'reply_to'=>[email,name]]
* @return array [bool success, ?string error]
*/
function uslugi_mail(string $to, string $subject, string $html, ?string $text = null, array $opts = []): array {
global $MAILCFG;
$mail = new PHPMailer(true);
try {
// SMTP
$mail->isSMTP();
$mail->Host = $MAILCFG['smtp']['host'];
$mail->Port = $MAILCFG['smtp']['port'];
$mail->SMTPSecure = $MAILCFG['smtp']['secure'];
$mail->SMTPAuth = $MAILCFG['smtp']['auth'];
$mail->Username = $MAILCFG['smtp']['username'];
$mail->Password = $MAILCFG['smtp']['password'];
// Base
$mail->CharSet = 'UTF-8';
$mail->Encoding = 'base64';
$mail->XMailer = ''; // pas d'X-Mailer bavard
// Expéditeur / Reply-To
$from = $opts['from'] ?? $MAILCFG['from'];
$rpto = $opts['reply_to'] ?? $MAILCFG['reply_to'];
$mail->setFrom($from[0], $from[1] ?? '');
if (!empty($rpto[0])) $mail->addReplyTo($rpto[0], $rpto[1] ?? '');
// Return-Path (enveloppe) → SPF/DMARC alignés
if (!empty($MAILCFG['bounce'])) $mail->Sender = $MAILCFG['bounce'];
// Destinataires
$mail->addAddress($to, $opts['to_name'] ?? '');
foreach (($opts['cc'] ?? []) as $cc) { $mail->addCC($cc); }
foreach (($opts['bcc'] ?? []) as $bcc) { $mail->addBCC($bcc); }
// Pièces jointes
foreach (($opts['attachments'] ?? []) as $k => $v) {
if (is_int($k)) $mail->addAttachment($v);
else $mail->addAttachment($k, $v);
}
// Entêtes optionnelles
foreach (($opts['headers'] ?? []) as $k => $v) {
$mail->addCustomHeader($k, $v);
}
if (!empty($MAILCFG['suppress_auto'])) {
$mail->addCustomHeader('Auto-Submitted', 'auto-generated');
$mail->addCustomHeader('X-Auto-Response-Suppress', 'All');
}
// Contenu
$mail->isHTML(true);
$mail->Subject = $subject; // PHPMailer encode proprement
$mail->Body = $html;
$mail->AltBody = $text ?? strip_tags($html);
$mail->send();
return [true, null];
} catch (Exception $e) {
error_log('PHPMailer error: '.$mail->ErrorInfo);
return [false, $mail->ErrorInfo];
}
}
+4
View File
@@ -0,0 +1,4 @@
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;600;700;800&display=swap" rel="stylesheet">
<?php //Liste des langues disponibles pour traduction de site ?>
<a href="/public/change_lang.php?lang=ru" class="lang-btn <?= ($_SESSION['lang'] ?? 'ru') === 'ru' ? 'active' : '' ?>">РУ</a>
<a href="/public/change_lang.php?lang=fr" class="lang-btn <?= ($_SESSION['lang'] ?? 'ru') === 'fr' ? 'active' : '' ?>">FR</a>
+937
View File
@@ -0,0 +1,937 @@
/* ==========================================================================
mobile.css — Overrides globaux mobile (≤ 956px)
Charge CE FICHIER EN DERNIER
========================================================================== */
@media (max-width: 956px) {
/* ---------- Garde-fous généraux ---------- */
html,
body {
width: 100%;
max-width: 100%;
overflow-x: hidden; /* coupe tout débordement horizontal */
-webkit-text-size-adjust: 100%;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
img,
video,
iframe {
max-width: 100%;
height: auto;
display: block;
}
body {
min-height: 100svh; /* hauteur écran fiable iOS/Android */
padding-left: env(safe-area-inset-left, 0);
padding-right: env(safe-area-inset-right, 0);
}
.row {
margin-left: 0 !important;
margin-right: 0 !important;
}
.container {
width: 100% !important;
max-width: 100% !important;
padding-left: 16px !important;
padding-right: 16px !important; /* ✅ symétrie gauche/droite */
}
/* ==== VISUEL COMMUN POUR TOUS LES MENUS ==== */
.menu-panel {
background: #fff;
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 12px;
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.08);
min-width: 230px;
padding: 8px 0;
}
/* Items (li custom OU .dropdown-item Bootstrap) */
.menu-panel > li,
.menu-panel .dropdown-item {
padding: 12px 18px;
font-size: 16px;
line-height: 1.25;
white-space: nowrap;
}
.menu-panel > li + li,
.menu-panel .dropdown-item + .dropdown-item {
border-top: 1px solid #f1f1f1;
}
.menu-panel a {
color: inherit;
text-decoration: none;
display: block;
}
/* ==== MENU “BURGER” CUSTOM ==== */
.menu-wrap {
position: relative;
}
.dropdown-menu-custom {
position: absolute;
top: calc(100% + 8px);
right: 0; /* ← fini le -180px */
list-style: none;
margin: 0;
display: none; /* fermé par défaut */
z-index: 1100;
cursor: pointer;
}
.dropdown-menu-custom.is-open {
display: block;
}
/* Sous-menu admin (inchangé) */
.has-submenu .submenu-toggle {
width: 100%;
text-align: left;
background: none;
border: 0;
padding: 0.55rem 1rem;
font-weight: 400;
display: flex;
align-items: center;
justify-content: space-between;
cursor: pointer;
color: #000;
}
.submenu {
list-style: none;
margin: 0;
padding: 0 0 0.5rem 0;
}
.submenu > li {
padding: 0.4rem 1rem 0.4rem 1.5rem;
cursor: pointer;
}
.submenu > li:hover {
background: #f8f9fa;
}
.submenu[hidden] {
display: none;
}
/* ==== MENU “?” BOOTSTRAP ==== */
.dropdown-menu.menu-panel {
/* on garde le comportement Bootstrap */
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 12px;
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.08);
min-width: 230px;
padding: 8px 0;
cursor: pointer;
}
/* Sécurité : que rien ne coupe les menus */
.site-header,
.header-actions,
.menu-wrap,
.dropdown {
overflow: visible;
background: #f1f1f1;
}
.brand {
display: flex;
align-items: center;
gap: 10px;
width: auto;
max-width: 100%;
}
/* ---------- HERO ---------- */
.hero-title {
font-size: 40px;
}
.results-title,
.results-meta,
.result-count {
margin-top: 12px;
color: #38378b;
text-align: center;
}
/* Chips : rangées centrées et espacées de façon homogène */
.hero .row {
display: flex;
flex-wrap: wrap;
justify-content: center; /* ✅ centrage horizontal des chips */
gap: 8px 10px; /* espace cohérent */
}
.chip {
font-size: 12px;
width: 110px; /* tu peux passer en auto + padding si tu veux */
}
/* ---------- Steps (mobile) ---------- */
.step-label {
background: var(--c-white);
color: var(--c-dark);
font-weight: 700;
font-size: clamp(13px, 2.6vw, 26px);
padding: 14px 26px 14px 38px; /* pour chevauchement côté gauche */
border-radius: 999px;
white-space: nowrap;
box-shadow: var(--c-shadow);
margin-left: -30px;
z-index: 0;
}
/* tailles différentes pour chaque étape */
.step:nth-child(1) .step-badge {
width: 70px;
height: 70px;
font-size: clamp(41px, 5vw, 56px);
}
.step:nth-child(2) .step-badge {
width: 90px;
height: 90px;
font-size: clamp(48px, 5.5vw, 64px);
}
.step:nth-child(3) .step-badge {
width: 110px;
height: 110px;
font-size: clamp(52px, 6vw, 72px);
}
.step--left {
justify-self: start;
transform: translateX(0);
}
.step--right {
justify-self: end;
transform: translateX(0);
}
/* ❗️Supprime le décalage latéral au milieu (source d’asymétrie visuelle) */
.step--center {
justify-self: center;
transform: translateY(12px);
padding-left: 100px; /* ✅ avant : 100px → crée l’effet “plus d’espace à gauche” */
}
/* ---------- Composants transverses ---------- */
main,
.main,
.main1,
.main2 {
display: block !important;
align-items: initial !important;
justify-content: initial !important;
background: transparent !important;
margin: 0 !important;
padding: 0 !important;
width: 100% !important;
max-width: none !important;
}
.text-end {
text-align: initial !important;
}
.text-end .btn {
width: 100%;
}
.card,
.card * {
overflow-wrap: anywhere;
word-break: break-word;
}
.card,
.card.shadow-sm.p-3,
.card.shadow-sm.p-3.avis-card {
min-width: 0 !important;
width: 100% !important;
max-width: 100% !important;
overflow: hidden;
border-radius: 16px;
box-sizing: border-box;
}
/*
.profile-grid > * { min-width: 0; min-height: 0; }
.big-photo { width: 100% !important; max-height: 400px; overflow: hidden; border-radius: 16px; }
.big-photo img { width: 100% !important; height: auto !important; object-fit: cover; display: block; }
.info-scroll, .vlist { max-height: none !important; overflow: visible !important; padding-right: 0 !important; }
.vlist { display: grid !important; grid-template-columns: 1fr !important; gap: 12px !important; }
.vlist .vitem { width: 100%; border-radius: 16px; overflow: hidden; background: #000; aspect-ratio: 21 / 9; }
.vlist .vitem img { width: 100%; height: 100%; object-fit: cover; display: block; }
.bottom-grid { display: grid !important; grid-template-columns: 1fr !important; gap: 12px !important; }
.card .d-flex.align-items-center { flex-direction: column !important; align-items: flex-start !important; gap: 6px; }
.avis-card .d-flex.justify-content-between { flex-direction: column !important; align-items: flex-start !important; gap: 4px; }
.avis-card .meta + div { overflow: hidden; display: block; line-height: 1.5; max-height: calc(1.5em * 3); }
.big-photo, section.panel, aside.panel, .profile-grid, .bottom-grid, .card {
position: static !important; float: none !important; transform: none !important;
z-index: auto !important; margin-left: 0 !important; margin-right: 0 !important; clear: both !important;
}*/
/* ---------- PROFIL ---------- */
/* ---------- PROFIL (mobile) ---------- */
.profile-grid {
display: grid !important;
grid-template-columns: 1fr !important; /* ← empile en 1 colonne */
grid-auto-rows: auto !important;
gap: 16px !important;
}
.profile-grid.profile-top {
height: auto !important;
} /* annule clamp() s'il existe */
.profile-grid > * {
min-width: 0;
min-height: 0;
}
/* annule les hauteurs "90%" du desktop */
.profile-grid .panel {
height: auto !important;
}
.big-photo {
width: 100% !important;
height: auto !important; /* ← annule le 90% */
max-width: 100% !important;
max-height: none !important; /* évite la “fenêtre” grise */
overflow: hidden;
border-radius: 16px;
}
.big-photo img {
width: 100% !important;
height: auto !important;
object-fit: cover;
display: block;
}
.info-scroll,
.vlist {
max-height: none !important;
overflow: visible !important;
padding-right: 0 !important;
}
.vlist {
display: grid !important;
grid-template-columns: 1fr !important;
gap: 12px !important;
}
.bottom-grid {
display: grid !important;
grid-template-columns: 1fr !important;
gap: 12px !important;
}
/* les cartes doivent pouvoir rétrécir */
.card {
min-width: 0 !important;
}
/* ---------- LISTES / RÉSULTATS ---------- */
.pro-list {
gap: 16px !important;
padding: 12px 0 24px !important;
}
.pro-card {
display: grid !important;
grid-template-columns: 1fr !important;
grid-template-areas: "photo" "info" "video" "cta" !important;
gap: 12px !important;
padding: 16px !important;
}
.pro-photo img {
width: 100% !important;
aspect-ratio: 3 / 4;
object-fit: cover;
border-radius: 16px;
}
.pro-info {
padding: 0 !important;
}
.pro-video-col {
width: 100% !important;
display: block !important;
}
.pro-video,
.pro-video.placeholder {
width: 100%;
aspect-ratio: 16 / 9;
max-height: none;
border-radius: 16px;
overflow: hidden;
background: #000;
}
.pro-cta-col {
display: block !important;
}
.pro-cta {
display: block;
width: 100%;
text-align: center;
}
/* ---------- FAVORIS ---------- */
.praticien-card {
display: block !important;
}
.praticien-card > div:first-child {
margin-bottom: 12px;
}
.praticien-card .card-img-top {
display: block;
width: 100%;
height: auto;
border-radius: 12px;
}
.praticien-card > .card-body.p-0 {
margin-bottom: 12px;
min-width: 0;
}
.praticien-card .d-flex {
display: flex;
flex-direction: column;
gap: 10px;
}
.praticien-card .d-flex .btn {
width: 100%;
justify-content: center;
}
/* ---------- Suggestions (autocomplete) ---------- */
.suggest,
#suggestions {
position: absolute !important;
top: calc(100% + 8px);
left: 0;
right: 0; /* => même largeur que l'input */
z-index: 2000;
background: #fff;
border: 1px solid #e9ecef;
border-radius: 14px;
padding: 8px 0;
margin: 0;
max-height: 50vh; /* scrollable si long */
overflow-y: auto;
-webkit-overflow-scrolling: touch;
box-shadow: 0 10px 24px rgba(0, 0, 0, 0.12);
width: auto !important; /* annule width:100% si besoin */
}
.suggest li,
#suggestions li {
list-style: none;
padding: 12px 16px;
font-size: clamp(14px, 3.8vw, 18px);
line-height: 1.25;
cursor: pointer;
}
.suggest li + li,
#suggestions li + li {
border-top: 1px solid #f3f4f6;
}
.suggest li:hover,
#suggestions li:hover {
background: #f8f9fa;
}
/* Largeurs figées à neutraliser en mobile */
.pto-list {
width: 100% !important;
}
/* Footer */
.site-footer {
font-size: 12px;
}
}
/*---------Transformation table en cards-----------------------*
***************************************************************/
/* ===== TABLE -> CARTES (mobile) ===== */
@media (max-width: 768px) {
/* ✅ N’affiche pas THEAD tant que data-label n’est pas prêt */
#table_rdv thead {
display: table-header-group;
}
.mc-ready #table_rdv thead {
display: none;
}
/* neutralise centrage hérité */
#table_rdv,
#table_rdv tr,
#table_rdv td,
#table_rdv td * {
text-align: left !important;
}
/* wrapper bootstrap */
.table-responsive {
overflow: visible;
}
#table_rdv {
width: 100%;
border: 0;
background: transparent;
border-collapse: separate;
border-spacing: 0;
}
#table_rdv tbody {
display: block;
}
/* chaque ligne -> carte */
#table_rdv tr {
display: block;
margin: 12px 0;
background: #fff;
border: 1px solid rgba(0, 0, 0, 0.06);
border-radius: 14px;
box-shadow: 0 6px 14px rgba(0, 0, 0, 0.08);
overflow: hidden;
}
#table_rdv tr::before {
content: "";
display: block;
height: 3px;
background: #38378b;
opacity: 0.9;
}
/* cellules empilées */
#table_rdv td {
display: block;
padding: 10px 14px !important;
background: transparent !important;
border: none !important;
box-sizing: border-box;
}
#table_rdv td + td {
border-top: 1px dashed rgba(0, 0, 0, 0.08) !important;
}
/* libellés auto (et on masque s’ils sont vides) */
#table_rdv td::before {
content: attr(data-label);
display: inline-block;
min-width: 120px;
margin-right: 8px;
font-weight: 600;
color: #666;
}
#table_rdv td::before:empty {
display: none;
}
/* hiérarchie visuelle (adapte si ordre diffère) */
#table_rdv td:nth-child(1) {
font-size: 16px;
font-weight: 700;
letter-spacing: 0.2px;
}
#table_rdv td:nth-child(2) {
color: #6b7280;
font-weight: 600;
padding-top: 6px;
}
/* select pleine largeur + iOS align */
#table_rdv select.form-select {
display: block;
width: 100%;
height: 40px;
font-size: 15px;
border-radius: 10px;
margin: 6px 0;
text-align: left !important;
text-align-last: left !important;
-webkit-text-size-adjust: 100%;
}
/* actions */
#table_rdv .row-actions {
display: flex;
gap: 8px;
flex-wrap: wrap;
margin-top: 6px;
justify-content: flex-start !important;
align-items: stretch !important;
}
#table_rdv .row-actions .btn {
flex: 1 1 calc(50% - 4px);
margin: 0 !important;
}
/* neutralise utilitaires Bootstrap qui centrent */
#table_rdv
:is(.text-center, .text-sm-center, .text-md-center, .text-lg-center) {
text-align: left !important;
}
#table_rdv :is(.mx-auto, .ms-auto, .me-auto) {
margin-left: 0 !important;
margin-right: 0 !important;
}
#table_rdv
:is(
.justify-content-center,
.justify-content-sm-center,
.justify-content-md-center,
.justify-content-lg-center
) {
justify-content: flex-start !important;
}
#table_rdv :is(.align-items-center) {
align-items: flex-start !important;
}
/* retire les zébrures en mode carte */
#table_rdv.table-striped > tbody > tr:nth-of-type(odd) > * {
background: transparent !important;
}
}
/******************************FIN TABLES MOBILE********************************************/
@media (max-width: 576px) {
/* Barre de recherche */
.searchbar {
background: #fff;
border-radius: 12px;
display: flex;
align-items: center;
gap: 14px;
padding: 10px 14px;
max-width: 760px;
margin: 0 auto;
box-shadow: 0 8px 22px rgba(0, 0, 0, 0.18);
}
.search-icon {
margin-left: 6px;
}
.input-wrap {
position: relative;
flex: 1.3;
}
.searchbar input {
width: 100%;
border: none;
outline: none;
font-size: clamp(10px, 3vw, 16px);
padding: 10px 4px;
}
.divider {
width: 1px;
height: 28px;
background: #e6e6e6;
}
}
.footer-top-links {
color: #38378b;
font-weight: 600;
}
.footer-top-links a {
color: inherit; /* hérite du bleu foncé */
}
/* Fallback si le navigateur ne supporte pas gap */
@supports not (gap: 1rem) {
.d-flex.gap-2 > * {
margin-right: 0.5rem;
}
.d-flex.gap-3 > * {
margin-right: 1rem;
}
.d-flex.gap-4 > * {
margin-right: 1.5rem;
}
.d-flex[class*="gap-"] > *:last-child {
margin-right: 0;
}
}
.scrollable {
-webkit-overflow-scrolling: touch;
}
.fullheight {
min-height: 100vh;
} /* défaut */
@supports (height: 100dvh) {
.fullheight {
min-height: 100dvh;
} /* iOS 16+ */
}
@supports not (height: 100dvh) {
.fullheight {
min-height: -webkit-fill-available;
} /* iOS anciens */
}
/*---------Transformation table en cards-----------------------*
***************************************************************/
/* ===== TABLE -> CARTES (mobile) ===== */
@media (max-width: 768px) {
/* ✅ N’affiche pas THEAD tant que data-label n’est pas prêt */
#table_rdv1 thead {
display: table-header-group;
}
.mc-ready #table_rdv1 thead {
display: none;
}
/* neutralise centrage hérité */
#table_rdv1,
#table_rdv1 tr,
#table_rdv1 td,
#table_rdv1 td * {
text-align: left !important;
}
/* wrapper bootstrap */
.table-responsive {
overflow: visible;
}
#table_rdv1 {
width: 100%;
border: 0;
background: transparent;
border-collapse: separate;
border-spacing: 0;
}
#table_rdv1 tbody {
display: block;
}
/* chaque ligne -> carte */
#table_rdv1 tr {
display: block;
margin: 12px 0;
background: #fff;
border: 1px solid rgba(0, 0, 0, 0.06);
border-radius: 14px;
box-shadow: 0 6px 14px rgba(0, 0, 0, 0.08);
overflow: hidden;
}
#table_rdv1 tr::before {
content: "";
display: block;
height: 3px;
background: #38378b;
opacity: 0.9;
}
/* cellules empilées */
#table_rdv1 td {
display: block;
padding: 10px 14px !important;
background: transparent !important;
border: none !important;
box-sizing: border-box;
}
#table_rdv1 td + td {
border-top: 1px dashed rgba(0, 0, 0, 0.08) !important;
}
/* libellés auto (et on masque s’ils sont vides) */
#table_rdv1 td::before {
content: attr(data-label);
display: inline-block;
min-width: 120px;
margin-right: 8px;
font-weight: 600;
color: #666;
}
#table_rdv1 td::before:empty {
display: none;
}
/* hiérarchie visuelle (adapte si ordre diffère) */
#table_rdv1 td:nth-child(1) {
font-size: 16px;
font-weight: 700;
letter-spacing: 0.2px;
}
#table_rdv1 td:nth-child(2) {
color: #6b7280;
font-weight: 600;
padding-top: 6px;
}
/* select pleine largeur + iOS align */
#table_rdv1 select.form-select {
display: block;
width: 100%;
height: 40px;
font-size: 15px;
border-radius: 10px;
margin: 6px 0;
text-align: left !important;
text-align-last: left !important;
-webkit-text-size-adjust: 100%;
}
/* actions */
#table_rdv1 .row-actions {
display: flex;
gap: 8px;
flex-wrap: wrap;
margin-top: 6px;
justify-content: flex-start !important;
align-items: stretch !important;
}
#table_rdv1 .row-actions .btn {
flex: 1 1 calc(50% - 4px);
margin: 0 !important;
}
/* neutralise utilitaires Bootstrap qui centrent */
#table_rdv1
:is(.text-center, .text-sm-center, .text-md-center, .text-lg-center) {
text-align: left !important;
}
#table_rdv1 :is(.mx-auto, .ms-auto, .me-auto) {
margin-left: 0 !important;
margin-right: 0 !important;
}
#table_rdv1
:is(
.justify-content-center,
.justify-content-sm-center,
.justify-content-md-center,
.justify-content-lg-center
) {
justify-content: flex-start !important;
}
#table_rdv1 :is(.align-items-center) {
align-items: flex-start !important;
}
/* retire les zébrures en mode carte */
#table_rdv1.table-striped > tbody > tr:nth-of-type(odd) > * {
background: transparent !important;
}
}
/******************************FIN TABLES MOBILE********************************************/
/* Header fixe en haut */
.site-header--fixed {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 1030;
}
/* Safe-area iOS (notch) */
@supports (padding: max(0px)) {
.site-header--fixed {
padding-top: max(env(safe-area-inset-top), 0px);
}
}
/* Espace en haut pour ne pas cacher le contenu */
body.has-fixed-header {
padding-top: 64px; /* ajuste si besoin */
}
@supports (padding: max(0px)) {
body.has-fixed-header {
padding-top: calc(64px + max(env(safe-area-inset-top), 0px));
}
}
.pwa-install {
position: fixed;
right: 14px;
top: 100px;
bottom: auto;
transform: translateY(-50%);
z-index: 9999;
border: 0;
border-radius: 9999px;
padding: 10px 12px;
font-weight: 600;
color: #ffffff;
background: #38378b;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.16);
display: inline-flex;
align-items: center;
gap: 10px;
}
.pwa-install img {
display: block;
}
.pwa-ios-hint {
position: fixed;
inset: 0;
z-index: 10000;
background: rgba(0, 0, 0, 0.45);
display: flex;
align-items: flex-end;
justify-content: center;
padding: 16px;
}
.pwa-ios-card {
width: 100%;
max-width: 520px;
background: #fff;
border-radius: 18px;
padding: 14px 14px 12px;
box-shadow: 0 18px 40px rgba(0, 0, 0, 0.25);
}
.pwa-ios-title {
font-weight: 800;
margin-bottom: 6px;
}
.pwa-ios-text {
line-height: 1.25;
margin-bottom: 10px;
}
.pwa-ios-close {
width: 100%;
border: 0;
border-radius: 999px;
padding: 10px 12px;
background: #e03c3f;
color: #fff;
font-weight: 700;
}
@media (min-width: 992px) {
.pwa-install,
.pwa-ios-hint {
display: none !important;
}
}
/*.d-none{ display:none !important; }*/
@media (max-width: 956px) {
/* Layout page : header + main qui prend l'espace + footer en bas */
body {
min-height: 100dvh; /* mieux que svh */
display: flex;
flex-direction: column;
}
.main {
flex: 1 0 auto; /* prend l’espace vide */
}
.site-footer {
margin-top: auto; /* pousse le footer tout en bas */
}
}
+44
View File
@@ -0,0 +1,44 @@
<?php
function refreshOutlookToken(PDO $pdo, int $praticien_id): ?string {
$stmt = $pdo->prepare("SELECT refresh_token FROM outlook_tokens WHERE praticien_id = ?");
$stmt->execute([$praticien_id]);
$refresh_token = $stmt->fetchColumn();
if (!$refresh_token) return null;
$client_id = '8328e538-3cee-4731-9122-8b7fd6f461a9';
$client_secret = 'xKY8Q~dygFtWlM62--4t0RVkAlFI3f5uspwOedkL';
$token_url = 'https://login.microsoftonline.com/consumers/oauth2/v2.0/token';
$data = [
'client_id' => $client_id,
'client_secret' => $client_secret,
'refresh_token' => $refresh_token,
'grant_type' => 'refresh_token',
'scope' => 'offline_access User.Read Calendars.ReadWrite'
];
$options = [
'http' => [
'method' => 'POST',
'header' => "Content-type: application/x-www-form-urlencoded",
'content' => http_build_query($data),
],
];
$response = file_get_contents($token_url, false, stream_context_create($options));
$tokens = json_decode($response, true);
if (!isset($tokens['access_token'])) {
return null;
}
$access_token = $tokens['access_token'];
$refresh_token = $tokens['refresh_token'] ?? $refresh_token;
$expires_at = time() + $tokens['expires_in'];
$pdo->prepare("UPDATE outlook_tokens SET access_token = ?, refresh_token = ?, expires_at = ? WHERE praticien_id = ?")
->execute([$access_token, $refresh_token, $expires_at, $praticien_id]);
return $access_token;
}
+18
View File
@@ -0,0 +1,18 @@
<?php
// --------------------------------------------------
// BOOTSTRAP GLOBAL — exécuté UNE SEULE FOIS
// --------------------------------------------------
if (!defined('APP_ROOT')) {
define('APP_ROOT', realpath(__DIR__ . '/..')); // vhosts/staging.uslugi.fr
}
if (!defined('APP_HTDOCS')) {
define('APP_HTDOCS', APP_ROOT . '/htdocs');
}
// Charger env UNE FOIS
if (empty($GLOBALS['ENV_LOADED'])) {
require_once APP_ROOT . '/includes/env.php';
app_load_env(APP_ROOT . '/.env');
$GLOBALS['ENV_LOADED'] = true;
}
+55
View File
@@ -0,0 +1,55 @@
<?php
// Évite la boucle quand router.php inclut la page
if (defined('ROUTED_PRETTY')) return;
// Mapping vilain -> joli
$UGLY_TO_PRETTY = [
//racine
'/index.php' => '/',
'/result_search.php' => '/recherche',
'/detail_public.php' => '/specialistes',
'/connexion.php' => '/login',
'/choix_inscr.php' => '/choix-inscription',
'/inscription_client.php' => '/inscription-client',
'/inscription_praticien.php' => '/inscription-praticien',
'/mdp_oublie.php' => '/mot-de-passe-oublie',
'/avis_public.php' => '/avis',
//dossier client
'/public/client/informations.php' => '/profil',
'/public/client/modifier.php' => '/modifier-profil',
'/public/client/compte.php' => '/mon-compte',
'/public/client/list_praticien.php' => '/trouver-specialistes',
'/public/client/mes_favori.php' => '/mes-preferences',
'/public/client/detail_praticien.php' => '/specialiste',
'/public/client/avis.php' => '/donner-avis',
'/public/client/reserver_rdv.php' => '/reservation',
'/public/client/mes_rendez_vous.php' => '/mes-rendez-vous',
//Dossier praticien
'/public/praticien/informations.php' => '/profil-pro',
'/public/praticien/modifier.php' => '/modifier-profil-pro',
'/public/praticien/compte.php' => '/mon-compte-pro',
'/public/praticien/disponibilites.php' => '/mes-creneaux',
'/public/praticien/disponibilites_date.php' => '/mes-creneaux-date',
'/public/praticien/rendez_vous.php' => '/mes-consultations',
'/public/praticien/rendez_vous_du_jour.php' => '/consultations-du-jour',
'/public/praticien/agenda.php' => '/agenda',
'/public/praticien/synchronisation.php' => '/connecter-agenda',
'/public/praticien/resiliation.php' => '/abonnement',
'/public/praticien/factures.php' => '/mes-factures',
//Dossier messages
];
// Debug : ajouter ?_noredir=1 pour désactiver la redirection
if (!empty($_GET['_noredir'])) return;
$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) ?? '/';
if (isset($UGLY_TO_PRETTY[$path])) {
$target = $UGLY_TO_PRETTY[$path];
$qs = $_SERVER['QUERY_STRING'] ? ('?' . $_SERVER['QUERY_STRING']) : '';
header('Location: ' . $target . $qs, true, 301);
exit;
}
+6
View File
@@ -0,0 +1,6 @@
<?php
// Ne PAS versionner ce fichier
return [
'site' => '6LcltswrAAAAAOQ3NwAgUbAu75WHQ9MsiFVd7ZVe', // publique
'secret' => '6LcltswrAAAAABTicobq3NigOaO3hTFNZBieZyeO', // privée
];
+65
View File
@@ -0,0 +1,65 @@
<?php
function has_cyrillic(string $s): bool {
return (bool) preg_match('/\p{Cyrillic}/u', $s);
}
function norm_key(string $s): string {
$s = mb_strtolower(trim($s), 'UTF-8');
$s = str_replace(['ё'], ['е'], $s); // homogénéiser RU
$s = preg_replace('/[\'"’\-\s]+/u', '', $s); // enlève tirets, espaces, quotes
return $s;
}
// Translittération simple RU -> latin (fallback si intl n’est pas dispo)
function translit_ru_to_lat(string $s): string {
if (class_exists('Transliterator')) {
// "Russian-Latin/BGN" produit Parizh (Париж) -> "parizh"
$t = \Transliterator::create('Russian-Latin/BGN; Lower();');
if ($t) return $t->transliterate($s);
}
$map = [
'а'=>'a','б'=>'b','в'=>'v','г'=>'g','д'=>'d','е'=>'e','ё'=>'e','ж'=>'zh','з'=>'z','и'=>'i','й'=>'i','к'=>'k',
'л'=>'l','м'=>'m','н'=>'n','о'=>'o','п'=>'p','р'=>'r','с'=>'s','т'=>'t','у'=>'u','ф'=>'f','х'=>'kh','ц'=>'ts',
'ч'=>'ch','ш'=>'sh','щ'=>'shch','ъ'=>'','ы'=>'y','ь'=>'','э'=>'e','ю'=>'yu','я'=>'ya',
];
$s = mb_strtolower($s, 'UTF-8');
return strtr($s, $map);
}
// Retourne une liste de candidats latins (pour LIKE) à partir d’une saisie q
function city_query_candidates(string $q): array {
$q = trim($q);
if ($q === '') return [];
$cands = [];
$cands[] = mb_strtolower($q, 'UTF-8'); // saisie brute (si déjà latine)
// charger alias RU -> latin
$aliases = @include __DIR__ . '/city_aliases_ru.php';
if (!is_array($aliases)) $aliases = [];
if (has_cyrillic($q)) {
// 1) correspondance par préfixe dans les alias (ПАР → Paris)
$key = norm_key($q);
foreach ($aliases as $ru => $lat) {
if (strpos(norm_key($ru), $key) === 0) {
$cands[] = $lat;
}
}
// 2) translittération simple (Париж -> parizh)
$cands[] = translit_ru_to_lat($q);
}
// Normalisation légère des candidats (minuscules, accents retirés si possible)
$res = [];
foreach (array_unique($cands) as $c) {
$lc = mb_strtolower($c, 'UTF-8');
// enlever accents (si intl transliterator dispo)
if (class_exists('Transliterator')) {
$t = \Transliterator::create('Any-Latin; Latin-ASCII;');
if ($t) $lc = $t->transliterate($lc);
}
$res[] = $lc;
}
return array_values(array_unique($res));
}
+916
View File
@@ -0,0 +1,916 @@
/* ==========================================================================
site_style.css — Refactor v2 (mobile‑first, compatible Bootstrap)
- Tokens centralisés (couleurs, rayons, ombres, largeurs)
- Nettoyage des imbrications, corrections de sélecteurs, fusion des doublons
- Breakpoints cohérents, priorités claires
========================================================================== */
/* 1) Design tokens */
:root {
--color-red: #e03c3f;
--color-white: #ffffff;
--color-blue: #38378b;
--color-mint: #06ffa8;
--color-hero: #8ca9ff;
--color-header: #f1f1f1;
--radius: 22px;
--radius-lg: 24px;
--radius-pill: 999px;
--shadow-card: 0 12px 40px rgba(0, 0, 0, 0.08);
--shadow-pop: 0 6px 15px rgba(0, 0, 0, 0.15);
--container: 1200px;
}
/* 2) Base & layout */
html,
body {
height: 100%;
}
body {
margin: 0;
font-family:
"Montserrat",
system-ui,
-apple-system,
Segoe UI,
Roboto,
Arial,
sans-serif;
min-height: 100vh;
display: flex;
flex-direction: column;
}
.main {
flex: 1;
background: var(--color-hero);
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
}
/* Compat anciens sélecteurs 'main1' / 'main2' (existaient comme balises) */
.main1 {
flex: 1;
background: var(--color-hero);
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
}
.main2 {
flex: 1;
background: var(--color-white);
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
}
/* HERO */
.hero {
padding: 40px 0 56px;
/*background: var(--color-hero);*/
}
.hero .container {
max-width: 1100px;
margin: 0 auto;
padding: 0 18px;
}
.hero-title {
color: #fff;
text-align: center;
font-size: 48px;
line-height: 1.16;
font-weight: 800;
margin: 10px 0 26px;
}
.container {
width: min(var(--container), 100% - 36px);
margin-inline: auto;
padding-inline: 18px;
}
.pro-name {
margin: 0 0 8px;
font-size: 26px;
font-weight: 800;
}
/* 3) Header & footer */
.site-header {
background: var(--color-header);
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
}
.site-header .container {
display: flex;
align-items: center;
justify-content: space-between;
padding-block: 10px;
}
.brand {
font-weight: 700;
letter-spacing: 0.5px;
}
.header-actions {
display: flex;
align-items: center;
gap: 10px;
}
footer {
background: var(--color-header);
border-top: 1px solid rgba(0, 0, 0, 0.06);
padding: 22px 20px;
}
footer p {
margin: 0;
text-align: center;
color: #222;
font-size: 13px;
}
/* Barre style "carte" */
.searchbar {
background: #fff;
border-radius: 12px;
display: flex;
align-items: center;
gap: 14px;
padding: 10px 14px;
max-width: 860px;
margin: 0 auto;
box-shadow: 0 12px 30px rgba(0, 0, 0, 0.1);
border: 1px solid rgba(0, 0, 0, 0.04);
}
.search-icon {
margin-left: 6px;
opacity: 0.7;
}
.input-wrap {
position: relative;
flex: 1 1 0;
min-width: 0; /* IMPORTANT: évite débordements */
}
/* Champs “plats” (comme sur ta capture) */
.searchbar input {
width: 100%;
border: none;
outline: none;
background: transparent;
font-size: 14px;
padding: 10px 8px;
color: #111;
}
.searchbar input::placeholder {
color: rgba(0, 0, 0, 0.45);
}
/* séparateur */
.divider {
width: 1px;
height: 34px;
background: rgba(0, 0, 0, 0.1);
}
/* Bouton intégré à droite */
.btn-find--inbar {
margin: 0;
border: none;
background: var(--red);
color: #fff;
font-weight: 500;
font-size: 14px;
padding: 10px 14px;
border-radius: 999px;
cursor: pointer;
box-shadow: 0 10px 24px rgba(0, 0, 0, 0.16);
white-space: nowrap;
}
/* 4) Boutons (override Bootstrap) */
.btn-primary {
background-color: var(--color-red) !important;
border-color: var(--color-red) !important;
}
.btn-primary:hover {
filter: brightness(1.05);
}
.btn-primary a {
color: #fff;
} /* corrige l'ancien '.btn-primary :a' */
/* 5) Sélecteur de langue & menu utilisateur */
.lang-switch {
display: none;
gap: 6px;
background: #fff;
padding: 4px;
border-radius: 20px;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08);
}
.lang-btn {
display: inline-block;
padding: 4px 10px;
font-size: 13px;
font-weight: 700;
color: var(--color-blue);
border-radius: 14px;
transition: all 0.2s;
}
.lang-btn:hover {
background: rgba(56, 55, 139, 0.08);
}
.lang-btn.active {
background: var(--color-blue);
color: #fff;
}
/* 6) Page Détails praticien */
.profile-card {
background: #fff;
border-radius: var(--radius-lg);
box-shadow: var(--shadow-card);
padding: 28px;
}
/* grille bas de page (fusion + align fix) */
.bottom-grid {
display: grid;
gap: 18px;
grid-template-columns: 0.8fr 1.6fr auto;
align-items: flex-start;
}
.bottom-grid .text-end {
display: flex;
align-items: flex-start;
justify-content: flex-end;
}
/* Rating (fusion doublons) */
.rating {
position: relative;
display: inline-block;
font-size: 20px;
letter-spacing: 2px;
line-height: 1;
}
.rating .stars-base {
color: #e3e5ea;
}
.rating .stars-fill {
color: #ffc107;
position: absolute;
top: 0;
left: 0;
overflow: hidden;
white-space: nowrap;
}
.avis-card .meta {
font-size: 0.9rem;
color: #667085;
}
/* 8) Champs communs + force MDP (fusion doublons) */
.field-wrapper {
position: relative;
}
.toggle-password {
position: absolute;
right: 10px;
top: 50%;
transform: translateY(-50%);
cursor: pointer;
font-size: 18px;
}
.strength-bar {
display: flex;
height: 8px;
border-radius: 5px;
overflow: hidden;
margin-bottom: 5px;
}
.strength-fill {
flex: 1;
background: #e0e0e0;
transition: background-color 0.3s;
}
/* 9) Listes & cartes (client) */
.card {
min-width: 380px;
border-radius: 15px;
}
.card h4 {
margin-bottom: 15px;
}
.btn-agenda {
background-color: #4285f4;
color: #fff;
border: none;
border-radius: 5px;
padding: 10px 20px;
font-size: 16px;
}
.btn-agenda:hover {
background-color: #2a69d1;
}
.card_sync {
margin-bottom: 30px;
border-radius: 10px;
box-shadow: 0 6px 15px rgba(0, 0, 0, 0.05);
}
.praticien-card {
display: flex;
align-items: center;
gap: 20px;
}
.card-img-top {
max-width: 150px;
aspect-ratio: 3/4;
object-fit: cover;
border-radius: 8px;
}
.card-body {
flex: 1;
}
.favori_lien {
cursor: pointer;
color: #055a92;
transition:
color 0.3s,
transform 0.2s;
}
.favori_lien:hover {
color: #d21427;
}
.favori_lien.ajoute {
color: #d21427;
transform: scale(1.2);
}
.card-img-top_liste {
width: 100px;
height: 100px;
object-fit: cover;
border-radius: 50%;
margin-right: 15px;
}
/* 10) Suggestions adresse */
.input-wrapper {
position: relative;
}
#suggestions {
position: absolute;
background: #f1f1f1;
border: 1px solid #ccc;
width: 100%;
max-height: 150px;
overflow-y: auto;
list-style: none;
padding: 0;
margin: 0;
z-index: 1000;
}
#suggestions li {
padding: 10px;
cursor: pointer;
}
#suggestions li:hover {
background: #f1f1f1;
}
/* 11) Partie avec scroll pour detail praticien pour scroller videos */
.profile-grid.profile-top {
/* hauteur comprise entre 420px et 780px, ~70% de la hauteur viewport */
height: clamp(420px, 70vh, 780px);
align-items: stretch; /* important pour que les colonnes prennent 100% de la hauteur */
}
/* Permettre l’overflow interne (déjà presque bon chez toi) */
.profile-grid > * {
min-height: 0;
min-width: 0;
}
.profile-grid {
display: grid;
gap: 24px;
grid-template-columns: 1.15fr 1fr 0.9fr;
}
.profile-grid .panel {
display: flex;
flex-direction: column;
min-height: 0; /* crucial */
height: 90%; /* ← donner une hauteur de référence */
}
/* La grande photo remplit la hauteur disponible */
.big-photo img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.big-photo {
height: 90%;
max-width: 350px;
border-radius: 22px;
overflow: hidden;
background: #f1f1f1;
}
/* contenu scrollable dans la colonne infos */
.info-scroll {
flex: 1 1 auto;
min-height: 0;
overflow: auto; /* scroll si ça dépasse */
padding-right: 4px; /* petit espace pour la scrollbar */
}
/* liste de vidéos scrollable (le <h5> reste fixe) */
.vlist {
flex: 1 1 auto;
min-height: 0;
overflow: auto; /* scroll si beaucoup de vignettes */
}
/* Optionnel : taille des vignettes vidéo */
.vlist .vitem {
border-radius: 18px;
overflow: hidden;
background: #000;
aspect-ratio: 21 / 9; /* 21:9 compact et propre */
margin-bottom: 12px;
}
.vlist .vitem img {
width: 100%;
height: 100%;
object-fit: cover;
}
.avatar {
width: 40px;
height: 40px;
border-radius: 50%;
background-color: #38378b; /* bleu foncé */
color: white;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
font-size: 16px;
}
a,
a:link,
a:visited {
text-decoration: none;
}
/* Personnalisation des boutons*/
/* Même hauteur / alignement que les autres boutons */
.btn-table {
vertical-align: middle; /* alignement dans la cellule */
margin: 0; /* pas de marge parasite */
}
/* Bord gris par défaut */
.btn-outline-secondary {
border: 1px solid #6c757d;
color: #6c757d;
background-color: transparent !important;
box-shadow: none !important;
}
/* Fond gris au survol */
.btn-outline-secondary:hover {
background-color: #6c757d !important;
color: #fff; /* texte blanc pour contraste */
}
/* Variante outline mais avec gris à la place du bleu */
.btn-outline-primary.custom-gray {
border-color: #6c757d; /* gris */
color: #6c757d; /* texte gris */
box-shadow: none !important;
}
.btn-outline-primary.custom-gray:hover {
background-color: #6c757d; /* fond gris au survol */
color: #fff; /* texte blanc */
border-color: #6c757d; /* garder la bordure grise */
}
/*.swal2-actions .swal2-confirm.swal2-styled {
box-shadow: 0 0 0 0.5px #6c757d;
border: none;
color: #6c757d;
background-color: transparent;
border-radius: 999px;
}
.swal2-actions .swal2-confirm.swal2-styled:hover {
background-color: #6c757d;
color: #fff;
}
.swal2-actions {
margin-bottom: 12px;
}*/
/* Dans la colonne actions du tableau */
#table_rdv td {
vertical-align: middle; /* centre verticalement la cellule */
text-align: center; /* optionnel : centre horizontalement */
}
/* Normaliser le <form> pour qu'il se comporte comme un bouton */
#table_rdv td form {
display: inline-block; /* au lieu de block */
margin: 0; /* supprime la marge par défaut */
}
/* Harmoniser <a> et <button> visuellement */
#table_rdv td a.btn,
#table_rdv td button.btn {
display: inline-block;
vertical-align: middle;
line-height: 1.2; /* même hauteur de ligne */
padding: 4px 10px; /* compacité identique */
border-radius: 999px;
box-shadow: none; /* même arrondi si tu l’utilises */
}
/* Styles de tes variantes (si tu utilises ces classes) */
.btn-table {
border: 1px solid #6c757d;
color: #6c757d;
background: transparent;
}
.btn-table:hover {
background: #6c757d;
color: #fff;
}
.btn-table_del {
border: 1px solid #dc3545;
color: #dc3545;
background: transparent;
}
.btn-table_del:hover {
background: #dc3545;
color: #fff;
}
/* === Global button polish (après Bootstrap) === */
:root {
/*Ajuste ici la rondeur et l’ombre */
--btn-radius: 999px; /*passe à 999px pour style "pill" */
--btn-shadow: 0 8px 22px rgba(0, 0, 0, 0.18);
--btn-shadow-hover: 0 8px 22px rgba(0, 0, 0, 0.18);
}
.btn:not(.btn-link) {
border-radius: var(--btn-radius);
box-shadow: var(--btn-shadow);
}
@media (prefers-reduced-motion: no-preference) {
.btn:not(.btn-link) {
transition:
box-shadow 0.2s ease,
transform 0.08s ease,
background-color 0.2s ease,
border-color 0.2s ease;
}
}
.btn:hover:not(.btn-link),
.btn:focus-visible:not(.btn-link) {
box-shadow: var(--btn-shadow-hover);
transform: translateY(0);
}
.btn:active:not(.btn-link) {
transform: translateY(0);
box-shadow: 0 8px px rgba(0, 0, 0, 0.18);
}
.btn:disabled,
.btn.disabled {
box-shadow: none;
opacity: 0.65;
}
/*Exceptions / utilitaires */
.btn.btn-link {
box-shadow: none;
border-radius: 0;
} /* les liens ne prennent pas d’ombre*/
.btn--no-shadow {
box-shadow: none !important;
} /*opt-out par bouton si besoin */
/*********************PAGE PRISE RENDEZ-VOUS**************************/
.rdv-card {
background: #fff;
border-radius: 22px;
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.08);
padding: 28px;
}
/* Titres */
.rdv-title {
margin: 0 0 6px;
text-align: center;
font-weight: 800;
color: #6c757d;
}
.rdv-subtitle {
margin: 0 0 22px;
text-align: center;
font-weight: 800;
color: #000;
font-size: 22px;
}
/* Pagination (boutons discrets “pill”) */
.pagination {
display: flex;
justify-content: center;
gap: 10px;
margin: 6px 0 18px;
}
.pagination a {
text-decoration: none;
padding: 8px 14px;
border-radius: 999px;
border: 1px solid rgba(0, 0, 0, 0.08);
background: #fff;
color: #38378b;
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.06);
transition:
transform 0.06s ease,
box-shadow 0.2s ease,
background-color 0.2s ease;
}
.pagination a:hover {
transform: translateY(0);
box-shadow: 0 10px 22px rgba(0, 0, 0, 0.1);
background: #f8f9ff;
}
/* Grille des jours (responsive) */
.grid-container {
display: grid;
gap: 16px;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
}
/******************Gestion des Slots affichage par 3 par ligne***************************/
@media (min-width: 1200px) {
.grid-container {
grid-template-columns: repeat(5, minmax(0, 1fr));
}
}
/* Colonne “jour” (carte) */
.day-column {
background: #ffffff;
border-radius: 18px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.06);
padding: 12px;
display: flex;
flex-direction: column;
gap: 10px;
min-width: 0;
}
/* En-tête jour */
.day-column h4 {
margin: 0 0 8px;
text-align: center;
font-size: 15px;
font-weight: 800;
color: #fff;
background: #38378b;
border-radius: 14px;
padding: 8px 10px;
letter-spacing: 0.2px;
}
/* Slot (heure) */
.slot {
background: #f1f1f1;
border: 1px solid #e6e9ff;
border-radius: 14px;
padding: 10px 12px;
text-align: center;
font-weight: 600;
color: #38378b;
line-height: 1.25;
box-shadow: inset 0 0 0 1px rgba(56, 55, 139, 0.05);
}
/* Bouton “Réserver” (pill) */
.slot .reserver-btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
margin-top: 8px;
padding: 8px 14px;
border-radius: 999px;
text-decoration: none;
font-weight: 700;
font-size: 14px;
color: #fff;
background: #e03c3f;
border: 1px solid transparent;
box-shadow: 0 8px 22px rgba(0, 0, 0, 0.12);
transition:
transform 0.06s ease,
box-shadow 0.2s ease,
background-color 0.2s ease;
}
.slot .reserver-btn:hover {
transform: translateY(0);
box-shadow: 0 12px 26px rgba(0, 0, 0, 0.16);
background: #cc3538; /* léger foncé */
}
.slot .reserver-btn:active {
transform: translateY(0);
}
/* États “désactivé” si besoin (au cas où) */
.slot.is-disabled {
opacity: 0.55;
filter: grayscale(20%);
}
.slot.is-disabled .reserver-btn {
background: #d4d7e5;
color: #fff;
box-shadow: none;
pointer-events: none;
}
/* Message “aucun créneau” */
.rdv-empty {
color: rgb(33, 37, 41);
text-align: center;
font-weight: 400;
font-size: 16px;
background: #fff;
border-radius: 18px;
padding: 18px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.06);
}
/* SweetAlert – harmonisation rapide avec tes boutons pills gris (optionnel) */
.swal2-actions .swal2-confirm.swal2-styled {
border-radius: 999px;
border: 1px solid rgba(0, 0, 0, 0.08);
background: #e03c3f;
color: #fff;
font-weight: 700;
box-shadow: 0 8px 22px rgba(0, 0, 0, 0.12);
}
.swal2-actions .swal2-confirm.swal2-styled:hover {
filter: brightness(1.05);
}
.swal2-actions .swal2-cancel.swal2-styled {
border-radius: 999px;
background: #fff;
color: #38378b;
border: 1px solid #e0e3eb;
}
/* Mobile: aération + swipe confortable si trop de colonnes */
@media (max-width: 640px) {
.rdv-card {
padding: 18px;
}
.grid-container {
grid-auto-flow: row;
grid-template-columns: 1fr; /* empile les jours */
}
/* Variante swipe (si tu préfères conserver les colonnes) :
dé-commente ce bloc et entoure .grid-container d’un wrapper .rdv-scroll
.rdv-scroll{ overflow-x:auto; -webkit-overflow-scrolling:touch; }
.grid-container{ grid-auto-flow: column; grid-auto-columns: 88%; }
*/
}
/**********************Paginantion pour le site ****************************/
.pager {
display: flex;
justify-content: center;
align-items: center;
gap: 10px;
margin: 22px 0;
}
.pager a {
text-decoration: none;
font-weight: 700;
}
.pager-arrow {
padding: 10px 18px;
border-radius: 999px;
border: 1px solid #e0e3eb;
background: #fff;
color: #38378b;
box-shadow: 0 6px 14px rgba(0, 0, 0, 0.06);
transition:
background 0.2s,
transform 0.1s,
opacity 0.2s;
}
.pager-arrow:hover {
background: #f8f9ff;
transform: translateY(-1px);
}
.pager-arrow.is-disabled {
opacity: 0.4;
pointer-events: none;
}
.pager-page {
width: 40px;
height: 40px;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 50%;
border: 1px solid #e0e3eb;
background: #fff;
color: #38378b;
box-shadow: 0 6px 14px rgba(0, 0, 0, 0.06);
transition:
background 0.2s,
transform 0.1s,
color 0.2s,
border-color 0.2s;
}
.pager-page:hover {
background: #f8f9ff;
transform: translateY(-1px);
}
.pager-page.is-active {
background: #e03c3f;
color: #fff;
border-color: #e03c3f;
transform: none;
}
/* =========================================
Header FIXE seulement en mobile + PWA
========================================= */
/* Desktop par défaut */
.site-header {
position: static;
}
/* Mobile navigateur */
@media (max-width: 899px) {
.site-header {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 1030;
background: #fff;
}
body {
padding-top: 64px; /* hauteur header mobile */
}
}
/* PWA (même si largeur > 899px) */
@media (display-mode: standalone) {
.site-header {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 1030;
background: #fff;
}
body {
padding-top: calc(64px + env(safe-area-inset-top));
}
}
+30
View File
@@ -0,0 +1,30 @@
<?php
// Map des clés de traduction -> valeur canonique (singulier) pour la BDD
return [
'ru' => [
'avocats' => 'адвокат',
'consultants' => 'консультант',
'vrachi' => 'врач',
'psychologues' => 'психолог',
'repetitori' => 'преподаватель',
'finance' => 'бухгалтер', // ajuste si besoin
'perevod' => 'переводчик',
'notarius' => 'нотариус',
'gid' => 'гид',
'rieltori' => 'риелтор',
],
'fr' => [
'avocats' => 'адвокат',
'consultants' => 'консультант',
'vrachi' => 'врач', // clé existante "vrachi" côté traductions
'psychologues' => 'психолог',
'repetitori' => 'преподаватель', // ou "Répetiteur" selon ta BDD
'finance' => 'бухгалтер', // ajuste si besoin
'perevod' => 'переводчик',
'notarius' => 'нотариус',
'gid' => 'гид',
'rieltori' => 'риелтор',
],
];
+26
View File
@@ -0,0 +1,26 @@
<?php
// Charge les scripts de tracking uniquement pour des pages HTML vues en GET.
// → Pas d’injection sur API/AJAX/assets/callbacks.
if (PHP_SAPI === 'cli') return;
$uri = $_SERVER['REQUEST_URI'] ?? '';
$accept = $_SERVER['HTTP_ACCEPT'] ?? '';
$method = $_SERVER['REQUEST_METHOD'] ?? 'GET';
$xrw = $_SERVER['HTTP_X_REQUESTED_WITH'] ?? '';
$isGet = strcasecmp($method, 'GET') === 0;
$wantsHtml = (stripos($accept, 'text/html') !== false) || ($accept === '');
$wantsJson = stripos($accept, 'application/json') !== false;
$isAjax = strcasecmp($xrw, 'XMLHttpRequest') === 0;
$isApi = preg_match('~/(api/|messages/api/|webhook|callback|notify)~i', $uri);
$isAsset = preg_match('~\.(css|js|map|png|jpe?g|gif|svg|webp|ico|json|xml|txt|pdf|woff2?|ttf|eot|mp4|webm)(\?|$)~i', $uri);
$isMatomo = stripos($uri, '/matomo/') !== false;
$isRdv = preg_match('~/(valider_rdv\.php|confirmer_rdv\.php|rdv/)~i', $uri);
if ($isGet && $wantsHtml && !$wantsJson && !$isAjax && !$isApi && !$isAsset && !$isMatomo && !$isRdv) {
// 100% externe (pas d’inline) → compatible CSP 'self' + domaines autorisés
echo '<script src="/js/tracking.js" defer></script>';
// Pixel noscript Matomo (ok même sans JS)
echo '<noscript><p><img src="https://staging.uslugi.fr/matomo/matomo.php?idsite=1&amp;rec=1" style="border:0" alt=""></p></noscript>';
}