parent
8ac705d8c2
commit
92d2045735
|
@ -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"]
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -0,0 +1,2 @@
|
|||
build/*.js
|
||||
config/*.js
|
|
@ -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],
|
||||
},
|
||||
}
|
|
@ -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
|
|
@ -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": {}
|
||||
}
|
||||
}
|
|
@ -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).
|
|
@ -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'
|
||||
))
|
||||
})
|
||||
})
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
})
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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'
|
||||
}
|
||||
}
|
|
@ -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]')
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
]
|
||||
})
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1,6 @@
|
|||
var merge = require('webpack-merge')
|
||||
var prodEnv = require('./prod.env')
|
||||
|
||||
module.exports = merge(prodEnv, {
|
||||
NODE_ENV: '"development"'
|
||||
})
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
module.exports = {
|
||||
NODE_ENV: '"production"'
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
var merge = require('webpack-merge')
|
||||
var devEnv = require('./dev.env')
|
||||
|
||||
module.exports = merge(devEnv, {
|
||||
NODE_ENV: '"testing"'
|
||||
})
|
|
@ -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>
|
File diff suppressed because it is too large
Load Diff
|
@ -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"
|
||||
]
|
||||
}
|
|
@ -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>
|
|
@ -0,0 +1,5 @@
|
|||
export default {
|
||||
login(state, user) {
|
||||
state.user = user.user;
|
||||
},
|
||||
};
|
|
@ -0,0 +1,2 @@
|
|||
export const API_HOST = 'http://0.0.0.0:4000';
|
||||
export const API_PATH = '/api';
|
|
@ -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;
|
||||
});
|
||||
});
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 6.7 KiB |
|
@ -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')}`,
|
||||
};
|
||||
},
|
||||
};
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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",
|
||||
},
|
||||
},
|
||||
};
|
|
@ -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",
|
||||
},
|
||||
},
|
||||
};
|
|
@ -0,0 +1,6 @@
|
|||
import en from './en';
|
||||
import fr from './fr';
|
||||
|
||||
export default {
|
||||
en, fr,
|
||||
};
|
|
@ -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 },
|
||||
});
|
|
@ -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;
|
|
@ -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 };
|
|
@ -0,0 +1,3 @@
|
|||
export const LOGIN_USER = 'LOGIN_USER';
|
||||
export const LOGOUT_USER = 'LOGOUT_USER';
|
||||
export const SAVE_USER = 'SAVE_USER';
|
Binary file not shown.
After Width: | Height: | Size: 22 KiB |
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
});
|
||||
});
|
|
@ -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();
|
||||
},
|
||||
};
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"env": {
|
||||
"mocha": true
|
||||
},
|
||||
"globals": {
|
||||
"expect": true,
|
||||
"sinon": true
|
||||
}
|
||||
}
|
|
@ -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);
|
|
@ -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' },
|
||||
]
|
||||
},
|
||||
});
|
||||
};
|
|
@ -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');
|
||||
});
|
||||
});
|
|
@ -4,21 +4,20 @@ defmodule Eventos.Accounts.Account do
|
|||
alias Eventos.Accounts.{Account, GroupAccount, GroupRequest, Group, User}
|
||||
alias Eventos.Events.Event
|
||||
|
||||
|
||||
schema "accounts" do
|
||||
field :username, :string
|
||||
field :description, :string
|
||||
field :display_name, :string
|
||||
field :domain, :string
|
||||
field :domain, :string, default: nil
|
||||
field :private_key, :string
|
||||
field :public_key, :string
|
||||
field :suspended, :boolean, default: false
|
||||
field :uri, :string
|
||||
field :url, :string
|
||||
field :username, :string
|
||||
has_many :organized_events, Event
|
||||
many_to_many :groups, Group, join_through: GroupAccount
|
||||
has_many :group_request, GroupRequest
|
||||
has_one :user_id, User
|
||||
has_one :user, User
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
@ -27,6 +26,7 @@ defmodule Eventos.Accounts.Account do
|
|||
def changeset(%Account{} = account, attrs) do
|
||||
account
|
||||
|> 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
|
||||
|
|
|
@ -1,16 +1,17 @@
|
|||
defmodule Eventos.Accounts.User do
|
||||
use Ecto.Schema
|
||||
import Ecto.Changeset
|
||||
alias Eventos.Accounts.{User}
|
||||
alias Eventos.Accounts.{Account, User}
|
||||
alias Eventos.Repo
|
||||
|
||||
import Logger
|
||||
|
||||
schema "users" do
|
||||
field :email, :string
|
||||
field :role, :integer, default: 0
|
||||
field :password, :string, virtual: true
|
||||
field :password_hash, :string
|
||||
field :account_id, :integer
|
||||
|
||||
belongs_to :account, Account
|
||||
timestamps()
|
||||
end
|
||||
|
||||
|
@ -18,7 +19,7 @@ defmodule Eventos.Accounts.User do
|
|||
@doc false
|
||||
def changeset(%User{} = user, attrs) do
|
||||
user
|
||||
|> cast(attrs, [:email, :password_hash, :role])
|
||||
|> cast(attrs, [:email, :password_hash])
|
||||
|> validate_required([:email])
|
||||
|> unique_constraint(:email)
|
||||
|> validate_format(:email, ~r/@/)
|
||||
|
|
|
@ -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
|
|
@ -1,11 +1,8 @@
|
|||
defmodule EventosWeb.PageController do
|
||||
use EventosWeb, :controller
|
||||
import Logger
|
||||
|
||||
def index(conn, _params) do
|
||||
render conn, "index.html"
|
||||
end
|
||||
|
||||
def app(conn, _params) do
|
||||
render conn, "index.html"
|
||||
end
|
||||
end
|
||||
|
|
|
@ -8,9 +8,12 @@ defmodule EventosWeb.SessionController do
|
|||
# Attempt to authenticate the user
|
||||
with {:ok, token, _claims} <- Accounts.authenticate(%{user: user, password: password}) do
|
||||
# Render the token
|
||||
render conn, "token.json", token: token
|
||||
user = Eventos.Repo.preload user, :account
|
||||
render conn, "token.json", %{token: token, user: user}
|
||||
end
|
||||
send_resp(conn, 400, "Bad login")
|
||||
end
|
||||
send_resp(conn, 400, "No such user")
|
||||
end
|
||||
|
||||
def sign_out(conn, _params) do
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
defmodule EventosWeb.UserController do
|
||||
use EventosWeb, :controller
|
||||
import Logger
|
||||
|
||||
alias Eventos.Accounts
|
||||
alias Eventos.Accounts.User
|
||||
|
@ -57,4 +58,34 @@ defmodule EventosWeb.UserController do
|
|||
|> put_flash(:info, "User deleted successfully.")
|
||||
|> redirect(to: user_path(conn, :index))
|
||||
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
|
||||
|
|
|
@ -9,7 +9,7 @@ defmodule EventosWeb.Endpoint do
|
|||
# when deploying your static files in production.
|
||||
plug Plug.Static,
|
||||
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_reloader configuration of your endpoint.
|
||||
|
@ -19,6 +19,7 @@ defmodule EventosWeb.Endpoint do
|
|||
plug Phoenix.CodeReloader
|
||||
end
|
||||
|
||||
plug CORSPlug
|
||||
plug Plug.RequestId
|
||||
plug Plug.Logger
|
||||
|
||||
|
|
|
@ -9,18 +9,26 @@ defmodule EventosWeb.Router do
|
|||
plug EventosWeb.AuthPipeline
|
||||
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
|
||||
|
||||
resources "/users", UserController, only: [:create]
|
||||
post "/sign-in", EventosWeb.SessionController, :sign_in
|
||||
post "/users", UserController, :register
|
||||
post "/login", SessionController, :sign_in
|
||||
resources "/groups", GroupController, only: [:index]
|
||||
end
|
||||
|
||||
# Other scopes may use custom stacks.
|
||||
scope "/api", EventosWeb do
|
||||
pipe_through :api_auth
|
||||
|
||||
|
||||
post "/sign-out", SessionController, :sign_out
|
||||
resources "/users", UserController
|
||||
resources "/accounts", AccountController
|
||||
|
@ -29,8 +37,14 @@ defmodule EventosWeb.Router do
|
|||
resources "/tags", TagController
|
||||
resources "/event_accounts", EventAccountsController
|
||||
resources "/event_requests", EventRequestController
|
||||
resources "/groups", GroupController
|
||||
resources "/groups", GroupController, except: [:index]
|
||||
resources "/group_accounts", GroupAccountController
|
||||
resources "/group_requests", GroupRequestController
|
||||
end
|
||||
|
||||
scope "/", EventosWeb do
|
||||
pipe_through :browser
|
||||
|
||||
get "/*path", AppController, :app
|
||||
end
|
||||
end
|
||||
|
|
|
@ -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>
|
|
@ -1,3 +1,15 @@
|
|||
defmodule EventosWeb.AccountView do
|
||||
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
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
defmodule EventosWeb.AppView do
|
||||
use EventosWeb, :view
|
||||
end
|
|
@ -1,7 +1,7 @@
|
|||
defmodule EventosWeb.SessionView do
|
||||
use EventosWeb, :view
|
||||
|
||||
def render("token.json", %{token: token}) do
|
||||
%{token: token}
|
||||
def render("token.json", %{token: token, user: user}) do
|
||||
%{token: token, user: render_one(user, EventosWeb.UserView, "user.json")}
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,3 +1,11 @@
|
|||
defmodule EventosWeb.UserView do
|
||||
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
|
||||
|
|
4
mix.exs
4
mix.exs
|
@ -43,7 +43,9 @@ defmodule Eventos.Mixfile do
|
|||
{:cowboy, "~> 1.0"},
|
||||
{:guardian, "~> 1.0"},
|
||||
{:comeonin, "~> 4.0"},
|
||||
{:argon2_elixir, "~> 1.2"}
|
||||
{:argon2_elixir, "~> 1.2"},
|
||||
{:cors_plug, "~> 1.2"},
|
||||
{:rsa_ex, "~> 0.1"}
|
||||
]
|
||||
end
|
||||
|
||||
|
|
2
mix.lock
2
mix.lock
|
@ -5,6 +5,7 @@
|
|||
"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"},
|
||||
"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"},
|
||||
"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"},
|
||||
|
@ -31,6 +32,7 @@
|
|||
"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"},
|
||||
"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"},
|
||||
"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"},
|
||||
|
|
|
@ -10,7 +10,7 @@ defmodule Eventos.Repo.Migrations.CreateAccounts do
|
|||
add :private_key, :text
|
||||
add :public_key, :text, null: false
|
||||
add :suspended, :boolean, default: false, null: false
|
||||
add :uri, :string, null: false
|
||||
add :uri, :string
|
||||
add :url, :string
|
||||
|
||||
timestamps()
|
||||
|
|
|
@ -5,8 +5,8 @@ defmodule Eventos.Repo.Migrations.CreateUsers do
|
|||
create table(:users) do
|
||||
add :email, :string, null: false
|
||||
add :role, :integer, default: 0, null: false
|
||||
add :password_hash, :string
|
||||
add :account_id, references(:accounts, on_delete: :delete_all, null: false)
|
||||
add :password_hash, :string, null: false
|
||||
add :account_id, references(:accounts, on_delete: :delete_all), null: false
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
|
|
@ -9,9 +9,29 @@
|
|||
#
|
||||
# We recommend using the bang functions (`insert!`, `update!`
|
||||
# and so on) as they will fail if something goes wrong.
|
||||
import Logger
|
||||
|
||||
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
|
@ -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
|
|
@ -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
Binary file not shown.
After Width: | Height: | Size: 22 KiB |
|
@ -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: /
|
Loading…
Reference in New Issue