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

+516
View File
@@ -0,0 +1,516 @@
<?php
declare (strict_types = 1);
require_once __DIR__ . '/../includes/bootstrap_global.php';
require_once APP_HTDOCS . '/includes/avatar_initiales.php';
require_once __DIR__ . '/includes/search_helpers.php';
$SPEC_CANON = require __DIR__ . '/includes/spec_map.php';
$canon = $SPEC_CANON[$lang] ?? [];
$H1_LABELS = [
'ru' => [
'avocats' => 'Адвокаты',
'consultants' => 'Консультанты',
'vrachi' => 'Врачи',
'psychologues' => 'Психологи',
'repetitori' => 'Преподаватели',
'finance' => 'Бухгалтеры',
'perevod' => 'Переводчики',
'notarius' => 'Нотариусы',
'gid' => 'Гиды',
'rieltori' => 'Риелторы',
'drugie' => 'Специалисты',
],
'fr' => [
'avocats' => 'Avocats',
'consultants' => 'Consultants',
'vrachi' => 'Médecins',
'psychologues' => 'Psychologues',
'repetitori' => 'Professeurs',
'finance' => 'Comptables',
'perevod' => 'Traducteurs',
'notarius' => 'Notaires',
'gid' => 'Guides',
'rieltori' => 'Agents immobiliers',
'drugie' => 'Spécialistes',
],
];
// Helper detection video
function video_thumb_info(?string $url): ?array
{
if (! $url || ! trim($url)) {
return null;
}
$u = trim($url);
// YouTube
if (preg_match('~(?:youtube\.com/(?:watch\?v=|embed/|shorts/)|youtu\.be/)([A-Za-z0-9_-]{6,})~i', $u, $m)) {
$id = $m[1];
return ['provider' => 'youtube', 'id' => $id, 'thumb' => "https://i.ytimg.com/vi/$id/hqdefault.jpg"];
}
// Dailymotion
if (preg_match('~dailymotion\.com/video/([A-Za-z0-9]+)|dai\.ly/([A-Za-z0-9]+)~i', $u, $m)) {
$id = $m[1] ?: $m[2];
return ['provider' => 'dailymotion', 'id' => $id, 'thumb' => "https://www.dailymotion.com/thumbnail/video/$id"];
}
// Direct file (mp4/webm/ogg) — no reliable thumb
$path = parse_url($u, PHP_URL_PATH) ?? '';
if (preg_match('~\.(mp4|webm|ogg)$~i', $path)) {
return ['provider' => 'file', 'id' => null, 'thumb' => null];
}
// Vimeo and others — fallback placeholder
if (strpos(strtolower($u), 'vimeo.com') !== false) {
return ['provider' => 'vimeo', 'id' => null, 'thumb' => null];
}
return ['provider' => 'other', 'id' => null, 'thumb' => null];
}
// Lecture et normalisation des filtres
$specialiteParam = trim($_GET['specialite'] ?? '');
$ville = trim($_GET['ville'] ?? '');
// inverse map: canon_value -> key (avocats, vrachi, ...)
$canonToKey = [];
foreach (($SPEC_CANON[$lang] ?? []) as $k => $v) {
$canonToKey[mb_strtolower($v, 'UTF-8')] = $k;
}
$inFrance = ($lang === 'fr') ? ' en France' : ' во Франции';
$h1Default = $translations['res_poisk'] ?? 'Результаты поиска';
$h1 = $h1Default;
// On normalise l'affichage du terme (pour l'utiliser plus tard si besoin)
$sp = trim(str_replace(["\xC2\xA0"], ' ', $specialiteParam));
$sp = preg_replace('/\s+/u', ' ', $sp);
$sp_l = mb_strtolower($sp, 'UTF-8');
if ($specialiteParam === '__OTHERS__') {
$label = $H1_LABELS[$lang]['drugie'] ?? $h1Default;
$h1 = $label . $inFrance;
} elseif ($sp !== '' && isset($canonToKey[$sp_l])) {
$key = $canonToKey[$sp_l];
$label = $H1_LABELS[$lang][$key] ?? ($translations[$key] ?? $sp);
$h1 = $label . $inFrance;
}
// sinon: on laisse $h1Default, on décidera après la requête SQL (selon résultats)
// Cas spécial : "Другие специалисты" (chip "drugie")
$onlyOthers = false;
$specialite = $specialiteParam;
if ($specialiteParam === '__OTHERS__') {
$onlyOthers = true;
$specialite = '';
}
// Pagination simple
$perPage = 12;
$page = max(1, (int) ($_GET['page'] ?? 1));
$offset = ($page - 1) * $perPage;
// Construction SQL
$where = [];
$params = [];
/*==========================BLOCK WHERE AVEC ABONNEMENT VALIDE ==========================*/
$where[] = "(date_expiration IS NOT NULL AND date_expiration >= DATE_SUB(NOW(), INTERVAL 14 DAY))";
/*=======================================================================================*/
// 🔎 Unifie: le champ "specialite" peut contenir une spécialité OU un nom/prénom
$term = trim((string) $specialite);
$term_l = mb_strtolower($term, 'UTF-8');
// 1) Cas "Другие": tout sauf les grandes catégories
if ($onlyOthers) {
$mainSpecs = array_values($canon);
if ($mainSpecs) {
$ors = [];
foreach ($mainSpecs as $i => $spc) {
$key = ":cat{$i}";
$ors[] = "LOWER(specialite) LIKE $key";
$params[$key] = mb_strtolower($spc, 'UTF-8') . '%';
}
$where[] = '(specialite IS NULL OR specialite = "" OR NOT (' . implode(' OR ', $ors) . '))';
}
}
// 2) Cas normal : recherche par spécialité / nom / prénom
elseif ($term !== '') {
$where[] =
"("
. "LOWER(specialite) LIKE :sp_like "
. "OR LOWER(nom) LIKE :nm_prefix1 "
. "OR LOWER(prenom) LIKE :nm_prefix2 "
. "OR LOWER(CONCAT(prenom,' ',nom)) LIKE :nm_like_full1 "
. "OR LOWER(CONCAT(nom,' ',prenom)) LIKE :nm_like_full2 "
. ")";
$params[':sp_like'] = '%' . $term_l . '%';
$params[':nm_prefix1'] = $term_l . '%';
$params[':nm_prefix2'] = $term_l . '%';
$params[':nm_like_full1'] = '%' . $term_l . '%';
$params[':nm_like_full2'] = '%' . $term_l . '%';
}
// Filtre ville — support latin + cyrillique (alias + translit)
if ($ville !== '') {
$villeCands = city_query_candidates($ville);
$villeCands[] = mb_strtolower($ville, 'UTF-8');
$villeCands = array_values(array_unique($villeCands));
$villeOrs = [];
foreach ($villeCands as $i => $cand) {
$key = ":ville{$i}";
$params[$key] = $cand . '%';
$villeOrs[] = "ville LIKE {$key}";
}
if ($villeOrs) {
$where[] = '(' . implode(' OR ', $villeOrs) . ')';
}
}
$whereSql = $where ? ('WHERE ' . implode(' AND ', $where)) : '';
// total
$sqlCount = "SELECT COUNT(*) FROM praticien $whereSql";
$stmt = $pdo->prepare($sqlCount);
$stmt->execute($params);
$total = (int) $stmt->fetchColumn();
$pages = max(1, (int) ceil($total / $perPage));
// data
$sql = "SELECT id, nom, prenom, specialite, ville, photo, presentation, video
FROM praticien
$whereSql";
if ($term !== '') {
$sql .= " ORDER BY
(LOWER(CONCAT(prenom,' ',nom)) = :nm_exact1
OR LOWER(CONCAT(nom,' ',prenom)) = :nm_exact2) DESC,
(LOWER(nom) = :nm_exact3 OR LOWER(prenom) = :nm_exact4) DESC,
(LOWER(nom) LIKE :nm_prefix3 OR LOWER(prenom) LIKE :nm_prefix4) DESC,
(LOWER(specialite) LIKE :sp_prefix1) DESC,
nom ASC, prenom ASC";
$params[':nm_exact1'] = $term_l;
$params[':nm_exact2'] = $term_l;
$params[':nm_exact3'] = $term_l;
$params[':nm_exact4'] = $term_l;
$params[':nm_prefix3'] = $term_l . '%';
$params[':nm_prefix4'] = $term_l . '%';
$params[':sp_prefix1'] = $term_l . '%';
} else {
$sql .= " ORDER BY updated_at DESC, id DESC";
}
$sql .= " LIMIT :offset, :limit";
$stmt = $pdo->prepare($sql);
// Lie tous les :villeN / :specialite
foreach ($params as $k => $v) {
$stmt->bindValue($k, $v, PDO::PARAM_STR);
}
$stmt->bindValue(':offset', (int) $offset, PDO::PARAM_INT);
$stmt->bindValue(':limit', (int) $perPage, PDO::PARAM_INT);
$stmt->execute();
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
// ✅ Si ce n'est pas une catégorie canon / pas __OTHERS__
// et qu'on a des résultats => on affiche "Résultats pour : X"
if ($h1 === $h1Default && $specialiteParam !== '__OTHERS__' && $sp !== '' && ! isset($canonToKey[$sp_l])) {
if (! empty($rows)) {
if ($lang === 'fr') {
$h1 = ''; //'Résultats de recherche pour : ' . $sp;
} else {
$h1 = ''; //'Результаты поиска по: ' . $sp;
}
}
}
// util
function h($s): string
{
return htmlspecialchars((string) $s, ENT_QUOTES, 'UTF-8');
}
function excerpt($text, $len = 160): string
{
$t = trim(strip_tags((string) $text));
if (mb_strlen($t, 'UTF-8') <= $len) {
return $t;
}
return mb_substr($t, 0, $len, 'UTF-8') . '…';
}
// pour liens de pagination
$qParams = [];
if ($onlyOthers) {
$qParams['specialite'] = '__OTHERS__';
} elseif ($specialite !== '') {
$qParams['specialite'] = $specialite;
}
if ($ville !== '') {
$qParams['ville'] = $ville;
}
$baseQuery = http_build_query($qParams);
?>
<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8">
<title><?php echo htmlspecialchars($translations['seo_result_search_title'] ?? '', ENT_QUOTES, 'UTF-8') ?></title>
<meta name="description" content="<?php echo htmlspecialchars($translations['seo_result_search_desc'] ?? '', ENT_QUOTES, 'UTF-8') ?>">
<meta name="keywords" content="<?php echo htmlspecialchars($translations['seo_result_search_keywords'] ?? '', ENT_QUOTES, 'UTF-8') ?>">
<meta name="author" content="Uslugi.fr">
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link href="/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="<?php echo asset('/includes/site_style.css') ?>">
<link rel="stylesheet" href="<?php echo asset('/includes/custom_style.css') ?>">
<link rel="stylesheet" href="<?php echo asset('/includes/mobil_style.css') ?>">
<?php
$trackingFile = __DIR__ . '/includes/tracking.php';
if (is_file($trackingFile)) {
include_once $trackingFile;
}
?>
</head>
<body class="layout">
<?php if (! empty($logged)):
include_header_clean(APP_HTDOCS . '/public/includes/header.php');
else: ?>
<header class="site-header">
<div class="container">
<div class="header-left">
<div class="brand">
<a href="index.php"><img src="/public/uploads/img/logo_u.png" alt="uslugi.fr" class="logo-mobile"></a>
<a href="index.php"><img src="/public/uploads/img/logo.png" alt="uslugi.fr" class="logo-desktop"></a>
</div>
<div class="lang-switch">
<?php require __DIR__ . '/includes/menu_langue.php'; ?>
</div>
</div>
<nav class="header-actions">
<a class="btn-login" href="/connexion.php">
<span class="d-sm-none"><?php echo h($translations['btn_se_connecter'] ?? '') ?></span>
<span class="d-none d-sm-inline"><?php echo h($translations['ja_spec'] ?? '') ?></span>
</a>
<div class="dropdown">
<button class="btn btn-logo rounded-circle d-inline-flex align-items-center justify-content-center question-btn"
type="button" data-bs-toggle="dropdown" data-bs-auto-close="true"
aria-expanded="false" aria-label="Помощь">?</button>
<ul class="dropdown-menu dropdown-menu-end shadow help-menu menu-panel">
<li><a href="/how-it-work.php"><?php echo h($translations['kak_rabotaet'] ?? '') ?></a></li>
<li><a href="/about.php"><?php echo h($translations['pro_nas'] ?? '') ?></a></li>
<li><a href="/faq.php"><?php echo h($translations['faq'] ?? '') ?></a></li>
<li><a href="/contact.php"><?php echo h($translations['nam_pisat'] ?? '') ?></a></li>
</ul>
</div>
</nav>
</div>
</header>
<?php endif; ?>
<div class="main1">
<div class="content">
<div class="container">
<h1 class="results-title"><?php echo h($h1) ?></h1>
</div>
<section class="cards-section">
<div class="container pro-list">
<?php if ($rows): ?>
<?php foreach ($rows as $p):
$img = ! empty($p['photo'])
? "/public/uploads/" . ltrim((string) $p['photo'], '/')
: "/public/uploads/img/user_placeholder.png";
?>
<article class="pro-card">
<div class="pro-photo">
<a href="/detail_public.php?id=<?php echo (int) $p['id'] ?>">
<img src="<?php echo h($img ?: '/public/uploads/img/user_placeholder.png') ?>" alt="Фото специалиста">
</a>
</div>
<div class="pro-info">
<h3 class="pro-name">
<a href="/detail_public.php?id=<?php echo (int) $p['id'] ?>">
<?php echo h(($p['prenom'] ?? '') . ' ' . ($p['nom'] ?? '')) ?>
</a>
</h3>
<?php if (! empty($p['specialite'])): ?>
<div class="pro-spec"><?php echo h($p['specialite']) ?></div>
<?php endif; ?>
<?php if (! empty($p['presentation'])): ?>
<p class="pro-desc"><?php echo h(excerpt($p['presentation'], 360)) ?></p>
<?php endif; ?>
<a href="/detail_public.php?id=<?php echo (int) $p['id'] ?>">
<?php echo h($translations['plus_details'] ?? 'Подробнее'); ?>
</a>
</div>
<?php if (! empty($p['video'])): ?>
<div class="pro-video-col">
<?php
$vinfo = video_thumb_info($p['video']);
$alt = htmlspecialchars($translations['video_alt'] ?? 'Vidéo', ENT_QUOTES, 'UTF-8');
$thumb = $vinfo['thumb'] ?? null;
$prov = $vinfo['provider'] ?? 'other';
$vid = $vinfo['id'] ?? null;
$raw = trim((string) ($p['video'] ?? ''));
?>
<?php if (in_array($prov, ['youtube', 'dailymotion'], true) && $vid): ?>
<a class="pro-video" href="#"
data-provider="<?php echo htmlspecialchars($prov, ENT_QUOTES, 'UTF-8') ?>"
data-id="<?php echo htmlspecialchars($vid, ENT_QUOTES, 'UTF-8') ?>"
aria-label="<?php echo $alt ?>">
<img src="<?php echo htmlspecialchars($thumb ?: '/public/uploads/img/video_placeholder.jpg', ENT_QUOTES, 'UTF-8') ?>"
alt="<?php echo $alt ?>">
</a>
<?php else: ?>
<a class="pro-video" href="<?php echo htmlspecialchars($raw, ENT_QUOTES, 'UTF-8') ?>" target="_blank" rel="noopener">
<img src="<?php echo htmlspecialchars($thumb ?: '/public/uploads/img/video_placeholder.jpg', ENT_QUOTES, 'UTF-8') ?>"
alt="<?php echo $alt ?>">
</a>
<?php endif; ?>
</div>
<?php endif; ?>
<div class="pro-cta-col">
<a class="pro-cta" href="public/client/reserver_rdv.php?praticien_id=<?php echo (int) $p['id'] ?>">
<?php echo h($translations['den'] ?? 'Выберите день') ?>
</a>
</div>
</article>
<?php endforeach; ?>
<?php else: ?>
<div class="no-results"><?php echo h($translations['probuite'] ?? 'Нет результатов') ?></div>
<?php endif; ?>
</div>
<?php if ($pages > 1): ?>
<?php
$qBase = $baseQuery ? ($baseQuery . '&') : '';
$prevPage = max(1, $page - 1);
$nextPage = min($pages, $page + 1);
?>
<nav class="pager" aria-label="Pagination">
<a class="pager-arrow <?php echo $page <= 1 ? 'is-disabled' : '' ?>"
href="<?php echo h('/result_search.php?' . $qBase . 'page=' . $prevPage) ?>"
aria-label="Page précédente">&laquo;&laquo;</a>
<?php
$start = max(1, $page - 2);
$end = min($pages, $page + 2);
for ($i = $start; $i <= $end; $i++):
$href = '/result_search.php?' . $qBase . 'page=' . $i;
?>
<a class="pager-page <?php echo $i === $page ? 'is-active' : '' ?>"
href="<?php echo h($href) ?>"><?php echo (int) $i ?></a>
<?php endfor; ?>
<a class="pager-arrow <?php echo $page >= $pages ? 'is-disabled' : '' ?>"
href="<?php echo h('/result_search.php?' . $qBase . 'page=' . $nextPage) ?>"
aria-label="Page suivante">&raquo;&raquo;</a>
</nav>
<?php endif; ?>
</section>
</div>
</div>
<!-- Modal vidéo -->
<div id="lightboxVideo" class="vmodal" aria-hidden="true">
<div class="vmodal__backdrop" data-close></div>
<div class="vmodal__dialog" role="dialog" aria-modal="true" aria-label="Vidéo">
<div class="vmodal__header">
<h5 class="vmodal__title">Vidéo</h5>
<button class="vmodal__close" type="button" title="Fermer" aria-label="Fermer" data-close>×</button>
</div>
<div class="vmodal__body">
<div class="vmodal__ratio">
<iframe id="vPlayer" src="" title="Video player"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowfullscreen
referrerpolicy="strict-origin-when-cross-origin"></iframe>
</div>
</div>
<div class="vmodal__footer">
<button class="btn btn-secondary-like" type="button" data-close>Fermer</button>
</div>
</div>
</div>
<footer class="site-footer">
<div class="container">
<p><?php echo $translations['copyr'] ?></p>
</div>
</footer>
<script>
(function(){
const modal = document.getElementById('lightboxVideo');
const frame = document.getElementById('vPlayer');
function buildSrc(provider, id){
if (!provider || !id) return '';
if (provider === 'youtube') return 'https://www.youtube-nocookie.com/embed/' + id + '?autoplay=1&rel=0&modestbranding=1';
if (provider === 'dailymotion') return 'https://www.dailymotion.com/embed/video/' + id + '?autoplay=1&ui-start-screen-info=false';
return '';
}
function openModal(src){
if (!src) return;
frame.src = src;
modal.setAttribute('aria-hidden','false');
document.body.style.overflow = 'hidden';
}
function closeModal(){
frame.src = '';
modal.setAttribute('aria-hidden','true');
document.body.style.overflow = '';
}
document.addEventListener('click', function(e){
const a = e.target.closest('a.pro-video');
if (!a) return;
const provider = a.getAttribute('data-provider');
const id = a.getAttribute('data-id');
if (provider && id){
e.preventDefault();
const src = buildSrc(provider, id);
openModal(src);
}
});
modal.addEventListener('click', (e)=>{ if (e.target.hasAttribute('data-close')) closeModal(); });
window.addEventListener('keydown', (e)=>{ if (e.key === 'Escape' && modal.getAttribute('aria-hidden') === 'false') closeModal(); });
})();
</script>
<script src="/js/bootstrap.bundle.min.js"></script>
</body>
</html>