Merge branch 'dev' into 23-implement-unit-testing-helper-functions
This commit is contained in:
commit
9bb2689989
|
@ -1,3 +1,7 @@
|
||||||
|
/**
|
||||||
|
* @param {rating[]} ratings An array of ratings
|
||||||
|
* @returns {number} Average rating
|
||||||
|
*/
|
||||||
export const getAvgRating = (ratings) => {
|
export const getAvgRating = (ratings) => {
|
||||||
if (!ratings) return 0;
|
if (!ratings) return 0;
|
||||||
if (ratings.length === 1) return ratings[0].rating;
|
if (ratings.length === 1) return ratings[0].rating;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
export * from './toProduct';
|
export * from './toProduct';
|
||||||
export * from './parse';
|
export * from './parse';
|
||||||
export * from './normalize';
|
export * from './getAvgRating';
|
||||||
export * from './getAvgRating';
|
export * from './param';
|
|
@ -1,2 +0,0 @@
|
||||||
export const normalize = (str) => str.toLowerCase().trim()
|
|
||||||
|
|
|
@ -0,0 +1,36 @@
|
||||||
|
import { normalize, parseSlug, parseName } from './';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets parameter from URL of field. Defaults to 'product'
|
||||||
|
* @param {string} field='product'
|
||||||
|
* @returns {string} parameter of field
|
||||||
|
*/
|
||||||
|
export const getProdParam = (field = 'product') =>
|
||||||
|
new URLSearchParams(window.location.search).get(field) || ''
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds a product in products store with matching name as parameter.
|
||||||
|
* @param {*} param parameter
|
||||||
|
* @param {*} $products products store
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const findProdFromParam = (param, $products) =>
|
||||||
|
$products.find(({ name }) => normalize(name) === parseSlug(param)) || {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resets params and navigates back to index
|
||||||
|
*/
|
||||||
|
export const resetParams = () => history.pushState({}, '', new URL(window.location.origin));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates a url with search parameters with name of product parameter.
|
||||||
|
* @param {URL} url a URL object
|
||||||
|
* @param {object} product
|
||||||
|
* @returns {URL} a URL object with search parameters appended.
|
||||||
|
*/
|
||||||
|
export const setUrlParam = (url, product) => {
|
||||||
|
url.searchParams.set('product', parseName(product.name));
|
||||||
|
return url;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,22 @@
|
||||||
import {normalize }from './normalize';
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} str Any string
|
||||||
|
* @returns {string} Lowercased string without any extra spaces
|
||||||
|
*/
|
||||||
|
export const normalize = (str) => str.toLowerCase().trim()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} slug Slug of a product name
|
||||||
|
* @returns Normalized slug with all dashes replaced with spaces
|
||||||
|
*/
|
||||||
export function parseSlug(slug) {
|
export function parseSlug(slug) {
|
||||||
return normalize(slug).replaceAll("-", " ")
|
return normalize(slug).replaceAll("-", " ")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} name Product name
|
||||||
|
* @returns Normalized name with all spaces replaced with '-'
|
||||||
|
*/
|
||||||
export function parseName(name) {
|
export function parseName(name) {
|
||||||
return normalize(name).replaceAll(" ","-")
|
return normalize(name).replaceAll(" ","-")
|
||||||
}
|
}
|
|
@ -1,8 +1,24 @@
|
||||||
import {normalize, parseName} from './index';
|
import { parseSlug, normalize, setUrlParam, resetParams } from './';
|
||||||
|
|
||||||
export const toProduct = (product, currentProduct) => {
|
/**
|
||||||
const url = new URL(window.location);
|
* Updates Window URL and checks that it is equal to product name
|
||||||
url.searchParams.set('product', normalize(parseName(product.name)));
|
* @param {URL} url a URL object
|
||||||
window.history.pushState({}, '', url);
|
* @param {product} product product to check URL against
|
||||||
currentProduct.set(product)
|
* @returns {Boolean} True if URL parameter is equal to product name
|
||||||
}
|
*/
|
||||||
|
const didWinUrlUpdate = (url, product) => {
|
||||||
|
window.history.pushState(window.history.state, '', url);
|
||||||
|
const params = new URLSearchParams(window.location.search).get('product');
|
||||||
|
return parseSlug(params) === normalize(product.name);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Takes a `product` and `currentProduct` store
|
||||||
|
* Sets URL params to parsed product name and updates Browser URL
|
||||||
|
* Sets `currentProduct` store to `product`.
|
||||||
|
* @param {object} product The product to switch feature to.
|
||||||
|
* @param {store} currentProduct currentProduct store
|
||||||
|
*/
|
||||||
|
export const toProduct = (product, currentProduct) =>
|
||||||
|
didWinUrlUpdate(setUrlParam(new URL(window.location), product), product)?
|
||||||
|
currentProduct.set(product): resetParams()
|
|
@ -2,6 +2,7 @@
|
||||||
import { urlFor } from '$lib/sanityClient';
|
import { urlFor } from '$lib/sanityClient';
|
||||||
import { currentProduct } from '$lib/stores';
|
import { currentProduct } from '$lib/stores';
|
||||||
import Rating from './Rating/Rating.svelte';
|
import Rating from './Rating/Rating.svelte';
|
||||||
|
import PrevNext from './PrevNext/PrevNext.svelte';
|
||||||
import Tag from './Tag/Tag.svelte';
|
import Tag from './Tag/Tag.svelte';
|
||||||
|
|
||||||
const { container, imageView, name, description, productInfo, date, tags, img, ratings } = {
|
const { container, imageView, name, description, productInfo, date, tags, img, ratings } = {
|
||||||
|
@ -9,7 +10,7 @@
|
||||||
imageView: 'flex m-16 gap-12 w-100',
|
imageView: 'flex m-16 gap-12 w-100',
|
||||||
productInfo: 'flex flex-col gap-2',
|
productInfo: 'flex flex-col gap-2',
|
||||||
name: 'font-bold text-2xl',
|
name: 'font-bold text-2xl',
|
||||||
description: '',
|
description: 'leading-5',
|
||||||
date: 'text-xs w-2/3 mt-auto mb-4',
|
date: 'text-xs w-2/3 mt-auto mb-4',
|
||||||
tags: 'flex gap-1',
|
tags: 'flex gap-1',
|
||||||
img: 'p-8 border w-72 h-72 min-w-36 min-h-36 border-black border-2 p-3',
|
img: 'p-8 border w-72 h-72 min-w-36 min-h-36 border-black border-2 p-3',
|
||||||
|
@ -44,6 +45,7 @@
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
<PrevNext />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|
|
@ -0,0 +1,55 @@
|
||||||
|
<script>
|
||||||
|
import { currentProduct, productsView } from '$lib/stores';
|
||||||
|
import { toProduct } from '$helpers';
|
||||||
|
|
||||||
|
$: foundIndex = $productsView.findIndex((prod) => prod._id === $currentProduct._id);
|
||||||
|
$: PREV = $productsView[foundIndex - 1];
|
||||||
|
$: NEXT = $productsView[foundIndex + 1];
|
||||||
|
|
||||||
|
const navigate = (e) => {
|
||||||
|
if (e.target.name === 'prev' && PREV) {
|
||||||
|
toProduct(PREV, currentProduct);
|
||||||
|
}
|
||||||
|
if (e.target.name === 'next' && NEXT) {
|
||||||
|
toProduct(NEXT, currentProduct);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const { container, btn } = {
|
||||||
|
container: 'flex justify-between m-16 mt-2 mb-6 h-12 ',
|
||||||
|
btn: 'flex items-center gap-4'
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class={container}>
|
||||||
|
{#if PREV}
|
||||||
|
<button on:click={navigate} class={btn} name={'prev'}
|
||||||
|
><svg focusable="false" width={50} height={50} viewBox="0 0 24 24">
|
||||||
|
<path d="m14 7-5 5 5 5V7z" />
|
||||||
|
</svg><span class="text-left">{PREV.name}</span></button
|
||||||
|
>
|
||||||
|
{:else}
|
||||||
|
<div />
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if NEXT}
|
||||||
|
<button on:click={navigate} class={btn} name={'next'}
|
||||||
|
><span class="text-right">{NEXT.name}</span><svg
|
||||||
|
focusable="false"
|
||||||
|
width={50}
|
||||||
|
height={50}
|
||||||
|
viewBox="0 0 24 24"><path d="m10 17 5-5-5-5v10z" /></svg
|
||||||
|
></button
|
||||||
|
>
|
||||||
|
{:else}
|
||||||
|
<div />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
svg,
|
||||||
|
path,
|
||||||
|
span {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -39,7 +39,7 @@
|
||||||
};
|
};
|
||||||
|
|
||||||
const { container, filterBar, filterTitle, filterSort } = {
|
const { container, filterBar, filterTitle, filterSort } = {
|
||||||
container: 'flex flex-col text-sm h-auto mb-4 mr-6',
|
container: 'flex flex-col text-sm h-auto mb-4 mr-6 mt-4',
|
||||||
filterBar: 'pl-8 p-2 pr-2 flex justify-between',
|
filterBar: 'pl-8 p-2 pr-2 flex justify-between',
|
||||||
filterTitle: 'font-bold',
|
filterTitle: 'font-bold',
|
||||||
filterSort: 'p-1 pl-9 mr-12 hover:bg-blue-300 w-full focus:outline-none'
|
filterSort: 'p-1 pl-9 mr-12 hover:bg-blue-300 w-full focus:outline-none'
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
export let reset;
|
export let reset;
|
||||||
const { title } = {
|
const { title } = {
|
||||||
title: 'hover:bg-blue-300 text-lg font-bold m-12 ml-0 pl-12'
|
title: 'hover:bg-blue-300 text-lg font-bold p-12 pl-12 pt-11 snap-start sticky top-0 bg-white h-28 shadow-sm shadow-white'
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
const { container, productStyle } = {
|
const { container, productStyle } = {
|
||||||
container: 'pt-4',
|
container: 'pt-4',
|
||||||
productList: 'flex flex-col items-start mt-10 text-sm',
|
productList: 'flex flex-col items-start mt-10 text-sm',
|
||||||
productStyle: 'w-full text-left'
|
productStyle: 'w-full text-left snap-start snap-always'
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -14,7 +14,7 @@
|
||||||
{#each productsView as product}
|
{#each productsView as product}
|
||||||
<button class={productStyle} on:click={() => toProduct(product, currentProduct)}>
|
<button class={productStyle} on:click={() => toProduct(product, currentProduct)}>
|
||||||
<p
|
<p
|
||||||
class={`pl-12 hover:bg-gray-200 ${
|
class={`pl-10 hover:bg-gray-200 ${
|
||||||
$currentProduct && $currentProduct.name === product.name ? 'bg-blue-300' : ''
|
$currentProduct && $currentProduct.name === product.name ? 'bg-blue-300' : ''
|
||||||
} text-sm`}
|
} text-sm`}
|
||||||
>
|
>
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
};
|
};
|
||||||
|
|
||||||
const { container, input } = {
|
const { container, input } = {
|
||||||
container: 'h-16 ml-auto mr-auto',
|
container: 'h-12 ml-auto mr-auto flex justify-center items-center',
|
||||||
input: 'outline outline-1 w-36 focus:outline-blue-600 p-1 text-sm'
|
input: 'outline outline-1 w-36 focus:outline-blue-600 p-1 text-sm'
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -16,22 +16,25 @@
|
||||||
filters = { selectedCat: 0, selectedRating: 0 };
|
filters = { selectedCat: 0, selectedRating: 0 };
|
||||||
};
|
};
|
||||||
|
|
||||||
const { main, sidebar } = {
|
const { main, container, sidebar } = {
|
||||||
main: 'flex w-screen h-screen sfmono',
|
main: 'flex w-screen h-screen sfmono',
|
||||||
sidebar: 'flex flex-col justify-start h-screen overflow-auto w-52 shrink-0'
|
container: 'flex flex-col h-screen justify-start shrink-0 overflow-auto w-56',
|
||||||
|
sidebar: ' flex flex-col shrink-0'
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<main class={main}>
|
<main class={main}>
|
||||||
<div class={sidebar}>
|
<div class={container}>
|
||||||
<Header {reset} />
|
<Header {reset} />
|
||||||
{#if !Object.keys($currentProduct).length}
|
<div class={sidebar}>
|
||||||
<Search />
|
<Search />
|
||||||
|
{#if !Object.keys($currentProduct).length}
|
||||||
<Filters bind:filters {reset} />
|
<Filters bind:filters {reset} />
|
||||||
<Sort />
|
<Sort />
|
||||||
{:else}
|
{:else}
|
||||||
<Products productsView={$productsView} {currentProduct} />
|
<Products productsView={$productsView} {currentProduct} />
|
||||||
{/if}
|
{/if}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<slot />
|
<slot />
|
||||||
</main>
|
</main>
|
||||||
|
|
|
@ -1,44 +1,32 @@
|
||||||
<script>
|
<script>
|
||||||
|
import { browser } from '$app/env';
|
||||||
import Grid from '$lib/Grid.svelte';
|
import Grid from '$lib/Grid.svelte';
|
||||||
import Feature from '$lib/Feature/Feature.svelte';
|
import Feature from '$lib/Feature/Feature.svelte';
|
||||||
import { products, productsView, tags, currentProduct, emotions } from '$lib/stores';
|
import { products, productsView, tags, currentProduct, emotions } from '$lib/stores';
|
||||||
import { browser } from '$app/env';
|
import { findProdFromParam, getProdParam, resetParams } from '$helpers';
|
||||||
import {normalize, parseSlug } from '$helpers';
|
|
||||||
|
|
||||||
export let data;
|
export let data;
|
||||||
|
|
||||||
products.set(data.products);
|
products.set(data.products);
|
||||||
tags.set(data.tags);
|
|
||||||
emotions.set(data.emotions)
|
|
||||||
|
|
||||||
productsView.set(data.products);
|
productsView.set(data.products);
|
||||||
const goToProduct = () => {
|
tags.set(data.tags);
|
||||||
const params = new URLSearchParams(window.location.search);
|
emotions.set(data.emotions);
|
||||||
const paramProd = params.get('product');
|
|
||||||
|
|
||||||
if (paramProd) {
|
const load = () => {
|
||||||
const foundProduct = $products.find(
|
currentProduct.set(findProdFromParam(getProdParam(), $products));
|
||||||
({ name }) => normalize(name) === normalize(parseSlug(paramProd))
|
if (getProdParam() && JSON.stringify($currentProduct) === '{}') resetParams();
|
||||||
);
|
|
||||||
if (foundProduct) {
|
|
||||||
currentProduct.set(foundProduct);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const url = new URL(window.location.origin);
|
|
||||||
history.pushState({}, '', url);
|
|
||||||
currentProduct.set({});
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (browser) {
|
if (browser) {
|
||||||
goToProduct();
|
load();
|
||||||
window.onpopstate = () => {
|
window.onpopstate = () => {
|
||||||
goToProduct();
|
load();
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const { container} = {
|
const { container } = {
|
||||||
container: 'h-screen overflow-auto w-screen'
|
container: 'h-screen overflow-auto w-screen'
|
||||||
}
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.8 KiB |
|
@ -4,7 +4,7 @@ import { configDefaults } from 'vitest/config';
|
||||||
|
|
||||||
/** @type {import('vite').UserConfig} */
|
/** @type {import('vite').UserConfig} */
|
||||||
const config = {
|
const config = {
|
||||||
plugins: [sveltekit()],
|
plugins: [sveltekit()],
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
$lib: resolve('./src/lib'),
|
$lib: resolve('./src/lib'),
|
||||||
|
|
Loading…
Reference in New Issue