Merge branch 'dev' into features/tracking

This commit is contained in:
wellá
2019-11-28 16:51:35 +01:00
committed by GitHub
17 changed files with 3350 additions and 257 deletions

58
.all-contributorsrc Normal file
View 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"
}

View File

@@ -1,4 +1,5 @@
[![Netlify Status](https://api.netlify.com/api/v1/badges/32128bab-176e-4a45-b21e-7a57425a36d1/deploy-status)](https://app.netlify.com/sites/epic-sammet-7ed06e/deploys)
[![All Contributors](https://img.shields.io/badge/all_contributors-3-orange.svg?style=flat-square)](#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
View File

@@ -0,0 +1,16 @@
module.exports = {
env: {
test: {
presets: [
[
"@babel/env",
{
targets: {
node: 11
}
}
]
]
}
}
};

View File

@@ -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>

View File

@@ -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));

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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
View 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>

View File

@@ -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"]
}
]
}

View File

@@ -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.",

View File

@@ -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"]
}
]
}

View File

@@ -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
View 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
View 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
View 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
View 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)
}