Move to GraphQL

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
Thomas Citharel
2018-11-06 10:30:27 +01:00
parent 7e137d1a1c
commit b54dae7e15
149 changed files with 5605 additions and 4665 deletions

View File

@@ -1,7 +1,7 @@
module.exports = {
root: true,
'extends': [
extends: [
'plugin:vue/essential',
'@vue/airbnb'
]
}
'@vue/airbnb',
],
};

View File

@@ -1,5 +1,5 @@
module.exports = {
plugins: {
autoprefixer: {}
}
}
autoprefixer: {},
},
};

58
js/Makefile Normal file
View File

@@ -0,0 +1,58 @@
# On OSX the PATH variable isn't exported unless "SHELL" is also set, see: http://stackoverflow.com/a/25506676
SHELL = /bin/bash
NODE_BINDIR = ./node_modules/.bin
export PATH := $(NODE_BINDIR):$(PATH)
# Where to find input files (it can be multiple paths).
INPUT_FILES = ./src
# Where to write the files generated by this makefile.
OUTPUT_DIR = ./src/i18n
# Available locales for the app.
LOCALES = en_US fr_FR
# Name of the generated .po files for each available locale.
LOCALE_FILES ?= $(patsubst %,$(OUTPUT_DIR)/locale/%/LC_MESSAGES/app.po,$(LOCALES))
GETTEXT_HTML_SOURCES = $(shell find $(INPUT_FILES) -name '*.vue' -o -name '*.html' 2> /dev/null)
GETTEXT_JS_SOURCES = $(shell find $(INPUT_FILES) -name '*.vue' -o -name '*.js')
# Makefile Targets
.PHONY: clean makemessages translations
clean:
rm -f /tmp/template.pot $(OUTPUT_DIR)/translations.json
makemessages: /tmp/template.pot
translations: ./$(OUTPUT_DIR)/translations.json
# Create a main .pot template, then generate .po files for each available language.
# Thanx to Systematic: https://github.com/Polyconseil/systematic/blob/866d5a/mk/main.mk#L167-L183
/tmp/template.pot: $(GETTEXT_HTML_SOURCES)
# `dir` is a Makefile built-in expansion function which extracts the directory-part of `$@`.
# `$@` is a Makefile automatic variable: the file name of the target of the rule.
# => `mkdir -p /tmp/`
mkdir -p $(dir $@)
which gettext-extract
# Extract gettext strings from templates files and create a POT dictionary template.
gettext-extract --attribute v-translate --quiet --output $@ $(GETTEXT_HTML_SOURCES)
# Extract gettext strings from JavaScript files.
xgettext --language=JavaScript --keyword=npgettext:1c,2,3 \
--from-code=utf-8 --join-existing --no-wrap \
--package-name=$(shell node -e "console.log(require('./package.json').name);") \
--package-version=$(shell node -e "console.log(require('./package.json').version);") \
--output $@ $(GETTEXT_JS_SOURCES)
# Generate .po files for each available language.
@for lang in $(LOCALES); do \
export PO_FILE=$(OUTPUT_DIR)/locale/$$lang/LC_MESSAGES/app.po; \
echo "msgmerge --update $$PO_FILE $@"; \
mkdir -p $$(dirname $$PO_FILE); \
[ -f $$PO_FILE ] && msgmerge --lang=$$lang --update $$PO_FILE $@ || msginit --no-translator --locale=$$lang --input=$@ --output-file=$$PO_FILE; \
msgattrib --no-wrap --no-obsolete -o $$PO_FILE $$PO_FILE; \
done;
$(OUTPUT_DIR)/translations.json: clean /tmp/template.pot
mkdir -p $(OUTPUT_DIR)
gettext-compile --output $@ $(LOCALE_FILES)

38
js/get_union_json.js Normal file
View File

@@ -0,0 +1,38 @@
const fetch = require('node-fetch');
const fs = require('fs');
fetch(`http://localhost:4000/graphiql`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
variables: {},
query: `
{
__schema {
types {
kind
name
possibleTypes {
name
}
}
}
}
`,
}),
})
.then(result => result.json())
.then(result => {
// here we're filtering out any type information unrelated to unions or interfaces
const filteredData = result.data.__schema.types.filter(
type => type.possibleTypes !== null,
);
result.data.__schema.types = filteredData;
fs.writeFile('./fragmentTypes.json', JSON.stringify(result.data), err => {
if (err) {
console.error('Error writing fragmentTypes file', err);
} else {
console.log('Fragment types successfully extracted!');
}
});
});

2180
js/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -6,22 +6,29 @@
"serve": "vue-cli-service serve",
"build": "vue-cli-service build --modern",
"lint": "vue-cli-service lint",
"test:unit": "vue-cli-service test:unit",
"test:e2e": "vue-cli-service test:e2e"
"test:e2e": "vue-cli-service test:e2e",
"test:unit": "vue-cli-service test:unit"
},
"dependencies": {
"apollo-absinthe-upload-link": "^1.4.0",
"apollo-cache-inmemory": "^1.3.6",
"apollo-link": "^1.2.3",
"apollo-link-http": "^1.5.5",
"easygettext": "^2.7.0",
"graphql-tag": "^2.9.0",
"material-design-icons": "^3.0.1",
"moment": "^2.22.2",
"ngeohash": "^0.6.0",
"register-service-worker": "^1.4.1",
"vue": "^2.5.17",
"vue-apollo": "^3.0.0-beta.25",
"vue-gettext": "^2.1.1",
"vue-gravatar": "^1.2.1",
"vue-markdown": "^2.2.4",
"vue-router": "^3.0.1",
"vuetify": "^1.2.7",
"vuetify": "^1.3.1",
"vuetify-google-autocomplete": "^2.0.0-beta.5",
"vuex": "^3.0.1",
"vuex-i18n": "^1.10.5"
"vuex": "^3.0.1"
},
"devDependencies": {
"@vue/cli-plugin-babel": "^3.0.5",
@@ -36,6 +43,7 @@
"dotenv-webpack": "^1.5.7",
"node-sass": "^4.9.3",
"sass-loader": "^7.1.0",
"vue-cli-plugin-apollo": "^0.17.1",
"vue-template-compiler": "^2.5.17"
},
"browserslist": [

View File

@@ -12,24 +12,24 @@
<v-list-group
value="false"
>
<v-list-tile avatar v-if="$store.state.actor" slot="activator">
<v-list-tile avatar v-if="actor" slot="activator">
<v-list-tile-avatar>
<img v-if="!$store.state.actor.avatar"
<img v-if="!actor.avatar"
class="img-circle elevation-7 mb-1"
src="https://picsum.photos/125/125/"
>
<img v-else
class="img-circle elevation-7 mb-1"
:src="$store.state.actor.avatar"
:src="actor.avatar"
>
</v-list-tile-avatar>
<v-list-tile-content @click="$router.push({name: 'Account', params: { name: $store.state.actor.username }})">
<v-list-tile-content @click="$router.push({name: 'Account', params: { name: actor.username }})">
<v-list-tile-title>{{ this.displayed_name }}</v-list-tile-title>
</v-list-tile-content>
</v-list-tile>
<v-list-tile avatar v-if="$store.state.actor">
<v-list-tile avatar v-if="actor">
<v-list-tile-avatar>
<img
class="img-circle elevation-7 mb-1"
@@ -93,11 +93,12 @@
<v-speed-dial
v-model="fab"
bottom
fixed
right
fixed
direction="top"
open-on-hover
transition="scale-transition"
v-if="getUser()"
v-if="user"
>
<v-btn
slot="activator"
@@ -129,7 +130,12 @@
</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>
<span
class="white--text"
v-translate="{
date: new Date().getFullYear(),
}">© The Mobilizon Contributors %{date} - 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>
<v-snackbar
:timeout="error.timeout"
@@ -143,8 +149,9 @@
</template>
<script>
import gql from 'graphql-tag';
import NavBar from '@/components/NavBar';
import { AUTH_USER_ACTOR, AUTH_USER_ID } from '@/constants';
export default {
name: 'app',
@@ -155,11 +162,17 @@ export default {
return {
drawer: false,
fab: false,
user: false,
user: localStorage.getItem(AUTH_USER_ID),
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: '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 },
@@ -171,14 +184,15 @@ export default {
text: '',
},
show_new_event_button: false,
actor: localStorage.getItem(AUTH_USER_ACTOR),
};
},
methods: {
showMenuItem(elem) {
return elem !== null && this.$store.state.user && this.$store.state.user.roles !== undefined ? this.$store.state.user.roles.includes(elem) : true;
return elem !== null && this.user && this.user.roles !== undefined ? this.user.roles.includes(elem) : true;
},
getUser() {
return this.$store.state.user === undefined ? false : this.$store.state.user;
return this.user === undefined ? false : this.user;
},
toggleDrawer() {
this.drawer = !this.drawer;
@@ -186,9 +200,9 @@ export default {
},
computed: {
displayed_name() {
return this.$store.state.actor.display_name === null ? this.$store.state.actor.username : this.$store.state.actor.display_name
return this.actor.display_name === null ? this.actor.username : this.actor.display_name;
},
}
},
};
</script>

View File

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

View File

@@ -1,29 +0,0 @@
import { API_ORIGIN, API_PATH } from './_entrypoint';
const jsonLdMimeType = 'application/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_ORIGIN + url : API_ORIGIN + API_PATH + url;
return fetch(link, options).then((response) => {
if (response.ok) return response;
throw response.text();
});
}

View File

@@ -1,120 +0,0 @@
import { API_ORIGIN, API_PATH } from '../api/_entrypoint';
import { LOGIN_USER, LOAD_USER, CHANGE_ACTOR } from '../store/mutation-types';
// URL and endpoint constants
const LOGIN_URL = `${API_ORIGIN}${API_PATH}/login`;
const SIGNUP_URL = `${API_ORIGIN}${API_PATH}/users/`;
const CHECK_AUTH = `${API_ORIGIN}${API_PATH}/user/`;
const REFRESH_TOKEN = `${API_ORIGIN}${API_PATH}/token/refresh`;
export default {
// Send a request to the login URL and save the returned JWT
login(creds, success, error) {
fetch(LOGIN_URL, { method: 'POST', body: creds, headers: { 'Content-Type': 'application/json' } })
.then((response) => {
if (response.status === 200) {
return response.json();
}
throw response.json();
})
.then((data) => {
localStorage.setItem('token', data.token);
// localStorage.setItem('refresh_token', data.refresh_token);
return success(data);
})
.catch(err => error(err));
},
signup(creds, success, error) {
fetch(SIGNUP_URL, { method: 'POST', body: creds, headers: { 'Content-Type': 'application/json' } })
.then((response) => {
if (response.status === 200) {
return response.json();
}
throw response.json();
})
.then((data) => {
localStorage.setItem('token', data.token);
// localStorage.setItem('refresh_token', data.refresh_token);
return success(data);
}).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();
}
return 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");
successHandler();
});
},
// To log out, we just need to remove the token
logout(store) {
localStorage.removeItem('refresh_token');
localStorage.removeItem('token');
this.authenticated = false;
store.commit('LOGOUT_USER');
},
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();
},
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();
}
return errorHandler('Error while authenticating');
}).then((response) => {
this.authenticated = true;
console.log(response);
store.commit(LOAD_USER, response.data);
store.commit(CHANGE_ACTOR, response.data.actors[0]);
return successHandler();
});
},
// The object to be passed as a header for authenticated requests
getAuthHeader() {
return {
Authorization: `Bearer ${localStorage.getItem('access_token')}`,
};
},
};

View File

@@ -1,216 +1,210 @@
<template>
<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-img :src="actor.banner || 'https://picsum.photos/400/'" height="300px">
<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.actor.id === actor.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="logoutUser()" v-if="$store.state.user && $store.state.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.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="!actor.avatar"
class="img-circle elevation-7 mb-1"
src="https://picsum.photos/125/125/"
>
<img v-else
<ApolloQuery :query="FETCH_ACTOR" :variables="{ name }">
<template slot-scope="{ result: { loading, error, data } }">
<v-progress-circular v-if="loading" indeterminate color="primary"></v-progress-circular>
<v-card v-if="data">
<v-img :src="data.actor.banner || 'https://picsum.photos/400/'" height="300px">
<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="actor.id === data.actor.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="logoutUser()" v-if="actor.id === data.actor.id">
<v-list-tile-title>User logout</v-list-tile-title>
</v-list-tile>
<v-list-tile @click="deleteAccount()" v-if="actor.id === data.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="!data.actor.avatarUrl"
class="img-circle elevation-7 mb-1"
:src="actor.avatar"
>
</v-avatar>
</div>
<v-container fluid grid-list-lg>
<v-layout row>
<v-flex xs7>
<div class="headline">{{ actor.display_name }}</div>
<div><span class="subheading">@{{ actor.username }}<span v-if="actor.domain">@{{ actor.domain }}</span></span></div>
<v-card-text v-if="actor.description" v-html="actor.description"></v-card-text>
src="https://picsum.photos/125/125/"
>
<img v-else
class="img-circle elevation-7 mb-1"
:src="data.actor.avatarUrl"
>
</v-avatar>
</div>
<v-container fluid grid-list-lg>
<v-layout row>
<v-flex xs7>
<div class="headline">{{ data.actor.name }}</div>
<div><span class="subheading">@{{ data.actor.preferredUsername }}<span v-if="data.actor.domain">@{{ data.actor.domain }}</span></span></div>
<v-card-text v-if="data.actor.description" v-html="data.actor.description"></v-card-text>
</v-flex>
</v-layout>
</v-container>
</v-layout>
</v-img>
<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="data.actor.participatingEvents && data.actor.participatingEvents.length > 0">
<v-subheader>Participated at</v-subheader>
<v-layout row wrap>
<v-flex v-for="event in data.actor.participatingEvents" :key="event.id">
<v-card>
<v-img
class="black--text"
height="200px"
src="https://picsum.photos/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-img>
<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-layout>
</v-img>
<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="actor.participatingEvents && actor.participatingEvents.length > 0">
<v-subheader>Participated at</v-subheader>
<v-layout row wrap>
<v-flex v-for="event in actor.participatingEvents" :key="event.id">
<v-card>
<v-card-media
class="black--text"
height="200px"
src="https://picsum.photos/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="actor.organized_events && actor.organized_events.length > 0">
<v-subheader>Organized events</v-subheader>
<v-layout row wrap>
<v-flex v-for="event in actor.organized_events" :key="event.id">
<v-card>
<v-card-media
class="black--text"
height="200px"
src="https://picsum.photos/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-container fluid grid-list-md v-if="data.actor.organizedEvents && data.actor.organizedEvents.length > 0">
<v-subheader>Organized events</v-subheader>
<v-layout row wrap>
<v-flex v-for="event in data.actor.organizedEvents" :key="event.id" md6>
<v-card>
<v-img
height="200px"
src="https://picsum.photos/400/200/"
/>
<v-card-title primary-title>
<div>
<router-link :to="{name: 'Event', params: {uuid: event.uuid}}">
<div class="headline">{{ event.title }}</div>
</router-link>
<span class="grey--text" v-html="nl2br(event.description)"></span>
</div>
</v-card-title>
<!-- <v-card-title>
<div>
<span class="grey--text" v-if="event.addressType === 'physical'">{{ event.startDate }} à {{ event.location }}</span><br>
<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>
</template>
</ApolloQuery>
</v-flex>
</v-layout>
</template>
<script>
import eventFetch from '@/api/eventFetch';
import auth from '@/auth';
import { FETCH_ACTOR } from '@/graphql/actor';
export default {
name: 'Account',
data() {
return {
actor: null,
loading: true,
}
loading: true,
};
},
props: {
name: {
type: String,
required: true,
}
name: {
type: String,
required: true,
},
},
created() {
this.fetchData();
},
watch: {
// call again the method if the route changes
'$route': 'fetchData'
$route: 'fetchData',
},
methods: {
fetchData() {
eventFetch(`/actors/${this.name}`, this.$store)
.then(response => response.json())
.then((response) => {
this.actor = response.data;
this.loading = false;
console.log('actor', this.actor);
})
},
logoutUser() {
auth.logout(this.$store);
// TODO : implement logout
this.$router.push({ name: 'Home' });
},
}
}
nl2br: function(text) {
return text.replace(/(?:\r\n|\r|\n)/g, '<br>');
}
},
};
</script>

View File

@@ -15,7 +15,7 @@
@click="$router.push({ name: 'Account', params: { name: actor.username } })"
>
<v-list-tile-action>
<v-icon v-if="$store.state.defaultActor === actor.username" color="pink">star</v-icon>
<v-icon v-if="defaultActor === actor.username" color="pink">star</v-icon>
</v-list-tile-action>
<v-list-tile-content>
@@ -67,29 +67,26 @@
</template>
<script>
import eventFetch from "@/api/eventFetch";
import auth from "@/auth";
export default {
name: "Identities",
name: 'Identities',
data() {
return {
actors: [],
newActor: {
preferred_username: "",
summary: ""
preferred_username: '',
summary: '',
},
loading: true,
showForm: false,
rules: {
required: value => !!value || "Required."
required: value => !!value || 'Required.',
},
state: {
username: {
status: false,
msg: []
}
}
msg: [],
},
},
};
},
created() {
@@ -97,9 +94,9 @@ export default {
},
methods: {
fetchData() {
eventFetch(`/user`, this.$store)
eventFetch('/user', this.$store)
.then(response => response.json())
.then(response => {
.then((response) => {
this.actors = response.data.actors;
this.loading = false;
});
@@ -107,12 +104,12 @@ export default {
sendData() {
this.loading = true;
this.showForm = false;
eventFetch(`/actors`, this.$store, {
method: "POST",
body: JSON.stringify({ actor: this.newActor })
eventFetch('/actors', this.$store, {
method: 'POST',
body: JSON.stringify({ actor: this.newActor }),
})
.then(response => response.json())
.then(response => {
.then((response) => {
this.actors.push(response.data);
this.loading = false;
});
@@ -126,7 +123,7 @@ export default {
},
host() {
return `@${window.location.host}`;
}
}
},
},
};
</script>

View File

@@ -60,83 +60,88 @@
<script>
import { LOGIN_USER } from '@/store/mutation-types';
import auth from '@/auth/index';
import Gravatar from 'vue-gravatar';
import RegisterAvatar from './RegisterAvatar';
import Gravatar from 'vue-gravatar';
import RegisterAvatar from './RegisterAvatar';
import { AUTH_TOKEN, AUTH_USER_ID, AUTH_USER_ACTOR } from '@/constants';
import { LOGIN } from '@/graphql/auth';
export default {
props: {
email: {
type: String,
required: false,
default: '',
export default {
props: {
email: {
type: String,
required: false,
default: '',
},
password: {
type: String,
required: false,
default: '',
},
},
beforeCreate() {
if (this.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: '',
},
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,
},
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';
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();
this.$apollo.mutate({
mutation: LOGIN,
variables: {
email: this.credentials.email,
password: this.credentials.password
}
}).then((result) => {
this.saveUserData(result.data);
this.$router.push({name: 'Home'});
}).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';
},
saveUserData({login: login}) {
localStorage.setItem(AUTH_USER_ID, login.user.id);
localStorage.setItem(AUTH_USER_ACTOR, JSON.stringify(login.actor));
localStorage.setItem(AUTH_TOKEN, login.token);
}
},
};
</script>

View File

@@ -37,8 +37,6 @@
</template>
<script>
import fetchStory from '@/api/eventFetch';
export default {
name: 'PasswordReset',
props: {
@@ -80,7 +78,7 @@ export default {
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: {

View File

@@ -9,7 +9,7 @@
<v-tooltip bottom>
<v-btn
slot="activator"
:to="{ name: 'Login', params: { email: this.credentials.email, password: this.credentials.password } }"
:to="{ name: 'Login', params: { email, password } }"
>
<!-- <v-icon large>login</v-icon> -->
<span>Login</span>
@@ -21,22 +21,22 @@
<div class="text-xs-center">
<v-avatar size="80px">
<transition name="avatar">
<component :is="validEmail()" v-bind="{email: credentials.email}"></component>
<component :is="validEmail()" v-bind="{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-form @submit="submit()" v-if="!validationSent">
<v-text-field
label="Username"
required
type="text"
v-model="credentials.username"
v-model="username"
:rules="[rules.required]"
:error="this.state.username.status"
:error-messages="this.state.username.msg"
:suffix="this.host()"
:error="state.username.status"
:error-messages="state.username.msg"
:suffix="host()"
hint="You will be able to create more identities once registered"
persistent-hint
>
@@ -46,30 +46,30 @@
required
type="email"
ref="email"
v-model="credentials.email"
v-model="email"
:rules="[rules.required, rules.email]"
:error="this.state.email.status"
:error-messages="this.state.email.msg"
:error="state.email.status"
:error-messages="state.email.msg"
>
</v-text-field>
<v-text-field
label="Password"
required
:type="showPassword ? 'text' : 'password'"
v-model="credentials.password"
v-model="password"
:rules="[rules.required, rules.password_length]"
:error="this.state.password.status"
:error-messages="this.state.password.msg"
:error="state.password.status"
:error-messages="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-btn @click="submit()" color="primary">Register</v-btn>
<router-link :to="{ name: 'ResendConfirmation', params: { 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-if="validationSent">
<h2><translate>A validation email was sent to %{email}</translate></h2>
<v-alert :value="true" type="info"><translate>Before you can login, you need to click on the link inside it to validate your account</translate></v-alert>
</div>
</v-card-text>
</v-card>
@@ -79,110 +79,101 @@
</template>
<script>
import auth from '@/auth/index';
import Gravatar from 'vue-gravatar';
import RegisterAvatar from './RegisterAvatar';
import Gravatar from 'vue-gravatar';
import RegisterAvatar from './RegisterAvatar';
import { CREATE_USER } from '@/graphql/user';
export default {
props: {
email: {
type: String,
required: false,
default: '',
export default {
props: {
default_email: {
type: String,
required: false,
default: '',
},
default_password: {
type: String,
required: false,
default: '',
},
},
components: {
'v-gravatar': Gravatar,
avatar: RegisterAvatar,
},
data() {
return {
username: '',
email: this.default_email,
password: this.default_password,
error: {
show: false,
},
password: {
type: String,
required: false,
default: '',
showPassword: false,
validationSent: false,
state: {
email: {
status: false,
msg: [],
},
username: {
status: false,
msg: [],
},
password: {
status: false,
msg: [],
},
},
},
components: {
'v-gravatar': Gravatar,
'avatar': RegisterAvatar
},
mounted() {
this.credentials.email = this.email;
this.credentials.password = this.password;
},
data() {
return {
credentials: {
username: '',
email: '',
password: '',
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.';
},
error: {
show: false,
},
};
},
methods: {
resetState() {
this.state = {
email: {
status: false,
msg: '',
},
showPassword: false,
validationSent: false,
state: {
email: {
status: false,
msg: [],
},
username: {
status: false,
msg: [],
},
password: {
status: false,
msg: [],
},
username: {
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.';
},
password: {
status: false,
msg: '',
},
};
},
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';
}
host() {
return `@${window.location.host}`;
},
};
validEmail() {
return this.rules.email(this.email) === true ? 'v-gravatar' : 'avatar';
},
submit() {
this.$apollo.mutate({
mutation: CREATE_USER,
variables: {
email: this.email,
password: this.password,
username: this.username,
},
}).then((data) => {
console.log(data);
this.validationSent = true;
}).catch((error) => {
console.error(error);
});
},
},
};
</script>
<style lang="scss">
.avatar-enter-active {

View File

@@ -3,7 +3,7 @@
</template>
<script>
export default {
name: 'RegisterAvatar'
}
name: 'RegisterAvatar',
};
</script>

View File

@@ -31,8 +31,6 @@
</template>
<script>
import fetchStory from '@/api/eventFetch';
export default {
name: 'ResendConfirmation',
props: {

View File

@@ -31,8 +31,6 @@
</template>
<script>
import fetchStory from '@/api/eventFetch';
export default {
name: 'SendPasswordReset',
props: {
@@ -43,8 +41,8 @@ export default {
},
},
mounted() {
this.credentials.email = this.email;
},
this.credentials.email = this.email;
},
data() {
return {
credentials: {

View File

@@ -1,18 +1,17 @@
<template>
<v-container>
<h1 v-if="loading">{{ $t('registration.validation.process') }}</h1>
<h1 v-if="loading"><translate>Your account is being validated</translate></h1>
<div v-else>
<div v-if="failed">
<v-alert :value="true" variant="danger">Error while validating account</v-alert>
<v-alert :value="true" variant="danger"><translate>Error while validating account</translate></v-alert>
</div>
<h1 v-else>{{ $t('registration.validation.finished') }}</h1>
<h1 v-else><translate>Your account has been validated</translate></h1>
</div>
</v-container>
</template>
<script>
import fetchStory from '@/api/eventFetch';
import { LOGIN_USER } from '@/store/mutation-types';
import { VALIDATE_USER } from '@/graphql/user';
export default {
name: 'Validate',
@@ -33,20 +32,27 @@ export default {
},
methods: {
validateAction() {
fetchStory(`/users/validate/${this.token}`, this.$store).then((data) => {
this.$apollo.mutate({
mutation: VALIDATE_USER,
variables: {
token: this.token,
},
}).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;
});
console.log(data);
this.saveUserData(data.data);
this.$router.push({name: 'Home'});
}).catch((error) => {
this.loading = false;
console.log(error);
this.failed = true;
});
},
saveUserData({validateUser: login}) {
localStorage.setItem(AUTH_USER_ID, login.user.id);
localStorage.setItem(AUTH_USER_ACTOR, JSON.stringify(login.actor));
localStorage.setItem(AUTH_TOKEN, login.token);
}
},
};
</script>

View File

@@ -1,42 +1,87 @@
<template>
<div>
<h3>Create a new category</h3>
<v-form>
<v-text-field
label="Name of the category"
v-model="category.title"
:counter="100"
required
></v-text-field>
</v-form>
<v-btn color="primary" @click="create">Create category</v-btn>
</div>
<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><translate>Create a new category</translate></v-toolbar-title>
</v-toolbar>
<v-card-text>
<v-form>
<v-text-field
:label="$gettext('Name of the category')"
v-model="title"
:counter="100"
required
></v-text-field>
<v-textarea
:label="$gettext('Description')"
v-model="description"
></v-textarea>
<v-flex xs12 class="text-xs-center text-sm-center text-md-center text-lg-center">
<v-img :src="image.url" height="150" v-if="image.url" aspect-ratio="1" contain/>
<v-text-field label="Select Image" @click='pickFile' v-model='image.name' prepend-icon='attach_file'></v-text-field>
<input
type="file"
style="display: none"
ref="image"
accept="image/*"
@change="onFilePicked"
>
</v-flex>
<v-btn color="primary" @click="create"><translate>Create category</translate></v-btn>
</v-form>
</v-card-text>
</v-card>
</v-flex>
</v-layout>
</v-container>
</template>
<script>
import eventFetch from '@/api/eventFetch';
import { UPLOAD_PICTURE } from '@/graphql/upload';
import { CREATE_CATEGORY } from '@/graphql/category';
export default {
name: 'create-category',
data() {
return {
category: {
title: '',
},
};
},
methods: {
create() {
const router = this.$router;
eventFetch('/categories', this.$store, { method: 'POST', body: JSON.stringify({ category: this.category }) })
.then(response => response.json())
.then(() => {
this.loading = false;
router.push('/category')
});
export default {
name: 'create-category',
data() {
return {
title: '',
description: '',
image: {
url: '',
name: '',
file: '',
},
};
},
methods: {
create() {
this.$apollo.mutate({
mutation: CREATE_CATEGORY,
variables: {
title: this.title,
description: this.description,
picture: this.$refs.image.files[0],
}
}).then((data) => {
console.log(data);
}).catch((error) => {
console.error(error);
});
},
};
pickFile () {
this.$refs.image.click ()
},
onFilePicked(e) {
const files = e.target.files;
if(files[0] === undefined || files[0].name.lastIndexOf('.') <= 0) {
console.error("File is incorrect")
}
this.image.name = files[0].name;
},
},
};
</script>
<style>

View File

@@ -1,14 +1,13 @@
<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-progress-circular v-if="$apollo.loading" indeterminate color="primary"></v-progress-circular>
<v-layout row wrap v-else>
<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-img v-if="category.picture.url" :src="HTTP_ENDPOINT + category.picture.url" height="200px">
</v-img>
<v-card-title primary-title>
<div>
<h3 class="headline mb-0">{{ category.title }}</h3>
@@ -16,8 +15,8 @@
</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-btn flat class="orange--text"><translate>Explore</translate></v-btn>
<v-btn flat class="red--text" v-on:click="deleteCategory(category.id)"><translate>Delete</translate></v-btn>
</v-card-actions>
</v-card>
</v-flex>
@@ -32,40 +31,36 @@
</template>
<script>
import eventFetch from '@/api/eventFetch';
import { FETCH_CATEGORIES } from '@/graphql/category';
export default {
name: 'Home',
data() {
return {
categories: [],
loading: true,
};
// TODO : remove this hardcode
export default {
name: 'Home',
data() {
return {
categories: [],
loading: true,
HTTP_ENDPOINT: 'http://localhost:4000',
};
},
apollo: {
categories: {
query: FETCH_CATEGORIES,
},
created() {
this.fetchData();
},
methods: {
fetchData() {
eventFetch('/categories', this.$store)
.then(response => response.json())
.then((response) => {
this.loading = false;
this.categories = response.data;
});
},
deleteCategory(categoryId) {
const router = this.$router;
eventFetch('/categories/' + categoryId, this.$store, {method: 'DELETE'})
.then(() => {
this.categories = this.categories.filter((category) => {
return category.id !== categoryId;
});
},
methods: {
deleteCategory(categoryId) {
const router = this.$router;
eventFetch(`/categories/${categoryId}`, this.$store, { method: 'DELETE' })
.then(() => {
this.categories = this.categories.filter(category => category.id !== categoryId);
router.push('/category');
});
}
},
};
},
};
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->

View File

@@ -14,6 +14,8 @@
:counter="100"
required
></v-text-field>
<v-date-picker v-model="event.begins_on">
</v-date-picker>
<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>
@@ -21,7 +23,7 @@
<v-radio label="Other" value="other"></v-radio>
</v-radio-group>
<!-- <vuetify-google-autocomplete
v-if="event.location_type === 'physical'"
v-if="event.location_type === 'physical'"
id="map"
append-icon="search"
classname="form-control"
@@ -64,134 +66,132 @@
</template>
<script>
// import Location from '@/components/Location';
import eventFetch from '@/api/eventFetch';
import VueMarkdown from 'vue-markdown';
// import Location from '@/components/Location';
import VueMarkdown from 'vue-markdown';
import { CREATE_EVENT, EDIT_EVENT } from '@/graphql/event';
import { FETCH_CATEGORIES } from '@/graphql/category';
import { AUTH_USER_ACTOR } from '@/constants';
export default {
name: 'create-event',
props: ['id'],
components: {
/* Location,*/
VueMarkdown,
export default {
name: 'create-event',
props: {
uuid: {
required: false,
type: String,
},
data() {
return {
e1: 0,
event: {
title: null,
description: null,
begins_on: new Date(),
ends_on: new Date(),
seats: null,
physical_address: null,
location_type: 'physical',
online_address: null,
tel_num: null,
price: null,
category: null,
category_id: null,
tags: [],
participants: [],
},
categories: [],
},
components: {
/* Location, */
VueMarkdown,
},
data() {
return {
e1: 0,
event: {
title: null,
description: '',
begins_on: (new Date()).toISOString().substr(0, 10),
ends_on: new Date(),
seats: null,
physical_address: null,
location_type: 'physical',
online_address: null,
tel_num: null,
price: null,
category: null,
category_id: null,
tags: [],
tagsToSend: [],
tagsFetched: [],
};
participants: [],
},
categories: [],
tags: [],
tagsToSend: [],
tagsFetched: [],
};
},
// created() {
// if (this.uuid) {
// this.fetchEvent();
// }
// },
apollo: {
categories: {
query: FETCH_CATEGORIES,
},
created() {
if (this.id) {
this.fetchEvent();
},
methods: {
create() {
// this.event.seats = parseInt(this.event.seats, 10);
// this.tagsToSend.forEach((tag) => {
// this.event.tags.push({
// title: tag,
// // '@type': 'Tag',
// });
// });
const actor = JSON.parse(localStorage.getItem(AUTH_USER_ACTOR));
this.event.category_id = this.event.category;
this.event.organizer_actor_id = actor.id;
this.event.participants = [actor.id];
// this.event.price = parseFloat(this.event.price);
if (this.uuid === undefined) {
this.$apollo.mutate({
mutation: CREATE_EVENT,
variables: {
title: this.event.title,
description: this.event.description,
organizerActorId: this.event.organizer_actor_id,
categoryId: this.event.category_id,
beginsOn: this.event.begins_on,
addressType: this.event.location_type,
}
}).then((data) => {
this.loading = false;
this.$router.push({ name: 'Event', params: { uuid: data.data.uuid } });
}).catch((error) => {
console.log(error);
});
} else {
this.$apollo.mutate({
mutation: EDIT_EVENT,
}).then((data) => {
this.loading = false;
this.$router.push({ name: 'Event', params: { uuid: data.data.uuid } });
}).catch((error) => {
console.log(error);
});
}
this.event.tags = [];
},
// fetchEvent() {
// eventFetch(`/events/${this.id}`, this.$store)
// .then(response => response.json())
// .then((data) => {
// this.loading = false;
// this.event = data;
// console.log(this.event);
// });
// },
getAddressData(addressData) {
if (addressData !== null) {
this.event.address = {
geom: {
data: {
latitude: addressData.latitude,
longitude: addressData.longitude,
},
type: 'point',
},
addressCountry: addressData.country,
addressLocality: addressData.locality,
addressRegion: addressData.administrative_area_level_1,
postalCode: addressData.postal_code,
streetAddress: `${addressData.street_number} ${addressData.route}`,
};
}
},
mounted() {
this.fetchCategories();
this.fetchTags();
},
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;
this.event.organizer_actor_id = this.$store.state.actor.id;
this.event.participants = [this.$store.state.actor.id];
// 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: {uuid: data.data.uuid}});
}).catch((err) => {
Promise.resolve(err).then((err) => {
console.log('err creation', err);
});
});
} else {
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: {uuid: data.uuid}});
});
}
this.event.tags = [];
},
fetchCategories() {
eventFetch('/categories', this.$store)
.then(response => response.json())
.then((response) => {
this.loading = false;
this.categories = response.data;
});
},
fetchTags() {
eventFetch('/tags', this.$store)
.then(response => response.json())
.then((response) => {
this.loading = false;
response.data.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) {
if (addressData !== null) {
this.event.address = {
geom: {
data: {
latitude: addressData.latitude,
longitude: addressData.longitude,
},
type: "point",
},
addressCountry: addressData.country,
addressLocality: addressData.locality,
addressRegion: addressData.administrative_area_level_1,
postalCode: addressData.postal_code,
streetAddress: `${addressData.street_number} ${addressData.route}`,
};
}
},
},
};
},
};
</script>
<style>

View File

@@ -98,29 +98,27 @@
</template>
<script>
import eventFetch from '@/api/eventFetch';
export default {
props: ['id'],
data() {
return {
loading: true,
event: null,
};
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);
});
},
created() {
this.fetchData();
},
methods: {
fetchData() {
eventFetch(`/events/${this.id}`, this.$store)
.then(response => response.json())
.then((data) => {
this.loading = false;
this.event = data;
console.log(this.event);
});
},
}
}
},
};
</script>

View File

@@ -1,239 +1,237 @@
<template>
<v-layout row>
<v-flex xs12 sm6 offset-sm3>
<span v-if="error">Error : event not found</span>
<v-progress-circular v-if="loading" indeterminate color="primary"></v-progress-circular>
<v-card v-if="!loading && !error">
<v-img
src="https://picsum.photos/600/400/"
height="200px"
>
<v-container fill-height fluid>
<v-layout fill-height>
<v-flex xs12 align-end flexbox>
<v-card-title>
<v-btn icon @click="$router.go(-1)" class="white--text">
<v-icon>chevron_left</v-icon>
</v-btn>
<v-progress-circular v-if="$apollo.loading" indeterminate color="primary"></v-progress-circular>
<div>{{ event }}</div>
<v-card v-if="event">
<!-- <v-img
src="https://picsum.photos/600/400/"
height="200px"
>
<v-container fill-height fluid>
<v-layout fill-height>
<v-flex xs12 align-end flexbox>
<v-card-title>
<v-btn icon @click="$router.go(-1)" class="white--text">
<v-icon>chevron_left</v-icon>
</v-btn>
<v-spacer></v-spacer>
<v-btn icon class="mr-3 white--text" v-if="actorIsOrganizer()" :to="{ name: 'EditEvent', params: {uuid: event.uuid}}">
<v-icon>edit</v-icon>
</v-btn>
<v-menu bottom left>
<v-btn icon slot="activator" class="white--text">
<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="actorIsOrganizer()">
<v-list-tile-title>Delete</v-list-tile-title>
</v-list-tile>
</v-list>
</v-menu>
</v-card-title>
</v-flex>
</v-layout>
</v-container>
</v-img> -->
<v-container grid-list-md>
<v-layout row wrap>
<v-flex md10>
<v-spacer></v-spacer>
<v-btn icon class="mr-3 white--text" v-if="actorIsOrganizer()" :to="{ name: 'EditEvent', params: {id: event.id}}">
<v-icon>edit</v-icon>
</v-btn>
<v-menu bottom left>
<v-btn icon slot="activator" class="white--text">
<v-icon>more_vert</v-icon>
</v-btn>
<v-list>
<v-list-tile @click="downloadIcsEvent()">
<v-list-tile-title>Download</v-list-tile-title>
<span class="subheading grey--text">{{ event.begins_on | formatDay }}</span>
<h1 class="display-1">{{ event.title }}</h1>
<div>
<!-- <router-link :to="{name: 'Account', params: { name: event.organizerActor.preferredUsername } }">
<v-avatar size="25px">
<img class="img-circle elevation-7 mb-1"
:src="event.organizer_actor.avatarUrl"
>
</v-avatar>
</router-link> -->
<!-- <span v-if="event.organizerActor">Organisé par {{ event.organizerActor.name ? event.organizerActor.name : event.organizerActor.preferredUsername }}</span> -->
</div>
<!-- <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>
</v-flex>
<!-- <v-flex md2>
<p v-if="actorIsOrganizer()">
Vous êtes organisateur de cet événement.
</p>
<div v-else>
<p v-if="actorIsParticipant()">
Vous avez annoncé aller à cet événement.
</p>
<p v-else>Vous y allez ?
<span class="text--darken-2 grey--text">{{ event.participants.length }} personnes y vont.</span>
</p>
</div>
<v-card-actions v-if="!actorIsOrganizer()">
<v-btn v-if="!actorIsParticipant()" @click="joinEvent" color="success"><v-icon>check</v-icon> Join</v-btn>
<v-btn v-if="actorIsParticipant()" @click="leaveEvent" color="error">Leave</v-btn>
</v-card-actions>
</v-flex> -->
</v-layout>
</v-container>
<v-divider></v-divider>
<v-container>
<v-layout row wrap>
<v-flex xs12 md4 order-md1>
<v-layout
column
fill-height
>
<v-list two-line>
<v-list-tile>
<v-list-tile-action>
<v-icon color="indigo">access_time</v-icon>
</v-list-tile-action>
<v-list-tile-content>
<v-list-tile-title>{{ event.begins_on | formatDate }}</v-list-tile-title>
<v-list-tile-sub-title>{{ event.ends_on | formatDate }}</v-list-tile-sub-title>
</v-list-tile-content>
</v-list-tile>
<v-list-tile @click="deleteEvent()" v-if="actorIsOrganizer()">
<v-list-tile-title>Delete</v-list-tile-title>
<v-divider inset></v-divider>
<v-list-tile>
<v-list-tile-action>
<v-icon color="indigo">place</v-icon>
</v-list-tile-action>
<v-list-tile-content>
<v-list-tile-title><span v-if="event.address_type === 'physical'">
{{ event.physical_address.streetAddress }}
</span></v-list-tile-title>
<v-list-tile-sub-title>Mobile</v-list-tile-sub-title>
</v-list-tile-content>
</v-list-tile>
</v-list>
</v-menu>
</v-card-title>
</v-flex>
</v-layout>
</v-container>
</v-img>
<v-container grid-list-md>
<v-layout row wrap>
<v-flex md10>
<v-spacer></v-spacer>
<span class="subheading grey--text">{{ event.begins_on | formatDay }}</span>
<h1 class="display-1">{{ event.title }}</h1>
<div>
<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 ? event.organizer.display_name : event.organizer.username }}</span>
</div>
<!--<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>-->
</v-flex>
<v-flex md2>
<p v-if="actorIsOrganizer()">
Vous êtes organisateur de cet événement.
</p>
<div v-else>
<p v-if="actorIsParticipant()">
Vous avez annoncé aller à cet événement.
</p>
<p v-else>Vous y allez ?
<span class="text--darken-2 grey--text">{{ event.participants.length }} personnes y vont.</span>
</p>
</div>
<v-card-actions v-if="!actorIsOrganizer()">
<v-btn v-if="!actorIsParticipant()" @click="joinEvent" color="success"><v-icon>check</v-icon> Join</v-btn>
<v-btn v-if="actorIsParticipant()" @click="leaveEvent" color="error">Leave</v-btn>
</v-card-actions>
</v-flex>
</v-layout>
</v-container>
<v-divider></v-divider>
<v-container>
<v-layout row wrap>
<v-flex xs12 md4 order-md1>
<v-layout
column
fill-height
>
<v-list two-line>
<v-list-tile>
<v-list-tile-action>
<v-icon color="indigo">access_time</v-icon>
</v-list-tile-action>
<v-list-tile-content>
<v-list-tile-title>{{ event.begins_on | formatDate }}</v-list-tile-title>
<v-list-tile-sub-title>{{ event.ends_on | formatDate }}</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">place</v-icon>
</v-list-tile-action>
<v-list-tile-content>
<v-list-tile-title><span v-if="event.address_type === 'physical'">
{{ event.physical_address.streetAddress }}
</span></v-list-tile-title>
<v-list-tile-sub-title>Mobile</v-list-tile-sub-title>
</v-list-tile-content>
</v-list-tile>
</v-list>
</v-layout>
</v-flex>
<v-flex md8 xs12>
<p>
<h2>Details</h2>
<vue-markdown :source="event.description" v-if="event.description" :toc-first-level="3" />
</p>
<v-subheader>Participants</v-subheader>
<v-flex md2 v-for="actor in event.participants" :key="actor.uuid">
<router-link :to="{name: 'Account', params: { name: actor.username }}">
<v-avatar size="75px">
<img v-if="!actor.avatar"
class="img-circle elevation-7 mb-1"
src="https://picsum.photos/125/125/"
>
<img v-else
class="img-circle elevation-7 mb-1"
:src="actor.avatar"
>
</v-avatar>
</router-link>
<span>{{ actor.username }}</span>
</v-flex>
</v-flex>
<span v-if="event.participants.length === 0">No participants yet.</span>
</v-layout>
</v-container>
</v-card>
</v-layout>
</v-flex>
<v-flex md8 xs12>
<p>
<h2>Details</h2>
<vue-markdown :source="event.description" v-if="event.description" :toc-first-level="3"></vue-markdown>
</p>
<v-subheader>Participants</v-subheader>
<!-- <v-flex md2 v-for="participant in event.participants" :key="participant.actor.uuid">
<router-link :to="{name: 'Account', params: { name: participant.actor.preferredUsername }}">
<v-card>
<v-avatar size="75px">
<img v-if="!participant.actor.avatarUrl"
class="img-circle elevation-7 mb-1"
src="https://picsum.photos/125/125/"
>
<img v-else
class="img-circle elevation-7 mb-1"
:src="participant.actor.avatarUrl"
>
</v-avatar>
<v-card-title>
<span>{{ participant.actor.preferredUsername }}</span>
</v-card-title>
</v-card>
</router-link>
</v-flex> -->
</v-flex>
<span v-if="event.participants.length === 0">No participants yet.</span>
</v-layout>
</v-container>
</v-card>
</v-flex>
</v-layout>
</template>
<script>
import eventFetch from '@/api/eventFetch';
import VueMarkdown from 'vue-markdown';
import VueMarkdown from 'vue-markdown';
import { FETCH_EVENT } from '@/graphql/event';
import { LOGGED_ACTOR } from '@/graphql/actor';
export default {
name: 'Home',
components: {
VueMarkdown,
},
data() {
return {
loading: true,
error: false,
event: {
name: '',
slug: '',
title: '',
export default {
name: 'Home',
components: {
VueMarkdown,
},
data() {
return {
event: {
name: '',
slug: '',
title: '',
uuid: this.uuid,
description: '',
organizer: {
id: null,
username: null,
},
participants: [],
},
};
},
apollo: {
event: {
query: FETCH_EVENT,
variables() {
return {
uuid: this.uuid,
description: '',
organizer: {
id: null,
username: null,
},
participants: [],
},
};
},
methods: {
deleteEvent() {
const router = this.$router;
eventFetch(`/events/${this.uuid}`, this.$store, { method: 'DELETE' })
.then(() => router.push({'name': 'EventList'}));
};
},
fetchData() {
eventFetch(`/events/${this.uuid}`, this.$store)
.then(response => response.json())
.then((data) => {
this.loading = false;
this.event = data.data;
console.log('event', this.event);
}).catch((res) => {
Promise.resolve(res).then((data) => {
console.log(data);
this.error = true;
this.loading = false;
});
},
// loggedActor: {
// query: LOGGED_ACTOR,
// }
},
methods: {
deleteEvent() {
const router = this.$router;
eventFetch(`/events/${this.uuid}`, this.$store, { method: 'DELETE' })
.then(() => router.push({ name: 'EventList' }));
},
joinEvent() {
eventFetch(`/events/${this.uuid}/join`, this.$store, { method: 'POST' })
.then(response => response.json())
.then((data) => {
console.log(data);
});
},
joinEvent() {
eventFetch(`/events/${this.uuid}/join`, this.$store, { method: 'POST' })
.then(response => response.json())
.then((data) => {
console.log(data);
});
},
leaveEvent() {
eventFetch(`/events/${this.uuid}/leave`, this.$store)
.then(response => response.json())
.then((data) => {
console.log(data);
});
},
downloadIcsEvent() {
eventFetch(`/events/${this.uuid}/ics`, 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);
})
},
actorIsParticipant() {
return this.$store.state.actor && this.event.participants.map(participant => participant.id).includes(this.$store.state.actor.id) || this.actorIsOrganizer();
},
actorIsOrganizer() {
return this.$store.state.actor && this.$store.state.actor.id === this.event.organizer.id;
}
},
props: {
uuid: {
type: String,
required: true,
},
leaveEvent() {
eventFetch(`/events/${this.uuid}/leave`, this.$store)
.then(response => response.json())
.then((data) => {
console.log(data);
});
},
created() {
this.fetchData();
downloadIcsEvent() {
eventFetch(`/events/${this.uuid}/ics`, 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);
});
},
};
// actorIsParticipant() {
// return this.loggedActor && this.event.participants.map(participant => participant.actor.preferredUsername).includes(this.loggedActor.preferredUsername) || this.actorIsOrganizer();
// },
// actorIsOrganizer() {
// return this.loggedActor && this.loggedActor.preferredUsername === this.event.organizer.preferredUsername;
// },
},
props: {
uuid: {
type: String,
required: true,
},
},
};
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->

View File

@@ -54,85 +54,84 @@
</template>
<script>
import ngeohash from 'ngeohash';
import VueMarkdown from 'vue-markdown';
import eventFetch from '@/api/eventFetch';
import VCardTitle from "vuetify/es5/components/VCard/VCardTitle";
import ngeohash from 'ngeohash';
import VueMarkdown from 'vue-markdown';
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'});
}
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();
},
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);
},
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((response) => {
this.loading = false;
this.events = response.data;
console.log(this.events);
});
},
deleteEvent(event) {
const router = this.$router;
eventFetch(`/events/${event.uuid}`, this.$store, {'method': 'DELETE'})
.then(() => router.push('/events'));
},
viewEvent(event) {
this.$router.push({ name: 'Event', params: { uuid: event.uuid } })
},
downloadIcsEvent(event) {
eventFetch(`/events/${event.uuid}/ics`, 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);
})
},
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((response) => {
this.loading = false;
this.events = response.data;
console.log(this.events);
});
},
};
deleteEvent(event) {
const router = this.$router;
eventFetch(`/events/${event.uuid}`, this.$store, { method: 'DELETE' })
.then(() => router.push('/events'));
},
viewEvent(event) {
this.$router.push({ name: 'Event', params: { uuid: event.uuid } });
},
downloadIcsEvent(event) {
eventFetch(`/events/${event.uuid}/ics`, 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 -->

View File

@@ -65,66 +65,65 @@
</template>
<script>
import eventFetch from '@/api/eventFetch';
import VueMarkdown from 'vue-markdown';
import VuetifyGoogleAutocomplete from 'vuetify-google-autocomplete';
import VueMarkdown from 'vue-markdown';
import VuetifyGoogleAutocomplete from 'vuetify-google-autocomplete';
export default {
name: 'create-group',
export default {
name: 'create-group',
components: {
VueMarkdown,
VuetifyGoogleAutocomplete,
components: {
VueMarkdown,
VuetifyGoogleAutocomplete,
},
data() {
return {
e1: 0,
group: {
preferred_username: '',
name: '',
summary: '',
// 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({ group: this.group }) })
.then(response => response.json())
.then((data) => {
this.loading = false;
this.$router.push({ path: 'Group', params: { id: data.id } });
});
},
data() {
return {
e1: 0,
group: {
preferred_username: '',
name: '',
summary: '',
// category: null,
fetchCategories() {
eventFetch('/categories', this.$store)
.then(response => response.json())
.then((data) => {
this.loading = false;
this.categories = data.data;
});
},
getAddressData(addressData) {
this.group.address = {
geo: {
latitude: addressData.latitude,
longitude: addressData.longitude,
},
categories: [],
addressCountry: addressData.country,
addressLocality: addressData.city,
addressRegion: addressData.administrative_area_level_1,
postalCode: addressData.postal_code,
streetAddress: `${addressData.street_number} ${addressData.route}`,
};
},
mounted() {
this.fetchCategories();
},
methods: {
create() {
// this.group.organizer = "/accounts/" + this.$store.state.user.id;
eventFetch('/groups', this.$store, { method: 'POST', body: JSON.stringify({group: 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.data;
});
},
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>

View File

@@ -202,39 +202,37 @@
</template>
<script>
import eventFetch from '@/api/eventFetch';
export default {
name: 'Group',
data() {
return {
group: null,
loading: true,
}
},
props: {
name: {
type: String,
required: true,
}
},
created() {
this.fetchData();
},
watch: {
// call again the method if the route changes
'$route': 'fetchData'
},
methods: {
fetchData() {
eventFetch(`/actors/${this.name}`, this.$store)
.then(response => response.json())
.then((response) => {
this.group = response.data;
this.loading = false;
console.log(this.group);
})
}
}
}
export default {
name: 'Group',
data() {
return {
group: null,
loading: true,
};
},
props: {
name: {
type: String,
required: true,
},
},
created() {
this.fetchData();
},
watch: {
// call again the method if the route changes
$route: 'fetchData',
},
methods: {
fetchData() {
eventFetch(`/actors/${this.name}`, this.$store)
.then(response => response.json())
.then((response) => {
this.group = response.data;
this.loading = false;
console.log(this.group);
});
},
},
};
</script>

View File

@@ -38,49 +38,47 @@
</template>
<script>
import eventFetch from '@/api/eventFetch';
export default {
name: 'GroupList',
data() {
return {
groups: [],
loading: true,
};
export default {
name: 'GroupList',
data() {
return {
groups: [],
loading: true,
};
},
created() {
this.fetchData();
},
methods: {
username_with_domain(actor) {
return actor.username + (actor.domain === null ? '' : `@${actor.domain}`);
},
created() {
this.fetchData();
fetchData() {
eventFetch('/groups', this.$store)
.then(response => response.json())
.then((data) => {
console.log(data);
this.loading = false;
this.groups = data.data;
});
},
methods: {
username_with_domain(actor) {
return actor.username + (actor.domain === null ? '' : `@${actor.domain}`)
},
fetchData() {
eventFetch('/groups', this.$store)
.then(response => response.json())
.then((data) => {
console.log(data);
this.loading = false;
this.groups = data.data;
});
},
deleteGroup(group) {
const router = this.$router;
eventFetch(`/groups/${this.username_with_domain(group)}`, this.$store, {'method': 'DELETE'})
.then(response => response.json())
.then(() => router.push('/groups'));
},
viewActor(actor) {
this.$router.push({ name: 'Group', params: { name: this.username_with_domain(actor) } })
},
joinGroup(group) {
const router = this.$router;
eventFetch(`/groups/${this.username_with_domain(group)}/join`, this.$store, { method: 'POST' })
.then(response => response.json())
.then(() => router.push({ name: 'Group', params: { name: this.username_with_domain(group) } }));
}
deleteGroup(group) {
const router = this.$router;
eventFetch(`/groups/${this.username_with_domain(group)}`, this.$store, { method: 'DELETE' })
.then(response => response.json())
.then(() => router.push('/groups'));
},
};
viewActor(actor) {
this.$router.push({ name: 'Group', params: { name: this.username_with_domain(actor) } });
},
joinGroup(group) {
const router = this.$router;
eventFetch(`/groups/${this.username_with_domain(group)}/join`, this.$store, { method: 'POST' })
.then(response => response.json())
.then(() => router.push({ name: 'Group', params: { name: this.username_with_domain(group) } }));
},
},
};
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->

View File

@@ -5,14 +5,14 @@
src="https://picsum.photos/1200/900"
dark
height="300"
v-if="$store.state.user === false"
v-if="!user"
>
<v-container fill-height>
<v-layout align-center>
<v-flex text-xs-center>
<h1 class="display-3">Find events you like</h1>
<h2>Share it with Mobilizon</h2>
<v-btn :to="{ name: 'Register' }">{{ $t("home.register") }}</v-btn>
<v-btn :to="{ name: 'Register' }"><translate>Register</translate></v-btn>
</v-flex>
</v-layout>
</v-container>
@@ -21,7 +21,7 @@
<v-flex xs12 sm8 offset-sm2>
<v-layout row wrap>
<v-flex xs12 sm6>
<h1>Welcome back {{ $store.state.actor.username }}</h1>
<h1><translate :translate-params="{username: actor.preferredUsername}">Welcome back %{username}</translate></h1>
</v-flex>
<v-flex xs12 sm6>
<v-layout align-center>
@@ -33,11 +33,14 @@
</v-layout>
</v-flex>
</v-layout>
<div v-if="$apollo.loading">
Still loading
</div>
<v-card v-if="events.length > 0">
<v-layout row wrap>
<v-flex md4 v-for="event in events" :key="event.uuid">
<v-card :to="{ name: 'Event', params:{ uuid: event.uuid } }">
<v-card-media v-if="!event.image"
<v-img v-if="!event.image"
class="white--text"
height="200px"
src="https://picsum.photos/g/400/200/"
@@ -49,26 +52,26 @@
</v-flex>
</v-layout>
</v-container>
</v-card-media>
</v-img>
<v-card-title primary-title>
<div>
<span class="grey--text">{{ event.begins_on | formatDay }}</span><br>
<router-link :to="{name: 'Account', params: { name: event.organizer.username } }">
<router-link :to="{name: 'Account', params: { name: event.organizerActor.preferredUsername } }">
<v-avatar size="25px">
<img class="img-circle elevation-7 mb-1"
:src="event.organizer.avatar"
:src="event.organizerActor.avatarUrl"
>
</v-avatar>
</router-link>
<span v-if="event.organizer">Organisé par {{ event.organizer.display_name ? event.organizer.display_name : event.organizer.username }}</span>
<span v-if="event.organizerActor">Organisé par {{ event.organizerActor.name ? event.organizerActor.name : event.organizerActor.preferredUsername }}</span>
</div>
</v-card-title>
</v-card>
</v-flex>
</v-layout>
</v-card>
<v-alert v-else :value="true" type="info">
No events found nearby {{ ipLocation() }}
<v-alert v-else :value="true" type="error">
No events found
</v-alert>
</v-flex>
</v-layout>
@@ -76,83 +79,75 @@
</template>
<script>
import ngeohash from 'ngeohash';
import eventFetch from "../api/eventFetch";
import {AUTH_USER_ACTOR, AUTH_USER_ID} from '@/constants';
import { FETCH_EVENTS } from '@/graphql/event';
export default {
name: 'Home',
data() {
return {
gradient: 'to top right, rgba(63,81,181, .7), rgba(25,32,72, .7)',
user: null,
searchTerm: null,
location_field: {
loading: false,
search: null,
},
locations: [],
events: [],
city: {name: null},
country: {name: null},
locations: [],
city: { name: null },
country: { name: null },
actor: JSON.parse(localStorage.getItem(AUTH_USER_ACTOR)),
user: localStorage.getItem(AUTH_USER_ID),
};
},
created() {
this.fetchData();
apollo: {
events: {
query: FETCH_EVENTS,
},
},
computed: {
displayed_name() {
return this.$store.state.actor.display_name === null ? this.$store.state.actor.username : this.$store.state.actor.display_name
return this.actor.name === null ? this.actor.preferredUsername : this.actor.name;
},
},
methods: {
fetchLocations() {
eventFetch('/locations', this.$store)
.then((response) => (response.json()))
.then(response => (response.json()))
.then((response) => {
this.locations = response;
});
},
fetchData() {
eventFetch('/events', this.$store)
.then(response => response.json())
.then((response) => {
this.loading = false;
this.events = response.data;
this.city = response.city;
this.country = response.country;
});
},
geoLocalize() {
const router = this.$router;
if (sessionStorage.getItem('City')) {
router.push({name: 'EventList', params: {location: localStorage.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}`), {
router.push({ name: 'EventList', params: { location: geohash } });
}, err => console.warn(`ERROR(${err.code}): ${err.message}`), {
enableHighAccuracy: true,
timeout: 5000,
maximumAge: 0
maximumAge: 0,
});
}
},
getAddressData: function (addressData) {
getAddressData(addressData) {
const geohash = ngeohash.encode(addressData.latitude, addressData.longitude, 11);
sessionStorage.setItem('City', geohash);
this.$router.push({name: 'EventList', params: {location: geohash}});
this.$router.push({ name: 'EventList', params: { location: geohash } });
},
viewEvent(event) {
this.$router.push({ name: 'Event', params: { uuid: event.uuid } })
this.$router.push({ name: 'Event', params: { uuid: event.uuid } });
},
ipLocation() {
return this.city.name ? this.city.name : this.country.name;
}
},
},
};
</script>

View File

@@ -26,27 +26,27 @@
<script>
export default {
data() {
return {
description: 'Paris, France',
center: { lat: 48.85, lng: 2.35 },
markers: [],
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);
},
props: ['address'],
methods: {
setPlace(place) {
this.center = {
lat: place.geometry.location.lat(),
lng: place.geometry.location.lng(),
};
this.markers = [{
position: { lat: this.center.lat, lng: this.center.lng },
}];
this.$emit('input', place.formatted_address);
},
},
};
},
};
</script>

View File

@@ -12,33 +12,36 @@
</router-link>
</v-toolbar-title>
<v-autocomplete
:loading="searchElement.loading"
:loading="$apollo.loading"
flat
solo-inverted
prepend-icon="search"
label="Search"
:label="$gettext('Search')"
required
item-text="displayedText"
item-text="label"
class="hidden-sm-and-down"
:items="searchElement.items"
:search-input.sync="search"
v-model="searchSelect"
:items="items"
:search-input.sync="searchText"
v-model="model"
return-object
>
<template slot="item" slot-scope="data">
<template v-if="typeof data.item !== 'object'">
<v-list-tile-content v-text="data.item"></v-list-tile-content>
</template>
<template v-else>
<!-- <div>{{ data }}</div> -->
<v-list-tile v-if="data.item.__typename === 'Event'">
<v-list-tile-avatar>
<img :src="data.item.avatar" v-if="data.item.avatar">
<v-icon v-else>event</v-icon>
<v-icon>event</v-icon>
</v-list-tile-avatar>
<v-list-tile-content v-text="data.item.label"></v-list-tile-content>
</v-list-tile>
<v-list-tile v-else-if="data.item.__typename === 'Actor'">
<v-list-tile-avatar>
<img :src="data.item.avatarUrl" v-if="data.item.avatarUrl">
<v-icon v-else>account_circle</v-icon>
</v-list-tile-avatar>
<v-list-tile-content>
<v-list-tile-title v-html="username_with_domain(data.item)"></v-list-tile-title>
<v-list-tile-sub-title v-html="data.item.type"></v-list-tile-sub-title>
</v-list-tile-content>
</template>
</v-list-tile>
</template>
</v-autocomplete>
<v-spacer></v-spacer>
@@ -47,7 +50,7 @@
:close-on-content-click="false"
:nudge-width="200"
v-model="notificationMenu"
v-if="getUser()"
v-if="user"
>
<v-btn icon slot="activator">
<v-badge left color="red">
@@ -70,115 +73,98 @@
</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-btn flat @click="notificationMenu = false"><translate>Close</translate></v-btn>
<v-btn color="primary" flat @click="notificationMenu = false"><translate>Save</translate></v-btn>
</v-card-actions>
</v-card>
</v-menu>
<v-btn v-if="!$store.state.user" :to="{ name: 'Login' }">Se connecter</v-btn>
<v-btn v-if="!user" :to="{ name: 'Login' }"><translate>Login</translate></v-btn>
</v-toolbar>
</template>
<script>
import eventFetch from '@/api/eventFetch';
import {AUTH_USER_ACTOR, AUTH_USER_ID} from '@/constants';
import {SEARCH} from '@/graphql/search';
export default {
name: 'NavBar',
props: {
toggleDrawer: {
type: Function,
required: true,
export default {
name: 'NavBar',
props: {
toggleDrawer: {
type: Function,
required: true,
},
},
data() {
return {
notificationMenu: false,
notifications: [
{ header: 'Coucou' },
{ title: "T'as une notification", subtitle: 'Et elle est cool' },
],
model: null,
search: [],
searchText: null,
searchSelect: null,
actor: localStorage.getItem(AUTH_USER_ACTOR),
user: localStorage.getItem(AUTH_USER_ID),
};
},
apollo: {
search: {
query: SEARCH,
variables() {
return {
searchText: this.searchText,
};
},
skip() {
return !this.searchText;
},
},
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('searchSelect', val);
if (val.type === 'Event') {
this.$router.push({name: 'Event', params: { uuid: val.uuid }});
} else if (val.type === 'Locality') {
this.$router.push({name: 'EventList', params: {location: val.geohash}});
} else {
this.$router.push({name: 'Account', params: { name : this.username_with_domain(val) }});
}
},
watch: {
model(val) {
switch(val.__typename) {
case 'Event':
this.$router.push({ name: 'Event', params: { uuid: val.uuid } });
break;
case 'Actor':
this.$router.push({ name: 'Account', params: { name: this.username_with_domain(val) } });
break;
}
},
computed: {
displayed_name: function() {
console.log('displayed name', this.$store.state.actor);
if (this.$store.state.actor) {
return this.$store.state.actor.display_name === null ? this.$store.state.actor.username : this.$store.state.actor.display_name;
},
computed: {
items() {
return this.search.map(searchEntry => {
switch (searchEntry.__typename) {
case 'Actor':
searchEntry.label = searchEntry.preferredUsername;
break;
case 'Event':
searchEntry.label = searchEntry.title;
break;
}
},
return searchEntry;
});
},
methods: {
username_with_domain(actor) {
if (actor.type !== 'Event') {
return actor.username + (actor.domain === null ? '' : `@${actor.domain}`)
}
return actor.title;
},
getUser() {
return this.$store.state.user === undefined ? false : this.$store.state.user;
},
querySelections(searchTerm) {
this.searchElement.loading = true;
eventFetch(`/search/${searchTerm}`, this.$store)
.then(response => response.json())
.then((results) => {
console.log('results');
console.log(results);
const accountResults = results.data.actors.map((result) => {
if (result.domain) {
result.displayedText = `${result.username}@${result.domain}`;
} else {
result.displayedText = result.username;
}
return result;
});
const eventsResults = results.data.events.map((result) => {
result.displayedText = result.title;
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(eventsResults);
this.searchElement.loading = false;
});
displayed_name() {
console.log('displayed name', this.actor);
if (this.actor) {
return this.actor.display_name === null ? this.actor.username : this.actor.display_name;
}
}
}
},
},
methods: {
username_with_domain(actor) {
return actor.preferredUsername + (actor.domain === undefined ? '' : `@${actor.domain}`);
},
},
};
</script>
<style>
nav.v-toolbar .v-input__slot {
margin-bottom: 0;
}
</style>
</style>

3
js/src/constants.js Normal file
View File

@@ -0,0 +1,3 @@
export const AUTH_TOKEN = 'auth-token';
export const AUTH_USER_ID = 'auth-user-id';
export const AUTH_USER_ACTOR = 'auth-user-actor';

39
js/src/graphql/actor.js Normal file
View File

@@ -0,0 +1,39 @@
import gql from 'graphql-tag';
export const FETCH_ACTOR = gql`
query($name:String!) {
actor(preferredUsername: $name) {
url,
outboxUrl,
inboxUrl,
followingUrl,
followersUrl,
sharedInboxUrl,
name,
domain,
summary,
preferredUsername,
suspended,
avatarUrl,
bannerUrl,
organizedEvents {
uuid,
title,
description,
organizer_actor {
avatarUrl,
preferred_username,
name,
}
},
}
}
`;
export const LOGGED_ACTOR = gql`
query {
loggedActor {
avatarUrl,
preferredUsername,
}
}`;

16
js/src/graphql/auth.js Normal file
View File

@@ -0,0 +1,16 @@
import gql from 'graphql-tag';
export const LOGIN = gql`
mutation Login($email: String!, $password: String!) {
login(email: $email, password: $password) {
token,
user {
id,
},
actor {
avatarUrl,
preferredUsername,
}
},
}
`;

View File

@@ -0,0 +1,29 @@
import gql from 'graphql-tag';
export const FETCH_CATEGORIES = gql`
query {
categories {
id,
title,
description,
picture {
url,
},
}
}
`;
export const CREATE_CATEGORY = gql`
mutation createCategory($title: String!, $description: String!, $picture: Upload!) {
createCategory(title: $title, description: $description, picture: $picture) {
id,
title,
description,
picture {
url,
url_thumbnail
},
},
},
`;

109
js/src/graphql/event.js Normal file
View File

@@ -0,0 +1,109 @@
import gql from 'graphql-tag';
export const FETCH_EVENT = gql`
query($uuid:UUID!) {
event(uuid: $uuid) {
uuid,
url,
local,
title,
description,
begins_on,
ends_on,
state,
status,
public,
thumbnail,
large_image,
publish_at,
# address_type,
online_address,
phone,
organizerActor {
avatarUrl,
preferredUsername,
name,
},
attributedTo {
avatarUrl,
preferredUsername,
name,
},
participants {
actor {
avatarUrl,
preferredUsername,
name,
},
role,
},
category {
title,
},
}
}
`;
export const FETCH_EVENTS = gql`
query {
events {
uuid,
url,
local,
title,
description,
begins_on,
ends_on,
state,
status,
public,
thumbnail,
large_image,
publish_at,
# address_type,
online_address,
phone,
organizerActor {
avatarUrl,
preferredUsername,
name,
},
attributedTo {
avatarUrl,
preferredUsername,
name,
},
category {
title,
},
}
}
`;
export const CREATE_EVENT = gql`
mutation CreateEvent(
$title: String!,
$description: String!,
$organizerActorId: Int!,
$categoryId: Int!,
$beginsOn: DateTime!,
$addressType: AddressType!,
) {
createEvent(title: $title, description: $description, beginsOn: $beginsOn, organizerActorId: $organizerActorId, categoryId: $categoryId, addressType: $addressType) {
uuid
}
}
`;
export const EDIT_EVENT = gql`
mutation EditEvent(
$title: String!,
$description: String!,
$organizerActorId: Int!,
$categoryId: Int!,
) {
EditEvent(title: $title, description: $description, organizerActorId: $organizerActorId, categoryId: $categoryId) {
uuid
}
}
`;

View File

@@ -0,0 +1 @@
{"__schema":{"types":[{"possibleTypes":[{"name":"Event"},{"name":"Actor"}],"name":"SearchResult","kind":"UNION"}]}}

17
js/src/graphql/search.js Normal file
View File

@@ -0,0 +1,17 @@
import gql from 'graphql-tag';
export const SEARCH = gql`
query SearchEvents($searchText: String!) {
search(search: $searchText) {
...on Event {
title,
uuid,
__typename
},
...on Actor {
preferredUsername,
__typename
}
}
}
`;

10
js/src/graphql/upload.js Normal file
View File

@@ -0,0 +1,10 @@
import gql from 'graphql-tag';
export const UPLOAD_PICTURE = gql`
mutation {
uploadPicture(file: "file") {
url,
url_thumbnail
}
}
`;

28
js/src/graphql/user.js Normal file
View File

@@ -0,0 +1,28 @@
import gql from 'graphql-tag';
export const CREATE_USER = gql`
mutation CreateUser($email: String!, $username: String!, $password: String!) {
createUser(email: $email, username: $username, password: $password) {
preferredUsername,
user {
email,
confirmationSentAt
}
}
}
`;
export const VALIDATE_USER = gql`
mutation ValidateUser($token: String!) {
validateUser(token: $token) {
token,
user {
id,
},
actor {
avatarUrl,
preferredUsername,
}
}
}
`;

View File

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

View File

@@ -1,20 +0,0 @@
export default {
home: {
welcome: 'Bienvenue sur Mobilizon, {username}!',
welcome_off: 'Bienvenue sur Mobilizon',
events: 'Événements',
groups: 'Groupes',
login: 'Se connecter',
register: "S'inscrire",
},
event: {
list: {
title: "Votre liste d'événements",
},
},
session: {
error: {
bad_login: 'Erreur lors de la connexion : Votre nom d\'utilisateur ou votre mot de passe est incorrect',
},
},
};

View File

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

View File

@@ -0,0 +1,30 @@
# English translations for mobilizon package.
# Copyright (C) 2018 THE mobilizon'S COPYRIGHT HOLDER
# This file is distributed under the same license as the mobilizon package.
# Automatically generated, 2018.
#
msgid ""
msgstr ""
"Project-Id-Version: mobilizon 0.1.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-10-24 16:25+0200\n"
"PO-Revision-Date: 2018-10-24 16:25+0200\n"
"Last-Translator: Automatically generated\n"
"Language-Team: none\n"
"Language: en_US\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: src/components/Account/Register.vue:70
msgid "A validation email was sent to %{email}"
msgstr "A validation email was sent to %{email}"
#: src/components/Account/Register.vue:71
msgid "Before you can login, you need to click on the link inside it to validate your account"
msgstr "Before you can login, you need to click on the link inside it to validate your account"
#: src/components/Home.vue:14
msgid "Register"
msgstr "Register"

View File

@@ -0,0 +1,30 @@
# French translations for mobilizon package.
# Copyright (C) 2018 THE mobilizon'S COPYRIGHT HOLDER
# This file is distributed under the same license as the mobilizon package.
# Automatically generated, 2018.
#
msgid ""
msgstr ""
"Project-Id-Version: mobilizon 0.1.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-10-24 16:25+0200\n"
"PO-Revision-Date: 2018-10-24 16:25+0200\n"
"Last-Translator: Automatically generated\n"
"Language-Team: none\n"
"Language: fr_FR\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
#: src/components/Account/Register.vue:70
msgid "A validation email was sent to %{email}"
msgstr ""
#: src/components/Account/Register.vue:71
msgid "Before you can login, you need to click on the link inside it to validate your account"
msgstr ""
#: src/components/Home.vue:14
msgid "Register"
msgstr "S'inscrire"

View File

@@ -0,0 +1,30 @@
# French translations for mobilizon package.
# Copyright (C) 2018 THE mobilizon'S COPYRIGHT HOLDER
# This file is distributed under the same license as the mobilizon package.
# Automatically generated, 2018.
#
msgid ""
msgstr ""
"Project-Id-Version: mobilizon 0.1.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-10-24 16:25+0200\n"
"PO-Revision-Date: 2018-10-24 16:25+0200\n"
"Last-Translator: Automatically generated\n"
"Language-Team: none\n"
"Language: fr\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
#: src/components/Account/Register.vue:70
msgid "A validation email was sent to %{email}"
msgstr ""
#: src/components/Account/Register.vue:71
msgid "Before you can login, you need to click on the link inside it to validate your account"
msgstr ""
#: src/components/Home.vue:14
msgid "Register"
msgstr "S'inscrire"

View File

@@ -0,0 +1 @@
{"en_US":{"A validation email was sent to %{email}":"A validation email was sent to %{email}","Before you can login, you need to click on the link inside it to validate your account":"Before you can login, you need to click on the link inside it to validate your account","Register":"Register"},"fr_FR":{"Register":"S'inscrire"}}

View File

@@ -5,60 +5,37 @@ import Vue from 'vue';
import VueMarkdown from 'vue-markdown';
import Vuetify from 'vuetify';
import moment from 'moment';
import VuexI18n from 'vuex-i18n';
import GetTextPlugin from 'vue-gettext';
import 'material-design-icons/iconfont/material-icons.css';
import 'vuetify/dist/vuetify.min.css';
import App from './App.vue';
import router from './router';
import store from './store';
import translations from './i18n';
import auth from './auth';
import App from '@/App.vue';
import router from '@/router';
// import store from './store';
import translations from '@/i18n/translations.json';
import { createProvider } from './vue-apollo';
Vue.config.productionTip = false;
Vue.use(VueMarkdown);
Vue.use(Vuetify);
let language = window.navigator.userLanguage || window.navigator.language;
const language = window.navigator.userLanguage || window.navigator.language;
moment.locale(language);
Vue.filter('formatDate', value => (value ? moment(String(value)).format('LLLL') : null));
Vue.filter('formatDay', value => (value ? moment(String(value)).format('LL') : null));
if (!(language in translations)) {
[language] = language.split('-', 1);
}
Vue.use(VuexI18n.plugin, store);
Object.entries(translations).forEach((key) => {
Vue.i18n.add(key[0], key[1]);
Vue.use(GetTextPlugin, {
translations,
defaultLanguage: 'en_US',
});
Vue.i18n.set(language);
Vue.i18n.fallback('en');
router.beforeEach((to, from, next) => {
if (to.matched.some(record => record.meta.requiredAuth) && !store.state.user) {
next({
name: 'Login',
query: { redirect: to.fullPath },
});
} else {
next();
}
});
auth.getUser(store, () => {}, (error) => {
console.warn(error);
});
console.log('store', store);
Vue.config.language = language.replace('-', '_');
/* eslint-disable no-new */
new Vue({
el: '#app',
router,
store,
template: '<App/>',
apolloProvider: createProvider(),
components: { App },
});

View File

@@ -1,44 +0,0 @@
import Vue from 'vue';
import Vuex from 'vuex';
import { LOGIN_USER, LOGOUT_USER, LOAD_USER, CHANGE_ACTOR } from './mutation-types';
const state = {
isLogged: !!localStorage.getItem('token'),
user: false,
actor: false,
defaultActor: localStorage.getItem('defaultActor') || null,
};
/* eslint-disable */
const mutations = {
[LOGIN_USER](state, user) {
state.isLogged = true;
state.user = user;
},
[LOAD_USER](state, user) {
state.user = user;
},
[LOGOUT_USER](state) {
state.isLogged = false;
state.user = null;
},
[CHANGE_ACTOR](state, actor) {
state.actor = actor;
state.defaultActor = actor.username;
}
};
/* eslint-enable */
Vue.use(Vuex);
const store = new Vuex.Store({ state, mutations });
store.subscribe((mutation, localState) => {
if (mutation === CHANGE_ACTOR) {
localStorage.setItem('defaultActor', localState.actor.username);
}
});
export default store;

View File

@@ -1,4 +0,0 @@
export const LOGIN_USER = 'LOGIN_USER';
export const LOAD_USER = 'LOAD_USER';
export const LOGOUT_USER = 'LOGOUT_USER';
export const CHANGE_ACTOR = 'CHANGE_ACTOR';

135
js/src/vue-apollo.js Normal file
View File

@@ -0,0 +1,135 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { ApolloLink } from 'apollo-link';
import { InMemoryCache, IntrospectionFragmentMatcher } from 'apollo-cache-inmemory';
import { createLink } from 'apollo-absinthe-upload-link';
import { createApolloClient, restartWebsockets } from 'vue-cli-plugin-apollo/graphql-client';
import { AUTH_TOKEN } from './constants';
// Install the vue plugin
Vue.use(VueApollo);
// Http endpoint
const httpEndpoint = process.env.VUE_APP_GRAPHQL_HTTP || 'http://localhost:4000/api';
const fragmentMatcher = new IntrospectionFragmentMatcher({
introspectionQueryResultData: {
__schema: {
types: [
{
kind: 'UNION',
name: 'SearchResult',
possibleTypes: [
{ name: 'Event' },
{ name: 'Actor' },
],
}, // this is an example, put your INTERFACE and UNION kinds here!
],
},
},
});
const cache = new InMemoryCache({ fragmentMatcher });
const authMiddleware = new ApolloLink((operation, forward) => {
// add the authorization to the headers
const token = localStorage.getItem(AUTH_TOKEN);
operation.setContext({
headers: {
authorization: token ? `Bearer ${token}` : null,
},
});
return forward(operation);
});
const uploadLink = createLink({
uri: httpEndpoint,
});
// const link = ApolloLink.from([
// uploadLink,
// authMiddleware,
// HttpLink,
// ]);
const link = authMiddleware.concat(uploadLink);
// Config
const defaultOptions = {
// You can use `https` for secure connection (recommended in production)
httpEndpoint,
// You can use `wss` for secure connection (recommended in production)
// Use `null` to disable subscriptions
// wsEndpoint: process.env.VUE_APP_GRAPHQL_WS || 'ws://localhost:4000/graphql',
// LocalStorage token
tokenName: AUTH_TOKEN,
// Enable Automatic Query persisting with Apollo Engine
persisting: false,
// Use websockets for everything (no HTTP)
// You need to pass a `wsEndpoint` for this to work
websocketsOnly: false,
// Is being rendered on the server?
ssr: false,
cache,
link,
defaultHttpLink: false,
};
// Call this in the Vue app file
export function createProvider(options = {}) {
// Create apollo client
const { apolloClient, wsClient } = createApolloClient({
...defaultOptions,
...options,
});
apolloClient.wsClient = wsClient;
// Create vue apollo provider
const apolloProvider = new VueApollo({
defaultClient: apolloClient,
link,
cache,
connectToDevTools: true,
defaultOptions: {
$query: {
// fetchPolicy: 'cache-and-network',
},
},
errorHandler(error) {
// eslint-disable-next-line no-console
console.log('%cError', 'background: red; color: white; padding: 2px 4px; border-radius: 3px; font-weight: bold;', error.message);
},
});
return apolloProvider;
}
// Manually call this when user log in
export async function onLogin(apolloClient, token) {
if (typeof localStorage !== 'undefined' && token) {
localStorage.setItem(AUTH_TOKEN, token);
}
if (apolloClient.wsClient) restartWebsockets(apolloClient.wsClient);
try {
await apolloClient.resetStore();
} catch (e) {
// eslint-disable-next-line no-console
console.log('%cError on cache reset (login)', 'color: orange;', e.message);
}
}
// Manually call this when user log out
export async function onLogout(apolloClient) {
if (typeof localStorage !== 'undefined') {
localStorage.removeItem(AUTH_TOKEN);
}
if (apolloClient.wsClient) restartWebsockets(apolloClient.wsClient);
try {
await apolloClient.resetStore();
} catch (e) {
// eslint-disable-next-line no-console
console.log('%cError on cache reset (logout)', 'color: orange;', e.message);
}
}

View File

@@ -1,8 +1,8 @@
module.exports = {
env: {
mocha: true
mocha: true,
},
rules: {
'import/no-extraneous-dependencies': 'off'
}
}
'import/no-extraneous-dependencies': 'off',
},
};