├── .babelrc ├── .editorconfig ├── .eslintrc.js ├── .gitignore ├── README.md ├── package.json ├── public └── _redirects ├── resources └── preview-inkscape.svg ├── src ├── App.vue ├── api │ ├── apollo.js │ └── fragment-types.json ├── assets │ ├── logo.png │ └── preview.svg ├── components │ ├── common │ │ ├── BaseGraph.vue │ │ └── index.js │ ├── modules │ │ ├── Categories.vue │ │ ├── DownloadsGraph.vue │ │ ├── MainPane.vue │ │ ├── ModuleDetails.vue │ │ ├── ModuleListItem.vue │ │ ├── ModuleOwner.vue │ │ ├── ModuleReadme.vue │ │ ├── ModuleRelease.vue │ │ ├── ModuleReleases.vue │ │ └── Releases.vue │ └── pages │ │ ├── Home.vue │ │ ├── Main.vue │ │ ├── NotFound.vue │ │ └── Welcome.vue ├── filters.js ├── graphql │ ├── Downloads.gql │ ├── EntityFragments.gql │ ├── ModuleCategories.gql │ ├── ModuleDetails.gql │ ├── ModuleFragments.gql │ ├── ModuleReadme.gql │ ├── ModuleReleases.gql │ ├── Modules.gql │ └── VueReleases.gql ├── index.html ├── main.js ├── mixins │ └── ObserveScroll.js ├── plugins.js ├── router.js ├── store │ └── index.js ├── style │ ├── imports.styl │ ├── main.styl │ ├── mixins.styl │ ├── transitions.styl │ └── vars.styl └── utils │ ├── emoji.js │ ├── graph.js │ ├── responsive.js │ └── search.js ├── tasks └── update-fragment-matcher.js ├── webpack.config.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { 4 | "modules": false, 5 | "userBuiltIns": "usage" 6 | }], 7 | "stage-0" 8 | ], 9 | "plugins": [ 10 | "transform-runtime" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: 'babel-eslint', 4 | parserOptions: { 5 | sourceType: 'module' 6 | }, 7 | // required to lint *.vue files 8 | plugins: [ 9 | 'html' 10 | ], 11 | // https://github.com/feross/standard/blob/master/RULES.md#javascript-standard-style 12 | extends: 'standard', 13 | // add your custom rules here 14 | 'rules': { 15 | // allow paren-less arrow functions 16 | 'arrow-parens': 0, 17 | // allow async-await 18 | 'generator-star-spacing': 0, 19 | // allow debugger during development 20 | 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0, 21 | // Trailing comma 22 | 'comma-dangle': ['error', 'always-multiline'] 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | dist/ 4 | npm-debug.log 5 | yarn-error.log 6 | .deploy 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vue-curated-client 2 | 3 | Deprecated in favor of [AwesomeJS.dev](https://github.com/Akryum/awesomejs.dev/) 4 | 5 | --- 6 | 7 | > Curated vue packages. 8 | 9 | Consumes [vue-curated](https://github.com/vuejs/vue-curated/blob/master/PACKAGES.md) and needs [vue-curated-server](https://github.com/vuejs/vue-curated-server). 10 | 11 | ## Build Setup 12 | 13 | ``` bash 14 | # install dependencies 15 | npm install 16 | 17 | # set GRAPHQL_URL env var to the graphql enpoint 18 | GRAPHQL_URL=http://localhost:3000/graphql 19 | 20 | # serve with hot reload at localhost:8080 21 | npm run dev 22 | 23 | # build for production with minification 24 | npm run build 25 | 26 | # serve as a static website (you need to build first) 27 | npm start 28 | ``` 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-curated-client", 3 | "description": "Web app displaying curated vue packages", 4 | "version": "2.0.0", 5 | "author": "Guillaume Chau ", 6 | "private": true, 7 | "scripts": { 8 | "dev": "yarn run build-tasks && cross-env NODE_ENV=development webpack-dev-server --inline --hot", 9 | "build": "yarn run build-tasks && cross-env NODE_ENV=production webpack --progress --hide-modules", 10 | "start": "serve -p 80 -s", 11 | "build-tasks": "yarn run task:fragment-matcher", 12 | "task:fragment-matcher": "node ./tasks/update-fragment-matcher.js", 13 | "analyze": "cross-env ANALYZE=on yarn run build" 14 | }, 15 | "engines": { 16 | "node": ">6.9.1" 17 | }, 18 | "dependencies": { 19 | "@vue/ui": "^0.1.2", 20 | "apollo-cache-inmemory": "^1.1.9", 21 | "apollo-client": "^2.2.5", 22 | "apollo-link-http": "^1.5.2", 23 | "emojione": "^3.0.3", 24 | "graphql": "^0.13.1", 25 | "graphql-tag": "^2.8.0", 26 | "html-webpack-plugin": "^2.30.1", 27 | "marked": "^0.3.6", 28 | "moment": "^2.18.1", 29 | "numeral": "^2.0.6", 30 | "serve": "^6.5.1", 31 | "style-loader": "^0.18.2", 32 | "vue": "^2.5.13", 33 | "vue-apollo": "^3.0.0-beta.4", 34 | "vue-router": "^3.0.1", 35 | "vuex": "^3.0.1" 36 | }, 37 | "devDependencies": { 38 | "babel-core": "^6.24.1", 39 | "babel-eslint": "^7.2.3", 40 | "babel-loader": "^7.0.0", 41 | "babel-plugin-transform-runtime": "^6.23.0", 42 | "babel-polyfill": "^6.26.0", 43 | "babel-preset-env": "^1.6.1", 44 | "babel-preset-stage-0": "^6.24.1", 45 | "copy-webpack-plugin": "^4.5.0", 46 | "cross-env": "^5.0.1", 47 | "css-loader": "^0.28.0", 48 | "eslint": "^3.19.0", 49 | "eslint-config-standard": "^10.2.1", 50 | "eslint-friendly-formatter": "^2.0.7", 51 | "eslint-loader": "^1.7.1", 52 | "eslint-plugin-html": "^2.0.1", 53 | "eslint-plugin-import": "^2.2.0", 54 | "eslint-plugin-node": "^4.2.2", 55 | "eslint-plugin-promise": "^3.5.0", 56 | "eslint-plugin-standard": "^3.0.1", 57 | "file-loader": "^0.11.1", 58 | "node-fetch": "^2.0.0", 59 | "stylus": "^0.54.5", 60 | "stylus-loader": "^3.0.1", 61 | "vue-loader": "^14.1.1", 62 | "vue-template-compiler": "^2.3.2", 63 | "webpack": "^3.2.0", 64 | "webpack-bundle-analyzer": "^2.8.2", 65 | "webpack-dev-server": "^2.9.7" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /public/_redirects: -------------------------------------------------------------------------------- 1 | /* / 200 2 | -------------------------------------------------------------------------------- /resources/preview-inkscape.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 20 | 22 | 60 | 62 | 63 | 65 | image/svg+xml 66 | 68 | 69 | 70 | 71 | 72 | 77 | 84 | 91 | 94 | 101 | 108 | 109 | 112 | 119 | 122 | 129 | 136 | 143 | 150 | 151 | 152 | 160 | 168 | 176 | 184 | 187 | 194 | 201 | 208 | 209 | 216 | 221 | 228 | 235 | 236 | 237 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | 13 | 16 | -------------------------------------------------------------------------------- /src/api/apollo.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueApollo from 'vue-apollo' 3 | import ApolloClient from 'apollo-client' 4 | import { HttpLink } from 'apollo-link-http' 5 | import { InMemoryCache, IntrospectionFragmentMatcher } from 'apollo-cache-inmemory' 6 | import introspectionQueryResultData from './fragment-types.json' 7 | 8 | export const GRAPHQL_URI = process.env.GRAPHQL_URL || 'http://localhost:3000/graphql' 9 | 10 | const link = new HttpLink({ 11 | // You should use an absolute URL here 12 | uri: GRAPHQL_URI, 13 | }) 14 | 15 | const fragmentMatcher = new IntrospectionFragmentMatcher({ 16 | introspectionQueryResultData, 17 | }) 18 | 19 | const cache = new InMemoryCache({ 20 | fragmentMatcher, 21 | }) 22 | 23 | // Create the apollo client 24 | export const apolloClient = new ApolloClient({ 25 | link, 26 | cache, 27 | }) 28 | 29 | // Install the vue plugin 30 | Vue.use(VueApollo) 31 | 32 | export function createProvider () { 33 | return new VueApollo({ 34 | defaultClient: apolloClient, 35 | }) 36 | } 37 | -------------------------------------------------------------------------------- /src/api/fragment-types.json: -------------------------------------------------------------------------------- 1 | {"__schema":{"types":[{"kind":"INTERFACE","name":"Entity","possibleTypes":[{"name":"Module"},{"name":"ModuleCategory"},{"name":"VueRelease"}]}]}} -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vuejs/vue-curated-client/f206588c90949c8f8f73ba9458a65589a10c308e/src/assets/logo.png -------------------------------------------------------------------------------- /src/assets/preview.svg: -------------------------------------------------------------------------------- 1 | 2 | 14 | 16 | 18 | 19 | 21 | image/svg+xml 22 | 24 | 25 | 26 | 27 | 28 | 31 | 38 | 45 | 48 | 55 | 62 | 63 | 66 | 73 | 76 | 83 | 90 | 97 | 104 | 105 | 106 | 114 | 122 | 130 | 138 | 141 | 148 | 155 | 162 | 163 | 170 | 174 | 181 | 188 | 189 | 190 | -------------------------------------------------------------------------------- /src/components/common/BaseGraph.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 100 | -------------------------------------------------------------------------------- /src/components/common/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | const components = require.context('./', true, /[a-z0-9]+\.vue$/) 4 | // To extract the component name 5 | const nameReg = /([a-z0-9]+)\./i 6 | // Registration 7 | components.keys().forEach(key => { 8 | const name = key.match(nameReg)[1] 9 | Vue.component(name, components(key).default) 10 | }) 11 | -------------------------------------------------------------------------------- /src/components/modules/Categories.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 54 | -------------------------------------------------------------------------------- /src/components/modules/DownloadsGraph.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 49 | 50 | 56 | -------------------------------------------------------------------------------- /src/components/modules/MainPane.vue: -------------------------------------------------------------------------------- 1 | 65 | 66 | 179 | 180 | 360 | -------------------------------------------------------------------------------- /src/components/modules/ModuleDetails.vue: -------------------------------------------------------------------------------- 1 | 151 | 152 | 226 | 227 | 249 | 250 | 580 | -------------------------------------------------------------------------------- /src/components/modules/ModuleListItem.vue: -------------------------------------------------------------------------------- 1 | 65 | 66 | 111 | 112 | 273 | -------------------------------------------------------------------------------- /src/components/modules/ModuleOwner.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 27 | 28 | 54 | -------------------------------------------------------------------------------- /src/components/modules/ModuleReadme.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 76 | 77 | 109 | -------------------------------------------------------------------------------- /src/components/modules/ModuleRelease.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 43 | 44 | 57 | -------------------------------------------------------------------------------- /src/components/modules/ModuleReleases.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 53 | 54 | 62 | -------------------------------------------------------------------------------- /src/components/modules/Releases.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 54 | -------------------------------------------------------------------------------- /src/components/pages/Home.vue: -------------------------------------------------------------------------------- 1 | 65 | 66 | 89 | 90 | 213 | -------------------------------------------------------------------------------- /src/components/pages/Main.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 31 | 32 | 103 | -------------------------------------------------------------------------------- /src/components/pages/NotFound.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 19 | 20 | 38 | 39 | 47 | -------------------------------------------------------------------------------- /src/components/pages/Welcome.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 33 | 34 | 35 | 91 | -------------------------------------------------------------------------------- /src/filters.js: -------------------------------------------------------------------------------- 1 | import numeral from 'numeral' 2 | import marked from 'marked' 3 | import moment from 'moment' 4 | 5 | export function shortenNumber (value) { 6 | value = parseInt(value) 7 | if (value < 1000) { 8 | return value 9 | } else { 10 | return numeral(value).format('0.0a') 11 | } 12 | } 13 | 14 | export function markdown (value) { 15 | return marked(value) 16 | } 17 | 18 | export function fromNow (value) { 19 | return moment(value).fromNow() 20 | } 21 | 22 | export function date (value) { 23 | return moment(value).format('L') 24 | } 25 | 26 | export function humanDate (value) { 27 | return moment(value).format('LL') 28 | } 29 | -------------------------------------------------------------------------------- /src/graphql/Downloads.gql: -------------------------------------------------------------------------------- 1 | query downloads($id: ID!) { 2 | module(id: $id) { 3 | id 4 | npm_package { 5 | range_downloads { 6 | day 7 | downloads 8 | } 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /src/graphql/EntityFragments.gql: -------------------------------------------------------------------------------- 1 | fragment Entity on Entity { 2 | id 3 | label 4 | } 5 | -------------------------------------------------------------------------------- /src/graphql/ModuleCategories.gql: -------------------------------------------------------------------------------- 1 | #import "./EntityFragments.gql" 2 | 3 | query categories { 4 | module_categories { 5 | ...Entity 6 | } 7 | } -------------------------------------------------------------------------------- /src/graphql/ModuleDetails.gql: -------------------------------------------------------------------------------- 1 | #import "./ModuleFragments.gql" 2 | 3 | query details($id: ID!) { 4 | module(id: $id) { 5 | ...Module 6 | } 7 | } -------------------------------------------------------------------------------- /src/graphql/ModuleFragments.gql: -------------------------------------------------------------------------------- 1 | #import "./EntityFragments.gql" 2 | 3 | fragment Module on Module { 4 | ...Entity 5 | url 6 | vue 7 | links { 8 | url 9 | label 10 | } 11 | status 12 | badge 13 | category { 14 | id 15 | label 16 | } 17 | details { 18 | name 19 | description 20 | forks_count 21 | stargazers_count 22 | open_issues_count 23 | has_wiki 24 | created_at 25 | pushed_at 26 | updated_at 27 | owner { 28 | login 29 | avatar_url 30 | html_url 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /src/graphql/ModuleReadme.gql: -------------------------------------------------------------------------------- 1 | query details($id: ID!) { 2 | module (id: $id) { 3 | id 4 | readme { 5 | content 6 | } 7 | } 8 | } -------------------------------------------------------------------------------- /src/graphql/ModuleReleases.gql: -------------------------------------------------------------------------------- 1 | query releases($id: ID!) { 2 | module(id: $id) { 3 | id 4 | releases { 5 | id 6 | html_url 7 | tag_name 8 | name 9 | body 10 | prerelease 11 | published_at 12 | files { 13 | download_url 14 | size 15 | download_count 16 | } 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/graphql/Modules.gql: -------------------------------------------------------------------------------- 1 | #import "./ModuleFragments.gql" 2 | 3 | query modules ($release: String) { 4 | modules (release: $release) { 5 | ...Module 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/graphql/VueReleases.gql: -------------------------------------------------------------------------------- 1 | #import "./EntityFragments.gql" 2 | 3 | query releases { 4 | vue_releases { 5 | ...Entity 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | Vue Curated 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import 'babel-polyfill' 2 | import Vue from 'vue' 3 | 4 | // Plugins 5 | import './plugins' 6 | 7 | // Filters 8 | import * as filters from './filters' 9 | 10 | // Global components 11 | import './components/common' 12 | 13 | // Injections 14 | 15 | import router from './router' 16 | import store from './store' 17 | // Apollo GraphQL 18 | import { createProvider } from './api/apollo' 19 | 20 | // Root component 21 | import App from './App' 22 | 23 | // Filters 24 | for (const k in filters) { 25 | Vue.filter(k, filters[k]) 26 | } 27 | 28 | const apolloProvider = createProvider() 29 | 30 | /* eslint-disable no-new */ 31 | new Vue({ 32 | router, 33 | store, 34 | provide: apolloProvider.provide(), 35 | el: '#app', 36 | ...App, 37 | created () { 38 | this.$store.dispatch('init') 39 | }, 40 | }) 41 | -------------------------------------------------------------------------------- /src/mixins/ObserveScroll.js: -------------------------------------------------------------------------------- 1 | export default { 2 | data () { 3 | return { 4 | scrollTop: 0, 5 | } 6 | }, 7 | 8 | methods: { 9 | handleScroll (event) { 10 | this.scrollTop = event.currentTarget.scrollTop 11 | }, 12 | }, 13 | } 14 | -------------------------------------------------------------------------------- /src/plugins.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueUi from '@vue/ui' 3 | import '@vue/ui/dist/vue-ui.css' 4 | import 'vue-resize/dist/vue-resize.css' 5 | import VueResize from 'vue-resize' 6 | import Responsive from './utils/responsive' 7 | import Emoji from './utils/emoji' 8 | 9 | Vue.use(VueUi) 10 | 11 | Vue.use(VueResize) 12 | 13 | Vue.use(Responsive, { 14 | computed: { 15 | mobile () { 16 | return this.width <= 768 17 | }, 18 | tablet () { 19 | return this.width <= 900 20 | }, 21 | desktop () { 22 | return !this.tablet 23 | }, 24 | }, 25 | }) 26 | 27 | Vue.use(Emoji) 28 | -------------------------------------------------------------------------------- /src/router.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueRouter from 'vue-router' 3 | 4 | import Main from 'components/pages/Main' 5 | import Welcome from 'components/pages/Welcome' 6 | import ModuleDetails from 'components/modules/ModuleDetails.vue' 7 | import NotFound from 'components/pages/NotFound' 8 | 9 | Vue.use(VueRouter) 10 | 11 | /* eslint-disable object-property-newline */ 12 | const routes = [ 13 | { path: '/', component: Main, children: [ 14 | { path: '', name: 'home', component: Welcome }, 15 | { path: 'module/:id', name: 'module', component: ModuleDetails, props: true }, 16 | ] }, 17 | { path: '*', component: NotFound }, 18 | ] 19 | 20 | const scrollBehavior = (to, from, savedPosition) => { 21 | if (savedPosition) { 22 | return savedPosition 23 | } else { 24 | const position = {} 25 | if (to.hash) { 26 | position.selector = to.hash 27 | } 28 | if (to.matched.some(m => m.meta.scrollToTop)) { 29 | position.x = 0 30 | position.y = 0 31 | } 32 | return position 33 | } 34 | } 35 | 36 | const router = new VueRouter({ 37 | routes, 38 | mode: 'history', 39 | scrollBehavior, 40 | }) 41 | 42 | export default router 43 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | import { apolloClient } from 'api/apollo' 4 | 5 | import RELEASES_QUERY from 'graphql/VueReleases.gql' 6 | import CATEGORIES_QUERY from 'graphql/ModuleCategories.gql' 7 | 8 | Vue.use(Vuex) 9 | 10 | const store = new Vuex.Store({ 11 | state: { 12 | categoryId: null, 13 | categories: [], 14 | categoriesReady: false, 15 | releaseId: null, 16 | releases: [], 17 | releasesReady: false, 18 | }, 19 | 20 | getters: { 21 | 'categoryId': state => state.categoryId, 22 | 'categories': state => state.categories, 23 | 'categoriesReady': state => state.categoriesReady, 24 | 25 | 'releaseId': state => state.releaseId, 26 | 'releases': state => state.releases, 27 | 'releasesReady': state => state.releasesReady, 28 | }, 29 | 30 | mutations: { 31 | 'set_category': (state, categoryId) => { 32 | state.categoryId = categoryId 33 | }, 34 | 35 | 'set_categories': (state, categories) => { 36 | state.categories = categories.slice().sort((a, b) => a.label < b.label ? -1 : 1) 37 | state.categoriesReady = true 38 | }, 39 | 40 | 'set_release': (state, releaseId) => { 41 | state.releaseId = releaseId 42 | }, 43 | 44 | 'set_releases': (state, releases) => { 45 | state.releases = releases 46 | state.releasesReady = true 47 | }, 48 | }, 49 | 50 | actions: { 51 | 'init' ({ dispatch }) { 52 | return Promise.all([ 53 | dispatch('fetch_releases'), 54 | dispatch('fetch_categories'), 55 | ]) 56 | }, 57 | 58 | 'fetch_releases' ({ commit }) { 59 | apolloClient.query({ 60 | query: RELEASES_QUERY, 61 | }).then(result => { 62 | const releases = result.data.vue_releases 63 | commit('set_release', releases[0].id) 64 | commit('set_releases', releases) 65 | }) 66 | }, 67 | 68 | 'fetch_categories' ({ commit }) { 69 | apolloClient.query({ 70 | query: CATEGORIES_QUERY, 71 | }).then(result => { 72 | commit('set_categories', result.data.module_categories) 73 | }) 74 | }, 75 | }, 76 | }) 77 | 78 | export default store 79 | -------------------------------------------------------------------------------- /src/style/imports.styl: -------------------------------------------------------------------------------- 1 | @import "~@vue/ui/src/style/imports"; 2 | @import "mixins"; 3 | @import "vars"; 4 | -------------------------------------------------------------------------------- /src/style/main.styl: -------------------------------------------------------------------------------- 1 | @import "~style/imports"; 2 | @import "./transitions"; 3 | 4 | body { 5 | margin: 0; 6 | font-family: 'Source Sans Pro', 'Helvetica Neue', Arial, sans-serif; 7 | -webkit-font-smoothing: antialiased; 8 | -moz-osx-font-smoothing: grayscale; 9 | } 10 | 11 | html, 12 | body, 13 | #app { 14 | height: 100%; 15 | overflow: hidden; 16 | } 17 | 18 | .emojione { 19 | width: 1em; 20 | height: 1em; 21 | vertical-align: middle; 22 | } 23 | 24 | a 25 | color $primary-color 26 | text-decoration none 27 | cursor pointer 28 | display inline-flex 29 | align-items center 30 | 31 | .vue-icon 32 | &:first-child 33 | margin-right 4px 34 | &:last-child 35 | margin-left 4px 36 | &:first-child:last-child 37 | margin 0 38 | svg 39 | fill @color 40 | 41 | &:hover 42 | color lighten($primary-color, 30%) 43 | .vue-icon 44 | svg 45 | fill @color 46 | 47 | .fab { 48 | position: fixed; 49 | z-index: 1; 50 | bottom: 24px; 51 | right: 18px; 52 | font-size: 24px; 53 | width: 56px; 54 | height: 56px; 55 | border-radius: 50%; 56 | padding: 0; 57 | display: flex; 58 | align-items: center; 59 | justify-content: center; 60 | box-shadow: 0 4px 10px rgba(black, .3); 61 | } 62 | 63 | .hero, 64 | .empty { 65 | color: $md-grey-400; 66 | text-align: center; 67 | padding: 24px; 68 | box-sizing: border-box; 69 | display: flex; 70 | flex-direction: row; 71 | align-items: center; 72 | justify-content: center; 73 | 74 | &.fill { 75 | height: 100%; 76 | } 77 | } 78 | 79 | .empty { 80 | .vue-icon { 81 | margin-right 4px 82 | } 83 | 84 | @media ({$small-screen}) { 85 | font-size: 22px; 86 | 87 | .vue-icon { 88 | width @font-size 89 | height @width 90 | } 91 | } 92 | } 93 | 94 | .hero { 95 | font-size: 24px; 96 | 97 | > .icon { 98 | font-size: 42px; 99 | opacity: .5; 100 | } 101 | } 102 | 103 | .markdown { 104 | img { 105 | max-width: 100%; 106 | } 107 | 108 | :first-child { 109 | margin-top: 0; 110 | } 111 | 112 | blockquote { 113 | border-left: solid 6px $primary-color; 114 | margin-left: 0; 115 | padding: 6px 12px; 116 | 117 | :last-child { 118 | margin-bottom: 0; 119 | } 120 | } 121 | 122 | code, 123 | pre { 124 | background: darken(white, 5%); 125 | border-radius: 2px; 126 | color: darken($primary-color, 50%); 127 | } 128 | 129 | code { 130 | padding: 0 2px; 131 | } 132 | 133 | pre { 134 | padding: 12px; 135 | overflow-x: auto; 136 | } 137 | 138 | hr { 139 | border: none; 140 | border-top: solid 1px rgba($primary-color, .5); 141 | } 142 | 143 | ul { 144 | margin-bottom: 24px; 145 | padding-left: 24px; 146 | } 147 | 148 | li { 149 | margin: 10px 0; 150 | } 151 | } 152 | 153 | .page-content { 154 | section { 155 | margin: 12px; 156 | } 157 | } 158 | 159 | .badges, 160 | .badge-group { 161 | display: inline-flex; 162 | align-items: center; 163 | } 164 | 165 | .badge { 166 | display: inline-flex; 167 | align-items center 168 | padding: 2px 4px; 169 | border-radius: 3px; 170 | font-size: 12px; 171 | color: white; 172 | background: grey; 173 | vertical-align: text-top; 174 | min-height 19px 175 | box-sizing border-box 176 | 177 | .vue-icon { 178 | svg { 179 | fill @color 180 | } 181 | } 182 | } 183 | 184 | .badge, 185 | .badge-group { 186 | &:not(:last-child) { 187 | margin-right: 4px; 188 | } 189 | } 190 | 191 | .badge-group { 192 | .badge { 193 | background: lighten($primary-color, 15%); 194 | margin-right: 0; 195 | 196 | &:not(:last-child) { 197 | border-right: solid 1px white; 198 | } 199 | 200 | &:first-child { 201 | border-radius: 3px 0 0 3px; 202 | } 203 | 204 | &:last-child { 205 | border-radius: 0 3px 3px 0; 206 | background: $primary-color; 207 | } 208 | 209 | &:first-child:last-child { 210 | border-radius: 3px; 211 | } 212 | } 213 | } 214 | 215 | // Mobile 216 | 217 | @media ({$not-small-screen}) { 218 | .mobile-only { 219 | display: none; 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /src/style/mixins.styl: -------------------------------------------------------------------------------- 1 | ellipsis() { 2 | overflow: hidden; 3 | -ms-text-overflow: ellipsis; 4 | text-overflow: ellipsis; 5 | white-space: nowrap; 6 | } 7 | 8 | bounds($distance) { 9 | top: $distance; 10 | bottom: $distance; 11 | right: $distance; 12 | left: $distance; 13 | } 14 | 15 | overlay() { 16 | position: absolute; 17 | bounds(0); 18 | } 19 | 20 | flex-box() { 21 | display: flex; 22 | 23 | & > * { 24 | flex: auto 0 0; 25 | } 26 | } 27 | 28 | h-box() { 29 | flex-box(); 30 | flex-direction: row; 31 | } 32 | 33 | v-box() { 34 | flex-box(); 35 | flex-direction: column; 36 | } 37 | 38 | flex-control() { 39 | width: 0 !important; 40 | } 41 | 42 | box-center() { 43 | align-items: center; 44 | justify-content: center; 45 | } 46 | 47 | toolbar-btn($bg) { 48 | background: fade($bg, 80%); 49 | color: black; 50 | transition: background 0.2s; 51 | 52 | &:hover { 53 | color: black; 54 | background: $bg; 55 | } 56 | } 57 | 58 | space-between-x($margin) { 59 | margin-right: $margin; 60 | 61 | &:last-child { 62 | margin-right: 0; 63 | } 64 | } 65 | 66 | space-between-y($margin) { 67 | margin-bottom: $margin; 68 | 69 | &:last-child { 70 | margin-bottom: 0; 71 | } 72 | } 73 | 74 | unselectable() { 75 | -moz-user-select: none; 76 | -webkit-user-select: none; 77 | -ms-user-select: none; 78 | user-select: none; 79 | } 80 | -------------------------------------------------------------------------------- /src/style/transitions.styl: -------------------------------------------------------------------------------- 1 | .zoom-enter-active, 2 | .zoom-leave-active { 3 | transition: transform .15s cubic-bezier(0.0, 0.0, 0.2, 1); 4 | } 5 | 6 | .zoom-enter, 7 | .zoom-leave-to { 8 | transform: scale(0); 9 | } 10 | 11 | .fade-enter-active, 12 | .fade-leave-active { 13 | transition: opacity .15s linear; 14 | } 15 | 16 | .fade-enter, 17 | .fade-leave-to { 18 | opacity: 0; 19 | } 20 | 21 | .mobile-page-enter-active, 22 | .mobile-page-leave-active { 23 | .page-content { 24 | transition: all .15s cubic-bezier(0.0, 0.0, 0.2, 1); 25 | } 26 | } 27 | 28 | .mobile-page-enter, 29 | .mobile-page-leave-to { 30 | .page-content { 31 | opacity: 0; 32 | transform: translateY(100px); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/style/vars.styl: -------------------------------------------------------------------------------- 1 | $primary-color = #40b883; 2 | 3 | $small-screen = "max-width: 800px"; 4 | $not-small-screen = "min-width: 800px"; 5 | $medium-screen = "max-width: 1024px"; 6 | $not-medium-screen = "min-width: 1024px"; 7 | -------------------------------------------------------------------------------- /src/utils/emoji.js: -------------------------------------------------------------------------------- 1 | import emojione from 'emojione' 2 | 3 | export default { 4 | install (Vue) { 5 | Vue.prototype.$parseEmoji = (text) => emojione.shortnameToUnicode(text) 6 | }, 7 | } 8 | -------------------------------------------------------------------------------- /src/utils/graph.js: -------------------------------------------------------------------------------- 1 | export function getSplineControlPoints (firstPoint, middlePoint, afterPoint, tension) { 2 | // Props to Rob Spencer at scaled innovation for his post on splining between points 3 | // http://scaledinnovation.com/analytics/splines/aboutSplines.html 4 | const d01 = Math.sqrt(Math.pow(middlePoint.x - firstPoint.x, 2) + Math.pow(middlePoint.y - firstPoint.y, 2)) 5 | const d12 = Math.sqrt(Math.pow(afterPoint.x - middlePoint.x, 2) + Math.pow(afterPoint.y - middlePoint.y, 2)) 6 | const fa = tension * d01 / (d01 + d12) // scaling factor for triangle Ta 7 | const fb = tension * d12 / (d01 + d12) 8 | return { 9 | inner: { 10 | x: middlePoint.x - fa * (afterPoint.x - firstPoint.x), 11 | y: middlePoint.y - fa * (afterPoint.y - firstPoint.y), 12 | }, 13 | outer: { 14 | x: middlePoint.x + fb * (afterPoint.x - firstPoint.x), 15 | y: middlePoint.y + fb * (afterPoint.y - firstPoint.y), 16 | }, 17 | } 18 | } 19 | 20 | export function getSplinePoints (points, min, max, tension = 0.5) { 21 | const result = [] 22 | 23 | for (let i = 1; i < points.length; i++) { 24 | const currentPoint = points[i] 25 | const previousPoint = points[i - 1] 26 | const nextPoint = i < points.length - 1 ? points[i + 1] : currentPoint 27 | 28 | const controlPoints = getSplineControlPoints(previousPoint, currentPoint, nextPoint, tension) 29 | 30 | // Prevent the curves from overflowing 31 | if (controlPoints.inner.y > max || controlPoints.outer.y > max) { 32 | controlPoints.inner.y = controlPoints.outer.y = max 33 | } else if (controlPoints.inner.y < min || controlPoints.outer.y < min) { 34 | controlPoints.inner.y = controlPoints.outer.y = min 35 | } 36 | 37 | result.push({ 38 | controlPoints, 39 | point: currentPoint, 40 | }) 41 | } 42 | 43 | return result 44 | } 45 | 46 | export function getSplineCurves (points, min, max, tension) { 47 | const result = [] 48 | const splinePoints = getSplinePoints(points, min, max, tension) 49 | 50 | for (let i = 1; i < splinePoints.length; i++) { 51 | const previousPoint = splinePoints[i - 1] 52 | const currentPoint = splinePoints[i] 53 | const coords = [ 54 | previousPoint.controlPoints.outer.x, 55 | previousPoint.controlPoints.outer.y, 56 | currentPoint.controlPoints.inner.x, 57 | currentPoint.controlPoints.inner.y, 58 | currentPoint.point.x, 59 | currentPoint.point.y, 60 | ] 61 | result.push(coords) 62 | } 63 | 64 | return result 65 | } 66 | 67 | export function printPoint (op, ...coords) { 68 | const points = [] 69 | for (let i = 0; i < coords.length; i += 2) { 70 | points.push(`${coords[i]},${coords[i + 1]}`) 71 | } 72 | return `${op} ${points.join(' ')} ` 73 | } 74 | -------------------------------------------------------------------------------- /src/utils/responsive.js: -------------------------------------------------------------------------------- 1 | 2 | export let responsive 3 | 4 | export default { 5 | install (Vue, options) { 6 | const finalOptions = Object.assign({}, { 7 | computed: {}, 8 | }, options) 9 | 10 | responsive = new Vue({ 11 | data () { 12 | return { 13 | width: window.innerWidth, 14 | height: window.innerHeight, 15 | } 16 | }, 17 | computed: finalOptions.computed, 18 | }) 19 | 20 | Object.defineProperty(Vue.prototype, '$responsive', { 21 | get: () => responsive, 22 | }) 23 | 24 | window.addEventListener('resize', () => { 25 | responsive.width = window.innerWidth 26 | responsive.height = window.innerHeight 27 | }) 28 | }, 29 | } 30 | -------------------------------------------------------------------------------- /src/utils/search.js: -------------------------------------------------------------------------------- 1 | 2 | export function search (items, text, fields) { 3 | // RegExps 4 | const finalText = text.trim() 5 | const words = { reg: new RegExp(`${finalText.replace(/\s+/g, '|')}`, 'gi'), weight: 1 } 6 | const normal = { reg: new RegExp(finalText, 'gi'), weight: 1000 } 7 | const exact = { reg: new RegExp(`^${finalText}$`, 'i'), weight: 100000 } 8 | // Order is important, the first reg is eliminatory 9 | // Here it's the most fuzzy one 10 | const regs = [words, normal, exact] 11 | 12 | const includedItems = [] 13 | items.forEach(item => { 14 | let score = 0 15 | 16 | // RegExps 17 | for (let i = 0; i < regs.length; i++) { 18 | const reg = regs[i] 19 | let regScore = 0 20 | 21 | // Fields 22 | for (const f of fields) { 23 | const value = f.field(item) 24 | if (value) { 25 | const matched = value.match(reg.reg) 26 | if (matched) { 27 | regScore += matched.length * f.weight 28 | } 29 | } 30 | } 31 | 32 | // The first regexp is eliminatory 33 | if (i === 0 && regScore === 0) { 34 | return false 35 | } 36 | 37 | // Matching score 38 | score += regScore * reg.weight 39 | } 40 | 41 | // Don't pollute the original array with score info 42 | includedItems.push({ 43 | item, 44 | score, 45 | }) 46 | }) 47 | 48 | // This should be quite fast 49 | includedItems.sort((a, b) => b.score - a.score) 50 | 51 | return includedItems.map(result => result.item) 52 | } 53 | -------------------------------------------------------------------------------- /tasks/update-fragment-matcher.js: -------------------------------------------------------------------------------- 1 | const fetch = require('node-fetch') 2 | const fs = require('fs') 3 | const path = require('path') 4 | 5 | const GRAPHQL_URI = process.env.GRAPHQL_URL || 'http://localhost:3000/graphql' 6 | 7 | fetch(GRAPHQL_URI, { 8 | method: 'POST', 9 | headers: { 'Content-Type': 'application/json' }, 10 | body: JSON.stringify({ 11 | query: ` 12 | { 13 | __schema { 14 | types { 15 | kind 16 | name 17 | possibleTypes { 18 | name 19 | } 20 | } 21 | } 22 | } 23 | `, 24 | }), 25 | }) 26 | .then(result => result.json()) 27 | .then(result => { 28 | // here we're filtering out any type information unrelated to unions or interfaces 29 | const filteredData = result.data.__schema.types.filter( 30 | type => type.possibleTypes !== null 31 | ) 32 | result.data.__schema.types = filteredData 33 | const file = path.resolve(__dirname, '../src/api/fragment-types.json') 34 | fs.writeFile(file, JSON.stringify(result.data), err => { 35 | if (err) { 36 | console.error(err) 37 | throw new Error('Error writing fragmentTypes file') 38 | } 39 | console.log('Fragment types successfully extracted!') 40 | }) 41 | }) 42 | .catch(e => { 43 | console.error(e) 44 | process.exit(1) 45 | }) 46 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const webpack = require('webpack') 3 | const HtmlPlugin = require('html-webpack-plugin') 4 | const CopyPlugin = require('copy-webpack-plugin') 5 | const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin 6 | 7 | const momentLocales = /en/ 8 | const outputPath = path.resolve(__dirname, './dist') 9 | 10 | module.exports = { 11 | entry: './src/main.js', 12 | output: { 13 | path: outputPath, 14 | publicPath: '/', 15 | filename: 'build.js', 16 | }, 17 | module: { 18 | rules: [ 19 | { 20 | test: /\.vue$/, 21 | loader: 'vue-loader', 22 | options: { 23 | loaders: { 24 | // Since sass-loader (weirdly) has SCSS as its default parse mode, we map 25 | // the "scss" and "sass" values for the lang attribute to the right configs here. 26 | // other preprocessors should work out of the box, no loader config like this nessessary. 27 | 'scss': 'vue-style-loader!css-loader!sass-loader', 28 | 'sass': 'vue-style-loader!css-loader!sass-loader?indentedSyntax', 29 | }, 30 | // other vue-loader options go here 31 | }, 32 | }, 33 | { 34 | test: /\.js$/, 35 | loader: 'babel-loader', 36 | exclude: /node_modules/, 37 | }, 38 | { 39 | test: /\.(png|jpg|gif|svg)$/, 40 | loader: 'file-loader', 41 | options: { 42 | name: '[name].[ext]?[hash]', 43 | }, 44 | }, 45 | { 46 | test: /\.css$/, 47 | loader: 'style-loader!css-loader', 48 | }, 49 | { 50 | test: /\.(graphql|gql)$/, 51 | exclude: /node_modules/, 52 | loader: 'graphql-tag/loader', 53 | }, 54 | ], 55 | }, 56 | resolve: { 57 | modules: ['src', 'node_modules'], 58 | extensions: ['.js', '.json', '.vue'], 59 | /* alias: { 60 | 'vue$': 'vue/dist/vue.common.js' 61 | } */ 62 | }, 63 | devServer: { 64 | historyApiFallback: true, 65 | noInfo: true, 66 | }, 67 | performance: { 68 | hints: false, 69 | }, 70 | devtool: '#eval-source-map', 71 | plugins: [ 72 | new HtmlPlugin({ 73 | template: 'src/index.html', 74 | }), 75 | new webpack.DefinePlugin({ 76 | 'process.env': { 77 | NODE_ENV: JSON.stringify(process.env.NODE_ENV), 78 | GRAPHQL_URL: JSON.stringify(process.env.GRAPHQL_URL || 'https://vue-curated-api.now.sh/graphql'), 79 | }, 80 | }), 81 | new webpack.ContextReplacementPlugin(/moment[/\\]locale$/, momentLocales), 82 | ], 83 | } 84 | 85 | if (process.env.NODE_ENV === 'production') { 86 | module.exports.devtool = '#source-map' 87 | // http://vue-loader.vuejs.org/en/workflow/production.html 88 | module.exports.plugins = (module.exports.plugins || []).concat([ 89 | new webpack.optimize.UglifyJsPlugin({ 90 | sourceMap: true, 91 | compress: { 92 | warnings: false, 93 | }, 94 | }), 95 | new webpack.LoaderOptionsPlugin({ 96 | minimize: true, 97 | }), 98 | new CopyPlugin([ 99 | { 100 | from: path.resolve(__dirname, './public'), 101 | to: outputPath, 102 | }, 103 | ]), 104 | ]) 105 | } 106 | 107 | if (process.env.ANALYZE === 'on') { 108 | module.exports.plugins = (module.exports.plugins || []).concat([ 109 | new BundleAnalyzerPlugin(), 110 | ]) 111 | } 112 | --------------------------------------------------------------------------------