Some work

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
Thomas Citharel
2018-07-04 14:29:17 +02:00
parent 394057d45e
commit 93a97b0865
56 changed files with 5577 additions and 4327 deletions

7755
js/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -11,28 +11,29 @@
},
"dependencies": {
"material-design-icons": "^3.0.1",
"moment": "^2.22.1",
"moment": "^2.22.2",
"ngeohash": "^0.6.0",
"register-service-worker": "^1.0.0",
"register-service-worker": "^1.4.1",
"vue": "^2.5.16",
"vue-gravatar": "^1.2.1",
"vue-markdown": "^2.2.4",
"vue-router": "^3.0.1",
"vuetify": "^1.0.18",
"vuetify-google-autocomplete": "^2.0.0-Alpha.9",
"vuetify": "^1.1.1",
"vuetify-google-autocomplete": "^2.0.0-beta.4",
"vuex": "^3.0.1",
"vuex-i18n": "^1.10.5"
},
"devDependencies": {
"dotenv-webpack": "^1.5.5",
"@vue/cli-plugin-babel": "^3.0.0-beta.10",
"@vue/cli-plugin-e2e-nightwatch": "^3.0.0-beta.10",
"@vue/cli-plugin-eslint": "^3.0.0-beta.10",
"@vue/cli-plugin-pwa": "^3.0.0-beta.10",
"@vue/cli-plugin-unit-mocha": "^3.0.0-beta.10",
"@vue/cli-service": "^3.0.0-beta.10",
"@vue/eslint-config-airbnb": "^3.0.0-beta.10",
"@vue/test-utils": "^1.0.0-beta.10",
"@vue/cli-plugin-babel": "^3.0.0-rc.3",
"@vue/cli-plugin-e2e-nightwatch": "^3.0.0-rc.3",
"@vue/cli-plugin-eslint": "^3.0.0-rc.3",
"@vue/cli-plugin-pwa": "^3.0.0-rc.3",
"@vue/cli-plugin-unit-mocha": "^3.0.0-rc.3",
"@vue/cli-service": "^3.0.0-rc.3",
"@vue/eslint-config-airbnb": "^3.0.0-rc.3",
"@vue/test-utils": "^1.0.0-beta.20",
"chai": "^4.1.2",
"dotenv-webpack": "^1.5.7",
"node-sass": "^4.7.2",
"sass-loader": "^6.0.6",
"vue-template-compiler": "^2.5.13"

View File

@@ -38,28 +38,54 @@
</template>
</v-list>
</v-navigation-drawer>
<NavBar></NavBar>
<NavBar v-bind="{toggleDrawer}"></NavBar>
<v-content>
<v-container fluid fill-height>
<v-layout xs-12>
<transition>
<transition name="router">
<router-view></router-view>
</transition>
</v-layout>
</v-container>
</v-content>
<v-btn
fixed
dark
fab
<v-speed-dial
v-model="fab"
bottom
fixed
right
color="pink"
@click="$router.push({name: 'CreateEvent'})"
direction="top"
transition="scale-transition"
v-if="getUser()"
>
<v-icon>add</v-icon>
</v-btn>
<v-btn
slot="activator"
v-model="fab"
color="blue darken-2"
dark
fab
>
<v-icon>add</v-icon>
<v-icon>close</v-icon>
</v-btn>
<v-btn
fab
dark
small
color="pink"
@click="$router.push({name: 'CreateEvent'})"
>
<v-icon>event</v-icon>
</v-btn>
<v-btn
fab
dark
small
color="purple"
@click="$router.push({name: 'CreateGroup'})"
>
<v-icon>group</v-icon>
</v-btn>
</v-speed-dial>
<v-footer class="indigo" app>
<span class="white--text">© Thomas Citharel {{ new Date().getFullYear() }} - Made with Elixir, Phoenix & <a href="https://vuejs.org/">VueJS</a> & <a href="https://www.vuetifyjs.com/">Vuetify</a> with some love and some weeks</span>
</v-footer>
@@ -85,7 +111,8 @@ export default {
},
data() {
return {
drawer: true,
drawer: false,
fab: false,
user: false,
items: [
{ icon: 'poll', text: 'Events', route: 'EventList', role: null },
@@ -110,10 +137,25 @@ export default {
},
getUser() {
return this.$store.state.user === undefined ? false : this.$store.state.user;
},
toggleDrawer() {
this.drawer = !this.drawer;
}
},
};
</script>
<style>
.router-enter-active, .router-leave-active {
transition-property: opacity;
transition-duration: .25s;
}
.router-enter-active {
transition-delay: .25s;
}
.router-enter, .router-leave-active {
opacity: 0
}
</style>

65
js/src/assets/profile.svg Normal file
View File

@@ -0,0 +1,65 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
data-name="Layer 1"
viewBox="0 0 100 125"
x="0px"
y="0px"
id="svg2"
version="1.1"
inkscape:version="0.91 r13725"
sodipodi:docname="profile.svg">
<metadata
id="metadata24">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs22" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="640"
inkscape:window-height="480"
id="namedview20"
showgrid="false"
inkscape:zoom="1.888"
inkscape:cx="50"
inkscape:cy="62.5"
inkscape:window-x="0"
inkscape:window-y="36"
inkscape:window-maximized="0"
inkscape:current-layer="svg2" />
<title
id="title4">47 all</title>
<g
id="g6">&quot;&gt;<g
id="g8">&quot;&gt;<path
d="M77.74,83.19H22.26V76.47a24,24,0,0,1,24-24h7.48a24,24,0,0,1,24,24Zm-51.48-4H73.74V76.47a20,20,0,0,0-20-20H46.26a20,20,0,0,0-20,20Z"
id="path10" />
</g>
<g
id="g12">&quot;&gt;<path
d="M50,50.5A16.85,16.85,0,1,1,66.85,33.66,16.87,16.87,0,0,1,50,50.5Zm0-29.7A12.85,12.85,0,1,0,62.85,33.66,12.86,12.86,0,0,0,50,20.81Z"
id="path14" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -61,10 +61,11 @@ export default {
},
// To log out, we just need to remove the token
logout() {
logout(store) {
localStorage.removeItem('refresh_token');
localStorage.removeItem('token');
this.authenticated = false;
store.commit('LOGOUT_USER');
},
jwt_decode(token) {

View File

@@ -14,20 +14,30 @@
<v-btn icon class="mr-3" v-if="$store.state.user && $store.state.user.actor.id === actor.id">
<v-icon>edit</v-icon>
</v-btn>
<v-btn icon>
<v-icon>more_vert</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="logoutUser()" v-if="$store.state.user && $store.state.user.actor.id === actor.id">
<v-list-tile-title>User logout</v-list-tile-title>
</v-list-tile>
<v-list-tile @click="deleteAccount()" v-if="$store.state.user && $store.state.user.actor.id === actor.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-avatar size="125px">
<img v-if="!account.avatar_url"
<img v-if="!actor.avatar_url"
class="img-circle elevation-7 mb-1"
src="https://picsum.photos/125/125/"
>
<img v-else
class="img-circle elevation-7 mb-1"
:src="account.avatar_url"
:src="actor.avatar_url"
>
</v-avatar>
</div>
@@ -166,6 +176,7 @@
<script>
import eventFetch from '@/api/eventFetch';
import auth from '@/auth';
export default {
name: 'Account',
@@ -197,6 +208,10 @@ export default {
this.loading = false;
console.log(this.actor);
})
},
logoutUser() {
auth.logout(this.$store);
this.$router.push({ name: 'Home' });
}
}
}

View File

@@ -0,0 +1,141 @@
<template>
<v-container fluid fill-height>
<v-layout align-center justify-center>
<v-flex xs12 sm8 md4>
<v-card class="elevation-12">
<v-toolbar dark color="primary">
<v-toolbar-title>Login</v-toolbar-title>
<v-spacer></v-spacer>
<v-tooltip bottom>
<v-btn
slot="activator"
:to="{ name: 'Register', params: { email: this.credentials.email, password: this.credentials.password } }"
>
<!-- <v-icon large>login</v-icon> -->
<span>Register</span>
</v-btn>
<span>Register</span>
</v-tooltip>
</v-toolbar>
<v-card-text>
<div class="text-xs-center">
<v-avatar size="80px">
<transition name="avatar">
<component :is="validEmail()" v-bind="{email: credentials.email}"></component>
<!-- <v-gravatar :email="credentials.email" default-img="mp" v-if="validEmail()"/>
<avatar v-else></avatar> -->
</transition>
</v-avatar>
</div>
<v-form @submit="loginAction" v-if="!validationSent">
<v-text-field
label="Email"
required
type="text"
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="loginAction" color="blue">Login</v-btn>
<router-link :to="{ name: 'SendPasswordReset', params: { email: credentials.email } }">Password forgotten ?</router-link>
</v-form>
<div v-else>
<h2>{{ $t('registration.form.validation_sent', { email: credentials.email }) }}</h2>
<b-alert show variant="info">{{ $t('registration.form.validation_sent_info') }}</b-alert>
</div>
</v-card-text>
</v-card>
</v-flex>
</v-layout>
</v-container>
</template>
<script>
import auth from '@/auth/index';
import Gravatar from 'vue-gravatar';
import RegisterAvatar from './RegisterAvatar';
export default {
props: {
email: {
type: String,
required: false,
default: '',
},
password: {
type: String,
required: false,
default: '',
},
},
beforeCreate() {
if (this.$store.state.user) {
this.$router.push('/');
}
},
components: {
'v-gravatar': Gravatar,
'avatar': RegisterAvatar
},
mounted() {
this.credentials.email = this.email;
this.credentials.password = this.password;
},
data() {
return {
credentials: {
email: '',
password: '',
},
validationSent: false,
error: {
show: false,
text: '',
timeout: 3000,
field: {
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: {
loginAction(e) {
e.preventDefault();
auth.login(JSON.stringify(this.credentials), (data) => {
this.$store.commit('LOGIN_USER', data.user);
this.$router.push({ name: 'Home' });
}, (error) => {
Promise.resolve(error).then((errorMsg) => {
console.log(errorMsg);
this.error.show = true;
this.error.text = this.$t(errorMsg.display_error);
}).catch((e) => {
console.log(e);
this.error.show = true;
this.error.text = e.message;
});
});
},
validEmail() {
return this.rules.email(this.credentials.email) === true ? 'v-gravatar' : 'avatar';
},
},
};
</script>

View File

@@ -0,0 +1,128 @@
<template>
<v-container fluid fill-height>
<v-layout align-center justify-center>
<v-flex xs12 sm8 md4>
<v-card class="elevation-12">
<v-toolbar dark color="primary">
<v-toolbar-title>Password Reset</v-toolbar-title>
</v-toolbar>
<v-card-text>
<v-alert type="error" :value="state.token.status === false">{{ state.token.msg }}</v-alert>
<v-form @submit="resetAction">
<v-text-field
label="Password"
type="password"
v-model="credentials.password"
required
:error="state.password.status"
:rules="[rules.required, rules.password_length]"
>
</v-text-field>
<v-text-field
label="Password (confirmation)"
type="password"
v-model="credentials.password_confirmation"
required
:rules="[rules.required, rules.password_length, rules.password_equal]"
:error="state.password_confirmation.status"
>
</v-text-field>
<v-btn type="submit" :disabled="!samePasswords" color="blue">Reset my password</v-btn>
</v-form>
</v-card-text>
</v-card>
</v-flex>
</v-layout>
</v-container>
</template>
<script>
import fetchStory from '@/api/eventFetch';
export default {
name: 'PasswordReset',
props: {
token: {
type: String,
required: true,
},
},
computed: {
samePasswords() {
return this.rules.password_length(this.credentials.password) === true &&
this.credentials.password === this.credentials.password_confirmation;
},
},
data() {
return {
credentials: {
password: '',
password_confirmation: '',
},
error: {
show: false,
},
state: {
token: {
status: null,
msg: '',
},
password: {
status: null,
msg: '',
},
password_confirmation: {
status: null,
msg: '',
},
},
rules: {
password_length: value => value.length > 6 || 'Password must be at least 6 caracters long',
required: value => !!value || 'Required.',
password_equal: value => value === this.credentials.password || 'Passwords must be the same',
}
};
},
methods: {
resetAction(e) {
this.resetState();
e.preventDefault();
console.log(this.token);
fetchStory('/users/password-reset/post', this.$store, { method: 'POST', body: JSON.stringify({ password: this.credentials.password, token: this.token }) }).then((data) => {
localStorage.setItem('token', data.token);
localStorage.setItem('refresh_token', data.refresh_token);
this.$store.commit('LOGIN_USER', data.account);
this.$snotify.success(this.$t('registration.success.login', { username: data.account.username }));
this.$router.push({ name: 'Home' });
}, (error) => {
Promise.resolve(error).then((errormsg) => {
console.log('errormsg', errormsg);
this.error.show = true;
Object.entries(JSON.parse(errormsg).errors).forEach(([key, val]) => {
console.log('key', key);
console.log('val', val[0]);
this.state[key] = { status: false, msg: val[0] };
console.log('state', this.state);
});
});
});
},
resetState() {
this.state = {
token: {
status: null,
msg: '',
},
password_confirmation: {
status: null,
msg: '',
},
password: {
status: null,
msg: '',
},
};
},
},
};
</script>

View File

@@ -0,0 +1,198 @@
<template>
<v-container fluid fill-height>
<v-layout align-center justify-center>
<v-flex xs12 sm8 md4>
<v-card class="elevation-12">
<v-toolbar dark color="primary">
<v-toolbar-title>Register</v-toolbar-title>
<v-spacer></v-spacer>
<v-tooltip bottom>
<v-btn
slot="activator"
:to="{ name: 'Login', params: { email: this.credentials.email, password: this.credentials.password } }"
>
<!-- <v-icon large>login</v-icon> -->
<span>Login</span>
</v-btn>
<span>Login</span>
</v-tooltip>
</v-toolbar>
<v-card-text>
<div class="text-xs-center">
<v-avatar size="80px">
<transition name="avatar">
<component :is="validEmail()" v-bind="{email: credentials.email}"></component>
<!-- <v-gravatar :email="credentials.email" default-img="mp" v-if="validEmail()"/>
<avatar v-else></avatar> -->
</transition>
</v-avatar>
</div>
<v-form @submit="registerAction" v-if="!validationSent">
<v-text-field
label="Username"
required
type="text"
v-model="credentials.username"
:rules="[rules.required]"
:error="this.state.username.status"
:error-messages="this.state.username.msg"
:suffix="this.host()"
hint="You will be able to create more identities once registered"
persistent-hint
>
</v-text-field>
<v-text-field
label="Email"
required
type="email"
ref="email"
v-model="credentials.email"
:rules="[rules.required, rules.email]"
:error="this.state.email.status"
:error-messages="this.state.email.msg"
>
</v-text-field>
<v-text-field
label="Password"
required
:type="showPassword ? 'text' : 'password'"
v-model="credentials.password"
:rules="[rules.required, rules.password_length]"
:error="this.state.password.status"
:error-messages="this.state.password.msg"
:append-icon="showPassword ? 'visibility_off' : 'visibility'"
@click:append="showPassword = !showPassword"
>
</v-text-field>
<v-btn @click="registerAction" color="primary">Register</v-btn>
<router-link :to="{ name: 'ResendConfirmation', params: { email: credentials.email }}">Didn't receive the instructions ?</router-link>
</v-form>
<div v-else>
<h2>{{ $t('registration.form.validation_sent', { email: credentials.email }) }}</h2>
<b-alert show variant="info">{{ $t('registration.form.validation_sent_info') }}</b-alert>
</div>
</v-card-text>
</v-card>
</v-flex>
</v-layout>
</v-container>
</template>
<script>
import auth from '@/auth/index';
import Gravatar from 'vue-gravatar';
import RegisterAvatar from './RegisterAvatar';
export default {
props: {
email: {
type: String,
required: false,
default: '',
},
password: {
type: String,
required: false,
default: '',
},
},
components: {
'v-gravatar': Gravatar,
'avatar': RegisterAvatar
},
mounted() {
this.credentials.email = this.email;
this.credentials.password = this.password;
},
data() {
return {
credentials: {
username: '',
email: '',
password: '',
},
error: {
show: false,
},
showPassword: false,
validationSent: false,
state: {
email: {
status: false,
msg: [],
},
username: {
status: false,
msg: [],
},
password: {
status: false,
msg: [],
},
},
rules: {
password_length: value => value.length > 6 || 'Password must be at least 6 caracters long',
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) {
this.resetState();
e.preventDefault();
auth.signup(JSON.stringify(this.credentials), (data) => {
console.log(data);
this.validationSent = true;
}, (error) => {
Promise.resolve(error).then((errormsg) => {
console.log(errormsg);
this.error.show = true;
Object.entries(errormsg.errors.user).forEach(([key, val]) => {
console.log(key);
console.log(val);
this.state[key] = { status: true, msg: val };
});
});
});
},
resetState() {
this.state = {
email: {
status: false,
msg: '',
},
username: {
status: false,
msg: '',
},
password: {
status: false,
msg: '',
},
};
},
host() {
return `@${window.location.host}`;
},
validEmail() {
return this.rules.email(this.credentials.email) === true ? 'v-gravatar' : 'avatar';
}
},
};
</script>
<style lang="scss">
.avatar-enter-active {
transition: opacity 1s ease;
}
.avatar-enter, .avatar-leave-to {
opacity: 0;
}
.avatar-leave {
display: none;
}
</style>

View File

@@ -0,0 +1,9 @@
<template>
<img class="img-circle elevation-7 mb-1" src="@/assets/profile.svg">
</template>
<script>
export default {
name: 'RegisterAvatar'
}
</script>

View File

@@ -0,0 +1,84 @@
<template>
<v-container fluid fill-height>
<v-layout align-center justify-center>
<v-flex xs12 sm8 md4>
<v-card class="elevation-12">
<v-toolbar dark color="primary">
<v-toolbar-title>Resend Instructions</v-toolbar-title>
</v-toolbar>
<v-card-text>
<v-form @submit="resendConfirmationAction" v-if="!validationSent">
<v-text-field
label="Email"
type="email"
v-model="credentials.email"
required
:state="state.email.status"
:rules="[rules.required, rules.email]"
>
</v-text-field>
<v-btn type="submit" color="blue">Send instructions again</v-btn>
</v-form>
<div v-else>
<h2>Validation email sent to {{ credentials.email }}</h2>
<v-alert :value="true" type="info">Please check you spam folder if you didn't receive the email.</v-alert>
</div>
</v-card-text>
</v-card>
</v-flex>
</v-layout>
</v-container>
</template>
<script>
import fetchStory from '@/api/eventFetch';
export default {
name: 'ResendConfirmation',
props: {
email: {
type: String,
required: false,
default: '',
},
},
data() {
return {
credentials: {
email: '',
},
validationSent: false,
error: false,
state: {
email: {
status: null,
msg: '',
},
},
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.';
},
},
};
},
mounted() {
this.credentials.email = this.email;
},
methods: {
resendConfirmationAction(e) {
e.preventDefault();
fetchStory('/users/resend', this.$store, { method: 'POST', body: JSON.stringify(this.credentials) }).then(() => {
this.validationSent = true;
}).catch((err) => {
Promise.resolve(err).then(() => {
this.error = true;
this.validationSent = true;
});
});
},
},
};
</script>

View File

@@ -0,0 +1,93 @@
<template>
<v-container fluid fill-height>
<v-layout align-center justify-center>
<v-flex xs12 sm8 md4>
<v-card class="elevation-12">
<v-toolbar dark color="primary">
<v-toolbar-title>Password Reset</v-toolbar-title>
</v-toolbar>
<v-card-text>
<v-form @submit="resendConfirmationAction" v-if="!validationSent">
<v-text-field
label="Email"
type="email"
v-model="credentials.email"
required
:state="state.email.status"
:rules="[rules.required, rules.email]"
>
</v-text-field>
<v-btn type="submit" color="blue">Reset my password</v-btn>
</v-form>
<div v-else>
<h2>Validation email sent to {{ credentials.email }}</h2>
<v-alert :value="true" type="info">Please check you spam folder if you didn't receive the email.</v-alert>
</div>
</v-card-text>
</v-card>
</v-flex>
</v-layout>
</v-container>
</template>
<script>
import fetchStory from '@/api/eventFetch';
export default {
name: 'SendPasswordReset',
props: {
email: {
type: String,
required: false,
default: '',
},
},
mounted() {
this.credentials.email = this.email;
},
data() {
return {
credentials: {
email: '',
},
validationSent: false,
error: false,
state: {
email: {
status: null,
msg: '',
},
},
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: {
resendConfirmationAction(e) {
e.preventDefault();
fetchStory('/users/password-reset/send', this.$store, { method: 'POST', body: JSON.stringify(this.credentials) }).then(() => {
this.error = false;
this.validationSent = true;
}).catch((err) => {
Promise.resolve(err).then((data) => {
this.error = true;
this.state.email = { status: false, msg: data.errors };
});
});
},
resetState() {
this.state = {
email: {
status: null,
msg: '',
},
};
},
},
};
</script>

View File

@@ -0,0 +1,51 @@
<template>
<b-container>
<h1 v-if="loading">{{ $t('registration.validation.process') }}</h1>
<div v-else>
<div v-if="failed">
<b-alert show variant="danger">{{ $t('registration.success.validation_failure') }}</b-alert>
</div>
<h1 v-else>{{ $t('registration.validation.finished') }}</h1>
</div>
</b-container>
</template>
<script>
import fetchStory from '@/api/eventFetch';
export default {
name: 'Validate',
data() {
return {
loading: true,
failed: false,
};
},
props: {
token: {
type: String,
required: true,
},
},
created() {
this.validateAction();
},
methods: {
validateAction() {
fetchStory(`/users/validate/${this.token}`, this.$store).then((data) => {
this.loading = false;
localStorage.setItem('token', data.token);
localStorage.setItem('refresh_token', data.refresh_token);
this.$store.commit('LOGIN_USER', data.account);
this.$snotify.success(this.$t('registration.success.login', { username: data.account.username }));
this.$router.push({ name: 'Home' });
}).catch((err) => {
Promise.resolve(err).then(() => {
this.failed = true;
this.loading = false;
});
});
},
},
};
</script>

View File

@@ -1,204 +1,65 @@
<template>
<v-container fluid grid-list-sm>
<h3>Create a new event</h3>
<v-form>
<v-stepper v-model="e1">
<v-stepper-header>
<v-stepper-step step="1" :complete="e1 > 1" editable>Basic Informations
<small>Title and description</small>
</v-stepper-step>
<v-divider></v-divider>
<v-stepper-step step="2" :complete="e1 > 2" editable>Date and place</v-stepper-step>
<v-divider></v-divider>
<v-stepper-step step="3" :complete="e1 > 3">Extra informations</v-stepper-step>
</v-stepper-header>
<v-stepper-items>
<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-container fluid fill-height>
<v-layout align-center justify-center>
<v-flex xs12 sm8 md4>
<v-card class="elevation-12">
<v-toolbar dark color="primary">
<v-toolbar-title>Create a new event</v-toolbar-title>
</v-toolbar>
<v-card-text>
<v-form>
<v-text-field
label="Description"
v-model="event.description"
multiLine
label="Title"
v-model="event.title"
:counter="100"
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="title"
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-content step="2">
Event starts at:
<v-text-field type="datetime-local" v-model="event.begins_on"></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.ends_on"></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
types="geocode"
v-on:placechanged="getAddressData"
>
</vuetify-google-autocomplete>
<v-btn color="primary" @click.native="e1 = 3">Next</v-btn>
</v-stepper-content>
<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-items>
</v-stepper>
</v-form>
<v-btn color="primary" @click="create">Create event</v-btn>
<v-radio-group v-model="event.location_type" row>
<v-radio label="Address" value="physical" off-icon="place"></v-radio>
<v-radio label="Online" value="online" off-icon="link"></v-radio>
<v-radio label="Phone" value="phone" off-icon="phone"></v-radio>
<v-radio label="Other" value="other"></v-radio>
</v-radio-group>
<vuetify-google-autocomplete
v-if="event.location_type === 'physical'"
id="map"
append-icon="search"
classname="form-control"
placeholder="Start typing"
label="Location"
enable-geolocation
types="geocode"
v-on:placechanged="getAddressData"
>
</vuetify-google-autocomplete>
<v-text-field
v-if="event.location_type === 'online'"
label="Meeting adress"
type="url"
v-model="event.url"
:required="event.location_type === 'online'"
></v-text-field>
<v-text-field
v-if="event.location_type === 'phone'"
label="Phone number"
type="tel"
v-model="event.phone"
:required="event.location_type === 'phone'"
></v-text-field>
<v-autocomplete
:items="categories"
v-model="event.category"
item-text="title"
item-value="id"
label="Categories"
>
</v-autocomplete>
<v-btn color="primary" @click="create">Create event</v-btn>
</v-form>
</v-card-text>
</v-card>
</v-flex>
</v-layout>
</v-container>
</template>
@@ -219,29 +80,18 @@
return {
e1: 0,
event: {
title: '',
description: '',
title: null,
description: null,
begins_on: new Date(),
ends_on: new Date(),
seats: 0,
address: {
description: null,
floor: null,
geo: {
type: null,
data: {
latitude: null,
longitude: null,
},
},
addressCountry: null,
addressLocality: null,
addressRegion: null,
postalCode: null,
streetAddress: null,
},
price: 0,
seats: null,
physical_address: null,
location_type: 'physical',
online_address: null,
tel_num: null,
price: null,
category: null,
category_id: null,
tags: [],
participants: [],
},
@@ -262,31 +112,35 @@
},
methods: {
create() {
this.event.seats = parseInt(this.event.seats, 10);
this.tagsToSend.forEach((tag) => {
this.event.tags.push({
title: tag,
// '@type': 'Tag',
});
});
this.event.category_id = this.event.category.id;
// this.event.seats = parseInt(this.event.seats, 10);
// this.tagsToSend.forEach((tag) => {
// this.event.tags.push({
// title: tag,
// // '@type': 'Tag',
// });
// });
this.event.category_id = this.event.category;
this.event.organizer_actor_id = this.$store.state.user.actor.id;
this.event.participants = [this.$store.state.user.actor.id];
this.event.price = parseFloat(this.event.price);
// this.event.price = parseFloat(this.event.price);
if (this.id === undefined) {
eventFetch('/events', this.$store, {method: 'POST', body: JSON.stringify({ event: this.event })})
.then(response => response.json())
.then((data) => {
this.loading = false;
this.$router.push({name: 'Event', params: {id: data.id}});
this.$router.push({name: 'Event', params: {uuid: data.uuid}});
}).catch((err) => {
Promise.resolve(err).then((err) => {
console.log('err creation', err);
});
});
} else {
eventFetch(`/events/${this.id}`, this.$store, {method: 'PUT', body: JSON.stringify(this.event)})
eventFetch(`/events/${this.uuid}`, 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}});
this.$router.push({name: 'Event', params: {uuid: data.uuid}});
});
}
this.event.tags = [];

View File

@@ -48,10 +48,10 @@
>
</v-avatar>
</router-link>
<span v-if="event.organizer">Organisé par {{ event.organizer.display_name }}</span>
<span v-if="event.organizer">Organisé par {{ event.organizer.display_name ? event.organizer.display_name : event.organizer.username }}</span>
</div>
<p>
<vue-markdown :source="event.description" />
<vue-markdown :source="event.description" v-if="event.description" />
</p>
<!--<p><router-link :to="{ name: 'Account', params: {id: event.organizer.id} }"><span class="grey&#45;&#45;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>-->

View File

@@ -19,38 +19,38 @@
<v-layout>
<v-flex xs12 sm8 offset-sm2>
<v-card>
<v-layout row wrap>
<v-flex xs4 v-for="event in events" :key="event.uuid">
<v-card :to="{ name: 'Event', params:{ uuid: event.uuid } }">
<v-card-media v-if="!event.image"
class="white--text"
height="200px"
src="https://picsum.photos/g/400/200/"
>
<v-container fill-height fluid>
<v-layout fill-height>
<v-flex xs12 align-end flexbox>
<span class="headline black--text">{{ event.title }}</span>
</v-flex>
</v-layout>
</v-container>
</v-card-media>
<v-card-title primary-title>
<div>
<span class="grey--text">{{ event.begins_on | formatDate }}</span><br>
<router-link :to="{name: 'Account', params: { name: event.organizer.username } }">
<v-avatar size="25px">
<img class="img-circle elevation-7 mb-1"
:src="event.organizer.avatar"
>
</v-avatar>
</router-link>
<span v-if="event.organizer">Organisé par {{ event.organizer.display_name }}</span>
</div>
</v-card-title>
</v-card>
</v-flex>
</v-layout>
<v-layout row wrap>
<v-flex xs4 v-for="event in events" :key="event.uuid">
<v-card :to="{ name: 'Event', params:{ uuid: event.uuid } }">
<v-card-media v-if="!event.image"
class="white--text"
height="200px"
src="https://picsum.photos/g/400/200/"
>
<v-container fill-height fluid>
<v-layout fill-height>
<v-flex xs12 align-end flexbox>
<span class="headline black--text">{{ event.title }}</span>
</v-flex>
</v-layout>
</v-container>
</v-card-media>
<v-card-title primary-title>
<div>
<span class="grey--text">{{ event.begins_on | formatDate }}</span><br>
<router-link :to="{name: 'Account', params: { name: event.organizer.username } }">
<v-avatar size="25px">
<img class="img-circle elevation-7 mb-1"
:src="event.organizer.avatar"
>
</v-avatar>
</router-link>
<span v-if="event.organizer">Organisé par {{ event.organizer.display_name }}</span>
</div>
</v-card-title>
</v-card>
</v-flex>
</v-layout>
</v-card>
</v-flex>
</v-layout>

View File

@@ -1,85 +0,0 @@
<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), (data) => {
this.$store.commit('LOGIN_USER', data.user);
this.$router.push({ name: 'Home' });
}, (error) => {
Promise.resolve(error).then((errorMsg) => {
console.log(errorMsg);
this.error.show = true;
this.error.text = this.$t(errorMsg.display_error);
}).catch((e) => {
console.log(e);
this.error.show = true;
this.error.text = e.message;
});
});
},
},
};
</script>

View File

@@ -3,24 +3,23 @@
class="blue darken-3"
dark
app
clipped-left
:clipped-left="$vuetify.breakpoint.lgAndUp"
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' }">
Eventos
<v-toolbar-title style="width: 300px" class="ml-0 pl-3 white--text">
<v-toolbar-side-icon @click.stop="toggleDrawer()"></v-toolbar-side-icon>
<router-link :to="{ name: 'Home' }" class="hidden-sm-and-down white--text">Eventos
</router-link>
</v-toolbar-title>
<v-select
autocomplete
<v-autocomplete
:loading="searchElement.loading"
light
solo
flat
solo-inverted
prepend-icon="search"
placeholder="Search"
label="Search"
required
item-text="displayedText"
class="hidden-sm-and-down"
:items="searchElement.items"
:search-input.sync="search"
v-model="searchSelect"
@@ -39,7 +38,7 @@
</v-list-tile-content>
</template>
</template>
</v-select>
</v-autocomplete>
<v-spacer></v-spacer>
<v-menu
offset-y
@@ -82,6 +81,12 @@
export default {
name: 'NavBar',
props: {
toggleDrawer: {
type: Function,
required: true,
},
},
data() {
return {
notificationMenu: false,
@@ -165,3 +170,9 @@
}
}
</script>
<style>
nav.v-toolbar .v-input__slot {
margin-bottom: 0;
}
</style>

View File

@@ -1,87 +0,0 @@
<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), (response) => {
console.log(response);
this.$store.commit('LOGIN_USER', response.user);
this.$router.push({ name: 'Home' });
}, (error) => {
this.error.show = true;
this.error.text = error.message;
this.error.field[error.field] = true;
});
},
},
};
</script>

View File

@@ -8,8 +8,12 @@ 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 Register from '@/components/Account/Register';
import Login from '@/components/Account/Login';
import Validate from '@/components/Account/Validate';
import ResendConfirmation from '@/components/Account/ResendConfirmation';
import SendPasswordReset from '@/components/Account/SendPasswordReset';
import PasswordReset from '@/components/Account/PasswordReset';
import Account from '@/components/Account/Account';
import CreateGroup from '@/components/Group/Create';
import Group from '@/components/Group/Group';
@@ -68,12 +72,42 @@ const router = new Router({
path: '/register',
name: 'Register',
component: Register,
props: true,
meta: { requiredAuth: false },
},
{
path: '/resend-instructions',
name: 'ResendConfirmation',
component: ResendConfirmation,
props: true,
meta: { requiresAuth: false },
},
{
path: '/password-reset/send',
name: 'SendPasswordReset',
component: SendPasswordReset,
props: true,
meta: { requiresAuth: false },
},
{
path: '/password-reset/:token',
name: 'PasswordReset',
component: PasswordReset,
meta: { requiresAuth: false },
props: true,
},
{
path: '/validate/:token',
name: 'Validate',
component: Validate,
props: true,
meta: { requiresAuth: false },
},
{
path: '/login',
name: 'Login',
component: Login,
props: true,
meta: { requiredAuth: false },
},
{
@@ -109,7 +143,8 @@ const router = new Router({
props: true,
meta: { requiredAuth: false },
},
{ path: "*",
{
path: '*',
name: 'PageNotFound',
component: PageNotFound,
meta: { requiredAuth: false },

View File

@@ -2,7 +2,7 @@ const Dotenv = require('dotenv-webpack');
module.exports = {
lintOnSave: false,
compiler: true,
runtimeCompiler: true,
configureWebpack: {
plugins: [
new Dotenv(),