Merge pull request #194 from webgems/features/search
Add search functionality & tests
This commit is contained in:
16
babel.config.js
Normal file
16
babel.config.js
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
module.exports = {
|
||||||
|
env: {
|
||||||
|
test: {
|
||||||
|
presets: [
|
||||||
|
[
|
||||||
|
"@babel/env",
|
||||||
|
{
|
||||||
|
targets: {
|
||||||
|
node: 11
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,7 +1,46 @@
|
|||||||
<template lang="pug">
|
<template lang="pug">
|
||||||
input.search(placeholder="Search (does not work currently, sorry)")
|
input.search(v-model="searchInput" type="text" placeholder="Search")
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import * as R from 'ramda'
|
||||||
|
import { isNotEmpty } from '../utils/pure'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
searchInput: '',
|
||||||
|
searchPath: '/search',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
// isTag :: String -> Bool
|
||||||
|
isTag: R.startsWith('#'),
|
||||||
|
|
||||||
|
// removeFirstChar :: String -> String
|
||||||
|
removeFirstChar: R.compose(
|
||||||
|
R.join(''),
|
||||||
|
R.adjust(0, () => '')
|
||||||
|
),
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
searchInput(input) {
|
||||||
|
const words = R.filter(isNotEmpty, R.split(' ', input))
|
||||||
|
const tags = R.filter(this.isTag, words)
|
||||||
|
const titles = R.filter(R.compose(R.not, this.isTag), words)
|
||||||
|
|
||||||
|
const searchParams = new URLSearchParams()
|
||||||
|
if (isNotEmpty(titles))
|
||||||
|
searchParams.append('keywords', titles)
|
||||||
|
if (isNotEmpty(tags))
|
||||||
|
searchParams.append('tags', R.map(this.removeFirstChar, tags))
|
||||||
|
|
||||||
|
this.$router.push(this.searchPath + '?' + searchParams.toString())
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
input {
|
input {
|
||||||
padding: .5rem 1.5rem .5rem 1.5rem;
|
padding: .5rem 1.5rem .5rem 1.5rem;
|
||||||
@@ -13,5 +52,4 @@ input {
|
|||||||
outline:none;
|
outline:none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
.layout
|
.layout
|
||||||
Github
|
Github
|
||||||
Logo
|
Logo
|
||||||
|
Search
|
||||||
Sidebar
|
Sidebar
|
||||||
no-ssr
|
no-ssr
|
||||||
template(v-if="showNotice")
|
template(v-if="showNotice")
|
||||||
@@ -118,7 +119,7 @@ h1 {
|
|||||||
grid-template-columns: fit-content(200px) auto;
|
grid-template-columns: fit-content(200px) auto;
|
||||||
grid-gap: 3rem;
|
grid-gap: 3rem;
|
||||||
grid-template-areas:
|
grid-template-areas:
|
||||||
'logo .'
|
'logo search'
|
||||||
'sidebar content';
|
'sidebar content';
|
||||||
max-width: 1200px;
|
max-width: 1200px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
|
|||||||
2964
package-lock.json
generated
2964
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
14
package.json
14
package.json
@@ -10,28 +10,38 @@
|
|||||||
"start": "nuxt start",
|
"start": "nuxt start",
|
||||||
"generate": "nuxt generate",
|
"generate": "nuxt generate",
|
||||||
"lint": "eslint --ext .js,.vue --ignore-path .gitignore .",
|
"lint": "eslint --ext .js,.vue --ignore-path .gitignore .",
|
||||||
"lintfix": "eslint --fix --ext .js,.vue --ignore-path .gitignore ."
|
"lintfix": "eslint --fix --ext .js,.vue --ignore-path .gitignore .",
|
||||||
|
"test": "jest"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cross-env": "^5.2.0",
|
"cross-env": "^5.2.0",
|
||||||
"nuxt": "^2.4.0",
|
"nuxt": "^2.4.0",
|
||||||
"nuxt-clipboard2": "^0.2.1",
|
"nuxt-clipboard2": "^0.2.1",
|
||||||
|
"ramda": "^0.26.1",
|
||||||
"vue-i18n": "^8.11.2"
|
"vue-i18n": "^8.11.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@babel/core": "^7.6.4",
|
||||||
|
"@babel/preset-env": "^7.6.3",
|
||||||
|
"@vue/test-utils": "^1.0.0-beta.29",
|
||||||
"autoprefixer": "^8.6.4",
|
"autoprefixer": "^8.6.4",
|
||||||
|
"babel-core": "^7.0.0-bridge.0",
|
||||||
"babel-eslint": "^10.0.3",
|
"babel-eslint": "^10.0.3",
|
||||||
|
"babel-jest": "^24.9.0",
|
||||||
"eslint": "^6.5.1",
|
"eslint": "^6.5.1",
|
||||||
"eslint-config-prettier": "^6.3.0",
|
"eslint-config-prettier": "^6.3.0",
|
||||||
"eslint-loader": "^3.0.2",
|
"eslint-loader": "^3.0.2",
|
||||||
"eslint-plugin-prettier": "^3.1.1",
|
"eslint-plugin-prettier": "^3.1.1",
|
||||||
"eslint-plugin-vue": "^5.2.3",
|
"eslint-plugin-vue": "^5.2.3",
|
||||||
|
"jest": "^24.9.0",
|
||||||
|
"jest-serializer-vue": "^2.0.2",
|
||||||
"node-sass": "^4.12.0",
|
"node-sass": "^4.12.0",
|
||||||
"nodemon": "^1.18.9",
|
"nodemon": "^1.18.9",
|
||||||
"prettier": "^1.18.2",
|
"prettier": "^1.18.2",
|
||||||
"pug": "^2.0.3",
|
"pug": "^2.0.3",
|
||||||
"pug-plain-loader": "^1.0.0",
|
"pug-plain-loader": "^1.0.0",
|
||||||
"sass-loader": "^7.1.0",
|
"sass-loader": "^7.1.0",
|
||||||
"tailwindcss": "^0.7.0"
|
"tailwindcss": "^0.7.0",
|
||||||
|
"vue-jest": "^3.0.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
81
pages/search.vue
Normal file
81
pages/search.vue
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
<template lang="pug">
|
||||||
|
div
|
||||||
|
transition(name="fade-title" @after-enter="afterEnter")
|
||||||
|
h1(v-if="showTitle") Search
|
||||||
|
transition(name="fade-card")
|
||||||
|
.cards(v-if="areCardsVisible && showCards")
|
||||||
|
template(v-if="resources.length")
|
||||||
|
template(v-for='resource in resources' )
|
||||||
|
Card(:resource='resource' :key='resource.title' :createCopyUrl="createCopyUrl" :isActive='activeCard === resource.cleanTitle')
|
||||||
|
p(v-else) No results
|
||||||
|
transition(name="fade-card")
|
||||||
|
table(v-if="!areCardsVisible && showCards")
|
||||||
|
template(v-if="resources.length")
|
||||||
|
template(v-for='resource in resources' )
|
||||||
|
TableRow(:resource='resource' :key='resource.title' :createCopyUrl="createCopyUrl" :isActive='activeCard === resource.cleanTitle')
|
||||||
|
p(v-else) No results
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Card from '../components/Card'
|
||||||
|
import TableRow from '../components/TableRow'
|
||||||
|
import * as R from 'ramda'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: { Card, TableRow },
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
activeCard: '',
|
||||||
|
resources: [],
|
||||||
|
searchInput: {},
|
||||||
|
showTitle: false,
|
||||||
|
showCards: false,
|
||||||
|
debounceID: 0,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
areCardsVisible() {
|
||||||
|
return this.$store.getters['Sidebar/areCardsVisible']
|
||||||
|
},
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
$route(updatedChanges) {
|
||||||
|
clearTimeout(this.debounceID)
|
||||||
|
this.debounceID = setTimeout(() => {
|
||||||
|
const keywords = updatedChanges.query.keywords
|
||||||
|
const tags = updatedChanges.query.tags
|
||||||
|
this.searchInput = {
|
||||||
|
keywords: keywords && R.split(',', keywords),
|
||||||
|
tags: tags && R.split(',', tags),
|
||||||
|
}
|
||||||
|
}, 500)
|
||||||
|
},
|
||||||
|
searchInput(searchInput) {
|
||||||
|
this.resources = this.$store.getters['data/findBySearchInputs'](searchInput.keywords, searchInput.tags)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.showTitle = true
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async createCopyUrl(resource) {
|
||||||
|
try {
|
||||||
|
const { path } = resource
|
||||||
|
await this.$copyText(`https://webgems.io${path}`)
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
afterEnter() {
|
||||||
|
this.showCards = true
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
table-layout: fixed;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -32,12 +32,6 @@
|
|||||||
"url": "https://javascript.info/",
|
"url": "https://javascript.info/",
|
||||||
"tags": ["tutorial", "explanations", "basics", "advanced"]
|
"tags": ["tutorial", "explanations", "basics", "advanced"]
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"title": "30 Seconds of Code",
|
|
||||||
"desc": "A curated collection of useful JavaScript snippets that you can understand in 30 seconds or less.",
|
|
||||||
"url": "https://30secondsofcode.org",
|
|
||||||
"tags": ["resources", "educational", "short", "beginner"]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"title": "JS Tips",
|
"title": "JS Tips",
|
||||||
"desc": "JS Tips is a collection of useful daily JavaScript tips that will allow you to improve your code writing.",
|
"desc": "JS Tips is a collection of useful daily JavaScript tips that will allow you to improve your code writing.",
|
||||||
|
|||||||
113
store/data.js
113
store/data.js
@@ -1,66 +1,65 @@
|
|||||||
import resources from '../resources'
|
import categories from '../resources'
|
||||||
|
import * as R from 'ramda'
|
||||||
// Polyfill for flat
|
import {
|
||||||
if (!Array.prototype.flat) {
|
getAllResources,
|
||||||
Object.defineProperty(Array.prototype, 'flat', {
|
getAllTags,
|
||||||
configurable: true,
|
includesElOf,
|
||||||
value: function flat () {
|
partiallyIncludesElOf,
|
||||||
var depth = isNaN(arguments[0]) ? 1 : Number(arguments[0])
|
tagsNotEmpty,
|
||||||
|
cleanString,
|
||||||
return depth ? Array.prototype.reduce.call(this, function (acc, cur) {
|
transformToResources,
|
||||||
if (Array.isArray(cur)) {
|
} from '../utils/pure'
|
||||||
acc.push.apply(acc, flat.call(cur, depth - 1))
|
|
||||||
} else {
|
|
||||||
acc.push(cur)
|
|
||||||
}
|
|
||||||
|
|
||||||
return acc
|
|
||||||
}, []) : Array.prototype.slice.call(this)
|
|
||||||
},
|
|
||||||
writable: true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if list 2 has an element of list 1.
|
|
||||||
* includesElOf(list1, list2) -> read as list1 includesElOf list2.
|
|
||||||
* @param {any[]} list1
|
|
||||||
* @param {any[]} list2
|
|
||||||
*/
|
|
||||||
const includesElOf = (list1, list2) => list1.some(element => list2.includes(element))
|
|
||||||
|
|
||||||
export const state = () => ({
|
export const state = () => ({
|
||||||
resources: resources.map(category => ({
|
resources: transformToResources(categories),
|
||||||
...category,
|
tags: getAllTags(categories),
|
||||||
resources: category.resources.map(resource => {
|
|
||||||
const cleanTitle = resource.title.replace(/ /g, '').toLowerCase()
|
|
||||||
return {
|
|
||||||
...resource,
|
|
||||||
cleanTitle,
|
|
||||||
path: `${category.slug}?card=${cleanTitle}`,
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
})),
|
|
||||||
// List of all tags, duplicates removed
|
|
||||||
tags: [...new Set(
|
|
||||||
resources
|
|
||||||
.map(resource => resource.resources).flat()
|
|
||||||
.map(resource => resource.tags).flat()
|
|
||||||
)],
|
|
||||||
})
|
})
|
||||||
|
|
||||||
export const getters = {
|
export const getters = {
|
||||||
tags: state => state.tags,
|
tags: R.prop('tags'),
|
||||||
resources: state => state.resources,
|
resources: R.prop('resources'),
|
||||||
findResources: state => title => {
|
findCategory: state => categoryTitle => {
|
||||||
return Object.assign(state.resources.find(resource => resource.title.toLowerCase() === title.toLowerCase()))
|
// equalsCategoryTitle :: Category -> Bool
|
||||||
|
const equalsCategoryTitle = R.compose(
|
||||||
|
R.equals(cleanString(categoryTitle)), cleanString, R.prop('title')
|
||||||
|
)
|
||||||
|
// findCategory :: [Category] -> Category
|
||||||
|
const findCategory = R.find(equalsCategoryTitle)
|
||||||
|
return findCategory(state.resources)
|
||||||
|
},
|
||||||
|
findByName: state => names => {
|
||||||
|
const cleaned = R.map(cleanString, names)
|
||||||
|
|
||||||
|
// [Resource] -> [Resource]
|
||||||
|
const appearsInResource = R.filter(({ cleanTitle, url, desc }) =>
|
||||||
|
partiallyIncludesElOf([cleanTitle, url, desc], cleaned)
|
||||||
|
)
|
||||||
|
// [Category] -> [Resource]
|
||||||
|
const getDesiredResources = R.compose(appearsInResource, getAllResources)
|
||||||
|
return getDesiredResources(state.resources)
|
||||||
},
|
},
|
||||||
findByTags: state => tags => {
|
findByTags: state => tags => {
|
||||||
const flat = state.resources.map(category => category.resources).flat()
|
const cleaned = R.map(cleanString, tags)
|
||||||
return flat.filter(resource => resource.tags && includesElOf(resource.tags, tags))
|
|
||||||
|
// containsTags :: [Resource] -> [Resource]
|
||||||
|
const containsTags = R.filter(tagsNotEmpty)
|
||||||
|
// includesDesiredTags :: Resource -> Bool
|
||||||
|
const includesDesiredTags = R.compose(includesElOf(cleaned), R.prop('tags'))
|
||||||
|
// findResourcesByTag :: [Resource] -> [Resource]
|
||||||
|
const findResourcesByTag = R.filter(includesDesiredTags)
|
||||||
|
// getDesiredResources :: [Category] -> [Resource]
|
||||||
|
const getDesiredResources = R.compose(findResourcesByTag, containsTags, getAllResources)
|
||||||
|
|
||||||
|
return getDesiredResources(state.resources)
|
||||||
|
},
|
||||||
|
findBySearchInputs: (_, getters) => (keywords = [], tags = []) => {
|
||||||
|
const foundByKeywords = getters.findByName(keywords)
|
||||||
|
const foundByTags = getters.findByTags(tags)
|
||||||
|
const uniqueResources = foundByTags.filter(x => !foundByKeywords.some(y => equalResources(x, y)))
|
||||||
|
return uniqueResources.concat(foundByKeywords)
|
||||||
},
|
},
|
||||||
sortByTitle: (_, getters) => title => {
|
sortByTitle: (_, getters) => title => {
|
||||||
const category = getters.findResources(title)
|
const category = getters.findCategory(title)
|
||||||
const clone = [...category.resources]
|
const clone = [...category.resources]
|
||||||
return {
|
return {
|
||||||
...category,
|
...category,
|
||||||
@@ -77,4 +76,8 @@ const compareTitles = (x, y) => {
|
|||||||
} else {
|
} else {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const equalResources = (a, b) =>
|
||||||
|
a.title === b.title &&
|
||||||
|
a.cleanTitle == b.cleanTitle
|
||||||
50
test/mockCategories.json
Normal file
50
test/mockCategories.json
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"title": "CSS",
|
||||||
|
"slug": "/css",
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"title": "CSS Grid Generator",
|
||||||
|
"desc": "Visually create your css grid and export the code.",
|
||||||
|
"url": "https://cssgrid-generator.netlify.com/",
|
||||||
|
"tags": ["generator", "grid", "layout", "visual tool"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Keyframes Editor",
|
||||||
|
"desc": "An insanely simple way to create CSS animations",
|
||||||
|
"url": "https://keyframes.app/editor/",
|
||||||
|
"tags": ["generator", "animation", "visual tool"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Flexbox Froggy",
|
||||||
|
"desc": "A game to learn Flexbox",
|
||||||
|
"url": "https://flexboxfroggy.com",
|
||||||
|
"tags": ["educational", "beginner"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Design",
|
||||||
|
"slug": "/design",
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"title": "UX/UI Designer Roadmap 2017",
|
||||||
|
"desc": "Roadmap to becoming a UI/UX Designer in 2017",
|
||||||
|
"url": "https://github.com/togiberlin/ui-ux-designer-roadmap",
|
||||||
|
"tags": ["career", "ui", "ux"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Undraw",
|
||||||
|
"desc": "Free vector illustrations for your website.",
|
||||||
|
"url": "https://undraw.co",
|
||||||
|
"tags": ["illustration", "svg", "ui"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Practical UI tips",
|
||||||
|
"desc": "7 Tips to boost your UI design.",
|
||||||
|
"url": "https://medium.com/refactoring-ui/7-practical-tips-for-cheating-at-design-40c736799886",
|
||||||
|
"tags": ["ui", "tips", "tricks"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
86
test/mockOutput.json
Normal file
86
test/mockOutput.json
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"title": "CSS",
|
||||||
|
"slug": "/css",
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"title": "CSS Grid Generator",
|
||||||
|
"desc": "Visually create your css grid and export the code.",
|
||||||
|
"url": "https://cssgrid-generator.netlify.com/",
|
||||||
|
"tags": [
|
||||||
|
"generator",
|
||||||
|
"grid",
|
||||||
|
"layout",
|
||||||
|
"visual tool"
|
||||||
|
],
|
||||||
|
"cleanTitle": "cssgridgenerator",
|
||||||
|
"path": "/css?card=cssgridgenerator"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Keyframes Editor",
|
||||||
|
"desc": "An insanely simple way to create CSS animations",
|
||||||
|
"url": "https://keyframes.app/editor/",
|
||||||
|
"tags": [
|
||||||
|
"generator",
|
||||||
|
"animation",
|
||||||
|
"visual tool"
|
||||||
|
],
|
||||||
|
"cleanTitle": "keyframeseditor",
|
||||||
|
"path": "/css?card=keyframeseditor"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Flexbox Froggy",
|
||||||
|
"desc": "A game to learn Flexbox",
|
||||||
|
"url": "https://flexboxfroggy.com",
|
||||||
|
"tags": [
|
||||||
|
"educational",
|
||||||
|
"beginner"
|
||||||
|
],
|
||||||
|
"cleanTitle": "flexboxfroggy",
|
||||||
|
"path": "/css?card=flexboxfroggy"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Design",
|
||||||
|
"slug": "/design",
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"title": "UX/UI Designer Roadmap 2017",
|
||||||
|
"desc": "Roadmap to becoming a UI/UX Designer in 2017",
|
||||||
|
"url": "https://github.com/togiberlin/ui-ux-designer-roadmap",
|
||||||
|
"tags": [
|
||||||
|
"career",
|
||||||
|
"ui",
|
||||||
|
"ux"
|
||||||
|
],
|
||||||
|
"cleanTitle": "ux/uidesignerroadmap2017",
|
||||||
|
"path": "/design?card=ux/uidesignerroadmap2017"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Undraw",
|
||||||
|
"desc": "Free vector illustrations for your website.",
|
||||||
|
"url": "https://undraw.co",
|
||||||
|
"tags": [
|
||||||
|
"illustration",
|
||||||
|
"svg",
|
||||||
|
"ui"
|
||||||
|
],
|
||||||
|
"cleanTitle": "undraw",
|
||||||
|
"path": "/design?card=undraw"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Practical UI tips",
|
||||||
|
"desc": "7 Tips to boost your UI design.",
|
||||||
|
"url": "https://medium.com/refactoring-ui/7-practical-tips-for-cheating-at-design-40c736799886",
|
||||||
|
"tags": [
|
||||||
|
"ui",
|
||||||
|
"tips",
|
||||||
|
"tricks"
|
||||||
|
],
|
||||||
|
"cleanTitle": "practicaluitips",
|
||||||
|
"path": "/design?card=practicaluitips"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
41
test/pure.test.js
Normal file
41
test/pure.test.js
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import * as P from '../utils/pure.js'
|
||||||
|
import mockCategories from './mockCategories.json'
|
||||||
|
import mockOutput from './mockOutput.json'
|
||||||
|
|
||||||
|
test('includesElOf 1', () => {
|
||||||
|
expect(P.includesElOf([1, 2])([2])).toBeTruthy
|
||||||
|
})
|
||||||
|
|
||||||
|
test('includesElOf 2', () => {
|
||||||
|
expect(P.includesElOf([1, 2], [3])).toBeFalsy
|
||||||
|
})
|
||||||
|
|
||||||
|
test('includesElOf 3', () => {
|
||||||
|
expect(P.includesElOf(['a', 'b'])(['a', 'c'])).toBeTruthy
|
||||||
|
})
|
||||||
|
|
||||||
|
test('includesElOf 4', () => {
|
||||||
|
expect(P.includesElOf(['aa', 'b'])(['a', 'c'])).toBeFalsy
|
||||||
|
})
|
||||||
|
|
||||||
|
test('partiallyIncludesElOf 1', () => {
|
||||||
|
expect(P.partiallyIncludesElOf(['a', 'b'], ['a'])).toBeTruthy
|
||||||
|
})
|
||||||
|
|
||||||
|
test('partiallyIncludesElOf 2', () => {
|
||||||
|
expect(P.partiallyIncludesElOf(['aa', 'b'])(['a', 'c'])).toBeTruthy
|
||||||
|
})
|
||||||
|
|
||||||
|
test('partiallyIncludesElOf 3', () => {
|
||||||
|
expect(P.partiallyIncludesElOf(['aa', 'b'], ['c', 'd'])).toBeFalsy
|
||||||
|
})
|
||||||
|
|
||||||
|
test('get all tags', () => {
|
||||||
|
expect(P.getAllTags(mockCategories)).toStrictEqual([
|
||||||
|
"generator", "grid", "layout", "visual tool", "animation", "educational", "beginner", "career", "ui", "ux", "illustration", "svg", "tips", "tricks",
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('transform resources', () => {
|
||||||
|
expect(P.transformToResources(mockCategories)).toStrictEqual(mockOutput)
|
||||||
|
})
|
||||||
79
utils/pure.js
Normal file
79
utils/pure.js
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
/*eslint-disable */
|
||||||
|
import * as R from 'ramda'
|
||||||
|
|
||||||
|
/// TYPES ///
|
||||||
|
// const Category = {
|
||||||
|
// title: String,
|
||||||
|
// slug: String,
|
||||||
|
// resources: [Resource],
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const RawResource = {
|
||||||
|
// title: String,
|
||||||
|
// desc: String,
|
||||||
|
// url: String,
|
||||||
|
// tags: [String]
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const Resource = {
|
||||||
|
// title: String,
|
||||||
|
// cleanTitle: String,
|
||||||
|
// desc: String,
|
||||||
|
// path: String,
|
||||||
|
// url: String,
|
||||||
|
// tags: [String],
|
||||||
|
// }
|
||||||
|
|
||||||
|
/// FUNCTIONS ///
|
||||||
|
// isNotEmpty [a] -> Bool
|
||||||
|
export const isNotEmpty = R.compose(R.not, R.isEmpty)
|
||||||
|
|
||||||
|
// getAllResources :: [Category] -> [Resource]
|
||||||
|
export const getAllResources = R.compose(R.flatten, R.pluck('resources'))
|
||||||
|
|
||||||
|
// getAllTags :: [Category] -> [String]
|
||||||
|
export const getAllTags = R.compose(
|
||||||
|
R.uniq,
|
||||||
|
R.flatten,
|
||||||
|
R.pluck('tags'),
|
||||||
|
getAllResources
|
||||||
|
)
|
||||||
|
|
||||||
|
// tagsNotEmpty :: Resource -> Bool
|
||||||
|
export const tagsNotEmpty = R.compose(isNotEmpty, R.prop('tags'))
|
||||||
|
|
||||||
|
// cleanString :: String -> String
|
||||||
|
export const cleanString = R.compose(R.toLower, R.trim)
|
||||||
|
|
||||||
|
// removeSpacesLower :: String -> String
|
||||||
|
export const cleanStringAndRemoveSpaces = R.compose(R.replace(/ /g, ''), cleanString)
|
||||||
|
|
||||||
|
// true if list2 has element that appears in list1 else false
|
||||||
|
// includesElOf :: [a] -> [a] -> Bool
|
||||||
|
export const includesElOf = R.curry((list1, list2) => R.any(el => R.includes(el, list2), list1))
|
||||||
|
|
||||||
|
// Similar to includesElOf, but partially included strings are also allowed
|
||||||
|
// partiallyIncludesElOf :: [String] -> [String] -> Bool
|
||||||
|
export const partiallyIncludesElOf = R.curry((list1, list2) =>
|
||||||
|
R.any(el2 =>
|
||||||
|
R.any(R.includes(el2), list1),
|
||||||
|
list2)
|
||||||
|
)
|
||||||
|
|
||||||
|
// addCleanTitleAndPath :: RawResource -> Resource
|
||||||
|
const addCleanTitleAndPath = R.curry((slug, obj) => {
|
||||||
|
const cleanTitle = cleanStringAndRemoveSpaces(obj.title)
|
||||||
|
return {
|
||||||
|
...obj,
|
||||||
|
cleanTitle,
|
||||||
|
path: `${slug}?card=${cleanTitle}`,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// transformToResources :: [RawResource] -> [Resource]
|
||||||
|
export const transformToResources = categories => {
|
||||||
|
const resourcesLens = R.lens(R.prop('resources'), R.assoc('resources'))
|
||||||
|
return R.map(category =>
|
||||||
|
R.over(resourcesLens, R.map(addCleanTitleAndPath(category.slug)), category),
|
||||||
|
categories)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user