{% extends 'application/whileresume/application/jobs/layout-social.html.twig' %}{% trans_default_domain 'whr-public' %}{% import 'application/whileresume/_macros/promo_card.html.twig' as promo %}{% block title %}{% if currentSubKeyword is defined and currentSubKeyword is not null and currentCityFilter is defined and currentCityFilter is not null %}{{ currentSubKeyword }} {{ currentCityFilter.label }}{% elseif currentFilter is defined and currentFilter is not null %}{{ currentFilter.shortTitle|default(currentFilter.label) }} — WhileResume{% else %}{{ 'job.dashboard.title'|trans }}{% endif %}{% endblock title %}{% block description %}{% if currentSubKeyword is defined and currentSubKeyword is not null and currentCityFilter is defined and currentCityFilter is not null %}Découvrez les offres "{{ currentSubKeyword }}" à {{ currentCityFilter.label }}.{% elseif currentFilter is defined and currentFilter is not null and currentFilter.shortDescription is not empty %}{{ currentFilter.shortDescription }}{% else %}{{ 'job.dashboard.description'|trans }}{% endif %}{% endblock description %}{% block robots %}index,follow{% endblock robots %}{% block css %}<style>/* ─── Page dashboard /jobs ─── */.jobs-dash{max-width:880px;margin:0 auto}.jobs-dash-header{margin-bottom:18px}.jobs-dash-title{font-size:24px;font-weight:800;color:#1E1B2E;line-height:1.2;letter-spacing:-0.02em;margin:0 0 6px}@media(min-width:768px){.jobs-dash-title{font-size:28px}}.jobs-dash-subtitle{font-size:14px;color:#6B7280;margin:0 0 18px}/* ─── Bandeau contexte ville (badge + bouton ALL) ─── */.city-context-bar{display:flex;align-items:center;flex-wrap:wrap;gap:8px;margin:0 0 16px;padding:10px 14px;background:#F5F3FF;border:1px solid rgba(108,58,237,.15);border-radius:12px}.city-context-label{font-size:11px;font-weight:600;color:#6B7280;text-transform:uppercase;letter-spacing:.06em}.city-context-badge{display:inline-flex;align-items:center;gap:6px;padding:4px 12px;border-radius:100px;background:#fff;color:var(--theme-color,#6C3AED);font-size:13px;font-weight:600;border:1px solid rgba(108,58,237,.2)}.city-context-badge-icon{font-size:14px;line-height:1}.city-context-clear-btn{margin-left:auto;display:inline-flex;align-items:center;gap:4px;padding:5px 12px;border-radius:100px;background:transparent;color:var(--theme-color,#6C3AED);font-size:12px;font-weight:600;border:1px dashed rgba(108,58,237,.4);cursor:pointer;text-decoration:none;font-family:inherit;transition:background .15s,border-style .15s,border-color .15s}.city-context-clear-btn:hover{background:#fff;border-style:solid;border-color:var(--theme-color,#6C3AED);color:var(--theme-color,#6C3AED)}.city-context-clear-btn svg{width:11px;height:11px}/* Widget recherche */.wr-search-widget{background:#fff;border-radius:14px;box-shadow:0 0 20px 0 rgba(0,0,0,0.05);padding:10px;margin-bottom:14px;position:relative;z-index:50}.wr-search-input-wrap{display:flex;align-items:center;gap:10px;padding:8px 14px;border-radius:10px;background:#F9FAFB;border:1px solid #E5E7EB;transition:border-color .2s,background .2s}.wr-search-input-wrap:focus-within{border-color:rgba(108,58,237,.3);background:#fff}.wr-search-icon{color:#9CA3AF;display:flex;flex-shrink:0}.wr-search-input{flex:1;border:none;outline:none;background:transparent;font-size:14px;color:#1E1B2E;padding:4px 0;font-family:inherit;min-width:0}.wr-search-input::placeholder{color:#9CA3AF}.wr-search-clear{background:none;border:none;color:#9CA3AF;cursor:pointer;padding:4px;display:none;font-family:inherit}.wr-search-clear.visible{display:flex;align-items:center}.wr-search-clear:hover{color:#6B7280}.wr-search-clear svg{width:14px;height:14px}.wr-search-kbd-hint{display:none;align-items:center;padding:3px 8px;background:#fff;border:1px solid #E5E7EB;border-radius:6px;font-size:10.5px;font-weight:600;color:#6B7280;font-family:inherit;letter-spacing:.02em;white-space:nowrap;flex-shrink:0}.wr-search-input-wrap:focus-within .wr-search-kbd-hint{display:inline-flex}/* Chips keywords */.search-keywords{display:flex;flex-wrap:wrap;gap:6px;margin:0 4px 18px}.search-keyword-chip{display:inline-flex;align-items:center;gap:5px;padding:6px 12px;border-radius:100px;background:#F5F3FF;color:var(--theme-color,#6C3AED);font-size:12.5px;font-weight:500;cursor:pointer;border:1px solid transparent;transition:background .15s,border-color .15s,transform .15s;font-family:inherit}.search-keyword-chip:hover{background:#EDE9FE;border-color:rgba(108,58,237,.2);transform:translateY(-1px);color:var(--theme-color,#6C3AED)}.search-keyword-chip.active{background:var(--theme-color,#6C3AED);color:#fff;border-color:var(--theme-color,#6C3AED)}.search-keyword-chip-icon{font-size:13px;line-height:1}.search-keywords-label{font-size:11px;font-weight:600;color:#9CA3AF;text-transform:uppercase;letter-spacing:.06em;margin-right:4px;align-self:center}/* ─── Sections de filtres SEO (chips → liens directs vers /jobs/{slug}) ─── */.filter-section{margin:0 0 14px}.filter-section-head{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:10px;padding:0 4px;flex-wrap:wrap}@media(max-width:540px){.dash-filter-search{max-width:100%;width:100%}}.filter-section-title{display:flex;align-items:center;gap:7px;font-size:11.5px;font-weight:700;color:#6B7280;text-transform:uppercase;letter-spacing:.06em;margin:0}.filter-section-title-icon{display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;border-radius:6px;background:#F5F3FF;color:var(--theme-color,#6C3AED)}.filter-section-title-icon svg{width:11px;height:11px}.filter-section-toggle{background:none;border:none;font-size:11px;color:var(--theme-color,#6C3AED);font-weight:600;cursor:pointer;font-family:inherit;padding:4px 8px;border-radius:6px;transition:background .15s}.filter-section-toggle:hover{background:#F5F3FF}.filter-chips{display:flex;flex-wrap:wrap;gap:6px;padding:0 4px}.filter-chip{display:inline-flex;align-items:center;gap:5px;padding:7px 13px;border-radius:100px;background:#fff;color:#374151;font-size:12.5px;font-weight:500;cursor:pointer;border:1px solid #E5E7EB;text-decoration:none;transition:background .15s,border-color .15s,transform .15s,color .15s;font-family:inherit;white-space:nowrap}.filter-chip:hover{background:#F5F3FF;border-color:rgba(108,58,237,.3);color:var(--theme-color,#6C3AED);transform:translateY(-1px)}.filter-chip.active{background:var(--theme-color,#6C3AED);border-color:var(--theme-color,#6C3AED);color:#fff}.filter-chip.active:hover{background:#5B21B6;color:#fff}.filter-chip-icon{font-size:14px;line-height:1}.filter-chip-arrow{display:inline-flex;color:currentColor;opacity:.5;margin-left:2px}.filter-chip-arrow svg{width:10px;height:10px}/* Mode "compact" : sections initialement collapsées si > 6 items */.filter-chips.collapsed{max-height:80px;overflow:hidden;position:relative}.filter-chips.collapsed::after{content:"";position:absolute;left:0;right:0;bottom:0;height:30px;background:linear-gradient(to bottom,transparent,#FAFAFA);pointer-events:none}/* ─── Bouton "Voir plus" sous les chips villes/métiers ─── */.dash-filter-more-wrap{display:flex;justify-content:center;margin-top:10px;padding:0 4px}.dash-filter-more-btn{display:inline-flex;align-items:center;gap:6px;padding:7px 16px;background:transparent;color:var(--theme-color,#6C3AED);border:1px dashed rgba(108,58,237,.35);border-radius:100px;font-size:12px;font-weight:600;font-family:inherit;cursor:pointer;transition:background .15s,border-style .15s,border-color .15s}.dash-filter-more-btn:hover{background:#F5F3FF;border-style:solid;border-color:var(--theme-color,#6C3AED)}.dash-filter-more-icon{display:inline-flex;align-items:center;color:currentColor;transition:transform .2s}.dash-filter-more-icon svg{width:12px;height:12px}.dash-filter-more-btn.is-expanded .dash-filter-more-icon{transform:rotate(180deg)}/* ─── Input de recherche dans le header (toujours visible) ─── */.dash-filter-search{display:block;position:relative;flex:1;max-width:240px}@keyframes dashFilterFadeIn{from{opacity:0;transform:translateY(-2px)}to{opacity:1;transform:translateY(0)}}.dash-filter-search-input{width:100%;padding:5px 10px 5px 28px;border:1px solid #E5E7EB;border-radius:100px;font-size:12px;color:#1E1B2E;background:#FAFAFA;font-family:inherit;outline:none;box-sizing:border-box;transition:border-color .15s,background .15s}.dash-filter-search-input:focus{border-color:rgba(108,58,237,.4);background:#fff}.dash-filter-search-input::placeholder{color:#9CA3AF}.dash-filter-search-icon{position:absolute;left:10px;top:50%;transform:translateY(-50%);color:#9CA3AF;pointer-events:none;display:flex}.dash-filter-search-icon svg{width:11px;height:11px}/* État "aucun résultat" sous le filtre live */.dash-filter-empty{display:none;padding:14px 12px;font-size:12.5px;color:#9CA3AF;font-style:italic;text-align:center;width:100%}.filter-chips.is-no-results .dash-filter-empty{display:block}/* Bandeau résultats */.jobs-results-info{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:8px 4px 14px;font-size:12px;color:#6B7280}.jobs-results-info strong{color:#1E1B2E;font-weight:700}/* Liste cards (style "similar-card" comme dans show.html.twig) */.jobs-list{display:flex;flex-direction:column;gap:10px}.jobs-card{background:#fff;border-radius:14px;padding:14px;box-shadow:0 0 16px 0 rgba(0,0,0,0.04);display:flex;align-items:center;gap:14px;text-decoration:none;color:inherit;transition:transform .15s,box-shadow .2s}.jobs-card:hover{transform:translateY(-1px);box-shadow:0 4px 20px rgba(108,58,237,.1);color:inherit}.jobs-card-logo{width:50px;height:50px;border-radius:12px;background:linear-gradient(135deg,#EDE9FE,#DDD6FE);display:flex;align-items:center;justify-content:center;font-weight:700;font-size:20px;color:var(--theme-color,#6C3AED);flex-shrink:0;overflow:hidden}.jobs-card-logo img{width:100%;height:100%;object-fit:cover;border-radius:8px}.jobs-card-logo svg{width:24px;height:24px;opacity:.85}.jobs-card-info{flex:1;min-width:0}.jobs-card-title{font-size:14px;font-weight:700;color:#1E1B2E;line-height:1.3;margin:0 0 4px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.jobs-card-meta{font-size:12px;color:#6B7280;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.jobs-card-salary{font-size:11px;font-weight:600;color:var(--theme-color,#6C3AED);background:#F5F3FF;padding:3px 9px;border-radius:100px;white-space:nowrap;flex-shrink:0}.jobs-card-arrow{flex-shrink:0;color:#9CA3AF;transition:color .15s,transform .15s}.jobs-card:hover .jobs-card-arrow{color:var(--theme-color,#6C3AED);transform:translateX(2px)}.jobs-card-arrow svg{width:18px;height:18px}/* Skeleton (pendant chargement) */.jobs-skeleton{display:flex;flex-direction:column;gap:10px}.jobs-skel-card{background:#fff;border-radius:14px;padding:14px;display:flex;align-items:center;gap:14px;box-shadow:0 0 16px 0 rgba(0,0,0,0.04)}.jobs-skel-logo{width:50px;height:50px;border-radius:12px;background:linear-gradient(90deg,#F3F4F6 0%,#E5E7EB 50%,#F3F4F6 100%);background-size:200% 100%;animation:jobsSkelShim 1.4s ease-in-out infinite;flex-shrink:0}.jobs-skel-info{flex:1}.jobs-skel-line{height:14px;background:linear-gradient(90deg,#F3F4F6 0%,#E5E7EB 50%,#F3F4F6 100%);background-size:200% 100%;animation:jobsSkelShim 1.4s ease-in-out infinite;border-radius:4px;margin-bottom:6px}.jobs-skel-line.s1{width:60%}.jobs-skel-line.s2{width:40%;height:12px;margin-bottom:0}@keyframes jobsSkelShim{0%{background-position:200% 0}100%{background-position:-200% 0}}/* Bouton charger plus */.jobs-load-more{display:flex;width:100%;padding:14px;margin-top:16px;background:#fff;color:var(--theme-color,#6C3AED);border:1.5px dashed rgba(108,58,237,.35);border-radius:12px;font-size:13.5px;font-weight:700;cursor:pointer;font-family:inherit;align-items:center;justify-content:center;gap:8px;transition:background .15s,border-color .15s}.jobs-load-more:hover{background:#F5F3FF;border-color:var(--theme-color,#6C3AED);border-style:solid;color:var(--theme-color,#6C3AED)}.jobs-load-more svg{width:14px;height:14px}.jobs-load-more:disabled{opacity:.6;cursor:not-allowed}.jobs-load-more.loading{cursor:wait}.jobs-load-spinner{display:inline-block;width:14px;height:14px;border:2px solid rgba(108,58,237,.2);border-top-color:var(--theme-color,#6C3AED);border-radius:50%;animation:jobsLoadSpin .7s linear infinite}@keyframes jobsLoadSpin{to{transform:rotate(360deg)}}/* État vide */.jobs-empty{text-align:center;padding:40px 20px;background:#fff;border-radius:14px;box-shadow:0 0 16px 0 rgba(0,0,0,0.04)}.jobs-empty-icon{display:inline-flex;align-items:center;justify-content:center;width:56px;height:56px;border-radius:16px;background:#F5F3FF;color:var(--theme-color,#6C3AED);margin-bottom:14px}.jobs-empty-icon svg{width:24px;height:24px}.jobs-empty-title{font-size:16px;font-weight:700;color:#1E1B2E;margin:0 0 6px}.jobs-empty-text{font-size:13px;color:#6B7280;margin:0}</style>{# Styles partagés de la promo card (définis dans la macro) #}{{ promo.styles() }}{% endblock css %}{% block body %}<div class="jobs-dash"><div class="jobs-dash-header">{% if currentSubKeyword is defined and currentSubKeyword is not null and currentCityFilter is defined and currentCityFilter is not null %}{# Cas combiné : ville + keyword #}<h1 class="jobs-dash-title">{% if currentCityFilter.icon is not empty %}{{ currentCityFilter.icon }} {% endif %}{{ currentSubKeyword }} {{ 'job.dashboard.cityHeadingIn'|trans }} {{ currentCityFilter.label }}</h1><p class="jobs-dash-subtitle">{{ 'job.dashboard.cityKeywordSubtitle'|trans({'%keyword%': currentSubKeyword, '%city%': currentCityFilter.label}) }}</p>{% elseif currentFilter is defined and currentFilter is not null %}<h1 class="jobs-dash-title">{% if currentFilter.icon is not empty %}{{ currentFilter.icon }} {% endif %}{{ currentFilter.shortTitle|default(currentFilter.label) }}</h1>{% if currentFilter.shortDescription is not empty %}<p class="jobs-dash-subtitle">{{ currentFilter.shortDescription }}</p>{% else %}<p class="jobs-dash-subtitle">{{ 'job.dashboard.subtitle'|trans }}</p>{% endif %}{% else %}<h1 class="jobs-dash-title">{{ 'job.dashboard.heading'|trans }}</h1><p class="jobs-dash-subtitle">{{ 'job.dashboard.subtitle'|trans }}</p>{% endif %}{# Badge "Ville active" + bouton "Tout voir" / ALL pour reset #}{% if currentCityFilter is defined and currentCityFilter is not null %}<div class="city-context-bar"><span class="city-context-label">{{ 'job.dashboard.contextLabel'|trans }}</span><span class="city-context-badge">{% if currentCityFilter.icon is not empty %}<span class="city-context-badge-icon">{{ currentCityFilter.icon }}</span>{% endif %}<span>{{ currentCityFilter.label }}</span></span><a href="{% if app.request.locale == 'en' %}{{ path('whileresume_jobs_list') }}{% else %}{{ path('locale_whileresume_jobs_list',{'_locale':app.request.locale}) }}{% endif %}"class="city-context-clear-btn"title="{{ 'job.dashboard.clearCity'|trans }}"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="9 18 15 12 9 6" transform="rotate(180 12 12)"/></svg>{{ 'job.dashboard.allCities'|trans }}</a></div>{% endif %}</div>{# ═══ Widget recherche ═══ #}<div class="wr-search-widget"><div class="wr-search-input-wrap"><span class="wr-search-icon"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><path d="M21 21l-4.35-4.35"/></svg></span><input type="text" id="dashSearchInput" class="wr-search-input"placeholder="{% if currentCityFilter is defined and currentCityFilter is not null %}{{ 'job.dashboard.searchInCity'|trans({'%city%': currentCityFilter.label}) }}{% else %}{{ 'job.show.searchPlaceholder'|trans }}{% endif %}"value="{{ initialQuery|default('') }}"autocomplete="off"><span class="wr-search-kbd-hint" id="dashSearchKbdHint">{{ 'job.dashboard.pressEnter'|trans }}</span><button type="button" id="dashSearchClear" class="wr-search-clear" aria-label="Clear"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg></button></div></div>{# Chips keywords cliquables #}{% if searchKeywords is defined and searchKeywords|length > 0 %}<div class="search-keywords"><span class="search-keywords-label">{{ 'job.show.keywordsLabel'|trans }}</span>{% for kw in searchKeywords %}{# Slug pour l'URL : translitération des accents FR + lowercase + remplacement caractères spéciaux #}{% set kwSlug = kw.label|replace({'à':'a','â':'a','ä':'a','á':'a','ã':'a','å':'a','À':'a','Â':'a','Ä':'a','Á':'a','Ã':'a','Å':'a','é':'e','è':'e','ê':'e','ë':'e','ẽ':'e','É':'e','È':'e','Ê':'e','Ë':'e','î':'i','ï':'i','ì':'i','í':'i','Î':'i','Ï':'i','Ì':'i','Í':'i','ô':'o','ö':'o','ò':'o','ó':'o','õ':'o','ø':'o','Ô':'o','Ö':'o','Ò':'o','Ó':'o','Õ':'o','Ø':'o','û':'u','ü':'u','ù':'u','ú':'u','Û':'u','Ü':'u','Ù':'u','Ú':'u','ÿ':'y','ý':'y','Ÿ':'y','Ý':'y','ç':'c','Ç':'c','ñ':'n','Ñ':'n','œ':'oe','Œ':'oe','æ':'ae','Æ':'ae',"'":'-',' ':'-','/':'-','_':'-','.':'-',',':'-','(':'','*':'',')':'','&':'-and-','+':'-plus-','#':'','?':'','!':'','"':'','’':'-','`':'-'})|lower|replace({'--':'-','---':'-'})|trim('-') %}{% if currentCityFilter is defined and currentCityFilter is not null %}{# Contexte ville : chip = lien vers /jobs/ville/keyword #}<a href="{% if app.request.locale == 'en' %}{{ path('cvs_application_jobs_filter_keyword',{'slug':currentCityFilter.slug,'keyword':kwSlug}) }}{% else %}{{ path('locale_cvs_application_jobs_filter_keyword',{'_locale':app.request.locale,'slug':currentCityFilter.slug,'keyword':kwSlug}) }}{% endif %}"class="search-keyword-chip{% if currentSubKeywordSlug is defined and currentSubKeywordSlug == kwSlug %} active{% endif %}">{% if kw.icon is not empty %}<span class="search-keyword-chip-icon">{{ kw.icon }}</span>{% endif %}<span>{{ kw.label }}</span></a>{% else %}{# Pas de contexte ville : chip pré-remplit la search (comportement existant) #}<button type="button"class="search-keyword-chip"data-keyword="{{ kw.searchKeyword|default(kw.label) }}">{% if kw.icon is not empty %}<span class="search-keyword-chip-icon">{{ kw.icon }}</span>{% endif %}<span>{{ kw.label }}</span></button>{% endif %}{% endfor %}</div>{% endif %}{# ═══ Filtres SEO : Villes (chips → /jobs/{slug}) — 14 visibles + reste cliqué pour révéler ═══ #}{% if sidebarFiltersCities is defined and sidebarFiltersCities|length > 0 %}{% set citiesInitial = 14 %}<div class="filter-section" id="filterSectionCities"><div class="filter-section-head"><h3 class="filter-section-title"><span class="filter-section-title-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0118 0z"/><circle cx="12" cy="10" r="3"/></svg></span>{{ 'job.dashboard.popularCities'|trans }}</h3>{# Input de recherche (visible uniquement quand expanded) #}{% if sidebarFiltersCities|length > citiesInitial %}<div class="dash-filter-search"><span class="dash-filter-search-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><path d="M21 21l-4.35-4.35"/></svg></span><input type="text"class="dash-filter-search-input"data-search-target="filterChipsCities"placeholder="{{ 'job.dashboard.filterSearchCity'|trans }}"autocomplete="off"></div>{% endif %}</div><div class="filter-chips dash-filter-chips" id="filterChipsCities" data-initial="{{ citiesInitial }}">{% for f in sidebarFiltersCities %}<a href="{% if app.request.locale == 'en' %}{{ path('cvs_application_jobs_filter',{'slug':f.slug}) }}{% else %}{{ path('locale_cvs_application_jobs_filter',{'_locale':app.request.locale,'slug':f.slug}) }}{% endif %}"class="filter-chip dash-filter-item{% if currentFilter is defined and currentFilter is not null and currentFilter.slug == f.slug %} active{% endif %}"data-filter-slug="{{ f.slug }}"data-filter-label="{{ f.label }}"data-index="{{ loop.index0 }}"{% if loop.index0 >= citiesInitial %} style="display:none"{% endif %}>{% if f.icon is not empty %}<span class="filter-chip-icon">{{ f.icon }}</span>{% endif %}<span>{{ f.label }}</span><span class="filter-chip-arrow"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="9 18 15 12 9 6"/></svg></span></a>{% endfor %}<div class="dash-filter-empty">{{ 'job.dashboard.filterNoResults'|trans }}</div></div>{% if sidebarFiltersCities|length > citiesInitial %}<div class="dash-filter-more-wrap"><button type="button" class="dash-filter-more-btn" data-target="filterChipsCities" data-section="filterSectionCities"><span class="dash-filter-more-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="6 9 12 15 18 9"/></svg></span><span class="dash-filter-more-label" data-more="{{ 'job.dashboard.showMoreCities'|trans({'%n%': sidebarFiltersCities|length - citiesInitial}) }}" data-less="{{ 'job.dashboard.showLessCities'|trans }}">{{ 'job.dashboard.showMoreCities'|trans({'%n%': sidebarFiltersCities|length - citiesInitial}) }}</span></button></div>{% endif %}</div>{% endif %}{# ═══ Filtres SEO : Métiers (chips → /jobs/{slug}) — 8 visibles + reste cliqué pour révéler ═══ #}{% if sidebarFiltersCategories is defined and sidebarFiltersCategories|length > 0 %}{% set categoriesInitial = 8 %}<div class="filter-section" id="filterSectionCategories"><div class="filter-section-head"><h3 class="filter-section-title"><span class="filter-section-title-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="7" width="20" height="14" rx="2"/><path d="M16 7V5a4 4 0 00-8 0v2"/></svg></span>{{ 'job.dashboard.popularCategories'|trans }}</h3>{# Input de recherche (visible uniquement quand expanded) #}{% if sidebarFiltersCategories|length > categoriesInitial %}<div class="dash-filter-search"><span class="dash-filter-search-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><path d="M21 21l-4.35-4.35"/></svg></span><input type="text"class="dash-filter-search-input"data-search-target="filterChipsCategories"placeholder="{{ 'job.dashboard.filterSearchCategory'|trans }}"autocomplete="off"></div>{% endif %}</div><div class="filter-chips dash-filter-chips" id="filterChipsCategories" data-initial="{{ categoriesInitial }}">{% for f in sidebarFiltersCategories %}<a href="{% if app.request.locale == 'en' %}{{ path('cvs_application_jobs_filter',{'slug':f.slug}) }}{% else %}{{ path('locale_cvs_application_jobs_filter',{'_locale':app.request.locale,'slug':f.slug}) }}{% endif %}"class="filter-chip dash-filter-item{% if currentFilter is defined and currentFilter is not null and currentFilter.slug == f.slug %} active{% endif %}"data-filter-slug="{{ f.slug }}"data-filter-label="{{ f.label }}"data-index="{{ loop.index0 }}"{% if loop.index0 >= categoriesInitial %} style="display:none"{% endif %}>{% if f.icon is not empty %}<span class="filter-chip-icon">{{ f.icon }}</span>{% endif %}<span>{{ f.label }}</span><span class="filter-chip-arrow"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="9 18 15 12 9 6"/></svg></span></a>{% endfor %}<div class="dash-filter-empty">{{ 'job.dashboard.filterNoResults'|trans }}</div></div>{% if sidebarFiltersCategories|length > categoriesInitial %}<div class="dash-filter-more-wrap"><button type="button" class="dash-filter-more-btn" data-target="filterChipsCategories" data-section="filterSectionCategories"><span class="dash-filter-more-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="6 9 12 15 18 9"/></svg></span><span class="dash-filter-more-label" data-more="{{ 'job.dashboard.showMoreCategories'|trans({'%n%': sidebarFiltersCategories|length - categoriesInitial}) }}" data-less="{{ 'job.dashboard.showLessCategories'|trans }}">{{ 'job.dashboard.showMoreCategories'|trans({'%n%': sidebarFiltersCategories|length - categoriesInitial}) }}</span></button></div>{% endif %}</div>{% endif %}{# Bandeau d'info (compteur de résultats) #}<div class="jobs-results-info" id="jobsResultsInfo"><span id="jobsResultsCount">{{ 'job.dashboard.loading'|trans }}</span></div>{# Liste des offres (peuplée en JS) #}<div class="jobs-list" id="jobsList"><div class="jobs-skeleton">{% for i in 1..5 %}<div class="jobs-skel-card"><div class="jobs-skel-logo"></div><div class="jobs-skel-info"><div class="jobs-skel-line s1"></div><div class="jobs-skel-line s2"></div></div></div>{% endfor %}</div></div>{# Bouton "Charger plus" #}<button type="button" class="jobs-load-more" id="jobsLoadMore" style="display:none;"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="6 9 12 15 18 9"/></svg><span id="jobsLoadMoreLabel">{{ 'job.dashboard.loadMore'|trans }}</span></button>{# Template HTML invisible cloné par le JS pour intercaler la promo card.Affiché uniquement aux utilisateurs non connectés via la logique JS.Les textes/bullets viennent automatiquement des clés job.show.promo* du YAML. #}{% if app.user is null %}{{ promo.card_template({template_id: 'jobs-promo-card-template',login_url: app.request.locale == 'en'? path('cvs_gestion_candidates_dashboard'): path('locale_cvs_gestion_candidates_dashboard',{'_locale':app.request.locale})}) }}{% endif %}</div>{% endblock body %}{% block footerjs %}<script>(function(){'use strict';try {var LOCALE = '{{ app.request.locale }}';var IS_AUTHENTICATED = {{ app.user ? 'true' : 'false' }};var SEARCH_API_URL = '{{ app.request.locale == "en" ? path("cvs_application_jobs_api_search") : path("locale_cvs_application_jobs_api_search",{"_locale":app.request.locale}) }}';var JOB_SHOW_URL_TEMPLATE = '{{ app.request.locale == "en" ? path("cvs_application_jobs_api_search",{"slug":"__SLUG__"}) : path("locale_cvs_application_job_show",{"_locale":app.request.locale,"slug":"__SLUG__"}) }}';var PAGE_SIZE = 10;var currentQuery = {{ initialQuery|default('')|json_encode|raw }};var currentOffset = 0;var currentTotal = 0;var isFetching = false;// Contexte ville (vient du serveur si on est sur /jobs/paris/... sinon vide)var CITY_KEYWORD = {{ currentCityKeyword|default('')|json_encode|raw }};// Slug ville pour générer les URLs de redirection au submitvar CITY_SLUG = {{ currentCityFilter is defined and currentCityFilter is not null ? currentCityFilter.slug|json_encode|raw : 'null' }};// URL pour le dashboard global (sans ville)var DASHBOARD_URL = '{{ app.request.locale == "en" ? path("whileresume_jobs_list") : path("locale_whileresume_jobs_list",{"_locale":app.request.locale}) }}';// Templates URL pour /jobs/{citySlug} et /jobs/{citySlug}/{keyword}var CITY_LANDING_URL_TEMPLATE = {{ currentCityFilter is defined and currentCityFilter is not null? (app.request.locale == 'en'? path('cvs_application_jobs_filter',{'slug':currentCityFilter.slug}): path('locale_cvs_application_jobs_filter',{'_locale':app.request.locale,'slug':currentCityFilter.slug}))|json_encode|raw: 'null' }};var CITY_KEYWORD_URL_TEMPLATE = {{ currentCityFilter is defined and currentCityFilter is not null? (app.request.locale == 'en'? path('cvs_application_jobs_filter_keyword',{'slug':currentCityFilter.slug,'keyword':'kwplaceholder'}): path('locale_cvs_application_jobs_filter_keyword',{'_locale':app.request.locale,'slug':currentCityFilter.slug,'keyword':'kwplaceholder'}))|json_encode|raw: 'null' }};// Liste des keywords BDD avec leur slug (pour le matching côté client au submit)var KEYWORDS_DATA = [{% for kw in searchKeywords|default([]) %}{% set kwSlugForJs = kw.label|replace({'à':'a','â':'a','ä':'a','á':'a','ã':'a','å':'a','À':'a','Â':'a','Ä':'a','Á':'a','Ã':'a','Å':'a','é':'e','è':'e','ê':'e','ë':'e','ẽ':'e','É':'e','È':'e','Ê':'e','Ë':'e','î':'i','ï':'i','ì':'i','í':'i','Î':'i','Ï':'i','Ì':'i','Í':'i','ô':'o','ö':'o','ò':'o','ó':'o','õ':'o','ø':'o','Ô':'o','Ö':'o','Ò':'o','Ó':'o','Õ':'o','Ø':'o','û':'u','ü':'u','ù':'u','ú':'u','Û':'u','Ü':'u','Ù':'u','Ú':'u','ÿ':'y','ý':'y','Ÿ':'y','Ý':'y','ç':'c','Ç':'c','ñ':'n','Ñ':'n','œ':'oe','Œ':'oe','æ':'ae','Æ':'ae',"'":'-',' ':'-','/':'-','_':'-','.':'-',',':'-','(':'','*':'',')':'','&':'-and-','+':'-plus-','#':'','?':'','!':'','"':'','’':'-','`':'-'})|lower|replace({'--':'-','---':'-'})|trim('-') %}{ label: {{ kw.label|json_encode|raw }}, searchKeyword: {{ (kw.searchKeyword ?: kw.label)|json_encode|raw }}, slug: {{ kwSlugForJs|json_encode|raw }} }{% if not loop.last %},{% endif %}{% endfor %}];var input = document.getElementById('dashSearchInput');var clearBtn = document.getElementById('dashSearchClear');var listEl = document.getElementById('jobsList');var infoEl = document.getElementById('jobsResultsCount');var loadMoreBtn = document.getElementById('jobsLoadMore');var loadMoreLabel = document.getElementById('jobsLoadMoreLabel');function escapeHtml(s){if(s == null) return '';return String(s).replace(/[&<>"']/g, function(c){return {'&':'&','<':'<','>':'>','"':'"',"'":'''}[c];});}function buildJobCard(item){// Fallback logo : icône "immeuble" stylisée si pas de logo entreprisevar fallbackLogo ='<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">' +'<path d="M3 21h18"/>' +'<path d="M5 21V7l8-4v18"/>' +'<path d="M19 21V11l-6-4"/>' +'<path d="M9 9h.01"/>' +'<path d="M9 13h.01"/>' +'<path d="M9 17h.01"/>' +'</svg>';var logoHtml = item.logo? '<img src="' + escapeHtml(item.logo) + '" alt="" onerror="this.parentNode.innerHTML=\'' + fallbackLogo.replace(/'/g, "\\'") + '\';">': fallbackLogo;var meta = [];if(item.companyName) meta.push(escapeHtml(item.companyName));if(item.city) meta.push(escapeHtml(item.city));if(item.employmentType) meta.push(escapeHtml(item.employmentType));var salaryHtml = (item.salaryMin && item.salaryMax)? '<span class="jobs-card-salary">' + item.salaryMin + '–' + item.salaryMax + ' ' + escapeHtml(item.devise || '') + '</span>': '';var url = item.url || JOB_SHOW_URL_TEMPLATE.replace('__SLUG__', encodeURIComponent(item.slug));var a = document.createElement('a');a.href = url;a.className = 'jobs-card';a.innerHTML ='<div class="jobs-card-logo">' + logoHtml + '</div>' +'<div class="jobs-card-info">' +'<div class="jobs-card-title">' + escapeHtml(item.title || '') + '</div>' +'<div class="jobs-card-meta">' + meta.join(' · ') + '</div>' +'</div>' +salaryHtml +'<span class="jobs-card-arrow"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="9 18 15 12 9 6"/></svg></span>';return a;}/*** Clone la promo card depuis le <template> rendu par la macro Twig.* Le HTML est défini une seule fois dans _macros/promo_card.html.twig.* Retourne null si le template n'est pas présent (ex: user connecté).*/function buildPromoCard(){var tpl = document.getElementById('jobs-promo-card-template');if(!tpl || !tpl.content) return null;var node = tpl.content.firstElementChild;return node ? node.cloneNode(true) : null;}function renderEmpty(){listEl.innerHTML ='<div class="jobs-empty">' +'<div class="jobs-empty-icon">' +'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><path d="M21 21l-4.35-4.35"/></svg>' +'</div>' +'<div class="jobs-empty-title">{{ 'job.dashboard.emptyTitle'|trans|escape('js') }}</div>' +'<div class="jobs-empty-text">{{ 'job.dashboard.emptyText'|trans|escape('js') }}</div>' +'</div>';}function setLoadMoreState(state){if(!loadMoreBtn) return;if(state === 'loading'){loadMoreBtn.classList.add('loading');loadMoreBtn.disabled = true;loadMoreLabel.innerHTML = '<span class="jobs-load-spinner"></span> {{ 'job.dashboard.loadingMore'|trans|escape('js') }}';} else {loadMoreBtn.classList.remove('loading');loadMoreBtn.disabled = false;loadMoreLabel.textContent = '{{ 'job.dashboard.loadMore'|trans|escape('js') }}';}}function fetchJobs(query, offset, append){if(isFetching) return;isFetching = true;if(!append){// Nouvelle recherche : on remet à zéro et affiche skeletonlistEl.innerHTML ='<div class="jobs-skeleton">' +Array(5).fill(0).map(function(){return '<div class="jobs-skel-card"><div class="jobs-skel-logo"></div><div class="jobs-skel-info"><div class="jobs-skel-line s1"></div><div class="jobs-skel-line s2"></div></div></div>';}).join('') +'</div>';if(infoEl) infoEl.textContent = '{{ 'job.dashboard.loading'|trans|escape('js') }}';if(loadMoreBtn) loadMoreBtn.style.display = 'none';} else {setLoadMoreState('loading');}var url = SEARCH_API_URL + '?q=' + encodeURIComponent(query || '') + '&locale=' + LOCALE + '&limit=' + PAGE_SIZE + '&offset=' + offset;if(CITY_KEYWORD){ url += '&city=' + encodeURIComponent(CITY_KEYWORD); }// Dashboard sans filtre : on demande explicitement les dernières offresif(!query && !CITY_KEYWORD){ url += '&all=1'; }fetch(url, {headers:{'Accept':'application/json'}}).then(function(r){ return r.ok ? r.json() : null; }).then(function(data){isFetching = false;if(!data){if(!append) renderEmpty();setLoadMoreState('idle');return;}var items = data.items || [];currentTotal = data.total || 0;if(!append){listEl.innerHTML = '';if(items.length === 0){renderEmpty();if(infoEl) infoEl.innerHTML = '<strong>0</strong> {{ 'job.dashboard.resultsCount'|trans|escape('js') }}';if(loadMoreBtn) loadMoreBtn.style.display = 'none';return;}}// Insérer la promo card après la 5ème offre (uniquement au 1er chargement,// si user non connecté et qu'on a au moins 5 offres)var insertPromoAt = (!append && !IS_AUTHENTICATED && items.length >= 5) ? 5 : -1;items.forEach(function(item, idx){listEl.appendChild(buildJobCard(item));if(idx === insertPromoAt - 1){var promoNode = buildPromoCard();if(promoNode) listEl.appendChild(promoNode);}});currentOffset = offset + items.length;if(infoEl){infoEl.innerHTML = '<strong>' + currentTotal + '</strong> {{ 'job.dashboard.resultsCount'|trans|escape('js') }}';}// Afficher / cacher "Charger plus"if(loadMoreBtn){if(currentOffset < currentTotal){loadMoreBtn.style.display = 'flex';} else {loadMoreBtn.style.display = 'none';}setLoadMoreState('idle');}}).catch(function(err){isFetching = false;console.error('[whr-dash] fetch error', err);setLoadMoreState('idle');});}/*** Slugifie un texte côté client. DOIT produire EXACTEMENT le même slug* que la méthode PHP slugify() du JobsController et que le filtre Twig* utilisé pour rendre les chips.*/function slugify(text){if(!text) return '';var t = String(text);// Pré-remplacements sémantiquest = t.replace(/&/g, '-and-').replace(/\+/g, '-plus-').replace(/[œŒ]/g, 'oe').replace(/[æÆ]/g, 'ae');// Translitération accents → ASCII via NFD + strip diacriticst = t.normalize ? t.normalize('NFD').replace(/[\u0300-\u036f]/g, '') : t;t = t.toLowerCase();// Tout ce qui n'est pas a-z 0-9 → tirett = t.replace(/[^a-z0-9]+/g, '-');return t.replace(/^-+|-+$/g, '');}/*** Tente de matcher un texte tapé avec un keyword en BDD (par slug ou label).* Retourne l'objet keyword si trouvé, null sinon.*/function findMatchingKeyword(text){if(!text || !KEYWORDS_DATA || !KEYWORDS_DATA.length) return null;var slug = slugify(text);if(!slug) return null;for(var i = 0; i < KEYWORDS_DATA.length; i++){var kw = KEYWORDS_DATA[i];// Match sur slug exact OU label/searchKeyword exact (case insensitive)if(kw.slug === slug) return kw;if(slugify(kw.searchKeyword) === slug) return kw;}return null;}/*** Au submit : décide où rediriger l'utilisateur en fonction du contexte.* - Si on est sur une ville et le texte matche un keyword BDD → /jobs/{ville}/{slug-kw}* - Sinon si on est sur une ville → /jobs/{ville}?search={texte}* - Sinon (pas de ville) → /jobs?q={texte}* - Si texte vide ET ville définie → /jobs/{ville} (reset)* - Si texte vide ET pas de ville → /jobs (reset)*/function submitSearch(query){query = (query || '').trim();// Reset (recherche vide)if(!query){if(CITY_SLUG && CITY_LANDING_URL_TEMPLATE){window.location.href = CITY_LANDING_URL_TEMPLATE;} else {window.location.href = DASHBOARD_URL;}return;}// Cas 1 : on est sur une villeif(CITY_SLUG && CITY_KEYWORD_URL_TEMPLATE){var matched = findMatchingKeyword(query);if(matched && matched.slug){// Match BDD → URL slug propre /jobs/paris/developpeurwindow.location.href = CITY_KEYWORD_URL_TEMPLATE.replace('kwplaceholder', encodeURIComponent(matched.slug));return;}// Pas de match → fallback ?search=textewindow.location.href = CITY_LANDING_URL_TEMPLATE + '?search=' + encodeURIComponent(query);return;}// Cas 2 : pas de ville → dashboard global avec ?q=window.location.href = DASHBOARD_URL + '?q=' + encodeURIComponent(query);}/*** Lance la search côté UI (sans redirection — utilisée pour les chips et* pour la "Charger plus").*/function startSearch(query){currentQuery = query;currentOffset = 0;currentTotal = 0;fetchJobs(query, 0, false);// Update clear buttonif(clearBtn){if(query) clearBtn.classList.add('visible');else clearBtn.classList.remove('visible');}// Update active chip (uniquement les chips qui sont des <button>, pas les <a>)document.querySelectorAll('button.search-keyword-chip').forEach(function(c){if(c.getAttribute('data-keyword') === query) c.classList.add('active');else c.classList.remove('active');});}// ═══ Submit-only : Entrée déclenche la search ET change l'URL ═══if(input){input.addEventListener('keydown', function(e){if(e.key === 'Enter' || e.keyCode === 13){e.preventDefault();submitSearch(input.value);}});// Affiche bouton clear si query initialeif(input.value.trim()){if(clearBtn) clearBtn.classList.add('visible');}}// Bouton clear : reset complet → redirection vers /jobs ou /jobs/{ville}if(clearBtn){clearBtn.addEventListener('click', function(){submitSearch('');});}// Chips keywords (cas dashboard global, sans contexte ville → comportement local)// En contexte ville, les chips sont des <a href> directs gérés par le navigateur.document.querySelectorAll('button.search-keyword-chip').forEach(function(chip){chip.addEventListener('click', function(){var kw = chip.getAttribute('data-keyword');if(!kw) return;// Sur dashboard global : on submit (redirige vers /jobs?q=kw)submitSearch(kw);});});// Bouton "Charger plus"if(loadMoreBtn){loadMoreBtn.addEventListener('click', function(){if(isFetching) return;fetchJobs(currentQuery, currentOffset, true);});}// ═══ Bouton "Voir plus" + Recherche live des chips ═══/*** Slugify simple côté UI pour matching tolérant aux accents* (ex: "developpeur" matche "Développeur")*/function dashSlug(str){if(!str) return '';var t = String(str);t = t.normalize ? t.normalize('NFD').replace(/[\u0300-\u036f]/g, '') : t;return t.toLowerCase().trim();}document.querySelectorAll('.dash-filter-more-btn').forEach(function(btn){var targetId = btn.getAttribute('data-target');var sectionId = btn.getAttribute('data-section');var container = document.getElementById(targetId);var section = sectionId ? document.getElementById(sectionId) : null;if(!container) return;var label = btn.querySelector('.dash-filter-more-label');if(!label) return;var initial = parseInt(container.getAttribute('data-initial'), 10) || 14;var items = container.querySelectorAll('.dash-filter-item');var searchInput = section ? section.querySelector('.dash-filter-search-input') : null;// 2 états indépendants :// - userExpanded : l'utilisateur a cliqué sur "Voir plus"// - searchActive : l'input contient du texte (auto-expand temporaire)// Mode développé = userExpanded || searchActivevar userExpanded = false;function isEffectivelyExpanded(){var hasSearch = searchInput && searchInput.value.trim().length > 0;return userExpanded || hasSearch;}function applyDisplay(){var filterText = searchInput ? searchInput.value : '';var query = dashSlug(filterText || '');var expanded = isEffectivelyExpanded();var visibleCount = 0;items.forEach(function(it, i){var label = it.getAttribute('data-filter-label') || '';var matches = !query || dashSlug(label).indexOf(query) !== -1;if(expanded){// Mode développé : on affiche tous les items qui matchent la rechercheit.style.display = matches ? '' : 'none';if(matches) visibleCount++;} else {// Mode collapsé : on affiche les N premiers (pas de filtrage)it.style.display = (i < initial) ? '' : 'none';if(i < initial) visibleCount++;}});// Toggle "aucun résultat" (uniquement quand recherche active)if(query && visibleCount === 0){container.classList.add('is-no-results');} else {container.classList.remove('is-no-results');}// Synchroniser le bouton "Voir plus/moins" avec l'état effectifif(expanded){btn.classList.add('is-expanded');label.textContent = label.getAttribute('data-less');} else {btn.classList.remove('is-expanded');label.textContent = label.getAttribute('data-more');}}btn.addEventListener('click', function(){userExpanded = !userExpanded;if(!userExpanded){// Reset input quand l'utilisateur collapse manuellementif(searchInput){ searchInput.value = ''; }// Scroll vers le boutonbtn.scrollIntoView({behavior:'smooth', block:'nearest'});}applyDisplay();});// Recherche live dans les chips quand on tape dans l'input// → auto-expand pendant la recherche, retour à l'état userExpanded quand on videif(searchInput){searchInput.addEventListener('input', function(){applyDisplay();});}});// Premier chargement (vide ou avec query initiale)fetchJobs(currentQuery, 0, false);} catch(err){console.error('[whr-dash] init error', err);}})();</script>{% endblock footerjs %}