[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

@@ -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,
}
}
}