templates/_snippets/components/cards/list_card.html.twig line 1

Open in your IDE?
  1. {#
  2. Carte croisière (chargée en lazy-loading via /api/cruise/card/list_card/{id}).
  3. Contexte attendu :
  4. - cruise : entité Cruise (obligatoire)
  5. - content : { isFloa: bool } (optionnel)
  6. #}
  7. {% import '_snippets/components/macros/cruise.html.twig' as cruiseMacros %}
  8. {% import '_snippets/components/macros/date.html.twig' as dateMacros %}
  9. {% macro cruise_image_tags(cruise) %}
  10. <div class="cruise-image-tags">
  11. {% for tagCruise in cruise.tagCruises %}
  12. {% set tag = tagCruise.tag %}
  13. {% if tag %}
  14. <div class="tag br1 text-sm-md" style="background-color:{{ tag.colorFill|default('transparent') }}; border:{{ tag.colorStroke ? '1px solid ' ~ tag.colorStroke|default('transparent') : 'none' }}; color:{{ tag.colorFont|default('#000000') }}">
  15. {{ getLucidIcon(tag.icon|default('')) }}
  16. {{ tag.title|default('Tag')|raw }}
  17. </div>
  18. {% endif %}
  19. {% endfor %}
  20. </div>
  21. {% endmacro %}
  22. {% import _self as listCard %}
  23. {% if cruise is defined and cruise is not null %}
  24. <div class="cruise animation3 br3" data-cruise-id="{{ cruise.id }}" data-index="{{ loop.index0|default(0) }}">
  25. <div class="top-section row">
  26. {# ─────────── Colonne images + modales ─────────── #}
  27. <div class="images col-lg-3 col-md-4 col-sm-12">
  28. <div class="thumbnails br2">
  29. {% set shipMedia = crea_media_get_media(['main'], cruise.ship, null, { width: 400 }) %}
  30. <img
  31. src="{{ shipMedia ? shipMedia.cdnUrl : '' }}"
  32. alt="{{ cruise.ship.name|default('') }}"
  33. loading="lazy"
  34. decoding="async"
  35. >
  36. {{ listCard.cruise_image_tags(cruise) }}
  37. </div>
  38. <div class="carousel-wrapper">
  39. <div class="carousel br2">
  40. {{ listCard.cruise_image_tags(cruise) }}
  41. <div class="arrow left-arrow">
  42. {{ getLucidIcon('chevron-left') }}
  43. </div>
  44. <div class="elements">
  45. {% for media in getCruiseStopoversMediaFiles(cruise) %}
  46. {% set cdnMedia = crea_media_get_cdn_media(media, { width: 650 }) %}
  47. <img class="carousel-image" src="{{ cdnMedia.cdnUrl }}" alt="">
  48. {% endfor %}
  49. {# {% for i in 1 .. random(3, 10) %}#}
  50. {# <img class="carousel-image" src="{{ '/img/photos-temporaires/Photos-escales/' ~ random(1, 17) ~ '.jpg' }}" alt="">#}
  51. {# {% endfor %}#}
  52. </div>
  53. <div class="arrow right-arrow">
  54. {{ getLucidIcon('chevron-right') }}
  55. </div>
  56. <div class="carousel-bottom-icons">
  57. <div class="itinerary-icon-wrapper">
  58. <div class="itinerary-icon" data-modal-target="itinerary-modal-{{ cruise.id }}">
  59. {{ getLucidIcon('map-pin') }}
  60. </div>
  61. </div>
  62. <div class="map-icon-wrapper">
  63. {# <div class="map-icon" data-modal-target="map-modal-{{ cruise.id }}">
  64. {{ cruiseMacros.leaflet_map(cruise, { staticMap: true, disableZoom: true }) }}
  65. </div> #}
  66. <div class="map-icon" data-modal-target="map-modal-{{ cruise.id }}">
  67. {{ getLucidIcon('map') }}
  68. </div>
  69. </div>
  70. </div>
  71. </div>
  72. {# ─────────── Modale Itinéraire ─────────── #}
  73. <div class="itinerary-modal" id="itinerary-modal-{{ cruise.id }}">
  74. <div class="h4">{{ 'list.cruise_card.discover_itinerary'|trans }}</div>
  75. <div class="itinerary-table-wrapper">
  76. <table class="itinerary-table">
  77. <thead>
  78. <tr>
  79. <th>{{ 'ddd.summary.date'|trans }}</th>
  80. <th>{{ 'details.itinerary.stopover'|trans }}</th>
  81. <th>{{ 'ddd.summary.arrival'|trans }}</th>
  82. <th>{{ 'ddd.summary.departure'|trans }}</th>
  83. </tr>
  84. </thead>
  85. <tbody>
  86. {% for segment in cruise.itinerary.itinerarySegments|default([]) %}
  87. {% set isSea = segment.segmentType.value == 'day_at_sea' %}
  88. <tr>
  89. <td>{{ getSegmentDateForCruise(cruise, segment)|date('d/m/Y') }}</td>
  90. <td>{{ isSea ? 'Journée en mer' : segment.stopover.name|default('') }}</td>
  91. <td>{{ isSea ? '-' : (segment.arrivalTime is not null ? segment.arrivalTime|date('H:i') : '') }}</td>
  92. <td>{{ isSea ? '-' : (segment.departureTime is not null ? segment.departureTime|date('H:i') : '') }}</td>
  93. </tr>
  94. {% endfor %}
  95. </tbody>
  96. </table>
  97. </div>
  98. </div>
  99. {# ─────────── Modale Carte (Leaflet injecté à l'ouverture) ─────────── #}
  100. {{ cruiseMacros.map_modal(cruise, 'map-modal-' ~ cruise.id) }}
  101. {# <div class="map-modal" id="map-modal-{{ cruise.id }}">
  102. {{ cruiseMacros.leaflet_map(cruise) }}
  103. </div> #}
  104. </div>
  105. </div>
  106. {# ─────────── Colonne contenu ─────────── #}
  107. <div class="content col-lg-6 col-md-4 col-sm-12">
  108. <div class="company-name text-xs-rg color-grey-text">{{ cruise.ship.company.name|default('') }}</div>
  109. <div class="title">
  110. {% set cruiseName = getCruiseName(cruise)|default('') %}
  111. {% if cruiseName is empty %}{% set cruiseName = 'snippets.cruise.title_fallback'|trans %}{% endif %}
  112. {% set MAX_TITLE_LENGTH = 80 %}
  113. {% set isTitleTruncated = cruiseName|length > MAX_TITLE_LENGTH %}
  114. <h2 class="text-lg-bd">
  115. {{ isTitleTruncated ? (cruiseName|slice(0, MAX_TITLE_LENGTH)|trim ~ '…') : cruiseName }}
  116. {% if isTitleTruncated %}
  117. <span class="cruise-title-info" tabindex="0" role="button" aria-label="{{ cruiseName }}">
  118. {{ getLucidIcon('info') }}
  119. <div class="popup popup--cruise-name">
  120. <div class="popup__content">
  121. <div class="popup__title text-sm-md">{{ cruiseName }}</div>
  122. </div>
  123. </div>
  124. </span>
  125. {% endif %}
  126. </h2>
  127. {% if cruise.ship.company is defined %}
  128. <div class="logo_company br1">
  129. <img
  130. src="{{ crea_media_get_media(['logo'], cruise.ship.company).cdnUrl|default('/img/placeholder-company.svg') }}"
  131. alt="{{ 'common.company'|trans }} {{ cruise.ship.company.name|default('Compagnie') }}"
  132. loading="lazy"
  133. decoding="async"
  134. >
  135. </div>
  136. {% endif %}
  137. </div>
  138. <div class="practices_informations text-sm-rg color-grey-text">
  139. <span class="text-sm-bd">
  140. {% if cruise.itinerary.secondaryDestinations is iterable %}
  141. {{ cruise.itinerary.secondaryDestinations.first.secondaryDestination.name|default('') }}
  142. {% else %}
  143. {{ cruise.itinerary.destination|default('') }}
  144. {% endif %}
  145. </span>
  146. {{ 'list.cruise_card.departure_from'|trans }}
  147. <span class="text-sm-bd">{{ get_departure_port(cruise.itinerary).name|default('') }}</span>
  148. <br>
  149. <span class="text-sm-bd">{{ cruise.itinerary.numberOfDays|default(0) }}</span>
  150. {{ 'list.cruise_card.days_on_ship'|trans }}
  151. <span class="text-sm-bd">{{ cruise.ship.name|default('list.placeholder.ship_fallback'|trans) }}</span>
  152. <br>
  153. {{ 'details.main_information.from'|trans|lower }}
  154. <span class="text-sm-bd">{{ cruise.departureDate|date('d/m/Y') }}</span>
  155. {{ 'details.main_information.to'|trans }}
  156. <span class="text-sm-bd">{{ cruise.arrivalDate|date('d/m/Y') }}</span>
  157. </div>
  158. {% if getAdvantagesForCruise(cruise) is iterable %}
  159. <div class="benefits-block">
  160. <div class="subtitle-category">{{ 'details.main_information.onboard_benefits'|trans }}</div>
  161. <div class="benefits br1">
  162. {% set totalBenefits = getAdvantagesForCruise(cruise)|length %}
  163. {% for advantage in getAdvantagesForCruise(cruise)|slice(0, 6) %}
  164. <div class="benefit text-xs-rg">
  165. <span class="icon">{{ getLucidIcon(advantage.icon|default('star'), {'width': '14px', 'height': '14px'}) }}</span>
  166. <span class="text">{{ advantage.name|default(advantage.title|default('')) }}</span>
  167. {% include 'pages/_includes/partials/_benefit_popup.html.twig' with {'benefit': advantage} %}
  168. </div>
  169. {% endfor %}
  170. {% if totalBenefits > 6 %}
  171. <div class="more-benefits">
  172. {% for advantage in getAdvantagesForCruise(cruise)|slice(6) %}
  173. <div class="benefit text-xs-rg">
  174. <span class="icon">{{ getLucidIcon(advantage.icon|default('star'), {'width': '14px', 'height': '14px'}) }}</span>
  175. <span class="text">{{ advantage.name|default(advantage.title|default('')) }}</span>
  176. {% include 'pages/_includes/partials/_benefit_popup.html.twig' with {'benefit': advantage} %}
  177. </div>
  178. {% endfor %}
  179. </div>
  180. <button
  181. type="button"
  182. class="toggle-benefits benefit text-xs-rg"
  183. aria-expanded="false"
  184. data-label-more="+{{ totalBenefits - 6 }}"
  185. data-label-less="{{ 'common.see_less'|trans|e('html_attr') }}"
  186. >+{{ totalBenefits - 6 }}</button>
  187. {% endif %}
  188. </div>
  189. </div>
  190. {% endif %}
  191. {% set cruiseOffers = getCruiseSpecialOffers(cruise) %}
  192. {% if cruiseOffers is not empty %}
  193. <div class="special_offers-block">
  194. <div class="subtitle-category">{{ 'list.cruise_card.promotional_offers'|trans }}</div>
  195. <div class="special_offers">
  196. {% for offer in cruiseOffers %}
  197. {% include 'pages/_includes/partials/_offer_with_popup.html.twig' with { offer: offer } only %}
  198. {% endfor %}
  199. </div>
  200. </div>
  201. {% endif %}
  202. {% set otherDepartures = get_active_cruises(cruise.itinerary) %}
  203. {% if otherDepartures is not empty %}
  204. <div class="other-departures-block">
  205. <div class="other-departures">
  206. <div class="subtitle-category">{{ 'details.cabin_choice.other_departures'|trans }}</div>
  207. <div class="departures-carousel-wrapper">
  208. <div class="arrow left-arrow color-primary-dark br1">
  209. <div class="icon">{{ getLucidIcon('chevron-left') }}</div>
  210. </div>
  211. <div class="departures-carousel-container">
  212. <div class="departures-carousel">
  213. {% for subCruise in otherDepartures %}
  214. <a href="{{ getUrlWithQueryParams(constant('App\\Controller\\Website\\DetailController::BASE_PATH'),{'croisiere': subCruise.id}) }}" class="departure-item br1">
  215. <div class="date text-sm-md color-grey-text">
  216. {% if subCruise.departureDate is defined and subCruise.departureDate is not empty %}
  217. {{ subCruise.departureDate|date('d') }}
  218. {{ dateMacros.short_month_fr(subCruise.departureDate|date('n')|number_format(0, '', '')) ?: subCruise.departureDate|date('M') }}
  219. {{ subCruise.departureDate|date('Y') }}
  220. {% else %}
  221. {{ subCruise.date }}
  222. {% endif %}
  223. </div>
  224. <div class="price text-2xs-bd color-grey">
  225. {{ 'common.from'|trans|lower }}
  226. {# {{ priceLabel }} #}
  227. {{ getCruiseBestPrice(subCruise)|number_format(0, ',', ' ') }} €
  228. </div>
  229. </a>
  230. {% endfor %}
  231. </div>
  232. </div>
  233. <div class="arrow right-arrow color-primary-dark br1">
  234. <div class="icon">{{ getLucidIcon('chevron-right') }}</div>
  235. </div>
  236. </div>
  237. </div>
  238. </div>
  239. {% endif %}
  240. </div>
  241. {# ─────────── Colonne aside (partner card + CTA) ─────────── #}
  242. <div class="aside desktop br2 col-lg-3 col-md-4 col-sm-12">
  243. {% set partner = getCurrentPartner() %}
  244. {% set company = cruise.ship.company|default(null) %}
  245. {% set discountPercent = 0 %}
  246. {% if partner and company %}
  247. {% set discount = get_partner_company_discount(company) %}
  248. {% set discountPercent = discount ? discount.rate|default(0) : 0 %}
  249. {% endif %}
  250. {% if partner and discountPercent > 0 and partner.name|default('') is not empty %}
  251. <div class="partner-card br2 shimmered">
  252. <div class="icon">
  253. {{ getLucidIcon('star') }}
  254. </div>
  255. <div class="tags">
  256. <div class="tag text-xs-bd color-price-tags-primary br1 ">{{ 'details.main_information.partner_tag'|trans }}</div>
  257. <div class="tag text-xs-bd br1">{{ partner.name }}</div>
  258. </div>
  259. <div class="discount color-card-background">
  260. <div class="reduction-value text-4xl-sebd">-{{ discountPercent }}%</div>
  261. <div class="text text-sm-bd">{{ 'details.main_information.exclusive_discount'|trans }}</div>
  262. </div>
  263. <div class=" text text-xs-md br1 color-card-background">
  264. {{ getLucidIcon('check') }}
  265. {{ 'details.main_information.partner_discount_applied'|trans }}
  266. </div>
  267. </div>
  268. {% endif %}
  269. <div class="floa_and_call_to_action">
  270. <div class="price">
  271. <div class="text-sm-md color-grey-text">
  272. {{ cruise.bestPrice.cabinCategory.cabinSubType.name|default('') ?: 'ddd.step.cabin'|trans }}
  273. {# {{ 'ddd.step.cabin'|trans }} #}
  274. {{ 'common.from'|trans|lower }}
  275. </div>
  276. <a href="">
  277. <div class="base br1">
  278. <span class="price">{{ getCruiseBestPrice(cruise)|number_format(0, ',', ' ') }}
  279. €</span>
  280. <span class="text-sm-lt color-secondary-dark">{{ 'common.per_person_short'|trans }}</span>
  281. </div>
  282. </a>
  283. <div class="service-fees">
  284. <div class="label text-3xs-md color-grey-text">
  285. {{ 'list.cruise_card.service_fees_notice'|trans }}
  286. {{ getLucidIcon('info') }}
  287. </div>
  288. <div class="wrapper"></div>
  289. </div>
  290. </div>
  291. <div class="floa text-sm-md color-grey-text">
  292. {% if content.isFloa|default(false) %}
  293. {{ 'list.cruise_card.floa_compatible'|trans }}
  294. <img
  295. src="https://www.croisiland.com/_imgs/floa/4X-bleu.svg?1710340929"
  296. alt="{{ 'list.placeholder.floa_alt'|trans }}"
  297. loading="lazy"
  298. decoding="async"
  299. >
  300. {% endif %}
  301. </div>
  302. <a href="{{ getUrlWithQueryParams(constant('App\\Controller\\Website\\DetailController::BASE_PATH'),{'croisiere': cruise.id}) }}" class="call_to_action btn medium style1 bg-w w-100 text-md-sbd">{{ 'common.see_offers'|trans }}</a>
  303. </div>
  304. </div>
  305. </div>
  306. </div>
  307. {% endif %}