🔥 Finish Cart implementation

- Use Select component in CartItem to change quantity
- Visual update when removing or changing an item quantity
- API: Remove Cart item action
- API: Handle Cart item adding and updating (quantity)
This commit is contained in:
2021-11-07 20:21:20 +01:00
parent e1c259164f
commit f7457b5f8d
8 changed files with 290 additions and 105 deletions

View File

@@ -1,17 +1,21 @@
<script lang="ts"> <script lang="ts">
import { createEventDispatcher } from 'svelte'; import { createEventDispatcher } from 'svelte'
// Components
import ButtonCircle from '$components/atoms/ButtonCircle.svelte'
import Select from '$components/molecules/Select.svelte'
export let item: any export let item: any
export let index: number
// console.log(item)
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
const quantityLimit = 5
// When changing item quantity // When changing item quantity
const updateQuantity = ({ target: { value }}: any) => { const updateQuantity = ({ detail }: any) => {
dispatch('updatedQuantity', { dispatch('updatedQuantity', {
id: item.id, id: item.id,
quantity: Number(value) quantity: Number(detail)
}) })
} }
@@ -21,29 +25,39 @@
} }
</script> </script>
<aside class="cart-item shadow-small"> <div class="cart-item shadow-small">
<div class="cart-item__left"> <div class="cart-item__left">
<img <img src={item.product.images[0].file.url} width={200} height={300} alt={item.product.name}>
src={item.product.images[0].file.url}
width={200}
height={300}
alt={item.product.name}
>
</div> </div>
<div class="cart-item__right"> <div class="cart-item__right">
<h3>Poster</h3> <h3>Poster</h3>
<p>{item.product.name} {item.price_total}</p> <p>{item.product.name} {item.price_total}</p>
{#if item && item.product} {#if item && item.quantity}
<div class="select"> <Select
<select on:change={updateQuantity}> name="sort" id="filter_sort"
{#each Array(5) as _, index} options={[...Array(item.quantity <= quantityLimit ? quantityLimit : item.quantity)].map((_, index) => {
<option value={index + 1} selected={item.quantity - 1 === index}>{index + 1}</option> return {
{/each} value: `${index + 1}`,
</select> name: `${index + 1}`,
</div> default: index === 0,
selected: index + 1 === item.quantity,
}
})}
on:change={updateQuantity}
value={String(item.quantity)}
>
<span>Quantity:</span>
</Select>
{/if} {/if}
<button on:click={removeItem}>Remove</button> <ButtonCircle class="remove"
size="tiny" color="gray"
on:click={removeItem}
>
<svg width="8" height="8" viewBox="0 0 8 8" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.81 1.104a.647.647 0 1 0-.914-.915L4 3.085 1.104.19a.647.647 0 1 0-.915.915L3.085 4 .19 6.896a.647.647 0 1 0 .915.915L4 4.915 6.896 7.81a.647.647 0 1 0 .915-.915L4.915 4 7.81 1.104Z" fill="#666"/>
</svg>
</ButtonCircle>
</div> </div>
</aside> </div>

View File

@@ -2,7 +2,7 @@
import { onMount } from 'svelte' import { onMount } from 'svelte'
import { fade, fly } from 'svelte/transition' import { fade, fly } from 'svelte/transition'
import { quartOut } from 'svelte/easing' import { quartOut } from 'svelte/easing'
import { cartOpen, cartId, cartData, cartAmount } from '$utils/store' import { cartOpen, cartId, cartData, cartAmount, cartIsUpdating } from '$utils/store'
// Components // Components
import Button from '$components/atoms/Button.svelte' import Button from '$components/atoms/Button.svelte'
import CartItem from '$components/molecules/CartItem.svelte' import CartItem from '$components/molecules/CartItem.svelte'
@@ -25,7 +25,7 @@
const cart = await existantCart.json() const cart = await existantCart.json()
$cartId = cart.id $cartId = cart.id
$cartData = cart $cartData = cart
console.log('fetched existant cart:', $cartId, $cartData) // console.log('Fetched existant cart:', $cartId, $cartData)
} }
} }
// Cart doesn't exists // Cart doesn't exists
@@ -41,7 +41,7 @@
const cart = await newCart.json() const cart = await newCart.json()
$cartId = cart.id $cartId = cart.id
$cartData = cart $cartData = cart
console.log('new cart:', localStorage.getItem('cartId')) // console.log('Created new cart:', localStorage.getItem('cartId'))
} }
} }
}) })
@@ -54,43 +54,53 @@
// Item quantity changed // Item quantity changed
const changedQuantity = async ({ detail: { id, quantity } }) => { const changedQuantity = async ({ detail: { id, quantity } }) => {
// Update item in cart // Cart is now updating
// const updatedCart = await swell.cart.updateItem(id, { $cartIsUpdating = true
// quantity
// })
// if (updatedCart) {
// $cartData = updatedCart
// }
// Get updated cart
const updatedCart = await fetch('/api/swell', { const updatedCart = await fetch('/api/swell', {
method: 'POST', method: 'POST',
body: JSON.stringify({ body: JSON.stringify({
action: 'updateCartItem', action: 'updateCartItem',
cartId: $cartId, cartId: $cartId,
productId: id, productId: id,
quantity: quantity quantity,
}) })
}) })
if (updatedCart.ok) { if (updatedCart.ok) {
// const cart = await updatedCart.json() const cart = await updatedCart.json()
// $cartData = cart $cartData = cart
// console.log('updated cart:', $cartData.items) // Cart is updated
$cartIsUpdating = false
} }
} }
// Item removed // Item removed
const removedItem = async ({ detail: id }) => { const removedItem = async ({ detail: id }) => {
// Cart is now updating
$cartIsUpdating = true
// Remove item from cart // Remove item from cart
// const removedItem = await swell.cart.removeItem(id) const updatedCart = await fetch('/api/swell', {
// if (removedItem) { method: 'POST',
// $cartData = removedItem body: JSON.stringify({
// } action: 'removeCartItem',
cartId: $cartId,
productId: id,
})
})
if (updatedCart.ok) {
const cart = await updatedCart.json()
$cartData = cart
// Cart is updated
$cartIsUpdating = false
}
} }
</script> </script>
{#if $cartOpen} {#if $cartOpen}
<aside class="cart shadow-box-dark" <aside class="cart shadow-box-dark"
class:is-updating={$cartIsUpdating}
transition:fly={{ x: 48, duration: 600, easing: quartOut }} transition:fly={{ x: 48, duration: 600, easing: quartOut }}
> >
<header class="cart__heading"> <header class="cart__heading">
@@ -100,20 +110,28 @@
<div class="cart__content"> <div class="cart__content">
{#if $cartAmount > 0} {#if $cartAmount > 0}
{#each $cartData.items as item} {#each $cartData.items as item, index}
<CartItem {item} <CartItem {item} {index}
on:updatedQuantity={changedQuantity} on:updatedQuantity={changedQuantity}
on:removed={removedItem} on:removed={removedItem}
/> />
{/each} {/each}
{:else} {:else}
<div class="cart__message shadow-small"> <div class="cart__empty shadow-small">
<div class="icon"> <div class="icon">
<svg width="23" height="29" viewBox="0 0 23 29" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.95 5.83a4.94 4.94 0 0 1 1.23-3.42 3.8 3.8 0 0 1 2.8-1.2c2.07.02 4 1.67 4.04 4.62h4.13c.6 0 1.08.52 1.03 1.11l-.02.32 2.68 18.19a.94.94 0 0 1-1.26 1.02l-1.02-.38a.09.09 0 0 0-.08.01l-1.84 1.2c-.08.5-.5.88-1.02.88H1.78c-.6 0-1.07-.51-1.03-1.1l1.53-20.3c.04-.53.5-.95 1.03-.95h3.64Zm1.2 0c.01-1.2.4-2.05.9-2.6a2.6 2.6 0 0 1 1.93-.81c1.34 0 2.8 1.07 2.84 3.41H8.15Zm6.87 1.2h3.95l-.02.27-1.48 19.68H1.97l1.5-19.95h3.48v3.41a1.38 1.38 0 1 0 1.2 0v-3.4h5.67v3.4a1.38 1.38 0 1 0 1.2 0v-3.4Zm3.74 18.77.99-13.1 1.84 12.5-.62-.23c-.38-.14-.81-.1-1.15.13l-1.06.7ZM7.87 11.68a.33.33 0 1 1-.65 0 .33.33 0 0 1 .65 0Zm6.55.33a.33.33 0 1 0 0-.66.33.33 0 0 0 0 .66Z"/>
</svg>
</div> </div>
<p>Your cart is empty</p> <p>Your cart is empty</p>
</div> </div>
{/if} {/if}
{#if !$cartData || $cartIsUpdating}
<div class="cart__update">
<p>Updating…</p>
</div>
{/if}
</div> </div>
<footer class="cart__total"> <footer class="cart__total">

View File

@@ -1,4 +1,4 @@
import { addToCart, createCart, fetchCart, getProduct, updateCartItem } from '$utils/swellFunctions' import { addToCart, createCart, fetchCart, getProduct, removeCartItem, updateCartItem } from '$utils/swellFunctions'
// Block GET requests // Block GET requests
@@ -41,11 +41,13 @@ export async function post ({ headers, query, body, params, ...rest }) {
result = await updateCartItem(cartId, productId, bodyParsed.quantity) result = await updateCartItem(cartId, productId, bodyParsed.quantity)
break break
} }
case 'removeCartItem': {
result = await removeCartItem(cartId, productId)
break
}
default: break default: break
} }
} }
return { return {
status: 200, status: 200,
body: result, body: result,

View File

@@ -1,11 +1,14 @@
// Cart item // Cart item
.cart-item { .cart-item {
position: relative;
display: flex; display: flex;
background-color: #fff;
color: $color-gray;
margin-bottom: 24px;
border-radius: 6px;
align-items: center; align-items: center;
margin-bottom: 24px;
background: #fff;
color: $color-gray;
border-radius: 6px;
overflow: hidden;
transition: opacity 0.3s var(--ease-quart);
&:last-child { &:last-child {
margin-bottom: 0; margin-bottom: 0;
@@ -13,14 +16,14 @@
// Left Image // Left Image
&__left { &__left {
margin-right: 20px;
width: 100px; width: 100px;
height: 150px; height: 150px;
margin-right: 20px;
@include bp (sm) { @include bp (sm) {
margin-right: 32px;
width: 124px; width: 124px;
height: 180px; height: 180px;
margin-right: 32px;
} }
img { img {
@@ -32,7 +35,11 @@
} }
} }
// Details
&__right { &__right {
display: flex;
flex-direction: column;
// Poster Title // Poster Title
h3 { h3 {
font-family: $font-serif; font-family: $font-serif;
@@ -45,14 +52,71 @@
} }
// Text // Text
p { p {
font-size: rem(12px);
line-height: 1.4;
max-width: 124px; max-width: 124px;
margin: 8px 0 20px; margin: 8px 0 20px;
font-size: rem(12px);
line-height: 1.4;
@include bp (sm) { @include bp (sm) {
font-size: rem(13px); font-size: rem(13px);
} }
} }
// Select
.select {
position: relative;
display: flex;
align-items: center;
height: 28px;
margin-right: auto;
padding: 0 12px;
border: 1px solid rgba($color-lightgray, 0.3);
border-radius: 100vh;
cursor: pointer;
transition: border-color 0.4s var(--ease-quart);
span {
display: block;
font-size: rem(12px);
color: $color-text;
& + span {
margin-left: 4px;
}
}
select {
position: absolute;
z-index: 1;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0;
cursor: pointer;
text-align: center;
}
&:after {
content: "";
display: block;
width: 8px;
height: 5px;
margin-left: 4px;
color: red;
background: url('data:image/svg+xml; utf8, <svg width="8" height="5" viewBox="0 0 8 5" fill="none" xmlns="http://www.w3.org/2000/svg">\
<path d="m1 1 3 3 3-3" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>\
</svg>');
}
// Hover
&:hover {
border-color: $color-secondary-light;
}
}
// Remove Icon
.remove {
position: absolute;
top: 16px;
right: 16px;
}
} }
} }

View File

@@ -1,12 +1,13 @@
.cart { .cart {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
overflow: hidden;
padding: 20px 20px 24px;
background-color: $color-cream; background-color: $color-cream;
border-radius: 8px; border-radius: 8px;
padding: 20px 20px 24px;
@include bp (sm) { @include bp (sm) {
padding: 24px 32px; padding: 24px 32px 32px;
} }
// Heading // Heading
@@ -51,21 +52,10 @@
// Content // Content
&__content { &__content {
position: relative;
flex: 1; flex: 1;
} }
// Message
&__message {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
padding: 24px;
background: #fff;
color: $color-gray;
border-radius: 6px;
}
// Total // Total
&__total { &__total {
margin-top: auto; margin-top: auto;
@@ -73,7 +63,7 @@
color: $color-gray; color: $color-gray;
@include bp (md) { @include bp (md) {
margin: 32px 0; margin: 32px 0 0;
} }
// Sum // Sum
@@ -138,6 +128,67 @@
} }
} }
// Empty content
&__empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
padding: 24px;
font-size: rem(20px);
font-weight: 200;
background: #fff;
color: $color-gray;
border-radius: 6px;
// Icon
.icon {
display: flex;
align-items: center;
justify-content: center;
width: 64px;
height: 64px;
margin-bottom: 16px;
color: #FF6C89;
background: $color-cream;
border-radius: 100%;
}
}
// Updating message
&__update {
position: absolute;
z-index: -1;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
font-size: rem(20px);
color: $color-secondary;
transition: opacity 0.6s var(--ease-quart), transform 0.6s var(--ease-quart);
opacity: 0;
transform: translate3d(0, -8px, 0);
}
// Updating state
&.is-updating {
.cart-item {
opacity: 0.1;
pointer-events: none;
user-select: none;
}
.cart__update {
opacity: 1;
transform: translate3d(0,0,0);
}
}
// Overlay // Overlay
&-overlay { &-overlay {
position: fixed; position: fixed;

View File

@@ -3,10 +3,8 @@
// Cart // Cart
.cart { .cart {
// display: flex;
// display: none;
position: fixed; position: fixed;
z-index: 100; z-index: 101;
top: 72px; top: 72px;
right: 0; right: 0;
width: 100%; width: 100%;
@@ -25,7 +23,7 @@
.shop-location { .shop-location {
display: flex; display: flex;
position: fixed; position: fixed;
z-index: 20; z-index: 100;
top: 18px; top: 18px;
left: 20px; left: 20px;
right: 20px; right: 20px;

View File

@@ -5,7 +5,7 @@ import { writable, derived } from 'svelte/store'
* Shop * Shop
*/ */
/** Open Cart state */ /** Open Cart state */
export const cartOpen = writable(true) export const cartOpen = writable(false)
/** Current Cart ID */ /** Current Cart ID */
export const cartId = writable(null) export const cartId = writable(null)
@@ -21,6 +21,9 @@ if (typeof localStorage !== 'undefined') {
/** Raw Cart data */ /** Raw Cart data */
export const cartData = writable(null) export const cartData = writable(null)
/** Cart data is being updated */
export const cartIsUpdating = writable(false)
/** Amount of products present in cart */ /** Amount of products present in cart */
export const cartAmount = derived(cartData, ($cart) => { export const cartAmount = derived(cartData, ($cart) => {
return $cart && $cart.item_quantity > 0 ? $cart.item_quantity : 0 return $cart && $cart.item_quantity > 0 ? $cart.item_quantity : 0

View File

@@ -9,7 +9,9 @@ swell.init(import.meta.env.VITE_SWELL_STORE_ID, import.meta.env.VITE_SWELL_API_T
* Retrieve a product * Retrieve a product
*/ */
export const getProduct = async (id: string) => { export const getProduct = async (id: string) => {
const product = await swell.get(`/products/${id}`) const product = await swell.get('/products/{id}', {
id
})
if (product) { if (product) {
return product return product
@@ -21,7 +23,7 @@ export const getProduct = async (id: string) => {
* Create a cart * Create a cart
*/ */
export const createCart = async () => { export const createCart = async () => {
const cart = await swell.post('/carts', {}) const cart = await swell.post('/carts')
if (cart) { if (cart) {
return cart return cart
@@ -32,9 +34,9 @@ export const createCart = async () => {
/** /**
* Retrieve cart * Retrieve cart
*/ */
export const fetchCart = async (cartId: string) => { export const fetchCart = async (id: string) => {
const cart = await swell.get('/carts/{id}', { const cart = await swell.get('/carts/{id}', {
id: cartId, id,
expand: [ expand: [
'items.product', 'items.product',
'items.variant', 'items.variant',
@@ -51,21 +53,26 @@ export const fetchCart = async (cartId: string) => {
* Add product to cart * Add product to cart
*/ */
export const addToCart = async (cartId: string, productId: string, quantity: number) => { export const addToCart = async (cartId: string, productId: string, quantity: number) => {
// TODO: Update current product quantity if adding again, otherwise add new product to existing items // Fetch current cart data
const currentCart = await fetchCart(cartId)
// Updated cart with new items
const updatedCart = await swell.put('/carts/{id}', { const updatedCart = await swell.put('/carts/{id}', {
id: cartId, id: cartId,
items: [{ items: [
...currentCart.items || [],
{
product_id: productId, product_id: productId,
quantity: quantity, quantity,
expand: [ }
'items.product', ],
'items.variant',
]
}],
}) })
if (updatedCart) { if (updatedCart) {
return updatedCart // Fetch latest cart with updates
const cart = await fetchCart(cartId)
if (cart) {
return cart
}
} }
} }
@@ -77,26 +84,54 @@ export const updateCartItem = async (cartId: string, productId: string, quantity
// Fetch current cart data // Fetch current cart data
const currentCart = await fetchCart(cartId) const currentCart = await fetchCart(cartId)
// Update cart
// const itemToUpdate = currentCart.items.find((item: any) => item.id === productId)
// itemToUpdate.quantity = quantity
// Updated items with replacing new item quantity // Updated items with replacing new item quantity
const updatedCartItems = currentCart.items.map((item: any) => { const updatedCartItems = currentCart.items.map((item: any) => {
console.log(item) // Replace item quantity with selected one
if (item.id === productId) {
item.quantity = quantity
}
return item return item
}) })
// const updatedCart = await swell.put('/carts/{id}', { const updatedCart = await swell.put('/carts/{id}', {
// id: cartId, id: cartId,
// items: updatedItems, $set: {
// }) items: updatedCartItems,
}
// console.log(updatedCart) })
if (updatedCart) {
return currentCart // Fetch latest cart with updates
// if (updatedCart) { const cart = await fetchCart(cartId)
// return {} if (cart) {
// // return updatedCart return cart
// } }
}
}
/**
* Remove cart item
*/
export const removeCartItem = async (cartId: string, productId: string) => {
// Fetch current cart data
const currentCart = await fetchCart(cartId)
// Updated items and remove selected item
const updatedCartItems = [...currentCart.items.filter((item: any) => {
return item.id !== productId
})]
const updatedCart = await swell.put('/carts/{id}', {
id: cartId,
$set: {
items: updatedCartItems,
}
})
if (updatedCart) {
// Fetch latest cart with updates
const cart = await fetchCart(cartId)
if (cart) {
return cart
}
}
} }