feat: create Toast component
This commit is contained in:
111
apps/website/src/components/molecules/Toast.svelte
Normal file
111
apps/website/src/components/molecules/Toast.svelte
Normal file
@@ -0,0 +1,111 @@
|
||||
<style lang="scss">
|
||||
@import "../../style/molecules/toast";
|
||||
</style>
|
||||
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte'
|
||||
import { fade, fly } from 'svelte/transition'
|
||||
import { quartOut } from 'svelte/easing'
|
||||
import { browser } from '$app/environment'
|
||||
import { cx } from 'classix'
|
||||
// Components
|
||||
import Button from '$components/atoms/Button.svelte'
|
||||
import Image from '$components/atoms/Image.svelte'
|
||||
|
||||
export let id: string
|
||||
export let type: 'global' | 'local'
|
||||
export let text: string
|
||||
export let cta: {
|
||||
label: string
|
||||
url: string
|
||||
color: string
|
||||
} = undefined
|
||||
export let images: { id: string, title: string }[] = undefined
|
||||
export let show = false
|
||||
|
||||
$: if (browser) {
|
||||
show = !localStorage.getItem(`toast-${id}`)
|
||||
}
|
||||
|
||||
// Image rotation
|
||||
let imagesLoop: ReturnType<typeof setTimeout>
|
||||
let currentImageIndex = 0
|
||||
|
||||
const incrementCurrentImageIndex = () => {
|
||||
currentImageIndex = currentImageIndex === images.length - 1 ? 0 : currentImageIndex + 1
|
||||
imagesLoop = setTimeout(() => requestAnimationFrame(incrementCurrentImageIndex), 3000)
|
||||
}
|
||||
|
||||
// Close toast
|
||||
const close = () => {
|
||||
localStorage.setItem(`toast-${id}`, 'closed')
|
||||
show = false
|
||||
}
|
||||
|
||||
$: classes = cx(
|
||||
'toast',
|
||||
`toast--${type}`,
|
||||
'shadow-small',
|
||||
$$props.class,
|
||||
)
|
||||
|
||||
|
||||
onMount(() => {
|
||||
if (images.length > 1) {
|
||||
incrementCurrentImageIndex()
|
||||
}
|
||||
|
||||
return () => {
|
||||
// Clear rotating words timeout
|
||||
if (imagesLoop) {
|
||||
clearTimeout(imagesLoop)
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
{#if show}
|
||||
<div
|
||||
class={classes}
|
||||
in:fly={{ y: '10%', duration: 1200, easing: quartOut }}
|
||||
out:fade={{ duration: 800, easing: quartOut }}
|
||||
>
|
||||
{#if images}
|
||||
<div class="media">
|
||||
<a href={cta.url}>
|
||||
{#each images as { id, title }, index}
|
||||
<Image
|
||||
class={index === currentImageIndex ? 'is-visible' : null}
|
||||
{id}
|
||||
sizeKey="square"
|
||||
sizes={{
|
||||
small: { width: 200, height: 200 },
|
||||
large: { width: 350, height: 350 },
|
||||
}}
|
||||
alt={title}
|
||||
/>
|
||||
{/each}
|
||||
</a>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="content">
|
||||
<p class="text">{@html text}</p>
|
||||
|
||||
{#if cta}
|
||||
<Button
|
||||
size="xsmall"
|
||||
text={cta.label}
|
||||
url={cta.url}
|
||||
color="pink"
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<button class="close" on:click={close} title="Close">
|
||||
<svg width="10" height="10">
|
||||
<use xlink:href="#cross" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
120
apps/website/src/style/molecules/_toast.scss
Normal file
120
apps/website/src/style/molecules/_toast.scss
Normal file
@@ -0,0 +1,120 @@
|
||||
.toast {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px;
|
||||
padding-right: 28px;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
|
||||
@include bp (md) {
|
||||
padding-right: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
// Media
|
||||
.media {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
flex: 0 0 clamp(40px, 14vw, 64px);
|
||||
aspect-ratio: 1 / 1.25;
|
||||
height: 100%;
|
||||
margin-right: 16px;
|
||||
border-radius: 4px;
|
||||
|
||||
@include bp (md) {
|
||||
flex: 0 0 min(7vw, 104px);
|
||||
margin-right: 24px;
|
||||
}
|
||||
|
||||
a {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
:global(picture) {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: 0;
|
||||
transform: scale(1.075);
|
||||
transition: opacity 0.8s, transform 1.6s var(--ease-quart);
|
||||
}
|
||||
:global(img) {
|
||||
object-position: center 32%;
|
||||
}
|
||||
|
||||
:global(.is-visible) {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
:global(img) {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
|
||||
@include bp (sm) {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Content
|
||||
.content {
|
||||
color: $color-text;
|
||||
font-size: rem(14px);
|
||||
|
||||
@include bp (md) {
|
||||
font-size: rem(16px);
|
||||
}
|
||||
|
||||
.text {
|
||||
margin-bottom: 12px;
|
||||
|
||||
@include bp (md) {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
:global(strong) {
|
||||
font-weight: normal;
|
||||
color: $color-secondary;
|
||||
}
|
||||
}
|
||||
|
||||
// Close
|
||||
.close {
|
||||
--size: 28px;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: var(--size);
|
||||
height: var(--size);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
@include bp (sm) {
|
||||
--size: 32px;
|
||||
}
|
||||
|
||||
:global(svg) {
|
||||
transition: transform 0.6s var(--ease-quart);
|
||||
}
|
||||
|
||||
// Hover
|
||||
&:hover {
|
||||
:global(svg) {
|
||||
transform: rotate(90deg) translateZ(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user