[wip] Rework Shop

- Change strategy for content fetching
- Add a route per page instead of using __layout for all
- Change the behaviors of the posters section (add a carousel on small screens)
- Change Poster buttons styling and make interactions only for desktop
This commit is contained in:
2021-12-14 23:23:12 +01:00
parent 140e8f1fa2
commit 82f5dd4727
17 changed files with 808 additions and 364 deletions

View File

@@ -9,7 +9,7 @@
onMount(async () => {
// Disable when changing filters on /photos and shop?
if (!$page.query.get('country') && !$page.path.includes('/shop/poster')) {
if (!$page.query.get('country') && !$page.path.includes('/shop')) {
await tick()
setTimeout(() => {
// Scroll back to top between page transitions

View File

@@ -3,8 +3,10 @@
import { addToCart } from '$utils/functions/shop'
import { capitalizeFirstLetter } from '$utils/functions'
// Components
import SplitText from '$components/SplitText.svelte'
import Button from '$components/atoms/Button.svelte'
import Image from '$components/atoms/Image.svelte'
import ScrollingTitle from '$components/atoms/ScrollingTitle.svelte'
import Carousel from '$components/organisms/Carousel.svelte'
export let product: any
@@ -60,9 +62,11 @@
</script>
<section class="poster-layout grid" id="poster">
<h2 class="title-huge">
{product.location.name}
</h2>
<div class="poster-layout__title">
<ScrollingTitle tag="h2" label={product.location.name}>
<SplitText mode="chars" text={product.location.name} />
</ScrollingTitle>
</div>
<aside class="poster-layout__buy">
<div class="poster-layout__info">

View File

@@ -33,11 +33,11 @@
text="View"
/>
<Button
type="button"
text="Add to cart"
color="pinklight"
tag="button"
size="xsmall"
on:click={() => addToCart($cartId, product)}
text="Add to cart"
color="pink"
/>
</div>
</div>

View File

@@ -1,10 +1,12 @@
<script lang="ts">
import { page } from '$app/stores'
import { goto } from '$app/navigation'
import { shopLocations } from '$utils/stores/shop'
import { getContext } from 'svelte'
export let isOver: boolean = false
const { shopLocations } = getContext('shop')
const classes = [
'shop-locationswitcher',
isOver && 'is-over',
@@ -26,7 +28,7 @@
<use xlink:href="#icon-map-pin" />
</svg>
<select on:change={quickLocationChange}>
{#each $shopLocations as { name, slug }}
{#each shopLocations as { name, slug }}
<option value={slug} selected={slug === $page.params.name}>{name}</option>
{/each}
</select>

View File

@@ -0,0 +1,115 @@
<script lang="ts">
import { getContext, onMount } from 'svelte'
import EmblaCarousel, { EmblaCarouselType } from 'embla-carousel'
// Components
import Poster from '$components/molecules/Poster.svelte'
import EmailForm from '$components/molecules/EmailForm.svelte'
import { debounce } from '$utils/functions'
export let posters: any = []
let innerWidth: number
let carouselEl: HTMLElement
let carousel: EmblaCarouselType
let currentSlide = 0
let carouselDots = []
const { shopProducts } = getContext('shop')
/** Navigate to specific slide */
const goToSlide = (index: number = 0) => {
carousel.scrollTo(index)
}
/** Init Carousel */
const initCarousel = () => {
if (innerWidth < 992) {
if (!carousel) {
carousel = EmblaCarousel(carouselEl, {
slidesToScroll: innerWidth < 550 ? 1 : 2,
})
// On init
carousel.on('init', () => {
// Define amounts of dots
carouselDots = carousel.scrollSnapList()
})
// On slide change
carousel.on('select', () => {
// Define current slide
currentSlide = carousel.selectedScrollSnap()
})
// On resize
carousel.on('resize', () => {
// Redefine options
carousel.reInit({
slidesToScroll: innerWidth < 550 ? 1 : 2
})
// Define amounts of dots
carouselDots = carousel.scrollSnapList()
// Define current slide
currentSlide = carousel.selectedScrollSnap()
})
}
} else {
if (carousel) {
carousel.destroy()
carousel = undefined
}
}
}
/** Destroy carousel for larger screens */
const handleResize = debounce(initCarousel, 200)
onMount(() => {
initCarousel()
// Destroy
return () => {
if (carousel) {
carousel.destroy()
}
}
})
</script>
<svelte:window
bind:innerWidth
on:resize={handleResize}
/>
{#if posters}
<section class="shop-page__posters grid">
<h3>View all of our available posters</h3>
<div class="set" bind:this={carouselEl}>
<div class="set__content">
{#each posters as { location, photos_product }}
<Poster
location={location}
image={photos_product.length && photos_product[1].directus_files_id}
product={shopProducts.find(item => item.slug.includes(location.slug))}
/>
{/each}
</div>
{#if carousel}
<ul class="set__dots">
{#each carouselDots as _, index}
<li class:is-active={index === currentSlide}>
<button on:click={() => goToSlide(index)} aria-label="Go to slide #{index + 1}" />
</li>
{/each}
</ul>
{/if}
</div>
<div class="subscribe">
<label class="subscribe__text" for="SUB_EMAIL">Subscribe to be notified when new posters become available</label>
<EmailForm />
</div>
</section>
{/if}

View File

@@ -3,7 +3,6 @@
import { navigating, page } from '$app/stores'
import { onMount, setContext } from 'svelte'
import { pageLoading, previousPage } from '$utils/stores'
import { scrollToTop } from '$utils/functions'
import { DURATION } from '$utils/contants'
import '$utils/polyfills'
// Components

View File

@@ -1,22 +1,199 @@
<script lang="ts">
import { browser } from '$app/env'
import { getContext, onMount } from 'svelte'
import anime from 'animejs'
import type { AnimeTimelineInstance } from 'animejs'
import { cartOpen } from '$utils/stores/shop'
import { capitalizeFirstLetter } from '$utils/functions'
// Components
import PageTransition from '$components/PageTransition.svelte'
import Metas from '$components/Metas.svelte'
import Image from '$components/atoms/Image.svelte'
import SiteTitle from '$components/atoms/SiteTitle.svelte'
import ButtonCart from '$components/atoms/ButtonCart.svelte'
import ShopLocationSwitcher from '$components/molecules/ShopLocationSwitcher.svelte'
import PostersGrid from '$components/organisms/PostersGrid.svelte'
import PosterLayout from '$components/layouts/PosterLayout.svelte'
export let product: any
export let shopProduct: any
const { shop, shopLocations, posters } = getContext('shop')
let introEl: HTMLElement
let navObserver: IntersectionObserver
let scrolledPastIntro = false
/**
* Transition: Anime timeline
*/
let timeline: AnimeTimelineInstance
if (browser) {
requestAnimationFrame(() => {
timeline = anime.timeline({
duration: 1600,
easing: 'easeOutQuart',
autoplay: false,
})
// Hero image
timeline.add({
targets: '.shop-page__background',
scale: [1.06, 1],
opacity: [0, 1],
duration: 2400,
}, 400)
// Intro top elements
timeline.add({
targets: '.shop-page__intro .top > *',
translateY: [-100, 0],
delay: anime.stagger(250),
}, 400)
// Intro navbar
timeline.add({
targets: '.shop-page__nav .container > *',
translateY: [100, 0],
delay: anime.stagger(250),
}, 700)
})
}
onMount(() => {
// Reveal the nav past the Intro
navObserver = new IntersectionObserver(entries => {
entries.forEach(entry => {
scrolledPastIntro = !entry.isIntersecting
})
}, {
threshold: 0,
rootMargin: '0px 0px 0px'
})
navObserver.observe(introEl)
// Transition in
requestAnimationFrame(() => {
timeline.play()
})
// Destroy
return () => {
navObserver && navObserver.unobserve(introEl)
}
})
</script>
<PosterLayout
product={product}
shopProduct={shopProduct}
<Metas
title="{product.location.name} {capitalizeFirstLetter(product.type)} Houses Of Shop"
description=""
image=""
/>
<PageTransition name="shop-page">
<section class="shop-page__intro" bind:this={introEl}>
<div class="top container">
<a href="/" class="back" sveltekit:noscroll>
<svg width="5" height="8" viewBox="0 0 5 8" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4 1 1 4l3 3" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<span>Back to Houses Of</span>
</a>
<span class="shop-title">Shop</span>
</div>
<SiteTitle
variant="inline"
/>
<nav class="shop-page__nav">
<div class="container">
<p class="text-label">Shop your city</p>
<nav>
<ul>
{#each shopLocations as { name, slug }}
<li class:is-active={slug === product.location.slug}>
<a href="/shop/poster-{slug}" sveltekit:prefetch sveltekit:noscroll>
{name}
</a>
</li>
{/each}
</ul>
</nav>
<ButtonCart />
</div>
</nav>
<Image
class="shop-page__background"
id={shop.page_heroimage.id}
alt="photo"
sizeKey="hero"
sizes={{
large: { width: 1800, height: 1200 },
medium: { width: 1200, height: 800 },
small: { width: 700, height: 700 },
}}
/>
</section>
<nav class="shop-location"
class:is-visible={scrolledPastIntro}
class:is-overlaid={$cartOpen}
>
<ShopLocationSwitcher />
<ButtonCart />
</nav>
<section class="shop-page__about grid">
<div class="description grid text-normal">
<div class="description__inner">
{@html shop.about}
</div>
</div>
</section>
<PosterLayout
product={product}
shopProduct={shopProduct}
/>
<PostersGrid {posters} />
</PageTransition>
<script context="module" lang="ts">
export async function load ({ page, fetch, session, stuff }) {
// Get content from stuff
let shopProduct: any
// Get a random product from the API
const productAPI = stuff.posters.find(poster => poster.location.slug === page.params.name)
// Fetch this product on Swell API
if (productAPI) {
const shopProductRes = await fetch('/api/swell', {
method: 'POST',
body: JSON.stringify({
action: 'getProduct',
productId: productAPI.product_id,
})
})
if (shopProductRes.ok) {
shopProduct = await shopProductRes.json()
}
}
return {
props: {
product: stuff.product,
shopProduct: stuff.shopProduct,
product: productAPI,
shopProduct,
}
}
}

View File

@@ -1,215 +1,47 @@
<script lang="ts">
import { browser } from '$app/env'
import { onMount } from 'svelte'
import { shopLocations, cartOpen, cartNotifications } from '$utils/stores/shop'
import anime from 'animejs'
import type { AnimeTimelineInstance } from 'animejs'
import { setContext } from 'svelte'
import { cartNotifications } from '$utils/stores/shop'
// Components
import Metas from '$components/Metas.svelte'
import PageTransition from '$components/PageTransition.svelte'
import SiteTitle from '$components/atoms/SiteTitle.svelte'
import Image from '$components/atoms/Image.svelte'
import ButtonCart from '$components/atoms/ButtonCart.svelte'
import Poster from '$components/molecules/Poster.svelte'
import NotificationCart from '$components/molecules/NotificationCart.svelte'
import EmailForm from '$components/molecules/EmailForm.svelte'
import ShopLocationSwitcher from '$components/molecules/ShopLocationSwitcher.svelte'
import Cart from '$components/organisms/Cart.svelte'
import NotificationCart from '$components/molecules/NotificationCart.svelte'
export let shop: any
export let locations: any
export let posters: any
export let product: any
export let shopProducts: any
let introEl: HTMLElement
let navObserver: IntersectionObserver
let scrolledPastIntro = false
// Locations with an existing poster product
$shopLocations = locations.filter(({ slug }: any) => {
const shopLocations = locations.filter(({ slug }: any) => {
if (posters.find((poster: any) => poster.location.slug === slug)) {
return true
}
})
/**
* Transition: Anime timeline
*/
let timeline: AnimeTimelineInstance
if (browser) {
requestAnimationFrame(() => {
timeline = anime.timeline({
duration: 1600,
easing: 'easeOutQuart',
autoplay: false,
})
// Hero image
timeline.add({
targets: '.shop-page__background',
scale: [1.06, 1],
opacity: [0, 1],
duration: 2400,
}, 400)
// Intro top elements
timeline.add({
targets: '.shop-page__intro .top > *',
translateY: [-100, 0],
delay: anime.stagger(250),
}, 400)
// Intro navbar
timeline.add({
targets: '.shop-page__nav .container > *',
translateY: [100, 0],
delay: anime.stagger(250),
}, 700)
})
}
onMount(() => {
// Reveal the nav past the Intro
navObserver = new IntersectionObserver(entries => {
entries.forEach(entry => {
scrolledPastIntro = !entry.isIntersecting
})
}, {
threshold: 0,
rootMargin: '0px 0px 0px'
})
navObserver.observe(introEl)
// Transition in
requestAnimationFrame(() => {
timeline.play()
})
// Destroy
return () => {
navObserver && navObserver.unobserve(introEl)
}
setContext('shop', {
shop,
posters,
shopLocations,
shopProducts,
})
</script>
<Metas
title="Shop Houses Of"
description=""
image=""
/>
<PageTransition name="shop-page">
<Cart />
<section class="shop-page__intro" bind:this={introEl}>
<div class="top container">
<a href="/" class="back" sveltekit:noscroll>
<svg width="5" height="8" viewBox="0 0 5 8" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4 1 1 4l3 3" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<span>Back to site</span>
</a>
<span class="shop-title">Shop</span>
</div>
<SiteTitle
variant="inline"
<Cart />
<div class="cart-notifications">
{#each $cartNotifications as { id, title, name, image } (id)}
<NotificationCart
title={title}
name={name}
image={image}
/>
{/each}
</div>
<nav class="shop-page__nav">
<div class="container">
<p class="text-label">Shop your city</p>
<nav>
<ul>
{#each $shopLocations as { name, slug }}
<li class:is-active={slug === product.location.slug}>
<a href="/shop/poster-{slug}" sveltekit:prefetch sveltekit:noscroll>
{name}
</a>
</li>
{/each}
</ul>
</nav>
<ButtonCart />
</div>
</nav>
<Image
class="shop-page__background"
id={shop.page_heroimage.id}
alt="photo"
sizeKey="hero"
sizes={{
large: { width: 1800, height: 1200 },
medium: { width: 1200, height: 800 },
small: { width: 700, height: 700 },
}}
/>
</section>
<nav class="shop-location"
class:is-visible={scrolledPastIntro}
class:is-overlaid={$cartOpen}
>
<ShopLocationSwitcher />
<ButtonCart />
<div class="shop-location__notifications">
{#each $cartNotifications as { id, title, name, image } (id)}
<NotificationCart
title={title}
name={name}
image={image}
/>
{/each}
</div>
</nav>
<section class="shop-page__about grid">
<div class="description grid text-normal">
<div class="description__inner">
{@html shop.about}
</div>
</div>
</section>
{#key product}
<slot />
{/key}
{#if posters}
<section class="shop-page__posters grid">
<h3>View all of our available posters</h3>
<div class="set">
{#each posters as { location, photos_product }}
<Poster
location={location}
image={photos_product.length && photos_product[1].directus_files_id}
product={shopProducts.find(item => item.slug.includes(location.slug))}
/>
{/each}
</div>
<div class="subscribe">
<label class="subscribe__text" for="SUB_EMAIL">Subscribe to be notified when new posters become available</label>
<EmailForm />
</div>
</section>
{/if}
</PageTransition>
<slot />
<script context="module" lang="ts">
import { fetchAPI } from '$utils/api'
import { getRandomElement } from '$utils/functions'
export async function load ({ page, fetch, session, stuff }) {
// Get content from API
@@ -259,24 +91,13 @@
}
`)
const { data } = res
/**
* Define product
*/
const productAPI = (!page.params.type && !page.params.name)
// Get a random product
? getRandomElement(data.posters)
// Get the current product from slug
: data.posters.find(({ location }: any) => location.slug === page.params.name)
const { data: { shop, location, posters } } = res
/**
* Get products data from Swell
*/
let shopProducts: any
let shopProduct: any
const shopProductRes = await fetch('/api/swell', {
method: 'POST',
@@ -291,27 +112,20 @@
if (results) {
shopProducts = results
}
// Define current product
const product = results.find((result: any) => result.slug.includes(productAPI.location.slug))
if (product) {
shopProduct = product
}
}
return {
props: {
shop: data.shop,
locations: data.location,
posters: data.posters,
product: productAPI,
shop,
locations: location,
posters,
shopProducts,
shopProduct,
},
stuff: {
product: productAPI,
shopProduct,
shop,
posters,
shopProducts,
}
}
}

View File

@@ -1,22 +1,201 @@
<script lang="ts">
import { browser } from '$app/env'
import { getContext, onMount } from 'svelte'
import anime from 'animejs'
import type { AnimeTimelineInstance } from 'animejs'
import { cartOpen } from '$utils/stores/shop'
// Components
import PageTransition from '$components/PageTransition.svelte'
import Metas from '$components/Metas.svelte'
import Image from '$components/atoms/Image.svelte'
import SiteTitle from '$components/atoms/SiteTitle.svelte'
import ButtonCart from '$components/atoms/ButtonCart.svelte'
import ShopLocationSwitcher from '$components/molecules/ShopLocationSwitcher.svelte'
import PostersGrid from '$components/organisms/PostersGrid.svelte'
import PosterLayout from '$components/layouts/PosterLayout.svelte'
export let product: any
export let shopProduct: any
const { shop, shopLocations, posters } = getContext('shop')
let introEl: HTMLElement
let navObserver: IntersectionObserver
let scrolledPastIntro = false
/**
* Transition: Anime timeline
*/
let timeline: AnimeTimelineInstance
if (browser) {
requestAnimationFrame(() => {
timeline = anime.timeline({
duration: 1600,
easing: 'easeOutQuart',
autoplay: false,
})
// Hero image
timeline.add({
targets: '.shop-page__background',
scale: [1.06, 1],
opacity: [0, 1],
duration: 2400,
}, 400)
// Intro top elements
timeline.add({
targets: '.shop-page__intro .top > *',
translateY: [-100, 0],
delay: anime.stagger(250),
}, 400)
// Intro navbar
timeline.add({
targets: '.shop-page__nav .container > *',
translateY: [100, 0],
delay: anime.stagger(250),
}, 700)
})
}
onMount(() => {
// Reveal the nav past the Intro
navObserver = new IntersectionObserver(entries => {
entries.forEach(entry => {
scrolledPastIntro = !entry.isIntersecting
})
}, {
threshold: 0,
rootMargin: '0px 0px 0px'
})
navObserver.observe(introEl)
// Transition in
requestAnimationFrame(() => {
timeline.play()
})
// Destroy
return () => {
navObserver && navObserver.unobserve(introEl)
}
})
</script>
<PosterLayout
product={product}
shopProduct={shopProduct}
<Metas
title="Shop Houses Of"
description=""
image=""
/>
<PageTransition name="shop-page">
<section class="shop-page__intro" bind:this={introEl}>
<div class="top container">
<a href="/" class="back" sveltekit:noscroll>
<svg width="5" height="8" viewBox="0 0 5 8" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4 1 1 4l3 3" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<span>Back to Houses Of</span>
</a>
<span class="shop-title">Shop</span>
</div>
<SiteTitle
variant="inline"
/>
<nav class="shop-page__nav">
<div class="container">
<p class="text-label">Shop your city</p>
<nav>
<ul>
{#each shopLocations as { name, slug }}
<li class:is-active={slug === product.location.slug}>
<a href="/shop/poster-{slug}" sveltekit:prefetch sveltekit:noscroll>
{name}
</a>
</li>
{/each}
</ul>
</nav>
<ButtonCart />
</div>
</nav>
<Image
class="shop-page__background"
id={shop.page_heroimage.id}
alt="photo"
sizeKey="hero"
sizes={{
large: { width: 1800, height: 1200 },
medium: { width: 1200, height: 800 },
small: { width: 700, height: 700 },
}}
/>
</section>
<nav class="shop-location"
class:is-visible={scrolledPastIntro}
class:is-overlaid={$cartOpen}
>
<ShopLocationSwitcher />
<ButtonCart />
</nav>
<section class="shop-page__about grid">
<div class="description grid text-normal">
<div class="description__inner">
{@html shop.about}
</div>
</div>
</section>
<PosterLayout
product={product}
shopProduct={shopProduct}
/>
<PostersGrid {posters} />
</PageTransition>
<script context="module" lang="ts">
import { getRandomElement } from '$utils/functions'
export async function load ({ page, fetch, session, stuff }) {
// Get content from stuff
let shopProduct: any
// Get a random product from the API
const productAPI = getRandomElement(stuff.posters)
// Fetch this product on Swell API
if (productAPI) {
const shopProductRes = await fetch('/api/swell', {
method: 'POST',
body: JSON.stringify({
action: 'getProduct',
productId: productAPI.product_id,
})
})
if (shopProductRes.ok) {
shopProduct = await shopProductRes.json()
}
}
return {
props: {
product: stuff.product,
shopProduct: stuff.shopProduct,
product: productAPI,
shopProduct,
}
}
}

View File

@@ -6,12 +6,22 @@
}
// Title Location
h2 {
&__title {
overflow: hidden;
width: auto;
grid-column: 1 / span var(--columns);
display: flex;
}
h2 {
display: block;
white-space: nowrap;
margin-bottom: 16px;
font-size: clamp(200px, 20vw, 340px);
color: $color-secondary;
margin-bottom: 40px;
@include bp (mob-lg) {
margin-bottom: 40px;
}
@include bp (sm) {
grid-column: 2 / span calc(var(--columns) - 1);
margin-bottom: 88px;
@@ -36,14 +46,25 @@
// Product Info
&__info {
display: flex;
justify-content: space-between;
margin-bottom: 32px;
@include bp (mob-lg) {
display: flex;
justify-content: space-between;
}
@include bp (sm) {
margin-bottom: 56px;
}
dl {
margin-bottom: 24px;
@include bp (mob-lg) {
flex: 1;
margin-bottom: 0;
padding-right: 16px;
}
}
// Title
dt {
color: $color-secondary;
@@ -51,16 +72,16 @@
font-size: rem(36px);
line-height: 1.3;
}
// Text
dd {
font-size: rem(16px);
color: $color-gray;
margin-top: 8px;
max-width: 140px;
font-size: rem(14px);
line-height: 1.4;
color: $color-gray;
@include bp (sm) {
max-width: none;
font-size: rem(16px);
}
}
}
@@ -97,22 +118,27 @@
&--1 {
grid-column: 1 / span 7;
@include bp (sm) {
grid-column: 1 / span 7;
}
}
&--2 {
grid-column: 2 / span 5;
grid-column: 2 / span 7;
margin: 32px 0;
@include bp (sm) {
grid-column: 2 / span 7;
margin: 48px 0;
}
}
&--3 {
grid-column: 4 / span 5;
grid-column: 1 / span 6;
z-index: 10;
margin-bottom: -64px;
@include bp (sm) {
grid-column: 5 / span 5;
grid-column: 5 / span 8;
margin-bottom: 0;
}
@@ -122,11 +148,13 @@
}
&--4 {
grid-column: 1 / span 8;
margin-bottom: 56px;
margin: -56px 20px 20px;
@include bp (sm) {
grid-column: 8 / span 13;
grid-column: 5 / span 15;
margin-bottom: 120px;
margin-left: 0;
margin-right: 0;
}
}
}
@@ -134,8 +162,8 @@
// About
&__about {
background-color: #fff;
padding: 144px 0 72px;
margin: -100px 0 -120px;
padding: 144px 0 112px;
margin: -24px 0 0;
font-size: rem(36px);
font-weight: 300;
color: $color-primary-tertiary20;
@@ -152,7 +180,7 @@
max-width: 768px;
margin-left: 0;
margin-right: 0;
font-size: rem(28px);
font-size: clamp(#{rem(22px)}, 5vw, #{rem(28px)});
font-weight: 200;
@include bp (sm) {

View File

@@ -6,6 +6,7 @@
align-items: center;
overflow: hidden;
cursor: pointer;
pointer-events: auto;
// Left Image
&__left {

View File

@@ -4,7 +4,7 @@
overflow: hidden;
max-width: 326px;
@include bp (sm) {
@include bp (md) {
background-color: $color-primary-tertiary20;
max-width: 600px;
}
@@ -33,12 +33,10 @@
// Buttons
.buttons {
display: flex;
margin-top: 40px;
margin-top: 32px;
justify-content: center;
transform: translate3d(0, 100%, 0);
transition: transform 0.8s var(--ease-quart);
@include bp (sm) {
@include bp (md) {
position: absolute;
z-index: 1;
bottom: 4.25%;
@@ -46,19 +44,43 @@
width: 100%;
align-items: center;
justify-content: center;
margin-top: 40px;
transform: translate3d(0, 100%, 0);
transition: transform 0.8s var(--ease-quart);
}
a {
margin: 0 clamp(4px, 0.3vw, 8px);
text-align: center;
& > * {
background: none;
font-size: rem(14px);
font-weight: 300;
color: $color-tertiary;
padding: 0;
&:first-child {
margin-right: 12px;
&:hover {
background: none;
}
}
&:last-child {
display: inline-flex;
align-items: center;
height: 30px;
padding: 0 12px;
color: $color-secondary;
border: 1px solid rgba($color-secondary, 0.4);
}
}
}
// Hover effect
&:hover {
img {
transform: scale(0.8) translate3d(0, -5%, 0);
border-radius: 8px;
@include bp (md) {
transform: scale(0.8) translate3d(0, -5%, 0);
border-radius: 8px;
}
}
.buttons {
transform: translate3d(0,0,0);

View File

@@ -1,12 +1,24 @@
.cart {
position: fixed;
z-index: 202;
top: 80px;
right: 0;
width: 100%;
height: calc(100vh - 80px);
display: flex;
flex-direction: column;
overflow: auto;
padding: 20px 20px 24px;
background-color: $color-cream;
border-radius: 8px;
border-radius: 8px 8px 0 0;
@include bp (sm) {
top: 24px;
right: 24px;
width: clamp(320px, 45vw, 500px);
height: calc(100vh - 48px);
max-height: 1000px;
border-radius: 8px;
padding: 24px 32px 32px;
}
@@ -85,10 +97,11 @@
}
}
span {
margin-left: 20px;
margin-left: 12px;
font-size: rem(12px);
@include bp (sm) {
margin-bottom: 20px;
font-size: rem(13px);
}
}
@@ -106,20 +119,25 @@
// Checkout
&--checkout {
display: flex;
@include bp (md) {
display: flex;
}
p {
max-width: 180px;
margin-right: auto;
margin-bottom: 16px;
padding-right: 12px;
font-size: rem(11px);
line-height: 1.5;
color: $color-gray;
@include bp (sm) {
max-width: 304px;
font-size: rem(12px);
line-height: 1.6;
}
@include bp (md) {
max-width: 304px;
}
}
}
}
@@ -199,7 +217,7 @@
// Location switcher
&-switcher {
position: fixed;
z-index: 100;
z-index: 202;
top: 20px;
left: 20px;
will-change: transform, opacity;
@@ -213,7 +231,7 @@
// Overlay
&-overlay {
position: fixed;
z-index: 99;
z-index: 201;
top: 0;
left: 0;
width: 100%;

View File

@@ -20,54 +20,53 @@
transform: translate3d(0, -88px, 0);
transition: transform 1s var(--ease-quart);
transition-delay: 100ms;
pointer-events: none;
@include bp (sm) {
--inset: 32px;
}
// Notifications
&__notifications {
position: absolute;
z-index: 100;
top: 72px;
right: 0;
opacity: 0;
transition: opacity 0.7s var(--ease-quart);
pointer-events: none;
& > * {
&:not(:last-child) {
margin-bottom: 8px;
}
}
}
// Visible state
&.is-visible {
transform: translate3d(0,0,0);
.shop-location__notifications {
opacity: 1;
pointer-events: auto;
}
}
}
// Cart
.cart {
position: fixed;
z-index: 101;
top: 72px;
right: 0;
width: 100%;
height: calc(100vh - 72px);
@include bp (sm) {
top: 24px;
right: 24px;
width: clamp(320px, 45vw, 500px);
height: calc(100vh - 48px);
max-height: 1000px;
}
}
}
// Cart
.cart {
position: fixed;
z-index: 101;
top: 72px;
right: 0;
width: 100%;
height: calc(100vh - 72px);
@include bp (sm) {
top: 24px;
right: 24px;
width: clamp(320px, 45vw, 500px);
height: calc(100vh - 48px);
max-height: 1000px;
}
}
// Notifications
.notifications {
position: fixed;
z-index: 100;
top: 104px;
right: 20px;
transition: opacity 0.7s var(--ease-quart);
pointer-events: none;
@include bp (sm) {
right: 32px;
}
& > * {
&:not(:last-child) {
margin-bottom: 8px;
}
}
}

View File

@@ -142,7 +142,7 @@
width: 100%;
.container {
padding: 0 16px 24px;
padding: 0 0 24px;
@include bp (md) {
display: grid;
@@ -166,10 +166,18 @@
// Navigation
nav {
display: flex;
justify-content: center;
ul {
display: flex;
align-items: center;
justify-content: center;
overflow: auto;
@include bp (sm) {
justify-content: center;
overflow: hidden;
}
}
li {
display: block;
@@ -199,4 +207,11 @@
}
}
}
// Cart button
.button-cart {
@include bp (sm, max) {
display: none;
}
}
}

View File

@@ -3,28 +3,31 @@
*/
&__posters {
background-color: $color-primary-darker;
padding: 0 20px 72px;
padding: clamp(56px, 10vw, 120px) 20px 72px;
border-bottom: solid 1px $color-primary-tertiary20 ;
@include bp (sm) {
@include bp (sd) {
padding: 120px 0;
}
// Title
h3 {
grid-column: 2 / span 6;
margin-bottom: 48px;
font-family: $font-serif;
color: $color-cream;
font-size: rem(32px);
line-height: 1.1;
text-align: center;
margin-bottom: 24px;
@include bp (sm) {
grid-column: 3 / span 14;
max-width: 360px;
margin-bottom: 48px;
font-size: rem(48px);
text-align: left;
max-width: 360px;
}
@include bp (sd) {
margin-bottom: 0;
}
}
@@ -32,64 +35,135 @@
// Posters Set
.set {
--gap: 24px;
grid-column: 1 / span 8;
grid-column: span calc(var(--columns));
display: block;
@include bp (sm) {
grid-column: 2 / span 22;
}
cursor: grab;
@include bp (mob-lg) {
--gap: 32px;
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: var(--gap);
grid-column: 2 / span calc(var(--columns) - 2);
}
@include bp (sm) {
grid-column: 3 / span calc(var(--columns) - 4);
}
@include bp (md) {
grid-template-columns: repeat(3, 1fr);
cursor: default;
}
@include bp (sd) {
--gap: 48px;
grid-template-columns: repeat(4, 1fr);
&.is-dragging {
cursor: grabbing;
}
&__content {
display: flex;
margin: 0 -8px;
@include bp (mob-lg) {
margin: 0 -12px;
}
// @include bp (sm) {
// grid-column: 2 / span 22;
// }
// @include bp (mob-lg) {
// --gap: 32px;
// display: grid;
// grid-template-columns: repeat(2, 1fr);
// gap: var(--gap);
// }
@include bp (md) {
--gap: 32px;
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: var(--gap);
margin: 0;
}
@include bp (sd) {
--gap: 48px;
grid-template-columns: repeat(4, 1fr);
}
}
// Poster
.poster {
margin: 0 auto auto;
position: relative;
flex: 0 0 100%;
margin: 0 12px;
@include bp (mob-lg) {
flex: 0 0 calc(50% - 24px);
}
@include bp (sm) {
flex: 0 0 calc(50% - 24px);
}
// margin: 0 auto auto;
// 2 columns
&:nth-child(2n + 1) {
@include bp (sm, max) {
margin-top: calc(var(--gap) * 2);
}
}
// &:nth-child(2n + 1) {
// @include bp (sm, max) {
// margin-top: calc(var(--gap) * 2);
// }
// }
// 3 columns
&:nth-child(3n + 1) {
@include bp (sd, max) {
margin-top: calc(var(--gap) * 2);
}
}
&:nth-child(3n + 2) {
@include bp (sd, max) {
margin-top: var(--gap);
}
}
// &:nth-child(3n + 1) {
// @include bp (sd, max) {
// margin-top: calc(var(--gap) * 2);
// }
// }
// &:nth-child(3n + 2) {
// @include bp (sd, max) {
// margin-top: var(--gap);
// }
// }
// 4 columns
&:nth-child(4n + 1) {
@include bp (sd) {
margin-top: 64px * 3;
// &:nth-child(4n + 1) {
// @include bp (sd) {
// margin-top: 64px * 3;
// }
// }
// &:nth-child(4n + 2) {
// @include bp (sd) {
// margin-top: 64px * 2;
// }
// }
// &:nth-child(4n + 3) {
// @include bp (sd) {
// margin-top: 64px;
// }
// }
}
// Pagination
&__dots {
display: flex;
justify-content: center;
margin: 32px 0 48px;
li {
display: block;
padding: 4px;
cursor: pointer;
// Hover
&:hover {
button {
box-shadow: inset 0 0 0 8px $color-tertiary;
}
}
}
&:nth-child(4n + 2) {
@include bp (sd) {
margin-top: 64px * 2;
}
button {
display: block;
width: 8px;
height: 8px;
border-radius: 100%;
background: $color-primary-tertiary20;
padding: 0;
transition: box-shadow 1.0s var(--ease-quart);
}
&:nth-child(4n + 3) {
@include bp (sd) {
margin-top: 64px;
.is-active {
button {
box-shadow: inset 0 0 0 8px $color-tertiary;
}
}
}
@@ -99,10 +173,10 @@
.subscribe {
grid-column: 1 / span var(--columns);
max-width: 380px;
margin: 72px auto 0;
margin: 56px auto 0;
text-align: center;
@include bp (sm) {
@include bp (md) {
grid-column: 14 / span 8;
margin-top: 0;
text-align: left;
@@ -129,10 +203,14 @@
@include bp (sm) {
margin: 0;
}
&__email {
margin-bottom: 0;
}
&__bottom {
display: none;
}
}
.newsletter-form__bottom {
display: none;
}
}
}

View File

@@ -1,13 +1,6 @@
import { writable, derived } from 'svelte/store'
/**
* Shop
*/
export const shopLocations = writable([])
/**
* Cart
*/