Update Helper Functions (#63)

- adjusted filter fns and added ci
This commit is contained in:
Xinrui Chen 2022-09-03 19:43:36 -07:00 committed by GitHub
parent 0279675828
commit 5d43a1186d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 213 additions and 115 deletions

31
.github/workflows/test.yml vendored Normal file
View File

@ -0,0 +1,31 @@
# This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
name: Testing CI
on:
push:
branches: [ "dev", "main" ]
pull_request:
branches: [ "dev", "main" ]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [16.x]
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
steps:
- uses: actions/checkout@v3
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- run: npm install
- run: npm run test:unit

View File

@ -1,23 +1,42 @@
import { getAvgRating } from '$helpers';
import type { Product, Tag } from '$types';
import { avgRating } from '$helpers';
import type { FullProduct, Tag } from '$types';
export type TagArgs = {
value: string;
$tags: Tag[];
};
export type RatingArgs = {
value: string | number;
};
/** Default value for filter selectors, saved in store */
export const defaultFilter = { selectedCat: 0, selectedRating: 0 };
/** Filters products by matching tags according to passed value */
export const filterByCat = (products: Product[], value: string, tags: Tag[]): Product[] =>
/** Rating greater than what is passed in */
export const _ratingHigher = (product: FullProduct, { value }: RatingArgs) =>
product.rating && avgRating(product.rating) >= Number(value);
/** Matches tags according to passed value */
export const _matchesTag = (product: FullProduct, { value, $tags }: TagArgs): boolean => {
try {
if (product.tags) {
const selectedTagId = $tags.find((tag) => tag.name === value)._id;
const productTags = product.tags.map((tag) => tag._ref);
if (productTags.includes(selectedTagId)) return true;
} else return false;
} catch {
return true;
}
};
/** Filters products */
export const filterProductsBy = (type: string, products: FullProduct[], config) =>
products.filter((product) => {
try {
if (product.tags) {
const selectedTagId = tags.find((tag) => tag.name === value)._id;
const productTags = product.tags.map((tag) => tag._ref);
if (productTags.includes(selectedTagId)) return true;
} else return false;
} catch {
return true;
switch (type) {
case 'tag':
return _matchesTag(product, config);
case 'rating':
return _ratingHigher(product, config);
}
});
/** Filters by rating greater than what is passed in */
export const filterByRating = (products: Product[], value: string | number) =>
products.filter((product) => product.rating && getAvgRating(product.rating) >= Number(value));

View File

@ -1,5 +1,5 @@
export * from './toProduct';
export * from './parse';
export * from './getAvgRating';
export * from './utils/parse';
export * from './utils/avgRating';
export * from './param';
export * from './filters'
export * from './filters';

View File

@ -1,22 +1,26 @@
import { filterByCat, filterByRating } from '$helpers';
import { filterProductsBy } from '$helpers';
import { describe, expect, test } from 'vitest';
import { testProducts, testTags } from './data';
describe('filterByCat', () => {
describe('filterProducts', () => {
test('filters products by passed category tag', () => {
expect(filterByCat(testProducts, 'Exercise', testTags)).toHaveLength(1);
expect(filterByCat(testProducts, 'Electronics', testTags)).toHaveLength(1);
expect(
filterProductsBy('tag', testProducts, { value: 'Exercise', $tags: testTags })
).toHaveLength(1);
expect(
filterProductsBy('tag', testProducts, { value: 'Electronics', $tags: testTags })
).toHaveLength(1);
});
test('returns unchanged products if tag does not exist', () => {
expect(filterByCat(testProducts, 'grapes', testTags)).toEqual(testProducts);
});
});
describe('filterByRating', () => {
test('filterByRating returns products only greater than # passed in', () => {
expect(filterByRating(testProducts, 3)).toHaveLength(3);
expect(filterByRating(testProducts, 5)).toHaveLength(0);
test('filter by tag returns unchanged products if tag does not exist', () => {
expect(filterProductsBy('tag', testProducts, { value: 'grapes', $tags: testTags })).toEqual(
testProducts
);
});
test('filters ratings only greater than # passed in', () => {
expect(filterProductsBy('rating', testProducts, { value: 3 })).toHaveLength(3);
expect(filterProductsBy('rating', testProducts, { value: 5 })).toHaveLength(0);
});
});

View File

@ -1,21 +1,19 @@
import { getAvgRating } from '$helpers';
import { avgRating } from '$helpers';
import { test, expect } from 'vitest';
import { testRating1, testRating2 } from './data';
test('return false if no parameters are passed', async () => {
expect(getAvgRating()).toBeFalsy();
expect(avgRating()).toBeFalsy();
});
test('return false if array with empty object is passed', async () => {
expect(getAvgRating([{}])).toBeFalsy();
expect(avgRating([{}])).toBeFalsy();
});
test('return single rating if only one rating is passed', async () => {
expect(getAvgRating([testRating1])).toBe(testRating1.rating);
expect(avgRating([testRating1])).toBe(testRating1.rating);
});
test('return average of 2 ratings passed', async () => {
expect(getAvgRating([testRating1, testRating2])).toBe(
(testRating1.rating + testRating2.rating) / 2
);
expect(avgRating([testRating1, testRating2])).toBe((testRating1.rating + testRating2.rating) / 2);
});

View File

@ -1,7 +1,7 @@
import type { Rating } from '$types';
/** Average of all ratings of a product */
export const getAvgRating = (ratings: Rating[]): number => {
export const avgRating = (ratings: Rating[]): number => {
if (!ratings) return 0;
if (ratings.length === 1) return ratings[0].rating;
else return ratings.reduce((total, curr) => total + curr.rating, 0) / ratings.length;

View File

@ -4,55 +4,42 @@
import Rating from './Rating/Rating.svelte';
import PrevNext from './PrevNext/PrevNext.svelte';
import Tag from './Tag/Tag.svelte';
const {
container,
imageView,
subname,
description,
productInfo,
date,
tags,
img,
ratings
} = {
container: 'flex flex-col',
imageView: 'flex m-16 gap-12 w-100',
productInfo: 'flex flex-col gap-2',
subname: '-mb-2 font-light text-sm',
description: 'leading-5',
date: 'text-xs w-2/3 mt-auto mb-4',
tags: 'flex gap-1 mb-1',
img: 'p-8 border w-72 h-72 min-w-36 min-h-36 border-black border-2 p-3',
ratings: 'w-100'
};
</script>
<div class={container}>
<div class={imageView}>
<div class="flex flex-col">
<div class="flex m-16 gap-12 w-full">
{#if $currentProduct.image}
<img src={urlFor($currentProduct.image).url()} class={img} alt={$currentProduct.name} />
<img
src={urlFor($currentProduct.image).url()}
class="min-w-36 min-h-36 w-72 h-72 border-black border-2 p-3"
alt={$currentProduct.name}
/>
{/if}
<div class={productInfo}>
<div class="productInfo">
{#if $currentProduct.subname}
<span class={subname}>{$currentProduct.subname}</span>
<span class="subname">{$currentProduct.subname}</span>
{/if}
<a href={$currentProduct?.url} target="_blank" class={`font-bold text-2xl title ${$currentProduct.url? 'text-sky-600':''}`}>{$currentProduct.name}</a>
<a
href={$currentProduct?.url}
target="_blank"
class={`font-bold text-2xl title ${$currentProduct.url ? 'text-sky-600' : ''}`}
>{$currentProduct.name}</a
>
{#if $currentProduct.tags}
<div class={tags}>
<div class="tags">
{#each $currentProduct.tags as tag (tag._ref)}
<Tag {tag} />
{/each}
</div>
{/if}
{#if $currentProduct.description}
<p class={description}>{$currentProduct.description}</p>
<p class="description">{$currentProduct.description}</p>
{/if}
<p class={date}>Created on: {new Date($currentProduct._createdAt)}</p>
<p class="date">Created on: {new Date($currentProduct._createdAt)}</p>
</div>
</div>
{#if $currentProduct.rating}
<div class={ratings}>
<div class="ratings">
{#each $currentProduct.rating as rating (rating._key)}
<Rating {rating} />
{/each}
@ -61,12 +48,31 @@
<PrevNext />
</div>
<style>
<style lang="postcss">
.productInfo {
@apply flex flex-col gap-2;
}
.subname {
@apply -mb-2 font-light text-sm;
}
.description {
@apply leading-5;
}
.date {
@apply text-xs w-2/3 mt-auto mb-4;
}
.tags {
@apply flex gap-1 mb-1;
}
.ratings {
@apply w-full;
}
img {
image-rendering: pixelated;
}
.title:hover, img:hover{
.title:hover,
img:hover {
background-image: url('dither.gif');
background-repeat: repeat;
background-repeat: repeat;
}
</style>

View File

@ -1,28 +1,27 @@
<script>
export let tag;
import { tags, currentProduct, productsView, products, filters } from '$lib/stores';
import { filterByCat, resetParams } from '$helpers';
import { filterProductsBy, resetParams } from '$helpers';
const tagName = $tags.find((dirTag) => dirTag._id === tag._ref).name;
const filter = () => {
const updatedByCat = filterByCat($products, tagName, $tags);
currentProduct.set({});
productsView.set(updatedByCat);
productsView.set(filterProductsBy('tag', $products, { value: tagName, $tags }));
filters.set({ ...$filters, selectedCat: tagName });
resetParams();
};
const { container } = {
container: 'text-sm flex rounded-md pl-2 pr-2 pt-1 pb-1 bg-gray-200'
};
</script>
<button class={container} on:click={filter}>{tagName}</button>
<button class="container" on:click={filter}>{tagName}</button>
<style>
<style lang="postcss">
button:hover {
background-image: url('dither.gif');
background-repeat: repeat;
}
.container {
@apply text-sm flex rounded-md pl-2 pr-2 pt-1 pb-1 bg-gray-200 w-auto;
}
</style>

View File

@ -1,48 +1,61 @@
<script>
import { productsView, products, tags, filters } from '$lib/stores';
import { normalize, filterByRating, filterByCat } from '$helpers';
import { normalize, filterProductsBy } from '$helpers';
export let reset;
let { selectedCat, selectedRating } = $filters;
const stars = ['⭐ ⬆', '⭐⭐ ⬆', '⭐⭐⭐ ⬆', '⭐⭐⭐⭐ ⬆', '⭐⭐⭐⭐⭐ ⬆'];
const filterProducts = (name, otherVal, { rating = '', tag = '', $tags = [] }) => {
const { otherValName, filterArgs, otherFilterArgs } =
name === 'rating'
? {
otherValName: 'tag',
filterArgs: { value: rating },
otherFilterArgs: { value: otherVal, $tags }
}
: {
otherValName: 'rating',
filterArgs: { value: tag, $tags },
otherFilterArgs: { value: otherVal }
};
const filterOne = filterProductsBy(name, $products, filterArgs);
const filterBoth = filterProductsBy(otherValName, filterOne, otherFilterArgs);
return otherVal ? filterBoth : filterOne;
};
const filter = ({ target: { name, value } }) => {
if (Number(value) === 0) {
reset();
return;
}
switch (normalize(name)) {
case 'rating':
if (selectedCat)
productsView.set(filterByCat(filterByRating($products, value), selectedCat, $tags));
else productsView.set(filterByRating($products, value));
productsView.set(
filterProducts(name, selectedCat, { rating: value, tag: selectedCat, $tags })
);
break;
case 'category':
if (selectedRating)
productsView.set(filterByRating(filterByCat($products, value, $tags), selectedRating));
else productsView.set(filterByCat($products, value, $tags));
case 'tag':
productsView.set(
filterProducts(name, selectedRating, { rating: selectedRating, tag: value, $tags })
);
break;
}
};
const { container, filterBar, filterTitle } = {
container: 'flex flex-col text-sm h-auto mb-4 mr-6 mt-4',
filterBar: 'pl-4 p-2 pr-2 flex justify-between',
filterTitle: 'font-bold'
};
</script>
<div class={container}>
<div class={filterBar}>
<div class={filterTitle}>Filter</div>
<div class="container">
<div class="filterBar">
<div class="filterTitle">Filter</div>
<button on:click={reset}> ✖️ </button>
</div>
<select
on:change={filter}
class={`p-1 pl-5 mr-12 w-full focus:outline-none ${$filters.selectedCat ? 'dither' : ''}`}
name="category"
name="tag"
bind:value={$filters.selectedCat}
>
<option select="selected" value={0}>Category</option>
@ -64,7 +77,18 @@
</select>
</div>
<style>
<style lang="postcss">
.container {
@apply flex flex-col text-sm h-auto mb-4 mr-6 mt-4;
}
.filterBar {
@apply pl-4 p-2 pr-2 flex justify-between;
}
.filterTitle {
@apply font-bold;
}
select:hover {
background-image: url('dither.gif');
background-repeat: repeat;

View File

@ -1,7 +1,7 @@
<script>
import SortOption from './SortOption.svelte';
import { productsView } from '$lib/stores';
import { normalize, getAvgRating } from '$helpers';
import { normalize, avgRating } from '$helpers';
const sortOptions = ['Rating', 'Name', 'Created'];
@ -10,8 +10,8 @@
case 'rating':
productsView.set(
$productsView.sort((prev, curr) => {
if (current) return getAvgRating(prev.rating) < getAvgRating(curr.rating) ? -1 : 1;
else return getAvgRating(prev.rating) < getAvgRating(curr.rating) ? 1 : -1;
if (current) return avgRating(prev.rating) < avgRating(curr.rating) ? -1 : 1;
else return avgRating(prev.rating) < avgRating(curr.rating) ? 1 : -1;
})
);
break;

View File

@ -18,20 +18,22 @@
load();
};
}
const { container } = {
container: 'h-screen overflow-auto w-screen'
};
</script>
<svelte:head>
<title>Rating Room</title>
</svelte:head>
<div class={container}>
<div class="container">
{#if Object.keys($currentProduct).length}
<Feature />
{:else}
<Grid products={$productsView} />
{/if}
</div>
<style lang="postcss">
.container {
@apply h-screen overflow-auto w-screen;
}
</style>

View File

@ -46,8 +46,23 @@ export interface Product {
description: string;
image?: Image;
name: string;
rating?: Ref[] | Rating[];
tags?: Ref[] | Tag[];
rating?: Ref[];
tags?: Ref[];
url?: string;
subname?: string;
}
export interface FullProduct {
_createdAt: string;
_id: string;
_rev: string;
_type: 'product';
_updatedAt: string;
description: string;
image?: Image;
name: string;
rating?: Rating[];
tags?: Ref[];
url?: string;
subname?: string;
}