Replace Vuetify with Bulma
Signed-off-by: Thomas Citharel <tcit@tcit.fr> Remove vuetify and add Bulma Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
parent
759a740625
commit
90fd0ff6b6
|
@ -37,7 +37,7 @@ translations: ./$(OUTPUT_DIR)/translations.json
|
||||||
mkdir -p $(dir $@)
|
mkdir -p $(dir $@)
|
||||||
which gettext-extract
|
which gettext-extract
|
||||||
# Extract gettext strings from templates files and create a POT dictionary template.
|
# Extract gettext strings from templates files and create a POT dictionary template.
|
||||||
gettext-extract --attribute v-translate --quiet --output $@ $(GETTEXT_HTML_SOURCES)
|
gettext-extract --attribute v-translate --quiet --parseScript false --output $@ $(GETTEXT_HTML_SOURCES)
|
||||||
# Extract gettext strings from JavaScript files.
|
# Extract gettext strings from JavaScript files.
|
||||||
xgettext --language=JavaScript --keyword=npgettext:1c,2,3 \
|
xgettext --language=JavaScript --keyword=npgettext:1c,2,3 \
|
||||||
--from-code=utf-8 --join-existing --no-wrap \
|
--from-code=utf-8 --join-existing --no-wrap \
|
||||||
|
|
936
js/package-lock.json
generated
936
js/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -8,7 +8,8 @@
|
||||||
"analyze-bundle": "npm run build -- --report-json && webpack-bundle-analyzer ../priv/static/report.json",
|
"analyze-bundle": "npm run build -- --report-json && webpack-bundle-analyzer ../priv/static/report.json",
|
||||||
"dev": "vue-cli-service serve",
|
"dev": "vue-cli-service serve",
|
||||||
"test:e2e": "vue-cli-service test:e2e",
|
"test:e2e": "vue-cli-service test:e2e",
|
||||||
"test:unit": "vue-cli-service test:unit"
|
"test:unit": "vue-cli-service test:unit",
|
||||||
|
"prepare": "patch-package"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"apollo-absinthe-upload-link": "^1.4.0",
|
"apollo-absinthe-upload-link": "^1.4.0",
|
||||||
|
@ -17,6 +18,7 @@
|
||||||
"apollo-link": "^1.2.6",
|
"apollo-link": "^1.2.6",
|
||||||
"apollo-link-http": "^1.5.9",
|
"apollo-link-http": "^1.5.9",
|
||||||
"apollo-link-state": "^0.4.2",
|
"apollo-link-state": "^0.4.2",
|
||||||
|
"buefy": "^0.7.1",
|
||||||
"easygettext": "^2.7.0",
|
"easygettext": "^2.7.0",
|
||||||
"graphql": "^14.1.1",
|
"graphql": "^14.1.1",
|
||||||
"graphql-tag": "^2.10.1",
|
"graphql-tag": "^2.10.1",
|
||||||
|
@ -32,8 +34,6 @@
|
||||||
"vue-markdown": "^2.2.4",
|
"vue-markdown": "^2.2.4",
|
||||||
"vue-property-decorator": "^7.2.0",
|
"vue-property-decorator": "^7.2.0",
|
||||||
"vue-router": "^3.0.2",
|
"vue-router": "^3.0.2",
|
||||||
"vuetify": "^1.3.9",
|
|
||||||
"vuetify-google-autocomplete": "^2.0.0-beta.5",
|
|
||||||
"vuex": "^3.0.1"
|
"vuex": "^3.0.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
@ -51,6 +51,7 @@
|
||||||
"chai": "^4.2.0",
|
"chai": "^4.2.0",
|
||||||
"dotenv-webpack": "^1.5.7",
|
"dotenv-webpack": "^1.5.7",
|
||||||
"node-sass": "^4.10.0",
|
"node-sass": "^4.10.0",
|
||||||
|
"patch-package": "^5.1.1",
|
||||||
"sass-loader": "^7.1.0",
|
"sass-loader": "^7.1.0",
|
||||||
"tslint-config-airbnb": "^5.11.1",
|
"tslint-config-airbnb": "^5.11.1",
|
||||||
"typescript": "^3.0.0",
|
"typescript": "^3.0.0",
|
||||||
|
|
41
js/patches/easygettext+2.7.0.patch
Normal file
41
js/patches/easygettext+2.7.0.patch
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
patch-package
|
||||||
|
--- a/node_modules/easygettext/src/extract-cli.js
|
||||||
|
+++ b/node_modules/easygettext/src/extract-cli.js
|
||||||
|
@@ -22,9 +22,12 @@ const endDelimiter = argv.endDelimiter === undefined ? constants.DEFAULT_DELIMIT
|
||||||
|
const extraAttribute = argv.attribute || false;
|
||||||
|
const extraFilter = argv.filter || false;
|
||||||
|
const filterPrefix = argv.filterPrefix || constants.DEFAULT_FILTER_PREFIX;
|
||||||
|
+const parseScript = argv.parseScript === undefined ? true : argv.parseScript === 'true';
|
||||||
|
|
||||||
|
if (!quietMode && (!files || files.length === 0)) {
|
||||||
|
- console.log('Usage:\n\tgettext-extract [--attribute EXTRA-ATTRIBUTE] [--filterPrefix FILTER-PREFIX] [--output OUTFILE] <FILES>');
|
||||||
|
+ console.log(
|
||||||
|
+ 'Usage:\n\tgettext-extract [--attribute EXTRA-ATTRIBUTE] [--filterPrefix FILTER-PREFIX] [--parseScript BOOLEAN] [--output OUTFILE] <FILES>',
|
||||||
|
+ );
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@@ -54,7 +57,7 @@ const extractor = new extract.Extractor({
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
-files.forEach(function(filename) {
|
||||||
|
+files.forEach(function (filename) {
|
||||||
|
let file = filename;
|
||||||
|
const ext = file.split('.').pop();
|
||||||
|
if (ALLOWED_EXTENSIONS.indexOf(ext) === -1) {
|
||||||
|
@@ -63,9 +66,13 @@ files.forEach(function(filename) {
|
||||||
|
}
|
||||||
|
console.log(`[${PROGRAM_NAME}] extracting: '${filename}`);
|
||||||
|
try {
|
||||||
|
- let data = fs.readFileSync(file, {encoding: 'utf-8'}).toString();
|
||||||
|
+ let data = fs.readFileSync(file, { encoding: 'utf-8' }).toString();
|
||||||
|
extractor.parse(file, extract.preprocessTemplate(data, ext));
|
||||||
|
|
||||||
|
+ if (!parseScript) {
|
||||||
|
+ return;
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
if (ext !== 'js') {
|
||||||
|
data = extract.preprocessScriptTags(data, ext);
|
||||||
|
}
|
|
@ -1,17 +1,21 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html class="has-navbar-fixed-top">
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
<head>
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||||
<title>mobilizon</title>
|
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
||||||
</head>
|
<link rel="stylesheet" href="//cdn.materialdesignicons.com/2.5.94/css/materialdesignicons.min.css">
|
||||||
<body>
|
<title>mobilizon</title>
|
||||||
<noscript>
|
</head>
|
||||||
<strong>We're sorry but mobilizon doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
|
||||||
</noscript>
|
<body>
|
||||||
<div id="app"></div>
|
<noscript>
|
||||||
<!-- built files will be auto injected -->
|
<strong>We're sorry but mobilizon doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
||||||
</body>
|
</noscript>
|
||||||
</html>
|
<div id="app"></div>
|
||||||
|
<!-- built files will be auto injected -->
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
222
js/src/App.vue
222
js/src/App.vue
|
@ -1,152 +1,19 @@
|
||||||
<template>
|
<template>
|
||||||
<v-app id="libre-event">
|
<div id="mobilizon">
|
||||||
<v-navigation-drawer
|
<NavBar></NavBar>
|
||||||
light
|
<main class="container">
|
||||||
clipped
|
<router-view></router-view>
|
||||||
fixed
|
</main>
|
||||||
app
|
<footer class="footer">
|
||||||
v-model="drawer"
|
<div class="content has-text-centered">
|
||||||
enable-resize-watcher
|
<span
|
||||||
>
|
v-translate="{
|
||||||
<v-list dense>
|
date: new Date().getFullYear(),
|
||||||
<v-list-group
|
}"
|
||||||
value="false"
|
>© The Mobilizon Contributors %{date} - Made with Elixir, Phoenix, VueJS & with some love and some weeks</span>
|
||||||
>
|
</div>
|
||||||
<v-list-tile avatar v-if="actor" slot="activator">
|
</footer>
|
||||||
<v-list-tile-avatar>
|
</div>
|
||||||
<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-list-tile-avatar>
|
|
||||||
|
|
||||||
<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="actor">
|
|
||||||
<v-list-tile-avatar>
|
|
||||||
<img
|
|
||||||
class="img-circle elevation-7 mb-1"
|
|
||||||
src="https://picsum.photos/125/125/"
|
|
||||||
>
|
|
||||||
</v-list-tile-avatar>
|
|
||||||
|
|
||||||
<v-list-tile-content>
|
|
||||||
<v-list-tile-title>Autre identité</v-list-tile-title>
|
|
||||||
</v-list-tile-content>
|
|
||||||
</v-list-tile>
|
|
||||||
|
|
||||||
<v-list-tile @click="$router.push({ name: 'Identities' })">
|
|
||||||
<v-list-tile-action>
|
|
||||||
<v-icon>group</v-icon>
|
|
||||||
</v-list-tile-action>
|
|
||||||
<v-list-tile-content>
|
|
||||||
<v-list-tile-title>Identities</v-list-tile-title>
|
|
||||||
</v-list-tile-content>
|
|
||||||
</v-list-tile>
|
|
||||||
</v-list-group>
|
|
||||||
<template v-for="(item, i) in items" v-if="showMenuItem(item.role)">
|
|
||||||
<v-layout
|
|
||||||
row
|
|
||||||
v-if="item.heading"
|
|
||||||
align-center
|
|
||||||
:key="i"
|
|
||||||
>
|
|
||||||
<v-flex xs6>
|
|
||||||
<v-subheader v-if="item.heading">
|
|
||||||
{{ item.heading }}
|
|
||||||
</v-subheader>
|
|
||||||
</v-flex>
|
|
||||||
<v-flex xs6 class="text-xs-center">
|
|
||||||
<a href="#!" class="body-2 black--text">EDIT</a>
|
|
||||||
</v-flex>
|
|
||||||
</v-layout>
|
|
||||||
<v-list-tile v-bind:key="item.route" v-else @click="$router.push({ name: item.route })">
|
|
||||||
<v-list-tile-action>
|
|
||||||
<v-icon>{{ item.icon }}</v-icon>
|
|
||||||
</v-list-tile-action>
|
|
||||||
<v-list-tile-content>
|
|
||||||
<v-list-tile-title>
|
|
||||||
{{ item.text }}
|
|
||||||
</v-list-tile-title>
|
|
||||||
</v-list-tile-content>
|
|
||||||
</v-list-tile>
|
|
||||||
</template>
|
|
||||||
</v-list>
|
|
||||||
</v-navigation-drawer>
|
|
||||||
<NavBar v-bind="{toggleDrawer}"></NavBar>
|
|
||||||
<v-content>
|
|
||||||
<v-container fluid fill-height :class="{'px-0': $vuetify.breakpoint.xsOnly }">
|
|
||||||
<v-layout xs12>
|
|
||||||
<transition name="router">
|
|
||||||
<router-view></router-view>
|
|
||||||
</transition>
|
|
||||||
</v-layout>
|
|
||||||
</v-container>
|
|
||||||
</v-content>
|
|
||||||
<v-speed-dial
|
|
||||||
v-model="fab"
|
|
||||||
bottom
|
|
||||||
right
|
|
||||||
fixed
|
|
||||||
direction="top"
|
|
||||||
open-on-hover
|
|
||||||
transition="scale-transition"
|
|
||||||
v-if="currentUser"
|
|
||||||
>
|
|
||||||
<v-btn
|
|
||||||
slot="activator"
|
|
||||||
v-model="fab"
|
|
||||||
color="blue darken-2"
|
|
||||||
dark
|
|
||||||
fab
|
|
||||||
>
|
|
||||||
<v-icon>add</v-icon>
|
|
||||||
<v-icon>close</v-icon>
|
|
||||||
</v-btn>
|
|
||||||
<v-btn
|
|
||||||
fab
|
|
||||||
dark
|
|
||||||
small
|
|
||||||
color="pink"
|
|
||||||
@click="$router.push({name: 'CreateEvent'})"
|
|
||||||
>
|
|
||||||
<v-icon>event</v-icon>
|
|
||||||
</v-btn>
|
|
||||||
<v-btn
|
|
||||||
fab
|
|
||||||
dark
|
|
||||||
small
|
|
||||||
color="purple"
|
|
||||||
@click="$router.push({name: 'CreateGroup'})"
|
|
||||||
>
|
|
||||||
<v-icon>group</v-icon>
|
|
||||||
</v-btn>
|
|
||||||
</v-speed-dial>
|
|
||||||
<v-footer class="indigo" app>
|
|
||||||
<span
|
|
||||||
class="white--text"
|
|
||||||
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"
|
|
||||||
:error="true"
|
|
||||||
v-model="error.show"
|
|
||||||
>
|
|
||||||
{{ error.text }}
|
|
||||||
<v-btn dark flat @click.native="error.show = false">Close</v-btn>
|
|
||||||
</v-snackbar>
|
|
||||||
</v-app>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
@ -163,31 +30,40 @@ import { ICurrentUser } from '@/types/current-user.model'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
NavBar,
|
NavBar
|
||||||
},
|
}
|
||||||
})
|
})
|
||||||
export default class App extends Vue {
|
export default class App extends Vue {
|
||||||
drawer = false;
|
drawer = false;
|
||||||
fab = false;
|
fab = false;
|
||||||
items = [
|
items = [
|
||||||
{
|
{
|
||||||
icon: 'poll', text: 'Events', route: 'EventList', role: null,
|
icon: "poll",
|
||||||
|
text: "Events",
|
||||||
|
route: "EventList",
|
||||||
|
role: null
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: 'group', text: 'Groups', route: 'GroupList', role: null,
|
icon: "group",
|
||||||
|
text: "Groups",
|
||||||
|
route: "GroupList",
|
||||||
|
role: null
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: 'content_copy', text: 'Categories', route: 'CategoryList', role: 'ROLE_ADMIN',
|
icon: "content_copy",
|
||||||
|
text: "Categories",
|
||||||
|
route: "CategoryList",
|
||||||
|
role: "ROLE_ADMIN"
|
||||||
},
|
},
|
||||||
{ icon: 'settings', text: 'Settings', role: 'ROLE_USER' },
|
{ icon: "settings", text: "Settings", role: "ROLE_USER" },
|
||||||
{ icon: 'chat_bubble', text: 'Send feedback', role: 'ROLE_USER' },
|
{ icon: "chat_bubble", text: "Send feedback", role: "ROLE_USER" },
|
||||||
{ icon: 'help', text: 'Help', role: null },
|
{ icon: "help", text: "Help", role: null },
|
||||||
{ icon: 'phonelink', text: 'App downloads', role: null },
|
{ icon: "phonelink", text: "App downloads", role: null }
|
||||||
];
|
];
|
||||||
error = {
|
error = {
|
||||||
timeout: 3000,
|
timeout: 3000,
|
||||||
show: false,
|
show: false,
|
||||||
text: '',
|
text: ""
|
||||||
};
|
};
|
||||||
currentUser!: ICurrentUser;
|
currentUser!: ICurrentUser;
|
||||||
|
|
||||||
|
@ -199,21 +75,21 @@ export default class App extends Vue {
|
||||||
|
|
||||||
get displayed_name () {
|
get displayed_name () {
|
||||||
// FIXME: load actor
|
// FIXME: load actor
|
||||||
return 'no implemented';
|
return "no implemented";
|
||||||
// return this.actor.display_name === null ? this.actor.username : this.actor.display_name
|
// return this.actor.display_name === null ? this.actor.username : this.actor.display_name
|
||||||
}
|
}
|
||||||
|
|
||||||
showMenuItem (elem) {
|
showMenuItem(elem) {
|
||||||
// FIXME: load actor
|
// FIXME: load actor
|
||||||
return false;
|
return false;
|
||||||
// return elem !== null && this.user && this.user.roles !== undefined ? this.user.roles.includes(elem) : true
|
// return elem !== null && this.user && this.user.roles !== undefined ? this.user.roles.includes(elem) : true
|
||||||
}
|
}
|
||||||
|
|
||||||
getUser () {
|
getUser (): ICurrentUser|false {
|
||||||
return this.currentUser.id ? this.currentUser : false;
|
return this.currentUser.id ? this.currentUser : false;
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleDrawer () {
|
toggleDrawer() {
|
||||||
this.drawer = !this.drawer;
|
this.drawer = !this.drawer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -236,16 +112,18 @@ export default class App extends Vue {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.router-enter-active, .router-leave-active {
|
.router-enter-active,
|
||||||
transition-property: opacity;
|
.router-leave-active {
|
||||||
transition-duration: .25s;
|
transition-property: opacity;
|
||||||
}
|
transition-duration: 0.25s;
|
||||||
|
}
|
||||||
|
|
||||||
.router-enter-active {
|
.router-enter-active {
|
||||||
transition-delay: .25s;
|
transition-delay: 0.25s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.router-enter, .router-leave-active {
|
.router-enter,
|
||||||
opacity: 0
|
.router-leave-active {
|
||||||
}
|
opacity: 0;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,213 +0,0 @@
|
||||||
<template>
|
|
||||||
<v-layout row>
|
|
||||||
<v-flex xs12 sm6 offset-sm3>
|
|
||||||
<v-progress-circular v-if="$apollo.loading" indeterminate color="primary"></v-progress-circular>
|
|
||||||
<v-card v-if="actor">
|
|
||||||
<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="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="actor.id === actor.id">
|
|
||||||
<v-list-tile-title>User logout</v-list-tile-title>
|
|
||||||
</v-list-tile>
|
|
||||||
<v-list-tile @click="deleteAccount()" v-if="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.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="actor.avatarUrl"
|
|
||||||
>
|
|
||||||
</v-avatar>
|
|
||||||
</div>
|
|
||||||
<v-container fluid grid-list-lg>
|
|
||||||
<v-layout row>
|
|
||||||
<v-flex xs7>
|
|
||||||
<div class="headline">{{ actor.name }}</div>
|
|
||||||
<div><span class="subheading">@{{ actor.preferredUsername }}<span v-if="actor.domain">@{{ actor.domain }}</span></span>
|
|
||||||
</div>
|
|
||||||
<v-card-text v-if="actor.description" v-html="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="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-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-container fluid grid-list-md v-if="actor.organizedEvents && actor.organizedEvents.length > 0">
|
|
||||||
<v-subheader>Organized events</v-subheader>
|
|
||||||
<v-layout row wrap>
|
|
||||||
<v-flex v-for="event in 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>
|
|
||||||
</v-flex>
|
|
||||||
</v-layout>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { FETCH_ACTOR } from '@/graphql/actor';
|
|
||||||
import { Component, Prop, Vue, Watch } from 'vue-property-decorator';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
apollo: {
|
|
||||||
actor: {
|
|
||||||
query: FETCH_ACTOR,
|
|
||||||
variables() {
|
|
||||||
return {
|
|
||||||
name: this.$route.params.name,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
export default class Account extends Vue {
|
|
||||||
@Prop({ type: String, required: true }) name!: string;
|
|
||||||
|
|
||||||
actor = null;
|
|
||||||
|
|
||||||
// call again the method if the route changes
|
|
||||||
@Watch('$route')
|
|
||||||
onRouteChange() {
|
|
||||||
// this.fetchData()
|
|
||||||
}
|
|
||||||
|
|
||||||
logoutUser() {
|
|
||||||
// TODO : implement logout
|
|
||||||
this.$router.push({ name: 'Home' });
|
|
||||||
}
|
|
||||||
|
|
||||||
nl2br(text) {
|
|
||||||
return text.replace(/(?:\r\n|\r|\n)/g, '<br>');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
|
@ -1,133 +0,0 @@
|
||||||
<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-toolbar dark color="primary">
|
|
||||||
<v-toolbar-title>Identities</v-toolbar-title>
|
|
||||||
</v-toolbar>
|
|
||||||
<v-card-text>
|
|
||||||
<v-list two-line>
|
|
||||||
<v-list-tile
|
|
||||||
v-for="actor in actors"
|
|
||||||
:key="actor.id"
|
|
||||||
avatar
|
|
||||||
@click="$router.push({ name: 'Account', params: { name: actor.username } })"
|
|
||||||
>
|
|
||||||
<v-list-tile-action>
|
|
||||||
<v-icon v-if="defaultActor === actor.username" color="pink">star</v-icon>
|
|
||||||
</v-list-tile-action>
|
|
||||||
|
|
||||||
<v-list-tile-content>
|
|
||||||
<v-list-tile-title v-text="actor.username"></v-list-tile-title>
|
|
||||||
<v-list-tile-sub-title v-if="actor.display_name" v-text="actor.display_name"></v-list-tile-sub-title>
|
|
||||||
</v-list-tile-content>
|
|
||||||
|
|
||||||
<v-list-tile-avatar>
|
|
||||||
<img :src="actor.avatar">
|
|
||||||
</v-list-tile-avatar>
|
|
||||||
</v-list-tile>
|
|
||||||
</v-list>
|
|
||||||
<v-divider v-if="showForm"></v-divider>
|
|
||||||
<v-form v-if="showForm">
|
|
||||||
<v-text-field
|
|
||||||
label="Username"
|
|
||||||
required
|
|
||||||
type="text"
|
|
||||||
v-model="newActor.preferred_username"
|
|
||||||
:rules="[rules.required]"
|
|
||||||
:error="this.state.username.status"
|
|
||||||
:error-messages="this.state.username.msg"
|
|
||||||
:suffix="this.host()"
|
|
||||||
hint="You will be able to create more identities once registered"
|
|
||||||
persistent-hint
|
|
||||||
>
|
|
||||||
</v-text-field>
|
|
||||||
<v-textarea
|
|
||||||
name="input-7-1"
|
|
||||||
label="Profile description"
|
|
||||||
hint="Will be displayed publicly on your profile"
|
|
||||||
></v-textarea>
|
|
||||||
</v-form>
|
|
||||||
<v-btn
|
|
||||||
color="pink"
|
|
||||||
dark
|
|
||||||
absolute
|
|
||||||
bottom
|
|
||||||
right
|
|
||||||
fab
|
|
||||||
@click="toggleForm()"
|
|
||||||
>
|
|
||||||
<v-icon>{{ showForm ? 'check' : 'add' }}</v-icon>
|
|
||||||
</v-btn>
|
|
||||||
</v-card-text>
|
|
||||||
</v-card>
|
|
||||||
</v-flex>
|
|
||||||
</v-layout>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { Component, Vue } from 'vue-property-decorator';
|
|
||||||
|
|
||||||
@Component
|
|
||||||
export default class Identities extends Vue {
|
|
||||||
actors = [];
|
|
||||||
newActor = {
|
|
||||||
preferred_username: '',
|
|
||||||
summary: '',
|
|
||||||
};
|
|
||||||
loading = true;
|
|
||||||
showForm = false;
|
|
||||||
rules = {
|
|
||||||
required: value => !!value || 'Required.',
|
|
||||||
};
|
|
||||||
state = {
|
|
||||||
username: {
|
|
||||||
status: false,
|
|
||||||
msg: [],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
created() {
|
|
||||||
this.fetchData();
|
|
||||||
}
|
|
||||||
|
|
||||||
fetchData() {
|
|
||||||
// Implements eventFetch
|
|
||||||
// eventFetch('/user', this.$store)
|
|
||||||
// .then(response => response.json())
|
|
||||||
// .then((response) => {
|
|
||||||
// this.actors = response.data.actors;
|
|
||||||
// this.loading = false;
|
|
||||||
// });
|
|
||||||
}
|
|
||||||
|
|
||||||
sendData() {
|
|
||||||
this.loading = true;
|
|
||||||
this.showForm = false;
|
|
||||||
|
|
||||||
// Implements eventFetch
|
|
||||||
// eventFetch('/actors', this.$store, {
|
|
||||||
// method: 'POST',
|
|
||||||
// body: JSON.stringify({ actor: this.newActor }),
|
|
||||||
// })
|
|
||||||
// .then(response => response.json())
|
|
||||||
// .then((response) => {
|
|
||||||
// this.actors.push(response.data);
|
|
||||||
// this.loading = false;
|
|
||||||
// });
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleForm() {
|
|
||||||
if (this.showForm === true) {
|
|
||||||
this.sendData();
|
|
||||||
} else {
|
|
||||||
this.showForm = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
host() {
|
|
||||||
return `@${window.location.host}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
|
@ -1,151 +0,0 @@
|
||||||
<template>
|
|
||||||
<v-container fluid fill-height>
|
|
||||||
<v-layout align-center justify-center>
|
|
||||||
<v-flex xs12 sm8 md4>
|
|
||||||
<v-card class="elevation-12">
|
|
||||||
<v-toolbar dark color="primary">
|
|
||||||
<v-toolbar-title>Login</v-toolbar-title>
|
|
||||||
<v-spacer></v-spacer>
|
|
||||||
<v-tooltip bottom>
|
|
||||||
<v-btn
|
|
||||||
slot="activator"
|
|
||||||
:to="{ name: 'Register', params: { email: this.credentials.email, password: this.credentials.password } }"
|
|
||||||
>
|
|
||||||
<!-- <v-icon large>login</v-icon> -->
|
|
||||||
<span>Register</span>
|
|
||||||
</v-btn>
|
|
||||||
<span>Register</span>
|
|
||||||
</v-tooltip>
|
|
||||||
</v-toolbar>
|
|
||||||
<v-card-text>
|
|
||||||
<div class="text-xs-center">
|
|
||||||
<v-avatar size="80px">
|
|
||||||
<transition name="avatar">
|
|
||||||
<component :is="validEmail()" v-bind="{email: credentials.email}"></component>
|
|
||||||
<!-- <v-gravatar :email="credentials.email" default-img="mp" v-if="validEmail()"/>
|
|
||||||
<avatar v-else></avatar> -->
|
|
||||||
</transition>
|
|
||||||
</v-avatar>
|
|
||||||
</div>
|
|
||||||
<v-form @submit="loginAction" v-if="!validationSent">
|
|
||||||
<v-text-field
|
|
||||||
label="Email"
|
|
||||||
required
|
|
||||||
type="text"
|
|
||||||
v-model="credentials.email"
|
|
||||||
:rules="[rules.required, rules.email]"
|
|
||||||
>
|
|
||||||
</v-text-field>
|
|
||||||
<v-text-field
|
|
||||||
label="password"
|
|
||||||
required
|
|
||||||
type="password"
|
|
||||||
v-model="credentials.password"
|
|
||||||
:rules="[rules.required]"
|
|
||||||
>
|
|
||||||
</v-text-field>
|
|
||||||
<v-btn @click="loginAction" color="blue">Login</v-btn>
|
|
||||||
<router-link :to="{ name: 'SendPasswordReset', params: { email: credentials.email } }">Password forgotten ?</router-link>
|
|
||||||
</v-form>
|
|
||||||
<div v-else>
|
|
||||||
<h2>{{ $t('registration.form.validation_sent', { email: credentials.email }) }}</h2>
|
|
||||||
<b-alert show variant="info">{{ $t('registration.form.validation_sent_info') }}</b-alert>
|
|
||||||
</div>
|
|
||||||
</v-card-text>
|
|
||||||
</v-card>
|
|
||||||
</v-flex>
|
|
||||||
</v-layout>
|
|
||||||
</v-container>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
|
|
||||||
import Gravatar from 'vue-gravatar';
|
|
||||||
import RegisterAvatar from './RegisterAvatar.vue';
|
|
||||||
import { Component, Prop, Vue } from 'vue-property-decorator';
|
|
||||||
import { LOGIN } from '@/graphql/auth';
|
|
||||||
import { validateEmailField, validateRequiredField } from '@/utils/validators';
|
|
||||||
import { saveUserData } from '@/utils/auth';
|
|
||||||
import { ILogin } from '@/types/login.model'
|
|
||||||
import { UPDATE_CURRENT_USER_CLIENT } from '@/graphql/user'
|
|
||||||
import { onLogin } from '@/vue-apollo'
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
components: {
|
|
||||||
'v-gravatar': Gravatar,
|
|
||||||
avatar: RegisterAvatar,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
export default class Login extends Vue {
|
|
||||||
@Prop({ type: String, required: false, default: '' }) email!: string;
|
|
||||||
@Prop({ type: String, required: false, default: '' }) password!: string;
|
|
||||||
|
|
||||||
credentials = {
|
|
||||||
email: '',
|
|
||||||
password: '',
|
|
||||||
};
|
|
||||||
validationSent = false;
|
|
||||||
error = {
|
|
||||||
show: false,
|
|
||||||
text: '',
|
|
||||||
timeout: 3000,
|
|
||||||
field: {
|
|
||||||
email: false,
|
|
||||||
password: false,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
rules = {
|
|
||||||
required: validateRequiredField,
|
|
||||||
email: validateEmailField
|
|
||||||
};
|
|
||||||
user: any;
|
|
||||||
|
|
||||||
beforeCreate() {
|
|
||||||
if (this.user) {
|
|
||||||
this.$router.push('/');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mounted() {
|
|
||||||
this.credentials.email = this.email;
|
|
||||||
this.credentials.password = this.password;
|
|
||||||
}
|
|
||||||
|
|
||||||
async loginAction(e: Event) {
|
|
||||||
e.preventDefault();
|
|
||||||
this.error.show = false;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await this.$apollo.mutate<{ login: ILogin }>({
|
|
||||||
mutation: LOGIN,
|
|
||||||
variables: {
|
|
||||||
email: this.credentials.email,
|
|
||||||
password: this.credentials.password,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
saveUserData(result.data.login);
|
|
||||||
|
|
||||||
await this.$apollo.mutate({
|
|
||||||
mutation: UPDATE_CURRENT_USER_CLIENT,
|
|
||||||
variables: {
|
|
||||||
id: result.data.login.user.id,
|
|
||||||
email: this.credentials.email,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
onLogin(this.$apollo);
|
|
||||||
|
|
||||||
this.$router.push({ name: 'Home' });
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
this.error.show = true;
|
|
||||||
this.error.text = err.message;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
validEmail() {
|
|
||||||
return this.rules.email(this.credentials.email) === true ? 'v-gravatar' : 'avatar';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
|
@ -1,123 +0,0 @@
|
||||||
<template>
|
|
||||||
<v-container fluid fill-height>
|
|
||||||
<v-layout align-center justify-center>
|
|
||||||
<v-flex xs12 sm8 md4>
|
|
||||||
<v-card class="elevation-12">
|
|
||||||
<v-toolbar dark color="primary">
|
|
||||||
<v-toolbar-title>Password Reset</v-toolbar-title>
|
|
||||||
</v-toolbar>
|
|
||||||
<v-card-text>
|
|
||||||
<v-alert type="error" :value="state.token.status === false">{{ state.token.msg }}</v-alert>
|
|
||||||
<v-form @submit="resetAction">
|
|
||||||
<v-text-field
|
|
||||||
label="Password"
|
|
||||||
type="password"
|
|
||||||
v-model="credentials.password"
|
|
||||||
required
|
|
||||||
:error="state.password.status"
|
|
||||||
:rules="[rules.required, rules.password_length]"
|
|
||||||
>
|
|
||||||
</v-text-field>
|
|
||||||
<v-text-field
|
|
||||||
label="Password (confirmation)"
|
|
||||||
type="password"
|
|
||||||
v-model="credentials.password_confirmation"
|
|
||||||
required
|
|
||||||
:rules="[rules.required, rules.password_length, rules.password_equal]"
|
|
||||||
:error="state.password_confirmation.status"
|
|
||||||
>
|
|
||||||
</v-text-field>
|
|
||||||
<v-btn type="submit" :disabled="!samePasswords" color="blue">Reset my password</v-btn>
|
|
||||||
</v-form>
|
|
||||||
</v-card-text>
|
|
||||||
</v-card>
|
|
||||||
</v-flex>
|
|
||||||
</v-layout>
|
|
||||||
</v-container>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
|
|
||||||
import { Component, Prop, Vue } from 'vue-property-decorator';
|
|
||||||
import { validateRequiredField } from '@/utils/validators';
|
|
||||||
import { RESET_PASSWORD } from '@/graphql/auth';
|
|
||||||
import { saveUserData } from '@/utils/auth';
|
|
||||||
import { ILogin } from '@/types/login.model'
|
|
||||||
|
|
||||||
@Component
|
|
||||||
export default class PasswordReset extends Vue {
|
|
||||||
@Prop({ type: String, required: true }) token!: string;
|
|
||||||
|
|
||||||
credentials = {
|
|
||||||
password: '',
|
|
||||||
password_confirmation: '',
|
|
||||||
};
|
|
||||||
error = {
|
|
||||||
show: false,
|
|
||||||
};
|
|
||||||
state = {
|
|
||||||
token: {
|
|
||||||
status: null,
|
|
||||||
msg: '',
|
|
||||||
},
|
|
||||||
password: {
|
|
||||||
status: null,
|
|
||||||
msg: '',
|
|
||||||
},
|
|
||||||
password_confirmation: {
|
|
||||||
status: null,
|
|
||||||
msg: '',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
rules = {
|
|
||||||
password_length: value => value.length > 6 || 'Password must be at least 6 characters long',
|
|
||||||
required: validateRequiredField,
|
|
||||||
password_equal: value => value === this.credentials.password || 'Passwords must be the same',
|
|
||||||
};
|
|
||||||
|
|
||||||
get samePasswords() {
|
|
||||||
return this.rules.password_length(this.credentials.password) === true &&
|
|
||||||
this.credentials.password === this.credentials.password_confirmation;
|
|
||||||
}
|
|
||||||
|
|
||||||
async resetAction(e) {
|
|
||||||
this.resetState();
|
|
||||||
this.error.show = false;
|
|
||||||
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await this.$apollo.mutate<{ resetPassword: ILogin}>({
|
|
||||||
mutation: RESET_PASSWORD,
|
|
||||||
variables: {
|
|
||||||
password: this.credentials.password,
|
|
||||||
token: this.token,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
saveUserData(result.data.resetPassword);
|
|
||||||
this.$router.push({ name: 'Home' });
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
this.error.show = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
resetState() {
|
|
||||||
this.state = {
|
|
||||||
token: {
|
|
||||||
status: null,
|
|
||||||
msg: '',
|
|
||||||
},
|
|
||||||
password_confirmation: {
|
|
||||||
status: null,
|
|
||||||
msg: '',
|
|
||||||
},
|
|
||||||
password: {
|
|
||||||
status: null,
|
|
||||||
msg: '',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
|
@ -1,185 +0,0 @@
|
||||||
<template>
|
|
||||||
<v-container fluid fill-height>
|
|
||||||
<v-layout align-center justify-center>
|
|
||||||
<v-flex xs12 sm8 md4>
|
|
||||||
<v-card class="elevation-12">
|
|
||||||
<v-toolbar dark color="primary">
|
|
||||||
<v-toolbar-title>Register</v-toolbar-title>
|
|
||||||
<v-spacer></v-spacer>
|
|
||||||
<v-tooltip bottom>
|
|
||||||
<v-btn
|
|
||||||
slot="activator"
|
|
||||||
:to="{ name: 'Login', params: { email, password } }"
|
|
||||||
>
|
|
||||||
<!-- <v-icon large>login</v-icon> -->
|
|
||||||
<span>Login</span>
|
|
||||||
</v-btn>
|
|
||||||
<span>Login</span>
|
|
||||||
</v-tooltip>
|
|
||||||
</v-toolbar>
|
|
||||||
<v-card-text>
|
|
||||||
<div class="text-xs-center">
|
|
||||||
<v-avatar size="80px">
|
|
||||||
<transition name="avatar">
|
|
||||||
<component :is="validEmail()" v-bind="{email}"></component>
|
|
||||||
<!-- <v-gravatar :email="credentials.email" default-img="mp" v-if="validEmail()"/>
|
|
||||||
<avatar v-else></avatar> -->
|
|
||||||
</transition>
|
|
||||||
</v-avatar>
|
|
||||||
</div>
|
|
||||||
<v-form @submit="submit()" v-if="!validationSent">
|
|
||||||
<v-text-field
|
|
||||||
label="Username"
|
|
||||||
required
|
|
||||||
type="text"
|
|
||||||
v-model="username"
|
|
||||||
:rules="[rules.required]"
|
|
||||||
:error="state.username.status"
|
|
||||||
:error-messages="state.username.msg"
|
|
||||||
:suffix="host()"
|
|
||||||
hint="You will be able to create more identities once registered"
|
|
||||||
persistent-hint
|
|
||||||
>
|
|
||||||
</v-text-field>
|
|
||||||
<v-text-field
|
|
||||||
label="Email"
|
|
||||||
required
|
|
||||||
type="email"
|
|
||||||
ref="email"
|
|
||||||
v-model="email"
|
|
||||||
:rules="[rules.required, rules.email]"
|
|
||||||
:error="state.email.status"
|
|
||||||
:error-messages="state.email.msg"
|
|
||||||
>
|
|
||||||
</v-text-field>
|
|
||||||
<v-text-field
|
|
||||||
label="Password"
|
|
||||||
required
|
|
||||||
:type="showPassword ? 'text' : 'password'"
|
|
||||||
v-model="password"
|
|
||||||
:rules="[rules.required, rules.password_length]"
|
|
||||||
: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="submit()" color="primary">Register</v-btn>
|
|
||||||
<router-link :to="{ name: 'ResendConfirmation', params: { email }}">Didn't receive the instructions ?</router-link>
|
|
||||||
</v-form>
|
|
||||||
<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>
|
|
||||||
</v-flex>
|
|
||||||
</v-layout>
|
|
||||||
</v-container>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import Gravatar from 'vue-gravatar';
|
|
||||||
import RegisterAvatar from './RegisterAvatar.vue';
|
|
||||||
import { CREATE_USER } from '@/graphql/user';
|
|
||||||
import { Component, Prop, Vue } from 'vue-property-decorator';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
components: {
|
|
||||||
'v-gravatar': Gravatar,
|
|
||||||
avatar: RegisterAvatar,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
export default class Register extends Vue {
|
|
||||||
@Prop({ type: String, required: false, default: '' }) default_email!: string;
|
|
||||||
@Prop({ type: String, required: false, default: '' }) default_password!: string;
|
|
||||||
|
|
||||||
username = '';
|
|
||||||
email = this.default_email;
|
|
||||||
password = this.default_password;
|
|
||||||
error = {
|
|
||||||
show: false,
|
|
||||||
};
|
|
||||||
showPassword = false;
|
|
||||||
validationSent = false;
|
|
||||||
state = {
|
|
||||||
email: {
|
|
||||||
status: false,
|
|
||||||
msg: [],
|
|
||||||
},
|
|
||||||
username: {
|
|
||||||
status: false,
|
|
||||||
msg: [],
|
|
||||||
},
|
|
||||||
password: {
|
|
||||||
status: false,
|
|
||||||
msg: [],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
rules = {
|
|
||||||
password_length: value => value.length > 6 || 'Password must be at least 6 characters long',
|
|
||||||
required: value => !!value || 'Required.',
|
|
||||||
email: (value: string) => value.includes('@') || 'Invalid e-mail.',
|
|
||||||
};
|
|
||||||
|
|
||||||
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.email) === true ? 'v-gravatar' : 'avatar';
|
|
||||||
}
|
|
||||||
|
|
||||||
async submit() {
|
|
||||||
try {
|
|
||||||
await this.$apollo.mutate({
|
|
||||||
mutation: CREATE_USER,
|
|
||||||
variables: {
|
|
||||||
email: this.email,
|
|
||||||
password: this.password,
|
|
||||||
username: this.username,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
this.validationSent = true;
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
.avatar-enter-active {
|
|
||||||
transition: opacity 1s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.avatar-enter, .avatar-leave-to {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.avatar-leave {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,12 +0,0 @@
|
||||||
<template>
|
|
||||||
<img class="img-circle elevation-7 mb-1" src="@/assets/profile.svg">
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { Component, Vue } from 'vue-property-decorator';
|
|
||||||
|
|
||||||
@Component
|
|
||||||
export default class RegisterAvatar extends Vue {
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
|
@ -1,82 +0,0 @@
|
||||||
<template>
|
|
||||||
<v-container fluid fill-height>
|
|
||||||
<v-layout align-center justify-center>
|
|
||||||
<v-flex xs12 sm8 md4>
|
|
||||||
<v-card class="elevation-12">
|
|
||||||
<v-toolbar dark color="primary">
|
|
||||||
<v-toolbar-title>Resend Instructions</v-toolbar-title>
|
|
||||||
</v-toolbar>
|
|
||||||
<v-card-text>
|
|
||||||
<v-form @submit="resendConfirmationAction" v-if="!validationSent">
|
|
||||||
<v-text-field
|
|
||||||
label="Email"
|
|
||||||
type="email"
|
|
||||||
v-model="credentials.email"
|
|
||||||
required
|
|
||||||
:state="state.email.status"
|
|
||||||
:rules="[rules.required, rules.email]"
|
|
||||||
>
|
|
||||||
</v-text-field>
|
|
||||||
<v-btn type="submit" color="blue">Send instructions again</v-btn>
|
|
||||||
</v-form>
|
|
||||||
<div v-else>
|
|
||||||
<h2>Validation email sent to {{ credentials.email }}</h2>
|
|
||||||
<v-alert :value="true" type="info">Please check you spam folder if you didn't receive the email.</v-alert>
|
|
||||||
</div>
|
|
||||||
</v-card-text>
|
|
||||||
</v-card>
|
|
||||||
</v-flex>
|
|
||||||
</v-layout>
|
|
||||||
</v-container>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { Component, Prop, Vue } from 'vue-property-decorator';
|
|
||||||
import { validateEmailField, validateRequiredField } from '@/utils/validators';
|
|
||||||
import { RESEND_CONFIRMATION_EMAIL } from '@/graphql/auth';
|
|
||||||
|
|
||||||
@Component
|
|
||||||
export default class ResendConfirmation extends Vue {
|
|
||||||
@Prop({ type: String, required: false, default: '' }) email!: string;
|
|
||||||
|
|
||||||
credentials = {
|
|
||||||
email: '',
|
|
||||||
};
|
|
||||||
validationSent = false;
|
|
||||||
error = false;
|
|
||||||
state = {
|
|
||||||
email: {
|
|
||||||
status: null,
|
|
||||||
msg: '',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
rules = {
|
|
||||||
required: validateRequiredField,
|
|
||||||
email: validateEmailField,
|
|
||||||
};
|
|
||||||
|
|
||||||
mounted() {
|
|
||||||
this.credentials.email = this.email;
|
|
||||||
}
|
|
||||||
|
|
||||||
async resendConfirmationAction(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
this.error = false;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await this.$apollo.mutate({
|
|
||||||
mutation: RESEND_CONFIRMATION_EMAIL,
|
|
||||||
variables: {
|
|
||||||
email: this.credentials.email,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
this.error = true;
|
|
||||||
} finally {
|
|
||||||
this.validationSent = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
|
@ -1,92 +0,0 @@
|
||||||
<template>
|
|
||||||
<v-container fluid fill-height>
|
|
||||||
<v-layout align-center justify-center>
|
|
||||||
<v-flex xs12 sm8 md4>
|
|
||||||
<v-card class="elevation-12">
|
|
||||||
<v-toolbar dark color="primary">
|
|
||||||
<v-toolbar-title>Password Reset</v-toolbar-title>
|
|
||||||
</v-toolbar>
|
|
||||||
<v-card-text>
|
|
||||||
<v-form @submit="resendConfirmationAction" v-if="!validationSent">
|
|
||||||
<v-text-field
|
|
||||||
label="Email"
|
|
||||||
type="email"
|
|
||||||
v-model="credentials.email"
|
|
||||||
required
|
|
||||||
:state="state.email.status"
|
|
||||||
:rules="[rules.required, rules.email]"
|
|
||||||
>
|
|
||||||
</v-text-field>
|
|
||||||
<v-btn type="submit" color="blue">Reset my password</v-btn>
|
|
||||||
</v-form>
|
|
||||||
<div v-else>
|
|
||||||
<h2>Validation email sent to {{ credentials.email }}</h2>
|
|
||||||
<v-alert :value="true" type="info">Please check you spam folder if you didn't receive the email.</v-alert>
|
|
||||||
</div>
|
|
||||||
</v-card-text>
|
|
||||||
</v-card>
|
|
||||||
</v-flex>
|
|
||||||
</v-layout>
|
|
||||||
</v-container>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { Component, Prop, Vue } from 'vue-property-decorator';
|
|
||||||
import { validateEmailField, validateRequiredField } from '@/utils/validators';
|
|
||||||
import { SEND_RESET_PASSWORD } from '@/graphql/auth';
|
|
||||||
|
|
||||||
@Component
|
|
||||||
export default class SendPasswordReset extends Vue {
|
|
||||||
@Prop({ type: String, required: false, default: '' }) email!: string;
|
|
||||||
|
|
||||||
credentials = {
|
|
||||||
email: '',
|
|
||||||
};
|
|
||||||
validationSent = false;
|
|
||||||
error = false;
|
|
||||||
state = {
|
|
||||||
email: {
|
|
||||||
status: null,
|
|
||||||
msg: '',
|
|
||||||
} as { status: boolean | null, msg: string },
|
|
||||||
};
|
|
||||||
|
|
||||||
rules = {
|
|
||||||
required: validateRequiredField,
|
|
||||||
email: validateEmailField,
|
|
||||||
};
|
|
||||||
|
|
||||||
mounted() {
|
|
||||||
this.credentials.email = this.email;
|
|
||||||
}
|
|
||||||
|
|
||||||
async resendConfirmationAction(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
this.error = false;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await this.$apollo.mutate({
|
|
||||||
mutation: SEND_RESET_PASSWORD,
|
|
||||||
variables: {
|
|
||||||
email: this.credentials.email,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
this.validationSent = true;
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
this.error = true;
|
|
||||||
this.state.email = { status: false, msg: err.errors };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
resetState() {
|
|
||||||
this.state = {
|
|
||||||
email: {
|
|
||||||
status: null,
|
|
||||||
msg: '',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
|
@ -1,60 +0,0 @@
|
||||||
<template>
|
|
||||||
<v-container>
|
|
||||||
<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">
|
|
||||||
<translate>Error while validating account</translate>
|
|
||||||
</v-alert>
|
|
||||||
</div>
|
|
||||||
<h1 v-else>
|
|
||||||
<translate>Your account has been validated</translate>
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
</v-container>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { VALIDATE_USER } from '@/graphql/user';
|
|
||||||
import { Component, Prop, Vue } from 'vue-property-decorator';
|
|
||||||
import { AUTH_TOKEN, AUTH_USER_ID } from '@/constants';
|
|
||||||
|
|
||||||
@Component
|
|
||||||
export default class Validate extends Vue {
|
|
||||||
@Prop({ type: String, required: true }) token!: string;
|
|
||||||
|
|
||||||
loading = true;
|
|
||||||
failed = false;
|
|
||||||
|
|
||||||
created() {
|
|
||||||
this.validateAction();
|
|
||||||
}
|
|
||||||
|
|
||||||
async validateAction() {
|
|
||||||
try {
|
|
||||||
const data = await this.$apollo.mutate({
|
|
||||||
mutation: VALIDATE_USER,
|
|
||||||
variables: {
|
|
||||||
token: this.token,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
this.saveUserData(data.data);
|
|
||||||
this.$router.push({ name: 'Home' });
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
this.failed = true;
|
|
||||||
} finally {
|
|
||||||
this.loading = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
saveUserData({ validateUser: login }) {
|
|
||||||
localStorage.setItem(AUTH_USER_ID, login.user.id);
|
|
||||||
localStorage.setItem(AUTH_TOKEN, login.token);
|
|
||||||
}
|
|
||||||
|
|
||||||
};
|
|
||||||
</script>
|
|
|
@ -1,93 +0,0 @@
|
||||||
<template>
|
|
||||||
<v-container fluid fill-height>
|
|
||||||
<v-layout align-center justify-center>
|
|
||||||
<v-flex xs12 sm8 md4>
|
|
||||||
<v-card class="elevation-12">
|
|
||||||
<v-toolbar dark color="primary">
|
|
||||||
<v-toolbar-title>
|
|
||||||
<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 lang="ts">
|
|
||||||
import { CREATE_CATEGORY } from '@/graphql/category';
|
|
||||||
import { Component, Vue } from 'vue-property-decorator';
|
|
||||||
|
|
||||||
@Component
|
|
||||||
export default class CreateCategory extends Vue {
|
|
||||||
title = '';
|
|
||||||
description = '';
|
|
||||||
image = {
|
|
||||||
url: '',
|
|
||||||
name: '',
|
|
||||||
file: '',
|
|
||||||
};
|
|
||||||
|
|
||||||
create() {
|
|
||||||
this.$apollo.mutate({
|
|
||||||
mutation: CREATE_CATEGORY,
|
|
||||||
variables: {
|
|
||||||
title: this.title,
|
|
||||||
description: this.description,
|
|
||||||
picture: (this.$refs['image'] as any).files[ 0 ],
|
|
||||||
},
|
|
||||||
}).then((data) => {
|
|
||||||
console.log(data);
|
|
||||||
}).catch((error) => {
|
|
||||||
console.error(error);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
pickFile() {
|
|
||||||
(this.$refs['image'] as any).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>
|
|
||||||
.markdown-render h1 {
|
|
||||||
font-size: 2em;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,70 +0,0 @@
|
||||||
<template>
|
|
||||||
<v-container>
|
|
||||||
<h1>Category List</h1>
|
|
||||||
<v-container fluid grid-list-md class="grey lighten-4">
|
|
||||||
<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-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>
|
|
||||||
<div>{{ category.description }}</div>
|
|
||||||
</div>
|
|
||||||
</v-card-title>
|
|
||||||
<v-card-actions>
|
|
||||||
<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>
|
|
||||||
<v-layout v-if="categories.length <= 0">
|
|
||||||
<h3>No categories :(</h3>
|
|
||||||
</v-layout>
|
|
||||||
</v-layout>
|
|
||||||
</v-container>
|
|
||||||
|
|
||||||
<router-link :to="{ name: 'CreateCategory' }" class="btn btn-default">Create</router-link>
|
|
||||||
</v-container>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { FETCH_CATEGORIES } from '@/graphql/category';
|
|
||||||
import { Component, Vue } from 'vue-property-decorator';
|
|
||||||
|
|
||||||
// TODO : remove this hardcode
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
apollo: {
|
|
||||||
categories: {
|
|
||||||
query: FETCH_CATEGORIES,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
export default class List extends Vue {
|
|
||||||
categories = [];
|
|
||||||
loading = true;
|
|
||||||
HTTP_ENDPOINT = 'http://localhost:4000';
|
|
||||||
|
|
||||||
deleteCategory(categoryId) {
|
|
||||||
const router = this.$router;
|
|
||||||
// FIXME: remove eventFetch
|
|
||||||
// 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 -->
|
|
||||||
<style scoped>
|
|
||||||
|
|
||||||
</style>
|
|
|
@ -1,193 +0,0 @@
|
||||||
<template>
|
|
||||||
<v-container fluid fill-height>
|
|
||||||
<v-layout align-center justify-center>
|
|
||||||
<v-flex xs12 sm8 md4>
|
|
||||||
<v-card class="elevation-12">
|
|
||||||
<v-toolbar dark color="primary">
|
|
||||||
<v-toolbar-title>Create a new event</v-toolbar-title>
|
|
||||||
</v-toolbar>
|
|
||||||
<v-card-text>
|
|
||||||
<v-form>
|
|
||||||
<v-text-field label="Title" v-model="event.title" :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>
|
|
||||||
<v-radio label="Phone" value="phone" off-icon="phone"></v-radio>
|
|
||||||
<v-radio label="Other" value="other"></v-radio>
|
|
||||||
</v-radio-group>
|
|
||||||
<!-- <vuetify-google-autocomplete
|
|
||||||
v-if="event.location_type === 'physical'"
|
|
||||||
id="map"
|
|
||||||
append-icon="search"
|
|
||||||
classname="form-control"
|
|
||||||
placeholder="Start typing"
|
|
||||||
label="Location"
|
|
||||||
enable-geolocation
|
|
||||||
types="geocode"
|
|
||||||
v-on:placechanged="getAddressData"
|
|
||||||
>
|
|
||||||
</vuetify-google-autocomplete>-->
|
|
||||||
<v-text-field
|
|
||||||
v-if="event.location_type === 'online'"
|
|
||||||
label="Meeting adress"
|
|
||||||
type="url"
|
|
||||||
v-model="event.url"
|
|
||||||
:required="event.location_type === 'online'"
|
|
||||||
></v-text-field>
|
|
||||||
<v-text-field
|
|
||||||
v-if="event.location_type === 'phone'"
|
|
||||||
label="Phone number"
|
|
||||||
type="tel"
|
|
||||||
v-model="event.phone"
|
|
||||||
:required="event.location_type === 'phone'"
|
|
||||||
></v-text-field>
|
|
||||||
<v-autocomplete
|
|
||||||
:items="categories"
|
|
||||||
v-model="event.category"
|
|
||||||
item-text="title"
|
|
||||||
item-value="id"
|
|
||||||
label="Categories"
|
|
||||||
></v-autocomplete>
|
|
||||||
<v-btn color="primary" @click="create">Create event</v-btn>
|
|
||||||
</v-form>
|
|
||||||
</v-card-text>
|
|
||||||
</v-card>
|
|
||||||
</v-flex>
|
|
||||||
</v-layout>
|
|
||||||
</v-container>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
// 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";
|
|
||||||
import { Component, Prop, Vue } from "vue-property-decorator";
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
components: {
|
|
||||||
VueMarkdown
|
|
||||||
},
|
|
||||||
apollo: {
|
|
||||||
categories: {
|
|
||||||
query: FETCH_CATEGORIES
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
export default class CreateEvent extends Vue {
|
|
||||||
@Prop({ required: false, type: String }) uuid!: string;
|
|
||||||
|
|
||||||
e1 = 0;
|
|
||||||
event = {
|
|
||||||
title: null,
|
|
||||||
organizer_actor_id: 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: [],
|
|
||||||
participants: []
|
|
||||||
} as any; // FIXME: correctly type an event
|
|
||||||
categories = [];
|
|
||||||
tags = [];
|
|
||||||
tagsToSend = [];
|
|
||||||
tagsFetched = [];
|
|
||||||
loading = false;
|
|
||||||
|
|
||||||
// created() {
|
|
||||||
// if (this.uuid) {
|
|
||||||
// this.fetchEvent();
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
create() {
|
|
||||||
// this.event.seats = parseInt(this.event.seats, 10);
|
|
||||||
// this.tagsToSend.forEach((tag) => {
|
|
||||||
// this.event.tags.push({
|
|
||||||
// title: tag,
|
|
||||||
// // '@type': 'Tag',
|
|
||||||
// });
|
|
||||||
// });
|
|
||||||
// FIXME: correctly parse actor JSON
|
|
||||||
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
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.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 = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
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}`
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.markdown-render h1 {
|
|
||||||
font-size: 2em;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,125 +0,0 @@
|
||||||
<template>
|
|
||||||
<v-container fluid grid-list-md>
|
|
||||||
<h3>Update event {{ event.title }}</h3>
|
|
||||||
<v-progress-circular v-if="loading" indeterminate color="primary"></v-progress-circular>
|
|
||||||
<v-form v-if="!loading">
|
|
||||||
<v-stepper v-model="e1" vertical>
|
|
||||||
<v-stepper-step step="1" :complete="e1 > 1">Basic Informations
|
|
||||||
<small>Title and description</small>
|
|
||||||
</v-stepper-step>
|
|
||||||
<v-stepper-content step="1">
|
|
||||||
<v-layout row wrap>
|
|
||||||
<v-flex xs12>
|
|
||||||
<v-text-field
|
|
||||||
label="Title"
|
|
||||||
v-model="event.title"
|
|
||||||
:counter="100"
|
|
||||||
required
|
|
||||||
></v-text-field>
|
|
||||||
</v-flex>
|
|
||||||
<v-flex md6>
|
|
||||||
<v-text-field
|
|
||||||
label="Description"
|
|
||||||
v-model="event.description"
|
|
||||||
multiLine
|
|
||||||
required
|
|
||||||
></v-text-field>
|
|
||||||
</v-flex>
|
|
||||||
<v-flex md6>
|
|
||||||
<vue-markdown class="markdown-render"
|
|
||||||
:watches="['show','html','breaks','linkify','emoji','typographer','toc']"
|
|
||||||
:source="event.description"
|
|
||||||
:show="true" :html="false" :breaks="true" :linkify="true"
|
|
||||||
:emoji="true" :typographer="true" :toc="false"
|
|
||||||
></vue-markdown>
|
|
||||||
</v-flex>
|
|
||||||
<v-flex md12>
|
|
||||||
<v-select
|
|
||||||
v-bind:items="categories"
|
|
||||||
v-model="event.category"
|
|
||||||
item-text="name"
|
|
||||||
item-value="@id"
|
|
||||||
label="Categories"
|
|
||||||
single-line
|
|
||||||
bottom
|
|
||||||
></v-select>
|
|
||||||
</v-flex>
|
|
||||||
<v-flex md12>
|
|
||||||
<!--<v-text-field
|
|
||||||
v-model="tagsToSend"
|
|
||||||
label="Tags"
|
|
||||||
></v-text-field>-->
|
|
||||||
<v-select
|
|
||||||
v-model="tagsToSend"
|
|
||||||
label="Tags"
|
|
||||||
chips
|
|
||||||
tags
|
|
||||||
:items="tagsFetched"
|
|
||||||
></v-select>
|
|
||||||
</v-flex>
|
|
||||||
</v-layout>
|
|
||||||
<v-btn color="primary" @click.native="e1 = 2">Next</v-btn>
|
|
||||||
</v-stepper-content>
|
|
||||||
<v-stepper-step step="2" :complete="e1 > 2">Date and place</v-stepper-step>
|
|
||||||
<v-stepper-content step="2">
|
|
||||||
Event starts at:
|
|
||||||
<v-text-field type="datetime-local" v-model="event.startDate"></v-text-field>
|
|
||||||
Event ends at:
|
|
||||||
<v-text-field type="datetime-local" v-model="event.endDate"></v-text-field>
|
|
||||||
|
|
||||||
<vuetify-google-autocomplete
|
|
||||||
id="map"
|
|
||||||
append-icon="search"
|
|
||||||
placeholder="Start typing"
|
|
||||||
label="Location"
|
|
||||||
enable-geolocation
|
|
||||||
v-on:placechanged="getAddressData"
|
|
||||||
>
|
|
||||||
</vuetify-google-autocomplete>
|
|
||||||
<v-btn color="primary" @click.native="e1 = 3">Next</v-btn>
|
|
||||||
</v-stepper-content>
|
|
||||||
<v-stepper-step step="3" :complete="e1 > 3">Extra informations</v-stepper-step>
|
|
||||||
<v-stepper-content step="3">
|
|
||||||
<v-text-field
|
|
||||||
label="Number of seats"
|
|
||||||
v-model="event.seats"
|
|
||||||
></v-text-field>
|
|
||||||
<v-text-field
|
|
||||||
label="Price"
|
|
||||||
prefix="$"
|
|
||||||
type="float"
|
|
||||||
v-model="event.price"
|
|
||||||
></v-text-field>
|
|
||||||
</v-stepper-content>
|
|
||||||
</v-stepper>
|
|
||||||
</v-form>
|
|
||||||
<v-btn color="primary" @click="create">Create event</v-btn>
|
|
||||||
</v-container>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { Component, Prop, Vue } from 'vue-property-decorator';
|
|
||||||
|
|
||||||
@Component
|
|
||||||
export default class EventEdit extends Vue {
|
|
||||||
@Prop(String) id!: string;
|
|
||||||
|
|
||||||
loading = true;
|
|
||||||
event = null;
|
|
||||||
|
|
||||||
created() {
|
|
||||||
this.fetchData();
|
|
||||||
}
|
|
||||||
|
|
||||||
fetchData() {
|
|
||||||
// FIXME: remove eventFetch
|
|
||||||
// eventFetch(`/events/${this.id}`, this.$store)
|
|
||||||
// .then(response => response.json())
|
|
||||||
// .then((data) => {
|
|
||||||
// this.loading = false;
|
|
||||||
// this.event = data;
|
|
||||||
// console.log(this.event);
|
|
||||||
// });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
|
@ -1,245 +0,0 @@
|
||||||
<template>
|
|
||||||
<v-layout row>
|
|
||||||
<v-flex xs12 sm6 offset-sm3>
|
|
||||||
<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>
|
|
||||||
<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--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>
|
|
||||||
{{ event.physical_address.streetAddress }}
|
|
||||||
</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"></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 lang="ts">
|
|
||||||
import { FETCH_EVENT } from '@/graphql/event';
|
|
||||||
import { Component, Prop, Vue } from 'vue-property-decorator';
|
|
||||||
import VueMarkdown from 'vue-markdown';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
components: {
|
|
||||||
VueMarkdown,
|
|
||||||
},
|
|
||||||
apollo: {
|
|
||||||
event: {
|
|
||||||
query: FETCH_EVENT,
|
|
||||||
variables() {
|
|
||||||
return {
|
|
||||||
uuid: this.uuid,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
},
|
|
||||||
// loggedActor: {
|
|
||||||
// query: LOGGED_ACTOR,
|
|
||||||
// }
|
|
||||||
},
|
|
||||||
})
|
|
||||||
export default class Event extends Vue {
|
|
||||||
@Prop({ type: String, required: true }) uuid!: string;
|
|
||||||
|
|
||||||
event = {
|
|
||||||
name: '',
|
|
||||||
slug: '',
|
|
||||||
title: '',
|
|
||||||
uuid: this.uuid,
|
|
||||||
description: '',
|
|
||||||
organizer: {
|
|
||||||
id: null,
|
|
||||||
username: null,
|
|
||||||
},
|
|
||||||
participants: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
deleteEvent() {
|
|
||||||
const router = this.$router;
|
|
||||||
// FIXME: remove eventFetch
|
|
||||||
// eventFetch(`/events/${this.uuid}`, this.$store, { method: 'DELETE' })
|
|
||||||
// .then(() => router.push({ name: 'EventList' }));
|
|
||||||
}
|
|
||||||
|
|
||||||
joinEvent() {
|
|
||||||
// FIXME: remove eventFetch
|
|
||||||
// eventFetch(`/events/${this.uuid}/join`, this.$store, { method: 'POST' })
|
|
||||||
// .then(response => response.json())
|
|
||||||
// .then((data) => {
|
|
||||||
// console.log(data);
|
|
||||||
// });
|
|
||||||
}
|
|
||||||
|
|
||||||
leaveEvent() {
|
|
||||||
// FIXME: remove eventFetch
|
|
||||||
// eventFetch(`/events/${this.uuid}/leave`, this.$store)
|
|
||||||
// .then(response => response.json())
|
|
||||||
// .then((data) => {
|
|
||||||
// console.log(data);
|
|
||||||
// });
|
|
||||||
}
|
|
||||||
|
|
||||||
downloadIcsEvent() {
|
|
||||||
// FIXME: remove eventFetch
|
|
||||||
// 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;
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<!-- Add "scoped" attribute to limit CSS to this component only -->
|
|
||||||
<style>
|
|
||||||
.v-card__media__background {
|
|
||||||
filter: contrast(0.4);
|
|
||||||
}
|
|
||||||
</style>
|
|
44
js/src/components/Event/EventCard.vue
Normal file
44
js/src/components/Event/EventCard.vue
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
<template>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-image" v-if="!event.image">
|
||||||
|
<figure class="image is-4by3">
|
||||||
|
<img src="https://picsum.photos/g/400/200/">
|
||||||
|
</figure>
|
||||||
|
</div>
|
||||||
|
<div class="card-content">
|
||||||
|
<div class="content">
|
||||||
|
<router-link :to="{ name: 'Event', params:{ uuid: event.uuid } }">
|
||||||
|
<h2 class="title">{{ event.title }}</h2>
|
||||||
|
</router-link>
|
||||||
|
<span>{{ event.begins_on | formatDay }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="!hideDetails">
|
||||||
|
<div v-if="event.participants.length === 1">
|
||||||
|
<translate
|
||||||
|
:translate-params="{name: event.participants[0].actor.preferredUsername}"
|
||||||
|
>%{name} organizes this event</translate>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<span v-for="participant in event.participants" :key="participant.actor.uuid">
|
||||||
|
{{ participant.actor.preferredUsername }}
|
||||||
|
<span v-if="participant.role === 4">(organizer)</span>,
|
||||||
|
<!-- <translate
|
||||||
|
:translate-params="{name: participant.actor.preferredUsername}"
|
||||||
|
> %{name} is in,</translate>-->
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { IEvent } from "@/types/event.model";
|
||||||
|
import { Component, Prop, Vue } from "vue-property-decorator";
|
||||||
|
|
||||||
|
@Component
|
||||||
|
export default class EventCard extends Vue {
|
||||||
|
@Prop({ required: true }) event!: IEvent;
|
||||||
|
@Prop({ default: false }) hideDetails!: boolean;
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -1,150 +0,0 @@
|
||||||
<template>
|
|
||||||
<v-layout>
|
|
||||||
<v-flex xs12 sm8 offset-sm2>
|
|
||||||
<v-card>
|
|
||||||
<h1>{{ $t('event.list.title') }}</h1>
|
|
||||||
|
|
||||||
<v-progress-circular v-if="loading" indeterminate color="primary"></v-progress-circular>
|
|
||||||
<v-chip close v-model="locationChip" label color="pink" text-color="white" v-if="$router.currentRoute.params.location">
|
|
||||||
<v-icon left>location_city</v-icon>
|
|
||||||
{{ locationText }}
|
|
||||||
</v-chip>
|
|
||||||
<v-container grid-list-sm fluid>
|
|
||||||
<v-layout row wrap>
|
|
||||||
<v-flex xs4 v-for="event in events" :key="event.id">
|
|
||||||
<v-card>
|
|
||||||
<v-card-media v-if="!event.image"
|
|
||||||
class="white--text"
|
|
||||||
height="200px"
|
|
||||||
src="https://picsum.photos/g/400/200/"
|
|
||||||
>
|
|
||||||
<v-container fill-height fluid>
|
|
||||||
<v-layout fill-height>
|
|
||||||
<v-flex xs12 align-end flexbox>
|
|
||||||
<span class="headline black--text">{{ event.title }}</span>
|
|
||||||
</v-flex>
|
|
||||||
</v-layout>
|
|
||||||
</v-container>
|
|
||||||
</v-card-media>
|
|
||||||
<v-card-title primary-title>
|
|
||||||
<div>
|
|
||||||
<span class="grey--text">{{ event.begins_on | formatDate }}</span><br>
|
|
||||||
<router-link :to="{name: 'Account', params: { name: event.organizer.username } }">
|
|
||||||
<v-avatar size="25px">
|
|
||||||
<img class="img-circle elevation-7 mb-1"
|
|
||||||
:src="event.organizer.avatar"
|
|
||||||
>
|
|
||||||
</v-avatar>
|
|
||||||
</router-link>
|
|
||||||
<span v-if="event.organizer">Organisé par <router-link
|
|
||||||
:to="{name: 'Account', params: {'name': event.organizer.username}}">{{ event.organizer.username }}</router-link></span>
|
|
||||||
</div>
|
|
||||||
</v-card-title>
|
|
||||||
<v-card-actions>
|
|
||||||
<v-btn flat color="orange" @click="downloadIcsEvent(event)">Share</v-btn>
|
|
||||||
<v-btn flat color="orange" @click="viewEvent(event)">Explore</v-btn>
|
|
||||||
<v-btn flat color="red" @click="deleteEvent(event)">Delete</v-btn>
|
|
||||||
</v-card-actions>
|
|
||||||
</v-card>
|
|
||||||
</v-flex>
|
|
||||||
</v-layout>
|
|
||||||
</v-container>
|
|
||||||
<router-link :to="{ name: 'CreateEvent' }" class="btn btn-default">Create</router-link>
|
|
||||||
</v-card>
|
|
||||||
</v-flex>
|
|
||||||
</v-layout>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import ngeohash from 'ngeohash';
|
|
||||||
import VueMarkdown from 'vue-markdown';
|
|
||||||
import VCardTitle from 'vuetify/es5/components/VCard/VCardTitle';
|
|
||||||
import { Component, Prop, Vue, Watch } from 'vue-property-decorator';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
components: {
|
|
||||||
VCardTitle: VCardTitle as any,
|
|
||||||
VueMarkdown,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
export default class EventList extends Vue {
|
|
||||||
@Prop(String) location!: string;
|
|
||||||
|
|
||||||
events = [];
|
|
||||||
loading = true;
|
|
||||||
locationChip = false;
|
|
||||||
locationText = '';
|
|
||||||
|
|
||||||
created() {
|
|
||||||
this.fetchData(this.$router.currentRoute.params[ 'location' ]);
|
|
||||||
}
|
|
||||||
|
|
||||||
beforeRouteUpdate(to, from, next) {
|
|
||||||
this.fetchData(to.params.location);
|
|
||||||
next();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Watch('locationChip')
|
|
||||||
onLocationChipChange(val) {
|
|
||||||
if (val === false) {
|
|
||||||
this.$router.push({ name: 'EventList' });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
// FIXME: remove eventFetch
|
|
||||||
// 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;
|
|
||||||
// FIXME: remove eventFetch
|
|
||||||
// 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) {
|
|
||||||
// FIXME: remove eventFetch
|
|
||||||
// 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 -->
|
|
||||||
<style scoped>
|
|
||||||
|
|
||||||
</style>
|
|
|
@ -1,136 +0,0 @@
|
||||||
<template>
|
|
||||||
<v-container>
|
|
||||||
<h3>Create a new group</h3>
|
|
||||||
<v-form>
|
|
||||||
<v-layout row wrap>
|
|
||||||
<v-flex xs12>
|
|
||||||
<v-text-field
|
|
||||||
label="Title"
|
|
||||||
v-model="group.preferred_username"
|
|
||||||
:counter="100"
|
|
||||||
required
|
|
||||||
></v-text-field>
|
|
||||||
</v-flex>
|
|
||||||
<v-flex xs12>
|
|
||||||
<v-text-field
|
|
||||||
label="Title"
|
|
||||||
v-model="group.name"
|
|
||||||
:counter="100"
|
|
||||||
required
|
|
||||||
></v-text-field>
|
|
||||||
</v-flex>
|
|
||||||
<v-flex md6>
|
|
||||||
<v-text-field
|
|
||||||
label="Description"
|
|
||||||
v-model="group.summary"
|
|
||||||
multiLine
|
|
||||||
required
|
|
||||||
></v-text-field>
|
|
||||||
</v-flex>
|
|
||||||
<v-flex md6>
|
|
||||||
<vue-markdown class="markdown-render"
|
|
||||||
:watches="['show','html','breaks','linkify','emoji','typographer','toc']"
|
|
||||||
:source="group.summary"
|
|
||||||
:show="true" :html="false" :breaks="true" :linkify="true"
|
|
||||||
:emoji="true" :typographer="true" :toc="false"
|
|
||||||
></vue-markdown>
|
|
||||||
</v-flex>
|
|
||||||
<!--<v-flex md12>-->
|
|
||||||
<!--<vuetify-google-autocomplete-->
|
|
||||||
<!--id="map"-->
|
|
||||||
<!--append-icon="search"-->
|
|
||||||
<!--classname="form-control"-->
|
|
||||||
<!--placeholder="Start typing"-->
|
|
||||||
<!--enable-geolocation-->
|
|
||||||
<!--v-on:placechanged="getAddressData"-->
|
|
||||||
<!-->-->
|
|
||||||
<!--</vuetify-google-autocomplete>-->
|
|
||||||
<!--</v-flex>-->
|
|
||||||
<!--<v-flex md12>-->
|
|
||||||
<!--<v-select-->
|
|
||||||
<!--v-bind:items="categories"-->
|
|
||||||
<!--v-model="group.category"-->
|
|
||||||
<!--item-text="title"-->
|
|
||||||
<!--item-value="@id"-->
|
|
||||||
<!--label="Categories"-->
|
|
||||||
<!--single-line-->
|
|
||||||
<!--bottom-->
|
|
||||||
<!--types="(cities)"-->
|
|
||||||
<!--></v-select>-->
|
|
||||||
<!--</v-flex>-->
|
|
||||||
</v-layout>
|
|
||||||
</v-form>
|
|
||||||
<v-btn color="primary" @click="create">Create group</v-btn>
|
|
||||||
</v-container>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import VueMarkdown from 'vue-markdown';
|
|
||||||
import VuetifyGoogleAutocomplete from 'vuetify-google-autocomplete';
|
|
||||||
import { Component, Vue } from 'vue-property-decorator';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
components: {
|
|
||||||
VueMarkdown,
|
|
||||||
VuetifyGoogleAutocomplete,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
export default class CreateGroup extends Vue {
|
|
||||||
e1 = 0;
|
|
||||||
// FIXME: correctly type group
|
|
||||||
group: { preferred_username: string, name: string, summary: string, address?: any } = {
|
|
||||||
preferred_username: '',
|
|
||||||
name: '',
|
|
||||||
summary: '',
|
|
||||||
// category: null,
|
|
||||||
};
|
|
||||||
categories = [];
|
|
||||||
|
|
||||||
mounted() {
|
|
||||||
this.fetchCategories();
|
|
||||||
}
|
|
||||||
|
|
||||||
create() {
|
|
||||||
// this.group.organizer = "/accounts/" + this.$store.state.user.id;
|
|
||||||
|
|
||||||
// FIXME: remove eventFetch
|
|
||||||
// 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() {
|
|
||||||
// FIXME: remove eventFetch
|
|
||||||
// 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,
|
|
||||||
},
|
|
||||||
addressCountry: addressData.country,
|
|
||||||
addressLocality: addressData.city,
|
|
||||||
addressRegion: addressData.administrative_area_level_1,
|
|
||||||
postalCode: addressData.postal_code,
|
|
||||||
streetAddress: `${addressData.street_number} ${addressData.route}`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.markdown-render h1 {
|
|
||||||
font-size: 2em;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,241 +0,0 @@
|
||||||
<template>
|
|
||||||
<v-container>
|
|
||||||
<v-layout row>
|
|
||||||
<v-flex xs12 sm6 offset-sm3>
|
|
||||||
<v-progress-circular v-if="loading" indeterminate color="primary"></v-progress-circular>
|
|
||||||
<v-card v-if="!loading">
|
|
||||||
<v-card-media :src="group.banner" height="400px">
|
|
||||||
<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-btn icon>
|
|
||||||
<v-icon>more_vert</v-icon>
|
|
||||||
</v-btn>
|
|
||||||
</v-card-title>
|
|
||||||
<v-spacer></v-spacer>
|
|
||||||
<div class="text-xs-center">
|
|
||||||
<v-avatar size="125px">
|
|
||||||
<img v-if="!group.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="group.avatar"
|
|
||||||
>
|
|
||||||
</v-avatar>
|
|
||||||
</div>
|
|
||||||
<v-container fluid grid-list-lg>
|
|
||||||
<v-layout row>
|
|
||||||
<v-flex xs7>
|
|
||||||
<div class="headline">{{ group.display_name }}</div>
|
|
||||||
<div>
|
|
||||||
<span class="subheading">
|
|
||||||
~{{ group.username }}
|
|
||||||
<span v-if="group.domain">
|
|
||||||
@{{ group.domain }}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
<v-chip color="indigo" text-color="white">
|
|
||||||
<v-avatar>
|
|
||||||
<v-icon>group</v-icon>
|
|
||||||
</v-avatar>
|
|
||||||
Group
|
|
||||||
</v-chip>
|
|
||||||
</div>
|
|
||||||
<v-card-text v-if="group.description" v-html="group.description"></v-card-text>
|
|
||||||
</v-flex>
|
|
||||||
</v-layout>
|
|
||||||
</v-container>
|
|
||||||
</v-layout>
|
|
||||||
</v-card-media>
|
|
||||||
<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="group.members.length > 0">
|
|
||||||
<v-subheader>Membres</v-subheader>
|
|
||||||
<v-layout row>
|
|
||||||
<v-flex xs2 v-for="member in group.members" :key="member.actor.username">
|
|
||||||
<router-link :to="{name: 'Account', params: { name: member.actor.username } }">
|
|
||||||
<v-badge overlap>
|
|
||||||
<span slot="badge" v-if="member.role === 1"><v-icon>star_half</v-icon></span>
|
|
||||||
<span slot="badge" v-if="member.role === 2"><v-icon>star</v-icon></span>
|
|
||||||
<v-avatar size="75px">
|
|
||||||
<img v-if="!member.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="member.actor.avatar"
|
|
||||||
>
|
|
||||||
</v-avatar>
|
|
||||||
</v-badge>
|
|
||||||
</router-link>
|
|
||||||
<span>{{ member.actor.username }}</span>
|
|
||||||
</v-flex>
|
|
||||||
</v-layout>
|
|
||||||
</v-container>
|
|
||||||
<v-container fluid grid-list-md v-if="group.participatingEvents && group.participatingEvents.length > 0">
|
|
||||||
<v-subheader>Participated at</v-subheader>
|
|
||||||
<v-layout row wrap>
|
|
||||||
<v-flex v-for="event in group.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="group.organizingEvents && group.organizingEvents.length > 0">
|
|
||||||
<v-subheader>Organized events</v-subheader>
|
|
||||||
<v-layout row wrap>
|
|
||||||
<v-flex v-for="event in group.organizingEvents" :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-flex>
|
|
||||||
</v-layout>
|
|
||||||
</v-container>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { Component, Prop, Vue, Watch } from 'vue-property-decorator';
|
|
||||||
|
|
||||||
@Component
|
|
||||||
export default class Group extends Vue {
|
|
||||||
@Prop({ type: String, required: true }) name!: string;
|
|
||||||
|
|
||||||
group = null;
|
|
||||||
loading = true;
|
|
||||||
|
|
||||||
created() {
|
|
||||||
this.fetchData();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Watch('$route')
|
|
||||||
onRouteChanged() {
|
|
||||||
// call again the method if the route changes
|
|
||||||
this.fetchData();
|
|
||||||
}
|
|
||||||
|
|
||||||
fetchData() {
|
|
||||||
// FIXME: remove eventFetch
|
|
||||||
// eventFetch(`/actors/${this.name}`, this.$store)
|
|
||||||
// .then(response => response.json())
|
|
||||||
// .then((response) => {
|
|
||||||
// this.group = response.data;
|
|
||||||
// this.loading = false;
|
|
||||||
// console.log(this.group);
|
|
||||||
// });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
30
js/src/components/Group/GroupCard.vue
Normal file
30
js/src/components/Group/GroupCard.vue
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
<template>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-image" v-if="!group.bannerUrl">
|
||||||
|
<figure class="image is-4by3">
|
||||||
|
<img src="https://picsum.photos/g/400/200/">
|
||||||
|
</figure>
|
||||||
|
</div>
|
||||||
|
<div class="card-content">
|
||||||
|
<div class="content">
|
||||||
|
<router-link :to="{ name: 'Group', params:{ uuid: group.uuid } }">
|
||||||
|
<h2 class="title">{{ group.name ? group.name : group.preferredUsername }}</h2>
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
<div v-if="!hideDetails">
|
||||||
|
<p>{{ group.summary }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { Component, Prop, Vue } from "vue-property-decorator";
|
||||||
|
import { IGroup } from "../../types/actor.model";
|
||||||
|
|
||||||
|
@Component
|
||||||
|
export default class GroupCard extends Vue {
|
||||||
|
@Prop({ required: true }) group!: IGroup;
|
||||||
|
@Prop({ default: false }) hideDetails!: boolean;
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -1,98 +0,0 @@
|
||||||
<template>
|
|
||||||
<v-container>
|
|
||||||
<h1>Group List</h1>
|
|
||||||
|
|
||||||
<v-progress-circular v-if="loading" indeterminate color="primary"></v-progress-circular>
|
|
||||||
<v-layout row wrap justify-space-around>
|
|
||||||
<v-flex xs12 md3 v-for="group in groups" :key="group.id">
|
|
||||||
<v-card>
|
|
||||||
<v-card-media
|
|
||||||
class="black--text"
|
|
||||||
height="200px"
|
|
||||||
src="https://picsum.photos/400/200/"
|
|
||||||
>
|
|
||||||
<v-container fill-height fluid>
|
|
||||||
<v-layout fill-height>
|
|
||||||
<v-flex xs12 align-end flexbox>
|
|
||||||
<span class="headline">{{ group.username }}</span>
|
|
||||||
</v-flex>
|
|
||||||
</v-layout>
|
|
||||||
</v-container>
|
|
||||||
</v-card-media>
|
|
||||||
<v-card-title>
|
|
||||||
<div>
|
|
||||||
<p>{{ group.summary }}</p>
|
|
||||||
<p v-if="group.organizer">Organisé par
|
|
||||||
<router-link :to="{name: 'Account', params: {'id': group.organizer.id}}">{{ group.organizer.username }}</router-link>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</v-card-title>
|
|
||||||
<v-card-actions>
|
|
||||||
<v-btn flat color="green" @click="joinGroup(group)">
|
|
||||||
<v-icon v-if="group.locked">lock</v-icon>
|
|
||||||
Join
|
|
||||||
</v-btn>
|
|
||||||
<v-btn flat color="orange" @click="viewActor(group)">Explore</v-btn>
|
|
||||||
<v-btn flat color="red" @click="deleteGroup(group)">Delete</v-btn>
|
|
||||||
</v-card-actions>
|
|
||||||
</v-card>
|
|
||||||
</v-flex>
|
|
||||||
</v-layout>
|
|
||||||
<router-link :to="{ name: 'CreateGroup' }" class="btn btn-default">Create</router-link>
|
|
||||||
</v-container>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
|
|
||||||
import { Component, Vue } from 'vue-property-decorator';
|
|
||||||
|
|
||||||
@Component
|
|
||||||
export default class GroupList extends Vue {
|
|
||||||
groups = [];
|
|
||||||
loading = true;
|
|
||||||
|
|
||||||
created() {
|
|
||||||
this.fetchData();
|
|
||||||
}
|
|
||||||
|
|
||||||
usernameWithDomain(actor) {
|
|
||||||
return actor.username + (actor.domain === null ? '' : `@${actor.domain}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
fetchData() {
|
|
||||||
// FIXME: remove eventFetch
|
|
||||||
// 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;
|
|
||||||
// FIXME: remove eventFetch
|
|
||||||
// eventFetch(`/groups/${this.usernameWithDomain(group)}`, this.$store, { method: 'DELETE' })
|
|
||||||
// .then(response => response.json())
|
|
||||||
// .then(() => router.push('/groups'));
|
|
||||||
}
|
|
||||||
|
|
||||||
viewActor(actor) {
|
|
||||||
this.$router.push({ name: 'Group', params: { name: this.usernameWithDomain(actor) } });
|
|
||||||
}
|
|
||||||
|
|
||||||
joinGroup(group) {
|
|
||||||
const router = this.$router;
|
|
||||||
// FIXME: remove eventFetch
|
|
||||||
// eventFetch(`/groups/${this.usernameWithDomain(group)}/join`, this.$store, { method: 'POST' })
|
|
||||||
// .then(response => response.json())
|
|
||||||
// .then(() => router.push({ name: 'Group', params: { name: this.usernameWithDomain(group) } }));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<!-- Add "scoped" attribute to limit CSS to this component only -->
|
|
||||||
<style scoped>
|
|
||||||
|
|
||||||
</style>
|
|
|
@ -1,178 +0,0 @@
|
||||||
<template>
|
|
||||||
<v-container>
|
|
||||||
<v-img
|
|
||||||
:gradient="gradient"
|
|
||||||
src="https://picsum.photos/1200/900"
|
|
||||||
dark
|
|
||||||
height="300"
|
|
||||||
v-if="!currentUser.id"
|
|
||||||
>
|
|
||||||
<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' }">
|
|
||||||
<translate>Register</translate>
|
|
||||||
</v-btn>
|
|
||||||
</v-flex>
|
|
||||||
</v-layout>
|
|
||||||
</v-container>
|
|
||||||
</v-img>
|
|
||||||
<v-layout v-else>
|
|
||||||
<v-flex xs12 sm8 offset-sm2>
|
|
||||||
<v-layout row wrap>
|
|
||||||
<v-flex xs12 sm6>
|
|
||||||
<h1>
|
|
||||||
<translate :translate-params="{username: actor.preferredUsername}">Welcome back %{username}</translate>
|
|
||||||
</h1>
|
|
||||||
</v-flex>
|
|
||||||
<v-flex xs12 sm6>
|
|
||||||
<v-layout align-center>
|
|
||||||
<span class="events-nearby title">Events nearby </span>
|
|
||||||
<v-text-field
|
|
||||||
solo
|
|
||||||
append-icon="place"
|
|
||||||
:value="ipLocation()"
|
|
||||||
></v-text-field>
|
|
||||||
</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-img v-if="!event.image"
|
|
||||||
class="white--text"
|
|
||||||
height="200px"
|
|
||||||
src="https://picsum.photos/g/400/200/"
|
|
||||||
>
|
|
||||||
<v-container fill-height fluid>
|
|
||||||
<v-layout fill-height>
|
|
||||||
<v-flex xs12 align-end flexbox>
|
|
||||||
<span class="headline black--text">{{ event.title }}</span>
|
|
||||||
</v-flex>
|
|
||||||
</v-layout>
|
|
||||||
</v-container>
|
|
||||||
</v-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.organizerActor.preferredUsername } }">
|
|
||||||
<v-avatar size="25px">
|
|
||||||
<img class="img-circle elevation-7 mb-1"
|
|
||||||
:src="event.organizerActor.avatarUrl"
|
|
||||||
>
|
|
||||||
</v-avatar>
|
|
||||||
</router-link>
|
|
||||||
<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="error">
|
|
||||||
No events found
|
|
||||||
</v-alert>
|
|
||||||
</v-flex>
|
|
||||||
</v-layout>
|
|
||||||
</v-container>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import ngeohash from 'ngeohash';
|
|
||||||
import { AUTH_USER_ACTOR, AUTH_USER_ID } from '@/constants';
|
|
||||||
import { FETCH_EVENTS } from '@/graphql/event';
|
|
||||||
import { Component, Vue } from 'vue-property-decorator';
|
|
||||||
import { ICurrentUser } from '@/types/current-user.model';
|
|
||||||
import { CURRENT_USER_CLIENT } from '@/graphql/user';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
apollo: {
|
|
||||||
events: {
|
|
||||||
query: FETCH_EVENTS,
|
|
||||||
},
|
|
||||||
currentUser: {
|
|
||||||
query: CURRENT_USER_CLIENT,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
export default class Home extends Vue {
|
|
||||||
gradient = 'to top right, rgba(63,81,181, .7), rgba(25,32,72, .7)';
|
|
||||||
searchTerm = null;
|
|
||||||
location_field = {
|
|
||||||
loading: false,
|
|
||||||
search: null,
|
|
||||||
};
|
|
||||||
events = [];
|
|
||||||
locations = [];
|
|
||||||
city = { name: null };
|
|
||||||
country = { name: null };
|
|
||||||
// FIXME: correctly parse local storage
|
|
||||||
actor = JSON.parse(localStorage.getItem(AUTH_USER_ACTOR) || '{}');
|
|
||||||
currentUser!: ICurrentUser;
|
|
||||||
|
|
||||||
get displayed_name() {
|
|
||||||
return this.actor.name === null ? this.actor.preferredUsername : this.actor.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
fetchLocations() {
|
|
||||||
// FIXME: remove eventFetch
|
|
||||||
// eventFetch('/locations', this.$store)
|
|
||||||
// .then(response => (response.json()))
|
|
||||||
// .then((response) => {
|
|
||||||
// this.locations = response;
|
|
||||||
// });
|
|
||||||
}
|
|
||||||
|
|
||||||
geoLocalize() {
|
|
||||||
const router = this.$router;
|
|
||||||
const sessionCity = sessionStorage.getItem('City');
|
|
||||||
if (sessionCity) {
|
|
||||||
router.push({ name: 'EventList', params: { location: sessionCity } });
|
|
||||||
} else {
|
|
||||||
navigator.geolocation.getCurrentPosition((pos) => {
|
|
||||||
const crd = pos.coords;
|
|
||||||
|
|
||||||
const geohash = ngeohash.encode(crd.latitude, crd.longitude, 11);
|
|
||||||
sessionStorage.setItem('City', geohash);
|
|
||||||
router.push({ name: 'EventList', params: { location: geohash } });
|
|
||||||
}, err => console.warn(`ERROR(${err.code}): ${err.message}`), {
|
|
||||||
enableHighAccuracy: true,
|
|
||||||
timeout: 5000,
|
|
||||||
maximumAge: 0,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getAddressData(addressData) {
|
|
||||||
const geohash = ngeohash.encode(addressData.latitude, addressData.longitude, 11);
|
|
||||||
sessionStorage.setItem('City', geohash);
|
|
||||||
this.$router.push({ name: 'EventList', params: { location: geohash } });
|
|
||||||
}
|
|
||||||
|
|
||||||
viewEvent(event) {
|
|
||||||
this.$router.push({ name: 'Event', params: { uuid: event.uuid } });
|
|
||||||
}
|
|
||||||
|
|
||||||
ipLocation() {
|
|
||||||
return this.city.name ? this.city.name : this.country.name;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<!-- Add "scoped" attribute to limit CSS to this component only -->
|
|
||||||
<style scoped>
|
|
||||||
.search-autocomplete {
|
|
||||||
border: 1px solid #dbdbdb;
|
|
||||||
color: rgba(0, 0, 0, .87);
|
|
||||||
}
|
|
||||||
|
|
||||||
.events-nearby {
|
|
||||||
margin-bottom: 25px;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,51 +0,0 @@
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
|
|
||||||
<!--<gmap-autocomplete :value="description" @input="setPlace"
|
|
||||||
@place_changed="setPlace">
|
|
||||||
</gmap-autocomplete>
|
|
||||||
<br />
|
|
||||||
|
|
||||||
<gmap-map
|
|
||||||
:center="center"
|
|
||||||
:zoom="15"
|
|
||||||
style="width: 500px; height: 300px"
|
|
||||||
>
|
|
||||||
<gmap-marker
|
|
||||||
:key="index"
|
|
||||||
v-for="(m, index) in markers"
|
|
||||||
:position="m.position"
|
|
||||||
:clickable="true"
|
|
||||||
:draggable="true"
|
|
||||||
@click="center=m.position"
|
|
||||||
></gmap-marker>
|
|
||||||
</gmap-map>-->
|
|
||||||
{{ center.lat }} - {{ center.lng }}
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { Component, Prop, Vue } from 'vue-property-decorator';
|
|
||||||
|
|
||||||
@Component
|
|
||||||
export default class Location extends Vue {
|
|
||||||
@Prop(String) address!: string;
|
|
||||||
|
|
||||||
description = 'Paris, France';
|
|
||||||
center = { lat: 48.85, lng: 2.35 };
|
|
||||||
markers: any[] = [];
|
|
||||||
|
|
||||||
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>
|
|
|
@ -1,110 +1,56 @@
|
||||||
<template>
|
<template>
|
||||||
<v-toolbar
|
<nav class="navbar is-fixed-top" role="navigation" aria-label="main navigation">
|
||||||
class="blue darken-3"
|
<div class="navbar-brand">
|
||||||
dark
|
<router-link class="navbar-item" :to="{ name: 'Home' }">Mobilizon</router-link>
|
||||||
app
|
|
||||||
:clipped-left="$vuetify.breakpoint.lgAndUp"
|
|
||||||
fixed
|
|
||||||
>
|
|
||||||
<v-toolbar-title style="width: 300px" class="ml-0 pl-3 white--text">
|
|
||||||
<v-toolbar-side-icon @click.stop="toggleDrawer()"></v-toolbar-side-icon>
|
|
||||||
<router-link :to="{ name: 'Home' }" class="hidden-sm-and-down white--text">Mobilizon
|
|
||||||
</router-link>
|
|
||||||
</v-toolbar-title>
|
|
||||||
<v-autocomplete
|
|
||||||
:loading="$apollo.loading"
|
|
||||||
flat
|
|
||||||
solo-inverted
|
|
||||||
prepend-icon="search"
|
|
||||||
:label="$gettext('Search')"
|
|
||||||
required
|
|
||||||
item-text="label"
|
|
||||||
class="hidden-sm-and-down"
|
|
||||||
:items="items"
|
|
||||||
:search-input.sync="searchText"
|
|
||||||
@keyup.enter="enter"
|
|
||||||
v-model="model"
|
|
||||||
return-object
|
|
||||||
>
|
|
||||||
<template slot="item" slot-scope="data">
|
|
||||||
<!-- <div>{{ data }}</div> -->
|
|
||||||
<v-list-tile v-if="data.item.__typename === 'Event'">
|
|
||||||
<v-list-tile-avatar>
|
|
||||||
<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-content>
|
|
||||||
</v-list-tile>
|
|
||||||
</template>
|
|
||||||
</v-autocomplete>
|
|
||||||
<v-spacer></v-spacer>
|
|
||||||
|
|
||||||
<span v-if="currentUser.id" @click="logout()">Logout</span>
|
<a
|
||||||
|
role="button"
|
||||||
<v-menu
|
class="navbar-burger burger"
|
||||||
offset-y
|
aria-label="menu"
|
||||||
:close-on-content-click="false"
|
aria-expanded="false"
|
||||||
:nudge-width="200"
|
data-target="navbarBasicExample"
|
||||||
v-model="notificationMenu"
|
>
|
||||||
v-if="currentUser.id"
|
<span aria-hidden="true"></span>
|
||||||
>
|
<span aria-hidden="true"></span>
|
||||||
<v-btn icon slot="activator">
|
<span aria-hidden="true"></span>
|
||||||
<v-badge left color="red">
|
</a>
|
||||||
<span slot="badge">{{ notifications.length }}</span>
|
</div>
|
||||||
<v-icon>notifications</v-icon>
|
<div class="navbar-end">
|
||||||
</v-badge>
|
<div class="navbar-item">
|
||||||
</v-btn>
|
<div class="buttons">
|
||||||
<v-card>
|
<router-link class="button is-primary" v-if="!currentUser.id" :to="{ name: 'Register' }">
|
||||||
<v-list two-line>
|
<strong>
|
||||||
<template v-for="item in notifications">
|
<translate>Sign up</translate>
|
||||||
<v-subheader v-if="item.header" v-text="item.header" v-bind:key="item.header"></v-subheader>
|
</strong>
|
||||||
<v-divider v-else-if="item.divider" v-bind:inset="item.inset" v-bind:key="item.inset"></v-divider>
|
</router-link>
|
||||||
<v-list-tile avatar v-else v-bind:key="item.title">
|
<router-link class="button is-light" v-if="!currentUser.id" :to="{ name: 'Login' }">
|
||||||
<v-list-tile-content>
|
<translate>Log in</translate>
|
||||||
<v-list-tile-title v-html="item.title"></v-list-tile-title>
|
</router-link>
|
||||||
<v-list-tile-sub-title v-html="item.subtitle"></v-list-tile-sub-title>
|
<router-link
|
||||||
</v-list-tile-content>
|
class="button is-light"
|
||||||
</v-list-tile>
|
v-if="currentUser.id"
|
||||||
</template>
|
:to="{ name: 'Profile', params: { name: loggedPerson.preferredUsername} }"
|
||||||
</v-list>
|
>
|
||||||
<v-card-actions>
|
<figure class="image is-24x24">
|
||||||
<v-spacer></v-spacer>
|
<img :src="loggedPerson.avatarUrl">
|
||||||
<v-btn flat @click="notificationMenu = false">
|
</figure>
|
||||||
<translate>Close</translate>
|
<span>{{ loggedPerson.preferredUsername }}</span>
|
||||||
</v-btn>
|
</router-link>
|
||||||
<v-btn color="primary" flat @click="notificationMenu = false">
|
</div>
|
||||||
<translate>Save</translate>
|
</div>
|
||||||
</v-btn>
|
</div>
|
||||||
</v-card-actions>
|
</nav>
|
||||||
</v-card>
|
|
||||||
</v-menu>
|
|
||||||
<v-btn v-if="!currentUser.id" :to="{ name: 'Login' }">
|
|
||||||
<translate>Login</translate>
|
|
||||||
</v-btn>
|
|
||||||
</v-toolbar>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style>
|
|
||||||
nav.v-toolbar .v-input__slot {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Prop, Vue, Watch } from 'vue-property-decorator';
|
import { Component, Prop, Vue, Watch } from 'vue-property-decorator';
|
||||||
import { AUTH_USER_ACTOR } from '@/constants';
|
import { AUTH_USER_ACTOR } from '@/constants';
|
||||||
import { SEARCH } from '@/graphql/search';
|
import { SEARCH } from '@/graphql/search';
|
||||||
import { CURRENT_USER_CLIENT } from '@/graphql/user';
|
import { CURRENT_USER_CLIENT } from '@/graphql/user';
|
||||||
import { onLogout } from '@/vue-apollo';
|
import { onLogout } from '@/vue-apollo';
|
||||||
import { deleteUserData } from '@/utils/auth';
|
import { deleteUserData } from '@/utils/auth';
|
||||||
|
import { LOGGED_PERSON } from "@/graphql/actor";
|
||||||
|
import { IPerson } from "../types/actor.model";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
apollo: {
|
apollo: {
|
||||||
|
@ -121,30 +67,32 @@
|
||||||
},
|
},
|
||||||
currentUser: {
|
currentUser: {
|
||||||
query: CURRENT_USER_CLIENT
|
query: CURRENT_USER_CLIENT
|
||||||
|
},
|
||||||
|
loggedPerson: {
|
||||||
|
query: LOGGED_PERSON
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
export default class NavBar extends Vue {
|
export default class NavBar extends Vue {
|
||||||
@Prop({ required: true, type: Function }) toggleDrawer!: Function;
|
|
||||||
|
|
||||||
notificationMenu = false;
|
|
||||||
notifications = [
|
notifications = [
|
||||||
{ header: 'Coucou' },
|
{ header: "Coucou" },
|
||||||
{ title: 'T\'as une notification', subtitle: 'Et elle est cool' },
|
{ title: "T'as une notification", subtitle: "Et elle est cool" }
|
||||||
];
|
];
|
||||||
model = null;
|
model = null;
|
||||||
search: any[] = [];
|
search: any[] = [];
|
||||||
searchText: string | null = null;
|
searchText: string | null = null;
|
||||||
searchSelect = null;
|
searchSelect = null;
|
||||||
actor = localStorage.getItem(AUTH_USER_ACTOR);
|
loggedPerson!: IPerson;
|
||||||
|
|
||||||
get items() {
|
get items() {
|
||||||
return this.search.map(searchEntry => {
|
return this.search.map(searchEntry => {
|
||||||
switch (searchEntry.__typename) {
|
switch (searchEntry.__typename) {
|
||||||
case 'Actor':
|
case "Actor":
|
||||||
searchEntry.label = searchEntry.preferredUsername + (searchEntry.domain === null ? '' : `@${searchEntry.domain}`);
|
searchEntry.label =
|
||||||
|
searchEntry.preferredUsername +
|
||||||
|
(searchEntry.domain === null ? "" : `@${searchEntry.domain}`);
|
||||||
break;
|
break;
|
||||||
case 'Event':
|
case "Event":
|
||||||
searchEntry.label = searchEntry.title;
|
searchEntry.label = searchEntry.title;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -152,25 +100,31 @@ export default class NavBar extends Vue {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Watch('model')
|
@Watch("model")
|
||||||
onModelChanged(val) {
|
onModelChanged(val) {
|
||||||
switch (val.__typename) {
|
switch (val.__typename) {
|
||||||
case 'Event':
|
case "Event":
|
||||||
this.$router.push({ name: 'Event', params: { uuid: val.uuid } });
|
this.$router.push({ name: "Event", params: { uuid: val.uuid } });
|
||||||
break;
|
break;
|
||||||
case 'Actor':
|
case "Actor":
|
||||||
this.$router.push({ name: 'Account', params: { name: this.username_with_domain(val) } });
|
this.$router.push({
|
||||||
|
name: "Profile",
|
||||||
|
params: { name: this.username_with_domain(val) }
|
||||||
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
username_with_domain(actor) {
|
username_with_domain(actor) {
|
||||||
return actor.preferredUsername + (actor.domain === null ? '' : `@${actor.domain}`);
|
return (
|
||||||
|
actor.preferredUsername +
|
||||||
|
(actor.domain === null ? "" : `@${actor.domain}`)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
enter() {
|
enter() {
|
||||||
console.log('enter');
|
console.log("enter");
|
||||||
this.$apollo.queries['search'].refetch();
|
this.$apollo.queries["search"].refetch();
|
||||||
}
|
}
|
||||||
|
|
||||||
logout() {
|
logout() {
|
||||||
|
|
|
@ -1,10 +0,0 @@
|
||||||
<template>
|
|
||||||
<v-container>
|
|
||||||
<v-layout row>
|
|
||||||
<v-flex xs12 sm6 offset-sm3>
|
|
||||||
<h1>404 !</h1>
|
|
||||||
<img src="../assets/oh_no.jpg" />
|
|
||||||
</v-flex>
|
|
||||||
</v-layout>
|
|
||||||
</v-container>
|
|
||||||
</template>
|
|
|
@ -1,14 +1,9 @@
|
||||||
import gql from 'graphql-tag';
|
import gql from 'graphql-tag';
|
||||||
|
|
||||||
export const FETCH_ACTOR = gql`
|
export const FETCH_PERSON = gql`
|
||||||
query($name:String!) {
|
query($name:String!) {
|
||||||
actor(preferredUsername: $name) {
|
person(preferredUsername: $name) {
|
||||||
url,
|
url,
|
||||||
outboxUrl,
|
|
||||||
inboxUrl,
|
|
||||||
followingUrl,
|
|
||||||
followersUrl,
|
|
||||||
sharedInboxUrl,
|
|
||||||
name,
|
name,
|
||||||
domain,
|
domain,
|
||||||
summary,
|
summary,
|
||||||
|
@ -18,22 +13,36 @@ query($name:String!) {
|
||||||
bannerUrl,
|
bannerUrl,
|
||||||
organizedEvents {
|
organizedEvents {
|
||||||
uuid,
|
uuid,
|
||||||
title,
|
title
|
||||||
description,
|
|
||||||
organizer_actor {
|
|
||||||
avatarUrl,
|
|
||||||
preferred_username,
|
|
||||||
name,
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const LOGGED_ACTOR = gql`
|
export const LOGGED_PERSON = gql`
|
||||||
query {
|
query {
|
||||||
loggedActor {
|
loggedPerson {
|
||||||
|
id,
|
||||||
avatarUrl,
|
avatarUrl,
|
||||||
preferredUsername,
|
preferredUsername,
|
||||||
}
|
}
|
||||||
}`;
|
}`;
|
||||||
|
|
||||||
|
export const IDENTITIES = gql`
|
||||||
|
query {
|
||||||
|
identities {
|
||||||
|
avatarUrl,
|
||||||
|
preferredUsername,
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
export const CREATE_PERSON = gql`
|
||||||
|
mutation CreatePerson($preferredUsername: String!) {
|
||||||
|
createPerson(preferredUsername: $preferredUsername) {
|
||||||
|
preferredUsername,
|
||||||
|
name,
|
||||||
|
avatarUrl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
|
@ -3,13 +3,14 @@ import gql from 'graphql-tag';
|
||||||
export const FETCH_EVENT = gql`
|
export const FETCH_EVENT = gql`
|
||||||
query($uuid:UUID!) {
|
query($uuid:UUID!) {
|
||||||
event(uuid: $uuid) {
|
event(uuid: $uuid) {
|
||||||
|
id,
|
||||||
uuid,
|
uuid,
|
||||||
url,
|
url,
|
||||||
local,
|
local,
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
begins_on,
|
beginsOn,
|
||||||
ends_on,
|
endsOn,
|
||||||
status,
|
status,
|
||||||
visibility,
|
visibility,
|
||||||
thumbnail,
|
thumbnail,
|
||||||
|
@ -22,11 +23,11 @@ export const FETCH_EVENT = gql`
|
||||||
preferredUsername,
|
preferredUsername,
|
||||||
name,
|
name,
|
||||||
},
|
},
|
||||||
attributedTo {
|
# attributedTo {
|
||||||
avatarUrl,
|
# # avatarUrl,
|
||||||
preferredUsername,
|
# preferredUsername,
|
||||||
name,
|
# name,
|
||||||
},
|
# },
|
||||||
participants {
|
participants {
|
||||||
actor {
|
actor {
|
||||||
avatarUrl,
|
avatarUrl,
|
||||||
|
@ -45,13 +46,14 @@ export const FETCH_EVENT = gql`
|
||||||
export const FETCH_EVENTS = gql`
|
export const FETCH_EVENTS = gql`
|
||||||
query {
|
query {
|
||||||
events {
|
events {
|
||||||
|
id,
|
||||||
uuid,
|
uuid,
|
||||||
url,
|
url,
|
||||||
local,
|
local,
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
begins_on,
|
beginsOn,
|
||||||
ends_on,
|
endsOn,
|
||||||
status,
|
status,
|
||||||
visibility,
|
visibility,
|
||||||
thumbnail,
|
thumbnail,
|
||||||
|
@ -72,16 +74,24 @@ export const FETCH_EVENTS = gql`
|
||||||
category {
|
category {
|
||||||
title,
|
title,
|
||||||
},
|
},
|
||||||
|
participants {
|
||||||
|
role,
|
||||||
|
actor {
|
||||||
|
preferredUsername,
|
||||||
|
avatarUrl,
|
||||||
|
name
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const CREATE_EVENT = gql`
|
export const CREATE_EVENT = gql`
|
||||||
mutation CreateEvent(
|
mutation CreateEvent(
|
||||||
$title: String!,
|
$title: String!,
|
||||||
$description: String!,
|
$description: String!,
|
||||||
$organizerActorId: Int!,
|
$organizerActorId: String!,
|
||||||
$categoryId: Int!,
|
$category: String!,
|
||||||
$beginsOn: DateTime!
|
$beginsOn: DateTime!
|
||||||
) {
|
) {
|
||||||
createEvent(
|
createEvent(
|
||||||
|
@ -89,8 +99,12 @@ export const CREATE_EVENT = gql`
|
||||||
description: $description,
|
description: $description,
|
||||||
beginsOn: $beginsOn,
|
beginsOn: $beginsOn,
|
||||||
organizerActorId: $organizerActorId,
|
organizerActorId: $organizerActorId,
|
||||||
categoryId: $categoryId
|
category: $category
|
||||||
)
|
) {
|
||||||
|
id,
|
||||||
|
uuid,
|
||||||
|
title
|
||||||
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
@ -106,3 +120,15 @@ export const EDIT_EVENT = gql`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const JOIN_EVENT = gql`
|
||||||
|
mutation JoinEvent(
|
||||||
|
$uuid: String!,
|
||||||
|
$username: String!
|
||||||
|
) {
|
||||||
|
joinEvent(
|
||||||
|
uuid: $uuid,
|
||||||
|
username: $username
|
||||||
|
)
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
|
@ -7,7 +7,7 @@ msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: mobilizon 0.1.0\n"
|
"Project-Id-Version: mobilizon 0.1.0\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2018-10-24 16:25+0200\n"
|
"POT-Creation-Date: 2019-01-17 16:08+0100\n"
|
||||||
"PO-Revision-Date: 2018-10-24 16:25+0200\n"
|
"PO-Revision-Date: 2018-10-24 16:25+0200\n"
|
||||||
"Last-Translator: Automatically generated\n"
|
"Last-Translator: Automatically generated\n"
|
||||||
"Language-Team: none\n"
|
"Language-Team: none\n"
|
||||||
|
@ -17,14 +17,178 @@ msgstr ""
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||||
|
|
||||||
#: src/components/Account/Register.vue:70
|
#: src/App.vue:8
|
||||||
|
msgid "© The Mobilizon Contributors %{date} - Made with Elixir, Phoenix, VueJS & with some love and some weeks"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/Account/Register.vue:89
|
||||||
msgid "A validation email was sent to %{email}"
|
msgid "A validation email was sent to %{email}"
|
||||||
msgstr "A validation email was sent to %{email}"
|
msgstr "A validation email was sent to %{email}"
|
||||||
|
|
||||||
#: src/components/Account/Register.vue:71
|
#: src/components/Account/Register.vue:26
|
||||||
|
msgid "About this instance"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/Account/Register.vue:92
|
||||||
msgid "Before you can login, you need to click on the link inside it to validate your account"
|
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"
|
msgstr "Before you can login, you need to click on the link inside it to validate your account"
|
||||||
|
|
||||||
#: src/components/Home.vue:14
|
#: src/components/Category/Create.vue:7
|
||||||
|
msgid "Create a new category"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/Category/Create.vue:34
|
||||||
|
msgid "Create category"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/Account/Register.vue:16
|
||||||
|
msgid "Create your communities and your events"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/Account/Profile.vue:48 src/components/Category/List.vue:21
|
||||||
|
#: src/components/Event/Event.vue:41
|
||||||
|
msgid "Delete"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/Account/Register.vue:80
|
||||||
|
msgid "Didn't receive the instructions ?"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/Event/Event.vue:36
|
||||||
|
msgid "Download"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/Event/Event.vue:31
|
||||||
|
msgid "Edit"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/Account/Validate.vue:8
|
||||||
|
msgid "Error while validating account"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/Category/List.vue:18
|
||||||
|
msgid "Explore"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/Account/Register.vue:14
|
||||||
|
msgid "Features"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/Account/Login.vue:46
|
||||||
|
msgid "Forgot your password ?"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/Account/Register.vue:20
|
||||||
|
msgid ""
|
||||||
|
"Learn more on\n"
|
||||||
|
" <a target=\"_blank\" href=\"https://joinmobilizon.org\">joinmobilizon.org</a>"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/NavBar.vue:26
|
||||||
|
msgid "Log in"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/Account/Login.vue:38
|
||||||
|
msgid "Login"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/Account/Register.vue:32
|
||||||
|
msgid "meditate a bit"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/Home.vue:33
|
||||||
|
msgid "No events found"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/Account/Profile.vue:29
|
||||||
|
msgid "Organized"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/Account/Register.vue:17
|
||||||
|
msgid "Other stuff…"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/Account/SendPasswordReset.vue:4
|
||||||
|
msgid "Password reset"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/Account/Register.vue:31
|
||||||
|
msgid "Please be nice to each other"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/Account/ResendConfirmation.vue:21
|
||||||
|
#: src/components/Account/SendPasswordReset.vue:22
|
||||||
|
msgid "Please check you spam folder if you didn't receive the email."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/Account/Register.vue:35
|
||||||
|
msgid "Please read the full rules"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/Account/Register.vue:72 src/components/Home.vue:9
|
||||||
msgid "Register"
|
msgid "Register"
|
||||||
msgstr "Register"
|
msgstr "Register"
|
||||||
|
|
||||||
|
#: src/components/Account/Register.vue:5
|
||||||
|
msgid "Register an account on Mobilizon!"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/Account/ResendConfirmation.vue:4
|
||||||
|
msgid "Resend confirmation email"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/Account/PasswordReset.vue:26
|
||||||
|
msgid "Reset my password"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/Account/ResendConfirmation.vue:11
|
||||||
|
msgid "Send confirmation email again"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/Account/SendPasswordReset.vue:12
|
||||||
|
msgid "Send email to reset my password"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/NavBar.vue:22
|
||||||
|
msgid "Sign up"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/Account/Profile.vue:43
|
||||||
|
msgid "User logout"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/Event/Event.vue:50
|
||||||
|
msgid "Vous avez annoncé aller à cet événement."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/Event/Event.vue:46
|
||||||
|
msgid "Vous êtes organisateur de cet événement."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/Account/SendPasswordReset.vue:17
|
||||||
|
msgid "We just sent an email to %{email}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/Account/ResendConfirmation.vue:16
|
||||||
|
msgid "We just sent another confirmation email to %{email}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/Home.vue:16
|
||||||
|
msgid "Welcome back %{username}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/Account/Login.vue:4
|
||||||
|
msgid "Welcome back!"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/Account/Validate.vue:12
|
||||||
|
msgid "Your account has been validated"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/Account/Validate.vue:3
|
||||||
|
msgid "Your account is being validated"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/Account/Register.vue:28
|
||||||
|
msgid "Your local administrator resumed it's policy:"
|
||||||
|
msgstr ""
|
||||||
|
|
30
js/src/i18n/locale/en_US/LC_MESSAGES/app.po~
Normal file
30
js/src/i18n/locale/en_US/LC_MESSAGES/app.po~
Normal 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"
|
|
@ -7,7 +7,7 @@ msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: mobilizon 0.1.0\n"
|
"Project-Id-Version: mobilizon 0.1.0\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2018-10-24 16:25+0200\n"
|
"POT-Creation-Date: 2019-01-17 16:08+0100\n"
|
||||||
"PO-Revision-Date: 2018-10-24 16:25+0200\n"
|
"PO-Revision-Date: 2018-10-24 16:25+0200\n"
|
||||||
"Last-Translator: Automatically generated\n"
|
"Last-Translator: Automatically generated\n"
|
||||||
"Language-Team: none\n"
|
"Language-Team: none\n"
|
||||||
|
@ -17,14 +17,178 @@ msgstr ""
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
|
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
|
||||||
|
|
||||||
#: src/components/Account/Register.vue:70
|
#: src/App.vue:8
|
||||||
|
msgid "© The Mobilizon Contributors %{date} - Made with Elixir, Phoenix, VueJS & with some love and some weeks"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/Account/Register.vue:89
|
||||||
msgid "A validation email was sent to %{email}"
|
msgid "A validation email was sent to %{email}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/Account/Register.vue:71
|
#: src/components/Account/Register.vue:26
|
||||||
|
msgid "About this instance"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/Account/Register.vue:92
|
||||||
msgid "Before you can login, you need to click on the link inside it to validate your account"
|
msgid "Before you can login, you need to click on the link inside it to validate your account"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/Home.vue:14
|
#: src/components/Category/Create.vue:7
|
||||||
|
msgid "Create a new category"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/Category/Create.vue:34
|
||||||
|
msgid "Create category"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/Account/Register.vue:16
|
||||||
|
msgid "Create your communities and your events"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/Account/Profile.vue:48 src/components/Category/List.vue:21
|
||||||
|
#: src/components/Event/Event.vue:41
|
||||||
|
msgid "Delete"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/Account/Register.vue:80
|
||||||
|
msgid "Didn't receive the instructions ?"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/Event/Event.vue:36
|
||||||
|
msgid "Download"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/Event/Event.vue:31
|
||||||
|
msgid "Edit"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/Account/Validate.vue:8
|
||||||
|
msgid "Error while validating account"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/Category/List.vue:18
|
||||||
|
msgid "Explore"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/Account/Register.vue:14
|
||||||
|
msgid "Features"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/Account/Login.vue:46
|
||||||
|
msgid "Forgot your password ?"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/Account/Register.vue:20
|
||||||
|
msgid ""
|
||||||
|
"Learn more on\n"
|
||||||
|
" <a target=\"_blank\" href=\"https://joinmobilizon.org\">joinmobilizon.org</a>"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/NavBar.vue:26
|
||||||
|
msgid "Log in"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/Account/Login.vue:38
|
||||||
|
msgid "Login"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/Account/Register.vue:32
|
||||||
|
msgid "meditate a bit"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/Home.vue:33
|
||||||
|
msgid "No events found"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/Account/Profile.vue:29
|
||||||
|
msgid "Organized"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/Account/Register.vue:17
|
||||||
|
msgid "Other stuff…"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/Account/SendPasswordReset.vue:4
|
||||||
|
msgid "Password reset"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/Account/Register.vue:31
|
||||||
|
msgid "Please be nice to each other"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/Account/ResendConfirmation.vue:21
|
||||||
|
#: src/components/Account/SendPasswordReset.vue:22
|
||||||
|
msgid "Please check you spam folder if you didn't receive the email."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/Account/Register.vue:35
|
||||||
|
msgid "Please read the full rules"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/Account/Register.vue:72 src/components/Home.vue:9
|
||||||
msgid "Register"
|
msgid "Register"
|
||||||
msgstr "S'inscrire"
|
msgstr "S'inscrire"
|
||||||
|
|
||||||
|
#: src/components/Account/Register.vue:5
|
||||||
|
msgid "Register an account on Mobilizon!"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/Account/ResendConfirmation.vue:4
|
||||||
|
msgid "Resend confirmation email"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/Account/PasswordReset.vue:26
|
||||||
|
msgid "Reset my password"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/Account/ResendConfirmation.vue:11
|
||||||
|
msgid "Send confirmation email again"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/Account/SendPasswordReset.vue:12
|
||||||
|
msgid "Send email to reset my password"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/NavBar.vue:22
|
||||||
|
msgid "Sign up"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/Account/Profile.vue:43
|
||||||
|
msgid "User logout"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/Event/Event.vue:50
|
||||||
|
msgid "Vous avez annoncé aller à cet événement."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/Event/Event.vue:46
|
||||||
|
msgid "Vous êtes organisateur de cet événement."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/Account/SendPasswordReset.vue:17
|
||||||
|
msgid "We just sent an email to %{email}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/Account/ResendConfirmation.vue:16
|
||||||
|
msgid "We just sent another confirmation email to %{email}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/Home.vue:16
|
||||||
|
msgid "Welcome back %{username}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/Account/Login.vue:4
|
||||||
|
msgid "Welcome back!"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/Account/Validate.vue:12
|
||||||
|
msgid "Your account has been validated"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/Account/Validate.vue:3
|
||||||
|
msgid "Your account is being validated"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/Account/Register.vue:28
|
||||||
|
msgid "Your local administrator resumed it's policy:"
|
||||||
|
msgstr ""
|
||||||
|
|
|
@ -11,7 +11,7 @@ msgstr ""
|
||||||
"PO-Revision-Date: 2018-10-24 16:25+0200\n"
|
"PO-Revision-Date: 2018-10-24 16:25+0200\n"
|
||||||
"Last-Translator: Automatically generated\n"
|
"Last-Translator: Automatically generated\n"
|
||||||
"Language-Team: none\n"
|
"Language-Team: none\n"
|
||||||
"Language: fr\n"
|
"Language: fr_FR\n"
|
||||||
"MIME-Version: 1.0\n"
|
"MIME-Version: 1.0\n"
|
||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
|
|
@ -3,10 +3,9 @@
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
// import * as VueGoogleMaps from 'vue2-google-maps';
|
// import * as VueGoogleMaps from 'vue2-google-maps';
|
||||||
import VueMarkdown from 'vue-markdown';
|
import VueMarkdown from 'vue-markdown';
|
||||||
import Vuetify from 'vuetify';
|
import Buefy from 'buefy'
|
||||||
|
import 'buefy/dist/buefy.css';
|
||||||
import GetTextPlugin from 'vue-gettext';
|
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 App from '@/App.vue';
|
||||||
import router from '@/router';
|
import router from '@/router';
|
||||||
import { apolloProvider } from './vue-apollo';
|
import { apolloProvider } from './vue-apollo';
|
||||||
|
@ -16,7 +15,9 @@ const translations = require('@/i18n/translations.json');
|
||||||
Vue.config.productionTip = false;
|
Vue.config.productionTip = false;
|
||||||
|
|
||||||
Vue.use(VueMarkdown);
|
Vue.use(VueMarkdown);
|
||||||
Vue.use(Vuetify);
|
Vue.use(Buefy, {
|
||||||
|
defaultContainerElement: '#mobilizon'
|
||||||
|
});
|
||||||
|
|
||||||
const language = (window.navigator as any).userLanguage || window.navigator.language;
|
const language = (window.navigator as any).userLanguage || window.navigator.language;
|
||||||
|
|
||||||
|
|
|
@ -1,24 +1,24 @@
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import Router from 'vue-router';
|
import Router from 'vue-router';
|
||||||
import PageNotFound from '@/components/PageNotFound.vue';
|
import PageNotFound from '@/views/PageNotFound.vue';
|
||||||
import Home from '@/components/Home.vue';
|
import Home from '@/views/Home.vue';
|
||||||
import Event from '@/components/Event/Event.vue';
|
import Event from '@/views/Event/Event.vue';
|
||||||
import EventList from '@/components/Event/EventList.vue';
|
import EventList from '@/views/Event/EventList.vue';
|
||||||
import Location from '@/components/Location.vue';
|
import Location from '@/views/Location.vue';
|
||||||
import CreateEvent from '@/components/Event/Create.vue';
|
import CreateEvent from '@/views/Event/Create.vue';
|
||||||
import CategoryList from '@/components/Category/List.vue';
|
import CategoryList from '@/views/Category/List.vue';
|
||||||
import CreateCategory from '@/components/Category/Create.vue';
|
import CreateCategory from '@/views/Category/Create.vue';
|
||||||
import Register from '@/components/Account/Register.vue';
|
import Register from '@/views/Account/Register.vue';
|
||||||
import Login from '@/components/Account/Login.vue';
|
import Login from '@/views/User/Login.vue';
|
||||||
import Validate from '@/components/Account/Validate.vue';
|
import Validate from '@/views/User/Validate.vue';
|
||||||
import ResendConfirmation from '@/components/Account/ResendConfirmation.vue';
|
import ResendConfirmation from '@/views/User/ResendConfirmation.vue';
|
||||||
import SendPasswordReset from '@/components/Account/SendPasswordReset.vue';
|
import SendPasswordReset from '@/views/User/SendPasswordReset.vue';
|
||||||
import PasswordReset from '@/components/Account/PasswordReset.vue';
|
import PasswordReset from '@/views/User/PasswordReset.vue';
|
||||||
import Account from '@/components/Account/Account.vue';
|
import Profile from '@/views/Account/Profile.vue';
|
||||||
import CreateGroup from '@/components/Group/Create.vue';
|
import CreateGroup from '@/views/Group/Create.vue';
|
||||||
import Group from '@/components/Group/Group.vue';
|
import Group from '@/views/Group/Group.vue';
|
||||||
import GroupList from '@/components/Group/GroupList.vue';
|
import GroupList from '@/views/Group/GroupList.vue';
|
||||||
import Identities from '../components/Account/Identities.vue';
|
import Identities from '@/views/Account/Identities.vue';
|
||||||
|
|
||||||
Vue.use(Router);
|
Vue.use(Router);
|
||||||
|
|
||||||
|
@ -45,7 +45,7 @@ const router = new Router({
|
||||||
meta: { requiredAuth: true },
|
meta: { requiredAuth: true },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/events/:id(\\d+)/edit',
|
path: '/events/:id/edit',
|
||||||
name: 'EditEvent',
|
name: 'EditEvent',
|
||||||
component: CreateEvent,
|
component: CreateEvent,
|
||||||
props: true,
|
props: true,
|
||||||
|
@ -124,7 +124,7 @@ const router = new Router({
|
||||||
meta: { requiredAuth: false },
|
meta: { requiredAuth: false },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/group-create',
|
path: '/groups/create',
|
||||||
name: 'CreateGroup',
|
name: 'CreateGroup',
|
||||||
component: CreateGroup,
|
component: CreateGroup,
|
||||||
meta: { requiredAuth: true },
|
meta: { requiredAuth: true },
|
||||||
|
@ -138,8 +138,8 @@ const router = new Router({
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/@:name',
|
path: '/@:name',
|
||||||
name: 'Account',
|
name: 'Profile',
|
||||||
component: Account,
|
component: Profile,
|
||||||
props: true,
|
props: true,
|
||||||
meta: { requiredAuth: false },
|
meta: { requiredAuth: false },
|
||||||
},
|
},
|
||||||
|
|
29
js/src/types/actor.model.ts
Normal file
29
js/src/types/actor.model.ts
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
export interface IActor {
|
||||||
|
id: string;
|
||||||
|
url: string;
|
||||||
|
name: string;
|
||||||
|
domain: string;
|
||||||
|
summary: string;
|
||||||
|
preferredUsername: string;
|
||||||
|
suspended: boolean;
|
||||||
|
avatarUrl: string;
|
||||||
|
bannerUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IPerson extends IActor {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IGroup extends IActor {
|
||||||
|
members: IMember[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum MemberRole {
|
||||||
|
PENDING, MEMBER, MODERATOR, ADMIN
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IMember {
|
||||||
|
role: MemberRole;
|
||||||
|
parent: IGroup;
|
||||||
|
actor: IActor;
|
||||||
|
}
|
46
js/src/types/event.model.ts
Normal file
46
js/src/types/event.model.ts
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
import { IActor } from "./actor.model";
|
||||||
|
|
||||||
|
export enum EventStatus {
|
||||||
|
TENTATIVE, CONFIRMED, CANCELLED
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum EventVisibility {
|
||||||
|
PUBLIC, PRIVATE
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ParticipantRole {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ICategory {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
picture: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IParticipant {
|
||||||
|
role: ParticipantRole,
|
||||||
|
actor: IActor,
|
||||||
|
event: IEvent
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IEvent {
|
||||||
|
uuid: string;
|
||||||
|
url: string;
|
||||||
|
local: boolean;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
begins_on: Date;
|
||||||
|
ends_on: Date;
|
||||||
|
status: EventStatus;
|
||||||
|
visibility: EventVisibility;
|
||||||
|
thumbnail: string;
|
||||||
|
large_image: string;
|
||||||
|
publish_at: Date;
|
||||||
|
// online_address: Adress;
|
||||||
|
// phone_address: string;
|
||||||
|
organizerActor: IActor;
|
||||||
|
attributedTo: IActor;
|
||||||
|
participants: IParticipant[];
|
||||||
|
category: ICategory;
|
||||||
|
}
|
92
js/src/views/Account/Identities.vue
Normal file
92
js/src/views/Account/Identities.vue
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
<template>
|
||||||
|
<section>
|
||||||
|
<b-loading :active.sync="$apollo.loading"></b-loading>
|
||||||
|
<h1 class="title">
|
||||||
|
<translate>Identities</translate>
|
||||||
|
</h1>
|
||||||
|
<a class="button is-primary" @click="showCreateProfileForm = true">
|
||||||
|
<translate>Add a new profile</translate>
|
||||||
|
</a>
|
||||||
|
<div class="columns" v-if="showCreateProfileForm">
|
||||||
|
<form @submit="createProfile" class="column is-half">
|
||||||
|
<b-message title="Error" type="is-danger" v-for="error in errors" :key="error">{{ error }}</b-message>
|
||||||
|
<b-field label="Username">
|
||||||
|
<b-input aria-required="true" required v-model="newPerson.preferredUsername"/>
|
||||||
|
</b-field>
|
||||||
|
<button class="button is-primary">
|
||||||
|
<translate>Register</translate>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<ul>
|
||||||
|
<li v-for="identity in identities" :key="identity.id">
|
||||||
|
<hr>
|
||||||
|
<div class="media">
|
||||||
|
<div class="media-left">
|
||||||
|
<figure class="image is-48x48">
|
||||||
|
<img :src="identity.avatarUrl">
|
||||||
|
</figure>
|
||||||
|
</div>
|
||||||
|
<div class="media-content">
|
||||||
|
<p class="title is-5">
|
||||||
|
{{ identity.name }}
|
||||||
|
<span
|
||||||
|
v-if="identity.preferredUsername === loggedPerson.preferredUsername"
|
||||||
|
class="tag is-primary"
|
||||||
|
>
|
||||||
|
<translate>Current</translate>
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<p class="subtitle is-6">@{{ identity.preferredUsername }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { Component, Vue } from "vue-property-decorator";
|
||||||
|
import { IDENTITIES, LOGGED_PERSON, CREATE_PERSON } from "../../graphql/actor";
|
||||||
|
import { IPerson } from "@/types/actor.model";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
apollo: {
|
||||||
|
identities: {
|
||||||
|
query: IDENTITIES
|
||||||
|
},
|
||||||
|
loggedPerson: {
|
||||||
|
query: LOGGED_PERSON
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
export default class Identities extends Vue {
|
||||||
|
identities: IPerson[] = [];
|
||||||
|
loggedPerson!: IPerson;
|
||||||
|
newPerson!: IPerson;
|
||||||
|
showCreateProfileForm: boolean = false;
|
||||||
|
errors: string[] = [];
|
||||||
|
|
||||||
|
async createProfile(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.$apollo.mutate({
|
||||||
|
mutation: CREATE_PERSON,
|
||||||
|
variables: this.newPerson
|
||||||
|
});
|
||||||
|
this.showCreateProfileForm = false;
|
||||||
|
this.$apollo.queries.identities.refresh();
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
err.graphQLErrors.forEach(({ message }) => {
|
||||||
|
this.errors.push(message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
host() {
|
||||||
|
return `@${window.location.host}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
111
js/src/views/Account/Profile.vue
Normal file
111
js/src/views/Account/Profile.vue
Normal file
|
@ -0,0 +1,111 @@
|
||||||
|
<template>
|
||||||
|
<section>
|
||||||
|
<div class="columns">
|
||||||
|
<div class="column">
|
||||||
|
<div class="card" v-if="person">
|
||||||
|
<div class="card-image" v-if="person.bannerUrl">
|
||||||
|
<figure class="image">
|
||||||
|
<img :src="person.bannerUrl">
|
||||||
|
</figure>
|
||||||
|
</div>
|
||||||
|
<div class="card-content">
|
||||||
|
<div class="media">
|
||||||
|
<div class="media-left">
|
||||||
|
<figure class="image is-48x48">
|
||||||
|
<img :src="person.avatarUrl">
|
||||||
|
</figure>
|
||||||
|
</div>
|
||||||
|
<div class="media-content">
|
||||||
|
<p class="title">{{ person.name }}</p>
|
||||||
|
<p class="subtitle">@{{ person.preferredUsername }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="content">
|
||||||
|
<p v-html="person.summary"></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<section v-if="person.organizedEvents.length > 0">
|
||||||
|
<h2 class="subtitle">
|
||||||
|
<translate>Organized</translate>
|
||||||
|
</h2>
|
||||||
|
<div class="columns">
|
||||||
|
<EventCard
|
||||||
|
v-for="event in person.organizedEvents"
|
||||||
|
:event="event"
|
||||||
|
:hideDetails="true"
|
||||||
|
:key="event.uuid"
|
||||||
|
class="column is-one-third"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="field is-grouped">
|
||||||
|
<p class="control">
|
||||||
|
<a
|
||||||
|
class="button"
|
||||||
|
@click="logoutUser()"
|
||||||
|
v-if="loggedPerson && loggedPerson.id === person.id"
|
||||||
|
>
|
||||||
|
<translate>User logout</translate>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<p class="control">
|
||||||
|
<a
|
||||||
|
class="button"
|
||||||
|
@click="deleteProfile()"
|
||||||
|
v-if="loggedPerson && loggedPerson.id === person.id"
|
||||||
|
>
|
||||||
|
<translate>Delete</translate>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { FETCH_PERSON, LOGGED_PERSON } from "@/graphql/actor";
|
||||||
|
import { Component, Prop, Vue, Watch } from "vue-property-decorator";
|
||||||
|
import EventCard from "@/components/Event/EventCard.vue";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
apollo: {
|
||||||
|
person: {
|
||||||
|
query: FETCH_PERSON,
|
||||||
|
variables() {
|
||||||
|
return {
|
||||||
|
name: this.$route.params.name
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
loggedPerson: {
|
||||||
|
query: LOGGED_PERSON
|
||||||
|
}
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
EventCard
|
||||||
|
}
|
||||||
|
})
|
||||||
|
export default class Profile extends Vue {
|
||||||
|
@Prop({ type: String, required: true }) name!: string;
|
||||||
|
|
||||||
|
person = null;
|
||||||
|
|
||||||
|
// call again the method if the route changes
|
||||||
|
@Watch("$route")
|
||||||
|
onRouteChange() {
|
||||||
|
// this.fetchData()
|
||||||
|
}
|
||||||
|
|
||||||
|
logoutUser() {
|
||||||
|
// TODO : implement logout
|
||||||
|
this.$router.push({ name: "Home" });
|
||||||
|
}
|
||||||
|
|
||||||
|
nl2br(text) {
|
||||||
|
return text.replace(/(?:\r\n|\r|\n)/g, "<br>");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
182
js/src/views/Account/Register.vue
Normal file
182
js/src/views/Account/Register.vue
Normal file
|
@ -0,0 +1,182 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<section class="hero">
|
||||||
|
<div class="hero-body">
|
||||||
|
<h1 class="title">
|
||||||
|
<translate>Register an account on Mobilizon!</translate>
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<div class="container">
|
||||||
|
<div class="columns is-mobile">
|
||||||
|
<div class="column">
|
||||||
|
<div class="content">
|
||||||
|
<h2 class="subtitle" v-translate>Features</h2>
|
||||||
|
<ul>
|
||||||
|
<li v-translate>Create your communities and your events</li>
|
||||||
|
<li v-translate>Other stuff…</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<p v-translate>
|
||||||
|
Learn more on
|
||||||
|
<a target="_blank" href="https://joinmobilizon.org">joinmobilizon.org</a>
|
||||||
|
</p>
|
||||||
|
<hr>
|
||||||
|
<div class="content">
|
||||||
|
<h2 class="subtitle" v-translate>About this instance</h2>
|
||||||
|
<p>
|
||||||
|
<translate>Your local administrator resumed it's policy:</translate>
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li v-translate>Please be nice to each other</li>
|
||||||
|
<li v-translate>meditate a bit</li>
|
||||||
|
</ul>
|
||||||
|
<p>
|
||||||
|
<translate>Please read the full rules</translate>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="column">
|
||||||
|
<form v-if="!validationSent">
|
||||||
|
<div class="columns is-mobile is-centered">
|
||||||
|
<div class="column is-narrow">
|
||||||
|
<figure class="image is-64x64">
|
||||||
|
<transition name="avatar">
|
||||||
|
<v-gravatar v-bind="{email: credentials.email}" default-img="mp"></v-gravatar>
|
||||||
|
</transition>
|
||||||
|
</figure>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<b-field label="Email">
|
||||||
|
<b-input
|
||||||
|
aria-required="true"
|
||||||
|
required
|
||||||
|
type="email"
|
||||||
|
v-model="credentials.email"
|
||||||
|
@blur="showGravatar = true"
|
||||||
|
@focus="showGravatar = false"
|
||||||
|
/>
|
||||||
|
</b-field>
|
||||||
|
|
||||||
|
<b-field label="Username">
|
||||||
|
<b-input aria-required="true" required v-model="credentials.username"/>
|
||||||
|
</b-field>
|
||||||
|
|
||||||
|
<b-field label="Password">
|
||||||
|
<b-input
|
||||||
|
aria-required="true"
|
||||||
|
required
|
||||||
|
type="password"
|
||||||
|
password-reveal
|
||||||
|
minlength="6"
|
||||||
|
v-model="credentials.password"
|
||||||
|
/>
|
||||||
|
</b-field>
|
||||||
|
|
||||||
|
<b-field grouped>
|
||||||
|
<div class="control">
|
||||||
|
<button type="button" class="button is-primary" @click="submit()">
|
||||||
|
<translate>Register</translate>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="control">
|
||||||
|
<router-link
|
||||||
|
class="button is-text"
|
||||||
|
:to="{ name: 'ResendConfirmation', params: { email: credentials.email }}"
|
||||||
|
>
|
||||||
|
<translate>Didn't receive the instructions ?</translate>
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
<div class="control">
|
||||||
|
<router-link
|
||||||
|
class="button is-text"
|
||||||
|
:to="{ name: 'Login', params: { email: credentials.email, password: credentials.password }}"
|
||||||
|
:disabled="validationSent"
|
||||||
|
>
|
||||||
|
<translate>Login</translate>
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
</b-field>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div v-if="validationSent">
|
||||||
|
<b-message title="Success" type="is-success">
|
||||||
|
<h2>
|
||||||
|
<translate>A validation email was sent to %{email}</translate>
|
||||||
|
</h2>
|
||||||
|
<p>
|
||||||
|
<translate>Before you can login, you need to click on the link inside it to validate your account</translate>
|
||||||
|
</p>
|
||||||
|
</b-message>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import Gravatar from "vue-gravatar";
|
||||||
|
import { CREATE_USER } from "@/graphql/user";
|
||||||
|
import { Component, Prop, Vue } from "vue-property-decorator";
|
||||||
|
import { MOBILIZON_INSTANCE_HOST } from "@/api/_entrypoint";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
components: {
|
||||||
|
"v-gravatar": Gravatar
|
||||||
|
}
|
||||||
|
})
|
||||||
|
export default class Register extends Vue {
|
||||||
|
@Prop({ type: String, required: false, default: "" }) email!: string;
|
||||||
|
@Prop({ type: String, required: false, default: "" }) password!: string;
|
||||||
|
|
||||||
|
credentials = {
|
||||||
|
username: "",
|
||||||
|
email: this.email,
|
||||||
|
password: this.password
|
||||||
|
} as { username: string; email: string; password: string };
|
||||||
|
errors: string[] = [];
|
||||||
|
validationSent: boolean = false;
|
||||||
|
showGravatar: boolean = false;
|
||||||
|
|
||||||
|
host() {
|
||||||
|
return MOBILIZON_INSTANCE_HOST;
|
||||||
|
}
|
||||||
|
|
||||||
|
validEmail() {
|
||||||
|
return this.credentials.email.includes("@") === true
|
||||||
|
? "v-gravatar"
|
||||||
|
: "avatar";
|
||||||
|
}
|
||||||
|
|
||||||
|
async submit() {
|
||||||
|
try {
|
||||||
|
this.validationSent = true;
|
||||||
|
await this.$apollo.mutate({
|
||||||
|
mutation: CREATE_USER,
|
||||||
|
variables: this.credentials
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.avatar-enter-active {
|
||||||
|
transition: opacity 1s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-enter,
|
||||||
|
.avatar-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-leave {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
</style>
|
75
js/src/views/Category/Create.vue
Normal file
75
js/src/views/Category/Create.vue
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
<template>
|
||||||
|
<section>
|
||||||
|
<h1 class="title">
|
||||||
|
<translate>Create a new category</translate>
|
||||||
|
</h1>
|
||||||
|
<div class="columns">
|
||||||
|
<form class="column" @submit="submit">
|
||||||
|
<b-field :label="$gettext('Name of the category')">
|
||||||
|
<b-input aria-required="true" required v-model="category.title"/>
|
||||||
|
</b-field>
|
||||||
|
|
||||||
|
<b-field :label="$gettext('Description')">
|
||||||
|
<b-input type="textarea" v-model="category.description"/>
|
||||||
|
</b-field>
|
||||||
|
|
||||||
|
<b-field class="file">
|
||||||
|
<b-upload v-model="file" @input="onFilePicked">
|
||||||
|
<a class="button is-primary">
|
||||||
|
<b-icon icon="upload"></b-icon>
|
||||||
|
<span>
|
||||||
|
<translate>Click to upload</translate>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</b-upload>
|
||||||
|
<span class="file-name" v-if="file">{{ this.image.name }}</span>
|
||||||
|
</b-field>
|
||||||
|
|
||||||
|
<button class="button is-primary">
|
||||||
|
<translate>Create the category</translate>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { CREATE_CATEGORY } from "@/graphql/category";
|
||||||
|
import { Component, Vue } from "vue-property-decorator";
|
||||||
|
import { ICategory } from "@/types/event.model";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO : No picture is uploaded ATM
|
||||||
|
*/
|
||||||
|
|
||||||
|
@Component
|
||||||
|
export default class CreateCategory extends Vue {
|
||||||
|
category!: ICategory;
|
||||||
|
image = {
|
||||||
|
name: ""
|
||||||
|
} as { name: string };
|
||||||
|
file: any = null;
|
||||||
|
|
||||||
|
create() {
|
||||||
|
this.$apollo
|
||||||
|
.mutate({
|
||||||
|
mutation: CREATE_CATEGORY,
|
||||||
|
variables: this.category
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
console.log(data);
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error(error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO : Check if we can upload as soon as file is picked and purge files not validated
|
||||||
|
onFilePicked(e) {
|
||||||
|
if (e === undefined || e.name.lastIndexOf(".") <= 0) {
|
||||||
|
console.error("File is incorrect");
|
||||||
|
}
|
||||||
|
this.image.name = e.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
55
js/src/views/Category/List.vue
Normal file
55
js/src/views/Category/List.vue
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
<template>
|
||||||
|
<section>
|
||||||
|
<h1 class="title">
|
||||||
|
<translate>Category List</translate>
|
||||||
|
</h1>
|
||||||
|
<b-loading :active.sync="$apollo.loading"></b-loading>
|
||||||
|
<div class="columns">
|
||||||
|
<div class="column card" v-for="category in categories" :key="category.id">
|
||||||
|
<div class="card-image">
|
||||||
|
<figure class="image is-4by3">
|
||||||
|
<img v-if="category.picture.url" :src="HTTP_ENDPOINT + category.picture.url">
|
||||||
|
</figure>
|
||||||
|
</div>
|
||||||
|
<div class="card-content">
|
||||||
|
<h2 class="title is-4">{{ category.title }}</h2>
|
||||||
|
<p>{{ category.description }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { FETCH_CATEGORIES } from "@/graphql/category";
|
||||||
|
import { Component, Vue } from "vue-property-decorator";
|
||||||
|
|
||||||
|
// TODO : remove this hardcode
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
apollo: {
|
||||||
|
categories: {
|
||||||
|
query: FETCH_CATEGORIES
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
export default class List extends Vue {
|
||||||
|
categories = [];
|
||||||
|
loading = true;
|
||||||
|
HTTP_ENDPOINT = "http://localhost:4000";
|
||||||
|
|
||||||
|
deleteCategory(categoryId) {
|
||||||
|
const router = this.$router;
|
||||||
|
// FIXME: remove eventFetch
|
||||||
|
// 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 -->
|
||||||
|
<style scoped>
|
||||||
|
</style>
|
165
js/src/views/Event/Create.vue
Normal file
165
js/src/views/Event/Create.vue
Normal file
|
@ -0,0 +1,165 @@
|
||||||
|
<template>
|
||||||
|
<section>
|
||||||
|
<h1 class="title">
|
||||||
|
<translate>Create a new event</translate>
|
||||||
|
</h1>
|
||||||
|
<div v-if="$apollo.loading">Loading...</div>
|
||||||
|
<div class="columns" v-else>
|
||||||
|
<form class="column" @submit="createEvent">
|
||||||
|
<b-field :label="$gettext('Title')">
|
||||||
|
<b-input aria-required="true" required v-model="event.title"/>
|
||||||
|
</b-field>
|
||||||
|
|
||||||
|
<b-datepicker v-model="event.begins_on" inline></b-datepicker>
|
||||||
|
|
||||||
|
<b-field :label="$gettext('Category')">
|
||||||
|
<b-select placeholder="Select a category" v-model="event.category">
|
||||||
|
<option
|
||||||
|
v-for="category in categories"
|
||||||
|
:value="category"
|
||||||
|
:key="category.title"
|
||||||
|
>{{ category.title }}</option>
|
||||||
|
</b-select>
|
||||||
|
</b-field>
|
||||||
|
|
||||||
|
<button class="button is-primary">
|
||||||
|
<translate>Create my event</translate>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
// 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";
|
||||||
|
import { Component, Prop, Vue } from "vue-property-decorator";
|
||||||
|
import {
|
||||||
|
IEvent,
|
||||||
|
ICategory,
|
||||||
|
EventVisibility,
|
||||||
|
EventStatus
|
||||||
|
} from "../../types/event.model";
|
||||||
|
import { LOGGED_PERSON } from "../../graphql/actor";
|
||||||
|
import { IPerson } from "../../types/actor.model";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
components: {
|
||||||
|
VueMarkdown
|
||||||
|
},
|
||||||
|
apollo: {
|
||||||
|
categories: {
|
||||||
|
query: FETCH_CATEGORIES
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
export default class CreateEvent extends Vue {
|
||||||
|
@Prop({ required: false, type: String }) uuid!: string;
|
||||||
|
|
||||||
|
loggedPerson!: IPerson;
|
||||||
|
categories: ICategory[] = [];
|
||||||
|
event!: IEvent; // FIXME: correctly type an event
|
||||||
|
|
||||||
|
// created() {
|
||||||
|
// if (this.uuid) {
|
||||||
|
// this.fetchEvent();
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
async created() {
|
||||||
|
// We put initialization here because we need loggedPerson to be ready before initalizing event
|
||||||
|
const { data } = await this.$apollo.query({ query: LOGGED_PERSON });
|
||||||
|
|
||||||
|
this.loggedPerson = data.loggedPerson;
|
||||||
|
|
||||||
|
this.event = {
|
||||||
|
title: "",
|
||||||
|
organizerActor: this.loggedPerson,
|
||||||
|
attributedTo: this.loggedPerson,
|
||||||
|
description: "",
|
||||||
|
begins_on: new Date(),
|
||||||
|
ends_on: new Date(),
|
||||||
|
category: this.categories[0],
|
||||||
|
participants: [],
|
||||||
|
uuid: "",
|
||||||
|
url: "",
|
||||||
|
local: true,
|
||||||
|
status: EventStatus.CONFIRMED,
|
||||||
|
visibility: EventVisibility.PUBLIC,
|
||||||
|
thumbnail: "",
|
||||||
|
large_image: "",
|
||||||
|
publish_at: new Date()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
createEvent(e: Event) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (this.uuid === undefined) {
|
||||||
|
this.$apollo
|
||||||
|
.mutate({
|
||||||
|
mutation: CREATE_EVENT,
|
||||||
|
variables: {
|
||||||
|
title: this.event.title,
|
||||||
|
description: this.event.description,
|
||||||
|
beginsOn: this.event.begins_on,
|
||||||
|
category: this.event.category.title,
|
||||||
|
organizerActorId: this.event.organizerActor.id
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
console.log("event created", data);
|
||||||
|
this.$router.push({
|
||||||
|
name: "Event",
|
||||||
|
params: { uuid: data.data.createEvent.uuid }
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.log(error);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.$apollo
|
||||||
|
.mutate({
|
||||||
|
mutation: EDIT_EVENT
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
this.$router.push({
|
||||||
|
name: "Event",
|
||||||
|
params: { uuid: data.data.uuid }
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.log(error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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}`
|
||||||
|
// };
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.markdown-render h1 {
|
||||||
|
font-size: 2em;
|
||||||
|
}
|
||||||
|
</style>
|
196
js/src/views/Event/Event.vue
Normal file
196
js/src/views/Event/Event.vue
Normal file
|
@ -0,0 +1,196 @@
|
||||||
|
<template>
|
||||||
|
<div class="columns is-centered">
|
||||||
|
<div class="column is-three-quarters">
|
||||||
|
<b-loading :active.sync="$apollo.loading"></b-loading>
|
||||||
|
<div class="card" v-if="event">
|
||||||
|
<div class="card-image">
|
||||||
|
<figure class="image is-4by3">
|
||||||
|
<img src="https://picsum.photos/600/400/">
|
||||||
|
</figure>
|
||||||
|
</div>
|
||||||
|
<div class="card-content">
|
||||||
|
<span>{{ event.begins_on | formatDay }}</span>
|
||||||
|
<span class="tag is-primary">{{ event.category.title }}</span>
|
||||||
|
<h1 class="title">{{ event.title }}</h1>
|
||||||
|
<router-link
|
||||||
|
:to="{name: 'Profile', params: { name: event.organizerActor.preferredUsername } }"
|
||||||
|
>
|
||||||
|
<figure v-if="event.organizerActor.avatarUrl">
|
||||||
|
<img :src="event.organizerActor.avatarUrl">
|
||||||
|
</figure>
|
||||||
|
</router-link>
|
||||||
|
<span
|
||||||
|
v-if="event.organizerActor"
|
||||||
|
>Organisé par {{ event.organizerActor.name ? event.organizerActor.name : event.organizerActor.preferredUsername }}</span>
|
||||||
|
<div class="field has-addons">
|
||||||
|
<p class="control">
|
||||||
|
<router-link
|
||||||
|
v-if="actorIsOrganizer()"
|
||||||
|
class="button"
|
||||||
|
:to="{ name: 'EditEvent', params: {uuid: event.uuid}}"
|
||||||
|
>
|
||||||
|
<translate>Edit</translate>
|
||||||
|
</router-link>
|
||||||
|
</p>
|
||||||
|
<p class="control">
|
||||||
|
<a class="button" @click="downloadIcsEvent()">
|
||||||
|
<translate>Download</translate>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<p class="control">
|
||||||
|
<a class="button is-danger" v-if="actorIsOrganizer()" @click="deleteEvent()">
|
||||||
|
<translate>Delete</translate>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span>{{ event.begins_on | formatDate }} - {{ event.ends_on | formatDate }}</span>
|
||||||
|
</div>
|
||||||
|
<p v-if="actorIsOrganizer()">
|
||||||
|
<translate>Vous êtes organisateur de cet événement.</translate>
|
||||||
|
</p>
|
||||||
|
<div v-else>
|
||||||
|
<p v-if="actorIsParticipant()">
|
||||||
|
<translate>Vous avez annoncé aller à cet événement.</translate>
|
||||||
|
</p>
|
||||||
|
<p v-else>
|
||||||
|
Vous y allez ?
|
||||||
|
<span>{{ event.participants.length }} personnes y vont.</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div v-if="!actorIsOrganizer()">
|
||||||
|
<a v-if="!actorIsParticipant()" @click="joinEvent" class="button">
|
||||||
|
<translate>Join</translate>
|
||||||
|
</a>
|
||||||
|
<a v-if="actorIsParticipant()" @click="leaveEvent" color="button">Leave</a>
|
||||||
|
</div>
|
||||||
|
<h2 class="subtitle">Details</h2>
|
||||||
|
<p v-if="event.description">
|
||||||
|
<vue-markdown :source="event.description"></vue-markdown>
|
||||||
|
</p>
|
||||||
|
<h2 class="subtitle">Participants</h2>
|
||||||
|
<span v-if="event.participants.length === 0">No participants yet.</span>
|
||||||
|
<div class="columns">
|
||||||
|
<router-link
|
||||||
|
class="card column"
|
||||||
|
v-for="participant in event.participants"
|
||||||
|
:key="participant.preferredUsername"
|
||||||
|
:to="{name: 'Profile', params: { name: participant.actor.preferredUsername }}"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<figure>
|
||||||
|
<img v-if="!participant.actor.avatarUrl" src="https://picsum.photos/125/125/">
|
||||||
|
<img v-else :src="participant.actor.avatarUrl">
|
||||||
|
</figure>
|
||||||
|
<span>{{ participant.actor.preferredUsername }}</span>
|
||||||
|
</div>
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { FETCH_EVENT } from "@/graphql/event";
|
||||||
|
import { Component, Prop, Vue } from "vue-property-decorator";
|
||||||
|
import VueMarkdown from "vue-markdown";
|
||||||
|
import { LOGGED_PERSON } from "../../graphql/actor";
|
||||||
|
import { IEvent } from "@/types/event.model";
|
||||||
|
import { JOIN_EVENT } from "../../graphql/event";
|
||||||
|
import { IPerson } from "@/types/actor.model";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
components: {
|
||||||
|
VueMarkdown
|
||||||
|
},
|
||||||
|
apollo: {
|
||||||
|
event: {
|
||||||
|
query: FETCH_EVENT,
|
||||||
|
variables() {
|
||||||
|
return {
|
||||||
|
uuid: this.uuid
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
loggedPerson: {
|
||||||
|
query: LOGGED_PERSON
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
export default class Event extends Vue {
|
||||||
|
@Prop({ type: String, required: true }) uuid!: string;
|
||||||
|
|
||||||
|
event!: IEvent;
|
||||||
|
loggedPerson!: IPerson;
|
||||||
|
validationSent: boolean = false;
|
||||||
|
|
||||||
|
deleteEvent() {
|
||||||
|
const router = this.$router;
|
||||||
|
// FIXME: remove eventFetch
|
||||||
|
// eventFetch(`/events/${this.uuid}`, this.$store, { method: 'DELETE' })
|
||||||
|
// .then(() => router.push({ name: 'EventList' }));
|
||||||
|
}
|
||||||
|
|
||||||
|
async joinEvent() {
|
||||||
|
try {
|
||||||
|
this.validationSent = true;
|
||||||
|
await this.$apollo.mutate({
|
||||||
|
mutation: JOIN_EVENT
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
leaveEvent() {
|
||||||
|
// FIXME: remove eventFetch
|
||||||
|
// eventFetch(`/events/${this.uuid}/leave`, this.$store)
|
||||||
|
// .then(response => response.json())
|
||||||
|
// .then((data) => {
|
||||||
|
// console.log(data);
|
||||||
|
// });
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadIcsEvent() {
|
||||||
|
// FIXME: remove eventFetch
|
||||||
|
// 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.loggedPerson &&
|
||||||
|
this.event.participants
|
||||||
|
.map(participant => participant.actor.preferredUsername)
|
||||||
|
.includes(this.loggedPerson.preferredUsername)) ||
|
||||||
|
this.actorIsOrganizer()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
//
|
||||||
|
actorIsOrganizer() {
|
||||||
|
return (
|
||||||
|
this.loggedPerson &&
|
||||||
|
this.loggedPerson.preferredUsername ===
|
||||||
|
this.event.organizerActor.preferredUsername
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Add "scoped" attribute to limit CSS to this component only -->
|
||||||
|
<style>
|
||||||
|
.v-card__media__background {
|
||||||
|
filter: contrast(0.4);
|
||||||
|
}
|
||||||
|
</style>
|
111
js/src/views/Event/EventList.vue
Normal file
111
js/src/views/Event/EventList.vue
Normal file
|
@ -0,0 +1,111 @@
|
||||||
|
<template>
|
||||||
|
<section>
|
||||||
|
<h1>
|
||||||
|
<translate>Event list</translate>
|
||||||
|
</h1>
|
||||||
|
<b-loading :active.sync="$apollo.loading"></b-loading>
|
||||||
|
<div v-if="events.length > 0" class="columns is-multiline">
|
||||||
|
<EventCard
|
||||||
|
v-for="event in events"
|
||||||
|
:key="event.uuid"
|
||||||
|
:event="event"
|
||||||
|
class="column is-one-quarter-desktop is-half-mobile"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<b-message v-if-else="events.length === 0 && $apollo.loading === false" type="is-danger">
|
||||||
|
<translate>No events found</translate>
|
||||||
|
</b-message>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import ngeohash from "ngeohash";
|
||||||
|
import VueMarkdown from "vue-markdown";
|
||||||
|
import { Component, Prop, Vue, Watch } from "vue-property-decorator";
|
||||||
|
import EventCard from "@/components/Event/EventCard.vue";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
components: {
|
||||||
|
VueMarkdown,
|
||||||
|
EventCard
|
||||||
|
}
|
||||||
|
})
|
||||||
|
export default class EventList extends Vue {
|
||||||
|
@Prop(String) location!: string;
|
||||||
|
|
||||||
|
events = [];
|
||||||
|
loading = true;
|
||||||
|
locationChip = false;
|
||||||
|
locationText = "";
|
||||||
|
|
||||||
|
created() {
|
||||||
|
this.fetchData(this.$router.currentRoute.params["location"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeRouteUpdate(to, from, next) {
|
||||||
|
this.fetchData(to.params.location);
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Watch("locationChip")
|
||||||
|
onLocationChipChange(val) {
|
||||||
|
if (val === false) {
|
||||||
|
this.$router.push({ name: "EventList" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
// FIXME: remove eventFetch
|
||||||
|
// 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;
|
||||||
|
// FIXME: remove eventFetch
|
||||||
|
// 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) {
|
||||||
|
// FIXME: remove eventFetch
|
||||||
|
// 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 -->
|
||||||
|
<style scoped>
|
||||||
|
</style>
|
98
js/src/views/Group/Create.vue
Normal file
98
js/src/views/Group/Create.vue
Normal file
|
@ -0,0 +1,98 @@
|
||||||
|
<template>
|
||||||
|
<section>
|
||||||
|
<h1>
|
||||||
|
<translate>Create a new group</translate>
|
||||||
|
</h1>
|
||||||
|
<div class="columns">
|
||||||
|
<form class="column" @submit="createGroup">
|
||||||
|
<b-field :label="$gettext('Group name')">
|
||||||
|
<b-input aria-required="true" required v-model="group.preferred_username"/>
|
||||||
|
</b-field>
|
||||||
|
|
||||||
|
<b-field :label="$gettext('Group full name')">
|
||||||
|
<b-input aria-required="true" required v-model="group.name"/>
|
||||||
|
</b-field>
|
||||||
|
|
||||||
|
<b-field :label="$gettext('Description')">
|
||||||
|
<b-input aria-required="true" required v-model="group.summary" type="textarea"/>
|
||||||
|
</b-field>
|
||||||
|
|
||||||
|
<button class="button is-primary">
|
||||||
|
<translate>Create my group</translate>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import VueMarkdown from "vue-markdown";
|
||||||
|
import { Component, Vue } from "vue-property-decorator";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
components: {
|
||||||
|
VueMarkdown
|
||||||
|
}
|
||||||
|
})
|
||||||
|
export default class CreateGroup extends Vue {
|
||||||
|
e1 = 0;
|
||||||
|
// FIXME: correctly type group
|
||||||
|
group: {
|
||||||
|
preferred_username: string;
|
||||||
|
name: string;
|
||||||
|
summary: string;
|
||||||
|
address?: any;
|
||||||
|
} = {
|
||||||
|
preferred_username: "",
|
||||||
|
name: "",
|
||||||
|
summary: ""
|
||||||
|
// category: null,
|
||||||
|
};
|
||||||
|
categories = [];
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.fetchCategories();
|
||||||
|
}
|
||||||
|
|
||||||
|
createGroup() {
|
||||||
|
// this.group.organizer = "/accounts/" + this.$store.state.user.id;
|
||||||
|
// FIXME: remove eventFetch
|
||||||
|
// 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() {
|
||||||
|
// FIXME: remove eventFetch
|
||||||
|
// 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
|
||||||
|
},
|
||||||
|
addressCountry: addressData.country,
|
||||||
|
addressLocality: addressData.city,
|
||||||
|
addressRegion: addressData.administrative_area_level_1,
|
||||||
|
postalCode: addressData.postal_code,
|
||||||
|
streetAddress: `${addressData.street_number} ${addressData.route}`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.markdown-render h1 {
|
||||||
|
font-size: 2em;
|
||||||
|
}
|
||||||
|
</style>
|
112
js/src/views/Group/Group.vue
Normal file
112
js/src/views/Group/Group.vue
Normal file
|
@ -0,0 +1,112 @@
|
||||||
|
<template>
|
||||||
|
<section>
|
||||||
|
<div class="columns">
|
||||||
|
<div class="column">
|
||||||
|
<div class="card" v-if="group">
|
||||||
|
<div class="card-image" v-if="group.bannerUrl">
|
||||||
|
<figure class="image">
|
||||||
|
<img :src="group.bannerUrl">
|
||||||
|
</figure>
|
||||||
|
</div>
|
||||||
|
<div class="card-content">
|
||||||
|
<div class="media">
|
||||||
|
<div class="media-left">
|
||||||
|
<figure class="image is-48x48">
|
||||||
|
<img :src="group.avatarUrl">
|
||||||
|
</figure>
|
||||||
|
</div>
|
||||||
|
<div class="media-content">
|
||||||
|
<p class="title">{{ group.name }}</p>
|
||||||
|
<p class="subtitle">@{{ group.preferredUsername }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="content">
|
||||||
|
<p v-html="group.summary"></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<section v-if="group.organizedEvents.length > 0">
|
||||||
|
<h2 class="subtitle">
|
||||||
|
<translate>Organized</translate>
|
||||||
|
</h2>
|
||||||
|
<div class="columns">
|
||||||
|
<EventCard
|
||||||
|
v-for="event in group.organizedEvents"
|
||||||
|
:event="event"
|
||||||
|
:hideDetails="true"
|
||||||
|
:key="event.uuid"
|
||||||
|
class="column is-one-third"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<section v-if="group.members.length > 0">
|
||||||
|
<h2 class="subtitle">
|
||||||
|
<translate>Members</translate>
|
||||||
|
</h2>
|
||||||
|
<div class="columns">
|
||||||
|
<span
|
||||||
|
v-for="member in group.members"
|
||||||
|
:key="member"
|
||||||
|
>{{ member.actor.preferredUsername }}</span>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
<b-message v-if-else="!group && $apollo.loading === false" type="is-danger">
|
||||||
|
<translate>No group found</translate>
|
||||||
|
</b-message>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { Component, Prop, Vue, Watch } from "vue-property-decorator";
|
||||||
|
import EventCard from "@/components/Event/EventCard.vue";
|
||||||
|
import { FETCH_PERSON, LOGGED_PERSON } from "@/graphql/actor";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
apollo: {
|
||||||
|
person: {
|
||||||
|
query: FETCH_PERSON,
|
||||||
|
variables() {
|
||||||
|
return {
|
||||||
|
name: this.$route.params.name
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
loggedPerson: {
|
||||||
|
query: LOGGED_PERSON
|
||||||
|
}
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
EventCard
|
||||||
|
}
|
||||||
|
})
|
||||||
|
export default class Group extends Vue {
|
||||||
|
@Prop({ type: String, required: true }) name!: string;
|
||||||
|
|
||||||
|
group = null;
|
||||||
|
loading = true;
|
||||||
|
|
||||||
|
created() {
|
||||||
|
this.fetchData();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Watch("$route")
|
||||||
|
onRouteChanged() {
|
||||||
|
// call again the method if the route changes
|
||||||
|
this.fetchData();
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchData() {
|
||||||
|
// FIXME: remove eventFetch
|
||||||
|
// eventFetch(`/actors/${this.name}`, this.$store)
|
||||||
|
// .then(response => response.json())
|
||||||
|
// .then((response) => {
|
||||||
|
// this.group = response.data;
|
||||||
|
// this.loading = false;
|
||||||
|
// console.log(this.group);
|
||||||
|
// });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
75
js/src/views/Group/GroupList.vue
Normal file
75
js/src/views/Group/GroupList.vue
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
<template>
|
||||||
|
<section>
|
||||||
|
<h1>
|
||||||
|
<translate>Group List</translate>
|
||||||
|
</h1>
|
||||||
|
<b-loading :active.sync="$apollo.loading"></b-loading>
|
||||||
|
<div class="columns">
|
||||||
|
<GroupCard
|
||||||
|
v-for="group in groups"
|
||||||
|
:key="group.uuid"
|
||||||
|
:group="group"
|
||||||
|
class="column is-one-quarter-desktop is-half-mobile"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<router-link class="button" :to="{ name: 'CreateGroup' }">
|
||||||
|
<translate>Create group</translate>
|
||||||
|
</router-link>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { Component, Vue } from "vue-property-decorator";
|
||||||
|
|
||||||
|
@Component
|
||||||
|
export default class GroupList extends Vue {
|
||||||
|
groups = [];
|
||||||
|
loading = true;
|
||||||
|
|
||||||
|
created() {
|
||||||
|
this.fetchData();
|
||||||
|
}
|
||||||
|
|
||||||
|
usernameWithDomain(actor) {
|
||||||
|
return actor.username + (actor.domain === null ? "" : `@${actor.domain}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchData() {
|
||||||
|
// FIXME: remove eventFetch
|
||||||
|
// 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;
|
||||||
|
// FIXME: remove eventFetch
|
||||||
|
// eventFetch(`/groups/${this.usernameWithDomain(group)}`, this.$store, { method: 'DELETE' })
|
||||||
|
// .then(response => response.json())
|
||||||
|
// .then(() => router.push('/groups'));
|
||||||
|
}
|
||||||
|
|
||||||
|
viewActor(actor) {
|
||||||
|
this.$router.push({
|
||||||
|
name: "Group",
|
||||||
|
params: { name: this.usernameWithDomain(actor) }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
joinGroup(group) {
|
||||||
|
const router = this.$router;
|
||||||
|
// FIXME: remove eventFetch
|
||||||
|
// eventFetch(`/groups/${this.usernameWithDomain(group)}/join`, this.$store, { method: 'POST' })
|
||||||
|
// .then(response => response.json())
|
||||||
|
// .then(() => router.push({ name: 'Group', params: { name: this.usernameWithDomain(group) } }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Add "scoped" attribute to limit CSS to this component only -->
|
||||||
|
<style scoped>
|
||||||
|
</style>
|
150
js/src/views/Home.vue
Normal file
150
js/src/views/Home.vue
Normal file
|
@ -0,0 +1,150 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<section class="hero is-link" v-if="!currentUser.id">
|
||||||
|
<div class="hero-body">
|
||||||
|
<div class="container">
|
||||||
|
<h1 class="title">Find events you like</h1>
|
||||||
|
<h2 class="subtitle">Share them with Mobilizon</h2>
|
||||||
|
<router-link class="button" :to="{ name: 'Register' }">
|
||||||
|
<translate>Register</translate>
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<section v-else>
|
||||||
|
<h1>
|
||||||
|
<translate
|
||||||
|
:translate-params="{username: loggedPerson.preferredUsername}"
|
||||||
|
>Welcome back %{username}</translate>
|
||||||
|
</h1>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<span class="events-nearby title">Events nearby you</span>
|
||||||
|
<b-loading :active.sync="$apollo.loading"></b-loading>
|
||||||
|
<div v-if="events.length > 0" class="columns is-multiline">
|
||||||
|
<EventCard
|
||||||
|
v-for="event in events"
|
||||||
|
:key="event.uuid"
|
||||||
|
:event="event"
|
||||||
|
class="column is-one-quarter-desktop is-half-mobile"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<b-message v-else type="is-danger">
|
||||||
|
<translate>No events found</translate>
|
||||||
|
</b-message>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import ngeohash from "ngeohash";
|
||||||
|
import { AUTH_USER_ACTOR, AUTH_USER_ID } from "@/constants";
|
||||||
|
import { FETCH_EVENTS } from "@/graphql/event";
|
||||||
|
import { Component, Vue } from "vue-property-decorator";
|
||||||
|
import EventCard from "@/components/Event/EventCard.vue";
|
||||||
|
import { LOGGED_PERSON } from "@/graphql/actor";
|
||||||
|
import { IPerson } from "../types/actor.model";
|
||||||
|
import { ICurrentUser } from "@/types/current-user.model";
|
||||||
|
import { CURRENT_USER_CLIENT } from "@/graphql/user";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
apollo: {
|
||||||
|
events: {
|
||||||
|
query: FETCH_EVENTS,
|
||||||
|
fetchPolicy: "no-cache" // Debug me: https://github.com/apollographql/apollo-client/issues/3030
|
||||||
|
},
|
||||||
|
loggedPerson: {
|
||||||
|
query: LOGGED_PERSON
|
||||||
|
},
|
||||||
|
currentUser: {
|
||||||
|
query: CURRENT_USER_CLIENT
|
||||||
|
}
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
EventCard
|
||||||
|
}
|
||||||
|
})
|
||||||
|
export default class Home extends Vue {
|
||||||
|
searchTerm = null;
|
||||||
|
location_field = {
|
||||||
|
loading: false,
|
||||||
|
search: null
|
||||||
|
};
|
||||||
|
events = [];
|
||||||
|
locations = [];
|
||||||
|
city = { name: null };
|
||||||
|
country = { name: null };
|
||||||
|
// FIXME: correctly parse local storage
|
||||||
|
loggedPerson!: IPerson;
|
||||||
|
currentUser!: ICurrentUser;
|
||||||
|
|
||||||
|
get displayed_name() {
|
||||||
|
return this.loggedPerson.name === null
|
||||||
|
? this.loggedPerson.preferredUsername
|
||||||
|
: this.loggedPerson.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchLocations() {
|
||||||
|
// FIXME: remove eventFetch
|
||||||
|
// eventFetch('/locations', this.$store)
|
||||||
|
// .then(response => (response.json()))
|
||||||
|
// .then((response) => {
|
||||||
|
// this.locations = response;
|
||||||
|
// });
|
||||||
|
}
|
||||||
|
|
||||||
|
geoLocalize() {
|
||||||
|
const router = this.$router;
|
||||||
|
const sessionCity = sessionStorage.getItem("City");
|
||||||
|
if (sessionCity) {
|
||||||
|
router.push({ name: "EventList", params: { location: sessionCity } });
|
||||||
|
} else {
|
||||||
|
navigator.geolocation.getCurrentPosition(
|
||||||
|
pos => {
|
||||||
|
const crd = pos.coords;
|
||||||
|
|
||||||
|
const geohash = ngeohash.encode(crd.latitude, crd.longitude, 11);
|
||||||
|
sessionStorage.setItem("City", geohash);
|
||||||
|
router.push({ name: "EventList", params: { location: geohash } });
|
||||||
|
},
|
||||||
|
err => console.warn(`ERROR(${err.code}): ${err.message}`),
|
||||||
|
{
|
||||||
|
enableHighAccuracy: true,
|
||||||
|
timeout: 5000,
|
||||||
|
maximumAge: 0
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getAddressData(addressData) {
|
||||||
|
const geohash = ngeohash.encode(
|
||||||
|
addressData.latitude,
|
||||||
|
addressData.longitude,
|
||||||
|
11
|
||||||
|
);
|
||||||
|
sessionStorage.setItem("City", geohash);
|
||||||
|
this.$router.push({ name: "EventList", params: { location: geohash } });
|
||||||
|
}
|
||||||
|
|
||||||
|
viewEvent(event) {
|
||||||
|
this.$router.push({ name: "Event", params: { uuid: event.uuid } });
|
||||||
|
}
|
||||||
|
|
||||||
|
ipLocation() {
|
||||||
|
return this.city.name ? this.city.name : this.country.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Add "scoped" attribute to limit CSS to this component only -->
|
||||||
|
<style scoped>
|
||||||
|
.search-autocomplete {
|
||||||
|
border: 1px solid #dbdbdb;
|
||||||
|
color: rgba(0, 0, 0, 0.87);
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-nearby {
|
||||||
|
margin-bottom: 25px;
|
||||||
|
}
|
||||||
|
</style>
|
30
js/src/views/Location.vue
Normal file
30
js/src/views/Location.vue
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
<template>
|
||||||
|
<div>{{ center.lat }} - {{ center.lng }}</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { Component, Prop, Vue } from "vue-property-decorator";
|
||||||
|
|
||||||
|
@Component
|
||||||
|
export default class Location extends Vue {
|
||||||
|
@Prop(String) address!: string;
|
||||||
|
|
||||||
|
description = "Paris, France";
|
||||||
|
center = { lat: 48.85, lng: 2.35 };
|
||||||
|
markers: any[] = [];
|
||||||
|
|
||||||
|
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>
|
8
js/src/views/PageNotFound.vue
Normal file
8
js/src/views/PageNotFound.vue
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
<template>
|
||||||
|
<section>
|
||||||
|
<h1>
|
||||||
|
<translate>Page not found!</translate>
|
||||||
|
<img src="../assets/oh_no.jpg">
|
||||||
|
</h1>
|
||||||
|
</section>
|
||||||
|
</template>
|
137
js/src/views/User/Login.vue
Normal file
137
js/src/views/User/Login.vue
Normal file
|
@ -0,0 +1,137 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<section class="hero">
|
||||||
|
<h1 class="title">
|
||||||
|
<translate>Welcome back!</translate>
|
||||||
|
</h1>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<div class="columns is-mobile is-centered">
|
||||||
|
<div class="column is-half card">
|
||||||
|
<b-message title="Error" type="is-danger" v-for="error in errors" :key="error">{{ error }}</b-message>
|
||||||
|
<form @submit="loginAction">
|
||||||
|
<b-field label="Email">
|
||||||
|
<b-input aria-required="true" required type="email" v-model="credentials.email"/>
|
||||||
|
</b-field>
|
||||||
|
|
||||||
|
<b-field label="Password">
|
||||||
|
<b-input
|
||||||
|
aria-required="true"
|
||||||
|
required
|
||||||
|
type="password"
|
||||||
|
password-reveal
|
||||||
|
v-model="credentials.password"
|
||||||
|
/>
|
||||||
|
</b-field>
|
||||||
|
|
||||||
|
<div class="control has-text-centered">
|
||||||
|
<button class="button is-primary is-large">
|
||||||
|
<translate>Login</translate>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="control">
|
||||||
|
<router-link
|
||||||
|
class="button is-text"
|
||||||
|
:to="{ name: 'SendPasswordReset', params: { email: credentials.email }}"
|
||||||
|
>
|
||||||
|
<translate>Forgot your password ?</translate>
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
<div class="control">
|
||||||
|
<router-link
|
||||||
|
class="button is-text"
|
||||||
|
:to="{ name: 'Register', params: { default_email: credentials.email, default_password: credentials.password }}"
|
||||||
|
>
|
||||||
|
<translate>Register</translate>
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import Gravatar from "vue-gravatar";
|
||||||
|
import { Component, Prop, Vue } from "vue-property-decorator";
|
||||||
|
import { LOGIN } from "@/graphql/auth";
|
||||||
|
import { validateEmailField, validateRequiredField } from "@/utils/validators";
|
||||||
|
import { saveUserData } from "@/utils/auth";
|
||||||
|
import { ILogin } from "@/types/login.model";
|
||||||
|
import { UPDATE_CURRENT_USER_CLIENT } from "@/graphql/user";
|
||||||
|
import { onLogin } from "@/vue-apollo";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
components: {
|
||||||
|
"v-gravatar": Gravatar
|
||||||
|
}
|
||||||
|
})
|
||||||
|
export default class Login extends Vue {
|
||||||
|
@Prop({ type: String, required: false, default: "" }) email!: string;
|
||||||
|
@Prop({ type: String, required: false, default: "" }) password!: string;
|
||||||
|
|
||||||
|
credentials = {
|
||||||
|
email: "",
|
||||||
|
password: ""
|
||||||
|
};
|
||||||
|
validationSent = false;
|
||||||
|
errors: string[] = [];
|
||||||
|
rules = {
|
||||||
|
required: validateRequiredField,
|
||||||
|
email: validateEmailField
|
||||||
|
};
|
||||||
|
user: any;
|
||||||
|
|
||||||
|
beforeCreate() {
|
||||||
|
if (this.user) {
|
||||||
|
this.$router.push("/");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.credentials.email = this.email;
|
||||||
|
this.credentials.password = this.password;
|
||||||
|
}
|
||||||
|
|
||||||
|
async loginAction(e: Event) {
|
||||||
|
e.preventDefault();
|
||||||
|
this.errors.splice(0);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await this.$apollo.mutate<{ login: ILogin }>({
|
||||||
|
mutation: LOGIN,
|
||||||
|
variables: {
|
||||||
|
email: this.credentials.email,
|
||||||
|
password: this.credentials.password
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
saveUserData(result.data.login);
|
||||||
|
|
||||||
|
await this.$apollo.mutate({
|
||||||
|
mutation: UPDATE_CURRENT_USER_CLIENT,
|
||||||
|
variables: {
|
||||||
|
id: result.data.login.user.id,
|
||||||
|
email: this.credentials.email
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onLogin(this.$apollo);
|
||||||
|
|
||||||
|
this.$router.push({ name: "Home" });
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
err.graphQLErrors.forEach(({ message }) => {
|
||||||
|
this.errors.push(message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
validEmail() {
|
||||||
|
return this.rules.email(this.credentials.email) === true
|
||||||
|
? "v-gravatar"
|
||||||
|
: "avatar";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
91
js/src/views/User/PasswordReset.vue
Normal file
91
js/src/views/User/PasswordReset.vue
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
<template>
|
||||||
|
<section class="columns is-mobile is-centered">
|
||||||
|
<div class="card column is-half-desktop">
|
||||||
|
<h1>
|
||||||
|
<translate>Password reset</translate>
|
||||||
|
</h1>
|
||||||
|
<b-message title="Error" type="is-danger" v-for="error in errors" :key="error">{{ error }}</b-message>
|
||||||
|
<form @submit="resetAction">
|
||||||
|
<b-field label="Password">
|
||||||
|
<b-input
|
||||||
|
aria-required="true"
|
||||||
|
required
|
||||||
|
type="password"
|
||||||
|
password-reveal
|
||||||
|
minlength="6"
|
||||||
|
v-model="credentials.password"
|
||||||
|
/>
|
||||||
|
</b-field>
|
||||||
|
<b-field label="Password (confirmation)">
|
||||||
|
<b-input
|
||||||
|
aria-required="true"
|
||||||
|
required
|
||||||
|
type="password"
|
||||||
|
password-reveal
|
||||||
|
minlength="6"
|
||||||
|
v-model="credentials.password_confirmation"
|
||||||
|
/>
|
||||||
|
</b-field>
|
||||||
|
<button class="button is-primary">
|
||||||
|
<translate>Reset my password</translate>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { Component, Prop, Vue } from "vue-property-decorator";
|
||||||
|
import { validateRequiredField } from "@/utils/validators";
|
||||||
|
import { RESET_PASSWORD } from "@/graphql/auth";
|
||||||
|
import { saveUserData } from "@/utils/auth";
|
||||||
|
import { ILogin } from "@/types/login.model";
|
||||||
|
|
||||||
|
@Component
|
||||||
|
export default class PasswordReset extends Vue {
|
||||||
|
@Prop({ type: String, required: true }) token!: string;
|
||||||
|
|
||||||
|
credentials = {
|
||||||
|
password: "",
|
||||||
|
password_confirmation: ""
|
||||||
|
} as { password: string; password_confirmation: string };
|
||||||
|
errors: string[] = [];
|
||||||
|
rules = {
|
||||||
|
password_length: value =>
|
||||||
|
value.length > 6 || "Password must be at least 6 characters long",
|
||||||
|
required: validateRequiredField,
|
||||||
|
password_equal: value =>
|
||||||
|
value === this.credentials.password || "Passwords must be the same"
|
||||||
|
};
|
||||||
|
|
||||||
|
get samePasswords() {
|
||||||
|
return (
|
||||||
|
this.rules.password_length(this.credentials.password) === true &&
|
||||||
|
this.credentials.password === this.credentials.password_confirmation
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async resetAction(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
this.errors.splice(0);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await this.$apollo.mutate<{ resetPassword: ILogin }>({
|
||||||
|
mutation: RESET_PASSWORD,
|
||||||
|
variables: {
|
||||||
|
password: this.credentials.password,
|
||||||
|
token: this.token
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
saveUserData(result.data.resetPassword);
|
||||||
|
this.$router.push({ name: "Home" });
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
err.graphQLErrors.forEach(({ message }) => {
|
||||||
|
this.errors.push(message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
77
js/src/views/User/ResendConfirmation.vue
Normal file
77
js/src/views/User/ResendConfirmation.vue
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
<template>
|
||||||
|
<section class="columns">
|
||||||
|
<div class="column card">
|
||||||
|
<h1 class="title">
|
||||||
|
<translate>Resend confirmation email</translate>
|
||||||
|
</h1>
|
||||||
|
<form v-if="!validationSent" @submit="resendConfirmationAction">
|
||||||
|
<b-field label="Email">
|
||||||
|
<b-input aria-required="true" required type="email" v-model="credentials.email"/>
|
||||||
|
</b-field>
|
||||||
|
<button class="button is-primary">
|
||||||
|
<translate>Send confirmation email again</translate>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<div v-else>
|
||||||
|
<b-message type="is-success" :closable="false" title="Success">
|
||||||
|
<translate
|
||||||
|
:translate-params="{email: credentials.email}"
|
||||||
|
>If an account with this email exists, we just sent another confirmation email to %{email}</translate>
|
||||||
|
</b-message>
|
||||||
|
<b-message type="is-info">
|
||||||
|
<translate>Please check you spam folder if you didn't receive the email.</translate>
|
||||||
|
</b-message>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { Component, Prop, Vue } from "vue-property-decorator";
|
||||||
|
import { validateEmailField, validateRequiredField } from "@/utils/validators";
|
||||||
|
import { RESEND_CONFIRMATION_EMAIL } from "@/graphql/auth";
|
||||||
|
|
||||||
|
@Component
|
||||||
|
export default class ResendConfirmation extends Vue {
|
||||||
|
@Prop({ type: String, required: false, default: "" }) email!: string;
|
||||||
|
|
||||||
|
credentials = {
|
||||||
|
email: ""
|
||||||
|
};
|
||||||
|
validationSent = false;
|
||||||
|
error = false;
|
||||||
|
state = {
|
||||||
|
email: {
|
||||||
|
status: null,
|
||||||
|
msg: ""
|
||||||
|
}
|
||||||
|
};
|
||||||
|
rules = {
|
||||||
|
required: validateRequiredField,
|
||||||
|
email: validateEmailField
|
||||||
|
};
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.credentials.email = this.email;
|
||||||
|
}
|
||||||
|
|
||||||
|
async resendConfirmationAction(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
this.error = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.$apollo.mutate({
|
||||||
|
mutation: RESEND_CONFIRMATION_EMAIL,
|
||||||
|
variables: {
|
||||||
|
email: this.credentials.email
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
this.error = true;
|
||||||
|
} finally {
|
||||||
|
this.validationSent = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
89
js/src/views/User/SendPasswordReset.vue
Normal file
89
js/src/views/User/SendPasswordReset.vue
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
<template>
|
||||||
|
<section class="columns">
|
||||||
|
<div class="card column">
|
||||||
|
<h1 class="title">
|
||||||
|
<translate>Password reset</translate>
|
||||||
|
</h1>
|
||||||
|
<b-message title="Error" type="is-danger" v-for="error in errors" :key="error">{{ error }}</b-message>
|
||||||
|
<form @submit="sendResetPasswordTokenAction" v-if="!validationSent">
|
||||||
|
<b-field label="Email">
|
||||||
|
<b-input aria-required="true" required type="email" v-model="credentials.email"/>
|
||||||
|
</b-field>
|
||||||
|
<button class="button is-primary">
|
||||||
|
<translate>Send email to reset my password</translate>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<div v-else>
|
||||||
|
<b-message type="is-success" :closable="false" title="Success">
|
||||||
|
<translate
|
||||||
|
:translate-params="{email: credentials.email}"
|
||||||
|
>We just sent an email to %{email}</translate>
|
||||||
|
</b-message>
|
||||||
|
<b-message type="is-info">
|
||||||
|
<translate>Please check you spam folder if you didn't receive the email.</translate>
|
||||||
|
</b-message>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { Component, Prop, Vue } from "vue-property-decorator";
|
||||||
|
import { validateEmailField, validateRequiredField } from "@/utils/validators";
|
||||||
|
import { SEND_RESET_PASSWORD } from "@/graphql/auth";
|
||||||
|
|
||||||
|
@Component
|
||||||
|
export default class SendPasswordReset extends Vue {
|
||||||
|
@Prop({ type: String, required: false, default: "" }) email!: string;
|
||||||
|
|
||||||
|
credentials = {
|
||||||
|
email: ""
|
||||||
|
} as { email: string };
|
||||||
|
validationSent: boolean = false;
|
||||||
|
errors: string[] = [];
|
||||||
|
state = {
|
||||||
|
email: {
|
||||||
|
status: null,
|
||||||
|
msg: ""
|
||||||
|
} as { status: boolean | null; msg: string }
|
||||||
|
};
|
||||||
|
|
||||||
|
rules = {
|
||||||
|
required: validateRequiredField,
|
||||||
|
email: validateEmailField
|
||||||
|
};
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.credentials.email = this.email;
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendResetPasswordTokenAction(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.$apollo.mutate({
|
||||||
|
mutation: SEND_RESET_PASSWORD,
|
||||||
|
variables: {
|
||||||
|
email: this.credentials.email
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.validationSent = true;
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
err.graphQLErrors.forEach(({ message }) => {
|
||||||
|
this.errors.push(message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resetState() {
|
||||||
|
this.state = {
|
||||||
|
email: {
|
||||||
|
status: null,
|
||||||
|
msg: ""
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
59
js/src/views/User/Validate.vue
Normal file
59
js/src/views/User/Validate.vue
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
<template>
|
||||||
|
<section>
|
||||||
|
<h1 class="title" v-if="loading">
|
||||||
|
<translate>Your account is being validated</translate>
|
||||||
|
</h1>
|
||||||
|
<div v-else>
|
||||||
|
<div v-if="failed">
|
||||||
|
<b-message title="Error" type="is-danger">
|
||||||
|
<translate>Error while validating account</translate>
|
||||||
|
</b-message>
|
||||||
|
</div>
|
||||||
|
<h1 class="title" v-else>
|
||||||
|
<translate>Your account has been validated</translate>
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { VALIDATE_USER } from "@/graphql/user";
|
||||||
|
import { Component, Prop, Vue } from "vue-property-decorator";
|
||||||
|
import { AUTH_TOKEN, AUTH_USER_ID } from "@/constants";
|
||||||
|
|
||||||
|
@Component
|
||||||
|
export default class Validate extends Vue {
|
||||||
|
@Prop({ type: String, required: true }) token!: string;
|
||||||
|
|
||||||
|
loading = true;
|
||||||
|
failed = false;
|
||||||
|
|
||||||
|
created() {
|
||||||
|
this.validateAction();
|
||||||
|
}
|
||||||
|
|
||||||
|
async validateAction() {
|
||||||
|
try {
|
||||||
|
const data = await this.$apollo.mutate({
|
||||||
|
mutation: VALIDATE_USER,
|
||||||
|
variables: {
|
||||||
|
token: this.token
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.saveUserData(data.data);
|
||||||
|
this.$router.push({ name: "Home" });
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
this.failed = true;
|
||||||
|
} finally {
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
saveUserData({ validateUser: login }) {
|
||||||
|
localStorage.setItem(AUTH_USER_ID, login.user.id);
|
||||||
|
localStorage.setItem(AUTH_TOKEN, login.token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -39,4 +39,4 @@
|
||||||
"exclude": [
|
"exclude": [
|
||||||
"node_modules"
|
"node_modules"
|
||||||
]
|
]
|
||||||
}
|
}
|
|
@ -42,7 +42,7 @@ defmodule Mobilizon.Actors.Actor do
|
||||||
field(:shared_inbox_url, :string)
|
field(:shared_inbox_url, :string)
|
||||||
field(:type, Mobilizon.Actors.ActorTypeEnum, default: :Person)
|
field(:type, Mobilizon.Actors.ActorTypeEnum, default: :Person)
|
||||||
field(:name, :string)
|
field(:name, :string)
|
||||||
field(:domain, :string)
|
field(:domain, :string, default: nil)
|
||||||
field(:summary, :string)
|
field(:summary, :string)
|
||||||
field(:preferred_username, :string)
|
field(:preferred_username, :string)
|
||||||
field(:keys, :string)
|
field(:keys, :string)
|
||||||
|
|
|
@ -77,9 +77,30 @@ defmodule Mobilizon.Actors do
|
||||||
Repo.all(from(a in Actor, where: a.user_id == ^user_id))
|
Repo.all(from(a in Actor, where: a.user_id == ^user_id))
|
||||||
end
|
end
|
||||||
|
|
||||||
def get_actor_with_everything!(id) do
|
@spec get_actor_with_everything(integer()) :: Ecto.Query
|
||||||
actor = Repo.get!(Actor, id)
|
defp do_get_actor_with_everything(id) do
|
||||||
Repo.preload(actor, [:organized_events, :followers, :followings])
|
from(a in Actor, where: a.id == ^id, preload: [:organized_events, :followers, :followings])
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Returns an actor with every relation
|
||||||
|
"""
|
||||||
|
@spec get_actor_with_everything(integer()) :: Mobilizon.Actors.Actor.t()
|
||||||
|
def get_actor_with_everything(id) do
|
||||||
|
id
|
||||||
|
|> do_get_actor_with_everything
|
||||||
|
|> Repo.one()
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Returns an actor with every relation
|
||||||
|
"""
|
||||||
|
@spec get_local_actor_with_everything(integer()) :: Mobilizon.Actors.Actor.t()
|
||||||
|
def get_local_actor_with_everything(id) do
|
||||||
|
id
|
||||||
|
|> do_get_actor_with_everything
|
||||||
|
|> where([a], is_nil(a.domain))
|
||||||
|
|> Repo.one()
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
|
@ -610,6 +631,19 @@ defmodule Mobilizon.Actors do
|
||||||
{:error, hd(email_msg)}
|
{:error, hd(email_msg)}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Create a new person actor
|
||||||
|
"""
|
||||||
|
def new_person(args) do
|
||||||
|
key = :public_key.generate_key({:rsa, 2048, 65_537})
|
||||||
|
entry = :public_key.pem_entry_encode(:RSAPrivateKey, key)
|
||||||
|
pem = [entry] |> :public_key.pem_encode() |> String.trim_trailing()
|
||||||
|
args = Map.put(args, :keys, pem)
|
||||||
|
|
||||||
|
actor = Mobilizon.Actors.Actor.registration_changeset(%Mobilizon.Actors.Actor{}, args)
|
||||||
|
Mobilizon.Repo.insert(actor)
|
||||||
|
end
|
||||||
|
|
||||||
def register_bot_account(%{name: name, summary: summary}) do
|
def register_bot_account(%{name: name, summary: summary}) do
|
||||||
key = :public_key.generate_key({:rsa, 2048, 65_537})
|
key = :public_key.generate_key({:rsa, 2048, 65_537})
|
||||||
entry = :public_key.pem_entry_encode(:RSAPrivateKey, key)
|
entry = :public_key.pem_entry_encode(:RSAPrivateKey, key)
|
||||||
|
|
|
@ -24,10 +24,12 @@ defmodule Mobilizon.Actors.Service.ResetPassword do
|
||||||
{:ok, user}
|
{:ok, user}
|
||||||
else
|
else
|
||||||
{:error, %Ecto.Changeset{errors: [password: {"registration.error.password_too_short", _}]}} ->
|
{:error, %Ecto.Changeset{errors: [password: {"registration.error.password_too_short", _}]}} ->
|
||||||
{:error, :password_too_short}
|
{:error,
|
||||||
|
"The password you have choosen is too short. Please make sure your password contains at least 6 charaters."}
|
||||||
|
|
||||||
_err ->
|
_err ->
|
||||||
{:error, :invalid_token}
|
{:error,
|
||||||
|
"The token you provided is invalid. Make sure that the URL is exactly the one provided inside the email you got."}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -68,11 +68,11 @@ defmodule Mobilizon.Events.Event do
|
||||||
:large_image,
|
:large_image,
|
||||||
:publish_at,
|
:publish_at,
|
||||||
:online_address,
|
:online_address,
|
||||||
:phone_address
|
:phone_address,
|
||||||
|
:uuid
|
||||||
])
|
])
|
||||||
|> cast_assoc(:tags)
|
|> cast_assoc(:tags)
|
||||||
|> cast_assoc(:physical_address)
|
|> cast_assoc(:physical_address)
|
||||||
|> build_url()
|
|
||||||
|> validate_required([
|
|> validate_required([
|
||||||
:title,
|
:title,
|
||||||
:begins_on,
|
:begins_on,
|
||||||
|
@ -82,31 +82,4 @@ defmodule Mobilizon.Events.Event do
|
||||||
:uuid
|
:uuid
|
||||||
])
|
])
|
||||||
end
|
end
|
||||||
|
|
||||||
@spec build_url(Ecto.Changeset.t()) :: Ecto.Changeset.t()
|
|
||||||
defp build_url(%Ecto.Changeset{changes: %{url: _url}} = changeset), do: changeset
|
|
||||||
|
|
||||||
defp build_url(%Ecto.Changeset{changes: %{organizer_actor: organizer_actor}} = changeset) do
|
|
||||||
organizer_actor
|
|
||||||
|> Actor.actor_acct_from_actor()
|
|
||||||
|> do_build_url(changeset)
|
|
||||||
end
|
|
||||||
|
|
||||||
defp build_url(%Ecto.Changeset{changes: %{organizer_actor_id: organizer_actor_id}} = changeset) do
|
|
||||||
organizer_actor_id
|
|
||||||
|> Mobilizon.Actors.get_actor!()
|
|
||||||
|> Actor.actor_acct_from_actor()
|
|
||||||
|> do_build_url(changeset)
|
|
||||||
end
|
|
||||||
|
|
||||||
defp build_url(%Ecto.Changeset{} = changeset), do: changeset
|
|
||||||
|
|
||||||
@spec do_build_url(String.t(), Ecto.Changeset.t()) :: Ecto.Changeset.t()
|
|
||||||
defp do_build_url(actor_acct, changeset) do
|
|
||||||
uuid = Ecto.UUID.generate()
|
|
||||||
|
|
||||||
changeset
|
|
||||||
|> put_change(:uuid, uuid)
|
|
||||||
|> put_change(:url, "#{MobilizonWeb.Endpoint.url()}/@#{actor_acct}/#{uuid}")
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -220,7 +220,7 @@ defmodule Mobilizon.Events do
|
||||||
from(
|
from(
|
||||||
e in Event,
|
e in Event,
|
||||||
where: e.visibility == ^:public,
|
where: e.visibility == ^:public,
|
||||||
preload: [:organizer_actor]
|
preload: [:organizer_actor, :participants]
|
||||||
)
|
)
|
||||||
|> paginate(page, limit)
|
|> paginate(page, limit)
|
||||||
|
|
||||||
|
|
|
@ -14,12 +14,12 @@ defmodule MobilizonWeb.API.Events do
|
||||||
%{
|
%{
|
||||||
title: title,
|
title: title,
|
||||||
description: description,
|
description: description,
|
||||||
organizer_actor_username: organizer_actor_username,
|
organizer_actor_id: organizer_actor_id,
|
||||||
begins_on: begins_on,
|
begins_on: begins_on,
|
||||||
category: category
|
category: category
|
||||||
} = args
|
} = args
|
||||||
) do
|
) do
|
||||||
with %Actor{url: url} = actor <- Actors.get_local_actor_by_name(organizer_actor_username),
|
with %Actor{url: url} = actor <- Actors.get_local_actor_with_everything(organizer_actor_id),
|
||||||
title <- String.trim(title),
|
title <- String.trim(title),
|
||||||
mentions <- Formatter.parse_mentions(description),
|
mentions <- Formatter.parse_mentions(description),
|
||||||
visibility <- Map.get(args, :visibility, "public"),
|
visibility <- Map.get(args, :visibility, "public"),
|
||||||
|
|
|
@ -39,8 +39,8 @@ defmodule MobilizonWeb.Resolvers.Event do
|
||||||
@doc """
|
@doc """
|
||||||
List participants for event (through an event request)
|
List participants for event (through an event request)
|
||||||
"""
|
"""
|
||||||
def list_participants_for_event(%{uuid: uuid}, %{page: page, limit: limit}, _resolution) do
|
def list_participants_for_event(%Event{uuid: uuid}, _args, _resolution) do
|
||||||
{:ok, Mobilizon.Events.list_participants_for_event(uuid, page, limit)}
|
{:ok, Mobilizon.Events.list_participants_for_event(uuid, 1, 10)}
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
|
@ -81,14 +81,7 @@ defmodule MobilizonWeb.Resolvers.Event do
|
||||||
"""
|
"""
|
||||||
def create_event(_parent, args, %{context: %{current_user: user}}) do
|
def create_event(_parent, args, %{context: %{current_user: user}}) do
|
||||||
with {:ok, %Activity{data: %{"object" => %{"type" => "Event"} = object}}} <-
|
with {:ok, %Activity{data: %{"object" => %{"type" => "Event"} = object}}} <-
|
||||||
args
|
MobilizonWeb.API.Events.create_event(args) do
|
||||||
# Set default organizer_actor_id if none set
|
|
||||||
|> Map.update(
|
|
||||||
:organizer_actor_username,
|
|
||||||
Actors.get_actor_for_user(user).preferred_username,
|
|
||||||
& &1
|
|
||||||
)
|
|
||||||
|> MobilizonWeb.API.Events.create_event() do
|
|
||||||
{:ok,
|
{:ok,
|
||||||
%Event{
|
%Event{
|
||||||
title: object["name"],
|
title: object["name"],
|
||||||
|
|
|
@ -3,6 +3,7 @@ defmodule MobilizonWeb.Resolvers.Person do
|
||||||
Handles the person-related GraphQL calls
|
Handles the person-related GraphQL calls
|
||||||
"""
|
"""
|
||||||
alias Mobilizon.Actors
|
alias Mobilizon.Actors
|
||||||
|
alias Mobilizon.Actors.Actor
|
||||||
alias Mobilizon.Service.ActivityPub
|
alias Mobilizon.Service.ActivityPub
|
||||||
|
|
||||||
@deprecated "Use find_person/3 or find_group/3 instead"
|
@deprecated "Use find_person/3 or find_group/3 instead"
|
||||||
|
@ -39,4 +40,28 @@ defmodule MobilizonWeb.Resolvers.Person do
|
||||||
def get_current_person(_parent, _args, _resolution) do
|
def get_current_person(_parent, _args, _resolution) do
|
||||||
{:error, "You need to be logged-in to view current person"}
|
{:error, "You need to be logged-in to view current person"}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Returns the list of identities for the logged-in user
|
||||||
|
"""
|
||||||
|
def identities(_parent, _args, %{context: %{current_user: user}}) do
|
||||||
|
{:ok, Actors.get_actors_for_user(user)}
|
||||||
|
end
|
||||||
|
|
||||||
|
def identities(_parent, _args, _resolution) do
|
||||||
|
{:error, "You need to be logged-in to view your list of identities"}
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_person(_parent, %{preferred_username: preferred_username} = args, %{
|
||||||
|
context: %{current_user: user}
|
||||||
|
}) do
|
||||||
|
args = Map.put(args, :user_id, user.id)
|
||||||
|
|
||||||
|
with {:ok, %Actor{} = new_person} <- Actors.new_person(args) do
|
||||||
|
{:ok, new_person}
|
||||||
|
else
|
||||||
|
{:error, %Ecto.Changeset{} = e} ->
|
||||||
|
{:error, "Unable to create a profile with this username"}
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -36,7 +36,7 @@ defmodule MobilizonWeb.Resolvers.User do
|
||||||
{:error, "User with email not found"}
|
{:error, "User with email not found"}
|
||||||
|
|
||||||
{:error, :unauthorized} ->
|
{:error, :unauthorized} ->
|
||||||
{:error, "Impossible to authenticate"}
|
{:error, "Impossible to authenticate, either your email or password are invalid."}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -104,7 +104,7 @@ defmodule MobilizonWeb.Schema do
|
||||||
end
|
end
|
||||||
|
|
||||||
def plugins do
|
def plugins do
|
||||||
[Absinthe.Middleware.Dataloader | Absinthe.Plugin.defaults()]
|
[Absinthe.Middleware.Dataloader] ++ Absinthe.Plugin.defaults()
|
||||||
end
|
end
|
||||||
|
|
||||||
@desc """
|
@desc """
|
||||||
|
@ -175,6 +175,11 @@ defmodule MobilizonWeb.Schema do
|
||||||
resolve(&Resolvers.Person.find_person/3)
|
resolve(&Resolvers.Person.find_person/3)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@desc "Get the persons for an user"
|
||||||
|
field :identities, list_of(:person) do
|
||||||
|
resolve(&Resolvers.Person.identities/3)
|
||||||
|
end
|
||||||
|
|
||||||
@desc "Get the list of categories"
|
@desc "Get the list of categories"
|
||||||
field :categories, non_null(list_of(:category)) do
|
field :categories, non_null(list_of(:category)) do
|
||||||
arg(:page, :integer, default_value: 1)
|
arg(:page, :integer, default_value: 1)
|
||||||
|
@ -201,7 +206,7 @@ defmodule MobilizonWeb.Schema do
|
||||||
arg(:publish_at, :datetime)
|
arg(:publish_at, :datetime)
|
||||||
arg(:online_address, :string)
|
arg(:online_address, :string)
|
||||||
arg(:phone_address, :string)
|
arg(:phone_address, :string)
|
||||||
arg(:organizer_actor_username, non_null(:string))
|
arg(:organizer_actor_id, non_null(:id))
|
||||||
arg(:category, non_null(:string))
|
arg(:category, non_null(:string))
|
||||||
|
|
||||||
resolve(&Resolvers.Event.create_event/3)
|
resolve(&Resolvers.Event.create_event/3)
|
||||||
|
@ -273,6 +278,16 @@ defmodule MobilizonWeb.Schema do
|
||||||
resolve(&Resolvers.User.change_default_actor/3)
|
resolve(&Resolvers.User.change_default_actor/3)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@desc "Create a new person for user"
|
||||||
|
field :create_person, :person do
|
||||||
|
arg(:preferred_username, non_null(:string))
|
||||||
|
arg(:name, :string, description: "The displayed name for the new profile")
|
||||||
|
|
||||||
|
arg(:description, :string, description: "The summary for the new profile", default_value: "")
|
||||||
|
|
||||||
|
resolve(&Resolvers.Person.create_person/3)
|
||||||
|
end
|
||||||
|
|
||||||
@desc "Create a group"
|
@desc "Create a group"
|
||||||
field :create_group, :group do
|
field :create_group, :group do
|
||||||
arg(:preferred_username, non_null(:string), description: "The name for the group")
|
arg(:preferred_username, non_null(:string), description: "The name for the group")
|
||||||
|
|
|
@ -5,12 +5,14 @@ defmodule MobilizonWeb.Schema.ActorInterface do
|
||||||
use Absinthe.Schema.Notation
|
use Absinthe.Schema.Notation
|
||||||
import Absinthe.Resolution.Helpers, only: [dataloader: 1]
|
import Absinthe.Resolution.Helpers, only: [dataloader: 1]
|
||||||
alias Mobilizon.Actors.Actor
|
alias Mobilizon.Actors.Actor
|
||||||
|
alias Mobilizon.Events
|
||||||
|
|
||||||
import_types(MobilizonWeb.Schema.Actors.FollowerType)
|
import_types(MobilizonWeb.Schema.Actors.FollowerType)
|
||||||
import_types(MobilizonWeb.Schema.EventType)
|
import_types(MobilizonWeb.Schema.EventType)
|
||||||
|
|
||||||
@desc "An ActivityPub actor"
|
@desc "An ActivityPub actor"
|
||||||
interface :actor do
|
interface :actor do
|
||||||
|
field(:id, :id, description: "Internal ID for this actor")
|
||||||
field(:url, :string, description: "The ActivityPub actor's URL")
|
field(:url, :string, description: "The ActivityPub actor's URL")
|
||||||
field(:type, :actor_type, description: "The type of Actor (Person, Group,…)")
|
field(:type, :actor_type, description: "The type of Actor (Person, Group,…)")
|
||||||
field(:name, :string, description: "The actor's displayed name")
|
field(:name, :string, description: "The actor's displayed name")
|
||||||
|
@ -51,6 +53,9 @@ defmodule MobilizonWeb.Schema.ActorInterface do
|
||||||
|
|
||||||
%Actor{type: :Group}, _ ->
|
%Actor{type: :Group}, _ ->
|
||||||
:group
|
:group
|
||||||
|
|
||||||
|
_, _ ->
|
||||||
|
nil
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -12,6 +12,7 @@ defmodule MobilizonWeb.Schema.Actors.GroupType do
|
||||||
object :group do
|
object :group do
|
||||||
interfaces([:actor])
|
interfaces([:actor])
|
||||||
|
|
||||||
|
field(:id, :id, description: "Internal ID for this group")
|
||||||
field(:url, :string, description: "The ActivityPub actor's URL")
|
field(:url, :string, description: "The ActivityPub actor's URL")
|
||||||
field(:type, :actor_type, description: "The type of Actor (Person, Group,…)")
|
field(:type, :actor_type, description: "The type of Actor (Person, Group,…)")
|
||||||
field(:name, :string, description: "The actor's displayed name")
|
field(:name, :string, description: "The actor's displayed name")
|
||||||
|
|
|
@ -5,12 +5,14 @@ defmodule MobilizonWeb.Schema.Actors.PersonType do
|
||||||
use Absinthe.Schema.Notation
|
use Absinthe.Schema.Notation
|
||||||
import Absinthe.Resolution.Helpers, only: [dataloader: 1]
|
import Absinthe.Resolution.Helpers, only: [dataloader: 1]
|
||||||
import_types(MobilizonWeb.Schema.UserType)
|
import_types(MobilizonWeb.Schema.UserType)
|
||||||
|
alias Mobilizon.Events
|
||||||
|
|
||||||
@desc """
|
@desc """
|
||||||
Represents a person identity
|
Represents a person identity
|
||||||
"""
|
"""
|
||||||
object :person do
|
object :person do
|
||||||
interfaces([:actor])
|
interfaces([:actor])
|
||||||
|
field(:id, :id, description: "Internal ID for this person")
|
||||||
field(:user, :user, description: "The user this actor is associated to")
|
field(:user, :user, description: "The user this actor is associated to")
|
||||||
|
|
||||||
field(:member_of, list_of(:member), description: "The list of groups this person is member of")
|
field(:member_of, list_of(:member), description: "The list of groups this person is member of")
|
||||||
|
|
|
@ -6,6 +6,7 @@ defmodule MobilizonWeb.Schema.CommentType do
|
||||||
|
|
||||||
@desc "A comment"
|
@desc "A comment"
|
||||||
object :comment do
|
object :comment do
|
||||||
|
field(:id, :id, description: "Internal ID for this comment")
|
||||||
field(:uuid, :uuid)
|
field(:uuid, :uuid)
|
||||||
field(:url, :string)
|
field(:url, :string)
|
||||||
field(:local, :boolean)
|
field(:local, :boolean)
|
||||||
|
|
|
@ -3,6 +3,7 @@ defmodule MobilizonWeb.Schema.EventType do
|
||||||
Schema representation for Event
|
Schema representation for Event
|
||||||
"""
|
"""
|
||||||
use Absinthe.Schema.Notation
|
use Absinthe.Schema.Notation
|
||||||
|
alias Mobilizon.Actors
|
||||||
import Absinthe.Resolution.Helpers, only: [dataloader: 1]
|
import Absinthe.Resolution.Helpers, only: [dataloader: 1]
|
||||||
import_types(MobilizonWeb.Schema.AddressType)
|
import_types(MobilizonWeb.Schema.AddressType)
|
||||||
import_types(MobilizonWeb.Schema.Events.ParticipantType)
|
import_types(MobilizonWeb.Schema.Events.ParticipantType)
|
||||||
|
@ -10,6 +11,7 @@ defmodule MobilizonWeb.Schema.EventType do
|
||||||
|
|
||||||
@desc "An event"
|
@desc "An event"
|
||||||
object :event do
|
object :event do
|
||||||
|
field(:id, :id, description: "Internal ID for this event")
|
||||||
field(:uuid, :uuid, description: "The Event UUID")
|
field(:uuid, :uuid, description: "The Event UUID")
|
||||||
field(:url, :string, description: "The ActivityPub Event URL")
|
field(:url, :string, description: "The ActivityPub Event URL")
|
||||||
field(:local, :boolean, description: "Whether the event is local or not")
|
field(:local, :boolean, description: "Whether the event is local or not")
|
||||||
|
@ -28,7 +30,7 @@ defmodule MobilizonWeb.Schema.EventType do
|
||||||
field(:online_address, :online_address, description: "Online address of the event")
|
field(:online_address, :online_address, description: "Online address of the event")
|
||||||
field(:phone_address, :phone_address, description: "Phone address for the event")
|
field(:phone_address, :phone_address, description: "Phone address for the event")
|
||||||
|
|
||||||
field(:organizer_actor, :person,
|
field(:organizer_actor, :actor,
|
||||||
resolve: dataloader(Actors),
|
resolve: dataloader(Actors),
|
||||||
description: "The event's organizer (as a person)"
|
description: "The event's organizer (as a person)"
|
||||||
)
|
)
|
||||||
|
|
|
@ -535,7 +535,8 @@ defmodule Mobilizon.Service.ActivityPub do
|
||||||
|
|
||||||
defp ical_event_to_activity(%ExIcal.Event{} = ical_event, %Actor{} = actor, _source) do
|
defp ical_event_to_activity(%ExIcal.Event{} = ical_event, %Actor{} = actor, _source) do
|
||||||
# Logger.debug(inspect ical_event)
|
# Logger.debug(inspect ical_event)
|
||||||
# TODO : refactor me !
|
# TODO : Use MobilizonWeb.API instead
|
||||||
|
# TODO : refactor me and move me somewhere else!
|
||||||
# TODO : also, there should be a form of cache that allows this to be more efficient
|
# TODO : also, there should be a form of cache that allows this to be more efficient
|
||||||
category =
|
category =
|
||||||
if is_nil(ical_event.categories) do
|
if is_nil(ical_event.categories) do
|
||||||
|
|
|
@ -118,7 +118,8 @@ defmodule Mobilizon.Service.ActivityPub.Utils do
|
||||||
"organizer_actor_id" => actor_id,
|
"organizer_actor_id" => actor_id,
|
||||||
"begins_on" => object["begins_on"],
|
"begins_on" => object["begins_on"],
|
||||||
"category_id" => Events.get_category_by_title(object["category"]).id,
|
"category_id" => Events.get_category_by_title(object["category"]).id,
|
||||||
"url" => object["id"]
|
"url" => object["id"],
|
||||||
|
"uuid" => object["uuid"]
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -21,10 +21,17 @@ actor = insert(:actor, user: user)
|
||||||
# Insert a second actor account for the same user
|
# Insert a second actor account for the same user
|
||||||
actor2 = insert(:actor, user: user)
|
actor2 = insert(:actor, user: user)
|
||||||
|
|
||||||
# Make actor organize an event
|
# Make actor organize a few events
|
||||||
event = insert(:event, organizer_actor: actor)
|
event = insert(:event, organizer_actor: actor)
|
||||||
|
event2 = insert(:event, organizer_actor: actor)
|
||||||
|
event3 = insert(:event, organizer_actor: actor)
|
||||||
|
event4 = insert(:event, organizer_actor: actor2)
|
||||||
|
|
||||||
participant = insert(:participant, actor: actor, event: event)
|
participant = insert(:participant, actor: actor, event: event, role: 4)
|
||||||
|
participant = insert(:participant, actor: actor, event: event2, role: 4)
|
||||||
|
participant = insert(:participant, actor: actor, event: event3, role: 4)
|
||||||
|
participant = insert(:participant, actor: actor2, event: event4, role: 4)
|
||||||
|
participant = insert(:participant, actor: actor, event: event4, role: 1)
|
||||||
|
|
||||||
# Insert a group
|
# Insert a group
|
||||||
group = insert(:actor, type: :Group)
|
group = insert(:actor, type: :Group)
|
||||||
|
|
|
@ -95,25 +95,29 @@ defmodule Mobilizon.Factory do
|
||||||
|
|
||||||
def event_factory do
|
def event_factory do
|
||||||
actor = build(:actor)
|
actor = build(:actor)
|
||||||
|
start = Timex.now()
|
||||||
|
uuid = Ecto.UUID.generate()
|
||||||
|
|
||||||
%Mobilizon.Events.Event{
|
%Mobilizon.Events.Event{
|
||||||
title: sequence("Ceci est un événement"),
|
title: sequence("Ceci est un événement"),
|
||||||
description: "Ceci est une description avec une première phrase assez longue,
|
description: "Ceci est une description avec une première phrase assez longue,
|
||||||
puis sur une seconde ligne",
|
puis sur une seconde ligne",
|
||||||
begins_on: nil,
|
begins_on: start,
|
||||||
ends_on: nil,
|
ends_on: Timex.shift(start, hours: 2),
|
||||||
organizer_actor: actor,
|
organizer_actor: actor,
|
||||||
category: build(:category),
|
category: build(:category),
|
||||||
physical_address: build(:address),
|
physical_address: build(:address),
|
||||||
visibility: :public,
|
visibility: :public,
|
||||||
url: "@#{actor.url}/#{Ecto.UUID.generate()}"
|
url: "#{actor.url}/#{uuid}",
|
||||||
|
uuid: uuid
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
def participant_factory do
|
def participant_factory do
|
||||||
%Mobilizon.Events.Participant{
|
%Mobilizon.Events.Participant{
|
||||||
event: build(:event),
|
event: build(:event),
|
||||||
actor: build(:actor)
|
actor: build(:actor),
|
||||||
|
role: 0
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue