Initial commit
20
.babelrc
Executable file
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"presets": [
|
||||
[
|
||||
"env", {
|
||||
"modules": false,
|
||||
"targets": {
|
||||
"browsers": ["> 1%", "last 2 versions", "not ie <= 8"]
|
||||
}
|
||||
}
|
||||
],
|
||||
"stage-2"
|
||||
],
|
||||
"plugins": ["transform-vue-jsx", "transform-runtime"],
|
||||
"env": {
|
||||
"test": {
|
||||
"presets": ["env", "stage-2"],
|
||||
"plugins": ["transform-vue-jsx", "istanbul"]
|
||||
}
|
||||
}
|
||||
}
|
9
.editorconfig
Executable file
|
@ -0,0 +1,9 @@
|
|||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
5
.eslintignore
Executable file
|
@ -0,0 +1,5 @@
|
|||
/build/
|
||||
/config/
|
||||
/dist/
|
||||
/*.js
|
||||
/test/unit/coverage/
|
76
.eslintrc.js
Executable file
|
@ -0,0 +1,76 @@
|
|||
// https://eslint.org/docs/user-guide/configuring
|
||||
|
||||
module.exports = {
|
||||
root: true,
|
||||
parserOptions: {
|
||||
parser: 'babel-eslint'
|
||||
},
|
||||
env: {
|
||||
browser: true,
|
||||
},
|
||||
// https://github.com/vuejs/eslint-plugin-vue#priority-a-essential-error-prevention
|
||||
// consider switching to `plugin:vue/strongly-recommended` or `plugin:vue/recommended` for stricter rules.
|
||||
extends: ['plugin:vue/essential', 'airbnb-base'],
|
||||
// required to lint *.vue files
|
||||
plugins: [
|
||||
'vue'
|
||||
],
|
||||
// check if imports actually resolve
|
||||
settings: {
|
||||
'import/resolver': {
|
||||
webpack: {
|
||||
config: 'build/webpack.base.conf.js'
|
||||
}
|
||||
}
|
||||
},
|
||||
// add your custom rules here
|
||||
rules: {
|
||||
"vue/order-in-components": [
|
||||
"error", {
|
||||
"order": [
|
||||
"el",
|
||||
"name",
|
||||
"parent",
|
||||
"functional",
|
||||
["delimiters", "comments"],
|
||||
["components", "directives", "filters"],
|
||||
"extends",
|
||||
"mixins",
|
||||
"inheritAttrs",
|
||||
"model",
|
||||
["props", "propsData"],
|
||||
"data",
|
||||
"computed",
|
||||
"watch",
|
||||
"LIFECYCLE_HOOKS",
|
||||
"methods",
|
||||
["template", "render"],
|
||||
"renderError",
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
// don't require .vue extension when importing
|
||||
'import/extensions': ['error', 'always', {
|
||||
js: 'never',
|
||||
vue: 'never'
|
||||
}],
|
||||
"indent": ["error", 4],
|
||||
// disallow reassignment of function parameters
|
||||
// disallow parameter object manipulation except for specific exclusions
|
||||
'no-param-reassign': ['error', {
|
||||
props: true,
|
||||
ignorePropertyModificationsFor: [
|
||||
'state', // for vuex state
|
||||
'acc', // for reduce accumulators
|
||||
'e' // for e.returnvalue
|
||||
]
|
||||
}],
|
||||
// allow optionalDependencies
|
||||
'import/no-extraneous-dependencies': ['error', {
|
||||
optionalDependencies: ['test/unit/index.js']
|
||||
}],
|
||||
// allow debugger during development
|
||||
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off'
|
||||
}
|
||||
}
|
5
.firebaserc
Executable file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"projects": {
|
||||
"default": "gamebrary-8c736"
|
||||
}
|
||||
}
|
16
.gitignore
vendored
Executable file
|
@ -0,0 +1,16 @@
|
|||
.DS_Store
|
||||
node_modules/
|
||||
/dist/
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
/test/unit/coverage/
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
.firebase
|
||||
.vscode
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
10
.postcssrc.js
Executable file
|
@ -0,0 +1,10 @@
|
|||
// https://github.com/michael-ciniawsky/postcss-load-config
|
||||
|
||||
module.exports = {
|
||||
"plugins": {
|
||||
"postcss-import": {},
|
||||
"postcss-url": {},
|
||||
// to edit target browsers: use "browserslist" field in package.json
|
||||
"autoprefixer": {}
|
||||
}
|
||||
}
|
83
README.md
Executable file
|
@ -0,0 +1,83 @@
|
|||
# SwitchList
|
||||
![kapture 2018-07-02 at 21 56 24](https://user-images.githubusercontent.com/645310/42199388-21895a4a-7e43-11e8-9959-513a535eea25.gif)
|
||||
|
||||
User friendly and intuitive tool to keep track of your switch collection.
|
||||
|
||||
## Getting Started
|
||||
|
||||
These instructions will get you a copy of the project up and running on your local machine for development and testing purposes. See deployment for notes on how to deploy the project on a live system.
|
||||
|
||||
### Prerequisites
|
||||
|
||||
What things you need to install the software and how to install them
|
||||
|
||||
```
|
||||
yarn / node / npm
|
||||
```
|
||||
|
||||
## Build Setup
|
||||
|
||||
``` bash
|
||||
# install dependencies
|
||||
yarn
|
||||
|
||||
# serve with hot reload at localhost:3000 (production backend)
|
||||
yarn dev
|
||||
|
||||
# serve with hot reload at localhost:3000 (local backend)
|
||||
yarn dev:local
|
||||
|
||||
# build for production with minification
|
||||
yarn build
|
||||
|
||||
# build for production and view the bundle analyzer report
|
||||
yarn build --report
|
||||
|
||||
# run unit tests
|
||||
yarn unit
|
||||
|
||||
```
|
||||
|
||||
## Running the tests
|
||||
|
||||
```
|
||||
yarn test
|
||||
```
|
||||
|
||||
## Deployment
|
||||
|
||||
```
|
||||
yarn build
|
||||
```
|
||||
|
||||
## Built With
|
||||
|
||||
* [VueJS](https://vuejs.org/) - VueJS
|
||||
* [Vuex](https://github.com/vuejs/vuex) - State management
|
||||
* [Vuei18n](https://github.com/kazupon/vue-i18n) - i18n
|
||||
* [Vue Draggable](https://github.com/SortableJS/Vue.Draggable) - Drag n drop component
|
||||
* [MongoDB](https://www.mongodb.com/) - Database
|
||||
* [IGDB](https://www.igdb.com/) - Game database API (These guys are awesome! Huge open source supporters!)
|
||||
|
||||
## Contributing
|
||||
|
||||
Please refer to [Issues](https://github.com/romancmx/switchlist/issues) for details on open issues. Any contribution is appreciated.
|
||||
|
||||
## Versioning
|
||||
|
||||
We use [SemVer](http://semver.org/) for versioning. For the versions available, see the [tags on this repository](https://github.com/romancmx/switchlist/releases).
|
||||
|
||||
## Authors
|
||||
|
||||
* **Roman** - *Lead Developer* - [Roman](https://twitter.com/romancm)
|
||||
* **Tristan** - *Contributor* - [3stan](https://github.com/3stan)
|
||||
|
||||
See also the list of [contributors](https://github.com/romancmx/switchlist/graphs/contributors) who participated in this project.
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the MIT License - see the [LICENSE.md](LICENSE.md) file for details
|
||||
|
||||
## Acknowledgments
|
||||
|
||||
* The [/r/NintendoSwitch/](https://www.reddit.com/r/NintendoSwitch/) community for great initial poll feedback
|
41
build/build.js
Executable file
|
@ -0,0 +1,41 @@
|
|||
'use strict'
|
||||
require('./check-versions')()
|
||||
|
||||
process.env.NODE_ENV = 'production'
|
||||
|
||||
const ora = require('ora')
|
||||
const rm = require('rimraf')
|
||||
const path = require('path')
|
||||
const chalk = require('chalk')
|
||||
const webpack = require('webpack')
|
||||
const config = require('../config')
|
||||
const webpackConfig = require('./webpack.prod.conf')
|
||||
|
||||
const spinner = ora('building for production...')
|
||||
spinner.start()
|
||||
|
||||
rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => {
|
||||
if (err) throw err
|
||||
webpack(webpackConfig, (err, stats) => {
|
||||
spinner.stop()
|
||||
if (err) throw err
|
||||
process.stdout.write(stats.toString({
|
||||
colors: true,
|
||||
modules: false,
|
||||
children: false, // If you are using ts-loader, setting this to true will make TypeScript errors show up during build.
|
||||
chunks: false,
|
||||
chunkModules: false
|
||||
}) + '\n\n')
|
||||
|
||||
if (stats.hasErrors()) {
|
||||
console.log(chalk.red(' Build failed with errors.\n'))
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
console.log(chalk.cyan(' Build complete.\n'))
|
||||
console.log(chalk.yellow(
|
||||
' Tip: built files are meant to be served over an HTTP server.\n' +
|
||||
' Opening index.html over file:// won\'t work.\n'
|
||||
))
|
||||
})
|
||||
})
|
54
build/check-versions.js
Executable file
|
@ -0,0 +1,54 @@
|
|||
'use strict'
|
||||
const chalk = require('chalk')
|
||||
const semver = require('semver')
|
||||
const packageConfig = require('../package.json')
|
||||
const shell = require('shelljs')
|
||||
|
||||
function exec (cmd) {
|
||||
return require('child_process').execSync(cmd).toString().trim()
|
||||
}
|
||||
|
||||
const versionRequirements = [
|
||||
{
|
||||
name: 'node',
|
||||
currentVersion: semver.clean(process.version),
|
||||
versionRequirement: packageConfig.engines.node
|
||||
}
|
||||
]
|
||||
|
||||
if (shell.which('npm')) {
|
||||
versionRequirements.push({
|
||||
name: 'npm',
|
||||
currentVersion: exec('npm --version'),
|
||||
versionRequirement: packageConfig.engines.npm
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = function () {
|
||||
const warnings = []
|
||||
|
||||
for (let i = 0; i < versionRequirements.length; i++) {
|
||||
const mod = versionRequirements[i]
|
||||
|
||||
if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) {
|
||||
warnings.push(mod.name + ': ' +
|
||||
chalk.red(mod.currentVersion) + ' should be ' +
|
||||
chalk.green(mod.versionRequirement)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (warnings.length) {
|
||||
console.log('')
|
||||
console.log(chalk.yellow('To use this template, you must update following to modules:'))
|
||||
console.log()
|
||||
|
||||
for (let i = 0; i < warnings.length; i++) {
|
||||
const warning = warnings[i]
|
||||
console.log(' ' + warning)
|
||||
}
|
||||
|
||||
console.log()
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
11
build/load-minified.js
Normal file
|
@ -0,0 +1,11 @@
|
|||
'use strict'
|
||||
|
||||
const fs = require('fs')
|
||||
const UglifyJS = require('uglify-es')
|
||||
|
||||
module.exports = function(filePath) {
|
||||
const code = fs.readFileSync(filePath, 'utf-8')
|
||||
const result = UglifyJS.minify(code)
|
||||
if (result.error) return ''
|
||||
return result.code
|
||||
}
|
BIN
build/logo.png
Executable file
After Width: | Height: | Size: 6.7 KiB |
17
build/service-worker-dev.js
Normal file
|
@ -0,0 +1,17 @@
|
|||
// This service worker file is effectively a 'no-op' that will reset any
|
||||
// previous service worker registered for the same host:port combination.
|
||||
// In the production build, this file is replaced with an actual service worker
|
||||
// file that will precache your site's local assets.
|
||||
// See https://github.com/facebookincubator/create-react-app/issues/2272#issuecomment-302832432
|
||||
|
||||
self.addEventListener('install', () => self.skipWaiting());
|
||||
|
||||
self.addEventListener('activate', () => {
|
||||
self.clients.matchAll({ type: 'window' }).then(windowClients => {
|
||||
for (let windowClient of windowClients) {
|
||||
// Force open pages to refresh, so that they have a chance to load the
|
||||
// fresh navigation response from the local dev server.
|
||||
windowClient.navigate(windowClient.url);
|
||||
}
|
||||
});
|
||||
});
|
55
build/service-worker-prod.js
Normal file
|
@ -0,0 +1,55 @@
|
|||
(function() {
|
||||
'use strict';
|
||||
|
||||
// Check to make sure service workers are supported in the current browser,
|
||||
// and that the current page is accessed from a secure origin. Using a
|
||||
// service worker from an insecure origin will trigger JS console errors.
|
||||
var isLocalhost = Boolean(window.location.hostname === 'localhost' ||
|
||||
// [::1] is the IPv6 localhost address.
|
||||
window.location.hostname === '[::1]' ||
|
||||
// 127.0.0.1/8 is considered localhost for IPv4.
|
||||
window.location.hostname.match(
|
||||
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
|
||||
)
|
||||
);
|
||||
|
||||
window.addEventListener('load', function() {
|
||||
if ('serviceWorker' in navigator &&
|
||||
(window.location.protocol === 'https:' || isLocalhost)) {
|
||||
navigator.serviceWorker.register('service-worker.js')
|
||||
.then(function(registration) {
|
||||
// updatefound is fired if service-worker.js changes.
|
||||
registration.onupdatefound = function() {
|
||||
// updatefound is also fired the very first time the SW is installed,
|
||||
// and there's no need to prompt for a reload at that point.
|
||||
// So check here to see if the page is already controlled,
|
||||
// i.e. whether there's an existing service worker.
|
||||
if (navigator.serviceWorker.controller) {
|
||||
// The updatefound event implies that registration.installing is set
|
||||
var installingWorker = registration.installing;
|
||||
|
||||
installingWorker.onstatechange = function() {
|
||||
switch (installingWorker.state) {
|
||||
case 'installed':
|
||||
// At this point, the old content will have been purged and the
|
||||
// fresh content will have been added to the cache.
|
||||
// It's the perfect time to display a "New content is
|
||||
// available; please refresh." message in the page's interface.
|
||||
break;
|
||||
|
||||
case 'redundant':
|
||||
throw new Error('The installing ' +
|
||||
'service worker became redundant.');
|
||||
|
||||
default:
|
||||
// Ignore
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
}).catch(function(e) {
|
||||
console.error('Error during service worker registration:', e);
|
||||
});
|
||||
}
|
||||
});
|
||||
})();
|
101
build/utils.js
Executable file
|
@ -0,0 +1,101 @@
|
|||
'use strict'
|
||||
const path = require('path')
|
||||
const config = require('../config')
|
||||
const ExtractTextPlugin = require('extract-text-webpack-plugin')
|
||||
const packageConfig = require('../package.json')
|
||||
|
||||
exports.assetsPath = function (_path) {
|
||||
const assetsSubDirectory = process.env.NODE_ENV === 'production'
|
||||
? config.build.assetsSubDirectory
|
||||
: config.dev.assetsSubDirectory
|
||||
|
||||
return path.posix.join(assetsSubDirectory, _path)
|
||||
}
|
||||
|
||||
exports.cssLoaders = function (options) {
|
||||
options = options || {}
|
||||
|
||||
const cssLoader = {
|
||||
loader: 'css-loader',
|
||||
options: {
|
||||
sourceMap: options.sourceMap
|
||||
}
|
||||
}
|
||||
|
||||
const postcssLoader = {
|
||||
loader: 'postcss-loader',
|
||||
options: {
|
||||
sourceMap: options.sourceMap
|
||||
}
|
||||
}
|
||||
|
||||
// generate loader string to be used with extract text plugin
|
||||
function generateLoaders (loader, loaderOptions) {
|
||||
const loaders = options.usePostCSS ? [cssLoader, postcssLoader] : [cssLoader]
|
||||
|
||||
if (loader) {
|
||||
loaders.push({
|
||||
loader: loader + '-loader',
|
||||
options: Object.assign({}, loaderOptions, {
|
||||
sourceMap: options.sourceMap
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// Extract CSS when that option is specified
|
||||
// (which is the case during production build)
|
||||
if (options.extract) {
|
||||
return ExtractTextPlugin.extract({
|
||||
use: loaders,
|
||||
fallback: 'vue-style-loader'
|
||||
})
|
||||
} else {
|
||||
return ['vue-style-loader'].concat(loaders)
|
||||
}
|
||||
}
|
||||
|
||||
// https://vue-loader.vuejs.org/en/configurations/extract-css.html
|
||||
return {
|
||||
css: generateLoaders(),
|
||||
postcss: generateLoaders(),
|
||||
less: generateLoaders('less'),
|
||||
sass: generateLoaders('sass', { indentedSyntax: true }),
|
||||
scss: generateLoaders('sass'),
|
||||
stylus: generateLoaders('stylus'),
|
||||
styl: generateLoaders('stylus')
|
||||
}
|
||||
}
|
||||
|
||||
// Generate loaders for standalone style files (outside of .vue)
|
||||
exports.styleLoaders = function (options) {
|
||||
const output = []
|
||||
const loaders = exports.cssLoaders(options)
|
||||
|
||||
for (const extension in loaders) {
|
||||
const loader = loaders[extension]
|
||||
output.push({
|
||||
test: new RegExp('\\.' + extension + '$'),
|
||||
use: loader
|
||||
})
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
exports.createNotifierCallback = () => {
|
||||
const notifier = require('node-notifier')
|
||||
|
||||
return (severity, errors) => {
|
||||
if (severity !== 'error') return
|
||||
|
||||
const error = errors[0]
|
||||
const filename = error.file && error.file.split('!').pop()
|
||||
|
||||
notifier.notify({
|
||||
title: packageConfig.name,
|
||||
message: severity + ': ' + error.name,
|
||||
subtitle: filename || '',
|
||||
icon: path.join(__dirname, 'logo.png')
|
||||
})
|
||||
}
|
||||
}
|
22
build/vue-loader.conf.js
Executable file
|
@ -0,0 +1,22 @@
|
|||
'use strict'
|
||||
const utils = require('./utils')
|
||||
const config = require('../config')
|
||||
const isProduction = process.env.NODE_ENV === 'production'
|
||||
const sourceMapEnabled = isProduction
|
||||
? config.build.productionSourceMap
|
||||
: config.dev.cssSourceMap
|
||||
|
||||
module.exports = {
|
||||
loaders: utils.cssLoaders({
|
||||
sourceMap: sourceMapEnabled,
|
||||
extract: isProduction
|
||||
}),
|
||||
cssSourceMap: sourceMapEnabled,
|
||||
cacheBusting: config.dev.cacheBusting,
|
||||
transformToRequire: {
|
||||
video: ['src', 'poster'],
|
||||
source: 'src',
|
||||
img: 'src',
|
||||
image: 'xlink:href'
|
||||
}
|
||||
}
|
93
build/webpack.base.conf.js
Executable file
|
@ -0,0 +1,93 @@
|
|||
'use strict'
|
||||
const path = require('path')
|
||||
const utils = require('./utils')
|
||||
const config = require('../config')
|
||||
const vueLoaderConfig = require('./vue-loader.conf')
|
||||
|
||||
function resolve (dir) {
|
||||
return path.join(__dirname, '..', dir)
|
||||
}
|
||||
|
||||
const createLintingRule = () => ({
|
||||
test: /\.(js|vue)$/,
|
||||
loader: 'eslint-loader',
|
||||
enforce: 'pre',
|
||||
include: [resolve('src'), resolve('test')],
|
||||
options: {
|
||||
formatter: require('eslint-friendly-formatter'),
|
||||
emitWarning: !config.dev.showEslintErrorsInOverlay
|
||||
}
|
||||
})
|
||||
|
||||
module.exports = {
|
||||
context: path.resolve(__dirname, '../'),
|
||||
entry: {
|
||||
app: './src/main.js'
|
||||
},
|
||||
output: {
|
||||
path: config.build.assetsRoot,
|
||||
filename: '[name].js',
|
||||
publicPath: process.env.NODE_ENV === 'production'
|
||||
? config.build.assetsPublicPath
|
||||
: config.dev.assetsPublicPath
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.js', '.vue', '.json'],
|
||||
alias: {
|
||||
'vue$': 'vue/dist/vue.esm.js',
|
||||
'styles': path.resolve('./src/styles'),
|
||||
'@': resolve('src'),
|
||||
}
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
...(config.dev.useEslint ? [createLintingRule()] : []),
|
||||
{
|
||||
test: /\.vue$/,
|
||||
loader: 'vue-loader',
|
||||
options: vueLoaderConfig
|
||||
},
|
||||
{
|
||||
test: /\.js$/,
|
||||
loader: 'babel-loader',
|
||||
include: [resolve('src'), resolve('test'), resolve('node_modules/webpack-dev-server/client')]
|
||||
},
|
||||
{
|
||||
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
|
||||
loader: 'url-loader',
|
||||
options: {
|
||||
limit: 10000,
|
||||
name: utils.assetsPath('img/[name].[hash:7].[ext]')
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
|
||||
loader: 'url-loader',
|
||||
options: {
|
||||
limit: 10000,
|
||||
name: utils.assetsPath('media/[name].[hash:7].[ext]')
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
|
||||
loader: 'url-loader',
|
||||
options: {
|
||||
limit: 10000,
|
||||
name: utils.assetsPath('fonts/[name].[hash:7].[ext]')
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
node: {
|
||||
// prevent webpack from injecting useless setImmediate polyfill because Vue
|
||||
// source contains it (although only uses it if it's native).
|
||||
setImmediate: false,
|
||||
// prevent webpack from injecting mocks to Node native modules
|
||||
// that does not make sense for the client
|
||||
dgram: 'empty',
|
||||
fs: 'empty',
|
||||
net: 'empty',
|
||||
tls: 'empty',
|
||||
child_process: 'empty'
|
||||
}
|
||||
}
|
98
build/webpack.dev.conf.js
Executable file
|
@ -0,0 +1,98 @@
|
|||
'use strict'
|
||||
const fs = require('fs')
|
||||
const utils = require('./utils')
|
||||
const webpack = require('webpack')
|
||||
const config = require('../config')
|
||||
const merge = require('webpack-merge')
|
||||
const path = require('path')
|
||||
const baseWebpackConfig = require('./webpack.base.conf')
|
||||
const CopyWebpackPlugin = require('copy-webpack-plugin')
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin')
|
||||
const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin')
|
||||
const portfinder = require('portfinder')
|
||||
|
||||
const HOST = process.env.HOST
|
||||
const PORT = process.env.PORT && Number(process.env.PORT)
|
||||
|
||||
const devWebpackConfig = merge(baseWebpackConfig, {
|
||||
module: {
|
||||
rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap, usePostCSS: true })
|
||||
},
|
||||
// cheap-module-eval-source-map is faster for development
|
||||
devtool: config.dev.devtool,
|
||||
|
||||
// these devServer options should be customized in /config/index.js
|
||||
devServer: {
|
||||
clientLogLevel: 'warning',
|
||||
historyApiFallback: {
|
||||
rewrites: [
|
||||
{ from: /.*/, to: path.posix.join(config.dev.assetsPublicPath, 'index.html') },
|
||||
],
|
||||
},
|
||||
hot: true,
|
||||
contentBase: false, // since we use CopyWebpackPlugin.
|
||||
compress: true,
|
||||
host: HOST || config.dev.host,
|
||||
port: PORT || config.dev.port,
|
||||
open: config.dev.autoOpenBrowser,
|
||||
overlay: config.dev.errorOverlay
|
||||
? { warnings: false, errors: true }
|
||||
: false,
|
||||
publicPath: config.dev.assetsPublicPath,
|
||||
proxy: config.dev.proxyTable,
|
||||
quiet: true, // necessary for FriendlyErrorsPlugin
|
||||
watchOptions: {
|
||||
poll: config.dev.poll,
|
||||
}
|
||||
},
|
||||
plugins: [
|
||||
new webpack.DefinePlugin({
|
||||
'process.env': require('../config/dev.env')
|
||||
}),
|
||||
new webpack.HotModuleReplacementPlugin(),
|
||||
new webpack.NamedModulesPlugin(), // HMR shows correct file names in console on update.
|
||||
new webpack.NoEmitOnErrorsPlugin(),
|
||||
// https://github.com/ampedandwired/html-webpack-plugin
|
||||
new HtmlWebpackPlugin({
|
||||
filename: 'index.html',
|
||||
template: 'index.html',
|
||||
inject: true,
|
||||
serviceWorkerLoader: `<script>${fs.readFileSync(path.join(__dirname,
|
||||
'./service-worker-dev.js'), 'utf-8')}</script>`
|
||||
}),
|
||||
// copy custom static assets
|
||||
new CopyWebpackPlugin([
|
||||
{
|
||||
from: path.resolve(__dirname, '../static'),
|
||||
to: config.dev.assetsSubDirectory,
|
||||
ignore: ['.*']
|
||||
}
|
||||
])
|
||||
]
|
||||
})
|
||||
|
||||
module.exports = new Promise((resolve, reject) => {
|
||||
portfinder.basePort = process.env.PORT || config.dev.port
|
||||
portfinder.getPort((err, port) => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
} else {
|
||||
// publish the new Port, necessary for e2e tests
|
||||
process.env.PORT = port
|
||||
// add port to devServer config
|
||||
devWebpackConfig.devServer.port = port
|
||||
|
||||
// Add FriendlyErrorsPlugin
|
||||
devWebpackConfig.plugins.push(new FriendlyErrorsPlugin({
|
||||
compilationSuccessInfo: {
|
||||
messages: [`Your application is running here: http://${devWebpackConfig.devServer.host}:${port}`],
|
||||
},
|
||||
onErrors: config.dev.notifyOnErrors
|
||||
? utils.createNotifierCallback()
|
||||
: undefined
|
||||
}))
|
||||
|
||||
resolve(devWebpackConfig)
|
||||
}
|
||||
})
|
||||
})
|
98
build/webpack.dev.local.conf.js
Normal file
|
@ -0,0 +1,98 @@
|
|||
'use strict'
|
||||
const fs = require('fs')
|
||||
const utils = require('./utils')
|
||||
const webpack = require('webpack')
|
||||
const config = require('../config')
|
||||
const merge = require('webpack-merge')
|
||||
const path = require('path')
|
||||
const baseWebpackConfig = require('./webpack.base.conf')
|
||||
const CopyWebpackPlugin = require('copy-webpack-plugin')
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin')
|
||||
const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin')
|
||||
const portfinder = require('portfinder')
|
||||
|
||||
const HOST = process.env.HOST
|
||||
const PORT = process.env.PORT && Number(process.env.PORT)
|
||||
|
||||
const devWebpackConfig = merge(baseWebpackConfig, {
|
||||
module: {
|
||||
rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap, usePostCSS: true })
|
||||
},
|
||||
// cheap-module-eval-source-map is faster for development
|
||||
devtool: config.dev.devtool,
|
||||
|
||||
// these devServer options should be customized in /config/index.js
|
||||
devServer: {
|
||||
clientLogLevel: 'warning',
|
||||
historyApiFallback: {
|
||||
rewrites: [
|
||||
{ from: /.*/, to: path.posix.join(config.dev.assetsPublicPath, 'index.html') },
|
||||
],
|
||||
},
|
||||
hot: true,
|
||||
contentBase: false, // since we use CopyWebpackPlugin.
|
||||
compress: true,
|
||||
host: HOST || config.dev.host,
|
||||
port: PORT || config.dev.port,
|
||||
open: config.dev.autoOpenBrowser,
|
||||
overlay: config.dev.errorOverlay
|
||||
? { warnings: false, errors: true }
|
||||
: false,
|
||||
publicPath: config.dev.assetsPublicPath,
|
||||
proxy: config.dev.proxyTable,
|
||||
quiet: true, // necessary for FriendlyErrorsPlugin
|
||||
watchOptions: {
|
||||
poll: config.dev.poll,
|
||||
}
|
||||
},
|
||||
plugins: [
|
||||
new webpack.DefinePlugin({
|
||||
'process.env': require('../config/dev.local.env')
|
||||
}),
|
||||
new webpack.HotModuleReplacementPlugin(),
|
||||
new webpack.NamedModulesPlugin(), // HMR shows correct file names in console on update.
|
||||
new webpack.NoEmitOnErrorsPlugin(),
|
||||
// https://github.com/ampedandwired/html-webpack-plugin
|
||||
new HtmlWebpackPlugin({
|
||||
filename: 'index.html',
|
||||
template: 'index.html',
|
||||
inject: true,
|
||||
serviceWorkerLoader: `<script>${fs.readFileSync(path.join(__dirname,
|
||||
'./service-worker-dev.js'), 'utf-8')}</script>`
|
||||
}),
|
||||
// copy custom static assets
|
||||
new CopyWebpackPlugin([
|
||||
{
|
||||
from: path.resolve(__dirname, '../static'),
|
||||
to: config.dev.assetsSubDirectory,
|
||||
ignore: ['.*']
|
||||
}
|
||||
])
|
||||
]
|
||||
})
|
||||
|
||||
module.exports = new Promise((resolve, reject) => {
|
||||
portfinder.basePort = process.env.PORT || config.dev.port
|
||||
portfinder.getPort((err, port) => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
} else {
|
||||
// publish the new Port, necessary for e2e tests
|
||||
process.env.PORT = port
|
||||
// add port to devServer config
|
||||
devWebpackConfig.devServer.port = port
|
||||
|
||||
// Add FriendlyErrorsPlugin
|
||||
devWebpackConfig.plugins.push(new FriendlyErrorsPlugin({
|
||||
compilationSuccessInfo: {
|
||||
messages: [`Your application is running here: http://${devWebpackConfig.devServer.host}:${port}`],
|
||||
},
|
||||
onErrors: config.dev.notifyOnErrors
|
||||
? utils.createNotifierCallback()
|
||||
: undefined
|
||||
}))
|
||||
|
||||
resolve(devWebpackConfig)
|
||||
}
|
||||
})
|
||||
})
|
161
build/webpack.prod.conf.js
Executable file
|
@ -0,0 +1,161 @@
|
|||
'use strict'
|
||||
const path = require('path')
|
||||
const utils = require('./utils')
|
||||
const webpack = require('webpack')
|
||||
const config = require('../config')
|
||||
const merge = require('webpack-merge')
|
||||
const baseWebpackConfig = require('./webpack.base.conf')
|
||||
const CopyWebpackPlugin = require('copy-webpack-plugin')
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin')
|
||||
const ExtractTextPlugin = require('extract-text-webpack-plugin')
|
||||
const OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin')
|
||||
const UglifyJsPlugin = require('uglifyjs-webpack-plugin')
|
||||
const SWPrecacheWebpackPlugin = require('sw-precache-webpack-plugin');
|
||||
const loadMinified = require('./load-minified')
|
||||
|
||||
const env = process.env.NODE_ENV === 'testing'
|
||||
? require('../config/test.env')
|
||||
: require('../config/prod.env')
|
||||
|
||||
const webpackConfig = merge(baseWebpackConfig, {
|
||||
module: {
|
||||
rules: utils.styleLoaders({
|
||||
sourceMap: config.build.productionSourceMap,
|
||||
extract: true,
|
||||
usePostCSS: true
|
||||
})
|
||||
},
|
||||
devtool: config.build.productionSourceMap ? config.build.devtool : false,
|
||||
output: {
|
||||
path: config.build.assetsRoot,
|
||||
filename: utils.assetsPath('js/[name].[chunkhash].js'),
|
||||
chunkFilename: utils.assetsPath('js/[id].[chunkhash].js')
|
||||
},
|
||||
plugins: [
|
||||
// http://vuejs.github.io/vue-loader/en/workflow/production.html
|
||||
new webpack.DefinePlugin({
|
||||
'process.env': env
|
||||
}),
|
||||
// service worker caching
|
||||
new SWPrecacheWebpackPlugin({
|
||||
cacheId: 'SwitchList',
|
||||
filename: 'service-worker.js',
|
||||
staticFileGlobs: ['dist/**/*.{js,html,css}'],
|
||||
minify: true,
|
||||
stripPrefix: 'dist/'
|
||||
}),
|
||||
new UglifyJsPlugin({
|
||||
uglifyOptions: {
|
||||
compress: {
|
||||
warnings: false
|
||||
}
|
||||
},
|
||||
sourceMap: config.build.productionSourceMap,
|
||||
parallel: true
|
||||
}),
|
||||
// extract css into its own file
|
||||
new ExtractTextPlugin({
|
||||
filename: utils.assetsPath('css/[name].[contenthash].css'),
|
||||
// Setting the following option to `false` will not extract CSS from codesplit chunks.
|
||||
// Their CSS will instead be inserted dynamically with style-loader when the codesplit chunk has been loaded by webpack.
|
||||
// It's currently set to `true` because we are seeing that sourcemaps are included in the codesplit bundle as well when it's `false`,
|
||||
// increasing file size: https://github.com/vuejs-templates/webpack/issues/1110
|
||||
allChunks: true,
|
||||
}),
|
||||
// Compress extracted CSS. We are using this plugin so that possible
|
||||
// duplicated CSS from different components can be deduped.
|
||||
new OptimizeCSSPlugin({
|
||||
cssProcessorOptions: config.build.productionSourceMap
|
||||
? { safe: true, map: { inline: false } }
|
||||
: { safe: true }
|
||||
}),
|
||||
// generate dist index.html with correct asset hash for caching.
|
||||
// you can customize output by editing /index.html
|
||||
// see https://github.com/ampedandwired/html-webpack-plugin
|
||||
new HtmlWebpackPlugin({
|
||||
filename: process.env.NODE_ENV === 'testing'
|
||||
? 'index.html'
|
||||
: config.build.index,
|
||||
template: 'index.html',
|
||||
inject: true,
|
||||
minify: {
|
||||
removeComments: true,
|
||||
collapseWhitespace: true,
|
||||
removeAttributeQuotes: true
|
||||
// more options:
|
||||
// https://github.com/kangax/html-minifier#options-quick-reference
|
||||
},
|
||||
// necessary to consistently work with multiple chunks via CommonsChunkPlugin
|
||||
chunksSortMode: 'dependency',
|
||||
serviceWorkerLoader: `<script>${loadMinified(path.join(__dirname,
|
||||
'./service-worker-prod.js'))}</script>`
|
||||
}),
|
||||
// keep module.id stable when vendor modules does not change
|
||||
new webpack.HashedModuleIdsPlugin(),
|
||||
// enable scope hoisting
|
||||
new webpack.optimize.ModuleConcatenationPlugin(),
|
||||
// split vendor js into its own file
|
||||
new webpack.optimize.CommonsChunkPlugin({
|
||||
name: 'vendor',
|
||||
minChunks (module) {
|
||||
// any required modules inside node_modules are extracted to vendor
|
||||
return (
|
||||
module.resource &&
|
||||
/\.js$/.test(module.resource) &&
|
||||
module.resource.indexOf(
|
||||
path.join(__dirname, '../node_modules')
|
||||
) === 0
|
||||
)
|
||||
}
|
||||
}),
|
||||
// extract webpack runtime and module manifest to its own file in order to
|
||||
// prevent vendor hash from being updated whenever app bundle is updated
|
||||
new webpack.optimize.CommonsChunkPlugin({
|
||||
name: 'manifest',
|
||||
minChunks: Infinity
|
||||
}),
|
||||
// This instance extracts shared chunks from code splitted chunks and bundles them
|
||||
// in a separate chunk, similar to the vendor chunk
|
||||
// see: https://webpack.js.org/plugins/commons-chunk-plugin/#extra-async-commons-chunk
|
||||
new webpack.optimize.CommonsChunkPlugin({
|
||||
name: 'app',
|
||||
async: 'vendor-async',
|
||||
children: true,
|
||||
minChunks: 3
|
||||
}),
|
||||
|
||||
// copy custom static assets
|
||||
new CopyWebpackPlugin([
|
||||
{
|
||||
from: path.resolve(__dirname, '../static'),
|
||||
to: config.build.assetsSubDirectory,
|
||||
ignore: ['.*']
|
||||
}
|
||||
])
|
||||
]
|
||||
})
|
||||
|
||||
if (config.build.productionGzip) {
|
||||
const CompressionWebpackPlugin = require('compression-webpack-plugin')
|
||||
|
||||
webpackConfig.plugins.push(
|
||||
new CompressionWebpackPlugin({
|
||||
asset: '[path].gz[query]',
|
||||
algorithm: 'gzip',
|
||||
test: new RegExp(
|
||||
'\\.(' +
|
||||
config.build.productionGzipExtensions.join('|') +
|
||||
')$'
|
||||
),
|
||||
threshold: 10240,
|
||||
minRatio: 0.8
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
if (config.build.bundleAnalyzerReport) {
|
||||
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
|
||||
webpackConfig.plugins.push(new BundleAnalyzerPlugin())
|
||||
}
|
||||
|
||||
module.exports = webpackConfig
|
32
build/webpack.test.conf.js
Executable file
|
@ -0,0 +1,32 @@
|
|||
'use strict'
|
||||
// This is the webpack config used for unit tests.
|
||||
|
||||
const utils = require('./utils')
|
||||
const webpack = require('webpack')
|
||||
const merge = require('webpack-merge')
|
||||
const baseWebpackConfig = require('./webpack.base.conf')
|
||||
|
||||
const webpackConfig = merge(baseWebpackConfig, {
|
||||
// use inline sourcemap for karma-sourcemap-loader
|
||||
module: {
|
||||
rules: utils.styleLoaders()
|
||||
},
|
||||
devtool: '#inline-source-map',
|
||||
resolveLoader: {
|
||||
alias: {
|
||||
// necessary to to make lang="scss" work in test when using vue-loader's ?inject option
|
||||
// see discussion at https://github.com/vuejs/vue-loader/issues/724
|
||||
'scss-loader': 'sass-loader'
|
||||
}
|
||||
},
|
||||
plugins: [
|
||||
new webpack.DefinePlugin({
|
||||
'process.env': require('../config/test.env')
|
||||
})
|
||||
]
|
||||
})
|
||||
|
||||
// no need for app entry during tests
|
||||
delete webpackConfig.entry
|
||||
|
||||
module.exports = webpackConfig
|
8
config/dev.env.js
Executable file
|
@ -0,0 +1,8 @@
|
|||
'use strict'
|
||||
const merge = require('webpack-merge')
|
||||
const prodEnv = require('./prod.env')
|
||||
|
||||
module.exports = merge(prodEnv, {
|
||||
NODE_ENV: '"development"',
|
||||
API_URL: '"https://switchlist-api.herokuapp.com"'
|
||||
})
|
8
config/dev.local.env.js
Normal file
|
@ -0,0 +1,8 @@
|
|||
'use strict'
|
||||
const merge = require('webpack-merge')
|
||||
const prodEnv = require('./prod.env')
|
||||
|
||||
module.exports = merge(prodEnv, {
|
||||
NODE_ENV: '"development"',
|
||||
API_URL: '"http://localhost:3333"'
|
||||
})
|
7
config/firebase/database.rules.json
Executable file
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
/* Visit https://firebase.google.com/docs/database/security to learn more about security rules. */
|
||||
"rules": {
|
||||
".read": true,
|
||||
".write": true
|
||||
}
|
||||
}
|
3
config/firebase/firestore.indexes.json
Executable file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"indexes": []
|
||||
}
|
7
config/firebase/firestore.rules
Executable file
|
@ -0,0 +1,7 @@
|
|||
service cloud.firestore {
|
||||
match /databases/{database}/documents {
|
||||
match /{document=**} {
|
||||
allow read, write: if request.auth.uid != null;
|
||||
}
|
||||
}
|
||||
}
|
7
config/firebase/storage.rules
Executable file
|
@ -0,0 +1,7 @@
|
|||
service firebase.storage {
|
||||
match /b/{bucket}/o {
|
||||
match /{allPaths=**} {
|
||||
allow read, write: if request.auth!=null;
|
||||
}
|
||||
}
|
||||
}
|
76
config/index.js
Executable file
|
@ -0,0 +1,76 @@
|
|||
'use strict'
|
||||
// Template version: 1.3.1
|
||||
// see http://vuejs-templates.github.io/webpack for documentation.
|
||||
|
||||
const path = require('path')
|
||||
|
||||
module.exports = {
|
||||
dev: {
|
||||
|
||||
// Paths
|
||||
assetsSubDirectory: 'static',
|
||||
assetsPublicPath: '',
|
||||
proxyTable: {},
|
||||
|
||||
// Various Dev Server settings
|
||||
host: 'localhost', // can be overwritten by process.env.HOST
|
||||
port: 3000, // can be overwritten by process.env.PORT, if port is in use, a free one will be determined
|
||||
autoOpenBrowser: false,
|
||||
errorOverlay: true,
|
||||
notifyOnErrors: true,
|
||||
poll: false, // https://webpack.js.org/configuration/dev-server/#devserver-watchoptions-
|
||||
|
||||
// Use Eslint Loader?
|
||||
// If true, your code will be linted during bundling and
|
||||
// linting errors and warnings will be shown in the console.
|
||||
useEslint: true,
|
||||
// If true, eslint errors and warnings will also be shown in the error overlay
|
||||
// in the browser.
|
||||
showEslintErrorsInOverlay: false,
|
||||
|
||||
/**
|
||||
* Source Maps
|
||||
*/
|
||||
|
||||
// https://webpack.js.org/configuration/devtool/#development
|
||||
devtool: 'cheap-module-eval-source-map',
|
||||
|
||||
// If you have problems debugging vue-files in devtools,
|
||||
// set this to false - it *may* help
|
||||
// https://vue-loader.vuejs.org/en/options.html#cachebusting
|
||||
cacheBusting: true,
|
||||
|
||||
cssSourceMap: true
|
||||
},
|
||||
|
||||
build: {
|
||||
// Template for index.html
|
||||
index: path.resolve(__dirname, '../dist/index.html'),
|
||||
|
||||
// Paths
|
||||
assetsRoot: path.resolve(__dirname, '../dist'),
|
||||
assetsSubDirectory: 'static',
|
||||
assetsPublicPath: '',
|
||||
|
||||
/**
|
||||
* Source Maps
|
||||
*/
|
||||
|
||||
productionSourceMap: true,
|
||||
// https://webpack.js.org/configuration/devtool/#production
|
||||
devtool: '#source-map',
|
||||
|
||||
// Gzip off by default as many popular static hosts such as
|
||||
// Surge or Netlify already gzip all static assets for you.
|
||||
// Before setting to `true`, make sure to:
|
||||
// npm install --save-dev compression-webpack-plugin
|
||||
productionGzip: false,
|
||||
productionGzipExtensions: ['js', 'css'],
|
||||
|
||||
// Run the build command with an extra argument to
|
||||
// View the bundle analyzer report after build finishes:
|
||||
// `npm run build --report`
|
||||
// Set to `true` or `false` to always turn it on or off
|
||||
bundleAnalyzerReport: process.env.npm_config_report
|
||||
}
|
||||
}
|
5
config/prod.env.js
Executable file
|
@ -0,0 +1,5 @@
|
|||
'use strict'
|
||||
module.exports = {
|
||||
NODE_ENV: '"production"',
|
||||
API_URL: '"https://switchlist-api.herokuapp.com"'
|
||||
}
|
7
config/test.env.js
Executable file
|
@ -0,0 +1,7 @@
|
|||
'use strict'
|
||||
const merge = require('webpack-merge')
|
||||
const devEnv = require('./dev.env')
|
||||
|
||||
module.exports = merge(devEnv, {
|
||||
NODE_ENV: '"testing"'
|
||||
})
|
26
firebase.json
Executable file
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"database": {
|
||||
"rules": "config/firebase/database.rules.json"
|
||||
},
|
||||
"firestore": {
|
||||
"rules": "config/firebase/firestore.rules",
|
||||
"indexes": "config/firebase/firestore.indexes.json"
|
||||
},
|
||||
"hosting": {
|
||||
"public": "dist",
|
||||
"ignore": [
|
||||
"config/firebase/firebase.json",
|
||||
"**/.*",
|
||||
"**/node_modules/**"
|
||||
],
|
||||
"rewrites": [
|
||||
{
|
||||
"source": "**",
|
||||
"destination": "/index.html"
|
||||
}
|
||||
]
|
||||
},
|
||||
"storage": {
|
||||
"rules": "config/firebase/storage.rules"
|
||||
}
|
||||
}
|
23
functions/index.js
Executable file
|
@ -0,0 +1,23 @@
|
|||
const functions = require('firebase-functions');
|
||||
const axios = require('axios');
|
||||
const circular = require('circular-json');
|
||||
|
||||
axios.defaults.headers.common['user-key'] = functions.config().igdb.key;
|
||||
|
||||
const igdbUrl = 'https://api-endpoint.igdb.com';
|
||||
|
||||
exports.search = functions.https.onRequest((req, res) => {
|
||||
const { searchText, platformId } = req.query;
|
||||
|
||||
axios.get(`${igdbUrl}/games/?search=${searchText}&fields=*&filter[platforms][eq]=${platformId}&limit=20&order=popularity:desc`)
|
||||
.then(({ data }) => { res.status(200).send(data) })
|
||||
.catch(() => { res.send(400) });
|
||||
});
|
||||
|
||||
exports.games = functions.https.onRequest((req, res) => {
|
||||
const { games, platformId } = req.query;
|
||||
|
||||
axios.get(`${igdbUrl}/games/${games}/?fields=id,name,slug,created_at,updated_at,summary,rating,category,player_perspectives,release_dates,name,cover,platforms,screenshots,videos,websites,esrb,pegi,themes.name,game.name&expand=game,themes,developers,publishers,game_engines,game_modes,genres,platforms,player_perspectives`)
|
||||
.then(({ data }) => { res.status(200).send(data) })
|
||||
.catch(() => { res.send(400) });
|
||||
});
|
19
functions/package.json
Executable file
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"name": "functions",
|
||||
"description": "Cloud Functions for Firebase",
|
||||
"scripts": {
|
||||
"serve": "firebase serve --only functions",
|
||||
"shell": "firebase functions:shell",
|
||||
"start": "npm run shell",
|
||||
"deploy": "firebase deploy --only functions",
|
||||
"logs": "firebase functions:log"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^0.18.0",
|
||||
"circular-json": "^0.5.7",
|
||||
"express": "^4.16.4",
|
||||
"firebase-admin": "~6.0.0",
|
||||
"firebase-functions": "^2.0.3"
|
||||
},
|
||||
"private": true
|
||||
}
|
2661
functions/yarn.lock
Normal file
41
index.html
Executable file
|
@ -0,0 +1,41 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<!-- Global site tag (gtag.js) - Google Analytics -->
|
||||
<script async src="https://www.googletagmanager.com/gtag/js?id=UA-120053966-1"></script>
|
||||
<script>
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){dataLayer.push(arguments);}
|
||||
gtag('js', new Date());
|
||||
|
||||
gtag('config', 'UA-120053966-1');
|
||||
</script>
|
||||
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.2.0/css/all.css" integrity="sha384-hWVjflwFxL6sNzntih27bfxkr27PmbbK/iSvJ+a4+0owXq79v+lsFkW54bOGbiDQ" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="//fonts.googleapis.com/css?family=Roboto:400,500,700,400italic|Material+Icons">
|
||||
<link rel="apple-touch-icon" sizes="57x57" href="static/apple-icon-57x57.png">
|
||||
<link rel="apple-touch-icon" sizes="60x60" href="static/apple-icon-60x60.png">
|
||||
<link rel="apple-touch-icon" sizes="72x72" href="static/apple-icon-72x72.png">
|
||||
<link rel="apple-touch-icon" sizes="76x76" href="static/apple-icon-76x76.png">
|
||||
<link rel="apple-touch-icon" sizes="114x114" href="static/apple-icon-114x114.png">
|
||||
<link rel="apple-touch-icon" sizes="120x120" href="static/apple-icon-120x120.png">
|
||||
<link rel="apple-touch-icon" sizes="144x144" href="static/apple-icon-144x144.png">
|
||||
<link rel="apple-touch-icon" sizes="152x152" href="static/apple-icon-152x152.png">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="static/apple-icon-180x180.png">
|
||||
<link rel="icon" type="image/png" sizes="192x192" href="static/android-icon-192x192.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="static/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="96x96" href="static/favicon-96x96.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="static/favicon-16x16.png">
|
||||
<link rel="manifest" href="static/manifest.json">
|
||||
<meta name="msapplication-TileColor" content="#ffffff">
|
||||
<meta name="msapplication-TileImage" content="static/ms-icon-144x144.png">
|
||||
<meta name="theme-color" content="#ffffff">
|
||||
|
||||
<title>GAMEBRARY</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app" />
|
||||
</body>
|
||||
</html>
|
113
package.json
Executable file
|
@ -0,0 +1,113 @@
|
|||
{
|
||||
"name": "switch-list",
|
||||
"version": "1.0.0",
|
||||
"description": "Keep track of your switch collection",
|
||||
"author": "Roman Cervantes",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js",
|
||||
"dev:local": "webpack-dev-server --inline --progress --config build/webpack.dev.local.conf.js",
|
||||
"start": "npm run dev",
|
||||
"unit": "cross-env BABEL_ENV=test karma start test/unit/karma.conf.js --single-run",
|
||||
"test": "npm run unit",
|
||||
"lint": "eslint --ext .js,.vue src test/unit",
|
||||
"build": "node build/build.js",
|
||||
"deploy": "node ./node_modules/vue-gh-pages/index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"EventEmitter": "^1.0.0",
|
||||
"axios": "^0.18.0",
|
||||
"eventemitter3": "^3.1.0",
|
||||
"firebase-admin": "^6.0.0",
|
||||
"firebase-functions": "^2.0.5",
|
||||
"lodash": "^4.17.10",
|
||||
"moment": "^2.22.1",
|
||||
"node-sass": "^4.8.3",
|
||||
"sass-loader": "^7.0.1",
|
||||
"sw-precache-webpack-plugin": "^0.11.5",
|
||||
"v-tooltip": "^2.0.0-rc.33",
|
||||
"vue": "^2.5.2",
|
||||
"vue-axios": "^2.1.1",
|
||||
"vue-content-placeholders": "^0.2.1",
|
||||
"vue-dragscroll": "^1.5.0",
|
||||
"vue-gravatar": "^1.2.1",
|
||||
"vue-i18n": "^8.0.0",
|
||||
"vue-router": "^3.0.1",
|
||||
"vue-sweetalert2": "^1.5.3",
|
||||
"vuedraggable": "^2.16.0",
|
||||
"vuex": "^3.0.1",
|
||||
"vuex-persist": "^1.2.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"autoprefixer": "^7.1.2",
|
||||
"babel-core": "^6.22.1",
|
||||
"babel-eslint": "^8.2.1",
|
||||
"babel-helper-vue-jsx-merge-props": "^2.0.3",
|
||||
"babel-loader": "^7.1.1",
|
||||
"babel-plugin-istanbul": "^4.1.1",
|
||||
"babel-plugin-syntax-jsx": "^6.18.0",
|
||||
"babel-plugin-transform-runtime": "^6.22.0",
|
||||
"babel-plugin-transform-vue-jsx": "^3.5.0",
|
||||
"babel-preset-env": "^1.3.2",
|
||||
"babel-preset-stage-2": "^6.22.0",
|
||||
"chai": "^4.1.2",
|
||||
"chalk": "^2.0.1",
|
||||
"copy-webpack-plugin": "^4.0.1",
|
||||
"cross-env": "^5.0.1",
|
||||
"css-loader": "^0.28.0",
|
||||
"eslint": "^4.15.0",
|
||||
"eslint-config-airbnb-base": "^11.3.0",
|
||||
"eslint-friendly-formatter": "^3.0.0",
|
||||
"eslint-import-resolver-webpack": "^0.8.3",
|
||||
"eslint-loader": "^1.7.1",
|
||||
"eslint-plugin-import": "^2.7.0",
|
||||
"eslint-plugin-promise": "^4.0.1",
|
||||
"eslint-plugin-vue": "^4.0.0",
|
||||
"extract-text-webpack-plugin": "^3.0.0",
|
||||
"file-loader": "^1.1.4",
|
||||
"friendly-errors-webpack-plugin": "^1.6.1",
|
||||
"html-webpack-plugin": "^2.30.1",
|
||||
"inject-loader": "^3.0.0",
|
||||
"karma": "^1.4.1",
|
||||
"karma-coverage": "^1.1.1",
|
||||
"karma-mocha": "^1.3.0",
|
||||
"karma-phantomjs-launcher": "^1.0.2",
|
||||
"karma-phantomjs-shim": "^1.4.0",
|
||||
"karma-sinon-chai": "^1.3.1",
|
||||
"karma-sourcemap-loader": "^0.3.7",
|
||||
"karma-spec-reporter": "0.0.31",
|
||||
"karma-webpack": "^2.0.2",
|
||||
"mocha": "^3.2.0",
|
||||
"node-notifier": "^5.1.2",
|
||||
"optimize-css-assets-webpack-plugin": "^3.2.0",
|
||||
"ora": "^1.2.0",
|
||||
"phantomjs-prebuilt": "^2.1.14",
|
||||
"portfinder": "^1.0.13",
|
||||
"postcss-import": "^11.0.0",
|
||||
"postcss-loader": "^2.0.8",
|
||||
"postcss-url": "^7.2.1",
|
||||
"rimraf": "^2.6.0",
|
||||
"semver": "^5.3.0",
|
||||
"shelljs": "^0.7.6",
|
||||
"sinon": "^4.0.0",
|
||||
"sinon-chai": "^2.8.0",
|
||||
"uglifyjs-webpack-plugin": "^1.1.1",
|
||||
"url-loader": "^0.5.8",
|
||||
"vue-loader": "^13.3.0",
|
||||
"vue-style-loader": "^3.0.1",
|
||||
"vue-template-compiler": "^2.5.2",
|
||||
"webpack": "^3.6.0",
|
||||
"webpack-bundle-analyzer": "^2.9.0",
|
||||
"webpack-dev-server": "^2.9.1",
|
||||
"webpack-merge": "^4.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6.0.0",
|
||||
"npm": ">= 3.0.0"
|
||||
},
|
||||
"browserslist": [
|
||||
"> 1%",
|
||||
"last 2 versions",
|
||||
"not ie <= 8"
|
||||
]
|
||||
}
|
50
src/App.vue
Executable file
|
@ -0,0 +1,50 @@
|
|||
<template>
|
||||
<div id="app">
|
||||
<nav-header />
|
||||
|
||||
<main>
|
||||
<router-view />
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import NavHeader from '@/components/NavHeader/NavHeader';
|
||||
|
||||
export default {
|
||||
name: 'App',
|
||||
|
||||
components: {
|
||||
NavHeader,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" rel="stylesheet/scss">
|
||||
@import "~styles/styles.scss";
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
#app {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
|
||||
> main {
|
||||
height: calc(100vh - #{$navHeight});
|
||||
overflow: auto;
|
||||
background: $color-gray;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@import url('https://fonts.googleapis.com/css?family=Roboto:400,700');
|
||||
|
||||
body {
|
||||
font-family: 'Roboto', sans-serif;
|
||||
// background: url('/static/background-pattern.png');
|
||||
}
|
||||
</style>
|
BIN
src/assets/logo.png
Executable file
After Width: | Height: | Size: 3.1 KiB |
202
src/components/GameBoard/List.vue
Normal file
|
@ -0,0 +1,202 @@
|
|||
<template lang="html">
|
||||
<div class="list">
|
||||
<div class="list-header">
|
||||
<list-name-edit
|
||||
:list-name="name"
|
||||
:list-index="listIndex"
|
||||
:game-count="games.length"
|
||||
@update="updateLists"
|
||||
/>
|
||||
|
||||
<div :class="['list-actions', { show: listOptionsActive }]">
|
||||
<div class="more-actions" v-show="listOptionsActive">
|
||||
<button
|
||||
v-if="hasGames"
|
||||
class="small"
|
||||
@click="sortList"
|
||||
>
|
||||
<i class="fas fa-sort-alpha-down" />
|
||||
</button>
|
||||
|
||||
<button @click="addGame" class="small">
|
||||
<i class="fas fa-plus" />
|
||||
</button>
|
||||
|
||||
<button @click="remove" class="small">
|
||||
<i class="far fa-trash-alt" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
v-if="listOptionsActive"
|
||||
class="small accent hollow"
|
||||
@click="cancelListEdit"
|
||||
>
|
||||
<i class="fas fa-times" />
|
||||
</button>
|
||||
|
||||
<button @click="editList" class="small accent hollow" v-else>
|
||||
<i class="fas fa-pencil-alt" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<game-search v-if="showSearch" :list-id="listIndex" />
|
||||
|
||||
<draggable
|
||||
v-else
|
||||
class="games"
|
||||
:list="games"
|
||||
:id="listIndex"
|
||||
:move="validateMove"
|
||||
:options="gameDraggableOptions"
|
||||
@end="end"
|
||||
@start="start"
|
||||
>
|
||||
<game-card
|
||||
v-for="game in games"
|
||||
:key="game"
|
||||
:id="game"
|
||||
:game-id="game"
|
||||
:list-id="listIndex"
|
||||
/>
|
||||
|
||||
<div class="empty-list" v-if="!games.length">
|
||||
<img src="/static/img/empty-collection.png" />
|
||||
<h3>This list is empty</h3>
|
||||
|
||||
<button class="primary" @click="addGame">
|
||||
<i class="fas fa-plus" />
|
||||
Add game
|
||||
</button>
|
||||
</div>
|
||||
</draggable>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import draggable from 'vuedraggable';
|
||||
import ListNameEdit from '@/components/GameBoard/ListNameEdit';
|
||||
import GameCard from '@/components/GameCard/GameCard';
|
||||
import GameSearch from '@/components/GameSearch/GameSearch';
|
||||
import { mapState } from 'vuex';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GameCard,
|
||||
GameSearch,
|
||||
ListNameEdit,
|
||||
draggable,
|
||||
},
|
||||
|
||||
props: {
|
||||
name: String,
|
||||
games: [Object, Array],
|
||||
listIndex: [String, Number],
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
showAddGame: false,
|
||||
listEditActive: false,
|
||||
gameDraggableOptions: {
|
||||
handle: '.game-drag-handle',
|
||||
ghostClass: 'card-placeholder',
|
||||
animation: 500,
|
||||
group: {
|
||||
name: 'games',
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapState(['user', 'activeList']),
|
||||
|
||||
showSearch() {
|
||||
return this.showAddGame && this.activeList === this.listIndex && this.listOptionsActive;
|
||||
},
|
||||
|
||||
listOptionsActive() {
|
||||
return this.listEditActive && this.activeList === this.listIndex;
|
||||
},
|
||||
|
||||
hasGames() {
|
||||
return this.games.length > 1;
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
editList() {
|
||||
this.listEditActive = true;
|
||||
this.$store.commit('SET_ACTIVE_LIST', this.listIndex);
|
||||
},
|
||||
|
||||
cancelListEdit() {
|
||||
this.listEditActive = false;
|
||||
this.showAddGame = false;
|
||||
this.$store.commit('SET_ACTIVE_LIST', null);
|
||||
},
|
||||
|
||||
addGame() {
|
||||
this.listEditActive = true;
|
||||
this.$store.commit('CLEAR_SEARCH_RESULTS');
|
||||
this.$store.commit('SET_ACTIVE_LIST', this.listIndex);
|
||||
this.showAddGame = true;
|
||||
},
|
||||
|
||||
updateLists() {
|
||||
this.$store.dispatch('UPDATE_LISTS');
|
||||
},
|
||||
|
||||
sortList() {
|
||||
this.$store.commit('SORT_LIST', this.listIndex);
|
||||
this.updateLists();
|
||||
},
|
||||
|
||||
validateMove({ from, to }) {
|
||||
const isDifferentList = from.id !== to.id;
|
||||
const isDuplicate = this.user.lists[to.id].games.includes(Number(this.draggingId));
|
||||
const validMove = isDifferentList && isDuplicate;
|
||||
return !validMove;
|
||||
},
|
||||
|
||||
start({ item }) {
|
||||
this.dragging = true;
|
||||
this.draggingId = item.id;
|
||||
},
|
||||
|
||||
end() {
|
||||
this.$emit('end');
|
||||
},
|
||||
|
||||
remove() {
|
||||
this.$emit('remove');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" rel="stylesheet/scss" scoped>
|
||||
@import "~styles/styles.scss";
|
||||
@import "~styles/game-board.scss";
|
||||
|
||||
.list-actions {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
transition: max-width 100ms ease;
|
||||
max-width: $iconSmallSize;
|
||||
overflow: hidden;
|
||||
|
||||
.more-actions {
|
||||
display: flex;
|
||||
min-width: $iconSmallSize;
|
||||
max-width: $iconSmallSize * 3;
|
||||
}
|
||||
|
||||
&.show {
|
||||
transition: max-width 300ms ease;
|
||||
max-width: $iconSmallSize * 4;
|
||||
}
|
||||
}
|
||||
</style>
|
82
src/components/GameBoard/ListNameEdit.vue
Normal file
|
@ -0,0 +1,82 @@
|
|||
<template lang="html">
|
||||
<div class="list-name">
|
||||
<input
|
||||
v-if="editing"
|
||||
class="small"
|
||||
v-model="localListName"
|
||||
ref="input"
|
||||
:id="`list-${listIndex}`"
|
||||
@keyup.enter="save"
|
||||
@keyup.esc="save"
|
||||
>
|
||||
|
||||
<span v-else @click="edit">
|
||||
{{ listName }} ({{ gameCount }})
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
listName: String,
|
||||
listIndex: [String, Number],
|
||||
gameCount: Number,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
editing: false,
|
||||
localListName: '',
|
||||
};
|
||||
},
|
||||
|
||||
methods: {
|
||||
handleClick(e) {
|
||||
const outsideClickEvent = e.target.id !== `list-${this.listIndex}`;
|
||||
const hasChanged = this.listName !== this.localListName;
|
||||
|
||||
if (outsideClickEvent) {
|
||||
this.exit();
|
||||
}
|
||||
|
||||
if (outsideClickEvent && hasChanged) {
|
||||
this.save();
|
||||
}
|
||||
},
|
||||
|
||||
save() {
|
||||
this.$store.commit('UPDATE_LIST_NAME', {
|
||||
listIndex: this.listIndex,
|
||||
listName: this.localListName,
|
||||
});
|
||||
|
||||
this.$emit('update');
|
||||
},
|
||||
|
||||
edit() {
|
||||
this.editing = true;
|
||||
this.localListName = this.listName;
|
||||
|
||||
this.$nextTick(() => {
|
||||
document.addEventListener('click', this.handleClick);
|
||||
this.$refs.input.focus();
|
||||
});
|
||||
},
|
||||
|
||||
exit() {
|
||||
this.editing = false;
|
||||
|
||||
this.$nextTick(() => {
|
||||
document.removeEventListener('click', this.handleClick);
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" rel="stylesheet/scss" scoped>
|
||||
.list-name {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
169
src/components/GameCard/GameCard.vue
Executable file
|
@ -0,0 +1,169 @@
|
|||
<template lang="html">
|
||||
<div
|
||||
v-if="gameId && games"
|
||||
:class="['game-card', { nightMode, 'search-result': searchResult }]"
|
||||
>
|
||||
<img :src="coverUrl" v-if="searchResult" width="50" />
|
||||
|
||||
<router-link
|
||||
v-else
|
||||
:to="{ name: 'game-detail', params: { id: this.game.id, slug: this.game.slug } }"
|
||||
>
|
||||
<img :src="coverUrl" width="80" />
|
||||
</router-link>
|
||||
|
||||
<div class="game-info">
|
||||
<h4 class="game-title">{{ game.name }}</h4>
|
||||
|
||||
<game-rating
|
||||
v-if="user.settings && user.settings.showGameRatings && !searchResult"
|
||||
small
|
||||
:rating="game.rating"
|
||||
/>
|
||||
|
||||
<button class="primary hollow small" @click="addGame" v-if="searchResult">
|
||||
<i class="fas fa-plus" />
|
||||
Add to list
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="options" v-if="!searchResult">
|
||||
<button
|
||||
v-if="!searchResult"
|
||||
class="game-drag-handle accent small hollow"
|
||||
title="Drag game"
|
||||
>
|
||||
<i class="fas fa-hand-rock" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
v-if="list.games.includes(gameId)"
|
||||
@click="removeGame"
|
||||
title="Delete game"
|
||||
class="accent small hollow"
|
||||
>
|
||||
<i class="fas fa-trash" />
|
||||
</button>
|
||||
|
||||
<button v-else @click="addGame" title="Add game">
|
||||
<i class="fas fa-plus" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import GameRating from '@/components/GameDetail/GameRating';
|
||||
import { mapState } from 'vuex';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GameRating,
|
||||
},
|
||||
|
||||
props: {
|
||||
gameId: Number,
|
||||
listId: Number,
|
||||
searchResult: Boolean,
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapState(['user', 'games']),
|
||||
|
||||
list() {
|
||||
return this.user.lists[this.listId];
|
||||
},
|
||||
|
||||
game() {
|
||||
return this.games[this.gameId];
|
||||
},
|
||||
|
||||
coverUrl() {
|
||||
const url = 'https://images.igdb.com/igdb/image/upload/t_cover_small/';
|
||||
|
||||
return this.games && this.games[this.gameId].cover
|
||||
? `${url}${this.games[this.gameId].cover.cloudinary_id}.jpg`
|
||||
: '/static/no-image.jpg';
|
||||
},
|
||||
|
||||
nightMode() {
|
||||
return this.user && this.user.settings ? this.user.settings.nightMode : null;
|
||||
},
|
||||
|
||||
showGameRating() {
|
||||
return this.user
|
||||
&& this.user.settings.showGameRatings
|
||||
&& Boolean(Number(this.game.rating));
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
addGame() {
|
||||
const data = {
|
||||
listId: this.listId,
|
||||
gameId: this.gameId,
|
||||
};
|
||||
|
||||
this.$emit('added');
|
||||
this.$store.commit('ADD_GAME', data);
|
||||
this.$store.dispatch('UPDATE_LISTS');
|
||||
},
|
||||
|
||||
removeGame() {
|
||||
const data = {
|
||||
listId: this.listId,
|
||||
gameId: this.gameId,
|
||||
};
|
||||
|
||||
this.$store.commit('REMOVE_GAME', data);
|
||||
this.$store.dispatch('UPDATE_LISTS');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" rel="stylesheet/scss" scoped>
|
||||
@import "~styles/styles.scss";
|
||||
@import "~styles/game-board.scss";
|
||||
|
||||
.game-card {
|
||||
background-color: $color-white;
|
||||
margin-bottom: $gp / 2;
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-columns: 80px auto 40px;
|
||||
|
||||
&.search-result {
|
||||
grid-template-columns: 50px auto 0;
|
||||
|
||||
.game-title { margin-bottom: $gp / 2; }
|
||||
.game-info { padding: $gp / 2; }
|
||||
}
|
||||
|
||||
.game-info { padding: $gp / 2 $gp; }
|
||||
.game-title { margin: 0; }
|
||||
|
||||
&:hover {
|
||||
.options {
|
||||
transition: all 300ms ease;
|
||||
max-height: 100px;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.options {
|
||||
opacity: 0;
|
||||
transition: all 300ms ease;
|
||||
max-height: 0;
|
||||
overflow: hidden;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
transition: all 300ms ease;
|
||||
|
||||
.game-drag-handle {
|
||||
@include drag-cursor;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
93
src/components/GameDetail/GameHeader.vue
Normal file
|
@ -0,0 +1,93 @@
|
|||
<template lang="html">
|
||||
<div class="game-header">
|
||||
<div class="game-background" :style="style">
|
||||
<img
|
||||
:src="coverUrl"
|
||||
:alt="game.name"
|
||||
class="game-cover"
|
||||
width="80"
|
||||
>
|
||||
|
||||
<div class="game-rating" v-if="game.esrb || game.pegi">
|
||||
<img
|
||||
v-if="game.esrb"
|
||||
:src='`/static/img/esrb/${esrb[game.esrb.rating]}.png`'
|
||||
:alt="esrb.synopsis"
|
||||
>
|
||||
|
||||
<img
|
||||
v-if="game.pegi"
|
||||
:src='`/static/img/pegi/${pegi[game.pegi.rating]}.png`'
|
||||
:alt="game.pegi.synopsis"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex';
|
||||
|
||||
export default {
|
||||
|
||||
props: {
|
||||
gameId: [Number, String],
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapState(['game', 'pegi', 'esrb']),
|
||||
|
||||
coverUrl() {
|
||||
const url = 'https://images.igdb.com/igdb/image/upload/t_cover_small/';
|
||||
return this.game && this.game.cover
|
||||
? `${url}${this.game.cover.cloudinary_id}.jpg`
|
||||
: '/static/no-image.jpg';
|
||||
},
|
||||
|
||||
style() {
|
||||
return this.game && this.game.screenshots
|
||||
? `background: url(${this.getImageUrl(this.game.screenshots[0].cloudinary_id)}); background-size: cover;`
|
||||
: '';
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
getImageUrl(cloudinaryId) {
|
||||
return cloudinaryId
|
||||
? `https://images.igdb.com/igdb/image/upload/t_screenshot_med/${cloudinaryId}.jpg`
|
||||
: null;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" rel="stylesheet/scss" scoped>
|
||||
@import "~styles/styles.scss";
|
||||
|
||||
.game-background {
|
||||
display: flex;
|
||||
min-height: 20vh;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
align-items: center;
|
||||
background-color: $color-red;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
|
||||
.game-cover {
|
||||
margin: 0 $gp;
|
||||
border: 5px solid $color-white;
|
||||
background-size: contain;
|
||||
box-shadow: 0 0 5px 0 $color-gray;
|
||||
}
|
||||
|
||||
.game-rating {
|
||||
position: absolute;
|
||||
top: $gp;
|
||||
right: $gp;
|
||||
img {
|
||||
height: 50px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
64
src/components/GameDetail/GameLinks.vue
Normal file
|
@ -0,0 +1,64 @@
|
|||
<template lang="html">
|
||||
<div class="links" v-if="game && game.websites">
|
||||
<a
|
||||
v-for="{ category, url } in game.websites"
|
||||
:key="category"
|
||||
:href="url"
|
||||
target="_blank"
|
||||
>
|
||||
<i :class="getIcon(category)" />
|
||||
{{ linkTypes[category] }}
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex';
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
icons: {
|
||||
official: 'fas fa-globe-americas',
|
||||
facebook: 'fab fa-facebook-f',
|
||||
steam: 'fab fa-steam',
|
||||
youtube: 'fab fa-youtube',
|
||||
twitter: 'fab fa-twitter',
|
||||
instagram: 'fab fa-instagram',
|
||||
iphone: 'fab fa-app-store-ios',
|
||||
wikipedia: 'fab fa-wikipedia-w',
|
||||
wikia: 'fas fa-link',
|
||||
twitch: 'fab fa-twitch',
|
||||
Reddit: 'fab fa-reddit',
|
||||
reddit: 'fab fa-reddit',
|
||||
},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState(['linkTypes', 'game']),
|
||||
},
|
||||
|
||||
methods: {
|
||||
getIcon(id) {
|
||||
const icon = this.linkTypes[id];
|
||||
|
||||
return this.icons[icon];
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" rel="stylesheet/scss" scoped>
|
||||
@import "~styles/styles.scss";
|
||||
.links {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-top: $gp;
|
||||
|
||||
a {
|
||||
color: $color-blue;
|
||||
text-transform: capitalize;
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
</style>
|
113
src/components/GameDetail/GameRating.vue
Normal file
|
@ -0,0 +1,113 @@
|
|||
<template lang="html">
|
||||
<div :class="['rating', ratingClass, { 'small': small}]">
|
||||
<span v-if="!isNaN(rating)">{{ parseInt(rating, 10) }}</span>
|
||||
<span v-else>?</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
rating: Number,
|
||||
small: Boolean,
|
||||
},
|
||||
|
||||
computed: {
|
||||
ratingClass() {
|
||||
const gameRating = Number(this.rating);
|
||||
if (gameRating >= 90) { return 'excellent'; }
|
||||
if (gameRating >= 80) { return 'good'; }
|
||||
if (gameRating >= 70) { return 'average'; }
|
||||
if (gameRating < 70) { return 'bad'; }
|
||||
return '';
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" rel="stylesheet/scss" scoped>
|
||||
@import "~styles/styles.scss";
|
||||
|
||||
.rating {
|
||||
position: relative;
|
||||
width: 100px;
|
||||
height: 57.74px;
|
||||
background-color: $color-gray;
|
||||
margin: 28.87px 0;
|
||||
font-size: 60px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: $color-white;
|
||||
font-weight: bold;
|
||||
|
||||
&.small {
|
||||
width: 26px;
|
||||
height: 15px;
|
||||
font-size: 15px;
|
||||
margin: 12px 0 8px;
|
||||
|
||||
&:before,
|
||||
&:after {
|
||||
border-left: 13px solid transparent;
|
||||
border-right: 13px solid transparent;
|
||||
}
|
||||
|
||||
&:before {
|
||||
border-bottom: 8px solid $color-gray;
|
||||
}
|
||||
|
||||
&:after {
|
||||
border-top: 8px solid $color-gray;
|
||||
}
|
||||
}
|
||||
|
||||
span {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
&:before,
|
||||
&:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: 0;
|
||||
left: 0;
|
||||
border-left: 50px solid transparent;
|
||||
border-right: 50px solid transparent;
|
||||
}
|
||||
|
||||
&:before {
|
||||
bottom: 100%;
|
||||
border-bottom: 28.87px solid $color-light-gray;
|
||||
}
|
||||
|
||||
&:after {
|
||||
top: 100%;
|
||||
border-top: 28.87px solid $color-light-gray;
|
||||
}
|
||||
|
||||
&.excellent {
|
||||
background-color: $color-green;
|
||||
|
||||
&:before { border-bottom-color: $color-green; }
|
||||
&:after { border-top-color: $color-green; }
|
||||
}
|
||||
&.good {
|
||||
background-color: $color-light-green;
|
||||
|
||||
&:before { border-bottom-color: $color-light-green; }
|
||||
&:after { border-top-color: $color-light-green; }
|
||||
}
|
||||
&.average {
|
||||
background-color: $color-orange;
|
||||
|
||||
&:before { border-bottom-color: $color-orange; }
|
||||
&:after { border-top-color: $color-orange; }
|
||||
}
|
||||
&.bad {
|
||||
background-color: $color-red;
|
||||
|
||||
&:before { border-bottom-color: $color-red; }
|
||||
&:after { border-top-color: $color-red; }
|
||||
}
|
||||
}
|
||||
</style>
|
80
src/components/GameDetail/GameReviewBox.vue
Normal file
|
@ -0,0 +1,80 @@
|
|||
<template lang="html">
|
||||
<div v-if="game" class="review-box">
|
||||
<game-rating :rating="game.rating" />
|
||||
|
||||
<div class="info">
|
||||
<section v-if="playerPerspectives">
|
||||
<strong>Perspective</strong> {{ playerPerspectives }}
|
||||
</section>
|
||||
|
||||
<section v-if="gameModes">
|
||||
<strong>Game Mode</strong> {{ gameModes }}
|
||||
</section>
|
||||
|
||||
<section v-if="genres">
|
||||
<strong>Genre</strong> {{ genres }}
|
||||
</section>
|
||||
|
||||
<section v-if="gamePlatforms">
|
||||
<strong>Platforms</strong> {{ gamePlatforms }}
|
||||
</section>
|
||||
|
||||
<section v-if="developers">
|
||||
<strong>Developer</strong> {{ developers }}
|
||||
</section>
|
||||
|
||||
<section v-if="publishers">
|
||||
<strong>Publishers</strong> {{ publishers }}
|
||||
</section>
|
||||
|
||||
<section v-if="releaseDate">
|
||||
<strong>Release date</strong> {{ releaseDate }}
|
||||
</section>
|
||||
<game-links />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import GameLinks from '@/components/GameDetail/GameLinks';
|
||||
import GameRating from '@/components/GameDetail/GameRating';
|
||||
import { mapState, mapGetters } from 'vuex';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GameRating,
|
||||
GameLinks,
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapState([
|
||||
'game',
|
||||
'platforms',
|
||||
]),
|
||||
|
||||
...mapGetters([
|
||||
'playerPerspectives',
|
||||
'developers',
|
||||
'gameModes',
|
||||
'gamePlatforms',
|
||||
'genres',
|
||||
'publishers',
|
||||
'releaseDate',
|
||||
]),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" rel="stylesheet/scss" scoped>
|
||||
@import "~styles/styles.scss";
|
||||
|
||||
.review-box {
|
||||
display: grid;
|
||||
grid-template-columns: 100px auto;
|
||||
grid-gap: $gp;
|
||||
|
||||
strong {
|
||||
color: $color-red;
|
||||
}
|
||||
}
|
||||
</style>
|
43
src/components/GameDetail/GameScreenshots.vue
Normal file
|
@ -0,0 +1,43 @@
|
|||
<template lang="html">
|
||||
<section v-if="game.screenshots">
|
||||
<h3>Screenshots ({{ game.screenshots.length }})</h3>
|
||||
|
||||
<div class="gallery">
|
||||
<img
|
||||
v-for="({ cloudinary_id }, index) in game.screenshots"
|
||||
:key="cloudinary_id"
|
||||
:src="`https://images.igdb.com/igdb/image/upload/t_screenshot_med/${cloudinary_id}.jpg`"
|
||||
:alt="`Screenshot ${index + 1} of ${game.screenshots.length}`"
|
||||
>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex';
|
||||
|
||||
export default {
|
||||
computed: {
|
||||
...mapState(['game']),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" rel="stylesheet/scss" scoped>
|
||||
@import "~styles/styles.scss";
|
||||
|
||||
.gallery {
|
||||
display: grid;
|
||||
grid-template-columns: 50% 50%;
|
||||
|
||||
@media($small) {
|
||||
grid-template-columns: 100%;
|
||||
}
|
||||
|
||||
img {
|
||||
padding: $gp / 4;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
56
src/components/GameDetail/GameVideos.vue
Normal file
|
@ -0,0 +1,56 @@
|
|||
<template lang="html">
|
||||
<div class="game-videos no-wrap" v-if="game.videos">
|
||||
<h3>Videos ({{ game.videos.length }})</h3>
|
||||
|
||||
<!-- TODO: create gallery with thumbs -->
|
||||
|
||||
<div
|
||||
class="game-video"
|
||||
v-for="{ video_id } in game.videos"
|
||||
:key="video_id"
|
||||
>
|
||||
<iframe
|
||||
:src="`https://www.youtube.com/embed/${video_id}?rel=0&autohide=1`"
|
||||
frameborder="0"
|
||||
width="426"
|
||||
height="240"
|
||||
allow="autoplay; encrypted-media"
|
||||
allowfullscreen
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex';
|
||||
|
||||
export default {
|
||||
computed: {
|
||||
...mapState(['game']),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" rel="stylesheet/scss" scoped>
|
||||
@import "~styles/styles.scss";
|
||||
|
||||
.game-video {
|
||||
margin-bottom: $gp / 2;
|
||||
position: relative;
|
||||
padding-bottom: 56.25%; /* 16:9 */
|
||||
padding-top: 25px;
|
||||
|
||||
iframe {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 50%;
|
||||
height: 50% !important;
|
||||
|
||||
@media($small) {
|
||||
width: 100%;
|
||||
height: 100% !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
127
src/components/GameSearch/GameSearch.vue
Executable file
|
@ -0,0 +1,127 @@
|
|||
<template lang="html">
|
||||
<form @submit.prevent="search" class="game-search">
|
||||
<input ref="searchInput" type="text" v-model="searchText" placeholder="Type here">
|
||||
|
||||
<div class="search-results" v-if="!loading && filteredResults.length > 0">
|
||||
<game-card
|
||||
v-for="{ id } in filteredResults"
|
||||
v-if="!list.games.includes(id)"
|
||||
:key="id"
|
||||
:game-id="id"
|
||||
:listId="listId"
|
||||
@added="added"
|
||||
search-result
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="!loading && filteredResults.length === 0">
|
||||
No results
|
||||
</div>
|
||||
|
||||
<content-placeholders v-for="n in 3" :key="n" v-if="loading">
|
||||
<content-placeholders-heading :img="true" />
|
||||
</content-placeholders>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import GameCard from '@/components/GameCard/GameCard';
|
||||
import { debounce } from 'lodash';
|
||||
import { mapState, mapGetters } from 'vuex';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GameCard,
|
||||
},
|
||||
|
||||
props: {
|
||||
listId: {
|
||||
type: [Number, String, Boolean],
|
||||
required: true,
|
||||
default: 0,
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
searchText: '',
|
||||
loading: false,
|
||||
styles: {
|
||||
width: '95%',
|
||||
'max-width': '800px',
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapState(['results', 'user']),
|
||||
...mapGetters(['auth']),
|
||||
|
||||
list() {
|
||||
return this.user.lists[this.listId];
|
||||
},
|
||||
|
||||
filteredResults() {
|
||||
return this.results.filter(({ id }) => !this.list.games.includes(id));
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
searchText(value) {
|
||||
if (value) {
|
||||
this.search();
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.$refs.searchInput.focus();
|
||||
},
|
||||
|
||||
methods: {
|
||||
added() {
|
||||
this.$emit('added');
|
||||
},
|
||||
|
||||
search: debounce(
|
||||
// eslint-disable-next-line
|
||||
function() {
|
||||
this.loading = true;
|
||||
|
||||
this.$store.dispatch('SEARCH', this.searchText)
|
||||
.then(() => {
|
||||
this.error = null;
|
||||
this.loading = false;
|
||||
})
|
||||
.catch(({ data }) => {
|
||||
this.loading = false;
|
||||
this.error = data;
|
||||
});
|
||||
}, 300),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" rel="stylesheet/scss" scoped>
|
||||
@import "~styles/styles.scss";
|
||||
|
||||
.game-search {
|
||||
background: $color-light-gray;
|
||||
margin-top: 30px;
|
||||
padding: $gp / 2;
|
||||
}
|
||||
|
||||
.search-results {
|
||||
// min-height: 120px;
|
||||
overflow: auto;
|
||||
max-height: calc(100vh - 160px);
|
||||
}
|
||||
|
||||
input {
|
||||
margin: 0 0 $gp / 2;
|
||||
}
|
||||
|
||||
.vue-content-placeholders {
|
||||
margin-bottom: $gp / 2;
|
||||
}
|
||||
</style>
|
138
src/components/Lists/AddList.vue
Executable file
|
@ -0,0 +1,138 @@
|
|||
<template lang="html">
|
||||
<div class="add-list">
|
||||
<form @submit.prevent="addList" v-if="show">
|
||||
<input
|
||||
v-model="newListName"
|
||||
type="text"
|
||||
ref="newListName"
|
||||
required
|
||||
placeholder="List name"
|
||||
/>
|
||||
|
||||
<!-- TODO: replace with toast -->
|
||||
<small v-if="isDuplicate" v-html="errorMessage" />
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
class="small primary"
|
||||
v-if="!isDuplicate"
|
||||
:disabled="!newListName.length"
|
||||
>
|
||||
Create
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="small accent"
|
||||
type="button"
|
||||
@click="reset"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<button class="add small info hollow" @click="toggleAddList" v-else>
|
||||
<i class="fas fa-plus" />
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex';
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
show: false,
|
||||
newListName: '',
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapState(['user']),
|
||||
|
||||
errorMessage() {
|
||||
return `You already have a list named <strong>${this.newListName}</strong>. Please use a different name.`;
|
||||
},
|
||||
|
||||
nightMode() {
|
||||
return this.user.settings.nightMode;
|
||||
},
|
||||
|
||||
isDuplicate() {
|
||||
const newListName = this.newListName.toLowerCase();
|
||||
// eslint-disable-next-line
|
||||
return this.user.lists.filter(({ name }) => name.toLowerCase() === newListName).length > 0;
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
show() {
|
||||
this.$nextTick(() => {
|
||||
if (this.$refs.newListName) {
|
||||
this.$refs.newListName.focus();
|
||||
}
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
toggleAddList() {
|
||||
if (!this.show) {
|
||||
this.$nextTick(() => {
|
||||
this.$emit('scroll');
|
||||
});
|
||||
}
|
||||
|
||||
this.show = !this.show;
|
||||
},
|
||||
|
||||
addList() {
|
||||
this.$store.commit('ADD_LIST', this.newListName);
|
||||
this.$emit('update');
|
||||
this.$emit('scroll');
|
||||
this.reset();
|
||||
|
||||
this.$swal({
|
||||
position: 'bottom-end',
|
||||
title: 'List added',
|
||||
type: 'success',
|
||||
toast: true,
|
||||
showConfirmButton: false,
|
||||
timer: 1500,
|
||||
});
|
||||
},
|
||||
|
||||
reset() {
|
||||
this.show = false;
|
||||
this.newListName = '';
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" rel="stylesheet/scss" scoped>
|
||||
@import "~styles/styles.scss";
|
||||
|
||||
.add {
|
||||
margin-right: $gp;
|
||||
}
|
||||
|
||||
form {
|
||||
border-radius: $border-radius;
|
||||
background: $color-light-gray;
|
||||
padding: $gp / 2;
|
||||
margin-right: $gp;
|
||||
}
|
||||
|
||||
small {
|
||||
background: $color-white;
|
||||
margin-bottom: $gp / 2;
|
||||
display: block;
|
||||
padding: $gp / 2;
|
||||
border-radius: $border-radius;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 240px;
|
||||
}
|
||||
</style>
|
75
src/components/NavHeader/NavHeader.vue
Executable file
|
@ -0,0 +1,75 @@
|
|||
<template lang="html">
|
||||
<nav>
|
||||
<platforms-dropdown />
|
||||
|
||||
<strong>gamebrary</strong>
|
||||
|
||||
<div class="links">
|
||||
<template v-if="auth">
|
||||
<router-link tag="button" class="info" :to="{ name: 'admin' }" v-if="user.admin">
|
||||
<i class="fas fa-screwdriver" />
|
||||
</router-link>
|
||||
|
||||
<router-link tag="button" class="info" :to="{ name: 'settings' }">
|
||||
<i class="fas fa-cog" />
|
||||
</router-link>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<router-link tag="button" class="info" :to="{ name: 'login' }">
|
||||
Login
|
||||
</router-link>
|
||||
|
||||
<router-link tag="button" class="primary" :to="{ name: 'register' }">
|
||||
Register
|
||||
</router-link>
|
||||
</template>
|
||||
</div>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import PlatformsDropdown from '@/components/PlatformsDropdown/PlatformsDropdown';
|
||||
import { mapGetters, mapState } from 'vuex';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
PlatformsDropdown,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
msg: 'test',
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapState(['user']),
|
||||
...mapGetters(['auth']),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" rel="stylesheet/scss" scoped>
|
||||
@import "~styles/styles.scss";
|
||||
|
||||
nav {
|
||||
height: $navHeight;
|
||||
width: 100%;
|
||||
background: $color-dark-gray;
|
||||
padding-right: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
a {
|
||||
color: $color-white;
|
||||
}
|
||||
|
||||
strong {
|
||||
color: $color-white;
|
||||
text-transform: uppercase;
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
</style>
|
44
src/components/Panel/Panel.vue
Normal file
|
@ -0,0 +1,44 @@
|
|||
<template lang="html">
|
||||
<div class="panel">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" rel="stylesheet/scss" scoped>
|
||||
@import "~styles/styles.scss";
|
||||
|
||||
.panel {
|
||||
padding: $gp / 2;
|
||||
border-radius: $border-radius;
|
||||
margin: $gp;
|
||||
|
||||
p {
|
||||
margin: $gp / 2 0;
|
||||
}
|
||||
|
||||
&.warning {
|
||||
background: $color-orange;
|
||||
color: darken($color-orange, 40);
|
||||
}
|
||||
|
||||
&.positive {
|
||||
background: $color-green;
|
||||
color: lighten($color-green, 50);
|
||||
}
|
||||
|
||||
&.info {
|
||||
background: $color-gray;
|
||||
color: lighten($color-gray, 40);
|
||||
}
|
||||
|
||||
&.error {
|
||||
background: $color-red;
|
||||
color: lighten($color-red, 40);
|
||||
}
|
||||
}
|
||||
</style>
|
86
src/components/PlatformsDropdown/PlatformsDropdown.vue
Executable file
|
@ -0,0 +1,86 @@
|
|||
<template lang="html">
|
||||
<div class="platform-switcher">
|
||||
<router-link :to="{ name: 'home' }" v-if="!platform">
|
||||
<i class="fas fa-home" />
|
||||
</router-link>
|
||||
|
||||
<v-popover v-else>
|
||||
<span>{{ platform.name }}</span>
|
||||
<i class="fas fa-caret-down dropdown" />
|
||||
|
||||
<template slot="popover">
|
||||
<div class="platforms">
|
||||
<a
|
||||
v-close-popover
|
||||
v-for="platform in platforms"
|
||||
:key="platform.name"
|
||||
@click="changePlatform(platform)"
|
||||
>
|
||||
{{ platform.name }}
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
</v-popover>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex';
|
||||
// eslint-disable-next-line
|
||||
import platforms from '@/platforms';
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
platforms,
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapState(['platform']),
|
||||
},
|
||||
|
||||
methods: {
|
||||
changePlatform(platform) {
|
||||
this.$store.commit('SET_PLATFORM', platform);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" rel="stylesheet/scss" scoped>
|
||||
@import "~styles/styles.scss";
|
||||
|
||||
.platform-switcher {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 0 $gp;
|
||||
color: $color-white;
|
||||
|
||||
a {
|
||||
color: $color-white;
|
||||
}
|
||||
}
|
||||
|
||||
.platforms {
|
||||
background: $color-white;
|
||||
border: 4px solid $color-dark-gray;
|
||||
border-top: 0;
|
||||
position: fixed;
|
||||
left: 0;
|
||||
width: 200px;
|
||||
height: 300px;
|
||||
overflow: auto;
|
||||
overflow: overlay;
|
||||
|
||||
a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: $gp / 2;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
padding: $gp / 2;
|
||||
}
|
||||
</style>
|
77
src/components/Register/PasswordStrengthIndicator.vue
Normal file
|
@ -0,0 +1,77 @@
|
|||
<template lang="html">
|
||||
<div class="password-strength-indicator" v-if="value">
|
||||
<h4>Password requirements:</h4>
|
||||
|
||||
<span :class="{ valid: meetsLength }">
|
||||
<i class="far fa-check-square" v-if="meetsLength"/>
|
||||
<i class="far fa-square" v-else />
|
||||
8 Character minimum
|
||||
</span>
|
||||
|
||||
<span :class="{ valid: hasNumber }">
|
||||
<i class="far fa-check-square" v-if="hasNumber"/>
|
||||
<i class="far fa-square" v-else />
|
||||
At least one number
|
||||
</span>
|
||||
|
||||
<span :class="{ valid: hasUppercase }">
|
||||
<i class="far fa-check-square" v-if="hasUppercase"/>
|
||||
<i class="far fa-square" v-else />
|
||||
At least one uppercase letter
|
||||
</span>
|
||||
|
||||
<span :class="{ valid: hasLowercase }">
|
||||
<i class="far fa-check-square" v-if="hasLowercase"/>
|
||||
<i class="far fa-square" v-else />
|
||||
At least one lowercase letter
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
value: String,
|
||||
},
|
||||
|
||||
computed: {
|
||||
meetsLength() {
|
||||
return this.value && this.value.length > 7;
|
||||
},
|
||||
|
||||
hasNumber() {
|
||||
return /\d/.test(this.value);
|
||||
},
|
||||
|
||||
hasUppercase() {
|
||||
return (/[A-Z]/.test(this.value));
|
||||
},
|
||||
|
||||
hasLowercase() {
|
||||
return (/[a-z]/.test(this.value));
|
||||
},
|
||||
|
||||
isValid() {
|
||||
const { meetsLength, hasNumber, hasUppercase, hasLowercase } = this;
|
||||
return meetsLength && hasNumber && hasUppercase && hasLowercase;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" rel="stylesheet/scss" scoped>
|
||||
@import "~styles/styles.scss";
|
||||
|
||||
.password-strength-indicator {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
span {
|
||||
color: $color-red;
|
||||
|
||||
&.valid {
|
||||
color: $color-green;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
14
src/i18n.js
Normal file
|
@ -0,0 +1,14 @@
|
|||
const messages = {
|
||||
en: {
|
||||
message: {
|
||||
hello: 'hello world',
|
||||
},
|
||||
},
|
||||
ja: {
|
||||
message: {
|
||||
hello: 'こんにちは、世界',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default messages;
|
61
src/main.js
Executable file
|
@ -0,0 +1,61 @@
|
|||
import VueDragscroll from 'vue-dragscroll';
|
||||
import VueContentPlaceholders from 'vue-content-placeholders';
|
||||
import axios from 'axios';
|
||||
import VueSweetalert2 from 'vue-sweetalert2';
|
||||
import VTooltip from 'v-tooltip';
|
||||
import VueAxios from 'vue-axios';
|
||||
import Vue from 'vue';
|
||||
import VueI18n from 'vue-i18n';
|
||||
import store from './store/';
|
||||
import App from './App';
|
||||
import router from './router';
|
||||
import messages from './i18n';
|
||||
|
||||
const EventBus = new Vue();
|
||||
|
||||
axios.interceptors.response.use(response => response, (error) => {
|
||||
if (error && error.response && error.response.status === 401) {
|
||||
window.location.href = '/#/session-expired';
|
||||
}
|
||||
});
|
||||
|
||||
Object.defineProperties(Vue.prototype, {
|
||||
$bus: {
|
||||
get() {
|
||||
return EventBus;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
Vue.use(VueI18n);
|
||||
Vue.use(VTooltip);
|
||||
Vue.use(VueSweetalert2);
|
||||
Vue.use(VueContentPlaceholders);
|
||||
Vue.use(VueDragscroll);
|
||||
Vue.use(VueAxios, axios);
|
||||
|
||||
Vue.config.productionTip = false;
|
||||
|
||||
router.beforeEach((to, from, next) => {
|
||||
if (to.meta.requiresAuth && !store.getters.auth) {
|
||||
next('/');
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
});
|
||||
|
||||
const i18n = new VueI18n({
|
||||
// TODO: get locale from user settings store
|
||||
locale: 'en',
|
||||
messages,
|
||||
});
|
||||
|
||||
/* eslint-disable no-new */
|
||||
new Vue({
|
||||
el: '#app',
|
||||
router,
|
||||
i18n,
|
||||
store,
|
||||
components: { App },
|
||||
template: '<App/>',
|
||||
});
|
31
src/mixins/toasts.js
Normal file
|
@ -0,0 +1,31 @@
|
|||
export default {
|
||||
data() {
|
||||
return {
|
||||
timer: 1500,
|
||||
};
|
||||
},
|
||||
|
||||
methods: {
|
||||
$error(title) {
|
||||
this.$swal({
|
||||
title,
|
||||
toast: true,
|
||||
timer: this.timer,
|
||||
position: 'bottom-end',
|
||||
type: 'error',
|
||||
showConfirmButton: false,
|
||||
});
|
||||
},
|
||||
|
||||
$success(title) {
|
||||
this.$swal({
|
||||
position: 'bottom-end',
|
||||
title,
|
||||
type: 'success',
|
||||
toast: true,
|
||||
showConfirmButton: false,
|
||||
timer: this.timer,
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
159
src/pages/Admin/Admin.vue
Normal file
|
@ -0,0 +1,159 @@
|
|||
<template lang="html">
|
||||
<div class="admin">
|
||||
<div class="loading" v-if="!adminData">
|
||||
<i class="fas fa-circle-notch fast-spin fa-3x" />
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<header class="stats">
|
||||
<strong>{{ adminData.length }} Users</strong>
|
||||
|
||||
<button class="primary" @click="load">
|
||||
<i class="fas fa-sync" />
|
||||
Reload data
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<div class="users">
|
||||
<div class="user">
|
||||
<strong>Avatar</strong>
|
||||
<strong>Email</strong>
|
||||
<strong>Lists</strong>
|
||||
<strong>Game count</strong>
|
||||
<strong>Joined</strong>
|
||||
<strong>View</strong>
|
||||
</div>
|
||||
|
||||
<div class="user" v-for="user in adminData" :key="user._id">
|
||||
<gravatar :email="user.email" />
|
||||
|
||||
{{ user.email }}
|
||||
|
||||
<div class="lists">
|
||||
<!-- {{ user.lists.length }} -->
|
||||
{{ getListNames(user.lists) }}
|
||||
</div>
|
||||
|
||||
<span>
|
||||
{{ getGameCount(user.lists) }}
|
||||
</span>
|
||||
|
||||
<span :title="moment(user.dateJoined).format('LL')">
|
||||
{{ moment(user.dateJoined).fromNow() }}
|
||||
</span>
|
||||
|
||||
<router-link
|
||||
tag="button"
|
||||
class="small info"
|
||||
:to="{ name: 'share', params: { id: user._id }}"
|
||||
>
|
||||
<i class="fas fa-link"></i>
|
||||
View
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex';
|
||||
import Gravatar from 'vue-gravatar';
|
||||
import moment from 'moment';
|
||||
import toasts from '@/mixins/toasts';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Gravatar,
|
||||
},
|
||||
|
||||
mixins: [toasts],
|
||||
|
||||
data() {
|
||||
return {
|
||||
moment,
|
||||
users: null,
|
||||
error: false,
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapState(['user', 'adminData']),
|
||||
},
|
||||
|
||||
mounted() {
|
||||
if (!this.adminData) {
|
||||
this.load();
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
getListNames(lists) {
|
||||
return lists.map(({ name }) => name).join(', ');
|
||||
},
|
||||
|
||||
getGameCount(lists) {
|
||||
return lists.map(({ games }) => games.length).reduce((a, b) => a + b, 0);
|
||||
},
|
||||
|
||||
load() {
|
||||
if (this.user.admin) {
|
||||
this.$store.dispatch('LOAD_USERS').catch(this.exit);
|
||||
} else {
|
||||
this.exit();
|
||||
}
|
||||
},
|
||||
|
||||
exit() {
|
||||
this.$error('Admins only, sorry!');
|
||||
this.$router.push({ name: 'home' });
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" rel="stylesheet/scss" scoped>
|
||||
@import "~styles/styles.scss";
|
||||
|
||||
.loading {
|
||||
height: calc(100vh - 48px);
|
||||
color: $color-dark-gray;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.stats {
|
||||
padding: $gp;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
strong {
|
||||
font-size: 24px;
|
||||
color: $color-light-gray;
|
||||
}
|
||||
}
|
||||
|
||||
.users {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-x: auto;
|
||||
|
||||
.user {
|
||||
padding: $gp / 3 $gp;
|
||||
display: grid;
|
||||
grid-template-columns: 50px 240px 300px 100px 120px 120px 120px;
|
||||
grid-gap: 10px;
|
||||
align-items: center;
|
||||
|
||||
&:hover {
|
||||
background: $color-light-gray;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 50px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
204
src/pages/GameBoard/GameBoard.vue
Executable file
|
@ -0,0 +1,204 @@
|
|||
<template lang="html">
|
||||
<div
|
||||
class="lists"
|
||||
ref="lists"
|
||||
@dragscrollstart="dragScrollActive = true"
|
||||
@dragscrollend="dragScrollActive = false"
|
||||
:class="{ nightMode, 'drag-scroll-active': dragScrollActive }"
|
||||
v-dragscroll:nochilddrag
|
||||
>
|
||||
<list
|
||||
:name="list.name"
|
||||
:games="list.games"
|
||||
:listIndex="listIndex"
|
||||
:key="list.name"
|
||||
v-if="list"
|
||||
v-for="(list, listIndex) in user.lists"
|
||||
@end="dragEnd"
|
||||
@remove="tryDelete(listIndex)"
|
||||
/>
|
||||
|
||||
<add-list
|
||||
@update="updateLists(true)"
|
||||
@scroll="scroll"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { dragscroll } from 'vue-dragscroll';
|
||||
import AddList from '@/components/Lists/AddList';
|
||||
import toasts from '@/mixins/toasts';
|
||||
import List from '@/components/GameBoard/List';
|
||||
import draggable from 'vuedraggable';
|
||||
import moment from 'moment';
|
||||
import { mapState } from 'vuex';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
draggable,
|
||||
List,
|
||||
AddList,
|
||||
},
|
||||
|
||||
directives: {
|
||||
dragscroll,
|
||||
},
|
||||
|
||||
mixins: [toasts],
|
||||
|
||||
data() {
|
||||
return {
|
||||
dragging: false,
|
||||
draggingId: null,
|
||||
gameData: null,
|
||||
loading: false,
|
||||
activeList: null,
|
||||
showDeleteConfirm: false,
|
||||
dragScrollActive: false,
|
||||
listDraggableOptions: {
|
||||
animation: 500,
|
||||
handle: '.list-drag-handle',
|
||||
group: { name: 'lists' },
|
||||
draggable: '.list',
|
||||
ghostClass: 'list-placeholder',
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapState(['user']),
|
||||
|
||||
isEmpty() {
|
||||
return !this.user.lists.filter(list => list && list.games && list.games.length).length;
|
||||
},
|
||||
|
||||
nightMode() {
|
||||
return this.user && this.user.settings ? this.user.settings.nightMode : false;
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
if (!this.isEmpty) {
|
||||
this.loadGameData();
|
||||
}
|
||||
|
||||
this.checkDataAge();
|
||||
},
|
||||
|
||||
methods: {
|
||||
checkDataAge() {
|
||||
const lastUpdated = this.$store.state.dataUpdatedTimestamp;
|
||||
const diff = moment.duration(moment().diff(lastUpdated));
|
||||
|
||||
if (diff.asMinutes() > 15) {
|
||||
this.$store.dispatch('LOAD_LISTS');
|
||||
}
|
||||
},
|
||||
|
||||
scroll() {
|
||||
this.$nextTick(() => {
|
||||
const lists = this.$refs.lists;
|
||||
lists.scrollLeft = lists.scrollWidth;
|
||||
});
|
||||
},
|
||||
|
||||
tryDelete(index) {
|
||||
const hasGames = this.user.lists[index].games.length > 0;
|
||||
|
||||
if (hasGames) {
|
||||
this.showDeleteConfirm = true;
|
||||
this.activeList = index;
|
||||
|
||||
this.$swal({
|
||||
title: 'Are you sure?',
|
||||
text: 'This lists contains games, all games will be deleted as well.',
|
||||
showCancelButton: true,
|
||||
buttonsStyling: false,
|
||||
confirmButtonClass: 'primary small',
|
||||
cancelButtonClass: 'small',
|
||||
confirmButtonText: 'Delete',
|
||||
}).then(({ value }) => {
|
||||
if (value) {
|
||||
this.deleteList(this.activeList);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.deleteList(index);
|
||||
}
|
||||
},
|
||||
|
||||
deleteList(index) {
|
||||
this.$store.commit('REMOVE_LIST', index);
|
||||
this.updateLists();
|
||||
this.$success('List deleted');
|
||||
},
|
||||
|
||||
dragEnd() {
|
||||
this.dragging = false;
|
||||
this.draggingId = null;
|
||||
this.$store.commit('UPDATE_LIST', this.user.lists);
|
||||
this.updateLists();
|
||||
},
|
||||
|
||||
updateLists(forceReload) {
|
||||
this.$store.dispatch('UPDATE_LISTS')
|
||||
.then(() => {
|
||||
if (this.user.lists.length === 1 && forceReload) {
|
||||
location.reload();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
loadGameData() {
|
||||
const gameList = [];
|
||||
this.user.lists.forEach((list) => {
|
||||
if (list && list.games.length) {
|
||||
list.games.forEach((id) => {
|
||||
if (!gameList.includes(id)) {
|
||||
gameList.push(id);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
this.$store.dispatch('LOAD_GAMES', gameList)
|
||||
.catch(() => {
|
||||
this.$swal({
|
||||
title: 'Uh no!',
|
||||
text: 'There was an error loading your game data',
|
||||
type: 'error',
|
||||
showCancelButton: true,
|
||||
confirmButtonClass: 'primary',
|
||||
confirmButtonText: 'Retry',
|
||||
}).then(({ value }) => {
|
||||
if (value) {
|
||||
this.loadGameData();
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" rel="stylesheet/scss" scoped>
|
||||
@import "~styles/styles.scss";
|
||||
@import "~styles/game-board.scss";
|
||||
|
||||
.draggable * {
|
||||
color: $color-white;
|
||||
}
|
||||
|
||||
.nightMode {
|
||||
background: #333 !important;
|
||||
|
||||
.games {
|
||||
background: $color-dark-gray;
|
||||
}
|
||||
|
||||
.list-header {
|
||||
box-shadow: 0 0 5px 5px $color-dark-gray;
|
||||
}
|
||||
}
|
||||
</style>
|
64
src/pages/GameDetail/GameDetail.vue
Normal file
|
@ -0,0 +1,64 @@
|
|||
<template lang="html">
|
||||
<div class="game" v-if="game">
|
||||
<game-header />
|
||||
|
||||
<section class="game-details">
|
||||
<h2 v-html="game.name" />
|
||||
<p class="game-description" v-html="game.summary" />
|
||||
<game-review-box />
|
||||
<game-screenshots />
|
||||
<game-videos />
|
||||
<div class="source">
|
||||
Source: <a href="https://www.igdb.com/">IGDB</a>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex';
|
||||
import GameHeader from '@/components/GameDetail/GameHeader';
|
||||
import GameScreenshots from '@/components/GameDetail/GameScreenshots';
|
||||
import GameVideos from '@/components/GameDetail/GameVideos';
|
||||
import GameReviewBox from '@/components/GameDetail/GameReviewBox';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GameHeader,
|
||||
GameScreenshots,
|
||||
GameVideos,
|
||||
GameReviewBox,
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapState(['game']),
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.$store.commit('SET_ACTIVE_GAME', this.$route.params.id);
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" rel="stylesheet/scss" scoped>
|
||||
@import "~styles/styles.scss";
|
||||
|
||||
.game {
|
||||
border-radius: $border-radius;
|
||||
background-color: $color-light-gray;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.game-details {
|
||||
padding: 0 $gp;
|
||||
}
|
||||
.source {
|
||||
padding-bottom: $gp;
|
||||
text-align: center;
|
||||
|
||||
a {
|
||||
color: $color-blue;
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
</style>
|
41
src/pages/Home/Home.vue
Executable file
|
@ -0,0 +1,41 @@
|
|||
<template lang="html">
|
||||
<main>
|
||||
<public-home v-if="!auth" />
|
||||
<game-board v-if="auth" />
|
||||
<!-- <game-board v-if="auth && platform" /> -->
|
||||
<!-- <platforms v-else /> -->
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import PublicHome from '@/pages/Home/PublicHome';
|
||||
import GameBoard from '@/pages/GameBoard/GameBoard';
|
||||
import Platforms from '@/pages/Platforms/Platforms';
|
||||
import { mapGetters, mapState } from 'vuex';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
PublicHome,
|
||||
GameBoard,
|
||||
Platforms,
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapState(['platform', 'user']),
|
||||
...mapGetters(['auth']),
|
||||
|
||||
hasPlatforms() {
|
||||
return this.user.platforms.length > 0 || this.platform;
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
platform(value) {
|
||||
// eslint-disable-next-line
|
||||
console.log('reload game board!');
|
||||
// eslint-disable-next-line
|
||||
console.log(value);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
53
src/pages/Home/PublicHome.vue
Executable file
|
@ -0,0 +1,53 @@
|
|||
<template lang="html">
|
||||
<div class="home">
|
||||
<h1>GAMEBRARY</h1>
|
||||
|
||||
<strong>500+ users! Thank you!</strong>
|
||||
|
||||
<p>A simple and user friendly way to manage your video game collection.</p>
|
||||
|
||||
<div class="actions">
|
||||
<router-link tag="button" class="primary" :to="{ name: 'register' }">
|
||||
Create an account
|
||||
</router-link>
|
||||
|
||||
<router-link tag="button" class="info" :to="{ name: 'login' }">
|
||||
Login
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Panel from '@/components/Panel/Panel';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Panel,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" rel="stylesheet/scss" scoped>
|
||||
@import "~styles/styles.scss";
|
||||
|
||||
.home {
|
||||
@include container-xs;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: $gp;
|
||||
border-bottom: 1px solid $color-light-gray;
|
||||
padding-bottom: $gp;
|
||||
|
||||
.info {
|
||||
margin-left: $gp;
|
||||
}
|
||||
}
|
||||
|
||||
strong {
|
||||
color: $color-green;
|
||||
}
|
||||
</style>
|
81
src/pages/Login/Login.vue
Executable file
|
@ -0,0 +1,81 @@
|
|||
<template lang="html">
|
||||
<form @submit.prevent="login" class="auth-form">
|
||||
<h3>Login</h3>
|
||||
<input type="email" ref="email" v-model="formModel.email" placeholder="Email" />
|
||||
<input type="password" v-model="formModel.password" placeholder="Password">
|
||||
<div class="checkbox">
|
||||
<span class="toggle-switch">
|
||||
<input type="checkbox" id="persist" v-model="formModel.persist" />
|
||||
<label for="persist" />
|
||||
</span>
|
||||
Keep me logged in
|
||||
</div>
|
||||
<button type="submit" class="primary" @click="login" :disabled="loading">Login</button>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
formModel: {
|
||||
email: '',
|
||||
password: '',
|
||||
persist: true,
|
||||
},
|
||||
loading: false,
|
||||
error: null,
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
disabled() {
|
||||
return this.loading || !this.formModel.email || !this.formModel.password;
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
if (this.$store.state.user) {
|
||||
this.$router.push({ name: 'home' });
|
||||
}
|
||||
|
||||
this.$refs.email.focus();
|
||||
},
|
||||
|
||||
methods: {
|
||||
login() {
|
||||
if (this.disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
|
||||
this.$store.dispatch('LOGIN', this.formModel)
|
||||
.then(() => {
|
||||
this.$router.push({ name: 'home' });
|
||||
})
|
||||
.catch(() => {
|
||||
this.loading = false;
|
||||
this.error = true;
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" rel="stylesheet/scss" scoped>
|
||||
@import "~styles/styles.scss";
|
||||
|
||||
form {
|
||||
@include container-xs;
|
||||
}
|
||||
|
||||
.toggle-switch, .checkbox {
|
||||
display: flex;
|
||||
margin-bottom: $gp;
|
||||
}
|
||||
|
||||
.toggle-switch {
|
||||
margin-right: $gp / 2;
|
||||
}
|
||||
</style>
|
37
src/pages/Platforms/Platforms.vue
Executable file
|
@ -0,0 +1,37 @@
|
|||
<template lang="html">
|
||||
<div>
|
||||
<div class="platforms">
|
||||
<a
|
||||
v-close-popover
|
||||
v-for="platform in platforms"
|
||||
:key="platform.name"
|
||||
@click="changePlatform(platform)"
|
||||
>
|
||||
{{ platform.name }}
|
||||
<br>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// eslint-disable-next-line
|
||||
import platforms from '@/platforms';
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
platforms,
|
||||
};
|
||||
},
|
||||
|
||||
methods: {
|
||||
changePlatform(platform) {
|
||||
this.$store.commit('SET_PLATFORM', platform);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" rel="stylesheet/scss" scoped>
|
||||
</style>
|
88
src/pages/Register/Register.vue
Executable file
|
@ -0,0 +1,88 @@
|
|||
<template lang="html">
|
||||
<form @submit.prevent="register" class="auth-form">
|
||||
<panel class="error" v-if="error">
|
||||
There was an error creating the account
|
||||
</panel>
|
||||
|
||||
<h3>Register</h3>
|
||||
<input type="email" ref="email" v-model="formModel.email" placeholder="Email" />
|
||||
<input type="password" v-model="formModel.password" placeholder="Password">
|
||||
|
||||
<panel class="warning" v-if="validationError">
|
||||
Please enter a valid password
|
||||
</panel>
|
||||
|
||||
<password-strength-indicator
|
||||
v-model="formModel.password"
|
||||
ref="passwordStrengthIndicator"
|
||||
/>
|
||||
|
||||
<button type="submit" class="primary" @click="register" :disabled="loading">
|
||||
Register
|
||||
</button>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import PasswordStrengthIndicator from '@/components/Register/PasswordStrengthIndicator';
|
||||
import Panel from '@/components/Panel/Panel';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
PasswordStrengthIndicator,
|
||||
Panel,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
formModel: {
|
||||
email: '',
|
||||
password: '',
|
||||
},
|
||||
error: false,
|
||||
validationError: false,
|
||||
loading: false,
|
||||
};
|
||||
},
|
||||
|
||||
mounted() {
|
||||
if (this.$store.state.user) {
|
||||
this.$router.push({ name: 'home' });
|
||||
}
|
||||
|
||||
this.$refs.email.focus();
|
||||
},
|
||||
|
||||
methods: {
|
||||
register() {
|
||||
this.validationError = false;
|
||||
|
||||
if (this.loading || !this.$refs.passwordStrengthIndicator.isValid) {
|
||||
this.validationError = true;
|
||||
return;
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
|
||||
this.$store.dispatch('REGISTER', this.formModel)
|
||||
.then(() => {
|
||||
this.error = false;
|
||||
this.loading = false;
|
||||
this.$router.push({ name: 'home' });
|
||||
})
|
||||
.catch(() => {
|
||||
this.error = true;
|
||||
this.loading = false;
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" rel="stylesheet/scss" scoped>
|
||||
@import "~styles/styles.scss";
|
||||
|
||||
form {
|
||||
@include container-xs;
|
||||
}
|
||||
</style>
|
13
src/pages/SessionExpired/SessionExpired.vue
Normal file
|
@ -0,0 +1,13 @@
|
|||
<template lang="html">
|
||||
<div>
|
||||
<h1>Session has expired</h1>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
mounted() {
|
||||
this.$store.commit('CLEAR_SESSION');
|
||||
},
|
||||
};
|
||||
</script>
|
275
src/pages/Settings/Settings.vue
Normal file
|
@ -0,0 +1,275 @@
|
|||
<template lang="html">
|
||||
<div class="settings">
|
||||
<aside>
|
||||
<gravatar :email="user.email" />
|
||||
|
||||
<div>
|
||||
<p><strong>App ID:</strong> {{ user._id }}</p>
|
||||
<p><strong>Email:</strong> {{ user.email }}</p>
|
||||
<p><strong>Joined:</strong> {{ dateJoined }}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button @click="promptDelete" class="small error">
|
||||
<i class="fas fa-exclamation-triangle" />
|
||||
Delete Account
|
||||
</button>
|
||||
|
||||
<button class="small info" @click="logout">
|
||||
<i class="fas fa-sign-out-alt" />
|
||||
Logout
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<main class="settings-grid">
|
||||
<section>
|
||||
<i class="fas fa-share-alt" />
|
||||
<h3>Share link</h3>
|
||||
|
||||
<input class="share-link value" type="text" v-model="shareUrl" readonly />
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<i class="fas fa-moon" />
|
||||
<h3>Night mode</h3>
|
||||
|
||||
<span class="toggle-switch value">
|
||||
<input type="checkbox" id="nightMode" v-model="settings.nightMode" />
|
||||
<label for="nightMode" />
|
||||
</span>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<i class="fas fa-star-half-alt" />
|
||||
<h3>Show Game Ratings</h3>
|
||||
|
||||
<span class="toggle-switch value">
|
||||
<input type="checkbox" id="gameRatings" v-model="settings.showGameRatings" />
|
||||
<label for="gameRatings" />
|
||||
</span>
|
||||
</section>
|
||||
|
||||
<!-- <section>
|
||||
<i class="fas fa-th-large" />
|
||||
<h3>Game Card Layout</h3>
|
||||
|
||||
<div class="value">
|
||||
<button
|
||||
:class="{ primary: settings.gameView === 'cover'}"
|
||||
@click="setGameView('cover')"
|
||||
>
|
||||
<i class="fas fa-portrait" />
|
||||
Game cover only
|
||||
</button>
|
||||
|
||||
<button
|
||||
:class="{ primary: settings.gameView === 'detailed' || !settings.gameView}"
|
||||
@click="setGameView('detailed')"
|
||||
>
|
||||
<i class="far fa-id-card" />
|
||||
Detailed view
|
||||
</button>
|
||||
</div>
|
||||
</section> -->
|
||||
|
||||
<panel class="positive">
|
||||
<p>
|
||||
<i class="fas fa-comments" />
|
||||
Have ideas, requests, feedback?
|
||||
</p>
|
||||
|
||||
<a class="link accent small" href="https://goo.gl/forms/r0juBCsZaUtJ03qb2">
|
||||
Submit feedback
|
||||
</a>
|
||||
</panel>
|
||||
|
||||
<panel class="info">
|
||||
<p>
|
||||
<i class="fas fa-bug" />
|
||||
Found a bug?
|
||||
</p>
|
||||
|
||||
<a class="link accent small" href="https://github.com/romancmx/switchlist/issues">
|
||||
Report it in GitHub
|
||||
</a>
|
||||
</panel>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { debounce } from 'lodash';
|
||||
import { mapState } from 'vuex';
|
||||
import Gravatar from 'vue-gravatar';
|
||||
import Panel from '@/components/Panel/Panel';
|
||||
import toasts from '@/mixins/toasts';
|
||||
import moment from 'moment';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Panel,
|
||||
Gravatar,
|
||||
},
|
||||
|
||||
mixins: [toasts],
|
||||
|
||||
data() {
|
||||
return {
|
||||
settings: {},
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapState(['user']),
|
||||
|
||||
dateJoined() {
|
||||
return moment(this.user.dateJoined).format('LL');
|
||||
},
|
||||
|
||||
shareUrl() {
|
||||
const url = process.env.NODE_ENV === 'development'
|
||||
? 'http://localhost:3000'
|
||||
: 'https://myswitchlist.com';
|
||||
|
||||
// eslint-disable-next-line
|
||||
return `${url}/#/share/${this.user._id}`;
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
settings: {
|
||||
handler(oldValue, newValue) {
|
||||
if (Object.keys(newValue).length) {
|
||||
this.save();
|
||||
}
|
||||
},
|
||||
deep: true,
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.settings = this.user.settings
|
||||
? JSON.parse(JSON.stringify(this.user.settings))
|
||||
: {};
|
||||
},
|
||||
|
||||
methods: {
|
||||
setGameView(view) {
|
||||
this.settings.gameView = view;
|
||||
this.save();
|
||||
},
|
||||
|
||||
promptDelete() {
|
||||
this.$swal({
|
||||
title: 'Are you sure?',
|
||||
text: 'Your account data will be deleted forever.',
|
||||
// type: 'warning',
|
||||
showCancelButton: true,
|
||||
confirmButtonClass: 'error',
|
||||
cancelButtonClass: 'accent',
|
||||
buttonsStyling: false,
|
||||
confirmButtonText: 'Yes, delete forever!',
|
||||
}).then(({ value }) => {
|
||||
if (value) {
|
||||
this.deleteAccount();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
logout() {
|
||||
this.$store.commit('CLEAR_SESSION');
|
||||
this.$router.push({ name: 'home' });
|
||||
},
|
||||
|
||||
deleteAccount() {
|
||||
this.$store.dispatch('DELETE_USER')
|
||||
.then(() => {
|
||||
this.$swal({
|
||||
position: 'bottom-end',
|
||||
title: 'Account deleted',
|
||||
type: 'success',
|
||||
toast: true,
|
||||
showConfirmButton: false,
|
||||
timer: 1500,
|
||||
});
|
||||
|
||||
this.logout();
|
||||
});
|
||||
},
|
||||
|
||||
save: debounce(
|
||||
// eslint-disable-next-line
|
||||
function() {
|
||||
this.$store.dispatch('UPDATE_SETTINGS', this.settings)
|
||||
.then(() => {
|
||||
this.$success('Settings saved');
|
||||
})
|
||||
.catch(() => {
|
||||
this.$error('There was an error saving your settings');
|
||||
});
|
||||
}, 500),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" rel="stylesheet/scss" scoped>
|
||||
@import "~styles/styles.scss";
|
||||
|
||||
.settings {
|
||||
background: $color-white;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 3fr;
|
||||
min-height: calc(100vh - #{$navHeight});
|
||||
|
||||
@media($small) {
|
||||
grid-template-columns: auto;
|
||||
}
|
||||
|
||||
aside {
|
||||
padding: $gp;
|
||||
background: $color-light-gray;
|
||||
}
|
||||
|
||||
main {
|
||||
section {
|
||||
color: $color-dark-gray;
|
||||
border-bottom: 1px solid $color-light-gray;
|
||||
padding: $gp * 2 $gp;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@media($small) {
|
||||
padding: $gp;
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: $color-green;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin: 0 $gp;
|
||||
}
|
||||
|
||||
.value {
|
||||
display: flex;
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.panel {
|
||||
width: 46%;
|
||||
float: left;
|
||||
|
||||
@media ($small) {
|
||||
width: 90%;
|
||||
}
|
||||
}
|
||||
|
||||
.share-link {
|
||||
max-width: 340px;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
136
src/pages/ShareList/ShareList.vue
Normal file
|
@ -0,0 +1,136 @@
|
|||
<template lang="html">
|
||||
<div class="public-game-board">
|
||||
<span v-if="loading">Loading...</span>
|
||||
|
||||
<router-link
|
||||
tag="button"
|
||||
class="small primary back"
|
||||
:to="{ name: 'admin' }"
|
||||
v-if="user && user.admin && !loading"
|
||||
>
|
||||
<i class="fas fa-chevron-left" />
|
||||
Back
|
||||
</router-link>
|
||||
|
||||
<div class="lists" v-if="!loading && games">
|
||||
<div class="list" v-for="list in lists" :key="list">
|
||||
<div class="list-header">
|
||||
{{ list.name }} ({{ list.games.length }})
|
||||
</div>
|
||||
|
||||
<div class="empty-list" v-if="!list.games.length">
|
||||
<img src="/static/img/empty-collection.png">
|
||||
<h3>This collection is empty</h3>
|
||||
</div>
|
||||
|
||||
<div class="games" v-else>
|
||||
<img
|
||||
v-for="game in list.games" :key="game"
|
||||
:src="getGameCover(game)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Panel from '@/components/Panel/Panel';
|
||||
import toasts from '@/mixins/toasts';
|
||||
import { mapState } from 'vuex';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Panel,
|
||||
},
|
||||
|
||||
mixins: [toasts],
|
||||
|
||||
data() {
|
||||
return {
|
||||
loading: true,
|
||||
lists: null,
|
||||
games: {},
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapState(['user']),
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.load();
|
||||
},
|
||||
|
||||
methods: {
|
||||
load() {
|
||||
const listId = this.$route.params.id;
|
||||
|
||||
if (listId) {
|
||||
this.$store.dispatch('LOAD_SHARE_LIST', listId)
|
||||
.then((lists) => {
|
||||
this.lists = lists;
|
||||
this.loadGameData();
|
||||
})
|
||||
.catch(() => {
|
||||
this.loading = false;
|
||||
this.$error('Error loading list');
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
getGameCover(id) {
|
||||
const url = 'https://images.igdb.com/igdb/image/upload/t_cover_small/';
|
||||
|
||||
return this.games && this.games[id].cover
|
||||
? `${url}${this.games[id].cover.cloudinary_id}.jpg`
|
||||
: '/static/no-image.jpg';
|
||||
},
|
||||
|
||||
loadGameData() {
|
||||
// TODO: Refactor this mess
|
||||
|
||||
const gameList = [];
|
||||
this.lists.forEach((list) => {
|
||||
if (list && list.games.length) {
|
||||
list.games.forEach((id) => {
|
||||
if (!gameList.includes(id)) {
|
||||
gameList.push(id);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (gameList.length > 0) {
|
||||
this.$store.dispatch('LOAD_SHARE_GAMES', gameList)
|
||||
.then((data) => {
|
||||
this.loading = false;
|
||||
|
||||
data.forEach((game) => {
|
||||
this.games[game.id] = { ...game };
|
||||
});
|
||||
});
|
||||
} else {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" rel="stylesheet/scss" scoped>
|
||||
@import "~styles/styles.scss";
|
||||
@import "~styles/game-board.scss";
|
||||
|
||||
.back {
|
||||
margin: $gp $gp 0;
|
||||
}
|
||||
|
||||
.games {
|
||||
column-count: 3;
|
||||
|
||||
img {
|
||||
padding: $gp / 4;
|
||||
}
|
||||
}
|
||||
</style>
|
282
src/platforms.js
Executable file
|
@ -0,0 +1,282 @@
|
|||
export default {
|
||||
xboxone: {
|
||||
name: 'Xbox One',
|
||||
code: 'Xbox One',
|
||||
id: 0,
|
||||
},
|
||||
nsw: {
|
||||
name: 'Nintendo Switch',
|
||||
code: 'Nintendo Switch',
|
||||
id: 130,
|
||||
},
|
||||
nes: {
|
||||
name: 'NES',
|
||||
code: 'NES',
|
||||
id: 0,
|
||||
},
|
||||
ps4: {
|
||||
name: 'PlayStation 4',
|
||||
code: 'PlayStation 4',
|
||||
id: 0,
|
||||
},
|
||||
mac: {
|
||||
name: 'Mac',
|
||||
code: 'Mac',
|
||||
id: 0,
|
||||
},
|
||||
win: {
|
||||
name: 'PC',
|
||||
code: 'PC',
|
||||
id: 0,
|
||||
},
|
||||
steam: {
|
||||
name: 'SteamOS',
|
||||
code: 'SteamOS',
|
||||
id: 0,
|
||||
},
|
||||
ps3: {
|
||||
name: 'PlayStation 3',
|
||||
code: 'PlayStation 3',
|
||||
id: 0,
|
||||
},
|
||||
psvita: {
|
||||
name: 'PlayStation Vita',
|
||||
code: 'PlayStation Vita',
|
||||
id: 0,
|
||||
},
|
||||
wiiu: {
|
||||
name: 'Wii U',
|
||||
code: 'Wii U',
|
||||
id: 0,
|
||||
},
|
||||
n3ds: {
|
||||
name: 'Nintendo 3DS',
|
||||
code: 'Nintendo 3DS',
|
||||
id: 0,
|
||||
},
|
||||
nds: {
|
||||
name: 'Nintendo DS',
|
||||
code: 'Nintendo DS',
|
||||
id: 0,
|
||||
},
|
||||
xbox360: {
|
||||
name: 'Xbox 360',
|
||||
code: 'Xbox 360',
|
||||
id: 0,
|
||||
},
|
||||
wii: {
|
||||
name: 'Wii',
|
||||
code: 'Wii',
|
||||
id: 0,
|
||||
},
|
||||
psp: {
|
||||
name: 'PSP',
|
||||
code: 'PSP',
|
||||
id: 0,
|
||||
},
|
||||
ps2: {
|
||||
name: 'PlayStation 2',
|
||||
code: 'PlayStation 2',
|
||||
id: 0,
|
||||
},
|
||||
ngage: {
|
||||
name: 'N-Gage',
|
||||
code: 'N-Gage',
|
||||
id: 0,
|
||||
},
|
||||
gba: {
|
||||
name: 'Game Boy Advance',
|
||||
code: 'Game Boy Advance',
|
||||
id: 0,
|
||||
},
|
||||
n64: {
|
||||
name: 'Nintendo 64',
|
||||
code: 'Nintendo 64',
|
||||
id: 0,
|
||||
},
|
||||
ngc: {
|
||||
name: 'Nintendo GameCube',
|
||||
code: 'Nintendo GameCube',
|
||||
id: 0,
|
||||
},
|
||||
xbox: {
|
||||
name: 'Xbox',
|
||||
code: 'Xbox',
|
||||
id: 0,
|
||||
},
|
||||
neogeopocketcolor: {
|
||||
name: 'Neo Geo Pocket Color',
|
||||
code: 'Neo Geo Pocket Color',
|
||||
id: 0,
|
||||
},
|
||||
neogeopocket: {
|
||||
name: 'Neo Geo Pocket',
|
||||
code: 'Neo Geo Pocket',
|
||||
id: 0,
|
||||
},
|
||||
dc: {
|
||||
name: 'Dreamcast',
|
||||
code: 'Dreamcast',
|
||||
id: 0,
|
||||
},
|
||||
gbc: {
|
||||
name: 'Game Boy Color',
|
||||
code: 'Game Boy Color',
|
||||
id: 0,
|
||||
},
|
||||
snes: {
|
||||
name: 'SNES',
|
||||
code: 'SNES',
|
||||
id: 0,
|
||||
},
|
||||
gb: {
|
||||
name: 'Game Boy',
|
||||
code: 'Game Boy',
|
||||
id: 0,
|
||||
},
|
||||
saturn: {
|
||||
name: 'Sega Saturn',
|
||||
code: 'Sega Saturn',
|
||||
id: 0,
|
||||
},
|
||||
ps: {
|
||||
name: 'PlayStation',
|
||||
code: 'PlayStation',
|
||||
id: 0,
|
||||
},
|
||||
sega32: {
|
||||
name: 'Sega 32X',
|
||||
code: 'Sega 32X',
|
||||
id: 0,
|
||||
},
|
||||
virtualboy: {
|
||||
name: 'Virtual Boy',
|
||||
code: 'Virtual Boy',
|
||||
id: 0,
|
||||
},
|
||||
neogeocd: {
|
||||
name: 'Neo Geo CD',
|
||||
code: 'Neo Geo CD',
|
||||
id: 0,
|
||||
},
|
||||
p3do: {
|
||||
name: '3DO',
|
||||
code: '3DO',
|
||||
id: 0,
|
||||
},
|
||||
jaguar: {
|
||||
name: 'Atari Jaguar',
|
||||
code: 'Atari Jaguar',
|
||||
id: 0,
|
||||
},
|
||||
segacd: {
|
||||
name: 'Sega CD',
|
||||
code: 'Sega CD',
|
||||
id: 0,
|
||||
},
|
||||
amiga: {
|
||||
name: 'Amiga',
|
||||
code: 'Amiga',
|
||||
id: 0,
|
||||
},
|
||||
gamegear: {
|
||||
name: 'Sega Game Gear',
|
||||
code: 'Sega Game Gear',
|
||||
id: 0,
|
||||
},
|
||||
philipscdi: {
|
||||
name: 'Philips CD-i',
|
||||
code: 'Philips CD-i',
|
||||
id: 0,
|
||||
},
|
||||
lynx: {
|
||||
name: 'Atari Lynx',
|
||||
code: 'Atari Lynx',
|
||||
id: 0,
|
||||
},
|
||||
turbografx: {
|
||||
name: 'Turbografx-16/PC Engine CD',
|
||||
code: 'Turbografx-16/PC Engine CD',
|
||||
id: 0,
|
||||
},
|
||||
smd: {
|
||||
name: 'Sega Genesis',
|
||||
code: 'Sega Genesis',
|
||||
id: 0,
|
||||
},
|
||||
sms: {
|
||||
name: 'Sega Master System',
|
||||
code: 'Sega Master System',
|
||||
id: 0,
|
||||
},
|
||||
atari5200: {
|
||||
name: 'Atari 5200',
|
||||
code: 'Atari 5200',
|
||||
id: 0,
|
||||
},
|
||||
atari7800: {
|
||||
name: 'Atari 7800',
|
||||
code: 'Atari 7800',
|
||||
id: 0,
|
||||
},
|
||||
c64: {
|
||||
name: 'Commodore C64/128',
|
||||
code: 'Commodore C64/128',
|
||||
id: 0,
|
||||
},
|
||||
atari2600: {
|
||||
name: 'Atari 2600',
|
||||
code: 'Atari 2600',
|
||||
id: 0,
|
||||
},
|
||||
vectrex: {
|
||||
name: 'Vectrex',
|
||||
code: 'Vectrex',
|
||||
id: 0,
|
||||
},
|
||||
msx: {
|
||||
name: 'MSX',
|
||||
code: 'MSX',
|
||||
id: 0,
|
||||
},
|
||||
msx2: {
|
||||
name: 'MSX2',
|
||||
code: 'MSX2',
|
||||
id: 0,
|
||||
},
|
||||
colecovision: {
|
||||
name: 'ColecoVision',
|
||||
code: 'ColecoVision',
|
||||
id: 0,
|
||||
},
|
||||
intellivision: {
|
||||
name: 'Intellivision',
|
||||
code: 'Intellivision',
|
||||
id: 0,
|
||||
},
|
||||
zxs: {
|
||||
name: 'ZX Spectrum',
|
||||
code: 'ZX Spectrum',
|
||||
id: 0,
|
||||
},
|
||||
dos: {
|
||||
name: 'PC DOS',
|
||||
code: 'PC DOS',
|
||||
id: 0,
|
||||
},
|
||||
atari8bit: {
|
||||
name: 'Atari 8-bit',
|
||||
code: 'Atari 8-bit',
|
||||
id: 0,
|
||||
},
|
||||
microvision: {
|
||||
name: 'Microvision',
|
||||
code: 'Microvision',
|
||||
id: 0,
|
||||
},
|
||||
appleii: {
|
||||
name: 'Apple II',
|
||||
code: 'Apple II',
|
||||
id: 0,
|
||||
},
|
||||
};
|
58
src/router/index.js
Executable file
|
@ -0,0 +1,58 @@
|
|||
import Vue from 'vue';
|
||||
import Router from 'vue-router';
|
||||
import Home from '@/pages/Home/Home';
|
||||
import ShareList from '@/pages/ShareList/ShareList';
|
||||
import SessionExpired from '@/pages/SessionExpired/SessionExpired';
|
||||
import Login from '@/pages/Login/Login';
|
||||
import GameDetail from '@/pages/GameDetail/GameDetail';
|
||||
import Settings from '@/pages/Settings/Settings';
|
||||
import Admin from '@/pages/Admin/Admin';
|
||||
import Register from '@/pages/Register/Register';
|
||||
|
||||
Vue.use(Router);
|
||||
|
||||
export default new Router({
|
||||
routes: [
|
||||
{
|
||||
path: '/share/:id',
|
||||
name: 'share',
|
||||
component: ShareList,
|
||||
},
|
||||
{
|
||||
name: 'game-detail',
|
||||
path: '/g/:id/:slug',
|
||||
component: GameDetail,
|
||||
},
|
||||
{
|
||||
name: 'settings',
|
||||
path: '/settings',
|
||||
component: Settings,
|
||||
},
|
||||
{
|
||||
path: '/session-expired',
|
||||
name: 'sessionExpired',
|
||||
component: SessionExpired,
|
||||
},
|
||||
{
|
||||
path: '/login',
|
||||
name: 'login',
|
||||
component: Login,
|
||||
},
|
||||
{
|
||||
path: '/register',
|
||||
name: 'register',
|
||||
component: Register,
|
||||
},
|
||||
{
|
||||
path: '/admin',
|
||||
name: 'admin',
|
||||
component: Admin,
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
name: 'home',
|
||||
component: Home,
|
||||
},
|
||||
{ path: '*', component: Home },
|
||||
],
|
||||
});
|
131
src/store/actions.js
Executable file
|
@ -0,0 +1,131 @@
|
|||
import axios from 'axios';
|
||||
|
||||
const API_URL = process.env.API_URL;
|
||||
|
||||
export default {
|
||||
REGISTER({ commit }, payload) {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios.post(`${API_URL}/auth/register`, payload)
|
||||
.then(({ data }) => {
|
||||
commit('SET_SESSION', data);
|
||||
resolve(data);
|
||||
}).catch(reject);
|
||||
});
|
||||
},
|
||||
|
||||
LOGIN({ commit }, payload) {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios.post(`${API_URL}/auth/login`, payload)
|
||||
.then(({ data }) => {
|
||||
commit('SET_SESSION', data);
|
||||
commit('SET_UPDATED_TIMESTAMP');
|
||||
resolve(data);
|
||||
}).catch(reject);
|
||||
});
|
||||
},
|
||||
|
||||
UPDATE_SETTINGS({ commit, state: { token } }, payload) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const options = { headers: { token } };
|
||||
|
||||
axios.put(`${API_URL}/settings`, payload, options)
|
||||
.then(({ data: { settings } }) => {
|
||||
commit('SET_SETTINGS', settings);
|
||||
resolve();
|
||||
}).catch(reject);
|
||||
});
|
||||
},
|
||||
|
||||
UPDATE_LISTS({ commit, state: { user, token } }) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const options = { headers: { token } };
|
||||
|
||||
const payload = {
|
||||
lists: user.lists,
|
||||
};
|
||||
|
||||
axios.put(`${API_URL}/lists`, payload, options)
|
||||
.then(() => {
|
||||
commit('SET_UPDATED_TIMESTAMP');
|
||||
resolve();
|
||||
}).catch(reject);
|
||||
});
|
||||
},
|
||||
|
||||
LOAD_LISTS({ commit, state: { token } }) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const options = { headers: { token } };
|
||||
|
||||
axios.get(`${API_URL}/lists`, options)
|
||||
.then(({ data: { lists } }) => {
|
||||
commit('UPDATE_LIST', lists);
|
||||
commit('SET_UPDATED_TIMESTAMP');
|
||||
resolve();
|
||||
}).catch(reject);
|
||||
});
|
||||
},
|
||||
|
||||
LOAD_USERS({ commit, state: { token } }) {
|
||||
commit('CLEAR_ADMIN_DATA');
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const options = { headers: { token } };
|
||||
|
||||
axios.get(`${API_URL}/users`, options)
|
||||
.then(({ data }) => {
|
||||
commit('SET_ADMIN_DATA', data.reverse());
|
||||
resolve();
|
||||
}).catch(reject);
|
||||
});
|
||||
},
|
||||
|
||||
LOAD_SHARE_LIST(context, listId) {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios.get(`${API_URL}/share/${listId}`)
|
||||
.then(({ data: { lists } }) => {
|
||||
resolve(lists);
|
||||
}).catch(reject);
|
||||
});
|
||||
},
|
||||
|
||||
LOAD_SHARE_GAMES(context, gameList) {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios.get(`${API_URL}/games?games=${gameList.join(',')}`)
|
||||
.then(({ data }) => {
|
||||
resolve(data);
|
||||
}).catch(reject);
|
||||
});
|
||||
},
|
||||
|
||||
LOAD_GAMES({ commit, state: { token } }, gameList) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const options = { headers: { token } };
|
||||
axios.get(`${API_URL}/games?games=${gameList.join(',')}`, options)
|
||||
.then(({ data }) => {
|
||||
commit('CACHE_GAME_DATA', data);
|
||||
resolve();
|
||||
}).catch(reject);
|
||||
});
|
||||
},
|
||||
|
||||
SEARCH({ commit, state: { token } }, searchText) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const options = { headers: { token } };
|
||||
axios.get(`${API_URL}/search?searchText=${searchText}&order=popularity:desc`, options)
|
||||
.then(({ data }) => {
|
||||
commit('SET_SEARCH_RESULTS', data);
|
||||
commit('CACHE_GAME_DATA', data);
|
||||
resolve();
|
||||
}).catch(reject);
|
||||
});
|
||||
},
|
||||
|
||||
DELETE_USER({ state: { token } }) {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios.delete(`${API_URL}/user?token=${token}`)
|
||||
.then(() => {
|
||||
resolve();
|
||||
}).catch(reject);
|
||||
});
|
||||
},
|
||||
};
|
54
src/store/getters.js
Executable file
|
@ -0,0 +1,54 @@
|
|||
import moment from 'moment';
|
||||
|
||||
export default {
|
||||
auth: state => Boolean(state.token && state.user),
|
||||
developers: (state) => {
|
||||
const developers = state.game.developers;
|
||||
|
||||
return developers
|
||||
? developers.map(developer => developer.name).join(', ')
|
||||
: null;
|
||||
},
|
||||
|
||||
publishers: (state) => {
|
||||
const publishers = state.game.publishers;
|
||||
|
||||
return publishers
|
||||
? publishers.map(publisher => publisher.name).join(', ')
|
||||
: null;
|
||||
},
|
||||
|
||||
genres: (state) => {
|
||||
const genres = state.game.genres;
|
||||
|
||||
return genres
|
||||
? genres.map(genre => genre.name).join(', ')
|
||||
: null;
|
||||
},
|
||||
|
||||
playerPerspectives: (state) => {
|
||||
const perspectives = state.game.player_perspectives;
|
||||
|
||||
return perspectives
|
||||
? perspectives.map(perspective => perspective.name).join(', ')
|
||||
: null;
|
||||
},
|
||||
|
||||
gameModes: (state) => {
|
||||
const gameModes = state.game.game_modes;
|
||||
|
||||
return gameModes
|
||||
? gameModes.map(gameMode => gameMode.name).join(', ')
|
||||
: null;
|
||||
},
|
||||
|
||||
gamePlatforms: (state) => {
|
||||
const gamePlatforms = state.game.platforms;
|
||||
|
||||
return gamePlatforms
|
||||
? gamePlatforms.map(gamePlatform => gamePlatform.name).join(', ')
|
||||
: null;
|
||||
},
|
||||
|
||||
releaseDate: state => moment(new Date(state.game.release_dates.find(r => r.platform === state.platforms.nsw)).human).format('LL'),
|
||||
};
|
23
src/store/index.js
Executable file
|
@ -0,0 +1,23 @@
|
|||
/* eslint no-shadow: ["error", { "allow": ["state"] }] */
|
||||
import Vue from 'vue';
|
||||
import Vuex from 'vuex';
|
||||
import VuexPersist from 'vuex-persist';
|
||||
import state from './state';
|
||||
import actions from './actions';
|
||||
import mutations from './mutations';
|
||||
import getters from './getters';
|
||||
|
||||
Vue.use(Vuex);
|
||||
|
||||
const vuexLocalStorage = new VuexPersist({
|
||||
key: 'vuex',
|
||||
storage: window.localStorage,
|
||||
});
|
||||
|
||||
export default new Vuex.Store({
|
||||
state,
|
||||
actions,
|
||||
mutations,
|
||||
getters,
|
||||
plugins: [vuexLocalStorage.plugin],
|
||||
});
|
113
src/store/mutations.js
Executable file
|
@ -0,0 +1,113 @@
|
|||
import moment from 'moment';
|
||||
|
||||
export default {
|
||||
SET_SESSION(state, { user, token }) {
|
||||
state.token = token;
|
||||
state.user = user;
|
||||
},
|
||||
|
||||
SET_SEARCH_RESULTS(state, results) {
|
||||
state.results = results;
|
||||
},
|
||||
|
||||
CLEAR_SEARCH_RESULTS(state) {
|
||||
state.results = [];
|
||||
},
|
||||
|
||||
SET_ACTIVE_GAME(state, game) {
|
||||
state.game = state.games[game];
|
||||
},
|
||||
|
||||
SET_EDIT_GAME(state, { listId, gameId }) {
|
||||
state.editGame = gameId;
|
||||
state.activeList = listId;
|
||||
},
|
||||
|
||||
SET_ACTIVE_LIST(state, listIndex) {
|
||||
state.activeList = listIndex;
|
||||
},
|
||||
|
||||
SET_ADMIN_DATA(state, data) {
|
||||
state.adminData = data;
|
||||
},
|
||||
|
||||
CLEAR_ADMIN_DATA(state) {
|
||||
state.adminData = null;
|
||||
},
|
||||
|
||||
SET_PLATFORM(state, platform) {
|
||||
state.platform = platform;
|
||||
},
|
||||
|
||||
CLEAR_ACTIVE_GAME(state) {
|
||||
state.game = null;
|
||||
},
|
||||
|
||||
UPDATE_USER(state, user) {
|
||||
state.user = user;
|
||||
},
|
||||
|
||||
UPDATE_LIST(state, lists) {
|
||||
state.user.lists = lists;
|
||||
},
|
||||
|
||||
SORT_LIST(state, listIndex) {
|
||||
const games = state.user.lists[listIndex].games;
|
||||
|
||||
games.sort((a, b) => {
|
||||
const gameA = state.games[a].name.toUpperCase();
|
||||
const gameB = state.games[b].name.toUpperCase();
|
||||
|
||||
if (gameA < gameB) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return gameA > gameB ? 1 : 0;
|
||||
});
|
||||
},
|
||||
|
||||
UPDATE_LIST_NAME(state, { listIndex, listName }) {
|
||||
state.user.lists[listIndex].name = listName;
|
||||
},
|
||||
|
||||
SET_UPDATED_TIMESTAMP(state) {
|
||||
state.dataUpdatedTimestamp = moment().format();
|
||||
},
|
||||
|
||||
SET_SETTINGS(state, settings) {
|
||||
state.user.settings = settings;
|
||||
},
|
||||
|
||||
REMOVE_LIST(state, index) {
|
||||
state.user.lists.splice(index, 1);
|
||||
},
|
||||
|
||||
ADD_GAME(state, { gameId, listId }) {
|
||||
state.user.lists[listId].games.push(gameId);
|
||||
},
|
||||
|
||||
ADD_LIST(state, listName) {
|
||||
const newList = {
|
||||
games: [],
|
||||
name: listName,
|
||||
};
|
||||
|
||||
state.user.lists.push(newList);
|
||||
},
|
||||
|
||||
REMOVE_GAME(state, { gameId, listId }) {
|
||||
state.user.lists[listId].games.splice(state.user.lists[listId].games.indexOf(gameId), 1);
|
||||
},
|
||||
|
||||
CACHE_GAME_DATA(state, data) {
|
||||
data.forEach((game) => {
|
||||
state.games[game.id] = { ...game };
|
||||
});
|
||||
},
|
||||
|
||||
CLEAR_SESSION(state) {
|
||||
state.token = null;
|
||||
state.user = null;
|
||||
state.platform = null;
|
||||
},
|
||||
};
|
52
src/store/state.js
Executable file
|
@ -0,0 +1,52 @@
|
|||
export default {
|
||||
token: null,
|
||||
user: null,
|
||||
activeList: null,
|
||||
platform: null,
|
||||
adminData: null,
|
||||
dataUpdatedTimestamp: null,
|
||||
results: null,
|
||||
games: {},
|
||||
game: null,
|
||||
platforms: {
|
||||
nsw: 130,
|
||||
},
|
||||
gameCategories: {
|
||||
0: 'Main game',
|
||||
1: 'DLC / Addon',
|
||||
2: 'Expansion',
|
||||
3: 'Bundle',
|
||||
4: 'Standalone expansion',
|
||||
},
|
||||
pegi: {
|
||||
1: '3',
|
||||
2: '7',
|
||||
3: '12',
|
||||
4: '16',
|
||||
5: '18',
|
||||
},
|
||||
esrb: {
|
||||
1: 'RP',
|
||||
2: 'EC',
|
||||
3: 'E',
|
||||
4: 'E10+',
|
||||
5: 'T',
|
||||
6: 'M',
|
||||
7: 'AO',
|
||||
},
|
||||
linkTypes: {
|
||||
1: 'official',
|
||||
2: 'wikia',
|
||||
3: 'wikipedia',
|
||||
4: 'facebook',
|
||||
5: 'twitter',
|
||||
6: 'twitch',
|
||||
8: 'instagram',
|
||||
9: 'youtube',
|
||||
10: 'iphone',
|
||||
11: 'ipad',
|
||||
12: 'android',
|
||||
13: 'steam',
|
||||
14: 'Reddit',
|
||||
},
|
||||
};
|
79
src/styles/game-board.scss
Normal file
|
@ -0,0 +1,79 @@
|
|||
.lists {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
height: calc(100vh - 48px);
|
||||
padding: $gp;
|
||||
box-sizing: border-box;
|
||||
background: $color-gray;
|
||||
overflow-x: auto;
|
||||
overflow-x: overlay;
|
||||
display: flex;
|
||||
@include drag-cursor;
|
||||
|
||||
&.drag-scroll-active {
|
||||
@include dragging-cursor;
|
||||
}
|
||||
|
||||
&.empty {
|
||||
background: $color-white;
|
||||
}
|
||||
}
|
||||
|
||||
.list {
|
||||
flex-shrink: 0;
|
||||
cursor: default;
|
||||
background: $color-white;
|
||||
position: relative;
|
||||
width: 300px;
|
||||
border-radius: $border-radius;
|
||||
overflow: hidden;
|
||||
margin-right: $gp;
|
||||
max-height: calc(100vh - 81px);
|
||||
}
|
||||
|
||||
.list-header {
|
||||
align-items: center;
|
||||
background: $color-dark-gray;
|
||||
color: $color-white;
|
||||
display: flex;
|
||||
height: 30px;
|
||||
justify-content: space-between;
|
||||
padding: 0 $gp / 2;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
|
||||
.list-actions {
|
||||
.list-drag-handle {
|
||||
@include drag-cursor;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.list-placeholder {
|
||||
opacity: 0.25;
|
||||
}
|
||||
|
||||
.games {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
min-height: 100px;
|
||||
max-height: calc(100vh - 120px);
|
||||
overflow-y: auto;
|
||||
overflow-y: overlay;
|
||||
column-gap: $gp;
|
||||
background: $color-light-gray;
|
||||
margin-top: 30px;
|
||||
padding: $gp / 2 $gp / 2 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.card-placeholder {
|
||||
opacity: 0.25;
|
||||
}
|
||||
|
||||
.empty-list {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
padding: $gp;
|
||||
}
|
2
src/styles/modules/_all.scss
Normal file
|
@ -0,0 +1,2 @@
|
|||
@import "_colors";
|
||||
@import "_utility";
|
9
src/styles/modules/_colors.scss
Normal file
|
@ -0,0 +1,9 @@
|
|||
$color-gray: #a5a2a2;
|
||||
$color-red: #dd2020;
|
||||
$color-orange: #ffae42;
|
||||
$color-white: #fff;
|
||||
$color-green: #1dbc60;
|
||||
$color-light-green: #7CB490;
|
||||
$color-blue: #00a8f0;
|
||||
$color-dark-gray: #555555;
|
||||
$color-light-gray: #e5e5e5;
|
35
src/styles/modules/_utility.scss
Normal file
|
@ -0,0 +1,35 @@
|
|||
@mixin container-xs {
|
||||
width: 320px;
|
||||
max-height: 100%;
|
||||
margin: 60px auto;
|
||||
background: $color-white;
|
||||
padding: $gp;
|
||||
border-radius: $border-radius;
|
||||
}
|
||||
|
||||
@mixin container {
|
||||
width: 1200px;
|
||||
max-width: 100%;
|
||||
margin: 100px auto;
|
||||
}
|
||||
|
||||
@mixin drag-cursor {
|
||||
cursor : -webkit-grab;
|
||||
cursor : -moz-grab;
|
||||
cursor : -o-grab;
|
||||
cursor : grab;
|
||||
}
|
||||
|
||||
@mixin dragging-cursor {
|
||||
cursor : -webkit-grabbing;
|
||||
cursor : -moz-grabbing;
|
||||
cursor : -o-grabbing;
|
||||
cursor : grabbing;
|
||||
}
|
||||
|
||||
@mixin floating-close-button {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
margin: $gp / 2;
|
||||
}
|
4
src/styles/partials/_base.scss
Normal file
|
@ -0,0 +1,4 @@
|
|||
@import "_reset";
|
||||
@import "_measurements";
|
||||
@import "_buttons";
|
||||
@import "_inputs";
|
105
src/styles/partials/_buttons.scss
Normal file
|
@ -0,0 +1,105 @@
|
|||
$iconSmallSize: 30px;
|
||||
|
||||
a.link {
|
||||
display: inline-block;
|
||||
line-height: 30px;
|
||||
text-decoration: none;
|
||||
|
||||
&.small {
|
||||
line-height: $iconSmallSize;
|
||||
}
|
||||
}
|
||||
|
||||
button, a.link {
|
||||
border: none;
|
||||
margin: 0;
|
||||
padding: 0 $gp;
|
||||
width: auto;
|
||||
overflow: visible;
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
font: inherit;
|
||||
border-radius: $border-radius;
|
||||
line-height: normal;
|
||||
-webkit-font-smoothing: inherit;
|
||||
-moz-osx-font-smoothing: inherit;
|
||||
-webkit-appearance: none;
|
||||
height: 40px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
transition: background-color 300ms ease;
|
||||
|
||||
&.small {
|
||||
height: $iconSmallSize;
|
||||
min-width: $iconSmallSize;
|
||||
padding: 0 $gp / 2;
|
||||
}
|
||||
|
||||
&.primary {
|
||||
background: $color-blue;
|
||||
color: $color-white;
|
||||
|
||||
&:hover { background-color: lighten($color-blue, 10%); }
|
||||
&:active { background-color: darken($color-blue, 10%); }
|
||||
}
|
||||
|
||||
&.accent {
|
||||
background: $color-light-gray;
|
||||
color: $color-dark-gray;
|
||||
|
||||
&:hover { background-color: lighten($color-light-gray, 10%); }
|
||||
&:active { background-color: darken($color-light-gray, 10%); }
|
||||
}
|
||||
|
||||
&.info {
|
||||
background: $color-dark-gray;
|
||||
color: $color-white;
|
||||
|
||||
&:hover { background-color: lighten($color-dark-gray, 10%); }
|
||||
&:active { background-color: darken($color-dark-gray, 10%); }
|
||||
}
|
||||
|
||||
&.error {
|
||||
background: $color-red;
|
||||
color: $color-white;
|
||||
|
||||
&:hover { background-color: lighten($color-red, 10%); }
|
||||
&:active { background-color: darken($color-red, 10%); }
|
||||
}
|
||||
|
||||
&.warning {
|
||||
background: $color-orange;
|
||||
color: $color-white;
|
||||
|
||||
&:hover { background-color: lighten($color-orange, 10%); }
|
||||
&:active { background-color: darken($color-orange, 10%); }
|
||||
}
|
||||
|
||||
&.hollow {
|
||||
&.primary {
|
||||
color: $color-blue;
|
||||
background: $color-white;
|
||||
}
|
||||
|
||||
&.accent {
|
||||
color: $color-gray;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&.info {
|
||||
color: $color-dark-gray;
|
||||
background: $color-white;
|
||||
}
|
||||
|
||||
&.error {
|
||||
color: $color-red;
|
||||
background: $color-white;
|
||||
}
|
||||
|
||||
&.warning {
|
||||
color: $color-orange;
|
||||
background: $color-white;
|
||||
}
|
||||
}
|
||||
}
|
54
src/styles/partials/_inputs.scss
Normal file
|
@ -0,0 +1,54 @@
|
|||
input {
|
||||
background: $color-white;
|
||||
border: 1px solid $color-dark-gray;
|
||||
border-radius: $border-radius;
|
||||
height: 30px;
|
||||
padding: 0 $gp / 2;
|
||||
width: 100%;
|
||||
margin-bottom: $gp;
|
||||
|
||||
&.small {
|
||||
height: 20px;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.toggle-switch {
|
||||
input[type=checkbox]{
|
||||
height: 0;
|
||||
width: 0;
|
||||
visibility: hidden;
|
||||
|
||||
&:checked + label {
|
||||
background: $color-green;
|
||||
}
|
||||
|
||||
&:checked + label:after {
|
||||
left: calc(100% - 3px);
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
}
|
||||
|
||||
label {
|
||||
cursor: pointer;
|
||||
text-indent: -9999px;
|
||||
width: 34px;
|
||||
height: 20px;
|
||||
background: grey;
|
||||
display: block;
|
||||
border-radius: 100px;
|
||||
position: relative;
|
||||
|
||||
&:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 3px;
|
||||
left: 3px;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
background: #fff;
|
||||
border-radius: 90px;
|
||||
transition: 0.3s;
|
||||
}
|
||||
}
|
||||
}
|
7
src/styles/partials/_measurements.scss
Normal file
|
@ -0,0 +1,7 @@
|
|||
$border-radius: 4px;
|
||||
$gp: 16px;
|
||||
$navHeight: 48px;
|
||||
|
||||
// Media queries
|
||||
$small: "max-width: 780px";
|
||||
$medium: "max-width: 1024px";
|
7
src/styles/partials/_reset.scss
Normal file
|
@ -0,0 +1,7 @@
|
|||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.fast-spin {
|
||||
animation: a 500ms infinite linear;
|
||||
}
|
3
src/styles/partials/_typography.scss
Normal file
|
@ -0,0 +1,3 @@
|
|||
// Typography
|
||||
$font-weight-normal: 400;
|
||||
$font-weight-bold: 700;
|
2
src/styles/styles.scss
Executable file
|
@ -0,0 +1,2 @@
|
|||
@import "modules/all";
|
||||
@import "partials/base";
|
0
static/.gitkeep
Executable file
BIN
static/icons/android-icon-144x144.png
Normal file
After Width: | Height: | Size: 4.7 KiB |
BIN
static/icons/android-icon-192x192.png
Normal file
After Width: | Height: | Size: 5.1 KiB |
BIN
static/icons/android-icon-36x36.png
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
static/icons/android-icon-48x48.png
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
static/icons/android-icon-72x72.png
Normal file
After Width: | Height: | Size: 2.5 KiB |
BIN
static/icons/android-icon-96x96.png
Normal file
After Width: | Height: | Size: 3.2 KiB |
BIN
static/icons/apple-icon-114x114.png
Normal file
After Width: | Height: | Size: 3.8 KiB |
BIN
static/icons/apple-icon-120x120.png
Normal file
After Width: | Height: | Size: 4 KiB |
BIN
static/icons/apple-icon-144x144.png
Normal file
After Width: | Height: | Size: 4.7 KiB |
BIN
static/icons/apple-icon-152x152.png
Normal file
After Width: | Height: | Size: 5.1 KiB |
BIN
static/icons/apple-icon-180x180.png
Normal file
After Width: | Height: | Size: 6 KiB |
BIN
static/icons/apple-icon-57x57.png
Normal file
After Width: | Height: | Size: 2.3 KiB |
BIN
static/icons/apple-icon-60x60.png
Normal file
After Width: | Height: | Size: 2.2 KiB |
BIN
static/icons/apple-icon-72x72.png
Normal file
After Width: | Height: | Size: 2.5 KiB |