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,
loading: true,
}
},
props: ['id'],
mounted() {
this.fetchData();
},
watch: {
// call again the method if the route changes
'$route': 'fetchData'
},
methods: {
fetchData() {
eventFetch('/accounts/' + this.id, this.$store)
.then(response => response.json())
.then((response) => {
this.account = response;
this.loading = false;
})
}
}
}
</script>

View file

@ -0,0 +1,59 @@
<template>
<div>
<h3>Create a new category</h3>
<v-form>
<v-text-field
label="Name of the category"
v-model="category.name"
:counter="100"
required
></v-text-field>
<input type="file" @change="processFile($event.target)">
</v-form>
<v-btn color="primary" @click="create">Create category</v-btn>
</div>
</template>
<script>
import eventFetch from '@/api/eventFetch';
export default {
name: 'create-category',
data() {
return {
category: {
name: '',
imageDataUri: null,
},
};
},
methods: {
create() {
const router = this.$router;
eventFetch('/categories', this.$store, { method: 'POST', body: JSON.stringify(this.category) })
.then(response => response.json())
.then(() => {
this.loading = false;
router.push('/category')
});
},
processFile(target) {
const reader = new FileReader();
const file = target.files[0];
reader.addEventListener('load', () => {
this.category.imageDataUri = reader.result;
});
if (file) {
reader.readAsDataURL(file);
}
}
},
};
</script>
<style>
.markdown-render h1 {
font-size: 2em;
}
</style>

View file

@ -0,0 +1,74 @@
<template>
<v-container>
<h1>Category List</h1>
<v-progress-circular v-if="loading" indeterminate color="primary"></v-progress-circular>
<v-container fluid grid-list-md class="grey lighten-4">
<v-layout row wrap v-if="!loading">
<v-flex xs12 sm6 md3 v-for="category in categories" :key="category.id">
<v-card>
<v-card-media v-if="category.image" :src="'/images/categories/' + category.image.name" height="200px">
</v-card-media>
<v-card-title primary-title>
<div>
<h3 class="headline mb-0">{{ category.name }}</h3>
<div>{{ category.description }}</div>
</div>
</v-card-title>
<v-card-actions>
<v-btn flat class="orange--text">Explore</v-btn>
<v-btn flat class="red--text" v-on:click="deleteCategory(category.id)">Delete</v-btn>
</v-card-actions>
</v-card>
</v-flex>
<v-layout v-if="categories.length <= 0">
<h3>No categories :(</h3>
</v-layout>
</v-layout>
</v-container>
<router-link :to="{ name: 'CreateCategory' }" class="btn btn-default">Create</router-link>
</v-container>
</template>
<script>
import eventFetch from '@/api/eventFetch';
export default {
name: 'Home',
data() {
return {
categories: [],
loading: true,
};
},
created() {
this.fetchData();
},
methods: {
fetchData() {
eventFetch('/categories', this.$store)
.then(response => response.json())
.then((data) => {
this.loading = false;
this.categories = data['hydra:member'];
});
},
deleteCategory(categoryId) {
const router = this.$router;
eventFetch('/categories/' + categoryId, this.$store, {method: 'DELETE'})
.then(() => {
this.categories = this.categories.filter((category) => {
return category.id !== categoryId;
});
router.push('/category');
});
}
},
};
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>

View file

@ -0,0 +1,333 @@
<template>
<v-container fluid grid-list-md>
<h3>Create a new event</h3>
<v-form>
<v-stepper v-model="e1" vertical>
<v-stepper-step step="1" :complete="e1 > 1">Basic Informations
<small>Title and description</small>
</v-stepper-step>
<v-stepper-content step="1">
<v-layout row wrap>
<v-flex xs12>
<v-text-field
label="Title"
v-model="event.title"
:counter="100"
required
></v-text-field>
</v-flex>
<v-flex md6>
<v-text-field
label="Description"
v-model="event.description"
multiLine
required
></v-text-field>
</v-flex>
<v-flex md6>
<vue-markdown class="markdown-render"
:watches="['show','html','breaks','linkify','emoji','typographer','toc']"
:source="event.description"
:show="true" :html="false" :breaks="true" :linkify="true"
:emoji="true" :typographer="true" :toc="false"
></vue-markdown>
</v-flex>
<v-flex md12>
<v-select
v-bind:items="categories"
v-model="event.category"
item-text="name"
item-value="@id"
label="Categories"
single-line
bottom
></v-select>
</v-flex>
<v-flex md12>
<!--<v-text-field
v-model="tagsToSend"
label="Tags"
></v-text-field>-->
<v-select
v-model="tagsToSend"
label="Tags"
chips
tags
:items="tagsFetched"
></v-select>
</v-flex>
</v-layout>
<v-btn color="primary" @click.native="e1 = 2">Next</v-btn>
</v-stepper-content>
<v-stepper-step step="2" :complete="e1 > 2">Date and place</v-stepper-step>
<v-stepper-content step="2">
Event starts at:
<v-text-field type="datetime-local" v-model="event.startDate"></v-text-field>
<!--<v-layout row wrap>
<v-flex md6>
<v-dialog
persistent
v-model="modals.beginning.date"
lazy
full-width
>
<v-text-field
slot="activator"
label="Beginning of the event date"
v-model="event.startDate.date"
prepend-icon="event"
readonly
></v-text-field>
<v-date-picker v-model="event.startDate.date" scrollable dateFormat="val => new Date(val).">
<template scope="{ save, cancel }">
<v-card-actions>
<v-btn flat primary @click.native="cancel()">Cancel</v-btn>
<v-btn flat primary @click.native="save()">Save</v-btn>
</v-card-actions>
</template>
</v-date-picker>
</v-dialog>
</v-flex>
<v-flex md6>
<v-dialog
persistent
v-model="modals.beginning.time"
lazy
>
<v-text-field
slot="activator"
label="Beginning of the event time"
v-model="event.startDate.time"
prepend-icon="access_time"
readonly
></v-text-field>
<v-time-picker v-model="event.startDate.time" actions format="24h">
<template scope="{ save, cancel }">
<v-card-actions>
<v-btn flat primary @click.native="cancel()">Cancel</v-btn>
<v-btn flat primary @click.native="save()">Save</v-btn>
</v-card-actions>
</template>
</v-time-picker>
</v-dialog>
</v-flex>
</v-layout>-->
Event ends at:
<v-text-field type="datetime-local" v-model="event.endDate"></v-text-field>
<!--<v-layout row wrap>
<v-flex md6>
<v-dialog
persistent
v-model="modals.end.date"
lazy
full-width
>
<v-text-field
slot="activator"
label="End of the event date"
v-model="event.endDate.date"
prepend-icon="event"
readonly
></v-text-field>
<v-date-picker v-model="event.endDate.date" scrollable >
<template scope="{ save, cancel }">
<v-card-actions>
<v-btn flat primary @click.native="cancel()">Cancel</v-btn>
<v-btn flat primary @click.native="save()">Save</v-btn>
</v-card-actions>
</template>
</v-date-picker>
</v-dialog>
</v-flex>
<v-flex md6>
<v-dialog
persistent
v-model="modals.end.time"
lazy
>
<v-text-field
slot="activator"
label="End of the event time"
v-model="event.endDate.time"
prepend-icon="access_time"
readonly
></v-text-field>
<v-time-picker v-model="event.endDate.time" format="24h" actions >
<template scope="{ save, cancel }">
<v-card-actions>
<v-btn flat primary @click.native="cancel()">Cancel</v-btn>
<v-btn flat primary @click.native="save()">Save</v-btn>
</v-card-actions>
</template>
</v-time-picker>
</v-dialog>
</v-flex>
</v-layout>-->
<vuetify-google-autocomplete
id="map"
append-icon="search"
classname="form-control"
placeholder="Start typing"
label="Location"
enable-geolocation
v-on:placechanged="getAddressData"
>
</vuetify-google-autocomplete>
<v-btn color="primary" @click.native="e1 = 3">Next</v-btn>
</v-stepper-content>
<v-stepper-step step="3" :complete="e1 > 3">Extra informations</v-stepper-step>
<v-stepper-content step="3">
<v-text-field
label="Number of seats"
v-model="event.seats"
></v-text-field>
<v-text-field
label="Price"
prefix="$"
type="float"
v-model="event.price"
></v-text-field>
</v-stepper-content>
</v-stepper>
</v-form>
<v-btn color="primary" @click="create">Create event</v-btn>
</v-container>
</template>
<script>
// import Location from '@/components/Location';
import VuetifyGoogleAutocomplete from 'vuetify-google-autocomplete';
import eventFetch from '@/api/eventFetch';
import VueMarkdown from 'vue-markdown';
export default {
name: 'create-event',
props: ['id'],
components: {
/* Location,*/
VueMarkdown,
VuetifyGoogleAutocomplete
},
data() {
return {
e1: 0,
event: {
title: '',
description: '',
startDate: new Date(),
endDate: new Date(),
seats: 0,
address: {
description: null,
floor: null,
geo: {
latitude: null,
longitude: null,
},
addressCountry: null,
addressLocality: null,
addressRegion: null,
postalCode: null,
streetAddress: null,
},
price: 0,
category: null,
tags: [],
participants: [],
},
categories: [],
tags: [{ name: 'test' }, { name: 'montag' }],
tagsToSend: [],
tagsFetched: [],
};
},
created() {
if (this.id) {
this.fetchEvent();
}
},
mounted() {
this.fetchCategories();
this.fetchTags();
},
methods: {
create() {
this.event.seats = parseInt(this.event.seats, 10);
this.tagsToSend.forEach((tag) => {
this.event.tags.push({
name: tag,
// '@type': 'Tag',
});
});
this.event.organizer = "/accounts/" + this.$store.state.user.account.id;
this.event.participants = ["/accounts/" + this.$store.state.user.account.id];
this.event.price = parseFloat(this.event.price);
if (this.id === undefined) {
eventFetch('/events', this.$store, {method: 'POST', body: JSON.stringify(this.event)})
.then(response => response.json())
.then((data) => {
this.loading = false;
this.$router.push({name: 'Event', params: {id: data.id}});
});
} else {
eventFetch(`/events/${this.id}`, this.$store, {method: 'PUT', body: JSON.stringify(this.event)})
.then(response => response.json())
.then((data) => {
this.loading = false;
this.$router.push({name: 'Event', params: {id: data.id}});
});
}
},
fetchCategories() {
eventFetch('/categories', this.$store)
.then(response => response.json())
.then((data) => {
this.loading = false;
this.categories = data['hydra:member'];
});
},
fetchTags() {
eventFetch('/tags', this.$store)
.then(response => response.json())
.then((data) => {
this.loading = false;
data['hydra:member'].forEach((tag) => {
this.tagsFetched.push(tag.name);
});
});
},
fetchEvent() {
eventFetch(`/events/${this.id}`, this.$store)
.then(response => response.json())
.then((data) => {
this.loading = false;
this.event = data;
console.log(this.event);
});
},
getAddressData: function (addressData) {
console.log(addressData);
this.event.address = {
geo: {
latitude: addressData.latitude,
longitude: addressData.longitude,
},
addressCountry: addressData.country,
addressLocality: addressData.locality,
addressRegion: addressData.administrative_area_level_1,
postalCode: addressData.postal_code,
streetAddress: `${addressData.street_number} ${addressData.route}`,
};
},
},
};
</script>
<style>
.markdown-render h1 {
font-size: 2em;
}
</style>

View file

@ -0,0 +1,127 @@
<template>
<v-container fluid grid-list-md>
<h3>Update event {{ event.title }}</h3>
<v-progress-circular v-if="loading" indeterminate color="primary"></v-progress-circular>
<v-form v-if="!loading">
<v-stepper v-model="e1" vertical>
<v-stepper-step step="1" :complete="e1 > 1">Basic Informations
<small>Title and description</small>
</v-stepper-step>
<v-stepper-content step="1">
<v-layout row wrap>
<v-flex xs12>
<v-text-field
label="Title"
v-model="event.title"
:counter="100"
required
></v-text-field>
</v-flex>
<v-flex md6>
<v-text-field
label="Description"
v-model="event.description"
multiLine
required
></v-text-field>
</v-flex>
<v-flex md6>
<vue-markdown class="markdown-render"
:watches="['show','html','breaks','linkify','emoji','typographer','toc']"
:source="event.description"
:show="true" :html="false" :breaks="true" :linkify="true"
:emoji="true" :typographer="true" :toc="false"
></vue-markdown>
</v-flex>
<v-flex md12>
<v-select
v-bind:items="categories"
v-model="event.category"
item-text="name"
item-value="@id"
label="Categories"
single-line
bottom
></v-select>
</v-flex>
<v-flex md12>
<!--<v-text-field
v-model="tagsToSend"
label="Tags"
></v-text-field>-->
<v-select
v-model="tagsToSend"
label="Tags"
chips
tags
:items="tagsFetched"
></v-select>
</v-flex>
</v-layout>
<v-btn color="primary" @click.native="e1 = 2">Next</v-btn>
</v-stepper-content>
<v-stepper-step step="2" :complete="e1 > 2">Date and place</v-stepper-step>
<v-stepper-content step="2">
Event starts at:
<v-text-field type="datetime-local" v-model="event.startDate"></v-text-field>
Event ends at:
<v-text-field type="datetime-local" v-model="event.endDate"></v-text-field>
<vuetify-google-autocomplete
id="map"
append-icon="search"
classname="form-control"
placeholder="Start typing"
label="Location"
enable-geolocation
v-on:placechanged="getAddressData"
>
</vuetify-google-autocomplete>
<v-btn color="primary" @click.native="e1 = 3">Next</v-btn>
</v-stepper-content>
<v-stepper-step step="3" :complete="e1 > 3">Extra informations</v-stepper-step>
<v-stepper-content step="3">
<v-text-field
label="Number of seats"
v-model="event.seats"
></v-text-field>
<v-text-field
label="Price"
prefix="$"
type="float"
v-model="event.price"
></v-text-field>
</v-stepper-content>
</v-stepper>
</v-form>
<v-btn color="primary" @click="create">Create event</v-btn>
</v-container>
</template>
<script>
import eventFetch from '@/api/eventFetch';
export default {
props: ['id'],
data() {
return {
loading: true,
event: null,
};
},
created() {
this.fetchData();
},
methods: {
fetchData() {
eventFetch(`/events/${this.id}`, this.$store)
.then(response => response.json())
.then((data) => {
this.loading = false;
this.event = data;
console.log(this.event);
});
},
}
}
</script>

View file

@ -0,0 +1,147 @@
<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="event.organizer.id === $store.state.user.account.id" :to="{ name: 'EditEvent', params: {id: event.id}}">
<v-icon>edit</v-icon>
</v-btn>
<v-menu bottom left>
<v-btn icon slot="activator">
<v-icon>more_vert</v-icon>
</v-btn>
<v-list>
<v-list-tile @click="downloadIcsEvent()">
<v-list-tile-title>Download</v-list-tile-title>
</v-list-tile>
<v-list-tile @click="deleteEvent()" v-if="$store.state.user.account.id === event.organizer.id">
<v-list-tile-title>Delete</v-list-tile-title>
</v-list-tile>
</v-list>
</v-menu>
</v-card-title>
<v-spacer></v-spacer>
<div class="text-xs-center">
<v-card-title class="pl-5 pt-5">
<div class="display-1 pl-5 pt-5">{{ event.title }}</div>
</v-card-title>
<p><router-link :to="{ name: 'Account', params: {id: event.organizer.id} }"><span class="grey--text">{{ event.organizer.username }}</span></router-link> organises {{ event.title }} <span v-if="event.address.addressLocality">in {{ event.address.addressLocality }}</span> on the {{ event.startDate | formatDate }}.</p>
<v-card-text v-if="event.description"><vue-markdown :source="event.description"></vue-markdown></v-card-text>
</div>
<v-container fluid grid-list-md v-if="event.participants.length > 0">
<v-subheader>Membres</v-subheader>
<v-layout row>
<v-flex xs2 v-for="account in event.participants" :key="account.id">
<router-link :to="{name: 'Account', params: {'id': account.id}}">
<v-avatar size="75px">
<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>
</router-link>
<span>{{ account.username }}</span>
</v-flex>
</v-layout>
</v-container>
<v-card-actions>
<button v-if="!event.participants.map(participant => participant.id).includes($store.state.user.account.id)" @click="joinEvent" class="btn btn-primary">Join</button>
<button v-if="event.participants.map(participant => participant.id).includes($store.state.user.account.id)" @click="leaveEvent" class="btn btn-primary">Leave</button>
<button @click="deleteEvent" class="btn btn-danger">Delete</button>
</v-card-actions>
</v-layout>
</v-card>
</v-flex>
</v-layout>
</v-container>
</template>
<script>
import eventFetch from '@/api/eventFetch';
import VueMarkdown from 'vue-markdown';
export default {
name: 'Home',
components: {
VueMarkdown,
},
data() {
return {
loading: true,
event: {
id: this.id,
title: '',
description: '',
organizer: {
id: null,
username: null,
},
participants: [],
},
};
},
methods: {
deleteEvent() {
const router = this.$router;
eventFetch(`/events/${this.id}`, this.$store, { method: 'DELETE' })
.then(response => response.json())
.then(() => router.push({'name': 'EventList'}));
},
fetchData() {
eventFetch(`/events/${this.id}`, this.$store)
.then(response => response.json())
.then((data) => {
this.loading = false;
this.event = data;
});
},
joinEvent() {
eventFetch(`/events/${this.id}/join`, this.$store)
.then(response => response.json())
.then((data) => {
console.log(data);
});
},
leaveEvent() {
eventFetch(`/events/${this.id}/leave`, this.$store)
.then(response => response.json())
.then((data) => {
console.log(data);
});
},
downloadIcsEvent() {
eventFetch('/events/' + this.event.id + '/export', this.$store, {responseType: 'arraybuffer'})
.then((response) => response.text())
.then(response => {
const blob = new Blob([response],{type: 'text/calendar'});
const link = document.createElement('a');
link.href = window.URL.createObjectURL(blob);
link.download = `${this.event.title}.ics`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
})
},
},
props: ['id'],
created() {
this.fetchData();
},
};
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>

View file

@ -0,0 +1,129 @@
<template>
<v-container>
<h1>{{ $t("event.list.title") }}</h1>
<v-progress-circular v-if="loading" indeterminate color="primary"></v-progress-circular>
<v-chip close v-model="locationChip" label color="pink" text-color="white" v-if="$router.currentRoute.params.location">
<v-icon left>location_city</v-icon>{{ locationText }}
</v-chip>
<v-layout row wrap justify-space-around>
<v-flex xs12 md3 v-for="event in events" :key="event.id">
<v-card>
<v-card-media v-if="event.image"
class="white--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 v-else primary-title>
<div class="headline">{{ event.title }}</div>
</v-card-title>
<v-container>
<span class="grey--text">{{ event.startDate | formatDate }} à <router-link :to="{name: 'EventList', params: {location: geocode(event.address.geo.latitude, event.address.geo.longitude, 10) }}">{{ event.address.addressLocality }}</router-link></span><br>
<p><vue-markdown>{{ event.description }}</vue-markdown></p>
<p v-if="event.organizer">Organisé par <router-link :to="{name: 'Account', params: {'id': event.organizer.id}}">{{ event.organizer.username }}</router-link></p>
</v-container>
<v-card-actions>
<v-btn flat color="orange" @click="downloadIcsEvent(event)">Share</v-btn>
<v-btn flat color="orange" @click="viewEvent(event.id)">Explore</v-btn>
<v-btn flat color="red" @click="deleteEvent(event.id)">Delete</v-btn>
</v-card-actions>
</v-card>
</v-flex>
</v-layout>
<router-link :to="{ name: 'CreateEvent' }" class="btn btn-default">Create</router-link>
</v-container>
</template>
<script>
import ngeohash from 'ngeohash';
import VueMarkdown from 'vue-markdown';
import eventFetch from '@/api/eventFetch';
import VCardTitle from "vuetify/es5/components/VCard/VCardTitle";
export default {
name: 'EventList',
components: {
VCardTitle,
VueMarkdown
},
data() {
return {
events: [],
loading: true,
locationChip: false,
locationText: '',
};
},
props: ['location'],
created() {
this.fetchData(this.$router.currentRoute.params.location);
},
watch: {
locationChip(val) {
if (val === false) {
this.$router.push({name: 'EventList'});
}
}
},
beforeRouteUpdate(to, from, next) {
this.fetchData(to.params.location);
next();
},
methods: {
geocode(lat, lon) {
console.log({lat, lon});
console.log(ngeohash.encode(lat, lon, 10));
return ngeohash.encode(lat, lon, 10);
},
fetchData(location) {
let queryString = '/events';
if (location) {
queryString += ('?geohash=' + location);
const { latitude, longitude } = ngeohash.decode(location);
this.locationText = latitude.toString() + ' : ' + longitude.toString();
}
this.locationChip = true;
eventFetch(queryString, this.$store)
.then(response => response.json())
.then((data) => {
this.loading = false;
this.events = data['hydra:member'];
});
},
deleteEvent(id) {
const router = this.$router;
eventFetch('/events/' + id, this.$store, {'method': 'DELETE'})
.then(() => router.push('/events'));
},
viewEvent(id) {
this.$router.push({ name: 'Event', params: { id } })
},
downloadIcsEvent(event) {
eventFetch('/events/' + event.id + '/export', this.$store, {responseType: 'arraybuffer'})
.then((response) => response.text())
.then(response => {
const blob = new Blob([response],{type: 'text/calendar'});
const link = document.createElement('a');
link.href = window.URL.createObjectURL(blob);
link.download = `${event.title}.ics`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
})
},
},
};
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>

View file

@ -0,0 +1,125 @@
<template>
<v-container>
<h3>Create a new group</h3>
<v-form>
<v-layout row wrap>
<v-flex xs12>
<v-text-field
label="Title"
v-model="group.title"
:counter="100"
required
></v-text-field>
</v-flex>
<v-flex md6>
<v-text-field
label="Description"
v-model="group.description"
multiLine
required
></v-text-field>
</v-flex>
<v-flex md6>
<vue-markdown class="markdown-render"
:watches="['show','html','breaks','linkify','emoji','typographer','toc']"
:source="group.description"
:show="true" :html="false" :breaks="true" :linkify="true"
:emoji="true" :typographer="true" :toc="false"
></vue-markdown>
</v-flex>
<v-flex md12>
<vuetify-google-autocomplete
id="map"
append-icon="search"
classname="form-control"
placeholder="Start typing"
enable-geolocation
v-on:placechanged="getAddressData"
>
</vuetify-google-autocomplete>
</v-flex>
<v-flex md12>
<v-select
v-bind:items="categories"
v-model="group.category"
item-text="name"
item-value="@id"
label="Categories"
single-line
bottom
types="(cities)"
></v-select>
</v-flex>
</v-layout>
</v-form>
<v-btn color="primary" @click="create">Create group</v-btn>
</v-container>
</template>
<script>
import eventFetch from '@/api/eventFetch';
import VueMarkdown from 'vue-markdown';
import VuetifyGoogleAutocomplete from 'vuetify-google-autocomplete';
export default {
name: 'create-group',
components: {
VueMarkdown,
VuetifyGoogleAutocomplete,
},
data() {
return {
e1: 0,
group: {
title: '',
description: '',
category: null,
},
categories: [],
};
},
mounted() {
this.fetchCategories();
},
methods: {
create() {
// this.group.organizer = "/accounts/" + this.$store.state.user.id;
eventFetch('/groups', this.$store, { method: 'POST', body: JSON.stringify(this.group) })
.then(response => response.json())
.then((data) => {
this.loading = false;
this.$router.push({ path: 'Group', params: { id: data.id } });
});
},
fetchCategories() {
eventFetch('/categories', this.$store)
.then(response => response.json())
.then((data) => {
this.loading = false;
this.categories = data['hydra:member'];
});
},
getAddressData: function (addressData) {
this.group.address = {
geo: {
latitude: addressData.latitude,
longitude: addressData.longitude,
},
addressCountry: addressData.country,
addressLocality: addressData.city,
addressRegion: addressData.administrative_area_level_1,
postalCode: addressData.postal_code,
streetAddress: `${addressData.street_number} ${addressData.route}`,
};
},
},
};
</script>
<style>
.markdown-render h1 {
font-size: 2em;
}
</style>

View file

@ -0,0 +1,179 @@
<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">
<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="!group.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="group.avatarRemoteUrl"
>
</v-avatar>
<v-card-title class="pl-5 pt-5">
<div class="display-1 pl-5 pt-5">{{ group.title }}<span v-if="group.server">@{{ group.server.address }}</span></div>
</v-card-title>
<v-card-text v-html="group.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-if="group.address">
<v-list-tile-action>
<v-icon color="indigo">location_on</v-icon>
</v-list-tile-action>
<v-list-tile-content>
<v-list-tile-title>{{ group.address.streetAddress }}</v-list-tile-title>
<v-list-tile-sub-title>{{ group.address.postalCode }} {{ group.address.locality }}</v-list-tile-sub-title>
</v-list-tile-content>
</v-list-tile>
</v-list>
<v-container fluid grid-list-md v-if="group.groupAccounts.length > 0">
<v-subheader>Membres</v-subheader>
<v-layout row>
<v-flex xs2 v-for="groupAccount in group.groupAccounts" :key="groupAccount.id">
<router-link :to="{name: 'Account', params: {'id': groupAccount.account.id}}">
<v-badge overlap>
<span slot="badge" v-if="groupAccount.role == 3"><v-icon>stars</v-icon></span>
<v-avatar size="75px">
<img v-if="!groupAccount.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="groupAccount.account.avatarRemoteUrl"
>
</v-avatar>
</v-badge>
</router-link>
<span>{{ groupAccount.account.username }}</span>
</v-flex>
</v-layout>
</v-container>
<v-container fluid grid-list-md v-if="group.events.length > 0">
<v-subheader>Participated at</v-subheader>
<v-layout row wrap>
<v-flex v-for="event in group.events" :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: 'Group',
props: ['id'],
data() {
return {
group: {
id: this.id,
title: '',
description: '',
},
loading: true,
}
},
methods: {
fetchData() {
eventFetch(`/groups/${this.id}`, this.$store)
.then(response => response.json())
.then((data) => {
this.loading = false;
this.group = data;
});
},
deleteGroup() {
const router = this.$router;
eventFetch(`/groups/${this.id}`, this.$store, { method: 'DELETE' })
.then(response => response.json())
.then(() => router.push('/groups'));
},
},
created() {
this.fetchData();
},
}
</script>

View file

@ -0,0 +1,86 @@
<template>
<v-container>
<h1>Group List</h1>
<v-progress-circular v-if="loading" indeterminate color="primary"></v-progress-circular>
<v-layout row wrap justify-space-around>
<v-flex xs12 md3 v-for="group in groups" :key="group.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">{{ group.title }}</span>
</v-flex>
</v-layout>
</v-container>
</v-card-media>
<v-card-title>
<div>
<span class="grey--text">{{ group.startDate | formatDate }} à {{ group.location }}</span><br>
<p>{{ group.description }}</p>
<p v-if="group.organizer">Organisé par <router-link :to="{name: 'Account', params: {'id': group.organizer.id}}">{{ group.organizer.username }}</router-link></p>
</div>
</v-card-title>
<v-card-actions>
<v-btn flat color="green" @click="joinGroup(group.id)"><v-icon v-if="group.locked">lock</v-icon>Join</v-btn>
<v-btn flat color="orange" @click="viewEvent(group.id)">Explore</v-btn>
<v-btn flat color="red" @click="deleteEvent(group.id)">Delete</v-btn>
</v-card-actions>
</v-card>
</v-flex>
</v-layout>
<router-link :to="{ name: 'CreateGroup' }" class="btn btn-default">Create</router-link>
</v-container>
</template>
<script>
import eventFetch from '@/api/eventFetch';
export default {
name: 'GroupList',
data() {
return {
groups: [],
loading: true,
};
},
created() {
this.fetchData();
},
methods: {
fetchData() {
eventFetch('/groups', this.$store)
.then(response => response.json())
.then((data) => {
this.loading = false;
this.groups = data['hydra:member'];
});
},
deleteEvent(id) {
const router = this.$router;
eventFetch('/groups/' + id, this.$store, {'method': 'DELETE'})
.then(response => response.json())
.then(() => router.push('/groups'));
},
viewEvent(id) {
this.$router.push({ name: 'Group', params: { id } })
},
joinGroup(id) {
const router = this.$router;
eventFetch('/groups/' + id + '/join', this.$store)
.then(response => response.json())
.then(() => router.push('/group/' + id))
}
},
};
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>

View file

@ -0,0 +1,93 @@
<template>
<v-container>
<h1 class="welcome" v-if="$store.state.user">{{ $t("home.welcome", { 'username': $store.state.user.username}) }}</h1>
<h1 class="welcome" v-else>{{ $t("home.welcome_off", { 'username': $store.state.user.username}) }}</h1>
<router-link :to="{ name: 'EventList' }">{{ $t('home.events') }}</router-link>
<router-link v-if="$store.state.user === false" :to="{ name: 'Login' }">{{ $t('home.login') }}</router-link>
<router-link v-if="$store.state.user === false" :to="{ name: 'Register' }">{{ $t('home.register') }}</router-link>
<v-layout row>
<v-flex xs6>
<v-btn large @click="geoLocalize"><v-icon>my_location</v-icon>Me géolocaliser</v-btn>
</v-flex>
<v-flex xs6>
<vuetify-google-autocomplete
id="map"
append-icon="search"
classname="form-control"
placeholder="Start typing"
enable-geolocation
types="(cities)"
v-on:placechanged="getAddressData"
>
</vuetify-google-autocomplete>
</v-flex>
</v-layout>
</v-container>
</template>
<script>
import VuetifyGoogleAutocomplete from 'vuetify-google-autocomplete';
import ngeohash from 'ngeohash';
import eventFetch from "../api/eventFetch";
export default {
components: { VuetifyGoogleAutocomplete },
name: 'Home',
data() {
return {
user: null,
searchTerm: null,
location_field: {
loading: false,
search: null,
},
locations: [],
};
},
mounted() {
// this.fetchLocations();
},
methods: {
fetchLocations() {
eventFetch('/locations', this.$store)
.then((response) => (response.json()))
.then((response) => {
this.locations = response['hydra:member'];
});
},
geoLocalize() {
const router = this.$router;
if (sessionStorage.getItem('City')) {
router.push({name: 'EventList', params: {location: localStorage.getItem('City')}})
} else {
navigator.geolocation.getCurrentPosition((pos) => {
const crd = pos.coords;
const geohash = ngeohash.encode(crd.latitude, crd.longitude, 11);
sessionStorage.setItem('City', geohash);
router.push({name: 'EventList', params: {location: geohash}});
}, (err) => console.warn(`ERROR(${err.code}): ${err.message}`), {
enableHighAccuracy: true,
timeout: 5000,
maximumAge: 0
});
}
},
getAddressData: function (addressData) {
const geohash = ngeohash.encode(addressData.latitude, addressData.longitude, 11);
sessionStorage.setItem('City', geohash);
this.$router.push({name: 'EventList', params: {location: geohash}});
},
},
};
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
.search-autocomplete {
border: 1px solid #dbdbdb;
color: rgba(0,0,0,.87);
}
</style>

View file

@ -0,0 +1,52 @@
<template>
<div>
<!--<gmap-autocomplete :value="description" @input="setPlace"
@place_changed="setPlace">
</gmap-autocomplete>
<br />
<gmap-map
:center="center"
:zoom="15"
style="width: 500px; height: 300px"
>
<gmap-marker
:key="index"
v-for="(m, index) in markers"
:position="m.position"
:clickable="true"
:draggable="true"
@click="center=m.position"
></gmap-marker>
</gmap-map>-->
{{ center.lat }} - {{ center.lng }}
</div>
</template>
<script>
export default {
data() {
return {
description: 'Paris, France',
center: { lat: 48.85, lng: 2.35 },
markers: [],
};
},
props: ['address'],
methods: {
setPlace(place) {
this.center = {
lat: place.geometry.location.lat(),
lng: place.geometry.location.lng(),
};
this.markers = [{
position: { lat: this.center.lat, lng: this.center.lng },
}];
this.$emit('input', place.formatted_address);
},
},
};
</script>

View file

@ -0,0 +1,76 @@
<template>
<div>
<v-form>
<v-text-field
label="Email"
required
type="text"
v-model="credentials.email"
:rules="[rules.required]"
>
</v-text-field>
<v-text-field
label="password"
required
type="password"
v-model="credentials.password"
:rules="[rules.required]"
>
</v-text-field>
<v-btn @click="loginAction" color="blue">Login</v-btn>
</v-form>
<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>
</div>
</template>
<script>
import auth from '@/auth/index';
export default {
beforeCreate() {
if (this.$store.state.user) {
this.$router.push('/');
}
},
data() {
return {
credentials: {
email: '',
password: '',
},
error: {
show: false,
text: '',
timeout: 3000,
field: {
email: false,
password: false,
},
},
rules: {
required: value => !!value || 'Required.',
},
};
},
methods: {
loginAction(e) {
e.preventDefault();
auth.login(JSON.stringify(this.credentials), this.$store, '/', (error) => {
this.error.show = true;
this.error.text = error.message;
this.error.field[error.field] = true;
});
},
},
};
</script>

View file

@ -0,0 +1,133 @@
<template>
<v-toolbar
class="blue darken-3"
dark
app
clipped-left
fixed
>
<v-toolbar-title style="width: 300px" class="ml-0 pl-3">
<v-toolbar-side-icon @click.stop="drawer = !drawer"></v-toolbar-side-icon>
<router-link :to="{ name: 'Home' }">
Libre-Event
</router-link>
</v-toolbar-title>
<v-select
autocomplete
:loading="searchElement.loading"
light
solo
prepend-icon="search"
placeholder="Search"
required
item-text="displayedText"
:items="searchElement.items"
:search-input.sync="search"
v-model="searchSelect"
></v-select>
<v-spacer></v-spacer>
<v-menu
offset-y
:close-on-content-click="false"
:nudge-width="200"
v-model="notificationMenu"
>
<v-btn icon slot="activator">
<v-badge left color="red">
<span slot="badge">{{ notifications.length }}</span>
<v-icon>notifications</v-icon>
</v-badge>
</v-btn>
<v-card>
<v-list two-line>
<template v-for="item in notifications">
<v-subheader v-if="item.header" v-text="item.header" v-bind:key="item.header"></v-subheader>
<v-divider v-else-if="item.divider" v-bind:inset="item.inset" v-bind:key="item.inset"></v-divider>
<v-list-tile avatar v-else v-bind:key="item.title">
<v-list-tile-content>
<v-list-tile-title v-html="item.title"></v-list-tile-title>
<v-list-tile-sub-title v-html="item.subtitle"></v-list-tile-sub-title>
</v-list-tile-content>
</v-list-tile>
</template>
</v-list>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn flat @click="notificationMenu = false">Close</v-btn>
<v-btn color="primary" flat @click="notificationMenu = false">Save</v-btn>
</v-card-actions>
</v-card>
</v-menu>
<v-btn flat @click="$router.push({name: 'Account', params: {'id': getUser().account.id}})" v-if="$store.state.isLogged && getUser()">{{ getUser().username }}</v-btn>
</v-toolbar>
</template>
<script>
import eventFetch from '@/api/eventFetch';
export default {
name: 'NavBar',
data() {
return {
notificationMenu: false,
notifications: [
{header: 'Coucou'},
{title: "T'as une notification", subtitle: 'Et elle est cool'},
],
searchElement: {
loading: false,
items: [],
},
search: null,
searchSelect: null,
};
},
watch: {
search (val) {
val && this.querySelections(val)
},
searchSelect(val) {
console.log(val);
if (val.hasOwnProperty('addressLocality')) {
this.$router.push({name: 'EventList', params: {location: val.geohash}});
} else {
this.$router.push({name: 'Account', params: {id: val.id}});
}
}
},
methods: {
getUser() {
return this.$store.state.user === undefined ? false : this.$store.state.user;
},
querySelections(searchTerm) {
this.searchElement.loading = true;
eventFetch('/find/', this.$store, {method: 'POST', body: JSON.stringify({search: searchTerm})})
.then(response => response.json())
.then((results) => {
console.log(results);
const accountResults = results.accounts.map((result) => {
if (result.server) {
result.displayedText = `${result.username}@${result.server.address}`;
} else {
result.displayedText = result.username;
}
return result;
});
const cities = new Set();
const placeResults = results.places.map((result) => {
result.displayedText = result.addressLocality;
return result;
}).filter((result) => {
if (cities.has(result.addressLocality)) {
return false;
}
cities.add(result.addressLocality);
return true;
});
this.searchElement.items = accountResults.concat(placeResults);
this.searchElement.loading = false;
});
}
}
}
</script>

View file

@ -0,0 +1,10 @@
<template>
<v-container>
<v-layout row>
<v-flex xs12 sm6 offset-sm3>
<h1>404 !</h1>
<img src="../../static/oh_no.jpg" />
</v-flex>
</v-layout>
</v-container>
</template>

View file

@ -0,0 +1,83 @@
<template>
<div>
<v-form>
<v-text-field
label="Username"
required
type="text"
v-model="credentials.username"
:rules="[rules.required]"
>
</v-text-field>
<v-text-field
label="email"
required
type="email"
v-model="credentials.email"
:rules="[rules.required, rules.email]"
>
</v-text-field>
<v-text-field
label="password"
required
type="password"
v-model="credentials.password"
:rules="[rules.required]"
>
</v-text-field>
<v-btn @click="registerAction" color="primary">Register</v-btn>
</v-form>
<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>
</div>
</template>
<script>
import auth from '@/auth/index';
export default {
data() {
return {
credentials: {
username: '',
email: '',
password: '',
},
error: {
show: false,
text: '',
timeout: 3000,
field: {
username: false,
email: false,
password: false,
},
},
rules: {
required: value => !!value || 'Required.',
email: (value) => {
const pattern = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
return pattern.test(value) || 'Invalid e-mail.';
},
},
};
},
methods: {
registerAction(e) {
e.preventDefault();
auth.signup(JSON.stringify(this.credentials), this.$store, {name: 'Home'}, (error) => {
this.error.show = true;
this.error.text = error.message;
this.error.field[error.field] = true;
});
},
},
};
</script>

15
js/src/i18n/en.js Normal file
View file

@ -0,0 +1,15 @@
export default {
home: {
welcome: 'Welcome on Libre-Event, {username}',
welcome_off: 'Welcome on Libre-Event',
events: 'Events',
groups: 'Groups',
login: 'Login',
register: 'Register',
},
event: {
list: {
title: "Your event list",
},
},
};

15
js/src/i18n/fr.js Normal file
View file

@ -0,0 +1,15 @@
export default {
home: {
welcome: 'Bienvenue sur Libre-Event, {username}!',
welcome_off: 'Bienvenue sur Libre-Event',
events: 'Événements',
groups: 'Groupes',
login: 'Se connecter',
register: "S'inscrire",
},
event: {
list: {
title: "Votre liste d'événements",
},
},
};

6
js/src/i18n/index.js Normal file
View file

@ -0,0 +1,6 @@
import en from './en';
import fr from './fr';
export default {
en, fr,
};

56
js/src/main.js Normal file
View file

@ -0,0 +1,56 @@
// The Vue build version to load with the `import` command
// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
import Vue from 'vue';
// import * as VueGoogleMaps from 'vue2-google-maps';
import VueMarkdown from 'vue-markdown';
import Vuetify from 'vuetify';
import Vuex from 'vuex';
import moment from 'moment';
import VuexI18n from 'vuex-i18n';
import 'vuetify/dist/vuetify.min.css';
import App from '@/App';
import router from '@/router';
import storeData from './store/index';
import translations from './i18n/index';
Vue.config.productionTip = false;
/*Vue.use(VueGoogleMaps, {
load: {
key: 'AIzaSyBF37pw38j0giICt73TCAPNogc07Upe_Q4',
libraries: 'places',
installComponents: false,
},
});*/
Vue.use(VueMarkdown);
Vue.use(Vuetify);
Vue.use(Vuex);
let language = window.navigator.userLanguage || window.navigator.language;
moment.locale(language);
Vue.filter('formatDate', value => (value ? moment(String(value)).format('LLLL') : null));
if (!(language in translations)) {
[language] = language.split('-', 1);
}
const store = new Vuex.Store(storeData);
Vue.use(VuexI18n.plugin, store);
Object.entries(translations).forEach((key) => {
Vue.i18n.add(key[0], key[1]);
});
Vue.i18n.set(language);
Vue.i18n.fallback('en');
/* eslint-disable no-new */
new Vue({
el: '#app',
router,
store,
template: '<App/>',
components: { App },
});

133
js/src/router/index.js Normal file
View file

@ -0,0 +1,133 @@
import Vue from 'vue';
import Router from 'vue-router';
import PageNotFound from '@/components/PageNotFound';
import Home from '@/components/Home';
import Event from '@/components/Event/Event';
import EventList from '@/components/Event/EventList';
import Location from '@/components/Location';
import CreateEvent from '@/components/Event/Create';
import CategoryList from '@/components/Category/List';
import CreateCategory from '@/components/Category/Create';
import Register from '@/components/Register';
import Login from '@/components/Login';
import Account from '@/components/Account/Account';
import CreateGroup from '@/components/Group/Create';
import Group from '@/components/Group/Group';
import GroupList from '@/components/Group/GroupList';
import Auth from '@/auth/index';
Vue.use(Router);
const router = new Router({
mode: 'history',
base: '/',
routes: [
{
path: '/',
name: 'Home',
component: Home,
meta: { requiredAuth: false },
},
{
path: '/events/list/:location?',
name: 'EventList',
component: EventList,
meta: { requiredAuth: false },
},
{
path: '/events/:id(\\d+)',
name: 'Event',
component: Event,
props: true,
meta: { requiredAuth: false },
},
{
path: '/events/create',
name: 'CreateEvent',
component: CreateEvent,
meta: { requiredAuth: true },
},
{
path: '/events/:id(\\d+)/edit',
name: 'EditEvent',
component: CreateEvent,
props: true,
meta: { requiredAuth: true },
},
{
path: '/location/new',
name: 'Location',
component: Location,
meta: { requiredAuth: true },
},
{
path: '/category',
name: 'CategoryList',
component: CategoryList,
meta: { requiredAuth: false },
},
{
path: '/category/create',
name: 'CreateCategory',
component: CreateCategory,
meta: { requiredAuth: true },
},
{
path: '/register',
name: 'Register',
component: Register,
meta: { requiredAuth: false },
},
{
path: '/login',
name: 'Login',
component: Login,
meta: { requiredAuth: false },
},
{
path: '/accounts/:id(\\d+)',
name: 'Account',
component: Account,
props: true,
meta: { requiredAuth: false },
},
{
path: '/group',
name: 'GroupList',
component: GroupList,
meta: { requiredAuth: false },
},
{
path: '/group-create',
name: 'CreateGroup',
component: CreateGroup,
meta: { requiredAuth: true },
},
{
path: '/group/:id',
name: 'Group',
component: Group,
props: true,
meta: { requiredAuth: false },
},
{ path: "*",
name: 'PageNotFound',
component: PageNotFound,
meta: { requiredAuth: false },
},
],
});
router.beforeEach((to, from, next) => {
if (to.matched.some(record => record.meta.requiredAuth) && !Auth.checkAuth()) {
console.log('needs login');
next({
path: '/',
query: { redirect: to.fullPath }
});
} else {
next();
}
});
export default router;

21
js/src/store/index.js Normal file
View file

@ -0,0 +1,21 @@
import { LOGIN_USER, LOGOUT_USER, SAVE_USER } from './mutation-types';
const state = {
isLogged: !!localStorage.getItem('token'),
user: false,
};
const mutations = {
[LOGIN_USER](state) {
state.isLogged = true;
},
[LOGOUT_USER](state) {
state.isLogged = false;
},
[SAVE_USER](state, user) {
state.user = user;
},
};
export default { state, mutations };

View file

@ -0,0 +1,3 @@
export const LOGIN_USER = 'LOGIN_USER';
export const LOGOUT_USER = 'LOGOUT_USER';
export const SAVE_USER = 'SAVE_USER';

0
js/static/.gitkeep Normal file
View file

BIN
js/static/oh_no.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View file

@ -0,0 +1,26 @@
// A custom Nightwatch assertion.
// the name of the method is the filename.
// can be used in tests like this:
//
// browser.assert.elementCount(selector, count)
//
// for how to write custom assertions see
// http://nightwatchjs.org/guide#writing-custom-assertions
exports.assertion = function (selector, count) {
this.message = 'Testing if element <' + selector + '> has count: ' + count;
this.expected = count;
this.pass = function (val) {
return val === this.expected;
}
this.value = function (res) {
return res.value;
}
this.command = function (cb) {
var self = this;
return this.api.execute(function (selector) {
return document.querySelectorAll(selector).length;
}, [selector], function (res) {
cb.call(self, res);
});
}
}

View file

@ -0,0 +1,46 @@
require('babel-register')
var config = require('../../config')
// http://nightwatchjs.org/gettingstarted#settings-file
module.exports = {
src_folders: ['test/e2e/specs'],
output_folder: 'test/e2e/reports',
custom_assertions_path: ['test/e2e/custom-assertions'],
selenium: {
start_process: true,
server_path: require('selenium-server').path,
host: '127.0.0.1',
port: 4444,
cli_args: {
'webdriver.chrome.driver': require('chromedriver').path
}
},
test_settings: {
default: {
selenium_port: 4444,
selenium_host: 'localhost',
silent: true,
globals: {
devServerURL: 'http://localhost:' + (process.env.PORT || config.dev.port)
}
},
chrome: {
desiredCapabilities: {
browserName: 'chrome',
javascriptEnabled: true,
acceptSslCerts: true
}
},
firefox: {
desiredCapabilities: {
browserName: 'firefox',
javascriptEnabled: true,
acceptSslCerts: true
}
}
}
}

33
js/test/e2e/runner.js Normal file
View file

@ -0,0 +1,33 @@
// 1. start the dev server using production config
process.env.NODE_ENV = 'testing';
var server = require('../../build/dev-server.js');
server.ready.then(() => {
// 2. run the nightwatch test suite against it
// to run in additional browsers:
// 1. add an entry in test/e2e/nightwatch.conf.json under "test_settings"
// 2. add it to the --env flag below
// or override the environment flag, for example: `npm run e2e -- --env chrome,firefox`
// For more information on Nightwatch's config file, see
// http://nightwatchjs.org/guide#settings-file
var opts = process.argv.slice(2);
if (opts.indexOf('--config') === -1) {
opts = opts.concat(['--config', 'test/e2e/nightwatch.conf.js']);
}
if (opts.indexOf('--env') === -1) {
opts = opts.concat(['--env', 'chrome']);
}
var spawn = require('cross-spawn');
var runner = spawn('./node_modules/.bin/nightwatch', opts, { stdio: 'inherit' });
runner.on('exit', function (code) {
server.close();
process.exit(code);
});
runner.on('error', function (err) {
server.close();
throw err;
});
});

19
js/test/e2e/specs/test.js Normal file
View file

@ -0,0 +1,19 @@
// For authoring Nightwatch tests, see
// http://nightwatchjs.org/guide#usage
module.exports = {
'default e2e tests': function test(browser) {
// automatically uses dev Server port from /config.index.js
// default: http://localhost:8080
// see nightwatch.conf.js
const devServer = browser.globals.devServerURL;
browser
.url(devServer)
.waitForElementVisible('#app', 5000)
.assert.elementPresent('.hello')
.assert.containsText('h1', 'Welcome to Your Vue.js App')
.assert.elementCount('img', 1)
.end();
},
};

9
js/test/unit/.eslintrc Normal file
View file

@ -0,0 +1,9 @@
{
"env": {
"mocha": true
},
"globals": {
"expect": true,
"sinon": true
}
}

13
js/test/unit/index.js Normal file
View file

@ -0,0 +1,13 @@
import Vue from 'vue';
Vue.config.productionTip = false;
// require all test files (files that ends with .spec.js)
const testsContext = require.context('./specs', true, /\.spec$/);
testsContext.keys().forEach(testsContext);
// require all src files except main.js for coverage.
// you can also change this to match only the subset of files that
// you want coverage for.
const srcContext = require.context('../../src', true, /^\.\/(?!main(\.js)?$)/);
srcContext.keys().forEach(srcContext);

View file

@ -0,0 +1,33 @@
// This is a karma config file. For more details see
// http://karma-runner.github.io/0.13/config/configuration-file.html
// we are also using it with karma-webpack
// https://github.com/webpack/karma-webpack
var webpackConfig = require('../../build/webpack.test.conf');
module.exports = function (config) {
config.set({
// to run in additional browsers:
// 1. install corresponding karma launcher
// http://karma-runner.github.io/0.13/config/browsers.html
// 2. add it to the `browsers` array below.
browsers: ['PhantomJS'],
frameworks: ['mocha', 'sinon-chai', 'phantomjs-shim'],
reporters: ['spec', 'coverage'],
files: ['./index.js'],
preprocessors: {
'./index.js': ['webpack', 'sourcemap']
},
webpack: webpackConfig,
webpackMiddleware: {
noInfo: true,
},
coverageReporter: {
dir: './coverage',
reporters: [
{ type: 'lcov', subdir: '.' },
{ type: 'text-summary' },
]
},
});
};

View file

@ -0,0 +1,11 @@
import Vue from 'vue';
import Hello from '@/components/Home';
describe('Hello.vue', () => {
it('should render correct contents', () => {
const Constructor = Vue.extend(Hello);
const vm = new Constructor().$mount();
expect(vm.$el.querySelector('.hello h1').textContent)
.to.equal('Welcome to Your Vue.js App');
});
});

View file

@ -4,21 +4,20 @@ defmodule Eventos.Accounts.Account do
alias Eventos.Accounts.{Account, GroupAccount, GroupRequest, Group, User} alias Eventos.Accounts.{Account, GroupAccount, GroupRequest, Group, User}
alias Eventos.Events.Event alias Eventos.Events.Event
schema "accounts" do schema "accounts" do
field :username, :string
field :description, :string field :description, :string
field :display_name, :string field :display_name, :string
field :domain, :string field :domain, :string, default: nil
field :private_key, :string field :private_key, :string
field :public_key, :string field :public_key, :string
field :suspended, :boolean, default: false field :suspended, :boolean, default: false
field :uri, :string field :uri, :string
field :url, :string field :url, :string
field :username, :string
has_many :organized_events, Event has_many :organized_events, Event
many_to_many :groups, Group, join_through: GroupAccount many_to_many :groups, Group, join_through: GroupAccount
has_many :group_request, GroupRequest has_many :group_request, GroupRequest
has_one :user_id, User has_one :user, User
timestamps() timestamps()
end end
@ -27,6 +26,7 @@ defmodule Eventos.Accounts.Account do
def changeset(%Account{} = account, attrs) do def changeset(%Account{} = account, attrs) do
account account
|> cast(attrs, [:username, :domain, :display_name, :description, :private_key, :public_key, :suspended, :uri, :url]) |> cast(attrs, [:username, :domain, :display_name, :description, :private_key, :public_key, :suspended, :uri, :url])
|> validate_required([:username, :domain, :display_name, :description, :private_key, :public_key, :suspended, :uri, :url]) |> validate_required([:username, :display_name, :description, :private_key, :public_key, :suspended])
|> unique_constraint(:username, name: :accounts_username_domain_index)
end end
end end

View file

@ -1,16 +1,17 @@
defmodule Eventos.Accounts.User do defmodule Eventos.Accounts.User do
use Ecto.Schema use Ecto.Schema
import Ecto.Changeset import Ecto.Changeset
alias Eventos.Accounts.{User} alias Eventos.Accounts.{Account, User}
alias Eventos.Repo
import Logger
schema "users" do schema "users" do
field :email, :string field :email, :string
field :role, :integer, default: 0 field :role, :integer, default: 0
field :password, :string, virtual: true field :password, :string, virtual: true
field :password_hash, :string field :password_hash, :string
field :account_id, :integer belongs_to :account, Account
timestamps() timestamps()
end end
@ -18,7 +19,7 @@ defmodule Eventos.Accounts.User do
@doc false @doc false
def changeset(%User{} = user, attrs) do def changeset(%User{} = user, attrs) do
user user
|> cast(attrs, [:email, :password_hash, :role]) |> cast(attrs, [:email, :password_hash])
|> validate_required([:email]) |> validate_required([:email])
|> unique_constraint(:email) |> unique_constraint(:email)
|> validate_format(:email, ~r/@/) |> validate_format(:email, ~r/@/)

View file

@ -0,0 +1,9 @@
defmodule EventosWeb.AppController do
use EventosWeb, :controller
plug :put_layout, false
def app(conn, _params) do
render conn, "index.html"
end
end

View file

@ -1,11 +1,8 @@
defmodule EventosWeb.PageController do defmodule EventosWeb.PageController do
use EventosWeb, :controller use EventosWeb, :controller
import Logger
def index(conn, _params) do def index(conn, _params) do
render conn, "index.html" render conn, "index.html"
end end
def app(conn, _params) do
render conn, "index.html"
end
end end

View file

@ -8,9 +8,12 @@ defmodule EventosWeb.SessionController do
# Attempt to authenticate the user # Attempt to authenticate the user
with {:ok, token, _claims} <- Accounts.authenticate(%{user: user, password: password}) do with {:ok, token, _claims} <- Accounts.authenticate(%{user: user, password: password}) do
# Render the token # Render the token
render conn, "token.json", token: token user = Eventos.Repo.preload user, :account
render conn, "token.json", %{token: token, user: user}
end end
send_resp(conn, 400, "Bad login")
end end
send_resp(conn, 400, "No such user")
end end
def sign_out(conn, _params) do def sign_out(conn, _params) do

View file

@ -1,5 +1,6 @@
defmodule EventosWeb.UserController do defmodule EventosWeb.UserController do
use EventosWeb, :controller use EventosWeb, :controller
import Logger
alias Eventos.Accounts alias Eventos.Accounts
alias Eventos.Accounts.User alias Eventos.Accounts.User
@ -57,4 +58,34 @@ defmodule EventosWeb.UserController do
|> put_flash(:info, "User deleted successfully.") |> put_flash(:info, "User deleted successfully.")
|> redirect(to: user_path(conn, :index)) |> redirect(to: user_path(conn, :index))
end end
def register(conn, %{"email" => email, "password" => password, "username" => username}) do
{:ok, {privkey, pubkey}} = RsaEx.generate_keypair("4096")
account_change = Ecto.Changeset.change(%Eventos.Accounts.Account{}, %{
username: username,
description: "tata",
display_name: "toto",
domain: nil,
private_key: privkey,
public_key: pubkey,
uri: "",
url: ""
})
user_change = Eventos.Accounts.User.registration_changeset(%Eventos.Accounts.User{}, %{
email: email,
password: password,
password_confirmation: password
})
account_with_user = Ecto.Changeset.put_assoc(account_change, :user, user_change)
Eventos.Repo.insert!(account_with_user)
user = Eventos.Accounts.find(email)
user = Eventos.Repo.preload user, :account
render conn, "user.json", %{user: user}
end
end end

View file

@ -9,7 +9,7 @@ defmodule EventosWeb.Endpoint do
# when deploying your static files in production. # when deploying your static files in production.
plug Plug.Static, plug Plug.Static,
at: "/", from: :eventos, gzip: false, at: "/", from: :eventos, gzip: false,
only: ~w(css fonts images js favicon.ico robots.txt) only: ~w(css fonts images js favicon.ico robots.txt index.html)
# Code reloading can be explicitly enabled under the # Code reloading can be explicitly enabled under the
# :code_reloader configuration of your endpoint. # :code_reloader configuration of your endpoint.
@ -19,6 +19,7 @@ defmodule EventosWeb.Endpoint do
plug Phoenix.CodeReloader plug Phoenix.CodeReloader
end end
plug CORSPlug
plug Plug.RequestId plug Plug.RequestId
plug Plug.Logger plug Plug.Logger

View file

@ -9,18 +9,26 @@ defmodule EventosWeb.Router do
plug EventosWeb.AuthPipeline plug EventosWeb.AuthPipeline
end end
scope "/api" do pipeline :browser do
plug :accepts, ["html"]
plug :fetch_session
plug :fetch_flash
plug :protect_from_forgery
plug :put_secure_browser_headers
end
scope "/api", EventosWeb do
pipe_through :api pipe_through :api
resources "/users", UserController, only: [:create] post "/users", UserController, :register
post "/sign-in", EventosWeb.SessionController, :sign_in post "/login", SessionController, :sign_in
resources "/groups", GroupController, only: [:index]
end end
# Other scopes may use custom stacks. # Other scopes may use custom stacks.
scope "/api", EventosWeb do scope "/api", EventosWeb do
pipe_through :api_auth pipe_through :api_auth
post "/sign-out", SessionController, :sign_out post "/sign-out", SessionController, :sign_out
resources "/users", UserController resources "/users", UserController
resources "/accounts", AccountController resources "/accounts", AccountController
@ -29,8 +37,14 @@ defmodule EventosWeb.Router do
resources "/tags", TagController resources "/tags", TagController
resources "/event_accounts", EventAccountsController resources "/event_accounts", EventAccountsController
resources "/event_requests", EventRequestController resources "/event_requests", EventRequestController
resources "/groups", GroupController resources "/groups", GroupController, except: [:index]
resources "/group_accounts", GroupAccountController resources "/group_accounts", GroupAccountController
resources "/group_requests", GroupRequestController resources "/group_requests", GroupRequestController
end end
scope "/", EventosWeb do
pipe_through :browser
get "/*path", AppController, :app
end
end end

View file

@ -0,0 +1 @@
<!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><link href=/css/app.c6f4f0637b07f4b32d59e43e26ada6c7.css rel=stylesheet></head><body><noscript>Mets du JS.</noscript><div id=app></div><script type=text/javascript src=/js/manifest.79c2975577a8222315fd.js></script><script type=text/javascript src=/js/vendor.94561603df84d1708ae1.js></script><script type=text/javascript src=/js/app.dc4c839388191b886181.js></script></body></html>

View file

@ -1,3 +1,15 @@
defmodule EventosWeb.AccountView do defmodule EventosWeb.AccountView do
use EventosWeb, :view use EventosWeb, :view
def render("account.json", %{"account": account}) do
%{
username: account.username,
description: account.description,
display_name: account.display_name,
domain: account.domain,
suspended: account.suspended,
uri: account.uri,
url: account.url,
}
end
end end

View file

@ -0,0 +1,3 @@
defmodule EventosWeb.AppView do
use EventosWeb, :view
end

View file

@ -1,7 +1,7 @@
defmodule EventosWeb.SessionView do defmodule EventosWeb.SessionView do
use EventosWeb, :view use EventosWeb, :view
def render("token.json", %{token: token}) do def render("token.json", %{token: token, user: user}) do
%{token: token} %{token: token, user: render_one(user, EventosWeb.UserView, "user.json")}
end end
end end

View file

@ -1,3 +1,11 @@
defmodule EventosWeb.UserView do defmodule EventosWeb.UserView do
use EventosWeb, :view use EventosWeb, :view
import Logger
def render("user.json", %{"user": user}) do
%{
email: user.email,
account: render_one(user.account, EventosWeb.AccountView, "account.json"),
}
end
end end

View file

@ -43,7 +43,9 @@ defmodule Eventos.Mixfile do
{:cowboy, "~> 1.0"}, {:cowboy, "~> 1.0"},
{:guardian, "~> 1.0"}, {:guardian, "~> 1.0"},
{:comeonin, "~> 4.0"}, {:comeonin, "~> 4.0"},
{:argon2_elixir, "~> 1.2"} {:argon2_elixir, "~> 1.2"},
{:cors_plug, "~> 1.2"},
{:rsa_ex, "~> 0.1"}
] ]
end end

View file

@ -5,6 +5,7 @@
"combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [], [], "hexpm"}, "combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [], [], "hexpm"},
"comeonin": {:hex, :comeonin, "4.0.3", "4e257dcb748ed1ca2651b7ba24fdbd1bd24efd12482accf8079141e3fda23a10", [:mix], [{:argon2_elixir, "~> 1.2", [hex: :argon2_elixir, repo: "hexpm", optional: true]}, {:bcrypt_elixir, "~> 0.12.1 or ~> 1.0", [hex: :bcrypt_elixir, repo: "hexpm", optional: true]}, {:pbkdf2_elixir, "~> 0.12", [hex: :pbkdf2_elixir, repo: "hexpm", optional: true]}], "hexpm"}, "comeonin": {:hex, :comeonin, "4.0.3", "4e257dcb748ed1ca2651b7ba24fdbd1bd24efd12482accf8079141e3fda23a10", [:mix], [{:argon2_elixir, "~> 1.2", [hex: :argon2_elixir, repo: "hexpm", optional: true]}, {:bcrypt_elixir, "~> 0.12.1 or ~> 1.0", [hex: :bcrypt_elixir, repo: "hexpm", optional: true]}, {:pbkdf2_elixir, "~> 0.12", [hex: :pbkdf2_elixir, repo: "hexpm", optional: true]}], "hexpm"},
"connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], [], "hexpm"}, "connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], [], "hexpm"},
"cors_plug": {:hex, :cors_plug, "1.5.0", "6311ea6ac9fb78b987df52a7654136626a7a0c3b77f83da265f952a24f2fc1b0", [], [{:plug, "~> 1.4", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
"cowboy": {:hex, :cowboy, "1.1.2", "61ac29ea970389a88eca5a65601460162d370a70018afe6f949a29dca91f3bb0", [:rebar3], [{:cowlib, "~> 1.0.2", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.3.2", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm"}, "cowboy": {:hex, :cowboy, "1.1.2", "61ac29ea970389a88eca5a65601460162d370a70018afe6f949a29dca91f3bb0", [:rebar3], [{:cowlib, "~> 1.0.2", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.3.2", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm"},
"cowlib": {:hex, :cowlib, "1.0.2", "9d769a1d062c9c3ac753096f868ca121e2730b9a377de23dec0f7e08b1df84ee", [:make], [], "hexpm"}, "cowlib": {:hex, :cowlib, "1.0.2", "9d769a1d062c9c3ac753096f868ca121e2730b9a377de23dec0f7e08b1df84ee", [:make], [], "hexpm"},
"db_connection": {:hex, :db_connection, "1.1.2", "2865c2a4bae0714e2213a0ce60a1b12d76a6efba0c51fbda59c9ab8d1accc7a8", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, repo: "hexpm", optional: false]}, {:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: true]}, {:sbroker, "~> 1.0", [hex: :sbroker, repo: "hexpm", optional: true]}], "hexpm"}, "db_connection": {:hex, :db_connection, "1.1.2", "2865c2a4bae0714e2213a0ce60a1b12d76a6efba0c51fbda59c9ab8d1accc7a8", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, repo: "hexpm", optional: false]}, {:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: true]}, {:sbroker, "~> 1.0", [hex: :sbroker, repo: "hexpm", optional: true]}], "hexpm"},
@ -31,6 +32,7 @@
"poolboy": {:hex, :poolboy, "1.5.1", "6b46163901cfd0a1b43d692657ed9d7e599853b3b21b95ae5ae0a777cf9b6ca8", [:rebar], [], "hexpm"}, "poolboy": {:hex, :poolboy, "1.5.1", "6b46163901cfd0a1b43d692657ed9d7e599853b3b21b95ae5ae0a777cf9b6ca8", [:rebar], [], "hexpm"},
"postgrex": {:hex, :postgrex, "0.13.3", "c277cfb2a9c5034d445a722494c13359e361d344ef6f25d604c2353185682bfc", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 1.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm"}, "postgrex": {:hex, :postgrex, "0.13.3", "c277cfb2a9c5034d445a722494c13359e361d344ef6f25d604c2353185682bfc", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 1.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm"},
"ranch": {:hex, :ranch, "1.3.2", "e4965a144dc9fbe70e5c077c65e73c57165416a901bd02ea899cfd95aa890986", [:rebar3], [], "hexpm"}, "ranch": {:hex, :ranch, "1.3.2", "e4965a144dc9fbe70e5c077c65e73c57165416a901bd02ea899cfd95aa890986", [:rebar3], [], "hexpm"},
"rsa_ex": {:hex, :rsa_ex, "0.2.1", "5c2c278270ba2bc7beeb268cc9f6e37f976b81011631a5111b86fb8528785a3f", [], [], "hexpm"},
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.1", "28a4d65b7f59893bc2c7de786dec1e1555bd742d336043fe644ae956c3497fbe", [], [], "hexpm"}, "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.1", "28a4d65b7f59893bc2c7de786dec1e1555bd742d336043fe644ae956c3497fbe", [], [], "hexpm"},
"swoosh": {:hex, :swoosh, "0.11.0", "5317c3df2708d14f6ce53aa96b38233aa73ff67c41fac26d8aacc733c116d7a4", [], [{:cowboy, "~> 1.0", [hex: :cowboy, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.12", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: false]}, {:mime, "~> 1.1", [hex: :mime, repo: "hexpm", optional: false]}, {:plug, "~> 1.4", [hex: :plug, repo: "hexpm", optional: true]}, {:poison, "~> 1.5 or ~> 2.0 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"}, "swoosh": {:hex, :swoosh, "0.11.0", "5317c3df2708d14f6ce53aa96b38233aa73ff67c41fac26d8aacc733c116d7a4", [], [{:cowboy, "~> 1.0", [hex: :cowboy, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.12", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: false]}, {:mime, "~> 1.1", [hex: :mime, repo: "hexpm", optional: false]}, {:plug, "~> 1.4", [hex: :plug, repo: "hexpm", optional: true]}, {:poison, "~> 1.5 or ~> 2.0 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"},
"timex": {:hex, :timex, "3.1.24", "d198ae9783ac807721cca0c5535384ebdf99da4976be8cefb9665a9262a1e9e3", [], [{:combine, "~> 0.7", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 0.1.8 or ~> 0.5", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm"}, "timex": {:hex, :timex, "3.1.24", "d198ae9783ac807721cca0c5535384ebdf99da4976be8cefb9665a9262a1e9e3", [], [{:combine, "~> 0.7", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 0.1.8 or ~> 0.5", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm"},

View file

@ -10,7 +10,7 @@ defmodule Eventos.Repo.Migrations.CreateAccounts do
add :private_key, :text add :private_key, :text
add :public_key, :text, null: false add :public_key, :text, null: false
add :suspended, :boolean, default: false, null: false add :suspended, :boolean, default: false, null: false
add :uri, :string, null: false add :uri, :string
add :url, :string add :url, :string
timestamps() timestamps()

View file

@ -5,8 +5,8 @@ defmodule Eventos.Repo.Migrations.CreateUsers do
create table(:users) do create table(:users) do
add :email, :string, null: false add :email, :string, null: false
add :role, :integer, default: 0, null: false add :role, :integer, default: 0, null: false
add :password_hash, :string add :password_hash, :string, null: false
add :account_id, references(:accounts, on_delete: :delete_all, null: false) add :account_id, references(:accounts, on_delete: :delete_all), null: false
timestamps() timestamps()
end end

View file

@ -9,9 +9,29 @@
# #
# We recommend using the bang functions (`insert!`, `update!` # We recommend using the bang functions (`insert!`, `update!`
# and so on) as they will fail if something goes wrong. # and so on) as they will fail if something goes wrong.
import Logger
Eventos.Repo.delete_all Eventos.Accounts.User Eventos.Repo.delete_all Eventos.Accounts.User
Eventos.Accounts.User.registration_changeset(%Eventos.Accounts.User{}, %{email: "testuser@example.com", password: "secret", password_confirmation: "secret"})
|> Eventos.Repo.insert!
{:ok, {privkey, pubkey}} = RsaEx.generate_keypair("4096")
account = Ecto.Changeset.change(%Eventos.Accounts.Account{}, %{
username: "tcit",
description: "myaccount",
display_name: "Thomas Citharel",
domain: nil,
private_key: privkey,
public_key: pubkey,
uri: "",
url: ""
})
user = Eventos.Accounts.User.registration_changeset(%Eventos.Accounts.User{}, %{
email: "tcit@tcit.fr",
password: "tcittcit",
password_confirmation: "tcittcit"
})
account_with_user = Ecto.Changeset.put_assoc(account, :user, user)
Eventos.Repo.insert!(account_with_user)

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,3 +0,0 @@
// for phoenix_html support, including form and button helpers
// copy the following scripts into your javascript bundle:
// * https://raw.githubusercontent.com/phoenixframework/phoenix_html/v2.10.0/priv/static/phoenix_html.js

View file

@ -0,0 +1,2 @@
!function(e){function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}var r=window.webpackJsonp;window.webpackJsonp=function(t,c,u){for(var a,i,f,s=0,l=[];s<t.length;s++)i=t[s],o[i]&&l.push(o[i][0]),o[i]=0;for(a in c)Object.prototype.hasOwnProperty.call(c,a)&&(e[a]=c[a]);for(r&&r(t,c,u);l.length;)l.shift()();if(u)for(s=0;s<u.length;s++)f=n(n.s=u[s]);return f};var t={},o={2:0};n.e=function(e){function r(){a.onerror=a.onload=null,clearTimeout(i);var n=o[e];0!==n&&(n&&n[1](new Error("Loading chunk "+e+" failed.")),o[e]=void 0)}var t=o[e];if(0===t)return new Promise(function(e){e()});if(t)return t[2];var c=new Promise(function(n,r){t=o[e]=[n,r]});t[2]=c;var u=document.getElementsByTagName("head")[0],a=document.createElement("script");a.type="text/javascript",a.charset="utf-8",a.async=!0,a.timeout=12e4,n.nc&&a.setAttribute("nonce",n.nc),a.src=n.p+"js/"+e+"."+{0:"94561603df84d1708ae1",1:"dc4c839388191b886181"}[e]+".js";var i=setTimeout(r,12e4);return a.onerror=a.onload=r,u.appendChild(a),c},n.m=e,n.c=t,n.d=function(e,r,t){n.o(e,r)||Object.defineProperty(e,r,{configurable:!1,enumerable:!0,get:t})},n.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(r,"a",r),r},n.o=function(e,n){return Object.prototype.hasOwnProperty.call(e,n)},n.p="/",n.oe=function(e){throw console.error(e),e}}([]);
//# sourceMappingURL=manifest.79c2975577a8222315fd.js.map

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

BIN
priv/static/oh_no.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View file

@ -1,5 +0,0 @@
# See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
#
# To ban all spiders from the entire site uncomment the next two lines:
# User-agent: *
# Disallow: /