Merge branch 'dev' into features/tracking
This commit is contained in:
58
.all-contributorsrc
Normal file
58
.all-contributorsrc
Normal file
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"files": [
|
||||
"README.md"
|
||||
],
|
||||
"imageSize": 100,
|
||||
"commit": false,
|
||||
"contributors": [{
|
||||
"login": "lostdesign",
|
||||
"name": "wellá",
|
||||
"avatar_url": "https://avatars0.githubusercontent.com/u/5164617?v=4",
|
||||
"profile": "https://lost.design",
|
||||
"contributions": [
|
||||
"business",
|
||||
"infra",
|
||||
"security",
|
||||
"dev",
|
||||
"bug",
|
||||
"code",
|
||||
"content",
|
||||
"ideas",
|
||||
"maintenance",
|
||||
"review",
|
||||
"test"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "S3B4S",
|
||||
"name": "Kevin van der Werff",
|
||||
"avatar_url": "https://avatars0.githubusercontent.com/u/17083334?v=4",
|
||||
"profile": "https://github.com/S3B4S",
|
||||
"contributions": [
|
||||
"dev",
|
||||
"bug",
|
||||
"code",
|
||||
"content",
|
||||
"ideas",
|
||||
"maintenance",
|
||||
"review",
|
||||
"test"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "Banou26",
|
||||
"name": "Banou",
|
||||
"avatar_url": "https://avatars0.githubusercontent.com/u/5209149?v=4",
|
||||
"profile": "https://banou.dev",
|
||||
"contributions": [
|
||||
"bug",
|
||||
"design"
|
||||
]
|
||||
}
|
||||
],
|
||||
"contributorsPerLine": 7,
|
||||
"projectName": "webgems",
|
||||
"projectOwner": "webgems",
|
||||
"repoType": "github",
|
||||
"repoHost": "https://github.com"
|
||||
}
|
||||
19
README.md
19
README.md
@@ -1,4 +1,5 @@
|
||||
[](https://app.netlify.com/sites/epic-sammet-7ed06e/deploys)
|
||||
[](#contributors)
|
||||
|
||||
# webgems.io
|
||||
|
||||
@@ -23,3 +24,21 @@ See also the list of [contributors](https://github.com/webgems/webgems/contribut
|
||||
## License
|
||||
|
||||
This project is licensed under the GNU General Public License v3.0 - see the [LICENSE](https://github.com/webgems/webgems/blob/master/LICENSE) file for details
|
||||
|
||||
## Contributors ✨
|
||||
|
||||
Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
|
||||
|
||||
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
|
||||
<!-- prettier-ignore -->
|
||||
<table>
|
||||
<tr>
|
||||
<td align="center"><a href="https://lost.design"><img src="https://avatars0.githubusercontent.com/u/5164617?v=4" width="100px;" alt="wellá"/><br /><sub><b>wellá</b></sub></a><br /><a href="#business-lostdesign" title="Business development">💼</a> <a href="#infra-lostdesign" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="#security-lostdesign" title="Security">🛡️</a> <a href="https://github.com/webgems/webgems/issues?q=author%3Alostdesign" title="Bug reports">🐛</a> <a href="https://github.com/webgems/webgems/commits?author=lostdesign" title="Code">💻</a> <a href="#content-lostdesign" title="Content">🖋</a> <a href="#ideas-lostdesign" title="Ideas, Planning, & Feedback">🤔</a> <a href="#maintenance-lostdesign" title="Maintenance">🚧</a> <a href="#review-lostdesign" title="Reviewed Pull Requests">👀</a> <a href="https://github.com/webgems/webgems/commits?author=lostdesign" title="Tests">⚠️</a></td>
|
||||
<td align="center"><a href="https://github.com/S3B4S"><img src="https://avatars0.githubusercontent.com/u/17083334?v=4" width="100px;" alt="Kevin van der Werff"/><br /><sub><b>Kevin van der Werff</b></sub></a><br /><a href="https://github.com/webgems/webgems/issues?q=author%3AS3B4S" title="Bug reports">🐛</a> <a href="https://github.com/webgems/webgems/commits?author=S3B4S" title="Code">💻</a> <a href="#content-S3B4S" title="Content">🖋</a> <a href="#ideas-S3B4S" title="Ideas, Planning, & Feedback">🤔</a> <a href="#maintenance-S3B4S" title="Maintenance">🚧</a> <a href="#review-S3B4S" title="Reviewed Pull Requests">👀</a> <a href="https://github.com/webgems/webgems/commits?author=S3B4S" title="Tests">⚠️</a></td>
|
||||
<td align="center"><a href="https://banou.dev"><img src="https://avatars0.githubusercontent.com/u/5209149?v=4" width="100px;" alt="Banou"/><br /><sub><b>Banou</b></sub></a><br /><a href="https://github.com/webgems/webgems/issues?q=author%3ABanou26" title="Bug reports">🐛</a> <a href="#design-Banou26" title="Design">🎨</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- ALL-CONTRIBUTORS-LIST:END -->
|
||||
|
||||
This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!
|
||||
|
||||
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">
|
||||
input.search(placeholder="Search (does not work currently, sorry)")
|
||||
input.search(v-model="searchInput" type="text" placeholder="Search")
|
||||
</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">
|
||||
input {
|
||||
padding: .5rem 1.5rem .5rem 1.5rem;
|
||||
@@ -13,5 +52,4 @@ input {
|
||||
outline:none;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@@ -46,10 +46,18 @@ export default {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
font-size: 14px;
|
||||
align-items: center;
|
||||
|
||||
a {
|
||||
padding: 0.5rem 1rem 0.5rem 1rem;
|
||||
font-weight: 600;
|
||||
transition-duration: 0.2s;
|
||||
transition-property: background-color,color;
|
||||
&:hover, &.nuxt-link-exact-active {
|
||||
background-color: #08e5ff;
|
||||
color: #000;
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
div {
|
||||
cursor: pointer;
|
||||
@@ -61,12 +69,11 @@ export default {
|
||||
border: 1px;
|
||||
border-color: #08e5ff;
|
||||
border-style: solid;
|
||||
border-radius: 0.25rem;
|
||||
overflow: hidden;
|
||||
margin: 1rem auto;
|
||||
}
|
||||
.viewToggle {
|
||||
padding: 0 0.2rem;
|
||||
padding: .2rem.2rem;
|
||||
color: #008190;
|
||||
}
|
||||
.active {
|
||||
@@ -75,11 +82,11 @@ export default {
|
||||
}
|
||||
hr {
|
||||
width: 80%;
|
||||
background-color: #08e5ff;
|
||||
border-color: #08e5ff;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 400px) {
|
||||
@media (max-width: 600px) {
|
||||
.sidebar {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(6rem, 1fr));
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
.layout
|
||||
Github
|
||||
Logo
|
||||
Search
|
||||
Sidebar
|
||||
no-ssr
|
||||
template(v-if="showNotice")
|
||||
@@ -118,7 +119,7 @@ h1 {
|
||||
grid-template-columns: fit-content(200px) auto;
|
||||
grid-gap: 3rem;
|
||||
grid-template-areas:
|
||||
'logo .'
|
||||
'logo search'
|
||||
'sidebar content';
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
@@ -176,7 +177,7 @@ h1 {
|
||||
}
|
||||
|
||||
|
||||
@media (max-width: 400px) {
|
||||
@media (max-width: 600px) {
|
||||
.layout {
|
||||
display: grid;
|
||||
grid-template-columns: 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,29 +10,39 @@
|
||||
"start": "nuxt start",
|
||||
"generate": "nuxt generate",
|
||||
"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": {
|
||||
"cross-env": "^5.2.0",
|
||||
"nuxt": "^2.4.0",
|
||||
"nuxt-clipboard2": "^0.2.1",
|
||||
"vue-i18n": "^8.11.2",
|
||||
"ramda": "^0.26.1",
|
||||
"vue-matomo": "^3.12.0-5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.6.4",
|
||||
"@babel/preset-env": "^7.6.3",
|
||||
"@vue/test-utils": "^1.0.0-beta.29",
|
||||
"autoprefixer": "^8.6.4",
|
||||
"babel-core": "^7.0.0-bridge.0",
|
||||
"babel-eslint": "^10.0.3",
|
||||
"babel-jest": "^24.9.0",
|
||||
"eslint": "^6.5.1",
|
||||
"eslint-config-prettier": "^6.3.0",
|
||||
"eslint-loader": "^3.0.2",
|
||||
"eslint-plugin-prettier": "^3.1.1",
|
||||
"eslint-plugin-vue": "^5.2.3",
|
||||
"jest": "^24.9.0",
|
||||
"jest-serializer-vue": "^2.0.2",
|
||||
"node-sass": "^4.12.0",
|
||||
"nodemon": "^1.18.9",
|
||||
"prettier": "^1.18.2",
|
||||
"pug": "^2.0.3",
|
||||
"pug-plain-loader": "^1.0.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>
|
||||
@@ -19,6 +19,12 @@
|
||||
"desc": "Lecture platform about anything ranging from basic Javascript to advanced React methods. Community courses available free of charge with an opt-in paid section for full course paths.",
|
||||
"url": "https://egghead.io",
|
||||
"tags": ["videos", "frontend", "react", "javascript"]
|
||||
},
|
||||
{
|
||||
"title": "Refactoring Guru",
|
||||
"desc": "This site makes it easy for you to discover everything you need to know about refactoring, design patterns, SOLID principles, and other smart programming topics.",
|
||||
"url": "https://refactoring.guru/",
|
||||
"tags": ["refactoring", "patterns", "educational", "learning"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -175,6 +175,18 @@
|
||||
"desc": "Messed up some of your commits? Make it undone",
|
||||
"url": "https://bokub.github.io/git-history-editor/",
|
||||
"tags": ["import", "export", "author", "messages"]
|
||||
},
|
||||
{
|
||||
"title": "CodeSandbox.io",
|
||||
"desc": "CodeSandbox is an online code editor with a focus on creating and sharing web application projects",
|
||||
"url": "https://codesandbox.io/",
|
||||
"tags": ["development", "ide","editor", "share", "testing"]
|
||||
},
|
||||
{
|
||||
"title": "Postwoman.io",
|
||||
"desc": "The Postwoman API request builder helps you create your requests faster, saving you precious time on your development.",
|
||||
"url": "https://Postwoman.io/",
|
||||
"tags": ["testing","api"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
113
store/data.js
113
store/data.js
@@ -1,66 +1,65 @@
|
||||
import resources from '../resources'
|
||||
|
||||
// Polyfill for flat
|
||||
if (!Array.prototype.flat) {
|
||||
Object.defineProperty(Array.prototype, 'flat', {
|
||||
configurable: true,
|
||||
value: function flat () {
|
||||
var depth = isNaN(arguments[0]) ? 1 : Number(arguments[0])
|
||||
|
||||
return depth ? Array.prototype.reduce.call(this, function (acc, cur) {
|
||||
if (Array.isArray(cur)) {
|
||||
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))
|
||||
import categories from '../resources'
|
||||
import * as R from 'ramda'
|
||||
import {
|
||||
getAllResources,
|
||||
getAllTags,
|
||||
includesElOf,
|
||||
partiallyIncludesElOf,
|
||||
tagsNotEmpty,
|
||||
cleanString,
|
||||
transformToResources,
|
||||
} from '../utils/pure'
|
||||
|
||||
export const state = () => ({
|
||||
resources: resources.map(category => ({
|
||||
...category,
|
||||
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()
|
||||
)],
|
||||
resources: transformToResources(categories),
|
||||
tags: getAllTags(categories),
|
||||
})
|
||||
|
||||
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()))
|
||||
tags: R.prop('tags'),
|
||||
resources: R.prop('resources'),
|
||||
findCategory: state => categoryTitle => {
|
||||
// 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 => {
|
||||
const flat = state.resources.map(category => category.resources).flat()
|
||||
return flat.filter(resource => resource.tags && includesElOf(resource.tags, tags))
|
||||
const cleaned = R.map(cleanString, 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 => {
|
||||
const category = getters.findResources(title)
|
||||
const category = getters.findCategory(title)
|
||||
const clone = [...category.resources]
|
||||
return {
|
||||
...category,
|
||||
@@ -77,4 +76,8 @@ const compareTitles = (x, y) => {
|
||||
} else {
|
||||
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