From 0d3a0489ae3f21a0342631528e046f219184b77d Mon Sep 17 00:00:00 2001 From: Kevin Van Der Werff Date: Mon, 21 Oct 2019 13:46:33 +0200 Subject: [PATCH 01/23] :art: Rewrite findByTags & add Search --- components/Search.vue | 23 ++++++++++++++++++++-- layouts/default.vue | 3 ++- package-lock.json | 46 +++++++++++++++---------------------------- package.json | 1 + store/data.js | 38 ++++++++++++++++++++++++----------- 5 files changed, 66 insertions(+), 45 deletions(-) diff --git a/components/Search.vue b/components/Search.vue index 920a7ff..9a51a9c 100644 --- a/components/Search.vue +++ b/components/Search.vue @@ -1,7 +1,27 @@ + + diff --git a/layouts/default.vue b/layouts/default.vue index e39a6da..27fffe9 100644 --- a/layouts/default.vue +++ b/layouts/default.vue @@ -2,6 +2,7 @@ .layout Github Logo + Search Sidebar nuxt.content @@ -73,7 +74,7 @@ h1 { grid-template-columns: fit-content(200px) auto; grid-gap: 1rem; grid-template-areas: - 'logo .' + 'logo search' 'sidebar content'; } diff --git a/package-lock.json b/package-lock.json index e52815a..662640b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4456,8 +4456,7 @@ }, "ansi-regex": { "version": "2.1.1", - "bundled": true, - "optional": true + "bundled": true }, "aproba": { "version": "1.2.0", @@ -4475,13 +4474,11 @@ }, "balanced-match": { "version": "1.0.0", - "bundled": true, - "optional": true + "bundled": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, - "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -4494,18 +4491,15 @@ }, "code-point-at": { "version": "1.1.0", - "bundled": true, - "optional": true + "bundled": true }, "concat-map": { "version": "0.0.1", - "bundled": true, - "optional": true + "bundled": true }, "console-control-strings": { "version": "1.1.0", - "bundled": true, - "optional": true + "bundled": true }, "core-util-is": { "version": "1.0.2", @@ -4608,8 +4602,7 @@ }, "inherits": { "version": "2.0.3", - "bundled": true, - "optional": true + "bundled": true }, "ini": { "version": "1.3.5", @@ -4619,7 +4612,6 @@ "is-fullwidth-code-point": { "version": "1.0.0", "bundled": true, - "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -4632,20 +4624,17 @@ "minimatch": { "version": "3.0.4", "bundled": true, - "optional": true, "requires": { "brace-expansion": "^1.1.7" } }, "minimist": { "version": "0.0.8", - "bundled": true, - "optional": true + "bundled": true }, "minipass": { "version": "2.3.5", "bundled": true, - "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -4662,7 +4651,6 @@ "mkdirp": { "version": "0.5.1", "bundled": true, - "optional": true, "requires": { "minimist": "0.0.8" } @@ -4735,8 +4723,7 @@ }, "number-is-nan": { "version": "1.0.1", - "bundled": true, - "optional": true + "bundled": true }, "object-assign": { "version": "4.1.1", @@ -4746,7 +4733,6 @@ "once": { "version": "1.4.0", "bundled": true, - "optional": true, "requires": { "wrappy": "1" } @@ -4822,8 +4808,7 @@ }, "safe-buffer": { "version": "5.1.2", - "bundled": true, - "optional": true + "bundled": true }, "safer-buffer": { "version": "2.1.2", @@ -4853,7 +4838,6 @@ "string-width": { "version": "1.0.2", "bundled": true, - "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -4871,7 +4855,6 @@ "strip-ansi": { "version": "3.0.1", "bundled": true, - "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -4910,13 +4893,11 @@ }, "wrappy": { "version": "1.0.2", - "bundled": true, - "optional": true + "bundled": true }, "yallist": { "version": "3.0.3", - "bundled": true, - "optional": true + "bundled": true } } }, @@ -8895,6 +8876,11 @@ "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", "integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=" }, + "ramda": { + "version": "0.26.1", + "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.26.1.tgz", + "integrity": "sha512-hLWjpy7EnsDBb0p+Z3B7rPi3GDeRG5ZtiI33kJhTt+ORCd38AbAIjB/9zRIUoeTbE/AVX5ZkU7m6bznsvrf8eQ==" + }, "randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", diff --git a/package.json b/package.json index 3ec9d5f..ea3782f 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "cross-env": "^5.2.0", "nuxt": "^2.4.0", "nuxt-clipboard2": "^0.2.1", + "ramda": "^0.26.1", "vue-i18n": "^8.11.2" }, "devDependencies": { diff --git a/store/data.js b/store/data.js index 2648b74..cd68909 100644 --- a/store/data.js +++ b/store/data.js @@ -1,4 +1,5 @@ import resources from '../resources' +import { prop, compose, flatten, map, filter, isEmpty, not, any, includes, curry } from 'ramda' // Polyfill for flat if (!Array.prototype.flat) { @@ -21,13 +22,13 @@ if (!Array.prototype.flat) { }) } -/** - * 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)) +// True if list2 has element that appears in list1 +// includesElOf([1, 2], [3]) -> false +// includesElOf([1, 2])([3]) -> false +// includesElOf([1, 2], [2]) -> true +// includesElOf([1, 2])([2]) -> true +// includesElOf :: [a] -> [a] -> Bool +// const includesElOf = curry((list1, list2) => any(flip(includes)(list2), list1)) export const state = () => ({ resources: resources.map(category => ({ @@ -52,15 +53,28 @@ export const state = () => ({ export const getters = { tags: state => state.tags, resources: state => state.resources, - findResources: state => title => { - return Object.assign(state.resources.find(resource => resource.title.toLowerCase() === title.toLowerCase())) + findCategory: state => categoryTitle => { + return Object.assign(state.resources.find(category => category.title.toLowerCase() === categoryTitle.toLowerCase())) }, findByTags: state => tags => { - const flat = state.resources.map(category => category.resources).flat() - return flat.filter(resource => resource.tags && includesElOf(resource.tags, tags)) + // true if list2 has element that appears in list1 else false + // includesElOf [a] -> [a] -> Bool + const includesElOf = curry((list1, list2) => any(el => includes(el, list2), list1)) + // getAllResources :: [Category] -> [Resource] + const getAllResources = compose(flatten, map(prop('resources'))) + // tagsNotEmpty :: [Resource] -> Bool + const tagsNotEmpty = compose(not, isEmpty, prop('tags')) + // containsTags :: [Resource] -> [Resource] + const containsTags = filter(tagsNotEmpty) + // findResourcesByTag :: [Resource] -> [Resource] + const findResourcesByTag = filter(compose(includesElOf(tags), prop('tags'))) + // getDesiredResources :: [Category] -> [Resource] + const getDesiredResources = compose(findResourcesByTag, containsTags, getAllResources) + + return getDesiredResources(state.resources) }, sortByTitle: (_, getters) => title => { - const category = getters.findResources(title) + const category = getters.findCategory(title) const clone = [...category.resources] return { ...category, From a0698f09bb45d616abdf29d5c1297f1037dcc1ba Mon Sep 17 00:00:00 2001 From: Kevin Van Der Werff Date: Mon, 21 Oct 2019 16:16:10 +0200 Subject: [PATCH 02/23] :art: Refactor into pure.js --- store/data.js | 22 +++++----------------- utils/pure.js | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 17 deletions(-) create mode 100644 utils/pure.js diff --git a/store/data.js b/store/data.js index cd68909..e537819 100644 --- a/store/data.js +++ b/store/data.js @@ -1,5 +1,6 @@ import resources from '../resources' -import { prop, compose, flatten, map, filter, isEmpty, not, any, includes, curry } from 'ramda' +import { prop, compose, filter } from 'ramda' +import { includesElOf, getAllResources, tagsNotEmpty } from '../utils/pure' // Polyfill for flat if (!Array.prototype.flat) { @@ -22,14 +23,6 @@ if (!Array.prototype.flat) { }) } -// True if list2 has element that appears in list1 -// includesElOf([1, 2], [3]) -> false -// includesElOf([1, 2])([3]) -> false -// includesElOf([1, 2], [2]) -> true -// includesElOf([1, 2])([2]) -> true -// includesElOf :: [a] -> [a] -> Bool -// const includesElOf = curry((list1, list2) => any(flip(includes)(list2), list1)) - export const state = () => ({ resources: resources.map(category => ({ ...category, @@ -57,17 +50,12 @@ export const getters = { return Object.assign(state.resources.find(category => category.title.toLowerCase() === categoryTitle.toLowerCase())) }, findByTags: state => tags => { - // true if list2 has element that appears in list1 else false - // includesElOf [a] -> [a] -> Bool - const includesElOf = curry((list1, list2) => any(el => includes(el, list2), list1)) - // getAllResources :: [Category] -> [Resource] - const getAllResources = compose(flatten, map(prop('resources'))) - // tagsNotEmpty :: [Resource] -> Bool - const tagsNotEmpty = compose(not, isEmpty, prop('tags')) // containsTags :: [Resource] -> [Resource] const containsTags = filter(tagsNotEmpty) + // includesDesiredTags :: Resource -> Bool + const includesDesiredTags = compose(includesElOf(tags), prop('tags')) // findResourcesByTag :: [Resource] -> [Resource] - const findResourcesByTag = filter(compose(includesElOf(tags), prop('tags'))) + const findResourcesByTag = filter(includesDesiredTags) // getDesiredResources :: [Category] -> [Resource] const getDesiredResources = compose(findResourcesByTag, containsTags, getAllResources) diff --git a/utils/pure.js b/utils/pure.js new file mode 100644 index 0000000..4ae4722 --- /dev/null +++ b/utils/pure.js @@ -0,0 +1,33 @@ +/*eslint-disable */ +import * as R from 'ramda' + +/// Types +const Resource = { + title: String, + cleanTitle: String, + desc: String, + path: String, + url: String, + tags: [String], +} + +const Category = { + title: String, + slug: String, + resources: [Resource], +} + +/// Functions +// getAllResources :: [Category] -> [Resource] +const getAllResources = R.compose(R.flatten, R.map(R.prop('resources'))) + +// tagsNotEmpty :: Resource -> Bool +const tagsNotEmpty = R.compose(R.not, R.isEmpty, R.prop('tags')) + +// true if list2 has element that appears in list1 else false +// includesElOf([1, 2])([2]) -> true +// includesElOf([1, 2], [3]) -> false +// includesElOf :: [a] -> [a] -> Bool +const includesElOf = R.curry((list1, list2) => R.any(el => R.includes(el, list2), list1)) + +export { getAllResources, tagsNotEmpty, includesElOf } \ No newline at end of file From b7523dbbbb22bc9f0acf89dec2cc0120fdfc6023 Mon Sep 17 00:00:00 2001 From: Kevin Van Der Werff Date: Mon, 21 Oct 2019 17:11:11 +0200 Subject: [PATCH 03/23] :art: :sparkles: find by name Search on word/name, if partially found in resource it will return the resource --- components/Search.vue | 16 +++++++++++++++- store/data.js | 26 ++++++++++++++++++++------ utils/pure.js | 23 +++++++++++++++++++++-- 3 files changed, 56 insertions(+), 9 deletions(-) diff --git a/components/Search.vue b/components/Search.vue index 9a51a9c..2bcaced 100644 --- a/components/Search.vue +++ b/components/Search.vue @@ -3,6 +3,8 @@ diff --git a/test/pure.test.js b/test/pure.test.js index 0a6432e..adebf0b 100644 --- a/test/pure.test.js +++ b/test/pure.test.js @@ -11,11 +11,11 @@ test('includesElOf 2', () => { }) test('includesElOf 3', () => { - expect(P.includesElOf(['a', 'b'])(['a'])).toBeTruthy + expect(P.includesElOf(['a', 'b'])(['a', 'c'])).toBeTruthy }) test('includesElOf 4', () => { - expect(P.includesElOf(['aa', 'b'])(['a'])).toBeFalsy + expect(P.includesElOf(['aa', 'b'])(['a', 'c'])).toBeFalsy }) test('partiallyIncludesElOf 1', () => { @@ -23,11 +23,11 @@ test('partiallyIncludesElOf 1', () => { }) test('partiallyIncludesElOf 2', () => { - expect(P.partiallyIncludesElOf(['aa', 'b'])(['a'])).toBeTruthy + expect(P.partiallyIncludesElOf(['aa', 'b'])(['a', 'c'])).toBeTruthy }) test('partiallyIncludesElOf 3', () => { - expect(P.partiallyIncludesElOf(['aa', 'b'], ['c'])).toBeFalsy + expect(P.partiallyIncludesElOf(['aa', 'b'], ['c', 'd'])).toBeFalsy }) test('get all tags', () => { From d3ebdd02209231ef0cbe0773f9f2c3ff54b13065 Mon Sep 17 00:00:00 2001 From: Kevin Van Der Werff Date: Tue, 22 Oct 2019 19:23:16 +0200 Subject: [PATCH 17/23] :art: add RawResource "type" --- utils/pure.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/utils/pure.js b/utils/pure.js index 710df32..5750366 100644 --- a/utils/pure.js +++ b/utils/pure.js @@ -8,6 +8,13 @@ import * as R from 'ramda' // resources: [Resource], // } +// const RawResource = { +// title: String, +// desc: String, +// url: String, +// tags: [String] +// } + // const Resource = { // title: String, // cleanTitle: String, @@ -53,7 +60,7 @@ export const partiallyIncludesElOf = R.curry((list1, list2) => list2) ) -// addCleanTitleAndPath :: Object -> Resource +// addCleanTitleAndPath :: RawResource -> Resource const addCleanTitleAndPath = R.curry((slug, obj) => { const cleanTitle = cleanStringAndRemoveSpaces(obj.title) return { @@ -63,7 +70,7 @@ const addCleanTitleAndPath = R.curry((slug, obj) => { } }) -// transformToResources :: [Object] -> [Resource] +// transformToResources :: [RawResource] -> [Resource] export const transformToResources = categories => { const resourcesLens = R.lens(R.prop('resources'), R.assoc('resources')) return R.map(category => From 6c5d8270c0688825db204d1a9d1528e798fdad19 Mon Sep 17 00:00:00 2001 From: Unknown Date: Thu, 24 Oct 2019 13:30:01 +0200 Subject: [PATCH 18/23] :art: change removeFirstChar --- components/Search.vue | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/components/Search.vue b/components/Search.vue index a475faf..0ac6429 100644 --- a/components/Search.vue +++ b/components/Search.vue @@ -15,7 +15,10 @@ export default { watch: { searchInput(input) { const isTag = R.startsWith('#') - const removeFirstChar = x => R.splitAt(1, x)[1] + const removeFirstChar = R.compose( + R.join(''), + R.adjust(0, () => '') + ) const words = R.filter(isNotEmpty, R.split(' ', input)) const tags = R.filter(isTag, words) From 23d97f00b168986b10c4372e69ee7cd5b87ff165 Mon Sep 17 00:00:00 2001 From: Kevin van der Werff Date: Wed, 6 Nov 2019 21:00:53 +0100 Subject: [PATCH 19/23] :sparkles: Add search page and show search results --- components/Search.vue | 35 ++++++++++------- pages/search.vue | 81 +++++++++++++++++++++++++++++++++++++++ resources/javascript.json | 6 --- 3 files changed, 102 insertions(+), 20 deletions(-) create mode 100644 pages/search.vue diff --git a/components/Search.vue b/components/Search.vue index 0ac6429..f2d8f49 100644 --- a/components/Search.vue +++ b/components/Search.vue @@ -10,25 +10,32 @@ 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 isTag = R.startsWith('#') - const removeFirstChar = R.compose( - R.join(''), - R.adjust(0, () => '') - ) - const words = R.filter(isNotEmpty, R.split(' ', input)) - const tags = R.filter(isTag, words) - const titles = R.filter(R.compose(R.not, isTag), words) - console.group() - console.log('words:', titles) - console.log('tags:', tags) - console.log('returned by words search:', this.$store.getters['data/findByName'](titles)) - console.log('returned by tags search:', this.$store.getters['data/findByTags'](R.map(removeFirstChar, tags))) - console.groupEnd() + 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()) }, }, } diff --git a/pages/search.vue b/pages/search.vue new file mode 100644 index 0000000..97d4a8e --- /dev/null +++ b/pages/search.vue @@ -0,0 +1,81 @@ + + + + + diff --git a/resources/javascript.json b/resources/javascript.json index f8eb981..048a72a 100644 --- a/resources/javascript.json +++ b/resources/javascript.json @@ -32,12 +32,6 @@ "url": "https://javascript.info/", "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", "desc": "JS Tips is a collection of useful daily JavaScript tips that will allow you to improve your code writing.", From 35a11766d8d381daf7820ada7e0dc8b8c7c6cdb4 Mon Sep 17 00:00:00 2001 From: Kevin van der Werff Date: Thu, 7 Nov 2019 00:18:13 +0100 Subject: [PATCH 20/23] :construction: Avoid duplicated returns --- pages/search.vue | 10 ++-------- store/data.js | 12 +++++++++++- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/pages/search.vue b/pages/search.vue index 97d4a8e..6dbe317 100644 --- a/pages/search.vue +++ b/pages/search.vue @@ -25,7 +25,6 @@ export default { activeCard: '', resources: [], searchInput: {}, - tags: [], showTitle: false, showCards: false, } @@ -46,13 +45,8 @@ export default { this.searchInput = newSearchInput }, searchInput(searchInput) { - let resources = [] - if (searchInput.keywords) - resources = resources.concat(this.$store.getters['data/findByName'](searchInput.keywords)) - if (searchInput.tags) - resources = resources.concat(this.$store.getters['data/findByTags'](searchInput.tags)) - this.resources = resources - }, + this.resources = this.$store.getters['data/findBySearchInputs'](searchInput.keywords, searchInput.tags) + }, }, mounted() { this.showTitle = true diff --git a/store/data.js b/store/data.js index dd31fe5..cec45bd 100644 --- a/store/data.js +++ b/store/data.js @@ -52,6 +52,12 @@ export const getters = { 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 => { const category = getters.findCategory(title) const clone = [...category.resources] @@ -70,4 +76,8 @@ const compareTitles = (x, y) => { } else { return 0 } -} \ No newline at end of file +} + +const equalResources = (a, b) => + a.title === b.title && + a.cleanTitle == b.cleanTitle \ No newline at end of file From 73a53c8161e0c0198b5708141fc9c0bfc48cd042 Mon Sep 17 00:00:00 2001 From: Kevin van der Werff Date: Thu, 7 Nov 2019 00:52:25 +0100 Subject: [PATCH 21/23] :construction: Debounce of 500ms --- components/Search.vue | 2 +- pages/search.vue | 19 +++++++++++-------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/components/Search.vue b/components/Search.vue index f2d8f49..5bc4360 100644 --- a/components/Search.vue +++ b/components/Search.vue @@ -1,5 +1,5 @@