Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
175 changes: 174 additions & 1 deletion app/assets/stylesheets/application.bootstrap.scss
Original file line number Diff line number Diff line change
Expand Up @@ -856,6 +856,177 @@ dialog {
font-size: clamp(1.35rem, 5.6vw, 2.25rem);
}

.home-shop-swiper {
position: relative;
--shop-swiper-card-width: min(28rem, 68vw);
--shop-swiper-offset: min(24rem, 38vw);
--shop-swiper-offset-far: min(40rem, 58vw);
--shop-swiper-progress: 0;
padding: 1.5rem 0 2.5rem;
overflow: visible;
}

.home-shop-swiper.swiper {
overflow: visible;
}

.home-shop-swiper .swiper-wrapper {
position: relative;
}

.home-shop-swiper .parallax-bg {
display: none;
}

.home-shop-swiper .swiper-wrapper {
align-items: stretch;
}

.home-shop-swiper .swiper-slide {
position: absolute;
top: 0;
left: 50%;
width: var(--shop-swiper-card-width);
max-width: 100%;
flex-shrink: 0;
height: auto;
display: flex;
cursor: pointer;
user-select: none;
margin: 0;
transform-origin: center center;
transition: transform 0.85s ease, opacity 0.85s ease, filter 0.85s ease;
opacity: 0;
pointer-events: none;
z-index: 1;
}

.home-shop-swiper .cardPopout {
width: 100%;
display: flex;
border-radius: 1rem;
padding: 0.2rem;
background: linear-gradient(180deg, rgba(255, 255, 255, 0.45), rgba(255, 255, 255, 0.18));
box-shadow: 0 20px 45px rgba(58, 23, 10, 0.12);
transition: transform 0.35s ease, box-shadow 0.35s ease;
}

.home-shop-swiper .swiper-slide-active .cardPopout,
.home-shop-swiper .swiper-slide:hover .cardPopout {
transform: translateY(-6px);
box-shadow: 0 28px 48px rgba(58, 23, 10, 0.18);
}

.home-shop-swiper .swiper-slide.is-active {
opacity: 1;
z-index: 5;
pointer-events: auto;
transform: translateX(-50%) translateZ(0) scale(1);
filter: none;
}

.home-shop-swiper .swiper-slide.is-prev {
opacity: 0.82;
z-index: 4;
pointer-events: auto;
transform: translateX(calc(-50% - var(--shop-swiper-offset))) rotateY(34deg) scale(0.84);
filter: saturate(0.7);
}

.home-shop-swiper .swiper-slide.is-next {
opacity: 0.82;
z-index: 4;
pointer-events: auto;
transform: translateX(calc(-50% + var(--shop-swiper-offset))) rotateY(-34deg) scale(0.84);
filter: saturate(0.7);
}

.home-shop-swiper .swiper-slide.is-prev-2 {
opacity: 0.34;
z-index: 3;
pointer-events: auto;
transform: translateX(calc(-50% - var(--shop-swiper-offset-far))) rotateY(42deg) scale(0.7);
filter: blur(0.4px) saturate(0.5);
}

.home-shop-swiper .swiper-slide.is-next-2 {
opacity: 0.34;
z-index: 3;
pointer-events: auto;
transform: translateX(calc(-50% + var(--shop-swiper-offset-far))) rotateY(-42deg) scale(0.7);
filter: blur(0.4px) saturate(0.5);
}

.home-shop-swiper .swiper-slide.is-hidden-left {
transform: translateX(calc(-50% - var(--shop-swiper-offset-far) - 8rem)) scale(0.62);
}

.home-shop-swiper .swiper-slide.is-hidden-right {
transform: translateX(calc(-50% + var(--shop-swiper-offset-far) + 8rem)) scale(0.62);
}

.home-shop-swiper__product-card {
margin-bottom: 0;
width: 100%;
}

.home-shop-swiper .swiper-slide-shadow-left,
.home-shop-swiper .swiper-slide-shadow-right {
border-radius: 1rem;
}

.home-shop-swiper .swiper-scrollbar {
position: relative;
width: min(21rem, 80%);
height: 10px;
margin-inline: auto;
margin-top: 1.2rem;
background: rgba(113, 28, 28, 0.12);
border-radius: 999px;
overflow: hidden;
}

.home-shop-swiper .swiper-scrollbar-drag {
background: linear-gradient(90deg, rgba(113, 28, 28, 0.9), rgba(199, 176, 126, 0.9));
}

.home-shop-swiper .swiper-scrollbar::before {
content: "";
position: absolute;
inset: 0 auto 0 0;
width: max(18%, 3.75rem);
border-radius: inherit;
background: linear-gradient(90deg, rgba(113, 28, 28, 0.9), rgba(199, 176, 126, 0.9));
transform: translateX(calc(var(--shop-swiper-progress) * (100% - 3.75rem)));
transition: transform 0.45s ease;
}

@media (max-width: 767.98px) {
.home-shop-swiper {
--shop-swiper-card-width: min(24rem, 90vw);
--shop-swiper-offset: min(10rem, 28vw);
--shop-swiper-offset-far: min(16rem, 40vw);
padding-inline: 0.2rem;
}

.home-shop-swiper .parallax-bg {
inset-inline: 0;
}

.home-shop-swiper .swiper-slide.is-prev,
.home-shop-swiper .swiper-slide.is-next {
opacity: 0.2;
}

.home-shop-swiper .swiper-slide.is-prev-2,
.home-shop-swiper .swiper-slide.is-next-2,
.home-shop-swiper .swiper-slide.is-hidden-left,
.home-shop-swiper .swiper-slide.is-hidden-right {
opacity: 0;
pointer-events: none;
}
}

/* Fallback dynamique du formulaire admin Home, même sans JS Stimulus */
.home-block-form:has(select[name="home_page[bloc_type]"] option[value="custom"]:checked)
[data-home-block-form-target="targetSection"],
Expand Down Expand Up @@ -1134,7 +1305,9 @@ dialog {
}

.product-card-link:hover .product-card-clickable,
.product-card-link:focus-visible .product-card-clickable {
.product-card-link:focus-visible .product-card-clickable,
.home-shop-swiper .swiper-slide:hover .product-card-clickable,
.home-shop-swiper .swiper-slide:focus-within .product-card-clickable {
border-color: rgba(113, 28, 28, 0.85);
box-shadow: 0 12px 28px rgba(113, 28, 28, 0.2);
transform: translateY(-3px);
Expand Down
126 changes: 126 additions & 0 deletions app/javascript/controllers/shop_swiper_controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
static targets = ["wrapper", "slide", "scrollbar"]

connect() {
this.activeIndex = 0
this.autoplayDelay = 3200
this.boundRefresh = this.refresh.bind(this)

window.addEventListener("resize", this.boundRefresh)
this.observeImages()
this.refresh()
this.resume()
}

disconnect() {
window.removeEventListener("resize", this.boundRefresh)
this.pause()
}

next(event) {
if (event) event.preventDefault()
if (this.slideTargets.length <= 1) return

this.activeIndex = (this.activeIndex + 1) % this.slideTargets.length
this.update()
}

previous(event) {
if (event) event.preventDefault()
if (this.slideTargets.length <= 1) return

this.activeIndex = (this.activeIndex - 1 + this.slideTargets.length) % this.slideTargets.length
this.update()
}

advanceFromClick(event) {
if (event.button !== 0) return
if (event.target.closest("a, button")) return

this.next()
}

wheel(event) {
if (Math.abs(event.deltaY) < 8) return

event.preventDefault()
if (event.deltaY > 0) {
this.next()
} else {
this.previous()
}
}

pause() {
if (!this.autoplayTimer) return

clearInterval(this.autoplayTimer)
this.autoplayTimer = null
}

resume() {
this.pause()
if (this.slideTargets.length <= 1) return

this.autoplayTimer = setInterval(() => this.next(), this.autoplayDelay)
}

refresh() {
const tallestSlide = this.slideTargets.reduce((maxHeight, slide) => {
return Math.max(maxHeight, slide.offsetHeight)
}, 0)

if (tallestSlide > 0) {
this.wrapperTarget.style.height = `${tallestSlide}px`
}

this.update()
}

update() {
const total = this.slideTargets.length
if (total === 0) return

this.slideTargets.forEach((slide, index) => {
slide.classList.remove(
"is-active",
"is-prev",
"is-next",
"is-prev-2",
"is-next-2",
"is-hidden-left",
"is-hidden-right"
)

const offset = index - this.activeIndex

if (offset === 0) {
slide.classList.add("is-active")
} else if (offset === -1) {
slide.classList.add("is-prev")
} else if (offset === 1) {
slide.classList.add("is-next")
} else if (offset === -2) {
slide.classList.add("is-prev-2")
} else if (offset === 2) {
slide.classList.add("is-next-2")
} else if (offset < 0) {
slide.classList.add("is-hidden-left")
} else {
slide.classList.add("is-hidden-right")
}
})

const progress = total > 1 ? this.activeIndex / (total - 1) : 0
this.scrollbarTarget.style.setProperty("--shop-swiper-progress", progress)
}

observeImages() {
this.element.querySelectorAll("img").forEach((image) => {
if (image.complete) return
image.addEventListener("load", this.boundRefresh, { once: true })
})
}
}
6 changes: 6 additions & 0 deletions app/views/home_pages/_shop_product_card.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<div class="card h-100 shadow-sm product-card-clickable home-shop-swiper__product-card">
<%= render product %>
<div class="px-2 pb-2 mt-auto d-flex justify-content-end">
<%= link_to "Voir plus", product_path(product), class: "btn btn-primary btn-sm" %>
</div>
</div>
66 changes: 24 additions & 42 deletions app/views/home_pages/index.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -73,49 +73,31 @@
<h2 class="text-center mb-4"><%= home_page.title %></h2>
<% products = home_page.shop_products.to_a %>
<% if products.any? %>
<% product_count = products.size %>
<% grid_classes =
if product_count == 1
"row row-cols-1 justify-content-center g-3"
elsif product_count == 2
"row row-cols-1 row-cols-md-2 g-3"
else
"row row-cols-1 row-cols-md-2 row-cols-lg-3 row-cols-xl-4 g-3"
end %>
<div class="<%= grid_classes %>">
<% products.each do |product| %>
<% column_classes = product_count == 1 ? "col-12 col-md-8 col-lg-6 col-xl-5" : "col" %>
<div class="<%= column_classes %>">
<div class="card h-100 shadow-sm">
<% if product.primary_image.present? %>
<div class="card-img p-0 product-card-media-frame">
<%= attachment_thumb(product.primary_image,
variant_options: { resize_to_limit: [700, 520] },
alt: product.title,
class: "w-100 h-100 product-card-media-image") %>
</div>
<% end %>
<div class="card-body d-flex flex-column">
<% if product.category.present? %>
<p class="gold"><strong><%= product.category %></strong></p>
<% end %>
<h5 class="card-title"><%= product.title %></h5>
<% if product.description.present? %>
<p class="card-text"><%= truncate(product.description, length: 120) %></p>
<% end %>
<% if product.price.present? %>
<div class="d-flex justify-content-between align-items-center mt-auto pt-3">
<h5 class="gold mb-0">
<%= number_to_currency(product.price, unit: "€", format: "%n %u") %>
</h5>
<%= link_to "Voir le produit", product_path(product), class: "btn btn-primary btn-sm" %>
</div>
<% end %>
<section class="home-shop-swiper swiper"
data-controller="shop-swiper"
data-action="mouseenter->shop-swiper#pause mouseleave->shop-swiper#resume wheel->shop-swiper#wheel">
<div class="home-shop-swiper__parallax parallax-bg"
data-swiper-parallax="600"
data-swiper-parallax-scale="0.88"></div>

<div class="swiper-wrapper" data-shop-swiper-target="wrapper">
<% products.each do |product| %>
<figure class="swiper-slide"
data-shop-swiper-target="slide"
data-action="click->shop-swiper#advanceFromClick">
<div class="cardPopout home-shop-swiper__card"
data-swiper-parallax="30"
data-swiper-parallax-scale="0.94"
data-swiper-parallax-opacity="0.85"
data-swiper-parallax-duration="900">
<%= render "home_pages/shop_product_card", product: product %>
</div>
</div>
</div>
<% end %>
</div>
</figure>
<% end %>
</div>

<div class="swiper-scrollbar" data-shop-swiper-target="scrollbar"></div>
</section>
<div class="text-center mt-3">
<% button_label = home_page.button_label.presence || "Accéder à la boutique" %>
<%= link_to button_label, products_path, class: "btn btn-primary" %>
Expand Down
Loading
Loading