🚧 Switch to monorepo with Turbo
This commit is contained in:
69
apps/website/src/components/molecules/CartItem.svelte
Normal file
69
apps/website/src/components/molecules/CartItem.svelte
Normal file
@@ -0,0 +1,69 @@
|
||||
<style lang="scss">
|
||||
@import "../../style/molecules/cart-item";
|
||||
</style>
|
||||
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
// Components
|
||||
import ButtonCircle from '$components/atoms/ButtonCircle.svelte'
|
||||
import Select from '$components/molecules/Select.svelte'
|
||||
|
||||
export let item: any
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const quantityLimit = 5
|
||||
|
||||
|
||||
// When changing item quantity
|
||||
const updateQuantity = ({ detail }: any) => {
|
||||
dispatch('updatedQuantity', {
|
||||
id: item.id,
|
||||
quantity: Number(detail)
|
||||
})
|
||||
}
|
||||
|
||||
// When removing item
|
||||
const removeItem = () => {
|
||||
dispatch('removed', item.id)
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="cart-item shadow-small">
|
||||
<div class="cart-item__left">
|
||||
<img src={item.product.images[0].file.url} width={200} height={300} alt={item.product.name}>
|
||||
</div>
|
||||
<div class="cart-item__right">
|
||||
<h3>Poster</h3>
|
||||
<p>
|
||||
{item.product.name}
|
||||
<br>– {item.price}€
|
||||
</p>
|
||||
|
||||
{#if item && item.quantity}
|
||||
<Select
|
||||
name="sort" id="filter_sort"
|
||||
options={[...Array(item.quantity <= quantityLimit ? quantityLimit : item.quantity)].map((_, index) => {
|
||||
return {
|
||||
value: `${index + 1}`,
|
||||
name: `${index + 1}`,
|
||||
default: index === 0,
|
||||
selected: index + 1 === item.quantity,
|
||||
}
|
||||
})}
|
||||
on:change={updateQuantity}
|
||||
value={String(item.quantity)}
|
||||
>
|
||||
<span>Quantity:</span>
|
||||
</Select>
|
||||
{/if}
|
||||
|
||||
<ButtonCircle class="remove"
|
||||
size="tiny" color="gray"
|
||||
on:click={removeItem}
|
||||
>
|
||||
<svg width="8" height="8">
|
||||
<use xlink:href="#cross" />
|
||||
</svg>
|
||||
</ButtonCircle>
|
||||
</div>
|
||||
</div>
|
||||
106
apps/website/src/components/molecules/EmailForm.svelte
Normal file
106
apps/website/src/components/molecules/EmailForm.svelte
Normal file
@@ -0,0 +1,106 @@
|
||||
<style lang="scss">
|
||||
@import "../../style/molecules/newsletter-form";
|
||||
</style>
|
||||
|
||||
<script lang="ts">
|
||||
import { fly } from 'svelte/transition'
|
||||
import { quartOut } from 'svelte/easing'
|
||||
import { sendEvent } from '$utils/analytics'
|
||||
// Components
|
||||
import IconArrow from '$components/atoms/IconArrow.svelte'
|
||||
import ButtonCircle from '$components/atoms/ButtonCircle.svelte'
|
||||
|
||||
export let past: boolean = false
|
||||
|
||||
let inputInFocus = false
|
||||
let formStatus: FormStatus = null
|
||||
let formMessageTimeout: ReturnType<typeof setTimeout> | number
|
||||
|
||||
interface FormStatus {
|
||||
error?: string
|
||||
success?: boolean
|
||||
message: string
|
||||
}
|
||||
const formMessages = {
|
||||
PENDING: `Almost there! Please confirm your email address through the email you'll receive soon.`,
|
||||
MEMBER_EXISTS_WITH_EMAIL_ADDRESS: `This email address is already subscribed to the newsletter.`,
|
||||
INVALID_EMAIL: `Woops. This email doesn't seem to be valid.`,
|
||||
}
|
||||
|
||||
$: isSuccess = formStatus && formStatus.success
|
||||
|
||||
// Toggle input focus
|
||||
const toggleFocus = () => inputInFocus = !inputInFocus
|
||||
|
||||
// Handle form submission
|
||||
async function handleForm (event: Event | HTMLFormElement) {
|
||||
const data = new FormData(this)
|
||||
const email = data.get('email')
|
||||
|
||||
if (email) {
|
||||
const req = await fetch(this.action, {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({ email })
|
||||
})
|
||||
const result: FormStatus = await req.json()
|
||||
formStatus = result
|
||||
console.log('SK api response:', result)
|
||||
|
||||
// If successful
|
||||
if (formStatus.success) {
|
||||
sendEvent('newsletterSubscribe')
|
||||
} else {
|
||||
// Hide message for errors
|
||||
clearTimeout(formMessageTimeout)
|
||||
formMessageTimeout = requestAnimationFrame(() => setTimeout(() => formStatus = null, 4000))
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="newsletter-form">
|
||||
{#if !isSuccess}
|
||||
<form method="POST" action="/api/newsletter" on:submit|preventDefault={handleForm}
|
||||
out:fly|local={{ y: -8, easing: quartOut, duration: 600 }}
|
||||
>
|
||||
<div class="newsletter-form__email" class:is-focused={inputInFocus}>
|
||||
<input type="email" placeholder="Your email address" name="email" id="newsletter_email" required
|
||||
on:focus={toggleFocus}
|
||||
on:blur={toggleFocus}
|
||||
>
|
||||
<ButtonCircle
|
||||
type="submit"
|
||||
color="pink" size="small"
|
||||
clone={true}
|
||||
label="Subscribe"
|
||||
>
|
||||
<IconArrow color="white" />
|
||||
</ButtonCircle>
|
||||
</div>
|
||||
|
||||
<div class="newsletter-form__bottom">
|
||||
{#if past}
|
||||
<a href="/subscribe" class="past-issues" data-sveltekit-noscroll>
|
||||
<svg width="20" height="16" viewBox="0 0 20 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg" aria-label="Newsletter icon">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M18 2.346H2a.5.5 0 0 0-.5.5v11.102a.5.5 0 0 0 .5.5h16a.5.5 0 0 0 .5-.5V2.846a.5.5 0 0 0-.5-.5ZM2 .846a2 2 0 0 0-2 2v11.102a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V2.846a2 2 0 0 0-2-2H2Zm13.75 4.25h-2v3h2v-3Zm-2-1a1 1 0 0 0-1 1v3a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1v-3a1 1 0 0 0-1-1h-2ZM3.5 6.5a.5.5 0 0 0 0 1h6a.5.5 0 0 0 0-1h-6Zm.25 3a.5.5 0 0 1 .5-.5h6a.5.5 0 0 1 0 1h-6a.5.5 0 0 1-.5-.5Zm1.25 2a.5.5 0 0 0 0 1h6a.5.5 0 1 0 0-1H5Z" />
|
||||
</svg>
|
||||
<span>See past issues</span>
|
||||
</a>
|
||||
{/if}
|
||||
<p>No spam, we promise!</p>
|
||||
</div>
|
||||
</form>
|
||||
{/if}
|
||||
|
||||
{#if formStatus && formStatus.message}
|
||||
<div class="newsletter-form__message shadow-small"
|
||||
class:is-error={!isSuccess}
|
||||
class:is-success={isSuccess}
|
||||
in:fly|local={{ y: 8, easing: quartOut, duration: 600, delay: isSuccess ? 600 : 0 }}
|
||||
out:fly|local={{ y: 8, easing: quartOut, duration: 600 }}
|
||||
>
|
||||
<p class="text-xsmall">{formMessages[formStatus.message]}</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
17
apps/website/src/components/molecules/Heading.svelte
Normal file
17
apps/website/src/components/molecules/Heading.svelte
Normal file
@@ -0,0 +1,17 @@
|
||||
<style lang="scss">
|
||||
@import "../../style/molecules/heading";
|
||||
</style>
|
||||
|
||||
<script lang="ts">
|
||||
import SiteTitle from '$components/atoms/SiteTitle.svelte'
|
||||
|
||||
export let text: string
|
||||
</script>
|
||||
|
||||
<section class="heading">
|
||||
<SiteTitle variant="inline" />
|
||||
|
||||
<div class="text text-medium">
|
||||
{@html text}
|
||||
</div>
|
||||
</section>
|
||||
66
apps/website/src/components/molecules/House.svelte
Normal file
66
apps/website/src/components/molecules/House.svelte
Normal file
@@ -0,0 +1,66 @@
|
||||
<style lang="scss">
|
||||
@import "../../style/molecules/house";
|
||||
</style>
|
||||
|
||||
<script lang="ts">
|
||||
import dayjs from 'dayjs'
|
||||
// Components
|
||||
import Image from '$components/atoms/Image.svelte'
|
||||
import Icon from '$components/atoms/Icon.svelte'
|
||||
|
||||
export let url: string
|
||||
export let photoId: string
|
||||
export let photoAlt: string
|
||||
export let title: string
|
||||
export let index: string
|
||||
export let ratio: number
|
||||
export let date: string = undefined
|
||||
export let city: string = undefined
|
||||
export let location: string
|
||||
</script>
|
||||
|
||||
<div class="house grid">
|
||||
<div class="house__info">
|
||||
<h2 class="title-image">
|
||||
{title}
|
||||
</h2>
|
||||
<p class="info text-info">
|
||||
{#if city}
|
||||
<a href="https://www.openstreetmap.org/search?query={title}, {city} {location}" target="_blank" rel="noopener noreferrer">
|
||||
<Icon class="icon" icon="map-pin" label="Map pin" /> {city}
|
||||
</a>
|
||||
<span class="sep">·</span>
|
||||
{/if}
|
||||
{#if date}
|
||||
<time datetime={dayjs(date).format('YYYY-MM-DD')}>
|
||||
{dayjs(date).format('MMMM YYYY')}
|
||||
</time>
|
||||
{/if}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="house__photo grid" class:not-landscape={ratio < 1.475}>
|
||||
<a href={url} tabindex="0">
|
||||
<figure class="house__image shadow-photo">
|
||||
<Image
|
||||
class="photo"
|
||||
id={photoId}
|
||||
sizeKey="photo-list"
|
||||
sizes={{
|
||||
small: { width: 500 },
|
||||
medium: { width: 850 },
|
||||
large: { width: 1280 },
|
||||
}}
|
||||
ratio={1.5}
|
||||
alt={photoAlt}
|
||||
/>
|
||||
</figure>
|
||||
</a>
|
||||
<span class="house__index title-index"
|
||||
class:has-one-start={index.startsWith('1')}
|
||||
class:has-one-end={index.endsWith('1')}
|
||||
>
|
||||
{index}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
125
apps/website/src/components/molecules/Location.svelte
Normal file
125
apps/website/src/components/molecules/Location.svelte
Normal file
@@ -0,0 +1,125 @@
|
||||
<style lang="scss">
|
||||
@import "../../style/molecules/location";
|
||||
</style>
|
||||
|
||||
<script lang="ts">
|
||||
import { getContext } from 'svelte'
|
||||
import { spring } from 'svelte/motion'
|
||||
import dayjs from 'dayjs'
|
||||
import { lerp } from '$utils/functions'
|
||||
import { PUBLIC_PREVIEW_COUNT } from '$env/static/public'
|
||||
import { seenLocations } from '$utils/stores'
|
||||
// Components
|
||||
import Image from '$components/atoms/Image.svelte'
|
||||
import Badge from '$components/atoms/Badge.svelte'
|
||||
|
||||
export let location: any
|
||||
export let latestPhoto: any
|
||||
|
||||
const { settings }: any = getContext('global')
|
||||
|
||||
let locationEl: HTMLElement
|
||||
let photoIndex = 0
|
||||
|
||||
// Location date limit
|
||||
let isNew = false
|
||||
const dateNowOffset = dayjs().subtract(settings.limit_new, 'day')
|
||||
const parsedSeenLocations = JSON.parse($seenLocations)
|
||||
|
||||
$: if (latestPhoto) {
|
||||
const dateUpdated = dayjs(latestPhoto.date_created)
|
||||
|
||||
// Detect if location has new content
|
||||
const seenLocationDate = dayjs(parsedSeenLocations[location.id])
|
||||
const isLocationSeen = parsedSeenLocations?.hasOwnProperty(location.id)
|
||||
|
||||
// Define if location is has new photos
|
||||
if (seenLocationDate && isLocationSeen) {
|
||||
// A more recent photo has been added (if has been seen and has a seen date)
|
||||
isNew = dateUpdated.isAfter(dateNowOffset) && dateUpdated.isAfter(seenLocationDate)
|
||||
} else {
|
||||
// The photo is after the offset
|
||||
isNew = dateUpdated.isAfter(dateNowOffset)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Moving cursor over
|
||||
*/
|
||||
const offset = spring({ x: 0, y: 0 }, {
|
||||
stiffness: 0.075,
|
||||
damping: 0.9
|
||||
})
|
||||
const handleMouseMove = ({ clientX }: MouseEvent) => {
|
||||
const { width, left } = locationEl.getBoundingClientRect()
|
||||
const moveProgress = (clientX - left) / width // 0 to 1
|
||||
|
||||
// Move horizontally
|
||||
offset.update(_ => ({
|
||||
x: lerp(-56, 56, moveProgress),
|
||||
y: 0
|
||||
}))
|
||||
|
||||
// Change photo index from mouse position percentage
|
||||
photoIndex = Math.round(lerp(0, Number(PUBLIC_PREVIEW_COUNT) - 1, moveProgress))
|
||||
}
|
||||
|
||||
// Leaving mouseover
|
||||
const handleMouseLeave = () => {
|
||||
offset.update($c => ({
|
||||
x: $c.x,
|
||||
y: 40
|
||||
}))
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="location" bind:this={locationEl}
|
||||
style:--offset-x="{$offset.x}px"
|
||||
style:--offset-y="{$offset.y}px"
|
||||
style:--rotate="{$offset.x * 0.125}deg"
|
||||
>
|
||||
<a href="/{location.country.slug}/{location.slug}"
|
||||
on:mousemove={handleMouseMove}
|
||||
on:mouseleave={handleMouseLeave}
|
||||
tabindex="0"
|
||||
>
|
||||
<Image
|
||||
class="flag"
|
||||
id={location.country.flag.id}
|
||||
sizeKey="square-small"
|
||||
width={32} height={32}
|
||||
alt="Flag of {location.country.name}"
|
||||
/>
|
||||
<div class="text">
|
||||
<dl>
|
||||
<dt class="location__name">
|
||||
{location.name}
|
||||
</dt>
|
||||
<dd class="location__country text-label">
|
||||
{location.country.name}
|
||||
</dd>
|
||||
</dl>
|
||||
{#if isNew}
|
||||
<Badge text="New" />
|
||||
{/if}
|
||||
</div>
|
||||
</a>
|
||||
|
||||
{#if location.photos.length}
|
||||
<div class="location__photos">
|
||||
{#each location.photos as { image }, index}
|
||||
{#if image}
|
||||
{@const classes = ['location__photo', index === photoIndex ? 'is-visible' : null].join(' ').trim()}
|
||||
<Image
|
||||
class={classes}
|
||||
id={image.id}
|
||||
sizeKey="photo-thumbnail"
|
||||
width={340} height={226}
|
||||
alt={image.title}
|
||||
/>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
33
apps/website/src/components/molecules/NewsletterIssue.svelte
Normal file
33
apps/website/src/components/molecules/NewsletterIssue.svelte
Normal file
@@ -0,0 +1,33 @@
|
||||
<style lang="scss">
|
||||
@import "../../style/molecules/issue";
|
||||
</style>
|
||||
|
||||
<script lang="ts">
|
||||
import dayjs from 'dayjs'
|
||||
import Image from '$components/atoms/Image.svelte'
|
||||
|
||||
export let title: string
|
||||
export let issue: number
|
||||
export let date: string
|
||||
export let link: string
|
||||
export let thumbnail: { id: string }
|
||||
export let size: string = undefined
|
||||
</script>
|
||||
|
||||
<div class="issue" class:is-large={size === 'large'}>
|
||||
<a href={link} target="_blank" rel="external noopener" tabindex="0">
|
||||
<Image
|
||||
id={thumbnail.id}
|
||||
sizeKey="issue-thumbnail-small"
|
||||
width={160} height={112}
|
||||
alt="Issue {issue} thumbnail"
|
||||
/>
|
||||
<dl>
|
||||
<dt>Issue #{issue}</dt>
|
||||
<dd>
|
||||
<p>{title}</p>
|
||||
<time>{dayjs(date).format('DD/MM/YYYY')}</time>
|
||||
</dd>
|
||||
</dl>
|
||||
</a>
|
||||
</div>
|
||||
@@ -0,0 +1,32 @@
|
||||
<style lang="scss">
|
||||
@import "../../style/molecules/notification-cart";
|
||||
</style>
|
||||
|
||||
<script lang="ts">
|
||||
import { fly } from 'svelte/transition'
|
||||
import { quartOut } from 'svelte/easing'
|
||||
import { cartOpen } from '$utils/stores/shop'
|
||||
|
||||
export let title: string
|
||||
export let name: string
|
||||
export let image: string
|
||||
|
||||
|
||||
const closeNotification = () => {
|
||||
// Open cart
|
||||
$cartOpen = true
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="notification-cart shadow-small"
|
||||
on:click={closeNotification}
|
||||
transition:fly={{ y: 20, duration: 700, easing: quartOut }}
|
||||
>
|
||||
<div class="notification-cart__left">
|
||||
<img src={image} width={58} height={88} alt={title}>
|
||||
</div>
|
||||
<div class="notification-cart__right">
|
||||
<h3>{title}</h3>
|
||||
<p>{name}</p>
|
||||
</div>
|
||||
</div>
|
||||
23
apps/website/src/components/molecules/Pagination.svelte
Normal file
23
apps/website/src/components/molecules/Pagination.svelte
Normal file
@@ -0,0 +1,23 @@
|
||||
<style lang="scss">
|
||||
@import "../../style/molecules/pagination";
|
||||
</style>
|
||||
|
||||
<script lang="ts">
|
||||
export let ended: boolean = false
|
||||
export let current: number
|
||||
export let total: number
|
||||
</script>
|
||||
|
||||
<div class="pagination" role="button" tabindex="0"
|
||||
disabled={ended ? ended : undefined}
|
||||
on:click
|
||||
on:keydown
|
||||
>
|
||||
<div class="pagination__progress">
|
||||
<span class="current">{current}</span>
|
||||
<span>/</span>
|
||||
<span class="total">{total}</span>
|
||||
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
70
apps/website/src/components/molecules/PhotoCard.svelte
Normal file
70
apps/website/src/components/molecules/PhotoCard.svelte
Normal file
@@ -0,0 +1,70 @@
|
||||
<style lang="scss">
|
||||
@import "../../style/molecules/photo-card";
|
||||
</style>
|
||||
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import Image from '$components/atoms/Image.svelte'
|
||||
|
||||
export let id: string
|
||||
export let alt: string
|
||||
export let url: string = undefined
|
||||
export let title: string = undefined
|
||||
export let location: any = undefined
|
||||
export let city: string = undefined
|
||||
export let hovered: boolean = false
|
||||
export let lazy: boolean = true
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const sizes = {
|
||||
small: { width: 224 },
|
||||
medium: { width: 464 },
|
||||
large: { width: 864 },
|
||||
}
|
||||
|
||||
const sendHover = (hover: boolean) => dispatch('hover', hover)
|
||||
</script>
|
||||
|
||||
<div class="photo-card"
|
||||
class:is-hovered={hovered}
|
||||
on:mouseenter={() => sendHover(true)}
|
||||
on:focus={() => sendHover(true)}
|
||||
on:mouseout={() => sendHover(false)}
|
||||
on:blur={() => sendHover(false)}
|
||||
>
|
||||
{#if url}
|
||||
<div class="photo-card__content">
|
||||
<a href={url} data-sveltekit-noscroll>
|
||||
<Image
|
||||
{id}
|
||||
sizeKey="postcard"
|
||||
{sizes}
|
||||
ratio={1.5}
|
||||
{alt}
|
||||
{lazy}
|
||||
/>
|
||||
{#if title && location}
|
||||
<div class="photo-card__info">
|
||||
<Image
|
||||
id={location.country.flag.id}
|
||||
sizeKey="square-small"
|
||||
width={24}
|
||||
height={24}
|
||||
alt="Flag of {location.country.name}"
|
||||
/>
|
||||
<p>{title} - {city ? `${city}, ` : ''}{location.name}, {location.country.name}</p>
|
||||
</div>
|
||||
{/if}
|
||||
</a>
|
||||
</div>
|
||||
{:else}
|
||||
<Image
|
||||
{id}
|
||||
sizeKey="postcard"
|
||||
{sizes}
|
||||
ratio={1.5}
|
||||
{alt}
|
||||
{lazy}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
48
apps/website/src/components/molecules/PostCard.svelte
Normal file
48
apps/website/src/components/molecules/PostCard.svelte
Normal file
@@ -0,0 +1,48 @@
|
||||
<style lang="scss">
|
||||
@import "../../style/organisms/postcard";
|
||||
</style>
|
||||
|
||||
<script lang="ts">
|
||||
import Image from '$components/atoms/Image.svelte'
|
||||
|
||||
export let street: string
|
||||
export let location: string
|
||||
export let region: string = undefined
|
||||
export let country: string
|
||||
export let flagId: string
|
||||
export let size: string = undefined
|
||||
|
||||
const className = 'postcard'
|
||||
$: classes = [
|
||||
className,
|
||||
...[size].map(variant => variant && `${className}--${variant}`),
|
||||
$$props.class
|
||||
].join(' ').trim()
|
||||
</script>
|
||||
|
||||
<div class={classes}>
|
||||
<div class="postcard__left">
|
||||
<p class="postcard__country">
|
||||
<span>Houses of</span><br>
|
||||
<strong class="title-country__purple">{country}</strong>
|
||||
</p>
|
||||
</div>
|
||||
<div class="postcard__right">
|
||||
<div class="postcard__stamp">
|
||||
<div class="frame">
|
||||
<img src="/images/icons/stamp.svg" width="32" height="42" alt="Stamp">
|
||||
</div>
|
||||
<Image
|
||||
class="flag"
|
||||
id={flagId}
|
||||
sizeKey="square-small"
|
||||
width={32} height={32}
|
||||
alt="Flag of {country}"
|
||||
/>
|
||||
</div>
|
||||
<ul class="postcard__address">
|
||||
<li>{street}</li>
|
||||
<li>{location}{region ? `, ${region}` : ''}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
50
apps/website/src/components/molecules/Poster.svelte
Normal file
50
apps/website/src/components/molecules/Poster.svelte
Normal file
@@ -0,0 +1,50 @@
|
||||
<style lang="scss">
|
||||
@import "../../style/molecules/poster";
|
||||
</style>
|
||||
|
||||
<script lang="ts">
|
||||
import { addToCart } from '$utils/functions/shop'
|
||||
import { smoothScroll } from '$utils/stores'
|
||||
// Components
|
||||
import Button from '$components/atoms/Button.svelte'
|
||||
import Image from '$components/atoms/Image.svelte'
|
||||
|
||||
export let product: any
|
||||
export let location: { name: string, slug: string }
|
||||
export let image: any
|
||||
</script>
|
||||
|
||||
<div class="poster">
|
||||
{#if image}
|
||||
<a href="/shop/poster-{location.slug}" data-sveltekit-noscroll
|
||||
on:click={() => $smoothScroll.scrollTo('#poster', { duration: 2 })}
|
||||
>
|
||||
<Image
|
||||
id={image.id}
|
||||
sizeKey="product"
|
||||
sizes={{
|
||||
small: { width: 326 },
|
||||
medium: { width: 326 },
|
||||
large: { width: 326 },
|
||||
}}
|
||||
ratio={1.5}
|
||||
alt="Poster of {location.name}"
|
||||
/>
|
||||
</a>
|
||||
{/if}
|
||||
<div class="buttons">
|
||||
<Button
|
||||
size="xsmall"
|
||||
url="/shop/poster-{location.slug}"
|
||||
text="View"
|
||||
on:click={() => $smoothScroll.scrollTo('#poster', { duration: 2 })}
|
||||
/>
|
||||
<Button
|
||||
tag="button"
|
||||
size="xsmall"
|
||||
text="Add to cart"
|
||||
color="pink"
|
||||
on:click={() => addToCart(product)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
51
apps/website/src/components/molecules/ProcessStep.svelte
Normal file
51
apps/website/src/components/molecules/ProcessStep.svelte
Normal file
@@ -0,0 +1,51 @@
|
||||
<style lang="scss">
|
||||
@import "../../style/molecules/process-step";
|
||||
</style>
|
||||
|
||||
<script lang="ts">
|
||||
import { scaleFade } from '$animations/transitions'
|
||||
|
||||
// Components
|
||||
import Image from '$components/atoms/Image.svelte'
|
||||
import { getAssetUrlKey } from '$utils/api'
|
||||
|
||||
export let index: number
|
||||
export let text: string
|
||||
export let image: any = undefined
|
||||
export let video: any = undefined
|
||||
|
||||
const imageRatio = image ? image.width / image.height : undefined
|
||||
</script>
|
||||
|
||||
<div class="step grid" style:--index={index}
|
||||
in:scaleFade|local={{ scale: [1.1, 1], opacity: [0, 1], x: [20, 0], delay: 0.2 }}
|
||||
out:scaleFade|local={{ scale: [1, 0.9], opacity: [1, 0], x: [0, -20] }}
|
||||
>
|
||||
{#if image || video}
|
||||
<div class="media">
|
||||
{#if image}
|
||||
<Image
|
||||
class="image shadow-box-dark"
|
||||
id={image.id}
|
||||
sizeKey="product"
|
||||
sizes={{
|
||||
small: { width: 400 },
|
||||
medium: { width: 600 },
|
||||
}}
|
||||
ratio={imageRatio}
|
||||
alt={image.title}
|
||||
/>
|
||||
{:else if video && video.mp4 && video.webm}
|
||||
<video muted loop playsinline autoplay allow="autoplay">
|
||||
<source type="video/mp4" src={getAssetUrlKey(video.mp4, 'step')} />
|
||||
<source type="video/webm" src={getAssetUrlKey(video.webm, 'step')} />
|
||||
<track kind="captions" />
|
||||
</video>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="text text-xsmall">
|
||||
{@html text}
|
||||
</div>
|
||||
</div>
|
||||
51
apps/website/src/components/molecules/Select.svelte
Normal file
51
apps/website/src/components/molecules/Select.svelte
Normal file
@@ -0,0 +1,51 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
|
||||
interface Option {
|
||||
value: string
|
||||
name: string
|
||||
default?: boolean
|
||||
selected?: boolean
|
||||
}
|
||||
|
||||
export let id: string
|
||||
export let name: string
|
||||
export let options: Option[]
|
||||
export let value: string = undefined
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const defaultOption = options.find(option => option.default)
|
||||
|
||||
let selected = value || options[0].value
|
||||
$: currentOption = options.find(option => option.value === selected)
|
||||
|
||||
// Redefine value from parent (when reset)
|
||||
$: if (value === defaultOption.value) {
|
||||
selected = defaultOption.value
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* When changing select value
|
||||
*/
|
||||
const handleChange = ({ target: { value }}: any) => {
|
||||
const option = options.find(option => option.value === value)
|
||||
|
||||
// Dispatch event to parent
|
||||
dispatch('change', option.value)
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="select">
|
||||
<slot />
|
||||
|
||||
<span>{currentOption.name}</span>
|
||||
|
||||
<select {name} {id} bind:value={selected} on:change={handleChange}>
|
||||
{#each options as { value, name }}
|
||||
<option {value} selected={value === selected}>
|
||||
{name}
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
@@ -0,0 +1,45 @@
|
||||
<style lang="scss">
|
||||
@import "../../style/molecules/shop-locationswitcher";
|
||||
</style>
|
||||
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation'
|
||||
import { getContext, tick } from 'svelte'
|
||||
import { shopCurrentProductSlug } from '$utils/stores/shop'
|
||||
import { smoothScroll } from '$utils/stores'
|
||||
|
||||
export let isOver: boolean = false
|
||||
|
||||
const { shopLocations }: any = getContext('shop')
|
||||
|
||||
const classes = [
|
||||
'shop-locationswitcher',
|
||||
isOver && 'is-over',
|
||||
$$props.class
|
||||
].join(' ').trim()
|
||||
|
||||
|
||||
// Quick location change
|
||||
const quickLocationChange = async ({ target: { value }}: any) => {
|
||||
const pathTo = `/shop/poster-${value}`
|
||||
goto(pathTo, { replaceState: true, noscroll: true, keepfocus: true })
|
||||
|
||||
// Scroll to anchor
|
||||
await tick()
|
||||
$smoothScroll.scrollTo('#poster', { duration: 2 })
|
||||
}
|
||||
</script>
|
||||
|
||||
<dl class={classes}>
|
||||
<dt class="text-label">Choose a city</dt>
|
||||
<dd>
|
||||
<svg width="18" height="18">
|
||||
<use xlink:href="#icon-map-pin" />
|
||||
</svg>
|
||||
<select on:change={quickLocationChange}>
|
||||
{#each shopLocations as { name, slug }}
|
||||
<option value={slug} selected={slug === $shopCurrentProductSlug}>{name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</dd>
|
||||
</dl>
|
||||
73
apps/website/src/components/molecules/Switcher.svelte
Normal file
73
apps/website/src/components/molecules/Switcher.svelte
Normal file
@@ -0,0 +1,73 @@
|
||||
<style lang="scss">
|
||||
@import "../../style/molecules/switcher";
|
||||
</style>
|
||||
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores'
|
||||
import { getContext } from 'svelte'
|
||||
import reveal from '$animations/reveal'
|
||||
import { sendEvent } from '$utils/analytics'
|
||||
// Components
|
||||
import Icon from '$components/atoms/Icon.svelte'
|
||||
|
||||
const { settings: { switcher_links }}: any = getContext('global')
|
||||
|
||||
let switcherEl: HTMLElement
|
||||
let isOpen = false
|
||||
|
||||
|
||||
/**
|
||||
* Toggle switcher open state
|
||||
*/
|
||||
const toggleSwitcher = () => {
|
||||
isOpen = !isOpen
|
||||
|
||||
// Record opening event
|
||||
!isOpen && sendEvent('switcherOpen')
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect outside click
|
||||
*/
|
||||
const windowClick = ({ target }) => {
|
||||
if (!switcherEl.contains(target) && isOpen) {
|
||||
// Close switcher
|
||||
toggleSwitcher()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window on:click={windowClick} />
|
||||
|
||||
<aside class="switcher" bind:this={switcherEl}
|
||||
class:is-open={isOpen}
|
||||
use:reveal={{
|
||||
animation: { y: [24, 0], opacity: [0, 1] },
|
||||
options: {
|
||||
duration: 1,
|
||||
delay: 0.6,
|
||||
threshold: 0,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<button class="switcher__button" title="{!isOpen ? 'Open' : 'Close'} menu" tabindex="0"
|
||||
on:click={toggleSwitcher}
|
||||
>
|
||||
<span>
|
||||
{#each Array(3) as _}
|
||||
<i />
|
||||
{/each}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<ul class="switcher__links" data-sveltekit-noscroll>
|
||||
{#each switcher_links as { text, url, icon, icon_label }}
|
||||
<li class:is-active={$page.url.pathname === url}>
|
||||
<a href={url} on:click={toggleSwitcher} tabindex="0">
|
||||
<Icon class="icon" icon={icon} label={icon_label} />
|
||||
<span>{text}</span>
|
||||
</a>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</aside>
|
||||
Reference in New Issue
Block a user