Merge pull request #215 from webgems/dev

🔀 dev into master
This commit is contained in:
wellá
2019-11-28 17:04:46 +01:00
committed by GitHub
20 changed files with 3328 additions and 283 deletions

View File

@@ -1,3 +1,5 @@
{
"editor.formatOnSave": false
"editor.formatOnSave": false,
"vue-i18n-ally.localesPaths": "locales",
"i18n-ally.localesPaths": "locales"
}

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
@@ -22,4 +23,4 @@ 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
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

View File

@@ -2,25 +2,6 @@
<html {{ HTML_ATTRS }}>
<head {{ HEAD_ATTRS }}>
{{ HEAD }}
<!-- Matomo -->
<script type="text/javascript">
var _paq = window._paq || [];
/* tracker methods like "setCustomDimension" should be called before "trackPageView" */
_paq.push(["setDocumentTitle", document.domain + "/" + document.title]);
_paq.push(["setCookieDomain", "*.webgems.io"]);
_paq.push(["setDomains", ["*.webgems.io"]]);
_paq.push(['trackPageView']);
_paq.push(['enableLinkTracking']);
(function() {
var u="//stats.lost.services/";
_paq.push(['setTrackerUrl', u+'matomo.php']);
_paq.push(['setSiteId', '1']);
var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
g.type='text/javascript'; g.async=true; g.defer=true; g.src=u+'matomo.js'; s.parentNode.insertBefore(g,s);
})();
</script>
<noscript><p><img src="//stats.lost.services/matomo.php?idsite=1&amp;rec=1" style="border:0;" alt="" /></p></noscript>
<!-- End Matomo Code -->
</head>
<body {{ BODY_ATTRS }}>
{{ APP }}

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;
@@ -185,6 +186,7 @@ h1 {
grid-template-areas:
'logo'
'sidebar'
'search'
'content';
}
hr {

View File

@@ -15,10 +15,10 @@ export default {
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
{ hid: 'description', name: 'description', content: pkg.description },
{ hid: 'keywords', name: 'keywords', content: 'css, html, php, server, resources, design, gems, nuxt, javascript, tutorials, development, software'},
{ name: 'robots', content: 'index, follow' },
{ name: 'distribution', content: 'global'},
{ name: 'distribution', content: 'global'},
{ name:'theme-color', content: '#ffffff' },
{ name: 'msapplication-TileColor', content: '#da532c' },
{ rel: 'mask-icon', href: '/safari-pinned-tab.svg', color: '#5bbad5' },
@@ -62,6 +62,10 @@ export default {
plugins: [
'~/plugins/i18n.js',
{
src: '~/plugins/vue-matomo.js',
ssr: false,
},
],
/*
** Nuxt.js modules

2971
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -10,28 +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"
"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>

34
plugins/vue-matomo.js Normal file
View File

@@ -0,0 +1,34 @@
import Vue from 'vue'
import VueMatomo from 'vue-matomo'
export default ({
app,
}) => {
Vue.use(VueMatomo, {
router: app.router,
// Configure your matomo server and site by providing
host: 'https://stats.lost.services',
siteId: 1,
// Changes the default .js and .php endpoint's filename
// Default: 'piwik'
trackerFileName: 'matomo',
// Enables link tracking on regular links. Note that this won't
// work for routing links (ie. internal Vue router links)
// Default: true
enableLinkTracking: true,
// Require consent before sending tracking information to matomo
// Default: false
requireConsent: false,
// Whether to track the initial page view
// Default: true
trackInitialView: true,
// Whether or not to log debug information
// Default: false
debug: false,
})
}

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