Files
maria_uslugi/detail_public.php

513 lines
21 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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] ?? [];
$currentLang = $lang ?? 'ru';
if (! in_array($currentLang, ['ru', 'fr', 'en'], true)) {
$currentLang = 'ru';
}
function h($s)
{return htmlspecialchars((string) $s, ENT_QUOTES, 'UTF-8');}
function percent_from_5($note)
{$n = max(0, min(5, (float) $note));return (int) round($n / 5 * 100);}
function pseudo_from($prenom, $nom)
{
$p = trim((string) $prenom);
$n = trim((string) $nom);
$p = $p !== '' ? mb_convert_case($p, MB_CASE_TITLE, 'UTF-8') : 'Client';
$i = $n !== '' ? mb_strtoupper(mb_substr($nom, 0, 1, 'UTF-8'), 'UTF-8') : '';
return $i ? ($p . '.' . $i) : $p;
}
function video_thumb_info(?string $url): ?array
{
if (! $url || ! trim($url)) {
return null;
}
$u = trim($url);
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"];
}
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"];
}
$path = parse_url($u, PHP_URL_PATH) ?? '';
if (preg_match('~\.(mp4|webm|ogg)$~i', $path)) {
return ['provider' => 'file', 'id' => null, 'thumb' => null];
}
if (stripos($u, 'vimeo.com') !== false) {
return ['provider' => 'vimeo', 'id' => null, 'thumb' => null];
}
return ['provider' => 'other', 'id' => null, 'thumb' => null];
}
// ID praticien
$praticien_id = (int) ($_GET['id'] ?? 0);
if ($praticien_id <= 0) {
echo "<div class='container py-5'>ID invalide.</div>";
exit;
}
// 1) Praticien
$st = $pdo->prepare("
SELECT
p.id,
p.titre,
p.nom,
p.prenom,
p.telephone,
p.adresse,
p.ville,
p.code_postal,
p.specialite,
p.presentation,
p.photo,
p.video,
p.disponibilite,
COALESCE(pt.nom_affiche, p.nom) AS nom_affiche,
COALESCE(pt.prenom_affiche, p.prenom) AS prenom_affiche,
COALESCE(pt.ville_affichee, p.ville) AS ville_affichee,
COALESCE(pt.titre, p.titre) AS titre_affiche,
COALESCE(pt.specialite, p.specialite) AS specialite_affichee,
COALESCE(pt.presentation, p.presentation) AS presentation_affichee,
COALESCE(pt.disponibilite, p.disponibilite) AS disponibilite_affichee
FROM praticien p
LEFT JOIN praticien_traduction pt
ON pt.praticien_id = p.id
AND pt.langue = :lang
WHERE p.id = :id
LIMIT 1
");
$st->execute([
':lang' => $currentLang,
':id' => $praticien_id,
]);
$p = $st->fetch(PDO::FETCH_ASSOC);
if (! $p) {
echo "<div class='container py-5'>" . ($translations['praticien_introuvable'] ?? 'Praticien introuvable') . "</div>";
exit;
}
$name = trim(($p['prenom_affiche'] ?? $p['prenom'] ?? '') . ' ' . ($p['nom_affiche'] ?? $p['nom'] ?? ''));
$photo = ! empty($p['photo'])
? "/public/uploads/" . ltrim((string) $p['photo'], '/')
: "/public/uploads/img/user_placeholder.png";
// Slug spécialité SEO depuis la valeur BDD
$specSlug = '';
if (! empty($p['specialite'])) {
$specDb = mb_strtolower(trim((string) $p['specialite']), 'UTF-8');
// 1) correspondance exacte
foreach (seo_spec_map() as $slug => $cfg) {
$dbVal = mb_strtolower(trim((string) ($cfg['db'] ?? '')), 'UTF-8');
if ($dbVal !== '' && $dbVal === $specDb) {
$specSlug = $slug;
break;
}
}
// 2) commence par
if ($specSlug === '') {
foreach (seo_spec_map() as $slug => $cfg) {
$dbVal = mb_strtolower(trim((string) ($cfg['db'] ?? '')), 'UTF-8');
if ($dbVal !== '' && mb_stripos($specDb, $dbVal, 0, 'UTF-8') === 0) {
$specSlug = $slug;
break;
}
}
}
// 3) contient
if ($specSlug === '') {
foreach (seo_spec_map() as $slug => $cfg) {
$dbVal = mb_strtolower(trim((string) ($cfg['db'] ?? '')), 'UTF-8');
if ($dbVal !== '' && mb_stripos($specDb, $dbVal, 0, 'UTF-8') !== false) {
$specSlug = $slug;
break;
}
}
}
// 4) fallback propre : slug du texte libre
if ($specSlug === '') {
$specSlug = seo_slug((string) $p['specialite']);
}
}
if ($specSlug === '') {
$specSlug = 'specialiste';
}
// Slug ville SEO
$citySlug = '';
if (! empty($p['ville'])) {
$citySlug = seo_city_slug_from_input((string) $p['ville']);
}
if ($citySlug === '') {
$citySlug = 'france';
}
// Slug nom
$nameSlug = seo_slug($name);
if ($nameSlug === '') {
$nameSlug = 'profil';
}
// URL SEO finale
$prettyPath = '/specialistes/' . rawurlencode($specSlug) . '/' . rawurlencode($citySlug) . '/' . rawurlencode($nameSlug . '-' . (int) $p['id']) . '/';
$canonicalUrl = 'https://uslugi.fr' . $prettyPath;
// Redirection 301 depuis ancienne URL
$currentPath = parse_url($_SERVER['REQUEST_URI'] ?? '', PHP_URL_PATH) ?? '';
if (
($currentPath === '/detail_public.php' || $currentPath === '/specialistes')
&& (int) ($_GET['id'] ?? 0) > 0
) {
$target = $prettyPath;
$extra = $_GET;
unset($extra['id'], $extra['slug']);
if (! empty($extra)) {
$target .= '?' . http_build_query($extra);
}
if (($_SERVER['REQUEST_URI'] ?? '') !== $target) {
header('Location: ' . $target, true, 301);
exit;
}
}
// Variables SEO
$seoTitle = $name;
if (! empty($p['specialite_affichee'])) {
$seoTitle .= ' — ' . $p['specialite_affichee'];
}
if (! empty($p['ville_affichee'])) {
$seoTitle .= ' à ' . $p['ville_affichee'];
}
$seoTitle .= ' | Uslugi.fr';
$presentationShort = trim(strip_tags((string) ($p['presentation_affichee'] ?? $p['presentation'] ?? '')));
if ($presentationShort !== '') {
$presentationShort = mb_substr($presentationShort, 0, 155, 'UTF-8');
}
$seoDesc = $presentationShort !== ''
? $presentationShort
: (
$name
. (! empty($p['specialite_affichee']) ? ' — ' . $p['specialite_affichee'] : '')
. (! empty($p['ville_affichee']) ? ' à ' . $p['ville_affichee'] : '')
. '.'
);
// 2) Vidéos additionnelles
$vs = $pdo->prepare("SELECT id, url, title, ordre FROM praticien_video WHERE praticien_id=? ORDER BY ordre ASC, id DESC");
$vs->execute([$praticien_id]);
$videos = $vs->fetchAll(PDO::FETCH_ASSOC);
// 3) Avis: moyenne + total + dernier
$avg = $pdo->prepare("SELECT ROUND(AVG(note),1) AS m, COUNT(*) AS c FROM avis_praticien WHERE praticien_id=? AND statut='publie'");
$avg->execute([$praticien_id]);
$stats = $avg->fetch(PDO::FETCH_ASSOC) ?: ['m' => null, 'c' => 0];
$last = $pdo->prepare("
SELECT a.note, a.titre, a.commentaire, a.visit_date, a.created_at, c.prenom, c.nom
FROM avis_praticien a
LEFT JOIN client c ON c.id = a.client_id
WHERE a.praticien_id = ? AND a.statut = 'publie'
ORDER BY a.created_at DESC
LIMIT 1
");
$last->execute([$praticien_id]);
$lrow = $last->fetch(PDO::FETCH_ASSOC);
?>
<!doctype html>
<html lang="<?php echo h($currentLang) ?>">
<head>
<meta charset="utf-8">
<title><?php echo h($seoTitle) ?></title>
<meta name="description" content="<?php echo h($seoDesc) ?>">
<link rel="canonical" href="<?php echo h($canonicalUrl) ?>">
<meta name="robots" content="index, follow">
<meta property="og:type" content="profile">
<meta property="og:url" content="<?php echo h($canonicalUrl) ?>">
<meta property="og:title" content="<?php echo h($seoTitle) ?>">
<meta property="og:description" content="<?php echo h($seoDesc) ?>">
<meta property="og:image" content="<?php echo h('https://uslugi.fr' . $photo) ?>">
<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 include_once __DIR__ . '/includes/tracking.php'; ?>
</head>
<body class="layout">
<?php if ($logged):
include_header_clean(APP_HTDOCS . '/public/includes/header.php');
else: ?>
<!-- Header public (Connexion + “?”) -->
<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 $translations['btn_se_connecter'] ?></span>
<span class="d-none d-sm-inline"><?php echo $translations['ja_spec'] ?></span>
</a>
<!--<a class="btn-login" href="/connexion.php"><?php echo $translations['ja_spec'] ?></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 $translations['kak_rabotaet'] ?></a></a></li>
<li><a href="/about.php"><?php echo $translations['pro_nas'] ?></a></li>
<li><a href="/faq.php"><?php echo $translations['faq'] ?></a></li>
<li><a href="/contact.php"><?php echo $translations['nam_pisat'] ?></a></li>
</ul>
</div>
</nav>
</div>
</header>
<?php endif; ?>
<div class="main">
<div class="container py-4">
<div class="profile-card">
<div class="profile-grid profile-top">
<!-- Cadre 1 : Infos -->
<section class="panel">
<div class="info-scroll">
<h2 class="pro-name"><?php echo h($name) ?></h2>
<?php if ($p['specialite_affichee']): ?><div><strong><?php echo h($translations['dp_specialite']) ?>:</strong> <?php echo h($p['specialite_affichee']) ?></div><?php endif; ?>
<!--<?php if ($p['telephone']): ?><div><strong><?php echo h($translations['dp_telephone']) ?>:</strong> <?php echo h($p['telephone']) ?></div><?php endif; ?>-->
<?php if ($p['titre_affiche']): ?><div><strong><?php echo h($translations['tarif_consultation']) ?>:</strong> <?php echo h($p['titre_affiche']) ?></div><?php endif; ?>
<?php if ($p['disponibilite_affichee']): ?><div><strong><?php echo h($translations['moyens_payement']) ?>:</strong> <?php echo h($p['disponibilite_affichee']) ?></div><?php endif; ?>
<?php if ($p['adresse']): ?><div><strong><?php echo h($translations['dp_adresse']) ?>:</strong> <?php echo nl2br(h($p['adresse'])) ?></div><?php endif; ?>
<?php if ($p['ville_affichee'] || $p['code_postal']): ?><div><strong><?php echo h($translations['dp_ville_cp']) ?>:</strong> <?php echo h(trim(($p['ville_affichee'] ?? '') . ' ' . ($p['code_postal'] ?? ''))) ?></div><?php endif; ?>
<?php if ($p['presentation_affichee']): ?><div class="mt-3"><strong><?php echo h($translations['dp_description']) ?>:</strong><br><?php echo nl2br(h($p['presentation_affichee'])) ?></div><?php endif; ?>
</div>
</section>
<!-- Centre : grande photo -->
<section class="big-photo">
<img class="img-fluid w-100"
src="<?php echo h($photo) ?>"
alt="<?php
echo h(
trim($name)
. (! empty($p['specialite_affichee']) ? ' — ' . $p['specialite_affichee'] : '')
. (! empty($p['ville_affichee']) ? ' à ' . $p['ville_affichee'] : '')
. ' — Uslugi.fr'
);
?>">
</section>
<!-- Cadre 2 : Vidéos -->
<?php if ($p['video'] != '') {?>
<aside class="panel">
<h2 class="pro-name"><?php echo h($translations['dp_videos']) ?></h2>
<div class="vlist">
<?php
$alt = h($translations['video_alt'] ?? 'Vidéo');
$printed = false;
// Petite fonction d'affichage d’une vignette
$printThumb = function ($provider, $id, $thumb, $rawUrl) use ($alt) {
if (in_array($provider, ['youtube', 'dailymotion']) && $id) {
echo '<a href="#" class="vitem d-block pro-video"
data-bs-toggle="modal" data-bs-target="#videoModal"
data-provider="' . h($provider) . '" data-id="' . h($id) . '">';
echo $thumb
? '<img src="' . h($thumb) . '" alt="' . $alt . '" loading="lazy">'
: '<img src="/public/uploads/img/video_placeholder.jpg" alt="' . $alt . '" loading="lazy">';
echo '</a>';
} else {
$safe = h($rawUrl);
echo '<a class="vitem d-block" href="' . $safe . '" target="_blank" rel="noopener">';
echo $thumb
? '<img src="' . h($thumb) . '" alt="' . $alt . '" loading="lazy">'
: '<img src="/public/uploads/img/video_placeholder.jpg" alt="' . $alt . '" loading="lazy">';
echo '</a>';
}
};
if (! empty($p['video'])) {
$vi = video_thumb_info($p['video']);
$thumb = $vi['thumb'] ?? null;
$prov = $vi['provider'] ?? null;
$vid = $vi['id'] ?? null;
$printThumb($prov, $vid, $thumb, $p['video']);
$printed = true;
}
foreach ($videos as $v) {
$vi = video_thumb_info($v['url']);
$thumb = $vi['thumb'] ?? null;
$prov = $vi['provider'] ?? null;
$vid = $vi['id'] ?? null;
$printThumb($prov, $vid, $thumb, $v['url']);
$printed = true;
}
if (! $printed) {
echo '<div class="vitem"><img src="/public/uploads/img/video_placeholder.jpg" alt="' . $alt . '"></div>';
//echo '<div class="text-muted small mt-1">'.h($translations['dp_aucune_video']).'</div>';
}
?>
</div>
</aside>
<?php }?>
</div>
<!-- Ligne du bas : Note / Dernier avis / Bouton RDV -->
<div class="bottom-grid mt-4">
<!-- Cadre 3 : Note -->
<div class="card shadow-sm p-3">
<div class="d-flex align-items-center gap-3">
<?php $m = (float) ($stats['m'] ?? 0);
$c = (int) ($stats['c'] ?? 0);
$w = percent_from_5($m); ?>
<div class="rating" aria-label="Note moyenne">
<div class="stars-base">★★★★★</div>
<div class="stars-fill" style="width:<?php echo $w ?>%">★★★★★</div>
</div>
<div>
<div><strong><?php echo h($translations['avis_note_moy']) ?>:</strong> <?php echo $m ? number_format($m, 1, ',', ' ') : '—' ?>/5</div>
<div class="text-muted">(<?php echo $c . ' ' . h($translations['avis_total']) ?>)</div>
</div>
</div>
</div>
<!-- Cadre 4 : Dernier avis -->
<div class="card shadow-sm p-3 avis-card">
<div class="d-flex justify-content-between align-items-start mb-2">
<strong><?php echo h($translations['avis_dernier']) ?></strong>
<a class="btn btn-link p-0" href="/avis_public.php?praticien_id=<?php echo (int) $praticien_id ?>">
<?php echo h($translations['avis_lire_tous']) ?>
</a>
</div>
<?php if ($lrow): ?>
<?php $lw = percent_from_5((float) $lrow['note']); ?>
<div class="rating mb-1">
<div class="stars-base">★★★★★</div>
<div class="stars-fill" style="width:<?php echo $lw ?>%">★★★★★</div>
</div>
<div class="meta">
<?php echo h(pseudo_from($lrow['prenom'] ?? '', $lrow['nom'] ?? '')) ?>
<?php if (! empty($lrow['visit_date'])): ?> • <?php echo date('d/m/Y', strtotime($lrow['visit_date'])) ?><?php endif; ?>
</div>
<div><?php echo nl2br(h(mb_strimwidth((string) $lrow['commentaire'], 0, 320, '…', 'UTF-8'))) ?></div>
<?php else: ?>
<div class="text-muted"><?php echo h($translations['avis_aucun']) ?></div>
<?php endif; ?>
</div>
<!-- Bouton RDV -->
<div class="text-end">
<a class="btn btn-danger btn-lg" href="/public/client/reserver_rdv.php?praticien_id=<?php echo (int) $praticien_id ?>">
<?php echo h($translations['dp_btn_rdv']) ?>
</a>
</div>
</div>
</div>
</div>
</div>
<!-- Modal vidéo -->
<div class="modal fade" id="videoModal" tabindex="-1" aria-labelledby="videoModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-xl">
<div class="modal-content border-0 shadow-lg rounded-4">
<div class="modal-header border-0">
<h5 class="modal-title" id="videoModalLabel"><?php echo h($translations['video_titre'] ?? 'Vidéo') ?></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="<?php echo h($translations['fermer'] ?? 'Fermer') ?>"></button>
</div>
<div class="modal-body pt-0">
<div class="ratio ratio-16x9 rounded-3 overflow-hidden bg-black">
<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="modal-footer border-0">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
<?php echo h($translations['fermer'] ?? 'Fermer') ?>
</button>
</div>
</div>
</div>
</div>
<script>
(function(){
const modal = document.getElementById('videoModal');
const iframe = document.getElementById('vPlayer');
function buildEmbed(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 '';
}
document.addEventListener('click', function(e){
const a = e.target.closest('a.pro-video[data-provider][data-id]');
if (!a) return;
e.preventDefault();
const src = buildEmbed(a.dataset.provider, a.dataset.id);
if (src) iframe.src = src;
}, false);
modal.addEventListener('hidden.bs.modal', function(){
iframe.src = '';
});
})();
</script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" defer></script>
<footer class="site-footer">
<div class="container">
<p><?php echo $translations['copyr'] ?></p>
</div>
</footer>
</body>
</html>