|
| 1 | +<template> |
| 2 | + <BlockWrapper |
| 3 | + :block-background-color="backgroundColor" |
| 4 | + :padding-top="96" |
| 5 | + :no-padding-bottom="false" |
| 6 | + :overlaps-next-section="false" |
| 7 | + > |
| 8 | + <div class="widest relative"> |
| 9 | + <ul |
| 10 | + ref="scroller" |
| 11 | + role="list" |
| 12 | + class="no-scrollbar flex w-full snap-x snap-mandatory gap-16 overflow-x-auto scroll-smooth !px-[max(16px,calc((100vw-370px)/2))] pb-40 pt-12 md:!px-[calc((100vw-2*370px-16px)/2)] xl:gap-32 xl:!px-[calc((100vw-3*370px-2*32px)/2)] xl:pt-16 2xl:!px-[calc((100vw-3*370px-2*32px)/2)]" |
| 13 | + :class="{'justify-center': visibleCards > response.allRegions.length }" |
| 14 | + @scroll.passive="calculateStep" |
| 15 | + > |
| 16 | + <li |
| 17 | + v-for="(region) in response.allRegions" |
| 18 | + :key="`card-${region.id}`" |
| 19 | + ref="slides" |
| 20 | + class="aspect-square w-[clamp(320px,370px,calc(100vw-40px))] shrink-0 snap-center snap-always" |
| 21 | + data-region |
| 22 | + > |
| 23 | + <RegionCard |
| 24 | + class="min-w-full" |
| 25 | + :region="region" |
| 26 | + compact |
| 27 | + /> |
| 28 | + </li> |
| 29 | + </ul> |
| 30 | + |
| 31 | + <button |
| 32 | + v-if="activeIndex > 0" |
| 33 | + class="hocus:bg-blue-dark/30 absolute left-32 top-1/2 z-10 -mt-24 hidden size-48 cursor-pointer items-center justify-center rounded bg-blue-dark/20 text-white transition-[background-color] active:bg-blue-dark/40 sm:flex" |
| 34 | + @click="goToPrevious" |
| 35 | + > |
| 36 | + <svg width="16" height="24" fill="none" xmlns="http://www.w3.org/2000/svg"> |
| 37 | + <path |
| 38 | + d="M1 12c0-.66.27-1.3.77-1.73L12.97.43a1.85 1.85 0 0 1 2.6.23c.63.77.56 1.89-.16 2.56l-9.78 8.6a.25.25 0 0 0-.02.35l.02.02 9.77 8.6a1.85 1.85 0 0 1-2.45 2.77L1.77 13.73A2.3 2.3 0 0 1 1 12Z" |
| 39 | + fill="currentColor" |
| 40 | + /> |
| 41 | + </svg> |
| 42 | + </button> |
| 43 | + |
| 44 | + <button |
| 45 | + v-if="activeIndex < response.allRegions.length - visibleCards" |
| 46 | + class="hocus:bg-blue-dark/30 absolute right-32 top-1/2 z-10 -mt-24 hidden size-48 cursor-pointer items-center justify-center rounded bg-blue-dark/20 text-white transition-[background-color] active:bg-blue-dark/40 sm:flex" |
| 47 | + @click="goToNext" |
| 48 | + > |
| 49 | + <svg width="16" height="24" fill="none" xmlns="http://www.w3.org/2000/svg" class="-mr-4 rotate-180"> |
| 50 | + <path |
| 51 | + d="M1 12c0-.66.27-1.3.77-1.73L12.97.43a1.85 1.85 0 0 1 2.6.23c.63.77.56 1.89-.16 2.56l-9.78 8.6a.25.25 0 0 0-.02.35l.02.02 9.77 8.6a1.85 1.85 0 0 1-2.45 2.77L1.77 13.73A2.3 2.3 0 0 1 1 12Z" |
| 52 | + fill="currentColor" |
| 53 | + /> |
| 54 | + </svg> |
| 55 | + </button> |
| 56 | + </div> |
| 57 | + |
| 58 | + <div v-if="visibleCards <= response.allRegions.length && amountOfItems > 1" class="flex flex-col"> |
| 59 | + <div class="relative mx-auto mt-48 flex"> |
| 60 | + <button |
| 61 | + v-for="(_, i) in response.allRegions" |
| 62 | + :key="i" |
| 63 | + class="mx-4 size-8 cursor-pointer rounded bg-blue-dark/10 transition-transform delay-75 after:min-h-[16px] after:min-w-[16px] first:ml-0 last:mr-4" |
| 64 | + :class="{ |
| 65 | + 'scale-0': i >= activeIndex && i < activeIndex + visibleCards, |
| 66 | + 'scale-100': i < activeIndex || i >= activeIndex + visibleCards, |
| 67 | + }" |
| 68 | + @click="() => slideTo(i)" |
| 69 | + /> |
| 70 | + <div class="pointer-events-none absolute h-8 w-full rounded"> |
| 71 | + <div |
| 72 | + class="h-full rounded bg-green transition-all duration-300" |
| 73 | + :style="`margin-left: ${16 * activeIndex - 2}px; width: ${visibleCards - 0.25}rem;`" |
| 74 | + /> |
| 75 | + </div> |
| 76 | + </div> |
| 77 | + </div> |
| 78 | + </BlockWrapper> |
| 79 | +</template> |
| 80 | + |
| 81 | +<script lang="ts" setup> |
| 82 | +import type { AsyncData } from 'nuxt/app' |
| 83 | +import type { Region } from '@/types/dato-models/Region' |
| 84 | +
|
| 85 | +defineProps({ |
| 86 | + data: { |
| 87 | + type: Object, |
| 88 | + required: true |
| 89 | + }, |
| 90 | + index: { |
| 91 | + type: Number, |
| 92 | + required: true |
| 93 | + }, |
| 94 | + backgroundColor: { |
| 95 | + type: String, |
| 96 | + required: true, |
| 97 | + default: 'white' |
| 98 | + } |
| 99 | +}) |
| 100 | +interface AllRegionsResponse { |
| 101 | + allRegions: Region[] |
| 102 | +} |
| 103 | +
|
| 104 | +const { data: { value: response } } = await useGraphqlQuery(`query { |
| 105 | + allRegions(orderBy: _createdAt_ASC) { |
| 106 | + url |
| 107 | + state |
| 108 | + name |
| 109 | + brandName |
| 110 | + subRegion |
| 111 | + id |
| 112 | + mainImage { |
| 113 | + responsiveImage(imgixParams: { fit: max, h: 1000, auto: format }) { |
| 114 | + srcSet |
| 115 | + webpSrcSet |
| 116 | + sizes |
| 117 | + src |
| 118 | + width |
| 119 | + height |
| 120 | + aspectRatio |
| 121 | + alt |
| 122 | + title |
| 123 | + base64 |
| 124 | + } |
| 125 | + } |
| 126 | + _allReferencingCities { |
| 127 | + id |
| 128 | + name |
| 129 | + } |
| 130 | + } |
| 131 | +}`) as AsyncData<AllRegionsResponse, RTCError> |
| 132 | +
|
| 133 | +const allRegions = computed(() => { |
| 134 | + return response.allRegions.filter(x => x.state !== 'hidden') |
| 135 | +}) |
| 136 | +
|
| 137 | +const amountOfItems = computed(() => { |
| 138 | + return allRegions.value.length + 1 |
| 139 | +}) |
| 140 | +
|
| 141 | +const activeIndex = ref(0) |
| 142 | +const scroller = ref<HTMLUListElement | null>(null) |
| 143 | +
|
| 144 | +const scrollerStyles = ref<CSSStyleDeclaration | null>(null) |
| 145 | +const scrollerPaddingLeft = computed(() => parseFloat(scrollerStyles.value?.paddingLeft || '0')) |
| 146 | +const scrollerPaddingRight = computed(() => parseFloat(scrollerStyles.value?.paddingRight || '0')) |
| 147 | +const scrollerGap = computed(() => parseFloat(scrollerStyles.value?.gap || '0')) |
| 148 | +const visibleCards = ref(1) |
| 149 | +
|
| 150 | +function onWindowResize () { |
| 151 | + visibleCards.value = window.innerWidth < 768 ? 1 : window.innerWidth < 1152 ? 2 : 3 |
| 152 | +
|
| 153 | + if (scroller.value) { |
| 154 | + scrollerStyles.value = window.getComputedStyle(scroller.value) |
| 155 | + } |
| 156 | +} |
| 157 | +
|
| 158 | +onMounted(() => { |
| 159 | + onWindowResize() |
| 160 | + window.addEventListener('resize', onWindowResize) |
| 161 | +}) |
| 162 | +
|
| 163 | +onBeforeUnmount(() => { |
| 164 | + window.removeEventListener('resize', onWindowResize) |
| 165 | +}) |
| 166 | +
|
| 167 | +function calculateStep (event: Event) { |
| 168 | + const target = event.target as HTMLDivElement |
| 169 | +
|
| 170 | + const padding = scrollerPaddingLeft.value + scrollerPaddingRight.value |
| 171 | + const gap = scrollerGap.value |
| 172 | + const cards = visibleCards.value |
| 173 | + const cardWidth = (target.offsetWidth - padding) / cards + (gap * 1) / cards |
| 174 | +
|
| 175 | + activeIndex.value = Math.round(target.scrollLeft / cardWidth) |
| 176 | +} |
| 177 | +
|
| 178 | +function slideTo (index: number) { |
| 179 | + // Clamp new index |
| 180 | + index = Math.min(Math.max(0, index), amountOfItems.value - visibleCards.value) |
| 181 | +
|
| 182 | + const card = scroller.value?.querySelectorAll('[data-region]')[index] as HTMLElement |
| 183 | +
|
| 184 | + scroller.value!.scrollTo({ |
| 185 | + top: 0, |
| 186 | + left: card.offsetLeft - scrollerPaddingLeft.value, |
| 187 | + behavior: 'smooth' |
| 188 | + }) |
| 189 | +} |
| 190 | +
|
| 191 | +function goToPrevious () { |
| 192 | + slideTo(activeIndex.value - visibleCards.value) |
| 193 | +} |
| 194 | +
|
| 195 | +function goToNext () { |
| 196 | + slideTo(activeIndex.value + visibleCards.value) |
| 197 | +} |
| 198 | +</script> |
0 commit comments