Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
Thomas Citharel 2018-01-09 17:52:26 +01:00
parent 8ac705d8c2
commit 92d2045735
97 changed files with 18243 additions and 1544 deletions

18
js/.babelrc Normal file
View File

@ -0,0 +1,18 @@
{
"presets": [
["env", {
"modules": false,
"targets": {
"browsers": ["> 1%", "last 2 versions", "not ie <= 8"]
}
}],
"stage-2"
],
"plugins": ["transform-runtime"],
"env": {
"test": {
"presets": ["env", "stage-2"],
"plugins": ["istanbul"]
}
}
}

9
js/.editorconfig Normal file
View 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

2
js/.eslintignore Normal file
View File

@ -0,0 +1,2 @@
build/*.js
config/*.js

22
js/.eslintrc.js Normal file
View File

@ -0,0 +1,22 @@
// http://eslint.org/docs/user-guide/configuring
module.exports = {
root: true,
parserOptions: {
parser: 'babel-eslint',
sourceType: 'module'
},
env: {
browser: true,
},
extends: [
'eslint:recommended',
'plugin:vue/recommended' // or 'plugin:vue/base'
],
// add your custom rules here
'rules': {
// don't require .vue extension when importing
'no-console': [0],
},
}

16
js/.gitignore vendored Normal file
View File

@ -0,0 +1,16 @@
.DS_Store
node_modules/
dist/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
test/unit/coverage
test/e2e/reports
selenium-debug.log
# Editor directories and files
.idea
*.suo
*.ntvs*
*.njsproj
*.sln

8
js/.postcssrc.js Normal file
View File

@ -0,0 +1,8 @@
// https://github.com/michael-ciniawsky/postcss-load-config
module.exports = {
"plugins": {
// to edit target browsers: use "browserslist" field in package.json
"autoprefixer": {}
}
}

30
js/README.md Normal file
View File

@ -0,0 +1,30 @@
# libre-event
> A Vue.js project
## Build Setup
``` bash
# install dependencies
npm install
# serve with hot reload at localhost:8080
npm run dev
# build for production with minification
npm run build
# build for production and view the bundle analyzer report
npm run build --report
# run unit tests
npm run unit
# run e2e tests
npm run e2e
# run all tests
npm test
```
For detailed explanation on how things work, checkout the [guide](http://vuejs-templates.github.io/webpack/) and [docs for vue-loader](http://vuejs.github.io/vue-loader).

40
js/build/build.js Normal file
View File

@ -0,0 +1,40 @@
require('./check-versions')()
process.env.NODE_ENV = 'production'
var ora = require('ora')
var rm = require('rimraf')
var path = require('path')
var chalk = require('chalk')
var webpack = require('webpack')
var config = require('../config')
var webpackConfig = require('./webpack.prod.conf')
var spinner = ora('building for production...')
spinner.start()
rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => {
if (err) throw err
webpack(webpackConfig, function (err, stats) {
spinner.stop()
if (err) throw err
process.stdout.write(stats.toString({
colors: true,
modules: false,
children: false,
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'
))
})
})

View File

@ -0,0 +1,48 @@
var chalk = require('chalk')
var semver = require('semver')
var packageConfig = require('../package.json')
var shell = require('shelljs')
function exec (cmd) {
return require('child_process').execSync(cmd).toString().trim()
}
var 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 () {
var warnings = []
for (var i = 0; i < versionRequirements.length; i++) {
var 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 (var i = 0; i < warnings.length; i++) {
var warning = warnings[i]
console.log(' ' + warning)
}
console.log()
process.exit(1)
}
}

9
js/build/dev-client.js Normal file
View File

@ -0,0 +1,9 @@
/* eslint-disable */
require('eventsource-polyfill')
var hotClient = require('webpack-hot-middleware/client?noInfo=true&reload=true')
hotClient.subscribe(function (event) {
if (event.action === 'reload') {
window.location.reload()
}
})

92
js/build/dev-server.js Normal file
View File

@ -0,0 +1,92 @@
require('./check-versions')()
var config = require('../config')
if (!process.env.NODE_ENV) {
process.env.NODE_ENV = JSON.parse(config.dev.env.NODE_ENV)
}
var opn = require('opn')
var path = require('path')
var express = require('express')
var webpack = require('webpack')
var proxyMiddleware = require('http-proxy-middleware')
var webpackConfig = (process.env.NODE_ENV === 'testing' || process.env.NODE_ENV === 'production')
? require('./webpack.prod.conf')
: require('./webpack.dev.conf')
// default port where dev server listens for incoming traffic
var port = process.env.PORT || config.dev.port
// automatically open browser, if not set will be false
var autoOpenBrowser = !!config.dev.autoOpenBrowser
// Define HTTP proxies to your custom API backend
// https://github.com/chimurai/http-proxy-middleware
var proxyTable = config.dev.proxyTable
var app = express()
var compiler = webpack(webpackConfig)
var devMiddleware = require('webpack-dev-middleware')(compiler, {
publicPath: webpackConfig.output.publicPath,
quiet: true
})
var hotMiddleware = require('webpack-hot-middleware')(compiler, {
log: false,
heartbeat: 2000
})
// force page reload when html-webpack-plugin template changes
compiler.plugin('compilation', function (compilation) {
compilation.plugin('html-webpack-plugin-after-emit', function (data, cb) {
hotMiddleware.publish({ action: 'reload' })
cb()
})
})
// proxy api requests
Object.keys(proxyTable).forEach(function (context) {
var options = proxyTable[context]
if (typeof options === 'string') {
options = { target: options }
}
app.use(proxyMiddleware(options.filter || context, options))
})
// handle fallback for HTML5 history API
app.use(require('connect-history-api-fallback')())
// serve webpack bundle output
app.use(devMiddleware)
// enable hot-reload and state-preserving
// compilation error display
app.use(hotMiddleware)
// serve pure static assets
var staticPath = path.posix.join(config.dev.assetsPublicPath, config.dev.assetsSubDirectory)
app.use(staticPath, express.static('./static'))
var uri = 'http://localhost:' + port
var _resolve
var readyPromise = new Promise(resolve => {
_resolve = resolve
})
console.log('> Starting dev server...')
devMiddleware.waitUntilValid(() => {
console.log('> Listening at ' + uri + '\n')
// when env is testing, don't need open it
if (autoOpenBrowser && process.env.NODE_ENV !== 'testing') {
opn(uri)
}
_resolve()
})
var server = app.listen(port)
module.exports = {
ready: readyPromise,
close: () => {
server.close()
}
}

71
js/build/utils.js Normal file
View File

@ -0,0 +1,71 @@
var path = require('path')
var config = require('../config')
var ExtractTextPlugin = require('extract-text-webpack-plugin')
exports.assetsPath = function (_path) {
var assetsSubDirectory = process.env.NODE_ENV === 'production'
? config.build.assetsSubDirectory
: config.dev.assetsSubDirectory
return path.posix.join(assetsSubDirectory, _path)
}
exports.cssLoaders = function (options) {
options = options || {}
var cssLoader = {
loader: 'css-loader',
options: {
minimize: process.env.NODE_ENV === 'production',
sourceMap: options.sourceMap
}
}
// generate loader string to be used with extract text plugin
function generateLoaders (loader, loaderOptions) {
var loaders = [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) {
var output = []
var loaders = exports.cssLoaders(options)
for (var extension in loaders) {
var loader = loaders[extension]
output.push({
test: new RegExp('\\.' + extension + '$'),
use: loader
})
}
return output
}

View File

@ -0,0 +1,18 @@
var utils = require('./utils')
var config = require('../config')
var isProduction = process.env.NODE_ENV === 'production'
module.exports = {
loaders: utils.cssLoaders({
sourceMap: isProduction
? config.build.productionSourceMap
: config.dev.cssSourceMap,
extract: isProduction
}),
transformToRequire: {
video: 'src',
source: 'src',
img: 'src',
image: 'xlink:href'
}
}

View File

@ -0,0 +1,75 @@
var path = require('path')
var utils = require('./utils')
var config = require('../config')
var vueLoaderConfig = require('./vue-loader.conf')
function resolve (dir) {
return path.join(__dirname, '..', dir)
}
module.exports = {
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',
'@': resolve('src'),
}
},
module: {
rules: [
{
test: /\.(js|vue)$/,
loader: 'eslint-loader',
enforce: 'pre',
include: [resolve('src'), resolve('test')],
options: {
formatter: require('eslint-friendly-formatter')
}
},
{
test: /\.vue$/,
loader: 'vue-loader',
options: vueLoaderConfig
},
{
test: /\.js$/,
loader: 'babel-loader',
include: [resolve('src'), resolve('test')]
},
{
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]')
}
}
]
}
}

View File

@ -0,0 +1,35 @@
var utils = require('./utils')
var webpack = require('webpack')
var config = require('../config')
var merge = require('webpack-merge')
var baseWebpackConfig = require('./webpack.base.conf')
var HtmlWebpackPlugin = require('html-webpack-plugin')
var FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin')
// add hot-reload related code to entry chunks
Object.keys(baseWebpackConfig.entry).forEach(function (name) {
baseWebpackConfig.entry[name] = ['./build/dev-client'].concat(baseWebpackConfig.entry[name])
})
module.exports = merge(baseWebpackConfig, {
module: {
rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap })
},
// cheap-module-eval-source-map is faster for development
devtool: '#cheap-module-eval-source-map',
plugins: [
new webpack.DefinePlugin({
'process.env': config.dev.env
}),
// https://github.com/glenjamin/webpack-hot-middleware#installation--usage
new webpack.HotModuleReplacementPlugin(),
new webpack.NoEmitOnErrorsPlugin(),
// https://github.com/ampedandwired/html-webpack-plugin
new HtmlWebpackPlugin({
filename: 'index.html',
template: 'index.html',
inject: true
}),
new FriendlyErrorsPlugin()
]
})

View File

@ -0,0 +1,126 @@
var path = require('path')
var utils = require('./utils')
var webpack = require('webpack')
var config = require('../config')
var merge = require('webpack-merge')
var baseWebpackConfig = require('./webpack.base.conf')
var CopyWebpackPlugin = require('copy-webpack-plugin')
var HtmlWebpackPlugin = require('html-webpack-plugin')
var ExtractTextPlugin = require('extract-text-webpack-plugin')
var OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin')
var env = process.env.NODE_ENV === 'testing'
? require('../config/test.env')
: config.build.env
var webpackConfig = merge(baseWebpackConfig, {
module: {
rules: utils.styleLoaders({
sourceMap: config.build.productionSourceMap,
extract: true
})
},
devtool: config.build.productionSourceMap ? '#source-map' : 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
}),
new webpack.optimize.UglifyJsPlugin({
compress: {
warnings: false
},
sourceMap: true
}),
// extract css into its own file
new ExtractTextPlugin({
filename: utils.assetsPath('css/[name].[contenthash].css')
}),
// Compress extracted CSS. We are using this plugin so that possible
// duplicated CSS from different components can be deduped.
new OptimizeCSSPlugin({
cssProcessorOptions: {
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'
}),
// keep module.id stable when vender modules does not change
new webpack.HashedModuleIdsPlugin(),
// split vendor js into its own file
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
minChunks: function (module, count) {
// 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',
chunks: ['vendor']
}),
// copy custom static assets
new CopyWebpackPlugin([
{
from: path.resolve(__dirname, '../static'),
to: config.build.assetsSubDirectory,
ignore: ['.*']
}
])
]
})
if (config.build.productionGzip) {
var 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) {
var BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
webpackConfig.plugins.push(new BundleAnalyzerPlugin())
}
module.exports = webpackConfig

View File

@ -0,0 +1,31 @@
// This is the webpack config used for unit tests.
var utils = require('./utils')
var webpack = require('webpack')
var merge = require('webpack-merge')
var baseConfig = require('./webpack.base.conf')
var webpackConfig = merge(baseConfig, {
// 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

6
js/config/dev.env.js Normal file
View File

@ -0,0 +1,6 @@
var merge = require('webpack-merge')
var prodEnv = require('./prod.env')
module.exports = merge(prodEnv, {
NODE_ENV: '"development"'
})

38
js/config/index.js Normal file
View File

@ -0,0 +1,38 @@
// see http://vuejs-templates.github.io/webpack for documentation.
var path = require('path')
module.exports = {
build: {
env: require('./prod.env'),
index: path.resolve(__dirname, '../../lib/eventos_web/templates/app/index.html.eex'),
assetsRoot: path.resolve(__dirname, '../../priv/static'),
assetsSubDirectory: '',
assetsPublicPath: '/',
productionSourceMap: true,
// 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
},
dev: {
env: require('./dev.env'),
port: 8080,
autoOpenBrowser: true,
assetsSubDirectory: 'static',
assetsPublicPath: '/',
proxyTable: {},
// CSS Sourcemaps off by default because relative paths are "buggy"
// with this option, according to the CSS-Loader README
// (https://github.com/webpack/css-loader#sourcemaps)
// In our experience, they generally work as expected,
// just be aware of this issue when enabling this option.
cssSourceMap: false
}
}

3
js/config/prod.env.js Normal file
View File

@ -0,0 +1,3 @@
module.exports = {
NODE_ENV: '"production"'
}

6
js/config/test.env.js Normal file
View File

@ -0,0 +1,6 @@
var merge = require('webpack-merge')
var devEnv = require('./dev.env')
module.exports = merge(devEnv, {
NODE_ENV: '"testing"'
})

16
js/index.html Normal file
View File

@ -0,0 +1,16 @@
<!DOCTYPE html>
<html>
<head>
<link href='https://fonts.googleapis.com/css?family=Roboto:300,400,500,700|Material+Icons' rel="stylesheet">
<script src="https://maps.googleapis.com/maps/api/js?key=AIzaSyBF37pw38j0giICt73TCAPNogc07Upe_Q4&libraries=places"></script>
<meta charset="utf-8">
<title>libre-event</title>
</head>
<body>
<noscript>
Mets du JS.
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>

14594
js/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

103
js/package.json Normal file
View File

@ -0,0 +1,103 @@
{
"name": "libre-event",
"version": "1.0.0",
"description": "A Vue.js project",
"author": "Thomas Citharel <tcit@tcit.fr>",
"private": true,
"scripts": {
"dev": "node build/dev-server.js",
"start": "node build/dev-server.js",
"build": "node build/build.js",
"unit": "cross-env BABEL_ENV=test karma start test/unit/karma.conf.js --single-run",
"e2e": "node test/e2e/runner.js",
"test": "npm run unit && npm run e2e",
"lint": "eslint --ext .js,.vue src test/unit/specs test/e2e/specs"
},
"dependencies": {
"jwt-decode": "^2.2.0",
"moment": "^2.20.1",
"ngeohash": "^0.6.0",
"vue": "^2.5.13",
"vue-markdown": "^2.2.4",
"vue-router": "^3.0.1",
"vue2-google-maps": "^0.8.4",
"vuetify": "^0.17.6",
"vuetify-google-autocomplete": "^1.1.0",
"vuex": "^2.5.0",
"vuex-i18n": "1.8.0"
},
"devDependencies": {
"autoprefixer": "^7.2.4",
"avoriaz": "^6.3.0",
"babel-eslint": "^7.1.1",
"babel-loader": "^7.1.1",
"babel-plugin-transform-runtime": "^6.22.0",
"babel-preset-env": "^1.3.2",
"babel-preset-stage-2": "^6.22.0",
"babel-register": "^6.22.0",
"chai": "^4.1.2",
"chalk": "^2.3.0",
"chromedriver": "^2.34.1",
"connect-history-api-fallback": "^1.5.0",
"copy-webpack-plugin": "^4.3.1",
"cross-env": "^5.1.3",
"cross-spawn": "^5.0.1",
"css-loader": "^0.28.8",
"cssnano": "^3.10.0",
"eslint": "^4.15.0",
"eslint-friendly-formatter": "^3.0.0",
"eslint-import-resolver-webpack": "^0.8.4",
"eslint-loader": "^1.7.1",
"eslint-plugin-html": "^3.2.2",
"eslint-plugin-import": "^2.8.0",
"eslint-plugin-vue": "^3.14.0",
"eventsource-polyfill": "^0.9.6",
"express": "^4.16.2",
"extract-text-webpack-plugin": "^3.0.2",
"file-loader": "^1.1.6",
"friendly-errors-webpack-plugin": "^1.1.3",
"html-webpack-plugin": "^2.28.0",
"http-proxy-middleware": "^0.17.3",
"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.5.0",
"karma-sinon-chai": "^1.3.3",
"karma-sourcemap-loader": "^0.3.7",
"karma-spec-reporter": "0.0.31",
"karma-webpack": "^2.0.9",
"mocha": "^4.1.0",
"nightwatch": "^0.9.19",
"opn": "^5.1.0",
"optimize-css-assets-webpack-plugin": "^3.2.0",
"ora": "^1.2.0",
"phantomjs-prebuilt": "^2.1.16",
"portfinder": "^1.0.13",
"rimraf": "^2.6.2",
"selenium-server": "^3.8.1",
"semver": "^5.3.0",
"shelljs": "^0.7.6",
"sinon": "^4.1.4",
"sinon-chai": "^2.14.0",
"url-loader": "^0.6.2",
"vue-loader": "^13.7.0",
"vue-style-loader": "^3.0.3",
"vue-template-compiler": "^2.5.13",
"webpack": "^3.10.0",
"webpack-dev-middleware": "^1.12.2",
"webpack-dev-server": "^2.10.1",
"webpack-hot-middleware": "^2.21.0",
"webpack-merge": "^4.1.1"
},
"engines": {
"node": ">= 4.0.0",
"npm": ">= 3.0.0"
},
"browserslist": [
"> 1%",
"last 2 versions",
"not ie <= 8"
]
}

128
js/src/App.vue Normal file
View File

@ -0,0 +1,128 @@
<template>
<v-app id="libre-event">
<v-navigation-drawer
light
clipped
fixed
app
v-model="drawer"
enable-resize-watcher
>
<v-list dense>
<template v-for="(item, i) in items" v-if="showMenuItem(item.role)">
<v-layout
row
v-if="item.heading"
align-center
:key="i"
>
<v-flex xs6>
<v-subheader v-if="item.heading">
{{ item.heading }}
</v-subheader>
</v-flex>
<v-flex xs6 class="text-xs-center">
<a href="#!" class="body-2 black--text">EDIT</a>
</v-flex>
</v-layout>
<v-list-tile v-bind:key="item.route" v-else @click="$router.push({ name: item.route })">
<v-list-tile-action>
<v-icon>{{ item.icon }}</v-icon>
</v-list-tile-action>
<v-list-tile-content>
<v-list-tile-title>
{{ item.text }}
</v-list-tile-title>
</v-list-tile-content>
</v-list-tile>
</template>
</v-list>
</v-navigation-drawer>
<NavBar></NavBar>
<v-content>
<v-container fluid fill-height>
<v-layout xs-12>
<transition>
<router-view></router-view>
</transition>
</v-layout>
</v-container>
</v-content>
<v-btn
fixed
dark
fab
bottom
right
color="pink"
@click="$router.push({name: 'CreateEvent'})"
v-if="getUser()"
>
<v-icon>add</v-icon>
</v-btn>
<v-footer class="indigo" app>
<span class="white--text">© Thomas Citharel {{ new Date().getFullYear() }} - Made with <a href="https://api-platform.com/">API Platform</a> & <a href="https://vuejs.org/">VueJS</a> & <a href="https://www.vuetifyjs.com/">Vuetify</a> with some love and some weeks</span>
</v-footer>
<v-snackbar
:timeout="error.timeout"
:error="true"
v-model="error.show"
>
{{ error.text }}
<v-btn dark flat @click.native="error.show = false">Close</v-btn>
</v-snackbar>
</v-app>
</template>
<script>
import auth from '@/auth/index';
import NavBar from '@/components/NavBar';
export default {
name: 'app',
components: {
NavBar,
},
data() {
return {
drawer: true,
user: false,
items: [
{ icon: 'poll', text: 'Events', route: 'EventList', role: null },
{ icon: 'group', text: 'Groups', route: 'GroupList', role: null },
{ icon: 'content_copy', text: 'Categories', route: 'CategoryList', role: 'ROLE_ADMIN' },
{ icon: 'settings', text: 'Settings', role: 'ROLE_USER' },
{ icon: 'chat_bubble', text: 'Send feedback', role: 'ROLE_USER' },
{ icon: 'help', text: 'Help', role: null },
{ icon: 'phonelink', text: 'App downloads', role: null },
],
error: {
timeout: 3000,
show: false,
text: '',
},
show_new_event_button: false,
};
},
created() {
this.checkAuthMethod();
},
methods: {
checkAuthMethod() {
if (auth.checkAuth(this.$store)) {
this.show_new_event_button = true;
}
},
showMenuItem(elem) {
return elem !== null && this.$store.state.user && this.$store.state.user.roles !== undefined ? this.$store.state.user.roles.includes(elem) : true;
},
getUser() {
return this.$store.state.user === undefined ? false : this.$store.state.user;
}
},
};
</script>
<style>
</style>

5
js/src/actions/login.js Normal file
View File

@ -0,0 +1,5 @@
export default {
login(state, user) {
state.user = user.user;
},
};

View File

@ -0,0 +1,2 @@
export const API_HOST = 'http://0.0.0.0:4000';
export const API_PATH = '/api';

39
js/src/api/eventFetch.js Normal file
View File

@ -0,0 +1,39 @@
import { API_HOST, API_PATH } from './_entrypoint';
const jsonLdMimeType = 'application/ld+json';
export default function eventFetch(url, store, optionsarg = {}) {
const options = optionsarg;
if (typeof options.headers === 'undefined') {
options.headers = new Headers();
}
if (options.headers.get('Accept') === null) {
options.headers.set('Accept', jsonLdMimeType);
}
if (options.body !== 'undefined' && !(options.body instanceof FormData) && options.headers.get('Content-Type') === null) {
options.headers.set('Content-Type', jsonLdMimeType);
}
if (store.state.user) {
options.headers.set('Authorization', `Bearer ${localStorage.getItem('token')}`);
}
const link = url.includes(API_PATH) ? API_HOST + url : API_HOST + API_PATH + url;
return fetch(link, options).then((response) => {
if (response.ok) return response;
return response
.json()
.then((json) => {
const error = json['hydra:description'] ? json['hydra:description'] : response.statusText;
if (!json.violations) throw Error(error);
// const errors = { _error: error };
// json.violations.map(violation => errors[violation.propertyPath] = violation.message);
// throw errors;
});
});
}

0
js/src/api/osm.js Normal file
View File

BIN
js/src/assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

156
js/src/auth/index.js Normal file
View File

@ -0,0 +1,156 @@
import router from '../router/index';
import { API_HOST, API_PATH } from '../api/_entrypoint';
// URL and endpoint constants
const LOGIN_URL = `${API_HOST}${API_PATH}/login`;
const SIGNUP_URL = `${API_HOST}${API_PATH}/users/`;
const CHECK_AUTH = `${API_HOST}${API_PATH}/users/`;
const REFRESH_TOKEN = `${API_HOST}${API_PATH}/token/refresh`;
function AuthError(field, message) {
this.field = field;
this.message = message;
}
AuthError.prototype.toString = function AuthErrorToString() {
return `AuthError: ${this.message}`;
};
export default {
// User object will let us check authentication status
user: false,
authenticated: false,
token: false,
// Send a request to the login URL and save the returned JWT
login(creds, $store, redirect, error) {
fetch(LOGIN_URL, { method: 'POST', body: creds, headers: { 'Content-Type': 'application/json' } })
.then(response => response.json())
.then((data) => {
if (data.code >= 300) {
throw new AuthError(null, data.message);
}
$store.commit('LOGIN_USER');
localStorage.setItem('token', data.token);
localStorage.setItem('refresh_token', data.refresh_token);
this.getUser(
$store,
() => router.push(redirect)
);
}).catch((err) => {
error(err);
});
},
signup(creds, $store, redirect, error) {
fetch(SIGNUP_URL, { method: 'POST', body: creds, headers: { 'Content-Type': 'application/json' } })
.then(response => response.json())
.then((data) => {
if (data.error) {
throw new AuthError(data.error.field, data.error.message);
}
$store.commit('LOGIN_USER');
localStorage.setItem('token', data.token);
localStorage.setItem('refresh_token', data.refresh_token);
if (redirect) {
router.push(redirect);
}
}).catch((err) => {
error(err);
});
},
refreshToken(store, successHandler, errorHandler) {
const refreshToken = localStorage.getItem('refresh_token');
console.log("We are refreshing the jwt token");
fetch(REFRESH_TOKEN, { method: 'POST', body: JSON.stringify({refresh_token: refreshToken}), headers: { 'Content-Type': 'application/json' }})
.then((response) => {
if (response.ok) {
return response.json();
} else {
errorHandler('Error while authenticating');
}
})
.then((response) => {
console.log("We have a new token");
this.authenticated = true;
store.commit('LOGIN_USER', response);
localStorage.setItem('token', response.token);
console.log("Let's try to auth again");
this.getUser(store, successHandler, errorHandler);
successHandler();
});
},
// To log out, we just need to remove the token
logout() {
localStorage.removeItem('refresh_token');
localStorage.removeItem('token');
this.authenticated = false;
},
jwt_decode(token) {
const base64Url = token.split('.')[1];
const base64 = base64Url.replace('-', '+').replace('_', '/');
return JSON.parse(window.atob(base64));
},
getTokenExpirationDate(encodedToken) {
const token = this.jwt_decode(encodedToken);
if (!token.exp) { return null; }
const date = new Date(0);
date.setUTCSeconds(token.exp);
return date;
},
isTokenExpired(token) {
const expirationDate = this.getTokenExpirationDate(token);
return expirationDate < new Date();
},
checkAuth(store = null) {
const token = localStorage.getItem('token');
if (store && token) {
this.getUser(store,() => null, () => null);
}
/* if (!!token && store && !this.isTokenExpired(token)) {
this.refreshToken(store, () => null, () => null);
} */
return !!token;
},
getUser(store, successHandler, errorHandler) {
console.log("We are checking the auth");
this.token = localStorage.getItem('token');
const options = {};
options.headers = new Headers();
options.headers.set('Authorization', `Bearer ${this.token}`);
fetch(CHECK_AUTH, options)
.then((response) => {
if (response.ok) {
return response.json();
} else {
errorHandler('Error while authenticating');
}
})
.then((response) => {
this.authenticated = true;
console.log(response);
store.commit('SAVE_USER', response);
successHandler();
});
},
// The object to be passed as a header for authenticated requests
getAuthHeader() {
return {
Authorization: `Bearer ${localStorage.getItem('access_token')}`,
};
},
};

View File

@ -0,0 +1,190 @@
<template>
<v-container>
<v-layout row>
<v-flex xs12 sm6 offset-sm3>
<v-progress-circular v-if="loading" indeterminate color="primary"></v-progress-circular>
<v-card v-if="!loading">
<v-layout column class="media">
<v-card-title>
<v-btn icon @click="$router.go(-1)">
<v-icon>chevron_left</v-icon>
</v-btn>
<v-spacer></v-spacer>
<v-btn icon class="mr-3" v-if="$store.state.user && $store.state.user.account.id === account.id">
<v-icon>edit</v-icon>
</v-btn>
<v-btn icon>
<v-icon>more_vert</v-icon>
</v-btn>
</v-card-title>
<v-spacer></v-spacer>
<div class="text-xs-center">
<v-avatar size="125px">
<img v-if="!account.avatarRemoteUrl"
class="img-circle elevation-7 mb-1"
src="http://lorempixel.com/125/125/"
>
<img v-else
class="img-circle elevation-7 mb-1"
:src="account.avatarRemoteUrl"
>
</v-avatar>
<v-card-title class="pl-5 pt-5">
<div class="display-1 pl-5 pt-5">@{{ account.username }}<span v-if="account.server">@{{ account.server.address }}</span></div>
</v-card-title>
<v-card-text v-if="account.description" v-html="account.description"></v-card-text>
</div>
</v-layout>
<v-list three-line>
<v-list-tile>
<v-list-tile-action>
<v-icon color="indigo">phone</v-icon>
</v-list-tile-action>
<v-list-tile-content>
<v-list-tile-title>(323) 555-6789</v-list-tile-title>
<v-list-tile-sub-title>Work</v-list-tile-sub-title>
</v-list-tile-content>
<v-list-tile-action>
<v-icon dark>chat</v-icon>
</v-list-tile-action>
</v-list-tile>
<v-divider inset></v-divider>
<v-list-tile>
<v-list-tile-action>
<v-icon color="indigo">mail</v-icon>
</v-list-tile-action>
<v-list-tile-content>
<v-list-tile-title>ali_connors@example.com</v-list-tile-title>
<v-list-tile-sub-title>Work</v-list-tile-sub-title>
</v-list-tile-content>
</v-list-tile>
<v-divider inset></v-divider>
<v-list-tile>
<v-list-tile-action>
<v-icon color="indigo">location_on</v-icon>
</v-list-tile-action>
<v-list-tile-content>
<v-list-tile-title>1400 Main Street</v-list-tile-title>
<v-list-tile-sub-title>Orlando, FL 79938</v-list-tile-sub-title>
</v-list-tile-content>
</v-list-tile>
</v-list>
<v-container fluid grid-list-md v-if="account.participatingEvents.length > 0">
<v-subheader>Participated at</v-subheader>
<v-layout row wrap>
<v-flex v-for="event in account.participatingEvents" :key="event.id">
<v-card>
<v-card-media
class="black--text"
height="200px"
src="http://lorempixel.com/400/200/"
>
<v-container fill-height fluid>
<v-layout fill-height>
<v-flex xs12 align-end flexbox>
<span class="headline">{{ event.title }}</span>
</v-flex>
</v-layout>
</v-container>
</v-card-media>
<v-card-title>
<div>
<span class="grey--text">{{ event.startDate | formatDate }} à {{ event.location }}</span><br>
<p>{{ event.description }}</p>
<p v-if="event.organizer">Organisé par <router-link :to="{name: 'Account', params: {'id': event.organizer.id}}">{{ event.organizer.username }}</router-link></p>
</div>
</v-card-title>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn icon>
<v-icon>favorite</v-icon>
</v-btn>
<v-btn icon>
<v-icon>bookmark</v-icon>
</v-btn>
<v-btn icon>
<v-icon>share</v-icon>
</v-btn>
</v-card-actions>
</v-card>
</v-flex>
</v-layout>
</v-container>
<v-container fluid grid-list-md v-if="account.organizingEvents.length > 0">
<v-subheader>Organized events</v-subheader>
<v-layout row wrap>
<v-flex v-for="event in account.organizingEvents" :key="event.id">
<v-card>
<v-card-media
class="black--text"
height="200px"
src="http://lorempixel.com/400/200/"
>
<v-container fill-height fluid>
<v-layout fill-height>
<v-flex xs12 align-end flexbox>
<span class="headline">{{ event.title }}</span>
</v-flex>
</v-layout>
</v-container>
</v-card-media>
<v-card-title>
<div>
<span class="grey--text">{{ event.startDate | formatDate }} à {{ event.location }}</span><br>
<p>{{ event.description }}</p>
<p v-if="event.organizer">Organisé par <router-link :to="{name: 'Account', params: {'id': event.organizer.id}}">{{ event.organizer.username }}</router-link></p>
</div>
</v-card-title>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn icon>
<v-icon>favorite</v-icon>
</v-btn>
<v-btn icon>
<v-icon>bookmark</v-icon>
</v-btn>
<v-btn icon>
<v-icon>share</v-icon>
</v-btn>
</v-card-actions>
</v-card>
</v-flex>
</v-layout>
</v-container>
</v-card>
</v-flex>
</v-layout>
</v-container>
</template>
<script>
import eventFetch from '@/api/eventFetch';
export default {
name: 'Account',
data() {
return {
account: null,