├── .nvmrc ├── src ├── fonts │ ├── .gitkeep │ └── source-sans │ │ ├── sourcesanspro-black-webfont.eot │ │ ├── sourcesanspro-black-webfont.ttf │ │ ├── sourcesanspro-bold-webfont.eot │ │ ├── sourcesanspro-bold-webfont.ttf │ │ ├── sourcesanspro-bold-webfont.woff │ │ ├── sourcesanspro-light-webfont.eot │ │ ├── sourcesanspro-light-webfont.ttf │ │ ├── sourcesanspro-black-webfont.woff │ │ ├── sourcesanspro-black-webfont.woff2 │ │ ├── sourcesanspro-bold-webfont.woff2 │ │ ├── sourcesanspro-italic-webfont.eot │ │ ├── sourcesanspro-italic-webfont.ttf │ │ ├── sourcesanspro-italic-webfont.woff │ │ ├── sourcesanspro-italic-webfont.woff2 │ │ ├── sourcesanspro-light-webfont.woff │ │ ├── sourcesanspro-light-webfont.woff2 │ │ ├── sourcesanspro-regular-webfont.eot │ │ ├── sourcesanspro-regular-webfont.ttf │ │ ├── sourcesanspro-regular-webfont.woff │ │ ├── sourcesanspro-semibold-webfont.eot │ │ ├── sourcesanspro-semibold-webfont.ttf │ │ ├── sourcesanspro-bolditalic-webfont.eot │ │ ├── sourcesanspro-bolditalic-webfont.ttf │ │ ├── sourcesanspro-extralight-webfont.eot │ │ ├── sourcesanspro-extralight-webfont.ttf │ │ ├── sourcesanspro-regular-webfont.woff2 │ │ ├── sourcesanspro-semibold-webfont.woff │ │ ├── sourcesanspro-semibold-webfont.woff2 │ │ ├── sourcesanspro-blackitalic-webfont.eot │ │ ├── sourcesanspro-blackitalic-webfont.ttf │ │ ├── sourcesanspro-blackitalic-webfont.woff │ │ ├── sourcesanspro-blackitalic-webfont.woff2 │ │ ├── sourcesanspro-bolditalic-webfont.woff │ │ ├── sourcesanspro-bolditalic-webfont.woff2 │ │ ├── sourcesanspro-extralight-webfont.woff │ │ ├── sourcesanspro-extralight-webfont.woff2 │ │ ├── sourcesanspro-lightitalic-webfont.eot │ │ ├── sourcesanspro-lightitalic-webfont.ttf │ │ ├── sourcesanspro-lightitalic-webfont.woff │ │ ├── sourcesanspro-lightitalic-webfont.woff2 │ │ ├── sourcesanspro-semibolditalic-webfont.eot │ │ ├── sourcesanspro-semibolditalic-webfont.ttf │ │ ├── sourcesanspro-semibolditalic-webfont.woff │ │ ├── sourcesanspro-extralightitalic-webfont.eot │ │ ├── sourcesanspro-extralightitalic-webfont.ttf │ │ ├── sourcesanspro-extralightitalic-webfont.woff │ │ ├── sourcesanspro-extralightitalic-webfont.woff2 │ │ └── sourcesanspro-semibolditalic-webfont.woff2 ├── styles │ ├── vendor │ │ └── .gitkeep │ ├── base │ │ ├── base-images.scss │ │ ├── base-links.scss │ │ ├── base-colors.scss │ │ ├── base-hr.scss │ │ ├── base-code.scss │ │ ├── base-tables.scss │ │ ├── base-rhythm.scss │ │ ├── base-lists.scss │ │ ├── base-inputs.scss │ │ └── base-type.scss │ ├── keyframes │ │ ├── keyframes-spin.scss │ │ └── keyframes-pulse.scss │ ├── utilities-base │ │ ├── utility-rhythm.scss │ │ ├── utility-cursor.scss │ │ ├── utility-radius.scss │ │ ├── utility-display.scss │ │ ├── utility-position.scss │ │ ├── utility-container.scss │ │ ├── utility-background.scss │ │ ├── utility-limit.scss │ │ ├── utility-keep.scss │ │ ├── utility-box-model.scss │ │ ├── utility-separate.scss │ │ ├── utility-fill.scss │ │ ├── utility-type.scss │ │ ├── utility-hide.scss │ │ ├── utility-buffer.scss │ │ ├── utility-pull.scss │ │ ├── utility-push.scss │ │ └── utility-pad.scss │ ├── mixins │ │ ├── mixin-clear.scss │ │ ├── mixin-overflow.scss │ │ ├── mixin-overlay.scss │ │ ├── mixin-display.scss │ │ ├── mixin-radius.scss │ │ ├── mixin-cursor.scss │ │ ├── mixin-rhythm.scss │ │ ├── mixin-keep.scss │ │ ├── mixin-shadow.scss │ │ ├── mixin-container.scss │ │ ├── mixin-font.scss │ │ ├── mixin-background.scss │ │ ├── mixin-box-model.scss │ │ ├── mixin-row.scss │ │ ├── mixin-animation.scss │ │ ├── mixin-fill.scss │ │ ├── mixin-limit.scss │ │ └── mixin-hide.scss │ ├── utilities-composed │ │ ├── utility-row.scss │ │ ├── utility-inverse.scss │ │ ├── utility-control.scss │ │ └── utility-bodytext.scss │ ├── vendor-overrides │ │ └── iphone-inline-video.scss │ ├── transitions │ │ ├── transition-fade-in.scss │ │ ├── transition-fade.scss │ │ ├── transition-flip-vertical.scss │ │ ├── transition-hide.scss │ │ └── transition-scale-fade.scss │ ├── shared.scss │ ├── definitions │ │ └── functions.scss │ ├── global.scss │ └── webfonts │ │ └── webfont-source-sans.scss ├── app-icon │ ├── app-icon.png │ └── favicon.png ├── vue │ ├── directives │ │ ├── images-loaded.js │ │ └── index.js │ ├── mixins │ │ ├── index.js │ │ └── persist.js │ └── plugins │ │ ├── vue-meta.js │ │ ├── vuex.js │ │ ├── vue-i18n.js │ │ ├── index.js │ │ └── vue-router.js ├── assets │ └── user-avatar-placeholder.png ├── util │ ├── startsWith.js │ ├── eventHasMetaKey.js │ ├── trimWhitespace.js │ ├── escapeRegExpString.js │ ├── scrollToElement.js │ ├── extractClassnames.js │ ├── clearSelection.js │ ├── replaceAll.js │ ├── getDomainName.js │ ├── linkIsExternal.js │ ├── processLargeArray.js │ ├── index.js │ └── composeClassnames.js ├── locales │ ├── number-formats.js │ ├── index.js │ ├── date-time-formats.js │ └── en.js ├── components │ ├── transitions │ │ ├── Fade.vue │ │ ├── FadeIn.vue │ │ ├── ScaleFade.vue │ │ ├── FlipVertical.vue │ │ └── CustomTransition.vue │ ├── containers │ │ ├── PanelContent.vue │ │ ├── Card.vue │ │ ├── Panel.vue │ │ └── Page.vue │ ├── pages │ │ ├── PageNoAccess.vue │ │ ├── PageOffline.vue │ │ ├── PageLogin.vue │ │ ├── PageNotFound.vue │ │ ├── PageConsole.vue │ │ └── PageHome.vue │ ├── snippets │ │ ├── InlineIcon.vue │ │ ├── Pic.vue │ │ ├── InlineSpinner.vue │ │ ├── Icon.vue │ │ ├── Ellipsis.vue │ │ ├── PicSvg.vue │ │ └── Spinner.vue │ ├── console │ │ ├── ConsoleConfiguration.vue │ │ ├── ConsoleModels.vue │ │ └── ConsoleVuex.vue │ ├── popovers │ │ ├── PopoverCounter.vue │ │ └── PopoverMainMenu.vue │ ├── sections │ │ ├── FooterSummary.vue │ │ └── BlankState.vue │ ├── models │ │ ├── PostListItem.vue │ │ └── PostList.vue │ ├── controls │ │ ├── IconButton.vue │ │ └── Click.vue │ ├── panels │ │ └── PanelReadme.vue │ ├── layout │ │ └── TitlebarMenu.vue │ └── counters │ │ ├── LocalCounter.vue │ │ ├── GlobalCounterIterator.vue │ │ └── Counter.vue ├── services │ ├── api │ │ ├── index.js │ │ ├── apiAuth.js │ │ ├── apiPosts.js │ │ ├── apiUsers.js │ │ └── apiComments.js │ ├── panels.js │ ├── index.js │ ├── notifications.js │ ├── network.js │ ├── time.js │ ├── env.js │ ├── auth.js │ └── menu.js ├── models │ ├── User.js │ ├── Comment.js │ ├── index.js │ ├── Remote.js │ ├── Account.js │ └── Post.js ├── config │ ├── config.styles.js │ ├── index.js │ ├── config.dev.base.js │ ├── config.dev.routes.js │ ├── config.aliases.js │ └── config.routes.js ├── svg │ ├── check.svg │ ├── chevron-up.svg │ ├── chevron-down.svg │ ├── chevron-left.svg │ ├── chevron-right.svg │ ├── cross.svg │ ├── hamburger.svg │ ├── chevron-left-left.svg │ ├── chevron-right-right.svg │ ├── arrow-left.svg │ ├── arrow-right.svg │ ├── power.svg │ └── app-icon.svg ├── .htmllintrc └── store │ └── index.js ├── static └── .gitkeep ├── .eslintignore ├── tooling ├── env │ ├── prod.env.js │ ├── test.env.js │ ├── dev.env.js │ └── index.js ├── unit │ ├── .eslintrc │ ├── index.js │ └── karma.conf.js ├── dev-client.js ├── vue-loader.conf.js ├── e2e │ ├── nightwatch.chrome.globals.js │ ├── nightwatch.globals.js │ ├── nightwatch.chrome.conf.js │ ├── custom-assertions │ │ └── elementCount.js │ ├── nightwatch.conf.js │ └── runner.js ├── webpack.test.conf.js ├── build.js ├── check-versions.js └── custom-config.js ├── .gitignore ├── docco.json ├── .stylelintrc ├── docs └── .gitignore ├── reports └── .gitignore ├── Dockerfile ├── README.md ├── jsconfig.json ├── .editorconfig ├── spec ├── .eslintrc.js ├── components │ └── pages │ │ └── PageHome.spec.js ├── e2e │ └── appContentVisibleAtStart.spec.js ├── store │ └── mutations │ │ └── incrementCounter.spec.js ├── services │ └── env.spec.js ├── models │ └── Post.spec.js └── util │ ├── trimWhitespace.spec.js │ └── getDomainName.spec.js ├── .babelrc ├── postcss.config.js ├── .vscode ├── launch.json └── tasks.json ├── .eslintrc.js ├── .markdownlint.json └── LICENSE /.nvmrc: -------------------------------------------------------------------------------- 1 | 8.1.2 2 | -------------------------------------------------------------------------------- /src/fonts/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/styles/vendor/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | tooling/*.js 2 | -------------------------------------------------------------------------------- /tooling/env/prod.env.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | NODE_ENV: '"production"' 3 | } 4 | -------------------------------------------------------------------------------- /src/styles/base/base-images.scss: -------------------------------------------------------------------------------- 1 | 2 | @import '~@shared-styles'; 3 | 4 | // img, 5 | // svg { 6 | // } 7 | -------------------------------------------------------------------------------- /src/styles/base/base-links.scss: -------------------------------------------------------------------------------- 1 | 2 | @import '~@shared-styles'; 3 | 4 | a { 5 | color: inherit; 6 | } 7 | -------------------------------------------------------------------------------- /src/app-icon/app-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jerryjappinen/bellevue-v1/HEAD/src/app-icon/app-icon.png -------------------------------------------------------------------------------- /src/app-icon/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jerryjappinen/bellevue-v1/HEAD/src/app-icon/favicon.png -------------------------------------------------------------------------------- /src/vue/directives/images-loaded.js: -------------------------------------------------------------------------------- 1 | 2 | import ImagesLoaded from 'vue-images-loaded'; 3 | export default ImagesLoaded; 4 | -------------------------------------------------------------------------------- /src/assets/user-avatar-placeholder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jerryjappinen/bellevue-v1/HEAD/src/assets/user-avatar-placeholder.png -------------------------------------------------------------------------------- /src/styles/keyframes/keyframes-spin.scss: -------------------------------------------------------------------------------- 1 | 2 | @keyframes spin { 3 | 4 | 100% { 5 | transform: rotate(360deg); 6 | } 7 | 8 | } 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | dist/ 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | selenium-debug.log 8 | .idea 9 | -------------------------------------------------------------------------------- /src/styles/utilities-base/utility-rhythm.scss: -------------------------------------------------------------------------------- 1 | 2 | @import '~@shared-styles'; 3 | 4 | 5 | 6 | .no-rhythm { 7 | @include no-rhythm; 8 | } 9 | -------------------------------------------------------------------------------- /src/util/startsWith.js: -------------------------------------------------------------------------------- 1 | export default function (string, substring) { 2 | return string.lastIndexOf(substring, 0) === 0 ? true : false; 3 | } 4 | -------------------------------------------------------------------------------- /src/styles/base/base-colors.scss: -------------------------------------------------------------------------------- 1 | 2 | @import '~@shared-styles'; 3 | 4 | html { 5 | color: $color-dark; 6 | background-color: $color-verylightgrey; 7 | } 8 | -------------------------------------------------------------------------------- /tooling/unit/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true 4 | }, 5 | "globals": { 6 | "expect": true, 7 | "sinon": true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/fonts/source-sans/sourcesanspro-black-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jerryjappinen/bellevue-v1/HEAD/src/fonts/source-sans/sourcesanspro-black-webfont.eot -------------------------------------------------------------------------------- /src/fonts/source-sans/sourcesanspro-black-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jerryjappinen/bellevue-v1/HEAD/src/fonts/source-sans/sourcesanspro-black-webfont.ttf -------------------------------------------------------------------------------- /src/fonts/source-sans/sourcesanspro-bold-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jerryjappinen/bellevue-v1/HEAD/src/fonts/source-sans/sourcesanspro-bold-webfont.eot -------------------------------------------------------------------------------- /src/fonts/source-sans/sourcesanspro-bold-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jerryjappinen/bellevue-v1/HEAD/src/fonts/source-sans/sourcesanspro-bold-webfont.ttf -------------------------------------------------------------------------------- /src/fonts/source-sans/sourcesanspro-bold-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jerryjappinen/bellevue-v1/HEAD/src/fonts/source-sans/sourcesanspro-bold-webfont.woff -------------------------------------------------------------------------------- /src/fonts/source-sans/sourcesanspro-light-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jerryjappinen/bellevue-v1/HEAD/src/fonts/source-sans/sourcesanspro-light-webfont.eot -------------------------------------------------------------------------------- /src/fonts/source-sans/sourcesanspro-light-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jerryjappinen/bellevue-v1/HEAD/src/fonts/source-sans/sourcesanspro-light-webfont.ttf -------------------------------------------------------------------------------- /docco.json: -------------------------------------------------------------------------------- 1 | { 2 | ".vue": { 3 | "name": "html", 4 | "symbol": "(//)" 5 | }, 6 | ".html": { 7 | "name": "html", 8 | "symbol": "(//)" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/fonts/source-sans/sourcesanspro-black-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jerryjappinen/bellevue-v1/HEAD/src/fonts/source-sans/sourcesanspro-black-webfont.woff -------------------------------------------------------------------------------- /src/fonts/source-sans/sourcesanspro-black-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jerryjappinen/bellevue-v1/HEAD/src/fonts/source-sans/sourcesanspro-black-webfont.woff2 -------------------------------------------------------------------------------- /src/fonts/source-sans/sourcesanspro-bold-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jerryjappinen/bellevue-v1/HEAD/src/fonts/source-sans/sourcesanspro-bold-webfont.woff2 -------------------------------------------------------------------------------- /src/fonts/source-sans/sourcesanspro-italic-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jerryjappinen/bellevue-v1/HEAD/src/fonts/source-sans/sourcesanspro-italic-webfont.eot -------------------------------------------------------------------------------- /src/fonts/source-sans/sourcesanspro-italic-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jerryjappinen/bellevue-v1/HEAD/src/fonts/source-sans/sourcesanspro-italic-webfont.ttf -------------------------------------------------------------------------------- /src/fonts/source-sans/sourcesanspro-italic-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jerryjappinen/bellevue-v1/HEAD/src/fonts/source-sans/sourcesanspro-italic-webfont.woff -------------------------------------------------------------------------------- /src/fonts/source-sans/sourcesanspro-italic-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jerryjappinen/bellevue-v1/HEAD/src/fonts/source-sans/sourcesanspro-italic-webfont.woff2 -------------------------------------------------------------------------------- /src/fonts/source-sans/sourcesanspro-light-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jerryjappinen/bellevue-v1/HEAD/src/fonts/source-sans/sourcesanspro-light-webfont.woff -------------------------------------------------------------------------------- /src/fonts/source-sans/sourcesanspro-light-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jerryjappinen/bellevue-v1/HEAD/src/fonts/source-sans/sourcesanspro-light-webfont.woff2 -------------------------------------------------------------------------------- /src/fonts/source-sans/sourcesanspro-regular-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jerryjappinen/bellevue-v1/HEAD/src/fonts/source-sans/sourcesanspro-regular-webfont.eot -------------------------------------------------------------------------------- /src/fonts/source-sans/sourcesanspro-regular-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jerryjappinen/bellevue-v1/HEAD/src/fonts/source-sans/sourcesanspro-regular-webfont.ttf -------------------------------------------------------------------------------- /src/fonts/source-sans/sourcesanspro-regular-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jerryjappinen/bellevue-v1/HEAD/src/fonts/source-sans/sourcesanspro-regular-webfont.woff -------------------------------------------------------------------------------- /src/fonts/source-sans/sourcesanspro-semibold-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jerryjappinen/bellevue-v1/HEAD/src/fonts/source-sans/sourcesanspro-semibold-webfont.eot -------------------------------------------------------------------------------- /src/fonts/source-sans/sourcesanspro-semibold-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jerryjappinen/bellevue-v1/HEAD/src/fonts/source-sans/sourcesanspro-semibold-webfont.ttf -------------------------------------------------------------------------------- /src/fonts/source-sans/sourcesanspro-bolditalic-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jerryjappinen/bellevue-v1/HEAD/src/fonts/source-sans/sourcesanspro-bolditalic-webfont.eot -------------------------------------------------------------------------------- /src/fonts/source-sans/sourcesanspro-bolditalic-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jerryjappinen/bellevue-v1/HEAD/src/fonts/source-sans/sourcesanspro-bolditalic-webfont.ttf -------------------------------------------------------------------------------- /src/fonts/source-sans/sourcesanspro-extralight-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jerryjappinen/bellevue-v1/HEAD/src/fonts/source-sans/sourcesanspro-extralight-webfont.eot -------------------------------------------------------------------------------- /src/fonts/source-sans/sourcesanspro-extralight-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jerryjappinen/bellevue-v1/HEAD/src/fonts/source-sans/sourcesanspro-extralight-webfont.ttf -------------------------------------------------------------------------------- /src/fonts/source-sans/sourcesanspro-regular-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jerryjappinen/bellevue-v1/HEAD/src/fonts/source-sans/sourcesanspro-regular-webfont.woff2 -------------------------------------------------------------------------------- /src/fonts/source-sans/sourcesanspro-semibold-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jerryjappinen/bellevue-v1/HEAD/src/fonts/source-sans/sourcesanspro-semibold-webfont.woff -------------------------------------------------------------------------------- /src/fonts/source-sans/sourcesanspro-semibold-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jerryjappinen/bellevue-v1/HEAD/src/fonts/source-sans/sourcesanspro-semibold-webfont.woff2 -------------------------------------------------------------------------------- /src/styles/mixins/mixin-clear.scss: -------------------------------------------------------------------------------- 1 | 2 | @mixin clear-after { 3 | *zoom: 1; 4 | &:after { 5 | content: ' '; 6 | display: table; 7 | clear: both; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/fonts/source-sans/sourcesanspro-blackitalic-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jerryjappinen/bellevue-v1/HEAD/src/fonts/source-sans/sourcesanspro-blackitalic-webfont.eot -------------------------------------------------------------------------------- /src/fonts/source-sans/sourcesanspro-blackitalic-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jerryjappinen/bellevue-v1/HEAD/src/fonts/source-sans/sourcesanspro-blackitalic-webfont.ttf -------------------------------------------------------------------------------- /src/fonts/source-sans/sourcesanspro-blackitalic-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jerryjappinen/bellevue-v1/HEAD/src/fonts/source-sans/sourcesanspro-blackitalic-webfont.woff -------------------------------------------------------------------------------- /src/fonts/source-sans/sourcesanspro-blackitalic-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jerryjappinen/bellevue-v1/HEAD/src/fonts/source-sans/sourcesanspro-blackitalic-webfont.woff2 -------------------------------------------------------------------------------- /src/fonts/source-sans/sourcesanspro-bolditalic-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jerryjappinen/bellevue-v1/HEAD/src/fonts/source-sans/sourcesanspro-bolditalic-webfont.woff -------------------------------------------------------------------------------- /src/fonts/source-sans/sourcesanspro-bolditalic-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jerryjappinen/bellevue-v1/HEAD/src/fonts/source-sans/sourcesanspro-bolditalic-webfont.woff2 -------------------------------------------------------------------------------- /src/fonts/source-sans/sourcesanspro-extralight-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jerryjappinen/bellevue-v1/HEAD/src/fonts/source-sans/sourcesanspro-extralight-webfont.woff -------------------------------------------------------------------------------- /src/fonts/source-sans/sourcesanspro-extralight-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jerryjappinen/bellevue-v1/HEAD/src/fonts/source-sans/sourcesanspro-extralight-webfont.woff2 -------------------------------------------------------------------------------- /src/fonts/source-sans/sourcesanspro-lightitalic-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jerryjappinen/bellevue-v1/HEAD/src/fonts/source-sans/sourcesanspro-lightitalic-webfont.eot -------------------------------------------------------------------------------- /src/fonts/source-sans/sourcesanspro-lightitalic-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jerryjappinen/bellevue-v1/HEAD/src/fonts/source-sans/sourcesanspro-lightitalic-webfont.ttf -------------------------------------------------------------------------------- /src/fonts/source-sans/sourcesanspro-lightitalic-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jerryjappinen/bellevue-v1/HEAD/src/fonts/source-sans/sourcesanspro-lightitalic-webfont.woff -------------------------------------------------------------------------------- /src/fonts/source-sans/sourcesanspro-lightitalic-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jerryjappinen/bellevue-v1/HEAD/src/fonts/source-sans/sourcesanspro-lightitalic-webfont.woff2 -------------------------------------------------------------------------------- /tooling/env/test.env.js: -------------------------------------------------------------------------------- 1 | var merge = require('webpack-merge') 2 | var devEnv = require('./dev.env') 3 | 4 | module.exports = merge(devEnv, { 5 | NODE_ENV: '"testing"' 6 | }) 7 | -------------------------------------------------------------------------------- /src/fonts/source-sans/sourcesanspro-semibolditalic-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jerryjappinen/bellevue-v1/HEAD/src/fonts/source-sans/sourcesanspro-semibolditalic-webfont.eot -------------------------------------------------------------------------------- /src/fonts/source-sans/sourcesanspro-semibolditalic-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jerryjappinen/bellevue-v1/HEAD/src/fonts/source-sans/sourcesanspro-semibolditalic-webfont.ttf -------------------------------------------------------------------------------- /src/fonts/source-sans/sourcesanspro-semibolditalic-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jerryjappinen/bellevue-v1/HEAD/src/fonts/source-sans/sourcesanspro-semibolditalic-webfont.woff -------------------------------------------------------------------------------- /src/styles/base/base-hr.scss: -------------------------------------------------------------------------------- 1 | 2 | @import '~@shared-styles'; 3 | 4 | hr { 5 | border-width: 1px 0 0 0; 6 | border-style: solid; 7 | border-color: $color-dark-translucent; 8 | } 9 | -------------------------------------------------------------------------------- /src/vue/directives/index.js: -------------------------------------------------------------------------------- 1 | 2 | import ImagesLoaded from './images-loaded'; 3 | 4 | export { 5 | ImagesLoaded 6 | }; 7 | 8 | export default { 9 | ImagesLoaded 10 | }; 11 | -------------------------------------------------------------------------------- /tooling/env/dev.env.js: -------------------------------------------------------------------------------- 1 | var merge = require('webpack-merge') 2 | var prodEnv = require('./prod.env') 3 | 4 | module.exports = merge(prodEnv, { 5 | NODE_ENV: '"development"' 6 | }) 7 | -------------------------------------------------------------------------------- /src/fonts/source-sans/sourcesanspro-extralightitalic-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jerryjappinen/bellevue-v1/HEAD/src/fonts/source-sans/sourcesanspro-extralightitalic-webfont.eot -------------------------------------------------------------------------------- /src/fonts/source-sans/sourcesanspro-extralightitalic-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jerryjappinen/bellevue-v1/HEAD/src/fonts/source-sans/sourcesanspro-extralightitalic-webfont.ttf -------------------------------------------------------------------------------- /src/fonts/source-sans/sourcesanspro-extralightitalic-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jerryjappinen/bellevue-v1/HEAD/src/fonts/source-sans/sourcesanspro-extralightitalic-webfont.woff -------------------------------------------------------------------------------- /src/fonts/source-sans/sourcesanspro-extralightitalic-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jerryjappinen/bellevue-v1/HEAD/src/fonts/source-sans/sourcesanspro-extralightitalic-webfont.woff2 -------------------------------------------------------------------------------- /src/fonts/source-sans/sourcesanspro-semibolditalic-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jerryjappinen/bellevue-v1/HEAD/src/fonts/source-sans/sourcesanspro-semibolditalic-webfont.woff2 -------------------------------------------------------------------------------- /.stylelintrc: -------------------------------------------------------------------------------- 1 | { 2 | "processors": [ 3 | "stylelint-processor-html" 4 | ], 5 | "extends": "stylelint-config-standard", 6 | "rules": { 7 | "no-empty-source": null 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/styles/utilities-composed/utility-row.scss: -------------------------------------------------------------------------------- 1 | 2 | @import '~@shared-styles'; 3 | 4 | 5 | 6 | .row { 7 | @include row; 8 | } 9 | 10 | .row-content { 11 | @include row-content; 12 | } 13 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore everything in this directory, except this file. 2 | * 3 | 4 | # see: http://stackoverflow.com/questions/115983/how-do-i-add-an-empty-directory-to-a-git-repository 5 | !.gitignore 6 | -------------------------------------------------------------------------------- /src/util/eventHasMetaKey.js: -------------------------------------------------------------------------------- 1 | // Check if keyboard event object includes meta key being pressed 2 | export default function (event) { 3 | return (event.ctrlKey || event.metaKey || event.shiftKey); 4 | } 5 | -------------------------------------------------------------------------------- /reports/.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore everything in this directory, except this file. 2 | * 3 | 4 | # see: http://stackoverflow.com/questions/115983/how-do-i-add-an-empty-directory-to-a-git-repository 5 | !.gitignore 6 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM kyma/docker-nginx 2 | 3 | # Example if you wanna swap the default server file. 4 | # COPY path/to/your/default /etc/nginx/sites-enabled/default 5 | 6 | COPY dist/ /var/www 7 | 8 | CMD 'nginx' 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bellevue 2 | 3 | Bellevue is a full-featured frontend project template for modern single-page applications built on Vue.js and Webpack. 4 | 5 | **THIS REPO has moved: https://github.com/Eiskis/bellevue** 6 | -------------------------------------------------------------------------------- /src/util/trimWhitespace.js: -------------------------------------------------------------------------------- 1 | import { trim } from 'lodash'; 2 | 3 | // Condence all whitespace in a string to maximum of one space 4 | export default function (string) { 5 | return trim(string).replace(/\s+/g, ' '); 6 | } 7 | -------------------------------------------------------------------------------- /src/util/escapeRegExpString.js: -------------------------------------------------------------------------------- 1 | // https://stackoverflow.com/questions/3561493/is-there-a-regexp-escape-function-in-javascript 2 | export default function (string) { 3 | return string.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); 4 | } 5 | -------------------------------------------------------------------------------- /src/styles/mixins/mixin-overflow.scss: -------------------------------------------------------------------------------- 1 | 2 | @mixin overflow-vertical { 3 | overflow-x: hidden; 4 | overflow-y: auto; 5 | } 6 | 7 | @mixin overflow-hidden { 8 | overflow: hidden; 9 | -webkit-overflow-scrolling: auto; 10 | } 11 | -------------------------------------------------------------------------------- /src/locales/number-formats.js: -------------------------------------------------------------------------------- 1 | 2 | // See details: https://kazupon.github.io/vue-i18n/en/number.html 3 | export default { 4 | 5 | 'en': { 6 | currency: { 7 | style: 'currency', 8 | currency: 'USD' 9 | } 10 | } 11 | 12 | }; 13 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES6", 4 | "experimentalDecorators": true, 5 | "allowSyntheticDefaultImports": true 6 | }, 7 | "include": [ 8 | "spec/**/*.js", 9 | "src/**/*.js", 10 | "src/**/*.vue" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /src/styles/vendor-overrides/iphone-inline-video.scss: -------------------------------------------------------------------------------- 1 | 2 | // @import '~@shared-styles'; 3 | 4 | .IIV::-webkit-media-controls-play-button, 5 | .IIV::-webkit-media-controls-start-playback-button { 6 | pointer-events: none; 7 | width: 5px; 8 | opacity: 0; 9 | } 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = tab 6 | # indent_size = 4 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.{yml,yaml}] 12 | indent_style = space 13 | indent_size = 2 14 | -------------------------------------------------------------------------------- /src/util/scrollToElement.js: -------------------------------------------------------------------------------- 1 | import naturalScroll from 'natural-scroll'; 2 | 3 | export default function (container, element, duration, offset) { 4 | let position = element.offsetTop - (offset ? offset : 0); 5 | naturalScroll.scrollTop(container, position, duration); 6 | } 7 | -------------------------------------------------------------------------------- /src/styles/mixins/mixin-overlay.scss: -------------------------------------------------------------------------------- 1 | 2 | @mixin overlay ($direction: 0deg, $opacity: 0.25, $color: $color-black) { 3 | background-image: linear-gradient( 4 | $direction, 5 | rgba($color, 0), 6 | rgba($color, ($opacity / 2)) 60%, 7 | rgba($color, $opacity) 8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /src/vue/mixins/index.js: -------------------------------------------------------------------------------- 1 | 2 | import persist from './persist'; 3 | 4 | export const local = {}; 5 | 6 | export const global = { 7 | persist 8 | }; 9 | 10 | export { 11 | // someMixin 12 | persist 13 | }; 14 | 15 | export default { 16 | // someMixin 17 | persist 18 | }; 19 | -------------------------------------------------------------------------------- /src/util/extractClassnames.js: -------------------------------------------------------------------------------- 1 | // Pick the keys of a classname hash whose values are truthy 2 | export default function (stateHash) { 3 | var classes = []; 4 | for (var key in stateHash) { 5 | if (stateHash[key]) { 6 | classes.push(key); 7 | } 8 | } 9 | return classes; 10 | } 11 | -------------------------------------------------------------------------------- /spec/.eslintrc.js: -------------------------------------------------------------------------------- 1 | 2 | var _ = require('lodash'); 3 | var base = require('../src/.eslintrc'); 4 | 5 | module.exports = _.merge( 6 | {}, 7 | base, 8 | { 9 | 'env': { 10 | 'mocha': true 11 | }, 12 | 'globals': { 13 | 'expect': true, 14 | 'sinon': true 15 | } 16 | } 17 | ); 18 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { "modules": false }], 4 | "stage-2" 5 | ], 6 | "plugins": ["transform-runtime"], 7 | "comments": false, 8 | "env": { 9 | "test": { 10 | "presets": ["env", "stage-2"], 11 | "plugins": [ "istanbul" ] 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /tooling/dev-client.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | require('eventsource-polyfill') 3 | var hotClient = require('webpack-hot-middleware/client?noInfo=true&reload=true') 4 | 5 | hotClient.subscribe(function (event) { 6 | if (event.action === 'reload') { 7 | window.location.reload() 8 | } 9 | }) 10 | -------------------------------------------------------------------------------- /src/util/clearSelection.js: -------------------------------------------------------------------------------- 1 | export default function () { 2 | 3 | if (window.document.selection && window.document.selection.empty) { 4 | window.document.selection.empty(); 5 | 6 | } else if (window.getSelection) { 7 | var sel = window.getSelection(); 8 | sel.removeAllRanges(); 9 | } 10 | 11 | } 12 | -------------------------------------------------------------------------------- /src/styles/utilities-base/utility-cursor.scss: -------------------------------------------------------------------------------- 1 | 2 | .cursor-grab { 3 | cursor: grab; 4 | } 5 | 6 | .cursor-pointer { 7 | cursor: pointer; 8 | } 9 | 10 | .cursor-text { 11 | cursor: text; 12 | } 13 | 14 | .cursor-default { 15 | cursor: default; 16 | } 17 | 18 | .cursor-inherit { 19 | cursor: inherit; 20 | } 21 | -------------------------------------------------------------------------------- /src/locales/index.js: -------------------------------------------------------------------------------- 1 | 2 | import en from './en'; 3 | 4 | import dateTimeFormats from './date-time-formats'; 5 | import numberFormats from './number-formats'; 6 | 7 | export default { 8 | 9 | dateTimeFormats: dateTimeFormats, 10 | numberFormats: numberFormats, 11 | 12 | messages: { 13 | en 14 | } 15 | 16 | }; 17 | -------------------------------------------------------------------------------- /src/styles/mixins/mixin-display.scss: -------------------------------------------------------------------------------- 1 | 2 | @mixin inline { 3 | display: inline; 4 | } 5 | 6 | @mixin inline-block { 7 | display: inline-block; 8 | } 9 | 10 | @mixin block { 11 | display: block; 12 | } 13 | 14 | @mixin flex { 15 | display: flex; 16 | } 17 | 18 | @mixin inline-flex { 19 | display: inline-flex; 20 | } 21 | -------------------------------------------------------------------------------- /src/vue/plugins/vue-meta.js: -------------------------------------------------------------------------------- 1 | 2 | // Handling meta tags and per component 3 | // https://github.com/declandewet/vue-meta 4 | import Vue from 'vue'; 5 | import VueMeta from 'vue-meta'; 6 | 7 | // Import route components for vue-router 8 | import config from '@config'; 9 | 10 | Vue.use(VueMeta, config.metaInfo); 11 | 12 | export default VueMeta; 13 | -------------------------------------------------------------------------------- /src/components/transitions/Fade.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /tooling/vue-loader.conf.js: -------------------------------------------------------------------------------- 1 | var utils = require('./utils') 2 | var config = require('./env') 3 | var isProduction = process.env.NODE_ENV === 'production' 4 | 5 | module.exports = { 6 | loaders: utils.cssLoaders({ 7 | sourceMap: isProduction 8 | ? config.build.productionSourceMap 9 | : config.dev.cssSourceMap, 10 | extract: isProduction 11 | }) 12 | } 13 | -------------------------------------------------------------------------------- /src/components/transitions/FadeIn.vue: -------------------------------------------------------------------------------- 1 | 2 | 14 | 15 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/styles/mixins/mixin-radius.scss: -------------------------------------------------------------------------------- 1 | 2 | @mixin radius ($rad: $radius) { 3 | border-radius: $rad; 4 | } 5 | 6 | @mixin radius-tight { 7 | border-radius: $radius-tight; 8 | } 9 | 10 | @mixin radius-loose { 11 | border-radius: $radius-loose; 12 | } 13 | 14 | @mixin radius-round { 15 | border-radius: 1000em; 16 | } 17 | 18 | @mixin no-radius { 19 | border-radius: 0; 20 | } 21 | -------------------------------------------------------------------------------- /src/styles/utilities-base/utility-radius.scss: -------------------------------------------------------------------------------- 1 | 2 | @import '~@shared-styles'; 3 | 4 | 5 | 6 | .radius { 7 | @include radius; 8 | } 9 | 10 | .radius-tight { 11 | @include radius-tight; 12 | } 13 | 14 | .radius-loose { 15 | @include radius-loose; 16 | } 17 | 18 | .radius-round { 19 | @include radius-round; 20 | } 21 | 22 | .no-radius { 23 | @include radius-round; 24 | } 25 | -------------------------------------------------------------------------------- /src/components/transitions/ScaleFade.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/components/transitions/FlipVertical.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | // https://github.com/michael-ciniawsky/postcss-load-config 2 | // https://github.com/postcss/postcss-scss/issues/49 3 | 4 | module.exports = { 5 | 'syntax': require('postcss-scss'), 6 | // 'syntax': 'postcss-scss', 7 | 'parser': 'scss', 8 | 'plugins': { 9 | // to edit target browsers: use 'browserlist' field in package.json 10 | 'autoprefixer': {} 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/services/api/index.js: -------------------------------------------------------------------------------- 1 | import { merge } from 'lodash'; 2 | import Vue from 'vue'; 3 | 4 | import api from './api'; 5 | import apiAuth from './apiAuth'; 6 | import apiComments from './apiComments'; 7 | import apiPosts from './apiPosts'; 8 | import apiUsers from './apiUsers'; 9 | 10 | export default new Vue(merge( 11 | api, 12 | apiAuth, 13 | apiComments, 14 | apiPosts, 15 | apiUsers 16 | )); 17 | -------------------------------------------------------------------------------- /src/styles/mixins/mixin-cursor.scss: -------------------------------------------------------------------------------- 1 | 2 | @mixin cursor-grab { 3 | cursor: grab; 4 | } 5 | 6 | @mixin cursor-grabbing { 7 | cursor: grabbing; 8 | } 9 | 10 | @mixin cursor-pointer { 11 | cursor: pointer; 12 | } 13 | 14 | @mixin cursor-text { 15 | cursor: text; 16 | } 17 | 18 | @mixin cursor-default { 19 | cursor: default; 20 | } 21 | 22 | @mixin cursor-inherit { 23 | cursor: inherit; 24 | } 25 | -------------------------------------------------------------------------------- /tooling/e2e/nightwatch.chrome.globals.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash') 2 | var chromedriver = require('chromedriver') 3 | 4 | var defaults = require('./nightwatch.globals.js') 5 | 6 | module.exports = _.merge(defaults, { 7 | 8 | before: function (done) { 9 | chromedriver.start() 10 | done() 11 | }, 12 | 13 | after: function (done) { 14 | chromedriver.stop() 15 | done() 16 | } 17 | 18 | }) 19 | -------------------------------------------------------------------------------- /src/styles/utilities-base/utility-display.scss: -------------------------------------------------------------------------------- 1 | 2 | @import '~@shared-styles'; 3 | 4 | 5 | 6 | // NOTE: order is important: blockiest classes last so they override inliny classes 7 | 8 | .inline { 9 | @include inline; 10 | } 11 | 12 | .inline-block { 13 | @include inline-block; 14 | } 15 | 16 | .block { 17 | @include block; 18 | } 19 | 20 | .flex { 21 | @include flex; 22 | } 23 | 24 | .inline-flex { 25 | @include inline-flex; 26 | } 27 | -------------------------------------------------------------------------------- /src/util/replaceAll.js: -------------------------------------------------------------------------------- 1 | import escapeRegExpString from './escapeRegExpString'; 2 | 3 | export default function (string, search, replacement) { 4 | 5 | if (search && (typeof search === 'string' || typeof search === 'number')) { 6 | search = '' + search; 7 | 8 | if (!replacement) { 9 | replacement = ''; 10 | } 11 | 12 | return string.replace(new RegExp(escapeRegExpString(search), 'g'), replacement); 13 | } 14 | 15 | return string; 16 | } 17 | -------------------------------------------------------------------------------- /src/vue/plugins/vuex.js: -------------------------------------------------------------------------------- 1 | 2 | // State management library 3 | // https://vuex.vuejs.org/en/ 4 | // NOTE: This file is only initializes the plugin, the state management logic is under store/ 5 | 6 | import Vue from 'vue'; 7 | import Vuex from 'vuex'; 8 | 9 | // Fetch the state, actions and getters etc. from under store/ 10 | import { store } from '@store'; 11 | 12 | // Autoload plugin 13 | Vue.use(Vuex); 14 | 15 | // Export a new plugin instance 16 | export default new Vuex.Store(store); 17 | -------------------------------------------------------------------------------- /src/components/containers/PanelContent.vue: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 15 | 16 | 29 | -------------------------------------------------------------------------------- /src/styles/utilities-base/utility-position.scss: -------------------------------------------------------------------------------- 1 | 2 | .static { 3 | position: static; 4 | } 5 | 6 | .relative { 7 | position: relative; 8 | } 9 | 10 | .absolute { 11 | position: absolute; 12 | } 13 | 14 | .fixed { 15 | position: fixed; 16 | } 17 | 18 | .absolute, 19 | .fixed { 20 | 21 | &.keep-top { 22 | top: 0; 23 | } 24 | 25 | &.keep-bottom { 26 | bottom: 0; 27 | } 28 | 29 | &.keep-left { 30 | left: 0; 31 | } 32 | 33 | &.keep-right { 34 | right: 0; 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/styles/transitions/transition-fade-in.scss: -------------------------------------------------------------------------------- 1 | 2 | @import '~@shared-styles'; 3 | 4 | // FIXME: would be better to not specify the opacity value 1, but let the default or component's defined value stand 5 | .transition-fade-in-enter-active { 6 | opacity: 1; 7 | @include transition-fast; 8 | @include transition-properties(opacity); 9 | } 10 | 11 | .transition-fade-in-leave-active { 12 | @include no-transition; 13 | } 14 | 15 | // Start state for enter 16 | .transition-fade-in-enter { 17 | opacity: 0; 18 | } 19 | -------------------------------------------------------------------------------- /src/styles/utilities-base/utility-container.scss: -------------------------------------------------------------------------------- 1 | 2 | @import '~@shared-styles'; 3 | 4 | .container-white { 5 | @include container-white; 6 | } 7 | 8 | .container-white-buffer { 9 | @include container-white-buffer; 10 | } 11 | 12 | .container-dark { 13 | @include container-dark; 14 | } 15 | 16 | .container-dark-buffer { 17 | @include container-dark-buffer; 18 | } 19 | 20 | .container-card { 21 | @include container-card; 22 | } 23 | 24 | .container-card-buffer { 25 | @include container-card-buffer; 26 | } 27 | -------------------------------------------------------------------------------- /src/components/pages/PageNoAccess.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 18 | 19 | 22 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible Node.js debug attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Launch Program", 11 | "program": "${workspaceRoot}/tooling/dev-server.js" 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /src/models/User.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | 3 | export default Vue.extend({ 4 | 5 | props: { 6 | 7 | id: { 8 | type: Number, 9 | required: true 10 | }, 11 | 12 | name: { 13 | type: String 14 | }, 15 | 16 | email: { 17 | type: String 18 | }, 19 | 20 | website: { 21 | type: String, 22 | default: null 23 | } 24 | 25 | }, 26 | 27 | computed: { 28 | 29 | hasWebsite: function () { 30 | return this.website && this.website.length ? true : false; 31 | } 32 | 33 | } 34 | 35 | }); 36 | -------------------------------------------------------------------------------- /src/util/getDomainName.js: -------------------------------------------------------------------------------- 1 | import { trim } from 'lodash'; 2 | 3 | export default function (string) { 4 | var domain = trim(string); 5 | 6 | // Find & remove protocol (http, ftp, etc.) and get domain 7 | if (domain.indexOf('://') > -1) { 8 | domain = domain.split('/')[2]; 9 | } else { 10 | domain = domain.split('/')[0]; 11 | } 12 | 13 | // Find & remove query parameters 14 | domain = domain.split('?')[0]; 15 | 16 | // Find & remove port number 17 | domain = domain.split(':')[0]; 18 | 19 | return domain; 20 | } 21 | -------------------------------------------------------------------------------- /src/styles/base/base-code.scss: -------------------------------------------------------------------------------- 1 | 2 | @import '~@shared-styles'; 3 | 4 | 5 | 6 | code { 7 | display: inline-block; 8 | } 9 | 10 | pre { 11 | code { 12 | // line-height: $line-height-tight; 13 | 14 | display: block; 15 | padding: 1.6em; 16 | // border-width: 1px; 17 | 18 | @include radius-loose; 19 | // border-color: $color-lightgrey; 20 | // background-color: $color-verylightgrey; 21 | // background-color: $color-white; 22 | color: $color-white; 23 | background-color: $color-darkgrey; 24 | 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/components/snippets/InlineIcon.vue: -------------------------------------------------------------------------------- 1 | 2 | 20 | 21 | 26 | 27 | 30 | -------------------------------------------------------------------------------- /src/components/pages/PageOffline.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 16 | 17 | 21 | -------------------------------------------------------------------------------- /src/locales/date-time-formats.js: -------------------------------------------------------------------------------- 1 | 2 | // NOTE: browser will handle the final part of localisation, e.g. month names etc. 3 | // See details: http://www.ecma-international.org/ecma-402/2.0/#sec-intl-datetimeformat-constructor 4 | export default { 5 | 6 | 'en': { 7 | short: { 8 | year: 'numeric', 9 | month: 'short', 10 | day: 'numeric' 11 | }, 12 | long: { 13 | year: 'numeric', 14 | month: 'short', 15 | day: 'numeric', 16 | weekday: 'short', 17 | hour: 'numeric', 18 | minute: 'numeric' 19 | } 20 | } 21 | 22 | }; 23 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // https: //code.visualstudio.com/docs/editor/tasks 3 | "version": "0.1.0", 4 | "tasks": [ 5 | 6 | // // Show all problems in the project 7 | // { 8 | // "taskName": "Show all problems in project", 9 | // "command": "eslint", 10 | // "isShellCommand": true, 11 | // "args": [ 12 | // "-w", 13 | // "-p", 14 | // ".", 15 | // "--noEmit" 16 | // ], 17 | // "showOutput": "silent", 18 | // "isBackground": true, 19 | // "problemMatcher": "$eslint-stylish" 20 | // } 21 | 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /src/models/Comment.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | 3 | export default Vue.extend({ 4 | 5 | props: { 6 | 7 | id: { 8 | type: Number, 9 | required: true 10 | }, 11 | 12 | postId: { 13 | type: Number, 14 | required: true 15 | }, 16 | 17 | name: { 18 | type: String 19 | }, 20 | 21 | email: { 22 | type: String 23 | }, 24 | 25 | body: { 26 | type: String 27 | } 28 | 29 | }, 30 | 31 | computed: { 32 | 33 | excerpt: function () { 34 | return this.body.substr(0, 100); 35 | } 36 | 37 | } 38 | 39 | }); 40 | -------------------------------------------------------------------------------- /tooling/e2e/nightwatch.globals.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var HtmlReporter = require('nightwatch-html-reporter') 3 | 4 | var reportsDirectory = path.join(__dirname, '../../reports') 5 | 6 | var reporter = new HtmlReporter({ 7 | openBrowser: false, 8 | reportsDirectory: reportsDirectory, 9 | reportFilename: 'e2e.html', 10 | relativeScreenshots: true, 11 | 12 | // https://github.com/jls/nightwatch-html-reporter/tree/master/lib/themes 13 | themeName: 'outlook' 14 | }) 15 | 16 | module.exports = { 17 | reporter: reporter.fn 18 | } 19 | -------------------------------------------------------------------------------- /src/components/containers/Card.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 25 | 26 | 30 | -------------------------------------------------------------------------------- /src/styles/utilities-base/utility-background.scss: -------------------------------------------------------------------------------- 1 | 2 | @import '~@shared-styles'; 3 | 4 | .background-contain { 5 | @include background-contain; 6 | } 7 | 8 | .background-cover { 9 | @include background-cover; 10 | } 11 | 12 | 13 | 14 | .background-fill-height { 15 | @include background-fill-height; 16 | } 17 | 18 | .background-fill-width { 19 | @include background-fill-width; 20 | } 21 | 22 | 23 | 24 | .no-background-color { 25 | @include no-background-color; 26 | } 27 | 28 | .no-background-image { 29 | @include no-background-image; 30 | } 31 | -------------------------------------------------------------------------------- /src/styles/utilities-base/utility-limit.scss: -------------------------------------------------------------------------------- 1 | 2 | @import '~@shared-styles'; 3 | 4 | 5 | 6 | .limit-max { 7 | max-width: 100%; 8 | } 9 | 10 | .limit-tiny { 11 | @include limit-tiny; 12 | } 13 | 14 | .limit-small { 15 | @include limit-small; 16 | } 17 | 18 | .limit-smallish { 19 | @include limit-smallish; 20 | } 21 | 22 | .limit-medium { 23 | @include limit-medium; 24 | } 25 | 26 | .limit-large { 27 | @include limit-large; 28 | } 29 | 30 | .limit-verylarge { 31 | @include limit-verylarge; 32 | } 33 | 34 | .no-limit { 35 | @include no-limit; 36 | } 37 | -------------------------------------------------------------------------------- /src/styles/utilities-base/utility-keep.scss: -------------------------------------------------------------------------------- 1 | 2 | @import '~@shared-styles'; 3 | 4 | 5 | 6 | // Regular horizontal stuff 7 | 8 | .keep-left { 9 | @include keep-left; 10 | } 11 | 12 | .keep-right { 13 | @include keep-right; 14 | } 15 | 16 | .keep-center { 17 | @include keep-center; 18 | } 19 | 20 | 21 | 22 | // Advanced positioning 23 | 24 | .keep-vertical-center { 25 | @include keep-vertical-center; 26 | } 27 | 28 | .keep-horizontal-center { 29 | @include keep-horizontal-center; 30 | } 31 | 32 | .keep-full-center { 33 | @include keep-full-center; 34 | } 35 | -------------------------------------------------------------------------------- /src/styles/mixins/mixin-rhythm.scss: -------------------------------------------------------------------------------- 1 | 2 | @import './mixin-push'; 3 | 4 | 5 | 6 | @mixin no-rhythm { 7 | 8 | object, 9 | iframe, 10 | h1, 11 | h2, 12 | h3, 13 | h4, 14 | h5, 15 | h6, 16 | p, 17 | blockquote, 18 | pre, 19 | abbr, 20 | address, 21 | q, 22 | dl, 23 | dt, 24 | dd, 25 | ol, 26 | ul, 27 | li, 28 | table, 29 | caption, 30 | tbody, 31 | tfoot, 32 | thead, 33 | tr, 34 | th, 35 | td, 36 | article, 37 | aside, 38 | footer, 39 | header, 40 | hgroup, 41 | menu, 42 | nav, 43 | section { 44 | @include no-push-vertical; 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /spec/components/pages/PageHome.spec.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import PageHome from '@vue-components/pages/PageHome'; 3 | 4 | describe('PageHome.vue', function () { 5 | 6 | it('should render correct page title', function () { 7 | 8 | // Set up a new instance of the component 9 | const Constructor = Vue.extend(PageHome); 10 | const vm = new Constructor().$mount(); 11 | 12 | // Expected results 13 | // console.log('PageHome.spec', vm.$el.querySelector('h1')); 14 | expect(vm.$el.querySelector('h1').textContent) 15 | .to.equal('Hello World!'); 16 | 17 | }); 18 | 19 | }); 20 | -------------------------------------------------------------------------------- /src/config/config.styles.js: -------------------------------------------------------------------------------- 1 | import { includes } from 'lodash'; 2 | 3 | // Get SCSS constants 4 | // eslint-disable-next-line import/no-webpack-loader-syntax 5 | import styles from '!sass-to-js-var-loader!@styles/definitions/constants'; 6 | 7 | // Convert some values to numbers 8 | var parsedStyles = {}; 9 | for (var key in styles) { 10 | var potentialUnit = styles[key].substr(-2); 11 | if (includes(['ms', 'px'], potentialUnit)) { 12 | parsedStyles[key] = parseInt(styles[key]); 13 | } else { 14 | parsedStyles[key] = styles[key]; 15 | } 16 | } 17 | 18 | export default parsedStyles; 19 | -------------------------------------------------------------------------------- /src/components/pages/PageLogin.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 29 | 30 | 33 | -------------------------------------------------------------------------------- /src/locales/en.js: -------------------------------------------------------------------------------- 1 | 2 | export default { 3 | 4 | // Titlebar titles 5 | // NOTE: each named route should have a corresponding title here 6 | pageTitles: { 7 | 8 | // Top-level pages 9 | home: 'Home', 10 | posts: 'Posts', 11 | post: '@:pageTitles.posts', 12 | 13 | // Console sub pages 14 | console: 'Console', 15 | consoleComponents: '@:pageTitles.console', 16 | consoleConfiguration: '@:pageTitles.console', 17 | consoleModels: '@:pageTitles.console', 18 | consolePlugins: '@:pageTitles.console', 19 | consoleServices: '@:pageTitles.console', 20 | consoleVuex: '@:pageTitles.console' 21 | 22 | } 23 | 24 | }; 25 | -------------------------------------------------------------------------------- /src/util/linkIsExternal.js: -------------------------------------------------------------------------------- 1 | import eventHasMetaKey from './eventHasMetaKey'; 2 | 3 | // Determine if a link element should be considered external or internal 4 | export default function (element) { 5 | if ( 6 | 7 | // Is link 8 | element.href && 9 | 10 | // Is external 11 | (element.hostname !== location.hostname) && 12 | 13 | // Not using meta key 14 | !eventHasMetaKey(event) && 15 | 16 | // No target specified 17 | (!element.target || element.target === '') && 18 | 19 | // Is an http(s) link 20 | (element.protocol.substr(0, 4) === 'http') 21 | 22 | ) { 23 | return true; 24 | } 25 | 26 | return false; 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/styles/mixins/mixin-keep.scss: -------------------------------------------------------------------------------- 1 | 2 | @mixin keep-left { 3 | clear: none; 4 | float: left; 5 | } 6 | 7 | @mixin keep-right { 8 | clear: none; 9 | float: right; 10 | } 11 | 12 | @mixin keep-center { 13 | margin-left: auto; 14 | margin-right: auto; 15 | } 16 | 17 | @mixin keep-vertical-center { 18 | top: 50%; 19 | transform: translateY(-50%); 20 | } 21 | 22 | @mixin keep-horizontal-center { 23 | left: 50%; 24 | transform: translateX(-50%); 25 | } 26 | 27 | @mixin keep-full-center ($extra-transform-values...) { 28 | position: absolute; 29 | top: 50%; 30 | left: 50%; 31 | transform: translate3d(-50%, -50%, 0) $extra-transform-values; 32 | } 33 | -------------------------------------------------------------------------------- /src/config/index.js: -------------------------------------------------------------------------------- 1 | import { merge } from 'lodash'; 2 | 3 | // Actual config files 4 | // import defaultAliases from './config.aliases'; 5 | import defaultBase from './config.base'; 6 | import defaultStyles from './config.styles'; 7 | 8 | // Dev overrides 9 | import devBase from './config.dev.base'; 10 | 11 | // Merge configs with multiple sources 12 | let mergedBase = defaultBase; 13 | 14 | if (process.env.NODE_ENV === 'development') { 15 | mergedBase = merge(mergedBase, devBase); 16 | } 17 | 18 | mergedBase.styles = defaultStyles; 19 | 20 | // Export base config with aliases, routes and styles added as separate items 21 | export default mergedBase; 22 | -------------------------------------------------------------------------------- /src/services/panels.js: -------------------------------------------------------------------------------- 1 | import { kebabCase } from 'lodash'; 2 | import Vue from 'vue'; 3 | 4 | export default new Vue({ 5 | 6 | data: function () { 7 | return { 8 | component: null 9 | }; 10 | }, 11 | 12 | computed: { 13 | 14 | shouldBeShown: function () { 15 | return this.component ? true : false; 16 | } 17 | 18 | }, 19 | 20 | methods: { 21 | 22 | open: function (component) { 23 | if (component) { 24 | this.component = kebabCase(component); 25 | } 26 | }, 27 | 28 | close: function () { 29 | this.component = null; 30 | } 31 | 32 | }, 33 | 34 | created: function () { 35 | window.b = this; 36 | } 37 | 38 | }); 39 | -------------------------------------------------------------------------------- /src/models/index.js: -------------------------------------------------------------------------------- 1 | // All models 2 | import Account from './Account'; 3 | import Comment from './Comment'; 4 | import Post from './Post'; 5 | import Remote from './Remote'; 6 | import User from './User'; 7 | 8 | // Helpers 9 | // Allow initialising a model without having to know Vue's syntax for passing property values 10 | const init = function (Model, data) { 11 | return new Model({ 12 | propsData: data 13 | }); 14 | }; 15 | 16 | // Exports 17 | 18 | export { 19 | init, 20 | Account, 21 | Comment, 22 | Post, 23 | Remote, 24 | User 25 | }; 26 | 27 | export default { 28 | init, 29 | Account, 30 | Comment, 31 | Post, 32 | Remote, 33 | User 34 | }; 35 | -------------------------------------------------------------------------------- /src/styles/utilities-composed/utility-inverse.scss: -------------------------------------------------------------------------------- 1 | 2 | @import '~@shared-styles'; 3 | 4 | 5 | 6 | // Text context 7 | 8 | // This is used for rich body text. Elements commonly used in building components, like links, are robbed of their sensible defaults. Attach this utility class on a body text container to style the rich text elements in a way that makes sense for natural article content. 9 | 10 | .inverse { 11 | color: $color-dark; 12 | 13 | // Light background buttons 14 | .button, 15 | .button-stroke, 16 | .button-plain { 17 | 18 | // Text color 19 | &:active { 20 | background-color: rgba($color-dark, 0.05); 21 | } 22 | 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/styles/base/base-tables.scss: -------------------------------------------------------------------------------- 1 | 2 | @import '~@shared-styles'; 3 | 4 | table { 5 | 6 | &.pad, 7 | &.separate, 8 | &.separate-bottom { 9 | 10 | td, 11 | th { 12 | @include pad-loose; 13 | } 14 | 15 | } 16 | 17 | &.pad { 18 | padding: 0; 19 | } 20 | 21 | &.separate { 22 | 23 | td, 24 | th { 25 | border-width: 1px; 26 | } 27 | 28 | } 29 | 30 | &.separate-bottom { 31 | 32 | td, 33 | th { 34 | border-bottom-width: 1px; 35 | } 36 | 37 | tr { 38 | &:last-child { 39 | td, 40 | th { 41 | border-bottom-width: 0; 42 | } 43 | } 44 | } 45 | 46 | } 47 | 48 | } 49 | 50 | th, 51 | td { 52 | padding: 0; 53 | } 54 | -------------------------------------------------------------------------------- /tooling/unit/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | Vue.config.productionTip = false 4 | 5 | // require all test files (files that ends with .spec.js) 6 | // const testsContext = require.context('@spec-unit', true, /\.spec$/) 7 | const testsContext = require.context('@spec', true, /^(?!e2e).*\.spec/gm) 8 | testsContext.keys().forEach(testsContext) 9 | 10 | // require all src files except main.js for coverage. 11 | // you can also change this to match only the subset of files that 12 | // you want coverage for. 13 | 14 | // /^\.\/(?!main(\.js)?$)(?!.*\.scss$)/ 15 | const srcContext = require.context('../../src', true, /^\.\/(?!main(\.js)?$)/) 16 | srcContext.keys().forEach(srcContext) 17 | -------------------------------------------------------------------------------- /spec/e2e/appContentVisibleAtStart.spec.js: -------------------------------------------------------------------------------- 1 | // For authoring Nightwatch tests, see 2 | // http://nightwatchjs.org/guide#usage 3 | 4 | module.exports = { 5 | 6 | 'App wrapper should be visible at start': function (browser) { 7 | 8 | // automatically uses dev Server port from `/tooling/env/index.js` 9 | // default: http://localhost:8080 10 | // see nightwatch.conf.js 11 | const devServer = browser.globals.devServerURL; 12 | 13 | browser 14 | .url(devServer) 15 | .waitForElementVisible('.view-app', 5000) 16 | .assert.elementPresent('.view-app-content') 17 | // .assert.elementPresent('.view-page-log-in-actions .view-click-button') 18 | .end(); 19 | 20 | } 21 | 22 | }; 23 | -------------------------------------------------------------------------------- /src/styles/transitions/transition-fade.scss: -------------------------------------------------------------------------------- 1 | 2 | @import '~@shared-styles'; 3 | 4 | 5 | 6 | // Transition code 7 | // https://vuejs.org/v2/guide/transitions.html 8 | // https://vuejs.org/images/transition.png 9 | 10 | // While either transition is playing 11 | // FIXME: would be better to not specify the opacity value 1, but let the default or component's defined value stand 12 | .transition-fade-enter-active, 13 | .transition-fade-leave-active { 14 | opacity: 1; 15 | @include transition-fast; 16 | @include transition-properties(opacity); 17 | } 18 | 19 | // Start state for enter 20 | .transition-fade-enter, 21 | 22 | // End state for exit 23 | .transition-fade-leave-to { 24 | opacity: 0; 25 | } 26 | -------------------------------------------------------------------------------- /src/services/index.js: -------------------------------------------------------------------------------- 1 | 2 | import api from './api'; 3 | import auth from './auth'; 4 | import env from './env'; 5 | import menu from './menu'; 6 | import network from './network'; 7 | import notifications from './notifications'; 8 | import panels from './panels'; 9 | import popovers from './popovers'; 10 | import time from './time'; 11 | import viewport from './viewport'; 12 | 13 | export { 14 | api, 15 | auth, 16 | env, 17 | menu, 18 | network, 19 | notifications, 20 | panels, 21 | popovers, 22 | time, 23 | viewport 24 | }; 25 | 26 | export default { 27 | api, 28 | auth, 29 | env, 30 | menu, 31 | network, 32 | notifications, 33 | panels, 34 | popovers, 35 | time, 36 | viewport 37 | }; 38 | -------------------------------------------------------------------------------- /src/styles/utilities-base/utility-box-model.scss: -------------------------------------------------------------------------------- 1 | 2 | @import '~@shared-styles'; 3 | 4 | 5 | 6 | .content-box { 7 | @include content-box; 8 | } 9 | 10 | .border-box { 11 | @include border-box; 12 | } 13 | 14 | 15 | 16 | .border-collapse { 17 | @include border-collapse; 18 | } 19 | 20 | .no-border-collapse { 21 | @include no-border-collapse; 22 | } 23 | 24 | 25 | 26 | .background-clip-content-box { 27 | @include background-clip-content-box; 28 | } 29 | 30 | .background-clip-border-box { 31 | @include background-clip-border-box; 32 | } 33 | 34 | .background-clip-padding-box { 35 | @include background-clip-padding-box; 36 | } 37 | 38 | .background-clip-text { 39 | @include background-clip-text; 40 | } 41 | -------------------------------------------------------------------------------- /src/components/console/ConsoleConfiguration.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 29 | 30 | 34 | -------------------------------------------------------------------------------- /src/components/popovers/PopoverCounter.vue: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 27 | 28 | 36 | -------------------------------------------------------------------------------- /src/svg/check.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | svg/check 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/components/sections/FooterSummary.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 30 | 31 | 40 | -------------------------------------------------------------------------------- /src/services/api/apiAuth.js: -------------------------------------------------------------------------------- 1 | // NOTE: this only provides mock API functionality, as you can see below 2 | 3 | import { Account } from '@models'; 4 | 5 | export default { 6 | 7 | methods: { 8 | 9 | transformAuth: function (user) { 10 | return { 11 | expires: null, 12 | account: new Account({ 13 | propsData: { 14 | id: user.id, 15 | email: user.email, 16 | name: user.name, 17 | role: 'loggedUser' 18 | } 19 | }) 20 | }; 21 | }, 22 | 23 | getAuth: function (onSuccess, onFail, onEither) { 24 | return this.get( 25 | 'users/1', 26 | {}, 27 | onSuccess, 28 | onFail, 29 | onEither, 30 | this.transformAuth 31 | ); 32 | } 33 | 34 | } 35 | 36 | }; 37 | -------------------------------------------------------------------------------- /src/components/containers/Panel.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 16 | 17 | 43 | -------------------------------------------------------------------------------- /src/svg/chevron-up.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | svg/chevron-up 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/svg/chevron-down.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | svg/chevron-down 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/svg/chevron-left.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | svg/chevron-left 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/svg/chevron-right.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | svg/chevron-right 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/styles/keyframes/keyframes-pulse.scss: -------------------------------------------------------------------------------- 1 | 2 | @keyframes pulse { 3 | 4 | 0%, 5 | 100% { 6 | opacity: 1; 7 | } 8 | 9 | 50% { 10 | opacity: 0; 11 | } 12 | 13 | } 14 | 15 | @keyframes pulse-delayed { 16 | 17 | 0%, 18 | 40%, 19 | 100% { 20 | opacity: 1; 21 | } 22 | 23 | 20% { 24 | opacity: 0; 25 | } 26 | 27 | } 28 | 29 | @keyframes pulse-scale { 30 | 31 | 0%, 32 | 100% { 33 | opacity: 1; 34 | transform: scale(1); 35 | } 36 | 37 | 50% { 38 | opacity: 0; 39 | transform: scale(0); 40 | } 41 | 42 | } 43 | 44 | @keyframes pulse-scale-delayed { 45 | 46 | 0%, 47 | 40%, 48 | 100% { 49 | opacity: 1; 50 | transform: scale(1); 51 | } 52 | 53 | 20% { 54 | opacity: 0; 55 | transform: scale(0); 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | // http://eslint.org/docs/user-guide/configuring 2 | 3 | module.exports = { 4 | root: true, 5 | parser: 'babel-eslint', 6 | parserOptions: { 7 | sourceType: 'module' 8 | }, 9 | env: { 10 | browser: true, 11 | }, 12 | // https://github.com/feross/standard/blob/master/RULES.md#javascript-standard-style 13 | extends: 'standard', 14 | // required to lint *.vue files 15 | plugins: [ 16 | 'html' 17 | ], 18 | // add your custom rules here 19 | 'rules': { 20 | // allow paren-less arrow functions 21 | 'arrow-parens': 0, 22 | // allow async-await 23 | 'generator-star-spacing': 0, 24 | // allow debugger during development 25 | 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/svg/cross.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | svg/cross 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/svg/hamburger.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | svg/hamburger 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/styles/mixins/mixin-shadow.scss: -------------------------------------------------------------------------------- 1 | 2 | @mixin shadow ($opacity: 0.15, $color: $color-darkgrey) { 3 | box-shadow: 0 2px 8px rgba($color, $opacity); 4 | } 5 | 6 | @mixin shadow-transparent ($color: $color-darkgrey) { 7 | @include shadow(0, $color); 8 | } 9 | 10 | 11 | 12 | @mixin shadow-tight ($opacity: 0.15, $color: $color-darkgrey) { 13 | box-shadow: 0 1px 2px rgba($color, $opacity); 14 | } 15 | 16 | @mixin shadow-tight-transparent ($color: $color-darkgrey) { 17 | @include shadow-tight(0, $color); 18 | } 19 | 20 | 21 | 22 | @mixin shadow-loose ($opacity: 0.15, $color: $color-darkgrey) { 23 | box-shadow: 0 2px 38px rgba($color, $opacity); 24 | } 25 | 26 | @mixin shadow-loose-transparent ($color: $color-darkgrey) { 27 | @include shadow-loose(0, $color); 28 | } 29 | -------------------------------------------------------------------------------- /src/services/notifications.js: -------------------------------------------------------------------------------- 1 | 2 | import { debounce } from 'lodash'; 3 | import Vue from 'vue'; 4 | 5 | const delay = 6 * 1000; 6 | 7 | export default new Vue({ 8 | 9 | data: function () { 10 | return { 11 | message: null 12 | }; 13 | }, 14 | 15 | computed: { 16 | 17 | shouldBeShown: function () { 18 | return this.message ? true : false; 19 | } 20 | 21 | }, 22 | 23 | methods: { 24 | 25 | show: function (message) { 26 | if (message) { 27 | this.message = message; 28 | } 29 | }, 30 | 31 | close: function () { 32 | if (this.message !== null) { 33 | this.message = null; 34 | } 35 | } 36 | 37 | }, 38 | 39 | watch: { 40 | 41 | message: debounce(function () { 42 | this.close(); 43 | }, delay) 44 | 45 | } 46 | 47 | }); 48 | -------------------------------------------------------------------------------- /src/services/api/apiPosts.js: -------------------------------------------------------------------------------- 1 | import { Post } from '@models'; 2 | 3 | export default { 4 | 5 | // computed: {}, 6 | 7 | methods: { 8 | 9 | // Return a new model instance instead of the raw data 10 | transformPost: function (postData) { 11 | return new Post({ 12 | propsData: postData 13 | }); 14 | }, 15 | 16 | getPosts: function (onSuccess, onFail, onEither) { 17 | return this.get( 18 | 'posts', 19 | {}, 20 | onSuccess, 21 | onFail, 22 | onEither, 23 | this.transformPost 24 | ); 25 | }, 26 | 27 | getPost: function (id, onSuccess, onFail, onEither) { 28 | return this.get( 29 | 'posts/' + id, 30 | {}, 31 | onSuccess, 32 | onFail, 33 | onEither, 34 | this.transformPost 35 | ); 36 | } 37 | 38 | } 39 | 40 | }; 41 | -------------------------------------------------------------------------------- /src/services/api/apiUsers.js: -------------------------------------------------------------------------------- 1 | import { User } from '@models'; 2 | 3 | export default { 4 | 5 | // computed: {}, 6 | 7 | methods: { 8 | 9 | // Return a new model instance instead of the raw data 10 | transformUser: function (userData) { 11 | return new User({ 12 | propsData: userData 13 | }); 14 | }, 15 | 16 | getUsers: function (onSuccess, onFail, onEither) { 17 | return this.get( 18 | 'users', 19 | {}, 20 | onSuccess, 21 | onFail, 22 | onEither, 23 | this.transformUser 24 | ); 25 | }, 26 | 27 | getUser: function (id, onSuccess, onFail, onEither) { 28 | return this.get( 29 | 'users/' + id, 30 | {}, 31 | onSuccess, 32 | onFail, 33 | onEither, 34 | this.transformUser 35 | ); 36 | } 37 | 38 | } 39 | 40 | }; 41 | -------------------------------------------------------------------------------- /src/components/pages/PageNotFound.vue: -------------------------------------------------------------------------------- 1 | 2 | 22 | 23 | 32 | 33 | 40 | -------------------------------------------------------------------------------- /src/svg/chevron-left-left.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | svg/chevron-left-left 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/svg/chevron-right-right.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | svg/chevron-right-right 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /tooling/e2e/nightwatch.chrome.conf.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash') 2 | var path = require('path') 3 | require('babel-register') 4 | 5 | var defaults = require('./nightwatch.conf.js') 6 | 7 | module.exports = _.merge(defaults, { 8 | globals_path: path.join(__dirname, './nightwatch.chrome.globals.js'), 9 | 10 | selenium: { 11 | start_process: false 12 | }, 13 | 14 | test_settings: { 15 | default: { 16 | selenium_port: 9515, 17 | default_path_prefix: '', 18 | 19 | desiredCapabilities: { 20 | browserName: 'chrome', 21 | acceptSslCerts: true, 22 | 23 | // https://sites.google.com/a/chromium.org/chromedriver/capabilities#TOC-chromeOptions-object 24 | chromeOptions: { 25 | args: ['--no-sandbox'] 26 | } 27 | 28 | } 29 | 30 | } 31 | } 32 | 33 | }) 34 | -------------------------------------------------------------------------------- /src/styles/transitions/transition-flip-vertical.scss: -------------------------------------------------------------------------------- 1 | 2 | @import '~@shared-styles'; 3 | 4 | // Shared, defaults 5 | .transition-flip-vertical-enter-active, 6 | .transition-flip-vertical-leave-active { 7 | transition-property: transform; 8 | } 9 | 10 | .transition-flip-vertical-enter-active { 11 | @include transition-veryfast; 12 | } 13 | 14 | .transition-flip-vertical-leave-active { 15 | @include transition-veryfast; 16 | } 17 | 18 | // Default state to transition a) to when entering and b) from when leaving 19 | .transition-flip-vertical-enter-active, 20 | .transition-flip-vertical-leave-active { 21 | transform: rotateX(0deg); 22 | } 23 | 24 | // Start state when entering 25 | .transition-flip-vertical-enter { 26 | transform: rotateX(-90deg); 27 | } 28 | 29 | // Out 30 | .transition-flip-vertical-leave-to { 31 | transform: rotateX(90deg); 32 | } 33 | -------------------------------------------------------------------------------- /src/styles/utilities-composed/utility-control.scss: -------------------------------------------------------------------------------- 1 | 2 | @import '~@shared-styles'; 3 | 4 | .control, 5 | .control-area, 6 | .control-scale { 7 | @include transition-hover-active; 8 | @include transition-properties-common; 9 | } 10 | 11 | // Default feedback for hit areas 12 | // NOTE: might be better not to set anything here since this might need a lot of overriding in many places 13 | .control-enabled { 14 | @include cursor-pointer; 15 | 16 | &.control-area { 17 | &:active { 18 | background-color: $color-feedback-dark; 19 | } 20 | } 21 | 22 | &.control-scale { 23 | &:active { 24 | transform: scale($scale-small); 25 | } 26 | } 27 | 28 | } 29 | 30 | // .control-disabled {} 31 | 32 | // Prevent user select when double clicking or swiping etc. 33 | .control-enabled { 34 | &.control-mouse-down { 35 | user-select: none; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/vue/plugins/vue-i18n.js: -------------------------------------------------------------------------------- 1 | 2 | // vue-i18n: Internationalization for Vue 3 | // http://kazupon.github.io/vue-i18n/en/installation.html 4 | import Vue from 'vue'; 5 | import VueI18n from 'vue-i18n'; 6 | 7 | // Import route components for vue-router 8 | import config from '@config'; 9 | import locales from '@locales'; 10 | 11 | 12 | 13 | // Autoload plugin 14 | Vue.use(VueI18n); 15 | 16 | // Provide options for the plugin constructor (see docs for details) 17 | const options = { 18 | 19 | // Set default locale 20 | locale: config.defaultLocale, 21 | fallbackLocale: config.fallbackLocale, 22 | 23 | // Inject copy text, formats etc. 24 | messages: locales.messages, 25 | dateTimeFormats: locales.dateTimeFormats, 26 | numberFormats: locales.numberFormats 27 | 28 | }; 29 | 30 | // Export a new plugin instance 31 | export default new VueI18n(options); 32 | -------------------------------------------------------------------------------- /src/styles/transitions/transition-hide.scss: -------------------------------------------------------------------------------- 1 | 2 | @import '~@shared-styles'; 3 | 4 | .transition-hide-up-enter-active, 5 | .transition-hide-up-leave-active, 6 | .transition-hide-right-enter-active, 7 | .transition-hide-right-leave-active { 8 | @include transition-slow; 9 | @include transition-properties(transform); 10 | @include no-translate; 11 | } 12 | 13 | // Open by moving down, close by moving up 14 | 15 | // .transition-hide-up-enter-active, 16 | // .transition-hide-up-leave-active {} 17 | 18 | .transition-hide-up-enter, 19 | .transition-hide-up-leave-to { 20 | @include translate-up; 21 | } 22 | 23 | 24 | 25 | // Open from right to left, close to the right 26 | 27 | // .transition-hide-right-enter-active, 28 | // .transition-hide-right-leave-active {} 29 | 30 | .transition-hide-right-enter, 31 | .transition-hide-right-leave-to { 32 | @include translate-right; 33 | } 34 | -------------------------------------------------------------------------------- /src/components/models/PostListItem.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 35 | 36 | 50 | -------------------------------------------------------------------------------- /src/svg/arrow-left.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | svg/arrow-left 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/styles/utilities-base/utility-separate.scss: -------------------------------------------------------------------------------- 1 | 2 | @import '~@shared-styles'; 3 | 4 | 5 | 6 | // Single 7 | 8 | .separate { 9 | border-width: $separator; 10 | } 11 | 12 | .separate-top { 13 | border-top-width: $separator; 14 | } 15 | 16 | .separate-right { 17 | border-right-width: $separator; 18 | } 19 | 20 | .separate-bottom { 21 | border-bottom-width: $separator; 22 | } 23 | 24 | .separate-left { 25 | border-left-width: $separator; 26 | } 27 | 28 | 29 | 30 | // Double 31 | 32 | .separate-double { 33 | border-width: $separator-double; 34 | } 35 | 36 | .separate-double-top { 37 | border-top-width: $separator-double; 38 | } 39 | 40 | .separate-double-right { 41 | border-right-width: $separator-double; 42 | } 43 | 44 | .separate-double-bottom { 45 | border-bottom-width: $separator-double; 46 | } 47 | 48 | .separate-double-left { 49 | border-left-width: $separator-double; 50 | } 51 | -------------------------------------------------------------------------------- /src/styles/mixins/mixin-container.scss: -------------------------------------------------------------------------------- 1 | 2 | @import './mixin-buffer'; 3 | @import './mixin-radius'; 4 | @import './mixin-shadow'; 5 | 6 | @mixin container-white { 7 | @include radius-loose; 8 | @include background-clip-padding-box; 9 | overflow: auto; 10 | border-width: 1px; 11 | border-color: $color-dark-translucent; 12 | background-color: $color-white; 13 | } 14 | 15 | @mixin container-white-buffer { 16 | @include container-white; 17 | @include buffer; 18 | } 19 | 20 | @mixin container-dark { 21 | @include radius; 22 | overflow: auto; 23 | background-color: color-translucent($color-dark, 0.05); 24 | } 25 | 26 | @mixin container-dark-buffer { 27 | @include container-dark; 28 | @include buffer; 29 | } 30 | 31 | @mixin container-card { 32 | @include container-white; 33 | @include shadow; 34 | } 35 | 36 | @mixin container-card-buffer { 37 | @include container-card; 38 | @include buffer; 39 | } 40 | -------------------------------------------------------------------------------- /src/components/controls/IconButton.vue: -------------------------------------------------------------------------------- 1 | 2 | 18 | 19 | 31 | 32 | 51 | -------------------------------------------------------------------------------- /tooling/e2e/custom-assertions/elementCount.js: -------------------------------------------------------------------------------- 1 | // A custom Nightwatch assertion. 2 | // the name of the method is the filename. 3 | // can be used in tests like this: 4 | // 5 | // browser.assert.elementCount(selector, count) 6 | // 7 | // for how to write custom assertions see 8 | // http://nightwatchjs.org/guide#writing-custom-assertions 9 | exports.assertion = function (selector, count) { 10 | this.message = 'Testing if element <' + selector + '> has count: ' + count 11 | this.expected = count 12 | this.pass = function (val) { 13 | return val === this.expected 14 | } 15 | this.value = function (res) { 16 | return res.value 17 | } 18 | this.command = function (cb) { 19 | var self = this 20 | return this.api.execute(function (selector) { 21 | return document.querySelectorAll(selector).length 22 | }, [selector], function (res) { 23 | cb.call(self, res) 24 | }) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/components/console/ConsoleModels.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 21 | 22 | 41 | 42 | 43 | 44 | 47 | -------------------------------------------------------------------------------- /src/styles/mixins/mixin-font.scss: -------------------------------------------------------------------------------- 1 | 2 | @mixin font-face ($font-family, $font-weight, $font-style, $filepath, $woffdata: null, $woff2data: null) { 3 | 4 | @font-face { 5 | font-family: $font-family; 6 | font-style: $font-style; 7 | font-weight: $font-weight; 8 | src: url('#{$filepath}.eot'); 9 | } 10 | 11 | @font-face { 12 | font-family: $font-family; 13 | font-style: $font-style; 14 | font-weight: $font-weight; 15 | 16 | @if $woffdata { 17 | src: 18 | url('data:application/font-woff;charset=utf-8;base64,#{$woffdata}') format('woff'), 19 | url('data:application/font-woff2;charset=utf-8;base64,#{$woff2data}') format('woff2'), 20 | url('#{$filepath}.ttf') format('truetype'); 21 | } 22 | 23 | @else { 24 | src: 25 | url('#{$filepath}.woff') format('woff'), 26 | url('#{$filepath}.woff2') format('woff2'), 27 | url('#{$filepath}.ttf') format('truetype'); 28 | } 29 | 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/util/processLargeArray.js: -------------------------------------------------------------------------------- 1 | // Process large array in chunks so that UI isn't blocked for too long at once 2 | export default function (array, processItemCallback, finalCallback) { 3 | let chunk = 250; 4 | let i = 0; 5 | 6 | let doChunk = function (processItemCallback) { 7 | let count = chunk; 8 | 9 | // Process each item in this chunk 10 | while (count-- && i < array.length) { 11 | 12 | // Process array[i] here 13 | // NOTE: processItemCallback should not move, delete or add array items, but updating them is OK 14 | processItemCallback(array[i], i, array); 15 | 16 | // Iterate counter 17 | ++i; 18 | } 19 | 20 | // Continue loop in next chunk after allowing other tasks to go on 21 | if (i < array.length) { 22 | setTimeout(function () { 23 | doChunk(processItemCallback); 24 | }, 2); 25 | } else { 26 | finalCallback(); 27 | } 28 | 29 | }; 30 | 31 | doChunk(processItemCallback); 32 | } 33 | -------------------------------------------------------------------------------- /src/styles/mixins/mixin-background.scss: -------------------------------------------------------------------------------- 1 | 2 | @mixin background-contain { 3 | background-position: 50% 50%; 4 | background-size: contain; 5 | background-repeat: no-repeat; 6 | } 7 | 8 | @mixin background-cover { 9 | background-position: 50% 50%; 10 | background-size: cover; 11 | background-repeat: no-repeat; 12 | } 13 | 14 | 15 | 16 | @mixin background-fill-height { 17 | background-position: 0 50%; 18 | background-size: auto 100%; 19 | background-repeat: repeat-x; 20 | } 21 | 22 | @mixin background-fill-width { 23 | background-position: 50% 0; 24 | background-size: 100% auto; 25 | background-repeat: repeat-y; 26 | } 27 | 28 | 29 | 30 | // NOTE 31 | // - @mixin no-background is intentionally missing 32 | // - please use either `-color` or `-image` or both explicitly 33 | 34 | @mixin no-background-color { 35 | background-color: transparent; 36 | } 37 | 38 | @mixin no-background-image { 39 | background-image: none; 40 | } 41 | -------------------------------------------------------------------------------- /src/styles/utilities-base/utility-fill.scss: -------------------------------------------------------------------------------- 1 | 2 | @import '~@shared-styles'; 3 | 4 | 5 | 6 | .fill, 7 | .fill-relative, 8 | .fill-fixed, 9 | .fill-width, 10 | .fill-width-relative, 11 | .fill-width-fixed, 12 | .fill-height, 13 | .fill-height-relative, 14 | .fill-height-fixed { 15 | box-sizing: border-box; 16 | } 17 | 18 | .fill, 19 | .fill-relative, 20 | .fill-fixed, 21 | .fill-width, 22 | .fill-width-relative, 23 | .fill-width-fixed { 24 | left: 0; 25 | width: 100%; 26 | } 27 | 28 | .fill, 29 | .fill-relative, 30 | .fill-fixed, 31 | .fill-height, 32 | .fill-height-relative, 33 | .fill-height-fixed { 34 | top: 0; 35 | height: 100%; 36 | } 37 | 38 | .fill, 39 | .fill-width, 40 | .fill-height { 41 | position: absolute; 42 | } 43 | 44 | .fill-relative, 45 | .fill-width-relative, 46 | .fill-height-relative { 47 | position: relative; 48 | } 49 | 50 | .fill-fixed, 51 | .fill-width-fixed, 52 | .fill-height-fixed { 53 | position: fixed; 54 | } 55 | -------------------------------------------------------------------------------- /src/components/snippets/Pic.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 39 | 40 | 48 | -------------------------------------------------------------------------------- /src/styles/mixins/mixin-box-model.scss: -------------------------------------------------------------------------------- 1 | 2 | // Box sizing 3 | 4 | @mixin box-sizing ($value) { 5 | box-sizing: $value; 6 | } 7 | 8 | @mixin content-box { 9 | @include box-sizing(content-box); 10 | } 11 | 12 | @mixin border-box { 13 | @include box-sizing(border-box); 14 | } 15 | 16 | 17 | 18 | // Border-collapse 19 | 20 | @mixin border-collapse { 21 | border-collapse: collapse; 22 | } 23 | 24 | @mixin no-border-collapse { 25 | border-collapse: separate; 26 | } 27 | 28 | 29 | 30 | // Background-clip 31 | 32 | @mixin background-clip ($value) { 33 | background-clip: $value; 34 | } 35 | 36 | @mixin background-clip-content-box { 37 | @include background-clip(content-box); 38 | } 39 | 40 | @mixin background-clip-border-box { 41 | @include background-clip(border-box); 42 | } 43 | 44 | @mixin background-clip-padding-box { 45 | @include background-clip(padding-box); 46 | } 47 | 48 | @mixin background-clip-text { 49 | @include background-clip(text); 50 | } 51 | -------------------------------------------------------------------------------- /tooling/webpack.test.conf.js: -------------------------------------------------------------------------------- 1 | // This is the webpack config used for unit tests. 2 | 3 | var utils = require('./utils') 4 | var webpack = require('webpack') 5 | var merge = require('webpack-merge') 6 | var baseConfig = require('./webpack.base.conf') 7 | 8 | var webpackConfig = merge(baseConfig, { 9 | // use inline sourcemap for karma-sourcemap-loader 10 | module: { 11 | rules: utils.styleLoaders() 12 | }, 13 | devtool: '#inline-source-map', 14 | resolveLoader: { 15 | alias: { 16 | // necessary to to make lang="scss" work in test when using vue-loader's inject option 17 | // see discussion at https://github.com/vuejs/vue-loader/issues/724 18 | 'scss-loader': 'sass-loader' 19 | } 20 | }, 21 | plugins: [ 22 | new webpack.DefinePlugin({ 23 | 'process.env': require('./env/test.env') 24 | }) 25 | ] 26 | }) 27 | 28 | // no need for app entry during tests 29 | delete webpackConfig.entry 30 | 31 | module.exports = webpackConfig 32 | -------------------------------------------------------------------------------- /src/svg/arrow-right.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | svg/arrow-right 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/components/models/PostList.vue: -------------------------------------------------------------------------------- 1 | 2 | 24 | 25 | 44 | 45 | 59 | -------------------------------------------------------------------------------- /src/components/panels/PanelReadme.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 45 | 46 | 50 | -------------------------------------------------------------------------------- /src/models/Remote.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | 3 | const pathValidator = function (path) { 4 | return typeof path === 'string'; 5 | }; 6 | 7 | export default Vue.extend({ 8 | 9 | props: { 10 | 11 | path: { 12 | type: String, 13 | required: true, 14 | validator: pathValidator 15 | }, 16 | 17 | api: { 18 | type: String, 19 | required: true, 20 | validator: pathValidator 21 | }, 22 | 23 | login: { 24 | type: String, 25 | required: true, 26 | validator: pathValidator 27 | }, 28 | 29 | logout: { 30 | type: String, 31 | required: true, 32 | validator: pathValidator 33 | } 34 | 35 | }, 36 | 37 | computed: { 38 | 39 | basePath: function () { 40 | return this.path; 41 | }, 42 | 43 | apiPath: function () { 44 | return this.basePath + this.api; 45 | }, 46 | 47 | loginPath: function () { 48 | return this.basePath + this.login; 49 | }, 50 | 51 | logoutPath: function () { 52 | return this.basePath + this.logout; 53 | } 54 | 55 | } 56 | 57 | }); 58 | -------------------------------------------------------------------------------- /src/services/api/apiComments.js: -------------------------------------------------------------------------------- 1 | import { Comment } from '@models'; 2 | 3 | export default { 4 | 5 | // computed: {}, 6 | 7 | methods: { 8 | 9 | transformComment: function (postData) { 10 | return new Comment({ 11 | propsData: postData 12 | }); 13 | }, 14 | 15 | getComments: function (onSuccess, onFail, onEither) { 16 | return this.get( 17 | 'comments', 18 | {}, 19 | onSuccess, 20 | onFail, 21 | onEither, 22 | this.transformComment 23 | ); 24 | }, 25 | 26 | getCommentsForPost: function (postId, onSuccess, onFail, onEither) { 27 | return this.get( 28 | 'comments', 29 | { 30 | postId: postId 31 | }, 32 | onSuccess, 33 | onFail, 34 | onEither, 35 | this.transformComment 36 | ); 37 | }, 38 | 39 | getComment: function (id, onSuccess, onFail, onEither) { 40 | return this.get( 41 | 'comments/' + id, 42 | {}, 43 | onSuccess, 44 | onFail, 45 | onEither, 46 | this.transformComment 47 | ); 48 | } 49 | 50 | } 51 | 52 | }; 53 | -------------------------------------------------------------------------------- /spec/store/mutations/incrementCounter.spec.js: -------------------------------------------------------------------------------- 1 | import { mutations } from '@store'; 2 | 3 | describe('mutations INCREMENT_COUNTER', function () { 4 | 5 | it('iterates from 0', function () { 6 | 7 | const mockState = { 8 | counter: 0 9 | }; 10 | 11 | // Mutations take the state as argument 12 | mutations['INCREMENT_COUNTER'](mockState); 13 | 14 | // Expected result 15 | expect(mockState.counter).to.equal(1); 16 | 17 | }); 18 | 19 | it('iterates from 1', function () { 20 | const mockState = { counter: 1 }; 21 | mutations['INCREMENT_COUNTER'](mockState); 22 | expect(mockState.counter).to.equal(2); 23 | }); 24 | 25 | it('iterates from -1', function () { 26 | const mockState = { counter: -1 }; 27 | mutations['INCREMENT_COUNTER'](mockState); 28 | expect(mockState.counter).to.equal(0); 29 | }); 30 | 31 | it('iterates from 999999', function () { 32 | const mockState = { counter: 999999 }; 33 | mutations['INCREMENT_COUNTER'](mockState); 34 | expect(mockState.counter).to.equal(1000000); 35 | }); 36 | 37 | }); 38 | -------------------------------------------------------------------------------- /src/styles/mixins/mixin-row.scss: -------------------------------------------------------------------------------- 1 | 2 | @import './mixin-buffer'; 3 | @import './mixin-clear'; 4 | @import './mixin-keep'; 5 | @import './mixin-viewport'; 6 | 7 | 8 | 9 | // NOTE: 10 | // we haven't finalized the actual row-column system yet 11 | // this is temporarily empty, but a row is a valid level of abstraction 12 | // hence we want to allow other elements using this 13 | 14 | // stylelint-disable block-no-empty 15 | @mixin row { 16 | // @include clear-after; 17 | } 18 | // stylelint-enable block-no-empty 19 | 20 | @mixin row-content { 21 | 22 | // Base setup 23 | position: relative; 24 | @include keep-center; 25 | @include clear-after; 26 | 27 | // Width limits 28 | // @include limit-small; 29 | // @include limit-smallish; 30 | // @include limit-medium; 31 | 32 | // Basic dimensions 33 | @include buffer-even; 34 | @include viewport-over-tiny { 35 | @include buffer-loose-even; 36 | } 37 | // @include viewport-over-medium { 38 | // @include buffer-relative; 39 | // } 40 | // @include viewport-over-large {} 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/styles/base/base-rhythm.scss: -------------------------------------------------------------------------------- 1 | 2 | @import '~@shared-styles'; 3 | 4 | 5 | 6 | html { 7 | line-height: $line-height; 8 | } 9 | 10 | hr, 11 | p, 12 | ul, 13 | ol, 14 | table, 15 | pre, 16 | blockquote { 17 | @include push-vertical; 18 | } 19 | 20 | // Headings 21 | h1, 22 | h2, 23 | h3, 24 | h4, 25 | h5, 26 | h6 { 27 | @include push-tight-bottom; 28 | } 29 | 30 | // Normal top margin for this 31 | h1 { 32 | @include push-top; 33 | } 34 | 35 | // More top margin for these 36 | h2, 37 | h3, 38 | h4, 39 | h5, 40 | h6 { 41 | @include push-top-even; 42 | } 43 | 44 | // Tables 45 | caption { 46 | @include push-tight-bottom; 47 | } 48 | 49 | // Definition lists 50 | dl { 51 | @include push-bottom; 52 | 53 | // Utilities 54 | &.no-push, 55 | &.no-push-vertical, 56 | &.no-push-top, 57 | &.inline, 58 | &.inline-block { 59 | dt { 60 | @include no-push-top; 61 | } 62 | } 63 | 64 | } 65 | 66 | dt { 67 | @include push-top; 68 | @include no-push-bottom; 69 | } 70 | 71 | dd { 72 | margin-left: 0; 73 | @include no-push-vertical; 74 | } 75 | -------------------------------------------------------------------------------- /src/styles/mixins/mixin-animation.scss: -------------------------------------------------------------------------------- 1 | 2 | @mixin animation ($name, $duration: $transition-veryveryslow, $easing: null, $count: infinite, $direction: normal) { 3 | animation-name: $name; 4 | animation-duration: $duration; 5 | animation-iteration-count: $count; 6 | animation-direction: $direction; 7 | 8 | // Use explicitly defined easing 9 | @if $easing { 10 | animation-timing-function: $easing; 11 | } 12 | 13 | // Default to linear for infinite animations 14 | @else if $count == infinite { 15 | animation-timing-function: linear; 16 | } 17 | 18 | } 19 | 20 | @mixin animation-alternate ($name, $duration: $transition-veryveryslow, $easing: null, $count: infinite) { 21 | @include animation($name, $duration, $easing, $count, alternate); 22 | } 23 | 24 | @mixin animation-once ($name, $duration: $transition-veryveryslow, $easing: null) { 25 | @include animation($name, $duration, $easing, 1); 26 | } 27 | 28 | @mixin animation-pulse ($count: infinite) { 29 | @include animation(pulse, $transition-slow, $easing-smooth, $count, alternate); 30 | } 31 | -------------------------------------------------------------------------------- /src/vue/plugins/index.js: -------------------------------------------------------------------------------- 1 | 2 | // Vue plugins 3 | // https://vuejs.org/v2/guide/plugins.html 4 | // https://eiskis.gitbooks.io/bellevue/app/vue.html 5 | 6 | // NOTE: 7 | // - These are integrated libraries that generally deliver new functionality to Vue components (or other objects) 8 | // - Generally they inject new functionality (such as this.$route) or read new values from the view model definition (such as metaInfo) 9 | // - When adding plugins, you need to look up the documentation for each to understand the correct way of loading them 10 | // - Each plugin is loaded via a separate file. You can import configuration from '@config' in each file. 11 | // - See documentation for more info on how to load various plugins. 12 | 13 | import VueI18n from './vue-i18n'; 14 | import VueMeta from './vue-meta'; 15 | import VueRouter from './vue-router'; 16 | import Vuex from './vuex'; 17 | 18 | export { 19 | VueI18n, 20 | VueMeta, 21 | VueRouter, 22 | Vuex 23 | }; 24 | 25 | export default { 26 | VueI18n, 27 | VueMeta, 28 | VueRouter, 29 | Vuex 30 | }; 31 | -------------------------------------------------------------------------------- /src/styles/base/base-lists.scss: -------------------------------------------------------------------------------- 1 | 2 | @import '~@shared-styles'; 3 | 4 | 5 | 6 | // Lists 7 | ul, 8 | ol { 9 | padding-left: 0; 10 | list-style-type: none; 11 | 12 | ul, 13 | ol { 14 | margin-top: 0; 15 | margin-bottom: 0; 16 | } 17 | 18 | } 19 | 20 | // Inline lists 21 | // NOTE: use .inline.block to keep the list item itself block element 22 | ul, 23 | ol, 24 | dl { 25 | 26 | // Adjust behavior when combined with utility classes 27 | 28 | &.inline { 29 | li { 30 | display: inline; 31 | } 32 | } 33 | 34 | &.inline-block { 35 | li { 36 | display: inline-block; 37 | } 38 | } 39 | 40 | 41 | 42 | // Horizontal lists with floated block elements 43 | // FIXME: this should be a separate utility, now these are just magical class names 44 | 45 | &.collapse, 46 | &.collapse-right { 47 | clear: none; 48 | li, 49 | dt, 50 | dd { 51 | clear: none; 52 | float: left; 53 | } 54 | } 55 | 56 | &.collapse { 57 | @include clear-after; 58 | // float: left; 59 | } 60 | &.collapse-right { 61 | // float: right; 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /src/components/snippets/InlineSpinner.vue: -------------------------------------------------------------------------------- 1 | 2 | 33 | 34 | 37 | 38 | 58 | -------------------------------------------------------------------------------- /.markdownlint.json: -------------------------------------------------------------------------------- 1 | { 2 | "default": false, 3 | "header-style": { 4 | "style": "atx" 5 | }, 6 | "ul-style": { 7 | "style": "dash" 8 | }, 9 | "list-indent": true, 10 | "ul-start-left": true, 11 | "ul-indent": { 12 | "indent": 1 13 | }, 14 | "no-trailing-spaces": true, 15 | "no-hard-tabs": false, 16 | "no-reversed-links": true, 17 | "no-multiple-blanks": { 18 | "maximum": 3 19 | }, 20 | "no-missing-space-atx": true, 21 | "no-multiple-space-atx": true, 22 | "no-missing-space-closed-atx": true, 23 | "no-multiple-space-closed-atx": true, 24 | "blanks-around-headers": true, 25 | "header-start-left": true, 26 | "no-multiple-space-blockquote": true, 27 | "no-blanks-blockquote": true, 28 | "ol-prefix": { 29 | "style": "ordered" 30 | }, 31 | "list-marker-space": true, 32 | "blanks-around-fences": true, 33 | "blanks-around-lists": true, 34 | "no-inline-html": false, 35 | "hr-style": { 36 | "style": "---" 37 | }, 38 | "no-emphasis-as-header": true, 39 | "no-space-in-emphasis": true, 40 | "no-space-in-code": true, 41 | "no-space-in-links": true, 42 | "no-empty-links": true 43 | } 44 | -------------------------------------------------------------------------------- /tooling/build.js: -------------------------------------------------------------------------------- 1 | require('./check-versions')() 2 | 3 | process.env.NODE_ENV = 'production' 4 | 5 | var ora = require('ora') 6 | var rm = require('rimraf') 7 | var path = require('path') 8 | var chalk = require('chalk') 9 | var webpack = require('webpack') 10 | var config = require('./env') 11 | var webpackConfig = require('./webpack.prod.conf') 12 | 13 | var spinner = ora('building for production...') 14 | spinner.start() 15 | 16 | rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => { 17 | if (err) throw err 18 | webpack(webpackConfig, function (err, stats) { 19 | spinner.stop() 20 | if (err) throw err 21 | process.stdout.write(stats.toString({ 22 | colors: true, 23 | modules: false, 24 | children: false, 25 | chunks: false, 26 | chunkModules: false 27 | }) + '\n\n') 28 | 29 | console.log(chalk.cyan(' Build complete.\n')) 30 | console.log(chalk.yellow( 31 | ' Tip: built files are meant to be served over an HTTP server.\n' + 32 | ' Opening index.html over file:// won\'t work.\n' 33 | )) 34 | }) 35 | }) 36 | -------------------------------------------------------------------------------- /src/styles/transitions/transition-scale-fade.scss: -------------------------------------------------------------------------------- 1 | 2 | @import '~@shared-styles'; 3 | 4 | // Shared, defaults 5 | .transition-scale-fade-enter, 6 | .transition-scale-fade-enter-active, 7 | .transition-scale-fade-leave, 8 | .transition-scale-fade-leave-active { 9 | transition-property: opacity, transform; 10 | } 11 | 12 | .transition-scale-fade-enter, 13 | .transition-scale-fade-enter-active { 14 | @include transition-fast; 15 | } 16 | 17 | .transition-scale-fade-leave, 18 | .transition-scale-fade-leave-active { 19 | @include transition-fast; 20 | } 21 | 22 | // Default state to transition a) to when entering and b) from when leaving 23 | .transition-scale-fade-enter-active, 24 | .transition-scale-fade-leave { 25 | opacity: 1; 26 | transform: scale(1); 27 | } 28 | 29 | // Start state when entering 30 | .transition-scale-fade-enter { 31 | opacity: 0; 32 | // transform: scale($scale-large); 33 | transform: scale($scale-verysmall); 34 | } 35 | 36 | // Out 37 | .transition-scale-fade-leave-active { 38 | opacity: 0; 39 | // transform: scale($scale-small); 40 | transform: scale($scale-verysmall); 41 | } 42 | -------------------------------------------------------------------------------- /src/models/Account.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import { includes } from 'lodash'; 3 | 4 | import config from '@config'; 5 | 6 | export default Vue.extend({ 7 | 8 | props: { 9 | 10 | id: { 11 | type: Number, 12 | required: true 13 | }, 14 | 15 | email: { 16 | type: String 17 | }, 18 | 19 | name: { 20 | type: String 21 | }, 22 | 23 | role: { 24 | type: String, 25 | default: null, 26 | validator: function (value) { 27 | return value === null || includes(config.routePermissionRoles, value); 28 | } 29 | }, 30 | 31 | avatarUrl: { 32 | type: String, 33 | required: false, 34 | default: 'user-avatar-placeholder.png' 35 | } 36 | 37 | }, 38 | 39 | computed: { 40 | 41 | nameOrEmail: function () { 42 | return this.name ? this.name : this.email; 43 | }, 44 | 45 | accessLevel: function () { 46 | return config.routePermissionRoles.indexOf(this.role) + 1; 47 | }, 48 | 49 | isAdmin: function () { 50 | let maxAccessLevel = config.routePermissionRoles.length; 51 | return this.accessLevel >= maxAccessLevel; 52 | } 53 | 54 | } 55 | 56 | }); 57 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Jerry Jäppinen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/config/config.dev.base.js: -------------------------------------------------------------------------------- 1 | // Override base config values when in dev environment 2 | module.exports = { 3 | env: 'development', 4 | 5 | // Don't develop against production 6 | currentRemote: 'staging', 7 | 8 | // Additional remotes useful for development 9 | remotes: { 10 | 11 | localhost: { 12 | path: 'https://localhost/', 13 | api: '', 14 | login: 'login/', 15 | logout: 'logout/' 16 | }, 17 | 18 | staging: { 19 | path: 'https://jsonplaceholder.typicode.com/', 20 | api: '', 21 | login: 'login/', 22 | logout: 'logout/' 23 | } 24 | 25 | }, 26 | 27 | meta: { 28 | // title: 'DEVELOPMENT MODE' 29 | }, 30 | 31 | // Turn off caching features for dev 32 | // 33 | // NOTE 34 | // There is a minor issue that might manifest itself if you change this: 35 | // https://github.com/Eiskis/bellevue/issues/28 36 | offlineCache: { 37 | enabled: false 38 | }, 39 | 40 | // These routes are for dev only 41 | routePermissions: { 42 | 'console': 0, 43 | 'consoleComponents': 0, 44 | 'consoleConfiguration': 0, 45 | 'consoleModels': 0, 46 | 'consolePlugins': 0, 47 | 'consoleServices': 0, 48 | 'consoleVuex': 0 49 | } 50 | 51 | }; 52 | -------------------------------------------------------------------------------- /src/vue/plugins/vue-router.js: -------------------------------------------------------------------------------- 1 | 2 | // The officially supported router 3 | // https://router.vuejs.org/en/ 4 | import Vue from 'vue'; 5 | import Router from 'vue-router'; 6 | 7 | // Import route components for vue-router 8 | import config from '@config'; 9 | 10 | // eslint-disable-next-line import/extensions 11 | import defaultRoutes from '@config/config.routes'; 12 | 13 | // eslint-disable-next-line import/extensions 14 | import devRoutes from '@config/config.dev.routes'; 15 | 16 | // Merge configs with multiple sources 17 | let mergedRoutes = defaultRoutes; 18 | 19 | if (process.env.NODE_ENV === 'development') { 20 | mergedRoutes = mergedRoutes.concat(devRoutes); 21 | } 22 | 23 | // Autoload plugin 24 | Vue.use(Router); 25 | 26 | // Set up router options 27 | export const options = { 28 | 29 | // Class names used by 30 | // NOTE: these should conform to our class naming conventions 31 | linkActiveClass: config.router.linkActiveClass, 32 | linkExactActiveClass: config.router.linkExactActiveClass, 33 | 34 | // Our frontend URL scheme 35 | routes: mergedRoutes 36 | 37 | }; 38 | 39 | // Export a new plugin instance 40 | export default new Router(options); 41 | -------------------------------------------------------------------------------- /spec/services/env.spec.js: -------------------------------------------------------------------------------- 1 | import { env } from '@services'; 2 | 3 | describe('Env service', function () { 4 | 5 | // https://stackoverflow.com/questions/19877924/what-is-the-list-of-possible-values-for-navigator-platform-as-of-today 6 | const n = { 7 | chromeMac: { 8 | ua: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36', 9 | platform: 'MacIntel' 10 | }, 11 | safariMac: { 12 | ua: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) AppleWebKit/603.2.4 (KHTML, like Gecko) Version/10.1.1 Safari/603.2.4', 13 | platform: 'MacIntel' 14 | } 15 | }; 16 | 17 | it('should return string for Mac on chromeMac', function () { 18 | var os = env.getOsFromNavigator(n.chromeMac.ua, n.chromeMac.platform); 19 | expect(os).to.be.a('string'); 20 | }); 21 | 22 | it('should detect Mac on chromeMac', function () { 23 | var os = env.getOsFromNavigator(n.chromeMac.ua, n.chromeMac.platform); 24 | expect(os).to.equal('mac'); 25 | }); 26 | 27 | it('should detect Mac on safariMac', function () { 28 | var os = env.getOsFromNavigator(n.safariMac.ua, n.safariMac.platform); 29 | expect(os).to.equal('mac'); 30 | }); 31 | 32 | }); 33 | -------------------------------------------------------------------------------- /src/util/index.js: -------------------------------------------------------------------------------- 1 | import clearSelection from './clearSelection'; 2 | import composeClassnames from './composeClassnames'; 3 | import escapeRegExpString from './escapeRegExpString'; 4 | import eventHasMetaKey from './eventHasMetaKey'; 5 | import extractClassnames from './extractClassnames'; 6 | import linkIsExternal from './linkIsExternal'; 7 | import getDomainName from './getDomainName'; 8 | import startsWith from './startsWith'; 9 | import processLargeArray from './processLargeArray'; 10 | import replaceAll from './replaceAll'; 11 | import scrollToElement from './scrollToElement'; 12 | import trimWhitespace from './trimWhitespace'; 13 | 14 | export { 15 | clearSelection, 16 | composeClassnames, 17 | escapeRegExpString, 18 | eventHasMetaKey, 19 | extractClassnames, 20 | linkIsExternal, 21 | getDomainName, 22 | startsWith, 23 | processLargeArray, 24 | replaceAll, 25 | scrollToElement, 26 | trimWhitespace 27 | }; 28 | 29 | export default { 30 | clearSelection, 31 | composeClassnames, 32 | escapeRegExpString, 33 | eventHasMetaKey, 34 | extractClassnames, 35 | linkIsExternal, 36 | getDomainName, 37 | startsWith, 38 | processLargeArray, 39 | replaceAll, 40 | scrollToElement, 41 | trimWhitespace 42 | }; 43 | -------------------------------------------------------------------------------- /src/components/popovers/PopoverMainMenu.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 47 | 48 | 57 | -------------------------------------------------------------------------------- /src/styles/utilities-base/utility-type.scss: -------------------------------------------------------------------------------- 1 | 2 | @import '~@shared-styles'; 3 | 4 | 5 | 6 | // Font styles 7 | .type-uppercase { 8 | @include type-uppercase; 9 | } 10 | 11 | .type-small { 12 | @include type-small; 13 | } 14 | 15 | .type-large { 16 | @include type-large; 17 | } 18 | 19 | .type-mono { 20 | @include type-mono; 21 | } 22 | 23 | .type-display { 24 | @include type-display; 25 | } 26 | 27 | .type-serif { 28 | @include type-serif; 29 | } 30 | 31 | .type-large { 32 | @include type-large; 33 | } 34 | 35 | 36 | 37 | // Headings 38 | .type-h1 { 39 | @include type-h1; 40 | } 41 | 42 | .type-h2 { 43 | @include type-h2; 44 | } 45 | 46 | .type-h3 { 47 | @include type-h3; 48 | } 49 | 50 | .type-h4 { 51 | @include type-h4; 52 | } 53 | 54 | .type-h5 { 55 | @include type-h5; 56 | } 57 | 58 | .type-hyphens { 59 | @include type-hyphens; 60 | } 61 | 62 | .type-nohyphens { 63 | @include type-nohyphens; 64 | } 65 | 66 | .type-nobreak { 67 | @include type-nobreak; 68 | } 69 | 70 | .type-ellipsis { 71 | @include type-ellipsis; 72 | } 73 | 74 | .type-discreet { 75 | @include type-discreet; 76 | } 77 | 78 | .type-discreet-small { 79 | @include type-discreet-small; 80 | } 81 | 82 | .type-warning { 83 | @include type-warning; 84 | } 85 | -------------------------------------------------------------------------------- /src/components/snippets/Icon.vue: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 41 | 42 | 47 | 48 | 67 | -------------------------------------------------------------------------------- /src/vue/mixins/persist.js: -------------------------------------------------------------------------------- 1 | import { debounce } from 'lodash'; 2 | 3 | // Set a computed property to automatically store in localStorage 4 | // https://vuejs.org/v2/guide/mixins.html 5 | export default { 6 | 7 | computed: { 8 | 9 | // NOTE: This can be undefined especially for non-components 10 | persistKey: function () { 11 | return this.$options.name; 12 | } 13 | 14 | }, 15 | 16 | watch: { 17 | 18 | // Store serialized data into localStorage when it changes (throttled) 19 | persist: debounce(function (data) { 20 | if (this.persistKey) { 21 | localStorage.setItem(this.persistKey, JSON.stringify(data)); 22 | } 23 | }, 500) 24 | 25 | }, 26 | 27 | created: function () { 28 | if (this.persistKey && this.persist) { 29 | 30 | // Load serialized data from localStorage 31 | // NOTE: this is a synchronous operation, theoretically it might slow things down 32 | var data = localStorage.getItem(this.persistKey); 33 | 34 | if (data) { 35 | try { 36 | data = JSON.parse(data); 37 | 38 | // We found data in local storage, let's load it up 39 | if (data) { 40 | this.persist = data; 41 | } 42 | 43 | } catch (error) { 44 | console.error(error); 45 | } 46 | } 47 | 48 | } 49 | 50 | } 51 | 52 | }; 53 | -------------------------------------------------------------------------------- /src/components/snippets/Ellipsis.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 32 | 33 | 60 | -------------------------------------------------------------------------------- /src/config/config.dev.routes.js: -------------------------------------------------------------------------------- 1 | 2 | // Adds new routes in addition to the defaults 3 | import components from '@vue-components'; 4 | 5 | export default [ 6 | 7 | { 8 | path: '/console', 9 | name: 'console', 10 | component: components.PageConsole, 11 | 12 | // Redirecting to the default child page 13 | redirect: { 14 | name: 'consoleComponents' 15 | }, 16 | 17 | // https://router.vuejs.org/en/essentials/nested-routes.html 18 | children: [ 19 | 20 | { 21 | path: 'components', 22 | name: 'consoleComponents', 23 | component: components.ConsoleComponents 24 | }, 25 | 26 | { 27 | path: 'configuration', 28 | name: 'consoleConfiguration', 29 | component: components.ConsoleConfiguration 30 | }, 31 | 32 | { 33 | path: 'models', 34 | name: 'consoleModels', 35 | component: components.ConsoleModels 36 | }, 37 | 38 | { 39 | path: 'plugins', 40 | name: 'consolePlugins', 41 | component: components.ConsolePlugins 42 | }, 43 | 44 | { 45 | path: 'services', 46 | name: 'consoleServices', 47 | component: components.ConsoleServices 48 | }, 49 | 50 | { 51 | path: 'vuex', 52 | name: 'consoleVuex', 53 | component: components.ConsoleVuex 54 | } 55 | 56 | ] 57 | 58 | } 59 | 60 | ]; 61 | -------------------------------------------------------------------------------- /src/styles/mixins/mixin-fill.scss: -------------------------------------------------------------------------------- 1 | 2 | @mixin fill { 3 | position: absolute; 4 | top: 0; 5 | left: 0; 6 | box-sizing: border-box; 7 | width: 100%; 8 | height: 100%; 9 | } 10 | 11 | @mixin fill-width { 12 | position: absolute; 13 | left: 0; 14 | box-sizing: border-box; 15 | width: 100%; 16 | } 17 | 18 | @mixin fill-height { 19 | position: absolute; 20 | top: 0; 21 | box-sizing: border-box; 22 | height: 100%; 23 | } 24 | 25 | 26 | 27 | @mixin fill-relative { 28 | position: relative; 29 | top: 0; 30 | left: 0; 31 | box-sizing: border-box; 32 | width: 100%; 33 | height: 100%; 34 | } 35 | 36 | @mixin fill-width-relative { 37 | position: relative; 38 | left: 0; 39 | box-sizing: border-box; 40 | width: 100%; 41 | } 42 | 43 | @mixin fill-height-relative { 44 | position: relative; 45 | top: 0; 46 | box-sizing: border-box; 47 | height: 100%; 48 | } 49 | 50 | 51 | 52 | @mixin fill-fixed { 53 | position: fixed; 54 | top: 0; 55 | left: 0; 56 | box-sizing: border-box; 57 | width: 100%; 58 | height: 100%; 59 | } 60 | 61 | @mixin fill-width-fixed { 62 | position: fixed; 63 | left: 0; 64 | box-sizing: border-box; 65 | width: 100%; 66 | } 67 | 68 | @mixin fill-height-fixed { 69 | position: fixed; 70 | top: 0; 71 | box-sizing: border-box; 72 | height: 100%; 73 | } 74 | -------------------------------------------------------------------------------- /src/models/Post.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | 3 | import { api } from '@services'; 4 | 5 | export default Vue.extend({ 6 | 7 | props: { 8 | 9 | id: { 10 | type: Number, 11 | required: true 12 | }, 13 | 14 | userId: { 15 | type: Number, 16 | required: true 17 | }, 18 | 19 | title: { 20 | type: String 21 | }, 22 | 23 | body: { 24 | type: String 25 | }, 26 | 27 | comments: { 28 | type: Array, 29 | default: function () { 30 | return []; 31 | } 32 | } 33 | 34 | }, 35 | 36 | data: function () { 37 | return { 38 | user: null 39 | }; 40 | }, 41 | 42 | computed: { 43 | 44 | hasComments: function () { 45 | return this.comments.length ? true : false; 46 | }, 47 | 48 | excerpt: function () { 49 | return this.body.substr(0, 300); 50 | } 51 | 52 | }, 53 | 54 | methods: { 55 | 56 | fetchComments: function () { 57 | let post = this; 58 | api.getCommentsForPost(this.id, function (response) { 59 | post.comments = response.data; 60 | }); 61 | }, 62 | 63 | fetchUser: function () { 64 | let post = this; 65 | api.getUser(this.userId, function (response) { 66 | post.user = response.data; 67 | }); 68 | } 69 | 70 | }, 71 | 72 | created: function () { 73 | // this.fetchUser(); 74 | } 75 | 76 | }); 77 | -------------------------------------------------------------------------------- /src/components/console/ConsoleVuex.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 55 | 56 | 59 | -------------------------------------------------------------------------------- /src/styles/base/base-inputs.scss: -------------------------------------------------------------------------------- 1 | 2 | @import '~@shared-styles'; 3 | 4 | 5 | 6 | // Labels 7 | 8 | label { 9 | @include cursor-pointer; 10 | } 11 | 12 | 13 | 14 | // Reset input styling 15 | button, 16 | input, 17 | textarea { 18 | font-family: inherit; 19 | font-size: inherit; 20 | line-height: inherit; 21 | 22 | padding: 0; 23 | 24 | border-width: 0; 25 | border-style: solid; 26 | outline-style: solid; 27 | border-radius: 0; 28 | 29 | vertical-align: middle; 30 | 31 | color: inherit; 32 | outline-color: transparent; 33 | 34 | background-color: transparent; 35 | 36 | &:focus { 37 | outline-width: 0; 38 | outline-offset: 0; 39 | } 40 | 41 | appearance: none; 42 | 43 | // &[type="search"] { 44 | // appearance: textfield; 45 | // } 46 | 47 | transition-property: color, border-color, background-color, box-shadow; 48 | } 49 | 50 | // Dimensions 51 | input, 52 | textarea { 53 | cursor: inherit; 54 | box-sizing: border-box; 55 | max-width: 100%; 56 | // border-color: transparent; 57 | // padding: ($pad-vertical - 1px) ($pad-horizontal - 1px); 58 | // border-width: 1px; 59 | 60 | &.block { 61 | width: 100%; 62 | max-height: 100%; 63 | } 64 | 65 | } 66 | 67 | // textarea { 68 | // height: 10.72em; 69 | // &.squeeze { 70 | // height: 4.02em; 71 | // } 72 | // } 73 | 74 | ::placeholder { 75 | color: $color-grey; 76 | } 77 | -------------------------------------------------------------------------------- /src/services/network.js: -------------------------------------------------------------------------------- 1 | 2 | import Vue from 'vue'; 3 | 4 | export default new Vue({ 5 | 6 | data: function () { 7 | return { 8 | isOnline: false, 9 | isConnecting: true 10 | }; 11 | }, 12 | 13 | computed: { 14 | 15 | isOffline: function () { 16 | return !this.isOnline; 17 | } 18 | 19 | }, 20 | 21 | methods: { 22 | 23 | setOffline: function () { 24 | this.isOnline = false; 25 | return this; 26 | }, 27 | 28 | setOnline: function () { 29 | this.isOnline = true; 30 | return this; 31 | }, 32 | 33 | getOnlineStatus: function () { 34 | return window.navigator.onLine ? true : false; 35 | }, 36 | 37 | updateOnlineStatus: function () { 38 | this.isOnline = this.getOnlineStatus(); 39 | return this; 40 | }, 41 | 42 | setListeners: function () { 43 | window.addEventListener('online', this.updateOnlineStatus); 44 | window.addEventListener('offline', this.updateOnlineStatus); 45 | }, 46 | 47 | removeListeners: function () { 48 | window.removeEventListener('online', this.updateOnlineStatus); 49 | window.removeEventListener('offline', this.updateOnlineStatus); 50 | } 51 | 52 | }, 53 | 54 | created: function () { 55 | this.updateOnlineStatus(); 56 | this.isConnecting = false; 57 | this.setListeners(); 58 | }, 59 | 60 | beforeDestroy: function () { 61 | this.removeListeners(); 62 | } 63 | 64 | }); 65 | -------------------------------------------------------------------------------- /src/styles/base/base-type.scss: -------------------------------------------------------------------------------- 1 | 2 | @import '~@shared-styles'; 3 | 4 | 5 | 6 | html { 7 | font-family: $font-sans; 8 | font-size: $font-size-default; 9 | // font-weight: 300; 10 | 11 | @include viewport-over-verylarge { 12 | font-size: $font-size-default-over-verylarge; 13 | } 14 | 15 | // @include viewport-over($breakpoint-ridiculous) { 16 | // font-size: 1.5 * $font-size-default; 17 | // } 18 | 19 | // @include viewport-over($breakpoint-totallyridiculous) { 20 | // font-size: 2 * $font-size-default; 21 | // } 22 | 23 | } 24 | 25 | blockquote, 26 | q { 27 | font-family: $font-sans; 28 | } 29 | 30 | i { 31 | svg { 32 | display: block; 33 | width: 1em; 34 | height: 1em; 35 | } 36 | } 37 | 38 | address { 39 | font-style: inherit; 40 | } 41 | 42 | // Headings 43 | h2, 44 | h3, 45 | h4 { 46 | font-weight: inherit; 47 | } 48 | 49 | h3, 50 | h4, 51 | h5 { 52 | font-size: inherit; 53 | } 54 | 55 | h1 { 56 | @include type-h1; 57 | } 58 | 59 | h2 { 60 | @include type-h2; 61 | } 62 | 63 | h3 { 64 | @include type-h3; 65 | } 66 | 67 | h4 { 68 | @include type-h4; 69 | } 70 | 71 | th, 72 | dt, 73 | h5 { 74 | @include type-h5; 75 | } 76 | 77 | 78 | 79 | // Code 80 | code { 81 | white-space: nowrap; 82 | } 83 | 84 | pre { 85 | code { 86 | white-space: inherit; 87 | } 88 | } 89 | 90 | pre, 91 | code, 92 | kbd, 93 | samp { 94 | @include type-mono; 95 | } 96 | -------------------------------------------------------------------------------- /src/styles/shared.scss: -------------------------------------------------------------------------------- 1 | 2 | // This shared.scss can easily be imported in components and global style files to ensure all mixins and constants are available 3 | // NOTE: shared SCSS should NOT output any CSS. This way it can be imported anywhere without duplicating output. 4 | 5 | // Constants, functions etc. 6 | @import './definitions/functions'; 7 | @import './definitions/constants'; 8 | 9 | // Atomic style properties as mixins 10 | @import './mixins/mixin-animation'; 11 | @import './mixins/mixin-background'; 12 | @import './mixins/mixin-box-model'; 13 | @import './mixins/mixin-buffer'; 14 | @import './mixins/mixin-clear'; 15 | @import './mixins/mixin-container'; 16 | @import './mixins/mixin-cursor'; 17 | @import './mixins/mixin-display'; 18 | @import './mixins/mixin-fill'; 19 | @import './mixins/mixin-font'; 20 | @import './mixins/mixin-hide'; 21 | @import './mixins/mixin-keep'; 22 | @import './mixins/mixin-limit'; 23 | @import './mixins/mixin-overflow'; 24 | @import './mixins/mixin-overlay'; 25 | @import './mixins/mixin-pad'; 26 | @import './mixins/mixin-pull'; 27 | @import './mixins/mixin-push'; 28 | @import './mixins/mixin-radius'; 29 | @import './mixins/mixin-rhythm'; 30 | @import './mixins/mixin-row'; 31 | @import './mixins/mixin-shadow'; 32 | @import './mixins/mixin-transform'; 33 | @import './mixins/mixin-transitions'; 34 | @import './mixins/mixin-type'; 35 | @import './mixins/mixin-viewport'; 36 | -------------------------------------------------------------------------------- /src/components/pages/PageConsole.vue: -------------------------------------------------------------------------------- 1 | 2 | 61 | 62 | 77 | 78 | 81 | -------------------------------------------------------------------------------- /src/styles/utilities-base/utility-hide.scss: -------------------------------------------------------------------------------- 1 | 2 | @import '~@shared-styles'; 3 | 4 | 5 | 6 | .hide { 7 | display: none; 8 | } 9 | 10 | 11 | 12 | // Media 13 | 14 | .hide-in-print { 15 | @include hide-in-print; 16 | } 17 | 18 | .hide-in-not-print { 19 | @include hide-in-not-print; 20 | } 21 | 22 | 23 | 24 | // Orientation 25 | 26 | .hide-in-landscape { 27 | @include hide-in-landscape; 28 | } 29 | 30 | .hide-in-portrait { 31 | @include hide-in-portrait; 32 | } 33 | 34 | 35 | 36 | // Responsive 37 | 38 | .hide-under-tiny { 39 | @include hide-under-tiny; 40 | } 41 | 42 | .hide-over-tiny { 43 | @include hide-over-tiny; 44 | } 45 | 46 | 47 | 48 | .hide-under-small { 49 | @include hide-under-small; 50 | } 51 | 52 | .hide-over-small { 53 | @include hide-over-small; 54 | } 55 | 56 | 57 | 58 | .hide-under-smallish { 59 | @include hide-under-smallish; 60 | } 61 | 62 | .hide-over-smallish { 63 | @include hide-over-smallish; 64 | } 65 | 66 | 67 | 68 | .hide-under-medium { 69 | @include hide-under-medium; 70 | } 71 | 72 | .hide-over-medium { 73 | @include hide-over-medium; 74 | } 75 | 76 | 77 | 78 | .hide-under-large { 79 | @include hide-under-large; 80 | } 81 | 82 | .hide-over-large { 83 | @include hide-over-large; 84 | } 85 | 86 | 87 | 88 | .hide-under-verylarge { 89 | @include hide-under-verylarge; 90 | } 91 | 92 | .hide-over-verylarge { 93 | @include hide-over-verylarge; 94 | } 95 | -------------------------------------------------------------------------------- /src/styles/utilities-composed/utility-bodytext.scss: -------------------------------------------------------------------------------- 1 | 2 | @import '~@shared-styles'; 3 | 4 | 5 | 6 | // Text context 7 | 8 | // This is used for rich body text. Elements commonly used in building components, like links, are robbed of their sensible defaults. Attach this utility class on a body text container to style the rich text elements in a way that makes sense for natural article content. 9 | 10 | .bodytext { 11 | 12 | // Links 13 | a { 14 | font-weight: 500; 15 | color: $color-link; 16 | 17 | &:hover, 18 | &:active { 19 | color: $color-link-active; 20 | } 21 | 22 | } 23 | 24 | strong { 25 | font-weight: 600; 26 | } 27 | 28 | // Code 29 | dt, 30 | h3, 31 | h4 { 32 | code { 33 | text-transform: none; 34 | } 35 | } 36 | 37 | code { 38 | padding: 0 0.2em; 39 | @include radius-tight; 40 | background-color: $color-verylightgrey; 41 | box-shadow: 0 0 0 1px $color-lightgrey; 42 | } 43 | 44 | // FIXME: duplicated from base/o-code.scss 45 | pre { 46 | code { 47 | @include radius-loose; 48 | padding: 1.6em; 49 | box-shadow: none; 50 | } 51 | } 52 | 53 | 54 | // Lists 55 | ul, 56 | ol { 57 | margin-left: 2em; 58 | 59 | li { 60 | margin-bottom: 0.4em; 61 | } 62 | 63 | ul, 64 | ol { 65 | margin-bottom: 0.8em; 66 | } 67 | 68 | } 69 | 70 | ul { 71 | list-style-type: disc; 72 | } 73 | 74 | ol { 75 | list-style-type: disc; 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /tooling/e2e/nightwatch.conf.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | require('babel-register') 3 | 4 | var config = require('../../tooling/env') 5 | var aliases = require('../../src/config/config.aliases') 6 | var specPath = aliases['@spec'] + '/e2e' 7 | 8 | // http://nightwatchjs.org/gettingstarted#settings-file 9 | module.exports = { 10 | globals_path: path.join(__dirname, './nightwatch.globals.js'), 11 | 12 | src_folders: [specPath], 13 | output_folder: 'reports/e2e', 14 | custom_assertions_path: ['tooling/e2e/custom-assertions'], 15 | 16 | selenium: { 17 | start_process: true, 18 | server_path: require('selenium-server').path, 19 | host: '127.0.0.1', 20 | port: 4444, 21 | cli_args: { 22 | 'webdriver.chrome.driver': require('chromedriver').path 23 | } 24 | }, 25 | 26 | test_settings: { 27 | default: { 28 | selenium_port: 4444, 29 | selenium_host: 'localhost', 30 | silent: true, 31 | globals: { 32 | devServerURL: 'http://localhost:' + (process.env.PORT || config.dev.port) 33 | } 34 | }, 35 | 36 | chrome: { 37 | desiredCapabilities: { 38 | browserName: 'chrome', 39 | javascriptEnabled: true, 40 | acceptSslCerts: true 41 | } 42 | }, 43 | 44 | firefox: { 45 | desiredCapabilities: { 46 | browserName: 'firefox', 47 | javascriptEnabled: true, 48 | acceptSslCerts: true 49 | } 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /tooling/check-versions.js: -------------------------------------------------------------------------------- 1 | var chalk = require('chalk') 2 | var semver = require('semver') 3 | var packageConfig = require('../package.json') 4 | var shell = require('shelljs') 5 | function exec (cmd) { 6 | return require('child_process').execSync(cmd).toString().trim() 7 | } 8 | 9 | var versionRequirements = [ 10 | { 11 | name: 'node', 12 | currentVersion: semver.clean(process.version), 13 | versionRequirement: packageConfig.engines.node 14 | }, 15 | ] 16 | 17 | if (shell.which('npm')) { 18 | versionRequirements.push({ 19 | name: 'npm', 20 | currentVersion: exec('npm --version'), 21 | versionRequirement: packageConfig.engines.npm 22 | }) 23 | } 24 | 25 | module.exports = function () { 26 | var warnings = [] 27 | for (var i = 0; i < versionRequirements.length; i++) { 28 | var mod = versionRequirements[i] 29 | if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) { 30 | warnings.push(mod.name + ': ' + 31 | chalk.red(mod.currentVersion) + ' should be ' + 32 | chalk.green(mod.versionRequirement) 33 | ) 34 | } 35 | } 36 | 37 | if (warnings.length) { 38 | console.log('') 39 | console.log(chalk.yellow('To use this template, you must update following to modules:')) 40 | console.log() 41 | for (var i = 0; i < warnings.length; i++) { 42 | var warning = warnings[i] 43 | console.log(' ' + warning) 44 | } 45 | console.log() 46 | process.exit(1) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/services/time.js: -------------------------------------------------------------------------------- 1 | 2 | import Vue from 'vue'; 3 | 4 | export default new Vue({ 5 | 6 | data: function () { 7 | return { 8 | current: new Date(), 9 | _timer: null 10 | }; 11 | }, 12 | 13 | computed: { 14 | 15 | }, 16 | 17 | methods: { 18 | 19 | // Update the current time with a new Date object 20 | setCurrentTime: function () { 21 | this.current = new Date(); 22 | return this; 23 | }, 24 | 25 | // Throttled callback for each update. 26 | onTimerUpdate: function () { 27 | window.requestAnimationFrame(this.setCurrentTime); 28 | return this; 29 | }, 30 | 31 | // Start timer and regular updates. Timer will run until `stopTimer` is called. If timer is already running, it will be stopped and then restarted. 32 | startTimer: function () { 33 | var clock = this; 34 | 35 | // Setup 36 | clock.stopTimer(); 37 | 38 | // Update immediately for the first time 39 | clock.onTimerUpdate(); 40 | 41 | // Start interval 42 | clock._timer = setInterval(function () { 43 | clock.onTimerUpdate(); 44 | }, 1000); 45 | 46 | return clock; 47 | }, 48 | 49 | // Stop updating the current time. `time` will keep the last time. 50 | stopTimer: function () { 51 | 52 | if (this._timer) { 53 | clearInterval(this._timer); 54 | this._timer = null; 55 | } 56 | 57 | return this; 58 | } 59 | 60 | }, 61 | 62 | created: function () { 63 | this.onTimerUpdate(); 64 | this.startTimer(); 65 | } 66 | 67 | }); 68 | -------------------------------------------------------------------------------- /src/components/layout/TitlebarMenu.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 53 | 54 | 65 | -------------------------------------------------------------------------------- /src/components/counters/LocalCounter.vue: -------------------------------------------------------------------------------- 1 | 2 | 57 | 58 | 66 | 67 | 80 | -------------------------------------------------------------------------------- /src/styles/definitions/functions.scss: -------------------------------------------------------------------------------- 1 | 2 | // Custom functions for SCSS 3 | // NOTE 4 | // - See built-in functions: http://sass-lang.com/documentation/Sass/Script/Functions.html 5 | // - If you find yourself using built-in functions with the same values often, considering creating a wrapper function in this file 6 | 7 | 8 | 9 | // Remove the unit of a length 10 | @function number ($number) { 11 | @if type-of($number) == 'number' and not unitless($number) { 12 | @return $number / ($number * 0 + 1); 13 | } 14 | @return $number; 15 | } 16 | 17 | 18 | 19 | // Color handling 20 | 21 | // Make translucent 22 | @function color-transparent ($color) { 23 | @return color-translucent($color, 0); 24 | } 25 | 26 | // Make color translucent 27 | // FIXME: should return 0.1 for black, 0.25 for light values 28 | @function color-translucent ($color, $strength: 0.15) { 29 | @return rgba($color, $strength); 30 | } 31 | 32 | // FIXME: should adjust based on hue 33 | @function color-translucent-light ($color) { 34 | @return color-translucent($color, 0.4); 35 | } 36 | 37 | // FIXME: should adjust based on hue 38 | @function color-translucent-verylight ($color) { 39 | @return color-translucent($color, 0.9); 40 | } 41 | 42 | 43 | 44 | // Make color darker 45 | @function color-darker ($color, $strength: 10%) { 46 | @return darken($color, $strength); 47 | } 48 | 49 | // Make color more saturated 50 | @function color-saturate ($color, $strength: 10%) { 51 | @return saturate(darken($color, 1.5 * $strength), $strength); 52 | } 53 | -------------------------------------------------------------------------------- /src/config/config.aliases.js: -------------------------------------------------------------------------------- 1 | 2 | // Aliases usable in codebase when doing imports and resolving URLs. From project root. 3 | // NOTE: unlike other configuration files, this is needed by the low-level tooling scripts BEFORE any aliases have been defined and full URL resolution is working (since those aliases are defined here, duh). That's why this is split from other configuration files. 4 | module.exports = { 5 | 6 | // src root 7 | // NOTE: prefer the other aliases over 8 | '@': 'src', 9 | 10 | // The base configuration (alias is mostly for the client) 11 | // NOTE: for client-side code it would be better to use a JS utility that reads configuration (merging values from multiple sources) instead of using these raw values in application code 12 | '@config': 'src/config', 13 | '@locales': 'src/locales', 14 | 15 | // Vendor code, services, utilities etc. 16 | '@models': 'src/models', 17 | '@services': 'src/services', 18 | '@util': 'src/util', 19 | 20 | // Assets 21 | '@assets': 'src/assets', 22 | '@fonts': 'src/fonts', 23 | '@svg': 'src/svg', 24 | 25 | // Vue application code 26 | '@vue-components': 'src/components', 27 | '@vue-directives': 'src/vue/directives', 28 | '@vue-mixins': 'src/vue/mixins', 29 | '@vue-plugins': 'src/vue/plugins', 30 | 31 | // State management 32 | '@store': 'src/store', 33 | 34 | // Global styles 35 | // NOTE: we could split this further 36 | '@styles': 'src/styles', 37 | '@shared-styles': 'src/styles/shared', 38 | 39 | // Test cases 40 | '@spec': 'spec' 41 | 42 | }; 43 | -------------------------------------------------------------------------------- /src/styles/utilities-base/utility-buffer.scss: -------------------------------------------------------------------------------- 1 | 2 | // To avoid code duplication, the utility classes are produced by a mixin 3 | @import '~@styles/toolchain/toolchain-utility-buffer'; 4 | 5 | 6 | 7 | // Default 8 | @include toolchain-utility-buffer; 9 | 10 | // Responsive versions 11 | @include viewport-under-tiny { 12 | @include toolchain-utility-buffer('under-tiny'); 13 | } 14 | 15 | @include viewport-over-tiny { 16 | @include toolchain-utility-buffer('over-tiny'); 17 | } 18 | 19 | @include viewport-under-small { 20 | @include toolchain-utility-buffer('under-small'); 21 | } 22 | 23 | @include viewport-over-small { 24 | @include toolchain-utility-buffer('over-small'); 25 | } 26 | 27 | @include viewport-under-smallish { 28 | @include toolchain-utility-buffer('under-smallish'); 29 | } 30 | 31 | @include viewport-over-smallish { 32 | @include toolchain-utility-buffer('over-smallish'); 33 | } 34 | 35 | @include viewport-under-medium { 36 | @include toolchain-utility-buffer('under-medium'); 37 | } 38 | 39 | @include viewport-over-medium { 40 | @include toolchain-utility-buffer('over-medium'); 41 | } 42 | 43 | @include viewport-under-large { 44 | @include toolchain-utility-buffer('under-large'); 45 | } 46 | 47 | @include viewport-over-large { 48 | @include toolchain-utility-buffer('over-large'); 49 | } 50 | 51 | @include viewport-under-verylarge { 52 | @include toolchain-utility-buffer('under-verylarge'); 53 | } 54 | 55 | @include viewport-over-verylarge { 56 | @include toolchain-utility-buffer('over-verylarge'); 57 | } 58 | -------------------------------------------------------------------------------- /src/styles/global.scss: -------------------------------------------------------------------------------- 1 | 2 | // CSS declarations 3 | 4 | // Keyframe animations 5 | @import './keyframes/keyframes-pulse'; 6 | @import './keyframes/keyframes-spin'; 7 | 8 | // Web fonts 9 | @import './webfonts/webfont-source-sans'; 10 | 11 | 12 | 13 | // Vendor code 14 | // 15 | // NOTE 16 | // 17 | // - only include vendor code that you want to build with the custom build pipeline (probably rarely if ever) 18 | // - precompiled vendor code should be included in index.html.ejs 19 | 20 | // @import './vendor/bootstrap-source'; 21 | 22 | 23 | 24 | // Vendor overrides 25 | // 26 | // NOTE 27 | // - many times the CSS code delivered with plugins causes a lot of issues and needs additional adjustments when it is included globally 28 | // - this is the place for it, so all these hacky overrides are in one place 29 | // - sometimes it is even better to not include the distributed vendor CSS at all and just rewrite the CSS without having to override anything 30 | 31 | @import './vendor-overrides/iphone-inline-video'; 32 | 33 | 34 | 35 | // Base CSS 36 | 37 | // Normalizing defaults 38 | @import './normalize/normalize'; 39 | @import './normalize/defaults'; 40 | 41 | // Global base styling 42 | @import './base/base-code'; 43 | @import './base/base-colors'; 44 | @import './base/base-hr'; 45 | @import './base/base-images'; 46 | @import './base/base-inputs'; 47 | @import './base/base-links'; 48 | @import './base/base-lists'; 49 | @import './base/base-rhythm'; 50 | @import './base/base-tables'; 51 | @import './base/base-tooltips'; 52 | @import './base/base-type'; 53 | -------------------------------------------------------------------------------- /spec/models/Post.spec.js: -------------------------------------------------------------------------------- 1 | // import Vue from 'vue'; 2 | import { Comment, Post } from '@models'; 3 | 4 | describe('Post model', function () { 5 | 6 | it('should have empty comments list when none is passed', function () { 7 | 8 | // Set up a new instance of model 9 | var post = new Post({ 10 | propsData: { 11 | id: 1, 12 | userId: 1 13 | } 14 | }); 15 | 16 | // Expected results 17 | expect(post.comments).to.be.a('array'); 18 | expect(post.comments.length).to.equal(0); 19 | 20 | }); 21 | 22 | it('should have no title when none is passed', function () { 23 | var post = new Post({ 24 | propsData: { 25 | id: 1, 26 | userId: 1 27 | } 28 | }); 29 | expect(post.title).to.not.be.ok; 30 | }); 31 | 32 | it('should have no body when none is passed', function () { 33 | var post = new Post({ 34 | propsData: { 35 | id: 1, 36 | userId: 1 37 | } 38 | }); 39 | expect(post.body).to.not.be.ok; 40 | }); 41 | 42 | it('should have hasComments as false when none is passed', function () { 43 | var post = new Post({ 44 | propsData: { 45 | id: 1, 46 | userId: 1 47 | } 48 | }); 49 | expect(post.hasComments).to.not.be.ok; 50 | }); 51 | 52 | it('should have hasComments as true when is passed', function () { 53 | var post = new Post({ 54 | propsData: { 55 | id: 1, 56 | userId: 1, 57 | comments: [ 58 | 59 | new Comment({ 60 | propsData: { 61 | id: 1, 62 | postId: 1 63 | } 64 | }) 65 | 66 | ] 67 | } 68 | }); 69 | expect(post.hasComments).to.be.ok; 70 | }); 71 | 72 | }); 73 | -------------------------------------------------------------------------------- /src/config/config.routes.js: -------------------------------------------------------------------------------- 1 | 2 | // Configuration for vue-router 3 | // NOTE: This is in a separate file, because Webpack has to be already running with aliases for the imports to work 4 | // NOTE: could maybe use `require.context` to make this more dynamic 5 | import components from '@vue-components'; 6 | 7 | export default [ 8 | 9 | // { 10 | // path: '*', 11 | // name: 'notFound', 12 | // component: components.PageNotFound 13 | // }, 14 | 15 | { 16 | // NOTE 17 | // - We could just display the home page component with this route 18 | // - But if we did, the router will think all top-level pages are children of that 19 | // - We also generally don't want more than one route for the same page 20 | // - So to allow users to use index paths, it's better to redirect to the default child page 21 | path: '/', 22 | name: 'root', 23 | redirect: { 24 | name: 'home' 25 | } 26 | }, 27 | 28 | { 29 | path: '/home', 30 | name: 'home', 31 | component: components.PageHome 32 | }, 33 | 34 | { 35 | path: '/login', 36 | name: 'login', 37 | component: components.PageLogin 38 | }, 39 | 40 | // NOTE 41 | // `page` here is optional, indicated by the question mark 42 | // See https://github.com/vuejs/vue-router/blob/dev/examples/route-matching/app.js#L6 43 | { 44 | path: '/posts/:page?', 45 | name: 'posts', 46 | component: components.PagePosts 47 | }, 48 | 49 | { 50 | path: '/post/:id', 51 | name: 'post', 52 | component: components.PagePost 53 | }, 54 | 55 | { 56 | path: '/secret', 57 | name: 'secret', 58 | component: components.PageHome 59 | } 60 | 61 | ]; 62 | -------------------------------------------------------------------------------- /src/styles/utilities-base/utility-pull.scss: -------------------------------------------------------------------------------- 1 | 2 | // To avoid code duplication, the utility classes are produced by a mixin 3 | @import '~@shared-styles'; 4 | @import '~@styles/toolchain/toolchain-utility-pull'; 5 | 6 | 7 | 8 | // Default 9 | @include toolchain-utility-pull; 10 | 11 | // Responsive versions 12 | @include viewport-under-tiny { 13 | @include toolchain-utility-pull('under-tiny'); 14 | } 15 | 16 | @include viewport-over-tiny { 17 | @include toolchain-utility-pull('over-tiny'); 18 | } 19 | 20 | 21 | 22 | @include viewport-under-small { 23 | @include toolchain-utility-pull('under-small'); 24 | } 25 | 26 | @include viewport-over-small { 27 | @include toolchain-utility-pull('over-small'); 28 | } 29 | 30 | 31 | 32 | @include viewport-under-smallish { 33 | @include toolchain-utility-pull('under-smallish'); 34 | } 35 | 36 | @include viewport-over-smallish { 37 | @include toolchain-utility-pull('over-smallish'); 38 | } 39 | 40 | 41 | 42 | @include viewport-under-medium { 43 | @include toolchain-utility-pull('under-medium'); 44 | } 45 | 46 | @include viewport-over-medium { 47 | @include toolchain-utility-pull('over-medium'); 48 | } 49 | 50 | 51 | 52 | @include viewport-under-large { 53 | @include toolchain-utility-pull('under-large'); 54 | } 55 | 56 | @include viewport-over-large { 57 | @include toolchain-utility-pull('over-large'); 58 | } 59 | 60 | 61 | 62 | @include viewport-under-verylarge { 63 | @include toolchain-utility-pull('under-verylarge'); 64 | } 65 | 66 | @include viewport-over-verylarge { 67 | @include toolchain-utility-pull('over-verylarge'); 68 | } 69 | 70 | 71 | -------------------------------------------------------------------------------- /src/styles/utilities-base/utility-push.scss: -------------------------------------------------------------------------------- 1 | 2 | // To avoid code duplication, the utility classes are produced by a mixin 3 | @import '~@shared-styles'; 4 | @import '~@styles/toolchain/toolchain-utility-push'; 5 | 6 | 7 | 8 | // Default 9 | @include toolchain-utility-push; 10 | 11 | // Responsive versions 12 | @include viewport-under-tiny { 13 | @include toolchain-utility-push('under-tiny'); 14 | } 15 | 16 | @include viewport-over-tiny { 17 | @include toolchain-utility-push('over-tiny'); 18 | } 19 | 20 | 21 | 22 | @include viewport-under-small { 23 | @include toolchain-utility-push('under-small'); 24 | } 25 | 26 | @include viewport-over-small { 27 | @include toolchain-utility-push('over-small'); 28 | } 29 | 30 | 31 | 32 | @include viewport-under-smallish { 33 | @include toolchain-utility-push('under-smallish'); 34 | } 35 | 36 | @include viewport-over-smallish { 37 | @include toolchain-utility-push('over-smallish'); 38 | } 39 | 40 | 41 | 42 | @include viewport-under-medium { 43 | @include toolchain-utility-push('under-medium'); 44 | } 45 | 46 | @include viewport-over-medium { 47 | @include toolchain-utility-push('over-medium'); 48 | } 49 | 50 | 51 | 52 | @include viewport-under-large { 53 | @include toolchain-utility-push('under-large'); 54 | } 55 | 56 | @include viewport-over-large { 57 | @include toolchain-utility-push('over-large'); 58 | } 59 | 60 | 61 | 62 | @include viewport-under-verylarge { 63 | @include toolchain-utility-push('under-verylarge'); 64 | } 65 | 66 | @include viewport-over-verylarge { 67 | @include toolchain-utility-push('over-verylarge'); 68 | } 69 | 70 | 71 | -------------------------------------------------------------------------------- /src/styles/mixins/mixin-limit.scss: -------------------------------------------------------------------------------- 1 | 2 | @import './mixin-viewport'; 3 | 4 | 5 | 6 | // Limit width of an element when viewport is big enough 7 | 8 | @mixin limit ($breakpoint, $limit: $breakpoint) { 9 | @include viewport-over($breakpoint) { 10 | max-width: $limit; 11 | } 12 | } 13 | 14 | @mixin limit-max { 15 | max-width: 100%; 16 | } 17 | 18 | @mixin limit-tiny { 19 | @include viewport-over-tiny { 20 | max-width: $limit-tiny; 21 | } 22 | } 23 | 24 | @mixin limit-small { 25 | @include viewport-over-small { 26 | max-width: $limit-small; 27 | } 28 | } 29 | 30 | @mixin limit-smallish { 31 | @include viewport-over-smallish { 32 | max-width: $limit-smallish; 33 | } 34 | } 35 | 36 | @mixin limit-medium { 37 | @include viewport-over-medium { 38 | max-width: $limit-medium; 39 | } 40 | } 41 | 42 | @mixin limit-large { 43 | @include viewport-over-large { 44 | max-width: $limit-large; 45 | } 46 | } 47 | 48 | @mixin limit-verylarge { 49 | @include viewport-over-verylarge { 50 | max-width: $limit-verylarge; 51 | } 52 | } 53 | 54 | @mixin limit-ridiculous { 55 | @include viewport-over-ridiculous { 56 | max-width: $limit-ridiculous; 57 | } 58 | } 59 | 60 | @mixin limit-totallyridiculous { 61 | @include viewport-over-totallyridiculous { 62 | max-width: $limit-totallyridiculous; 63 | } 64 | } 65 | 66 | @mixin no-limit { 67 | max-width: none; 68 | } 69 | 70 | 71 | 72 | // Limit height of an element when viewport is big enough 73 | 74 | @mixin limit-height ($breakpoint, $limit: $breakpoint) { 75 | @include viewport-height-over($breakpoint) { 76 | max-height: $limit; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /tooling/env/index.js: -------------------------------------------------------------------------------- 1 | 2 | // see http://vuejs-templates.github.io/webpack for documentation. 3 | var path = require('path') 4 | 5 | module.exports = { 6 | build: { 7 | env: require('./prod.env'), 8 | index: path.resolve(__dirname, '../../dist/index.html'), 9 | assetsRoot: path.resolve(__dirname, '../../dist'), 10 | assetsSubDirectory: 'static', 11 | assetsPublicPath: '/', 12 | productionSourceMap: true, 13 | // Gzip off by default as many popular static hosts such as 14 | // Surge or Netlify already gzip all static assets for you. 15 | // Before setting to `true`, make sure to: 16 | // npm install --save-dev compression-webpack-plugin 17 | productionGzip: false, 18 | productionGzipExtensions: ['js', 'css'], 19 | // Run the build command with an extra argument to 20 | // View the bundle analyzer report after build finishes: 21 | // `npm run build --report` 22 | // Set to `true` or `false` to always turn it on or off 23 | bundleAnalyzerReport: process.env.npm_config_report 24 | }, 25 | dev: { 26 | env: require('./dev.env'), 27 | port: 8888, 28 | autoOpenBrowser: true, 29 | assetsSubDirectory: 'static', 30 | assetsPublicPath: '/', 31 | proxyTable: {}, 32 | // CSS Sourcemaps off by default because relative paths are "buggy" 33 | // with this option, according to the CSS-Loader README 34 | // (https://github.com/webpack/css-loader#sourcemaps) 35 | // In our experience, they generally work as expected, 36 | // just be aware of this issue when enabling this option. 37 | cssSourceMap: false 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/components/snippets/PicSvg.vue: -------------------------------------------------------------------------------- 1 | 2 | 60 | 61 | 69 | 70 | 74 | -------------------------------------------------------------------------------- /src/util/composeClassnames.js: -------------------------------------------------------------------------------- 1 | import { isNumber, isString, kebabCase } from 'lodash'; 2 | 3 | // Generate HTML/CSS class names based on a set of state, with prefixes and negatives added 4 | export default function (stateHash, prefixPositive, prefixNegative) { 5 | var classes = []; 6 | 7 | // Custom prefixes 8 | if (prefixPositive) { 9 | 10 | // Only positive prefix was passed, using it as the base for negative as well 11 | if (!prefixNegative) { 12 | prefixNegative = prefixPositive + '-not'; 13 | } 14 | 15 | // Default prefixes 16 | } else { 17 | prefixPositive = 'is'; 18 | prefixPositive = 'not'; 19 | } 20 | 21 | // State classes 22 | for (var key in stateHash) { 23 | var className; 24 | 25 | // String/number value goes into the class name 26 | if (isString(stateHash[key]) || isNumber(stateHash[key])) { 27 | className = key + '-' + stateHash[key]; 28 | 29 | // Otherwise we use boolean classnames 30 | } else { 31 | 32 | // Prevent duplicating prefixes if they're passed in the keys 33 | if (key.substr(0, prefixPositive.length) === prefixPositive) { 34 | className = key.substr(prefixPositive.length); 35 | 36 | } else if (key.substr(0, prefixPositive.length) === prefixPositive) { 37 | className = key.substr(prefixPositive.length); 38 | 39 | // Nothing to sanitize 40 | } else { 41 | className = key; 42 | } 43 | 44 | } 45 | 46 | // Compose prefix + value 47 | // Turn into kebab-case 48 | // Push into results array 49 | classes.push(kebabCase((stateHash[key] ? prefixPositive : prefixNegative) + '-' + className)); 50 | 51 | } 52 | 53 | return classes; 54 | } 55 | -------------------------------------------------------------------------------- /src/.htmllintrc: -------------------------------------------------------------------------------- 1 | { 2 | "attr-bans": [ 3 | "align", 4 | "background", 5 | "bgcolor", 6 | "border", 7 | "dynsrc", 8 | "id", 9 | "frameborder", 10 | "longdesc", 11 | "lowsrc", 12 | "onclick", 13 | "ondblclick", 14 | "onload", 15 | "marginwidth", 16 | "marginheight", 17 | "scrolling", 18 | "style", 19 | "width" 20 | ], 21 | "attr-name-ignore-regex": false, 22 | "attr-name-style": false, 23 | "attr-no-dup": true, 24 | "attr-no-unsafe-char": false, 25 | "attr-quote-style": "double", 26 | "attr-req-value": false, 27 | "class-no-dup": true, 28 | "class-style": "dash", 29 | "csslint": false, 30 | "doctype-first": false, 31 | "doctype-html5": false, 32 | "fig-req-figcaption": true, 33 | "focusable-tabindex-style": false, 34 | "head-req-title": true, 35 | "href-style": false, 36 | "html-req-lang": true, 37 | "id-class-ignore-regex": false, 38 | "id-class-no-ad": true, 39 | "id-class-style": "dash", 40 | "id-no-dup": true, 41 | "input-radio-req-name": true, 42 | "img-req-alt": false, 43 | "img-req-src": false, 44 | "indent-style": "tabs", 45 | "indent-width-cont": true, 46 | "input-req-label": false, 47 | "label-req-for": false, 48 | "lang-style": "case", 49 | "line-end-style": "lf", 50 | "line-max-len": false, 51 | "line-max-len-ignore-regex": "/href/g", 52 | "page-title": true, 53 | "spec-char-escape": false, 54 | "table-req-caption": false, 55 | "table-req-header": false, 56 | "tag-bans": [ 57 | "b", 58 | "i", 59 | "keygen" 60 | ], 61 | "tag-close": true, 62 | "tag-name-lowercase": true, 63 | "tag-name-match": true, 64 | "tag-self-close": "never", 65 | "title-max-len": 60, 66 | "title-no-dup": true 67 | } 68 | -------------------------------------------------------------------------------- /src/svg/power.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | svg/power 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/services/env.js: -------------------------------------------------------------------------------- 1 | // Env service 2 | // 3 | // - OS detection 4 | // - Feature detection 5 | // - Dev/prod handling 6 | // - Access to current remote 7 | import Vue from 'vue'; 8 | 9 | import { Remote } from '@models'; 10 | import config from '@config'; 11 | 12 | const macosPlatforms = ['macintosh', 'macintel', 'macppc', 'mac68k']; 13 | const windowsPlatforms = ['win32', 'win64', 'windows', 'wince']; 14 | const iosPlatforms = ['iphone', 'ipad', 'ipod']; 15 | 16 | export default new Vue({ 17 | 18 | data: function () { 19 | return { 20 | 21 | os: this.getOsFromNavigator(window.navigator.userAgent, window.navigator.platform), 22 | 23 | currentRemote: new Remote({ 24 | propsData: config.remotes[config.currentRemote] 25 | }) 26 | 27 | }; 28 | }, 29 | 30 | computed: { 31 | 32 | isDevelopment: function () { 33 | return config.env === 'development'; 34 | }, 35 | 36 | isProduction: function () { 37 | return !this.isDevelopment; 38 | } 39 | 40 | }, 41 | 42 | methods: { 43 | 44 | // OS checking 45 | // NOTE: platform detection is never 100 % reliable 46 | getOsFromNavigator: function (userAgent, platform) { 47 | userAgent = userAgent.toLowerCase(); 48 | platform = platform.toLowerCase(); 49 | 50 | if (macosPlatforms.indexOf(platform) !== -1) { 51 | return 'mac'; 52 | 53 | } else if (iosPlatforms.indexOf(platform) !== -1) { 54 | return 'ios'; 55 | 56 | } else if (windowsPlatforms.indexOf(platform) !== -1) { 57 | return 'windows'; 58 | 59 | } else if (new RegExp('Android').test(userAgent)) { 60 | return 'android'; 61 | 62 | } else if (new RegExp('Linux').test(platform)) { 63 | return 'linux'; 64 | } 65 | 66 | return null; 67 | } 68 | 69 | } 70 | 71 | }); 72 | -------------------------------------------------------------------------------- /src/styles/webfonts/webfont-source-sans.scss: -------------------------------------------------------------------------------- 1 | @import '~@shared-styles'; 2 | 3 | @include font-face ( 4 | 'Source Sans', 5 | 200, 6 | normal, 7 | '~@fonts/source-sans/sourcesanspro-extralight-webfont' 8 | ); 9 | 10 | @include font-face ( 11 | 'Source Sans', 12 | 200, 13 | italic, 14 | '~@fonts/source-sans/sourcesanspro-extralightitalic-webfont' 15 | ); 16 | 17 | @include font-face ( 18 | 'Source Sans', 19 | 300, 20 | normal, 21 | '~@fonts/source-sans/sourcesanspro-light-webfont' 22 | ); 23 | 24 | @include font-face ( 25 | 'Source Sans', 26 | 300, 27 | italic, 28 | '~@fonts/source-sans/sourcesanspro-lightitalic-webfont' 29 | ); 30 | 31 | @include font-face ( 32 | 'Source Sans', 33 | 400, 34 | normal, 35 | '~@fonts/source-sans/sourcesanspro-regular-webfont' 36 | ); 37 | 38 | @include font-face ( 39 | 'Source Sans', 40 | 400, 41 | italic, 42 | '~@fonts/source-sans/sourcesanspro-italic-webfont' 43 | ); 44 | 45 | @include font-face ( 46 | 'Source Sans', 47 | 500, 48 | normal, 49 | '~@fonts/source-sans/sourcesanspro-semibold-webfont' 50 | ); 51 | 52 | @include font-face ( 53 | 'Source Sans', 54 | 500, 55 | italic, 56 | '~@fonts/source-sans/sourcesanspro-semibolditalic-webfont' 57 | ); 58 | 59 | @include font-face ( 60 | 'Source Sans', 61 | 600, 62 | normal, 63 | '~@fonts/source-sans/sourcesanspro-bold-webfont' 64 | ); 65 | 66 | @include font-face ( 67 | 'Source Sans', 68 | 600, 69 | italic, 70 | '~@fonts/source-sans/sourcesanspro-bolditalic-webfont' 71 | ); 72 | 73 | @include font-face ( 74 | 'Source Sans', 75 | 700, 76 | normal, 77 | '~@fonts/source-sans/sourcesanspro-black-webfont' 78 | ); 79 | 80 | @include font-face ( 81 | 'Source Sans', 82 | 700, 83 | italic, 84 | '~@fonts/source-sans/sourcesanspro-blackitalic-webfont' 85 | ); 86 | -------------------------------------------------------------------------------- /tooling/e2e/runner.js: -------------------------------------------------------------------------------- 1 | // 1. start the dev server using production config 2 | process.env.NODE_ENV = 'testing' 3 | var server = require('../../tooling/dev-server.js') 4 | 5 | // Select which driver to use based on environment variable passed 6 | var useSelenium = false 7 | if (process.env.DRIVER) { 8 | var param = ('' + process.env.DRIVER).toLowerCase() 9 | if (param === 'selenium') { 10 | useSelenium = true 11 | } else if (param !== 'chrome') { 12 | throw Error('Unknown driver requested. Supported drivers are "Selenium" and "Chrome".') 13 | } 14 | } 15 | 16 | // Drivers have different config files 17 | var testConfigFilePath = 'nightwatch.chrome.conf.js' 18 | if (useSelenium) { 19 | testConfigFilePath = 'nightwatch.conf.js' 20 | } 21 | 22 | server.ready.then(() => { 23 | // 2. run the nightwatch test suite against it 24 | // to run in additional browsers: 25 | // 1. add an entry in tooling/e2e/nightwatch.conf.js under "test_settings" 26 | // 2. add it to the --env flag below 27 | // or override the environment flag, for example: `npm run e2e -- --env chrome,firefox` 28 | // For more information on Nightwatch's config file, see 29 | // http://nightwatchjs.org/guide#settings-file 30 | var opts = process.argv.slice(2) 31 | if (opts.indexOf('--config') === -1) { 32 | opts = opts.concat(['--config', 'tooling/e2e/' + testConfigFilePath]) 33 | } 34 | if (opts.indexOf('--env') === -1) { 35 | opts = opts.concat(['--env', 'chrome']) 36 | } 37 | 38 | var spawn = require('cross-spawn') 39 | var runner = spawn('./node_modules/.bin/nightwatch', opts, { stdio: 'inherit' }) 40 | 41 | runner.on('exit', function (code) { 42 | server.close() 43 | process.exit(code) 44 | }) 45 | 46 | runner.on('error', function (err) { 47 | server.close() 48 | throw err 49 | }) 50 | }) 51 | -------------------------------------------------------------------------------- /src/components/pages/PageHome.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 37 | 38 | 42 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | 2 | // FIXME 3 | // 4 | // Obviously this should be split into smaller modules and multiple files 5 | 6 | 7 | 8 | // State (similar to `data` in Vue) 9 | export const state = { 10 | counter: 1 11 | }; 12 | 13 | // Dynamic getters (similar to `computed` in Vue) 14 | export const getters = { 15 | 16 | canDecrementCounter: function (state, getters) { 17 | return (state.counter > 1) ? true : false; 18 | }, 19 | 20 | counterSquared: function (state, getters) { 21 | return Math.pow(state.counter, 2); 22 | } 23 | 24 | }; 25 | 26 | // Mutations (similar to atomic methods under `methods` in Vue) 27 | // Use these in actions with `context.commit('increment')` 28 | export const mutations = { 29 | 30 | 'INCREMENT_COUNTER': function (state) { 31 | state.counter++; 32 | }, 33 | 34 | 'DECREMENT_COUNTER': function (state) { 35 | state.counter--; 36 | } 37 | 38 | }; 39 | 40 | // Actions (similar to helpers under `methods` in Vue) 41 | // Use these in components with `this.$store.dispatch('increment')` 42 | // Context includes actions and getters 43 | export const actions = { 44 | 45 | increment: function (context) { 46 | context.commit('INCREMENT_COUNTER'); 47 | }, 48 | 49 | decrement: function (context) { 50 | if (context.getters.canDecrementCounter) { 51 | context.commit('DECREMENT_COUNTER'); 52 | } 53 | }, 54 | 55 | // Increment counter 56 | incrementAsync: function (context) { 57 | var callback = function () { 58 | 59 | // NOTE: this is the action, not mutation 60 | context.dispatch('increment'); 61 | 62 | }; 63 | setTimeout(callback, 200); 64 | } 65 | 66 | }; 67 | 68 | // State management split into smaller chunks 69 | export const modules = {}; 70 | 71 | // Export all parts as one object 72 | export const store = { 73 | state: state, 74 | getters: getters, 75 | mutations: mutations, 76 | actions: actions, 77 | modules: modules 78 | }; 79 | -------------------------------------------------------------------------------- /src/components/sections/BlankState.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 59 | 60 | 89 | -------------------------------------------------------------------------------- /src/svg/app-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | app-icon 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/components/counters/GlobalCounterIterator.vue: -------------------------------------------------------------------------------- 1 | 2 | 58 | 59 | 76 | 77 | 85 | -------------------------------------------------------------------------------- /src/services/auth.js: -------------------------------------------------------------------------------- 1 | // Auth service 2 | // 3 | // - Current login status 4 | import Vue from 'vue'; 5 | 6 | import api from './api'; 7 | import config from '@config'; 8 | 9 | export default new Vue({ 10 | 11 | data: function () { 12 | return { 13 | maxAccessLevel: config.routePermissionRoles.length, 14 | currentAccount: null, 15 | isLoading: false 16 | }; 17 | }, 18 | 19 | computed: { 20 | 21 | isConfirmed: function () { 22 | return this.currentAccount ? true : false; 23 | } 24 | 25 | }, 26 | 27 | methods: { 28 | 29 | // Privileges checking 30 | 31 | isAdmin: function () { 32 | return this.isConfirmed && this.currentAccount.isAdmin; 33 | }, 34 | 35 | canAccessRoute: function (routeName) { 36 | let routeLevel = config.routePermissions[routeName]; 37 | if ( 38 | // 0 means guests can access 39 | !routeLevel || 40 | 41 | // level is > 0, user with at least matching level must be set 42 | (this.isConfirmed && routeLevel <= this.currentAccount.accessLevel) 43 | ) { 44 | return true; 45 | } 46 | return false; 47 | }, 48 | 49 | 50 | 51 | // Log in or out 52 | 53 | unsetCurrentAccount: function () { 54 | if (this.currentAccount) { 55 | this.currentAccount = null; 56 | } 57 | }, 58 | 59 | confirmAuthentication: function (id) { 60 | this.isLoading = true; 61 | 62 | // Return promise 63 | let service = this; 64 | return api.getAuth( 65 | 66 | // Success callback 67 | function (response) { 68 | // console.log('auth service: confirmed authentication OK', response); 69 | service.currentAccount = response.data.account; 70 | }, 71 | 72 | // Error callback 73 | function (error) { 74 | console.log('auth service: ERROR confirming authentication', error); 75 | }, 76 | 77 | // Always callback 78 | function () { 79 | service.isLoading = false; 80 | } 81 | 82 | ); 83 | 84 | } 85 | 86 | }, 87 | 88 | created: function () { 89 | this.confirmAuthentication(); 90 | } 91 | 92 | }); 93 | -------------------------------------------------------------------------------- /src/styles/utilities-base/utility-pad.scss: -------------------------------------------------------------------------------- 1 | 2 | @import '~@shared-styles'; 3 | 4 | 5 | 6 | .pad { 7 | @include pad; 8 | } 9 | 10 | 11 | 12 | .pad-horizontal { 13 | @include pad-horizontal; 14 | } 15 | .pad-vertical { 16 | @include pad-vertical; 17 | } 18 | 19 | 20 | 21 | .pad-even { 22 | @include pad-even; 23 | } 24 | 25 | .pad-even-max { 26 | @include pad-even-max; 27 | } 28 | 29 | 30 | 31 | .pad-top { 32 | @include pad-top; 33 | } 34 | 35 | .pad-right { 36 | @include pad-right; 37 | } 38 | 39 | .pad-bottom { 40 | @include pad-bottom; 41 | } 42 | 43 | .pad-left { 44 | @include pad-left; 45 | } 46 | 47 | 48 | 49 | // Tight 50 | 51 | .pad-tight { 52 | @include pad-tight; 53 | } 54 | 55 | 56 | 57 | .pad-tight-horizontal { 58 | @include pad-tight-horizontal; 59 | } 60 | .pad-tight-vertical { 61 | @include pad-tight-vertical; 62 | } 63 | 64 | 65 | 66 | .pad-tight-even { 67 | @include pad-tight-even; 68 | } 69 | 70 | .pad-tight-even-max { 71 | @include pad-tight-even-max; 72 | } 73 | 74 | 75 | 76 | .pad-tight-top { 77 | @include pad-tight-top; 78 | } 79 | 80 | .pad-tight-right { 81 | @include pad-tight-right; 82 | } 83 | 84 | .pad-tight-bottom { 85 | @include pad-tight-bottom; 86 | } 87 | 88 | .pad-tight-left { 89 | @include pad-tight-left; 90 | } 91 | 92 | 93 | 94 | // Loose 95 | 96 | .pad-loose { 97 | @include pad-loose; 98 | } 99 | 100 | 101 | 102 | .pad-loose-horizontal { 103 | @include pad-loose-horizontal; 104 | } 105 | 106 | .pad-loose-vertical { 107 | @include pad-loose-vertical; 108 | } 109 | 110 | 111 | 112 | .pad-loose-even { 113 | @include pad-loose-even; 114 | } 115 | 116 | .pad-loose-even-max { 117 | @include pad-loose-even-max; 118 | } 119 | 120 | 121 | 122 | .pad-loose-top { 123 | @include pad-loose-top; 124 | } 125 | 126 | .pad-loose-right { 127 | @include pad-loose-right; 128 | } 129 | 130 | .pad-loose-bottom { 131 | @include pad-loose-bottom; 132 | } 133 | 134 | .pad-loose-left { 135 | @include pad-loose-left; 136 | } 137 | -------------------------------------------------------------------------------- /src/components/controls/Click.vue: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 83 | 84 | 87 | 88 | 91 | -------------------------------------------------------------------------------- /src/components/transitions/CustomTransition.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 61 | 62 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /src/styles/mixins/mixin-hide.scss: -------------------------------------------------------------------------------- 1 | 2 | @import './mixin-viewport'; 3 | 4 | 5 | 6 | // Super simple base mixin 7 | 8 | @mixin hide { 9 | display: none; 10 | } 11 | 12 | 13 | 14 | // Print 15 | 16 | @mixin hide-in-print { 17 | @include viewport-print { 18 | display: none; 19 | } 20 | } 21 | 22 | @mixin hide-in-not-print { 23 | @include viewport-not-print { 24 | display: none; 25 | } 26 | } 27 | 28 | 29 | 30 | // Orientation 31 | 32 | @mixin hide-in-landscape { 33 | @include viewport-landscape { 34 | display: none; 35 | } 36 | } 37 | 38 | @mixin hide-in-portrait { 39 | @include viewport-portrait { 40 | display: none; 41 | } 42 | } 43 | 44 | 45 | 46 | // Breakpoints 47 | 48 | @mixin hide-under-tiny { 49 | @include viewport-under-tiny { 50 | display: none; 51 | } 52 | } 53 | 54 | @mixin hide-over-tiny { 55 | @include viewport-over-tiny { 56 | display: none; 57 | } 58 | } 59 | 60 | @mixin hide-under-small { 61 | @include viewport-under-small { 62 | display: none; 63 | } 64 | } 65 | 66 | @mixin hide-over-small { 67 | @include viewport-over-small { 68 | display: none; 69 | } 70 | } 71 | 72 | @mixin hide-under-smallish { 73 | @include viewport-under-smallish { 74 | display: none; 75 | } 76 | } 77 | 78 | @mixin hide-over-smallish { 79 | @include viewport-over-smallish { 80 | display: none; 81 | } 82 | } 83 | 84 | @mixin hide-under-medium { 85 | @include viewport-under-medium { 86 | display: none; 87 | } 88 | } 89 | 90 | @mixin hide-over-medium { 91 | @include viewport-over-medium { 92 | display: none; 93 | } 94 | } 95 | 96 | @mixin hide-under-large { 97 | @include viewport-under-large { 98 | display: none; 99 | } 100 | } 101 | 102 | @mixin hide-over-large { 103 | @include viewport-over-large { 104 | display: none; 105 | } 106 | } 107 | 108 | @mixin hide-under-verylarge { 109 | @include viewport-under-verylarge { 110 | display: none; 111 | } 112 | } 113 | 114 | @mixin hide-over-verylarge { 115 | @include viewport-over-verylarge { 116 | display: none; 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/services/menu.js: -------------------------------------------------------------------------------- 1 | // Menu service 2 | // Offers up the main menu structure for the current user that can be used by any component 3 | import Vue from 'vue'; 4 | import { filter, isUndefined, merge } from 'lodash'; 5 | 6 | import { auth } from '@services'; 7 | // import config from '@config'; 8 | 9 | export default new Vue({ 10 | 11 | data: function () { 12 | return { 13 | links: [ 14 | 15 | // Home link 16 | { 17 | label: 'Home', 18 | route: { 19 | name: 'root' 20 | } 21 | }, 22 | 23 | // Category link 24 | { 25 | label: 'Posts', 26 | // keepParams: [], 27 | route: { 28 | name: 'posts', 29 | params: { 30 | // page: 1 31 | } 32 | } 33 | } 34 | 35 | ] 36 | }; 37 | }, 38 | 39 | computed: { 40 | 41 | accessibleLinks: function () { 42 | return filter(this.links, function (link) { 43 | return auth.canAccessRoute(link.route.name) ? true : false; 44 | }); 45 | }, 46 | 47 | accessibleLinksWithoutRoot: function () { 48 | var links = this.accessibleLinks; 49 | 50 | // Look for root link in all links 51 | for (var i = 0; i < links.length; i++) { 52 | var link = links[i]; 53 | 54 | // Match based on the named route name 55 | if (link.route.name === 'root') { 56 | 57 | // Remove link 58 | links.splice(i, 1); 59 | 60 | // Only removing the first root link, since we shouldn't have more 61 | break; 62 | } 63 | 64 | } 65 | 66 | return links; 67 | } 68 | 69 | }, 70 | 71 | methods: { 72 | 73 | composeRouterLink: function (link, currentRoute) { 74 | var linkParams = link.route.params ? link.route.params : {}; 75 | 76 | var keepParams = {}; 77 | if (link.keepParams) { 78 | 79 | // Some URLs might be defined in URL and we want to keep them 80 | for (var i = 0; i < link.keepParams.length; i++) { 81 | var paramName = link.keepParams[i]; 82 | if (!isUndefined(currentRoute.params[paramName])) { 83 | keepParams[paramName] = currentRoute.params[paramName]; 84 | } 85 | } 86 | 87 | } 88 | 89 | return { 90 | name: link.route.name, 91 | params: merge(keepParams, linkParams) 92 | }; 93 | 94 | } 95 | 96 | } 97 | 98 | }); 99 | -------------------------------------------------------------------------------- /spec/util/trimWhitespace.spec.js: -------------------------------------------------------------------------------- 1 | import { trimWhitespace } from '@util'; 2 | 3 | // Test cases 4 | // These will be tested at the start, end and in the middle of the string 5 | const whitespaceStrings = { 6 | space: ' ', 7 | spaces: ' ', 8 | tab: '\t', 9 | tabAndSpace1: ' ' + '\t', 10 | tabAndSpace2: ' ' + '\t', 11 | tabAndSpace3: ' ' + '\t' + ' ', 12 | tabAndMultipleSpaces1: ' ' + '\t', 13 | tabAndMultipleSpaces2: ' ' + '\t', 14 | tabAndMultipleSpaces3: ' ' + '\t' + ' ', 15 | tabAndMultipleSpacesAndTabs1: ' ' + '\t', 16 | tabAndMultipleSpacesAndTabs2: ' ' + '\t', 17 | tabAndMultipleSpacesAndTabs3: ' ' + '\t' + ' ', 18 | newline: '\n', 19 | newlines: '\n\n\n' 20 | }; 21 | 22 | describe('Util trimWhitespace with trailing whitespace', function () { 23 | 24 | // Expected result is the same for all these test cases 25 | const expectedResult = 'Foooo'; 26 | 27 | // Test this with each of the test cases provided above 28 | for (let key in whitespaceStrings) { 29 | it('should trim ' + key, function () { 30 | 31 | // Whitespace is at the end of string 32 | expect(trimWhitespace(expectedResult + whitespaceStrings[key])).to.equal(expectedResult); 33 | 34 | }); 35 | } 36 | 37 | }); 38 | 39 | describe('Util trimWhitespace with leading whitespace', function () { 40 | 41 | // Expected result is the same for all these test cases 42 | const expectedResult = 'Foooo'; 43 | 44 | // Test this with each of the test cases provided above 45 | for (let key in whitespaceStrings) { 46 | it('should trim ' + key, function () { 47 | 48 | // Whitespace is at the start of string 49 | expect(trimWhitespace(whitespaceStrings[key] + expectedResult)).to.equal(expectedResult); 50 | 51 | }); 52 | } 53 | 54 | }); 55 | 56 | describe('Util trimWhitespace with excess whitespace', function () { 57 | 58 | // Expected result is the same for all these test cases 59 | const partOne = 'Foo'; 60 | const partTwo = 'oo'; 61 | const expectedResult = partOne + ' ' + partTwo; 62 | 63 | // Test this with each of the test cases provided above 64 | for (let key in whitespaceStrings) { 65 | it('should trim ' + key, function () { 66 | 67 | // Whitespace is injected in the middle 68 | expect(trimWhitespace(partOne + whitespaceStrings[key] + partTwo)).to.equal(expectedResult); 69 | 70 | }); 71 | } 72 | 73 | }); 74 | -------------------------------------------------------------------------------- /src/components/counters/Counter.vue: -------------------------------------------------------------------------------- 1 | 2 | 14 | 15 | 78 | 79 | 87 | 88 | 101 | -------------------------------------------------------------------------------- /tooling/unit/karma.conf.js: -------------------------------------------------------------------------------- 1 | // This is a karma config file. For more details see 2 | // http://karma-runner.github.io/0.13/config/configuration-file.html 3 | // we are also using it with karma-webpack 4 | // https://github.com/webpack/karma-webpack 5 | 6 | var webpackConfig = require('../../tooling/webpack.test.conf') 7 | 8 | module.exports = function (config) { 9 | config.set({ 10 | // to run in additional browsers: 11 | // 1. install corresponding karma launcher 12 | // http://karma-runner.github.io/0.13/config/browsers.html 13 | // 2. add it to the `browsers` array below. 14 | browsers: [ 15 | // 'Chrome', 16 | 'PhantomJS' 17 | ], 18 | frameworks: ['mocha', 'sinon-chai', 'phantomjs-shim'], 19 | reporters: ['spec', 'coverage', 'html'], 20 | files: [ 21 | 22 | // Required to suppress issue with ES6 Promise polyfill missing in PhantomJS 23 | '../../node_modules/babel-polyfill/dist/polyfill.js', 24 | 25 | // Required to suppress weird issues when constructing new instances of objects 26 | '../../node_modules/phantomjs-polyfill-object-assign/object-assign-polyfill.js', 27 | 28 | './index.js' 29 | ], 30 | preprocessors: { 31 | './index.js': ['webpack', 'sourcemap'] 32 | }, 33 | webpack: webpackConfig, 34 | webpackMiddleware: { 35 | noInfo: true 36 | }, 37 | coverageReporter: { 38 | dir: '../../reports', 39 | reporters: [ 40 | { type: 'lcov', subdir: '.' }, 41 | { type: 'text-summary' } 42 | ] 43 | }, 44 | // the default configuration 45 | htmlReporter: { 46 | outputDir: 'reports', // where to put the reports 47 | templatePath: null, // set if you moved jasmine_template.html 48 | focusOnFailures: true, // reports show failures on start 49 | namedFiles: true, // name files instead of creating sub-directories 50 | pageTitle: 'Unit test report', // page title for reports; browser info by default 51 | urlFriendlyName: true, // simply replaces spaces with _ for files/dirs 52 | reportName: 'unit', // report summary filename; browser info by default 53 | 54 | // experimental 55 | preserveDescribeNesting: false, // folded suites stay folded 56 | foldAll: false // reports start folded (only with preserveDescribeNesting) 57 | } 58 | 59 | }) 60 | } 61 | -------------------------------------------------------------------------------- /src/components/snippets/Spinner.vue: -------------------------------------------------------------------------------- 1 | 2 | 57 | 58 | 75 | 76 | 115 | -------------------------------------------------------------------------------- /tooling/custom-config.js: -------------------------------------------------------------------------------- 1 | 2 | // Utility file to normalise custom configuration for tooling 3 | // NOTE: This utility is meant only for Webpack tooling, not application code 4 | 5 | var _ = require('lodash') 6 | var path = require('path') 7 | 8 | // Get base custom project configuration values 9 | var aliases = require('../src/config/config.aliases.js'); 10 | var values = require('../src/config/config.base.js'); 11 | var devValues = require('../src/config/config.dev.base.js'); 12 | if (process.env.NODE_ENV === 'development') { 13 | values = _.merge(values, devValues); 14 | } 15 | 16 | // Path helper 17 | // FIXME: duplicated from webpack.base.conf.js 18 | function resolve (dir) { 19 | return path.join(__dirname, '..', dir) 20 | } 21 | 22 | 23 | 24 | // Base values 25 | var normalized = { 26 | 27 | // No action needed 28 | meta: Object.assign({}, values.meta), 29 | svgSpritePath: values.svgSpritePath, 30 | compileAppIcons: values.compileAppIcons, 31 | appIconPlatforms: values.appIconPlatforms, 32 | robotsTxt: values.robotsTxt, 33 | webAppManifest: values.webAppManifest, 34 | 35 | // Prefix some paths with src 36 | appIconSourceFile: resolve('src/' + values.appIconSourceFile), 37 | 38 | // Basics needed for tooling 39 | customAliases: { 40 | 'vue$': 'vue/dist/vue.esm.js', 41 | } 42 | 43 | }; 44 | 45 | // https://github.com/NekR/offline-plugin/blob/master/docs/options.md 46 | if (values.offlineCache && values.offlineCache.enabled) { 47 | normalized.offlineCache = { 48 | caches: 'all', 49 | responseStrategy: values.offlineCache.responseStrategy, 50 | updateStrategy: values.offlineCache.updateStrategy, 51 | externals: [] 52 | } 53 | } 54 | // https://github.com/NekR/offline-plugin/blob/master/docs/options.md 55 | 56 | // We use a separate plugin for favicons 57 | normalized.appIconPlatforms.favicons = false; 58 | 59 | // SVGO wants its configuration values in a really weird format 60 | // https://github.com/karify/external-svg-sprite-loader/blob/master/index.js 61 | normalized.svgo = { 62 | plugins: [] 63 | }; 64 | for (var key in values.svgo) { 65 | var item = {}; 66 | item[key] = values.svgo[key]; 67 | normalized.svgo.plugins.push(item); 68 | } 69 | 70 | // Treat aliases with resolve helper and merge with other configuration 71 | // This will do '@styles': resolve('src/styles') 72 | for (var key in aliases) { 73 | normalized.customAliases[key] = resolve(aliases[key]); 74 | } 75 | 76 | 77 | 78 | // Export final values 79 | module.exports = normalized; 80 | -------------------------------------------------------------------------------- /src/components/containers/Page.vue: -------------------------------------------------------------------------------- 1 | 2 | 52 | 53 | 60 | 61 | 118 | -------------------------------------------------------------------------------- /spec/util/getDomainName.spec.js: -------------------------------------------------------------------------------- 1 | import { getDomainName } from '@util'; 2 | 3 | // Expected result is the same for all these test cases 4 | const expectedResult = 'github.com'; 5 | 6 | // Test cases 7 | const stringsWithDomainName = { 8 | bare: 'github.com', 9 | bareWithProtocol: 'http://github.com', 10 | bareWithProtocolHttps: 'https://github.com', 11 | bareWithProtocolFtp: 'ftp://github.com', 12 | bareWithSlash: 'github.com/foo', 13 | bareWithSlashes: 'github.com/foo/bar', 14 | bareWithQueryParameter: 'github.com?foo', 15 | bareWithQueryParameters: 'github.com?foo&bar', 16 | bareWithQueryParametersAndValues: 'github.com?foo&bar=123', 17 | bareWithSlashesAndQueryParameter: 'github.com/foo?foo', 18 | bareWithSlashesAndQueryParameters: 'github.com/foo?foo&bar', 19 | bareWithSlashesAndQueryParametersAndValues: 'github.com/foo?foo&bar=123', 20 | bareWithProtocolAndSlash: 'http://github.com/foo', 21 | bareWithProtocolAndSlashes: 'http://github.com/foo/bar', 22 | bareWithProtocolAndQueryParameter: 'http://github.com?foo', 23 | bareWithProtocolAndQueryParameters: 'http://github.com?foo&bar', 24 | bareWithProtocolAndQueryParametersAndValues: 'http://github.com?foo&bar=123', 25 | bareWithProtocolAndSlashesAndQueryParameter: 'http://github.com/foo?foo', 26 | bareWithProtocolAndSlashesAndQueryParameters: 'http://github.com/foo?foo&bar', 27 | bareWithProtocolAndSlashesAndQueryParametersAndValues: 'http://github.com/foo?foo&bar=123', 28 | bareWithLeadingSpace: ' github.com', 29 | bareWithTrailingSpace: 'github.com ', 30 | leadingSpaceWithProtocolAndSlashesAndQueryParametersAndValues: ' http://github.com/foo?foo&bar=123' 31 | }; 32 | 33 | const stringsWithSubDomainUnchanged = { 34 | oneSubDomain: 'foo.github.com', 35 | twoSubDomains: 'foo.bar.github.com' 36 | }; 37 | 38 | describe('Util getDomainName', function () { 39 | 40 | // Test this with each of the test cases provided above 41 | for (let key in stringsWithDomainName) { 42 | it('should extract domain from ' + key, function () { 43 | expect(getDomainName(stringsWithDomainName[key])).to.equal(expectedResult); 44 | }); 45 | } 46 | 47 | }); 48 | 49 | describe('Util getDomainName with subdomains', function () { 50 | 51 | // Test this with each of the test cases provided above 52 | for (let key in stringsWithSubDomainUnchanged) { 53 | it('should extract domain with subdomain from ' + key, function () { 54 | expect(getDomainName(stringsWithSubDomainUnchanged[key])).to.equal(stringsWithSubDomainUnchanged[key]); 55 | }); 56 | } 57 | 58 | }); 59 | --------------------------------------------------------------------------------