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 $@)
|
||||
which gettext-extract
|
||||
# Extract gettext strings from templates files and create a POT dictionary template.
|
||||
gettext-extract --attribute v-translate --quiet --output $@ $(GETTEXT_HTML_SOURCES)
|
||||
gettext-extract --attribute v-translate --quiet --parseScript false --output $@ $(GETTEXT_HTML_SOURCES)
|
||||
# Extract gettext strings from JavaScript files.
|
||||
xgettext --language=JavaScript --keyword=npgettext:1c,2,3 \
|
||||
--from-code=utf-8 --join-existing --no-wrap \
|
||||
|
926
js/package-lock.json
generated
926
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",
|
||||
"dev": "vue-cli-service serve",
|
||||
"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": {
|
||||
"apollo-absinthe-upload-link": "^1.4.0",
|
||||
@ -17,6 +18,7 @@
|
||||
"apollo-link": "^1.2.6",
|
||||
"apollo-link-http": "^1.5.9",
|
||||
"apollo-link-state": "^0.4.2",
|
||||
"buefy": "^0.7.1",
|
||||
"easygettext": "^2.7.0",
|
||||
"graphql": "^14.1.1",
|
||||
"graphql-tag": "^2.10.1",
|
||||
@ -32,8 +34,6 @@
|
||||
"vue-markdown": "^2.2.4",
|
||||
"vue-property-decorator": "^7.2.0",
|
||||
"vue-router": "^3.0.2",
|
||||
"vuetify": "^1.3.9",
|
||||
"vuetify-google-autocomplete": "^2.0.0-beta.5",
|
||||
"vuex": "^3.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
@ -51,6 +51,7 @@
|
||||
"chai": "^4.2.0",
|
||||
"dotenv-webpack": "^1.5.7",
|
||||
"node-sass": "^4.10.0",
|
||||
"patch-package": "^5.1.1",
|
||||
"sass-loader": "^7.1.0",
|
||||
"tslint-config-airbnb": "^5.11.1",
|
||||
"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,12 +1,15 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<html class="has-navbar-fixed-top">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
||||
<link rel="stylesheet" href="//cdn.materialdesignicons.com/2.5.94/css/materialdesignicons.min.css">
|
||||
<title>mobilizon</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<noscript>
|
||||
<strong>We're sorry but mobilizon doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
||||
@ -14,4 +17,5 @@
|
||||
<div id="app"></div>
|
||||
<!-- built files will be auto injected -->
|
||||
</body>
|
||||
|
||||
</html>
|
200
js/src/App.vue
200
js/src/App.vue
@ -1,152 +1,19 @@
|
||||
<template>
|
||||
<v-app id="libre-event">
|
||||
<v-navigation-drawer
|
||||
light
|
||||
clipped
|
||||
fixed
|
||||
app
|
||||
v-model="drawer"
|
||||
enable-resize-watcher
|
||||
>
|
||||
<v-list dense>
|
||||
<v-list-group
|
||||
value="false"
|
||||
>
|
||||
<v-list-tile avatar v-if="actor" slot="activator">
|
||||
<v-list-tile-avatar>
|
||||
<img v-if="!actor.avatar"
|
||||
class="img-circle elevation-7 mb-1"
|
||||
src="https://picsum.photos/125/125/"
|
||||
>
|
||||
<img v-else
|
||||
class="img-circle elevation-7 mb-1"
|
||||
:src="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">
|
||||
<div id="mobilizon">
|
||||
<NavBar></NavBar>
|
||||
<main class="container">
|
||||
<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>
|
||||
</main>
|
||||
<footer class="footer">
|
||||
<div class="content has-text-centered">
|
||||
<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>
|
||||
}"
|
||||
>© The Mobilizon Contributors %{date} - Made with Elixir, Phoenix, VueJS & with some love and some weeks</span>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
@ -163,31 +30,40 @@ import { ICurrentUser } from '@/types/current-user.model'
|
||||
}
|
||||
},
|
||||
components: {
|
||||
NavBar,
|
||||
},
|
||||
NavBar
|
||||
}
|
||||
})
|
||||
export default class App extends Vue {
|
||||
drawer = false;
|
||||
fab = false;
|
||||
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: 'chat_bubble', text: 'Send feedback', role: 'ROLE_USER' },
|
||||
{ icon: 'help', text: 'Help', role: null },
|
||||
{ icon: 'phonelink', text: 'App downloads', role: null },
|
||||
{ icon: "settings", text: "Settings", role: "ROLE_USER" },
|
||||
{ icon: "chat_bubble", text: "Send feedback", role: "ROLE_USER" },
|
||||
{ icon: "help", text: "Help", role: null },
|
||||
{ icon: "phonelink", text: "App downloads", role: null }
|
||||
];
|
||||
error = {
|
||||
timeout: 3000,
|
||||
show: false,
|
||||
text: '',
|
||||
text: ""
|
||||
};
|
||||
currentUser!: ICurrentUser;
|
||||
|
||||
@ -199,7 +75,7 @@ export default class App extends Vue {
|
||||
|
||||
get displayed_name () {
|
||||
// FIXME: load actor
|
||||
return 'no implemented';
|
||||
return "no implemented";
|
||||
// return this.actor.display_name === null ? this.actor.username : this.actor.display_name
|
||||
}
|
||||
|
||||
@ -209,7 +85,7 @@ export default class App extends Vue {
|
||||
// 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;
|
||||
}
|
||||
|
||||
@ -236,16 +112,18 @@ export default class App extends Vue {
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.router-enter-active, .router-leave-active {
|
||||
.router-enter-active,
|
||||
.router-leave-active {
|
||||
transition-property: opacity;
|
||||
transition-duration: .25s;
|
||||
transition-duration: 0.25s;
|
||||
}
|
||||
|
||||
.router-enter-active {
|
||||
transition-delay: .25s;
|
||||
transition-delay: 0.25s;
|
||||
}
|
||||
|
||||
.router-enter, .router-leave-active {
|
||||
opacity: 0
|
||||
.router-enter,
|
||||
.router-leave-active {
|
||||
opacity: 0;
|
||||
}
|
||||
</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,103 +1,47 @@
|
||||
<template>
|
||||
<v-toolbar
|
||||
class="blue darken-3"
|
||||
dark
|
||||
app
|
||||
:clipped-left="$vuetify.breakpoint.lgAndUp"
|
||||
fixed
|
||||
<nav class="navbar is-fixed-top" role="navigation" aria-label="main navigation">
|
||||
<div class="navbar-brand">
|
||||
<router-link class="navbar-item" :to="{ name: 'Home' }">Mobilizon</router-link>
|
||||
|
||||
<a
|
||||
role="button"
|
||||
class="navbar-burger burger"
|
||||
aria-label="menu"
|
||||
aria-expanded="false"
|
||||
data-target="navbarBasicExample"
|
||||
>
|
||||
<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
|
||||
<span aria-hidden="true"></span>
|
||||
<span aria-hidden="true"></span>
|
||||
<span aria-hidden="true"></span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="navbar-end">
|
||||
<div class="navbar-item">
|
||||
<div class="buttons">
|
||||
<router-link class="button is-primary" v-if="!currentUser.id" :to="{ name: 'Register' }">
|
||||
<strong>
|
||||
<translate>Sign up</translate>
|
||||
</strong>
|
||||
</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>
|
||||
|
||||
<v-menu
|
||||
offset-y
|
||||
:close-on-content-click="false"
|
||||
:nudge-width="200"
|
||||
v-model="notificationMenu"
|
||||
v-if="currentUser.id"
|
||||
>
|
||||
<v-btn icon slot="activator">
|
||||
<v-badge left color="red">
|
||||
<span slot="badge">{{ notifications.length }}</span>
|
||||
<v-icon>notifications</v-icon>
|
||||
</v-badge>
|
||||
</v-btn>
|
||||
<v-card>
|
||||
<v-list two-line>
|
||||
<template v-for="item in notifications">
|
||||
<v-subheader v-if="item.header" v-text="item.header" v-bind:key="item.header"></v-subheader>
|
||||
<v-divider v-else-if="item.divider" v-bind:inset="item.inset" v-bind:key="item.inset"></v-divider>
|
||||
<v-list-tile avatar v-else v-bind:key="item.title">
|
||||
<v-list-tile-content>
|
||||
<v-list-tile-title v-html="item.title"></v-list-tile-title>
|
||||
<v-list-tile-sub-title v-html="item.subtitle"></v-list-tile-sub-title>
|
||||
</v-list-tile-content>
|
||||
</v-list-tile>
|
||||
</template>
|
||||
</v-list>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn flat @click="notificationMenu = false">
|
||||
<translate>Close</translate>
|
||||
</v-btn>
|
||||
<v-btn color="primary" flat @click="notificationMenu = false">
|
||||
<translate>Save</translate>
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-menu>
|
||||
<v-btn v-if="!currentUser.id" :to="{ name: 'Login' }">
|
||||
<router-link class="button is-light" v-if="!currentUser.id" :to="{ name: 'Login' }">
|
||||
<translate>Log in</translate>
|
||||
</v-btn>
|
||||
</v-toolbar>
|
||||
</router-link>
|
||||
<router-link
|
||||
class="button is-light"
|
||||
v-if="currentUser.id"
|
||||
:to="{ name: 'Profile', params: { name: loggedPerson.preferredUsername} }"
|
||||
>
|
||||
<figure class="image is-24x24">
|
||||
<img :src="loggedPerson.avatarUrl">
|
||||
</figure>
|
||||
<span>{{ loggedPerson.preferredUsername }}</span>
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
nav.v-toolbar .v-input__slot {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue, Watch } from 'vue-property-decorator';
|
||||
import { AUTH_USER_ACTOR } from '@/constants';
|
||||
@ -105,6 +49,8 @@
|
||||
import { CURRENT_USER_CLIENT } from '@/graphql/user';
|
||||
import { onLogout } from '@/vue-apollo';
|
||||
import { deleteUserData } from '@/utils/auth';
|
||||
import { LOGGED_PERSON } from "@/graphql/actor";
|
||||
import { IPerson } from "../types/actor.model";
|
||||
|
||||
@Component({
|
||||
apollo: {
|
||||
@ -121,30 +67,32 @@
|
||||
},
|
||||
currentUser: {
|
||||
query: CURRENT_USER_CLIENT
|
||||
},
|
||||
loggedPerson: {
|
||||
query: LOGGED_PERSON
|
||||
}
|
||||
},
|
||||
})
|
||||
export default class NavBar extends Vue {
|
||||
@Prop({ required: true, type: Function }) toggleDrawer!: Function;
|
||||
|
||||
notificationMenu = false;
|
||||
notifications = [
|
||||
{ header: 'Coucou' },
|
||||
{ title: 'T\'as une notification', subtitle: 'Et elle est cool' },
|
||||
{ header: "Coucou" },
|
||||
{ title: "T'as une notification", subtitle: "Et elle est cool" }
|
||||
];
|
||||
model = null;
|
||||
search: any[] = [];
|
||||
searchText: string | null = null;
|
||||
searchSelect = null;
|
||||
actor = localStorage.getItem(AUTH_USER_ACTOR);
|
||||
loggedPerson!: IPerson;
|
||||
|
||||
get items() {
|
||||
return this.search.map(searchEntry => {
|
||||
switch (searchEntry.__typename) {
|
||||
case 'Actor':
|
||||
searchEntry.label = searchEntry.preferredUsername + (searchEntry.domain === null ? '' : `@${searchEntry.domain}`);
|
||||
case "Actor":
|
||||
searchEntry.label =
|
||||
searchEntry.preferredUsername +
|
||||
(searchEntry.domain === null ? "" : `@${searchEntry.domain}`);
|
||||
break;
|
||||
case 'Event':
|
||||
case "Event":
|
||||
searchEntry.label = searchEntry.title;
|
||||
break;
|
||||
}
|
||||
@ -152,25 +100,31 @@ export default class NavBar extends Vue {
|
||||
});
|
||||
}
|
||||
|
||||
@Watch('model')
|
||||
@Watch("model")
|
||||
onModelChanged(val) {
|
||||
switch (val.__typename) {
|
||||
case 'Event':
|
||||
this.$router.push({ name: 'Event', params: { uuid: val.uuid } });
|
||||
case "Event":
|
||||
this.$router.push({ name: "Event", params: { uuid: val.uuid } });
|
||||
break;
|
||||
case 'Actor':
|
||||
this.$router.push({ name: 'Account', params: { name: this.username_with_domain(val) } });
|
||||
case "Actor":
|
||||
this.$router.push({
|
||||
name: "Profile",
|
||||
params: { name: this.username_with_domain(val) }
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
username_with_domain(actor) {
|
||||
return actor.preferredUsername + (actor.domain === null ? '' : `@${actor.domain}`);
|
||||
return (
|
||||
actor.preferredUsername +
|
||||
(actor.domain === null ? "" : `@${actor.domain}`)
|
||||
);
|
||||
}
|
||||
|
||||
enter() {
|
||||
console.log('enter');
|
||||
this.$apollo.queries['search'].refetch();
|
||||
console.log("enter");
|
||||
this.$apollo.queries["search"].refetch();
|
||||
}
|
||||
|
||||
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';
|
||||
|
||||
export const FETCH_ACTOR = gql`
|
||||
export const FETCH_PERSON = gql`
|
||||
query($name:String!) {
|
||||
actor(preferredUsername: $name) {
|
||||
person(preferredUsername: $name) {
|
||||
url,
|
||||
outboxUrl,
|
||||
inboxUrl,
|
||||
followingUrl,
|
||||
followersUrl,
|
||||
sharedInboxUrl,
|
||||
name,
|
||||
domain,
|
||||
summary,
|
||||
@ -18,22 +13,36 @@ query($name:String!) {
|
||||
bannerUrl,
|
||||
organizedEvents {
|
||||
uuid,
|
||||
title,
|
||||
description,
|
||||
organizer_actor {
|
||||
avatarUrl,
|
||||
preferred_username,
|
||||
name,
|
||||
}
|
||||
title
|
||||
},
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const LOGGED_ACTOR = gql`
|
||||
export const LOGGED_PERSON = gql`
|
||||
query {
|
||||
loggedActor {
|
||||
loggedPerson {
|
||||
id,
|
||||
avatarUrl,
|
||||
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`
|
||||
query($uuid:UUID!) {
|
||||
event(uuid: $uuid) {
|
||||
id,
|
||||
uuid,
|
||||
url,
|
||||
local,
|
||||
title,
|
||||
description,
|
||||
begins_on,
|
||||
ends_on,
|
||||
beginsOn,
|
||||
endsOn,
|
||||
status,
|
||||
visibility,
|
||||
thumbnail,
|
||||
@ -22,11 +23,11 @@ export const FETCH_EVENT = gql`
|
||||
preferredUsername,
|
||||
name,
|
||||
},
|
||||
attributedTo {
|
||||
avatarUrl,
|
||||
preferredUsername,
|
||||
name,
|
||||
},
|
||||
# attributedTo {
|
||||
# # avatarUrl,
|
||||
# preferredUsername,
|
||||
# name,
|
||||
# },
|
||||
participants {
|
||||
actor {
|
||||
avatarUrl,
|
||||
@ -45,13 +46,14 @@ export const FETCH_EVENT = gql`
|
||||
export const FETCH_EVENTS = gql`
|
||||
query {
|
||||
events {
|
||||
id,
|
||||
uuid,
|
||||
url,
|
||||
local,
|
||||
title,
|
||||
description,
|
||||
begins_on,
|
||||
ends_on,
|
||||
beginsOn,
|
||||
endsOn,
|
||||
status,
|
||||
visibility,
|
||||
thumbnail,
|
||||
@ -72,6 +74,14 @@ export const FETCH_EVENTS = gql`
|
||||
category {
|
||||
title,
|
||||
},
|
||||
participants {
|
||||
role,
|
||||
actor {
|
||||
preferredUsername,
|
||||
avatarUrl,
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
@ -80,8 +90,8 @@ export const CREATE_EVENT = gql`
|
||||
mutation CreateEvent(
|
||||
$title: String!,
|
||||
$description: String!,
|
||||
$organizerActorId: Int!,
|
||||
$categoryId: Int!,
|
||||
$organizerActorId: String!,
|
||||
$category: String!,
|
||||
$beginsOn: DateTime!
|
||||
) {
|
||||
createEvent(
|
||||
@ -89,8 +99,12 @@ export const CREATE_EVENT = gql`
|
||||
description: $description,
|
||||
beginsOn: $beginsOn,
|
||||
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 ""
|
||||
"Project-Id-Version: mobilizon 0.1.0\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"
|
||||
"Last-Translator: Automatically generated\n"
|
||||
"Language-Team: none\n"
|
||||
@ -17,14 +17,178 @@ msgstr ""
|
||||
"Content-Transfer-Encoding: 8bit\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}"
|
||||
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"
|
||||
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"
|
||||
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 ""
|
||||
"Project-Id-Version: mobilizon 0.1.0\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"
|
||||
"Last-Translator: Automatically generated\n"
|
||||
"Language-Team: none\n"
|
||||
@ -17,14 +17,178 @@ msgstr ""
|
||||
"Content-Transfer-Encoding: 8bit\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}"
|
||||
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"
|
||||
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"
|
||||
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"
|
||||
"Last-Translator: Automatically generated\n"
|
||||
"Language-Team: none\n"
|
||||
"Language: fr\n"
|
||||
"Language: fr_FR\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
@ -3,10 +3,9 @@
|
||||
import Vue from 'vue';
|
||||
// import * as VueGoogleMaps from 'vue2-google-maps';
|
||||
import VueMarkdown from 'vue-markdown';
|
||||
import Vuetify from 'vuetify';
|
||||
import Buefy from 'buefy'
|
||||
import 'buefy/dist/buefy.css';
|
||||
import GetTextPlugin from 'vue-gettext';
|
||||
import 'material-design-icons/iconfont/material-icons.css';
|
||||
import 'vuetify/dist/vuetify.min.css';
|
||||
import App from '@/App.vue';
|
||||
import router from '@/router';
|
||||
import { apolloProvider } from './vue-apollo';
|
||||
@ -16,7 +15,9 @@ const translations = require('@/i18n/translations.json');
|
||||
Vue.config.productionTip = false;
|
||||
|
||||
Vue.use(VueMarkdown);
|
||||
Vue.use(Vuetify);
|
||||
Vue.use(Buefy, {
|
||||
defaultContainerElement: '#mobilizon'
|
||||
});
|
||||
|
||||
const language = (window.navigator as any).userLanguage || window.navigator.language;
|
||||
|
||||
|
@ -1,24 +1,24 @@
|
||||
import Vue from 'vue';
|
||||
import Router from 'vue-router';
|
||||
import PageNotFound from '@/components/PageNotFound.vue';
|
||||
import Home from '@/components/Home.vue';
|
||||
import Event from '@/components/Event/Event.vue';
|
||||
import EventList from '@/components/Event/EventList.vue';
|
||||
import Location from '@/components/Location.vue';
|
||||
import CreateEvent from '@/components/Event/Create.vue';
|
||||
import CategoryList from '@/components/Category/List.vue';
|
||||
import CreateCategory from '@/components/Category/Create.vue';
|
||||
import Register from '@/components/Account/Register.vue';
|
||||
import Login from '@/components/Account/Login.vue';
|
||||
import Validate from '@/components/Account/Validate.vue';
|
||||
import ResendConfirmation from '@/components/Account/ResendConfirmation.vue';
|
||||
import SendPasswordReset from '@/components/Account/SendPasswordReset.vue';
|
||||
import PasswordReset from '@/components/Account/PasswordReset.vue';
|
||||
import Account from '@/components/Account/Account.vue';
|
||||
import CreateGroup from '@/components/Group/Create.vue';
|
||||
import Group from '@/components/Group/Group.vue';
|
||||
import GroupList from '@/components/Group/GroupList.vue';
|
||||
import Identities from '../components/Account/Identities.vue';
|
||||
import PageNotFound from '@/views/PageNotFound.vue';
|
||||
import Home from '@/views/Home.vue';
|
||||
import Event from '@/views/Event/Event.vue';
|
||||
import EventList from '@/views/Event/EventList.vue';
|
||||
import Location from '@/views/Location.vue';
|
||||
import CreateEvent from '@/views/Event/Create.vue';
|
||||
import CategoryList from '@/views/Category/List.vue';
|
||||
import CreateCategory from '@/views/Category/Create.vue';
|
||||
import Register from '@/views/Account/Register.vue';
|
||||
import Login from '@/views/User/Login.vue';
|
||||
import Validate from '@/views/User/Validate.vue';
|
||||
import ResendConfirmation from '@/views/User/ResendConfirmation.vue';
|
||||
import SendPasswordReset from '@/views/User/SendPasswordReset.vue';
|
||||
import PasswordReset from '@/views/User/PasswordReset.vue';
|
||||
import Profile from '@/views/Account/Profile.vue';
|
||||
import CreateGroup from '@/views/Group/Create.vue';
|
||||
import Group from '@/views/Group/Group.vue';
|
||||
import GroupList from '@/views/Group/GroupList.vue';
|
||||
import Identities from '@/views/Account/Identities.vue';
|
||||
|
||||
Vue.use(Router);
|
||||
|
||||
@ -45,7 +45,7 @@ const router = new Router({
|
||||
meta: { requiredAuth: true },
|
||||
},
|
||||
{
|
||||
path: '/events/:id(\\d+)/edit',
|
||||
path: '/events/:id/edit',
|
||||
name: 'EditEvent',
|
||||
component: CreateEvent,
|
||||
props: true,
|
||||
@ -124,7 +124,7 @@ const router = new Router({
|
||||
meta: { requiredAuth: false },
|
||||
},
|
||||
{
|
||||
path: '/group-create',
|
||||
path: '/groups/create',
|
||||
name: 'CreateGroup',
|
||||
component: CreateGroup,
|
||||
meta: { requiredAuth: true },
|
||||
@ -138,8 +138,8 @@ const router = new Router({
|
||||
},
|
||||
{
|
||||
path: '/@:name',
|
||||
name: 'Account',
|
||||
component: Account,
|
||||
name: 'Profile',
|
||||
component: Profile,
|
||||
props: true,
|
||||
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>
|
@ -42,7 +42,7 @@ defmodule Mobilizon.Actors.Actor do
|
||||
field(:shared_inbox_url, :string)
|
||||
field(:type, Mobilizon.Actors.ActorTypeEnum, default: :Person)
|
||||
field(:name, :string)
|
||||
field(:domain, :string)
|
||||
field(:domain, :string, default: nil)
|
||||
field(:summary, :string)
|
||||
field(:preferred_username, :string)
|
||||
field(:keys, :string)
|
||||
|
@ -77,9 +77,30 @@ defmodule Mobilizon.Actors do
|
||||
Repo.all(from(a in Actor, where: a.user_id == ^user_id))
|
||||
end
|
||||
|
||||
def get_actor_with_everything!(id) do
|
||||
actor = Repo.get!(Actor, id)
|
||||
Repo.preload(actor, [:organized_events, :followers, :followings])
|
||||
@spec get_actor_with_everything(integer()) :: Ecto.Query
|
||||
defp do_get_actor_with_everything(id) do
|
||||
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
|
||||
|
||||
@doc """
|
||||
@ -610,6 +631,19 @@ defmodule Mobilizon.Actors do
|
||||
{:error, hd(email_msg)}
|
||||
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
|
||||
key = :public_key.generate_key({:rsa, 2048, 65_537})
|
||||
entry = :public_key.pem_entry_encode(:RSAPrivateKey, key)
|
||||
|
@ -24,10 +24,12 @@ defmodule Mobilizon.Actors.Service.ResetPassword do
|
||||
{:ok, user}
|
||||
else
|
||||
{: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 ->
|
||||
{: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
|
||||
|
||||
|
@ -68,11 +68,11 @@ defmodule Mobilizon.Events.Event do
|
||||
:large_image,
|
||||
:publish_at,
|
||||
:online_address,
|
||||
:phone_address
|
||||
:phone_address,
|
||||
:uuid
|
||||
])
|
||||
|> cast_assoc(:tags)
|
||||
|> cast_assoc(:physical_address)
|
||||
|> build_url()
|
||||
|> validate_required([
|
||||
:title,
|
||||
:begins_on,
|
||||
@ -82,31 +82,4 @@ defmodule Mobilizon.Events.Event do
|
||||
:uuid
|
||||
])
|
||||
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
|
||||
|
@ -220,7 +220,7 @@ defmodule Mobilizon.Events do
|
||||
from(
|
||||
e in Event,
|
||||
where: e.visibility == ^:public,
|
||||
preload: [:organizer_actor]
|
||||
preload: [:organizer_actor, :participants]
|
||||
)
|
||||
|> paginate(page, limit)
|
||||
|
||||
|
@ -14,12 +14,12 @@ defmodule MobilizonWeb.API.Events do
|
||||
%{
|
||||
title: title,
|
||||
description: description,
|
||||
organizer_actor_username: organizer_actor_username,
|
||||
organizer_actor_id: organizer_actor_id,
|
||||
begins_on: begins_on,
|
||||
category: category
|
||||
} = args
|
||||
) 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),
|
||||
mentions <- Formatter.parse_mentions(description),
|
||||
visibility <- Map.get(args, :visibility, "public"),
|
||||
|
@ -39,8 +39,8 @@ defmodule MobilizonWeb.Resolvers.Event do
|
||||
@doc """
|
||||
List participants for event (through an event request)
|
||||
"""
|
||||
def list_participants_for_event(%{uuid: uuid}, %{page: page, limit: limit}, _resolution) do
|
||||
{:ok, Mobilizon.Events.list_participants_for_event(uuid, page, limit)}
|
||||
def list_participants_for_event(%Event{uuid: uuid}, _args, _resolution) do
|
||||
{:ok, Mobilizon.Events.list_participants_for_event(uuid, 1, 10)}
|
||||
end
|
||||
|
||||
@doc """
|
||||
@ -81,14 +81,7 @@ defmodule MobilizonWeb.Resolvers.Event do
|
||||
"""
|
||||
def create_event(_parent, args, %{context: %{current_user: user}}) do
|
||||
with {:ok, %Activity{data: %{"object" => %{"type" => "Event"} = object}}} <-
|
||||
args
|
||||
# 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
|
||||
MobilizonWeb.API.Events.create_event(args) do
|
||||
{:ok,
|
||||
%Event{
|
||||
title: object["name"],
|
||||
|
@ -3,6 +3,7 @@ defmodule MobilizonWeb.Resolvers.Person do
|
||||
Handles the person-related GraphQL calls
|
||||
"""
|
||||
alias Mobilizon.Actors
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Service.ActivityPub
|
||||
|
||||
@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
|
||||
{:error, "You need to be logged-in to view current person"}
|
||||
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
|
||||
|
@ -36,7 +36,7 @@ defmodule MobilizonWeb.Resolvers.User do
|
||||
{:error, "User with email not found"}
|
||||
|
||||
{:error, :unauthorized} ->
|
||||
{:error, "Impossible to authenticate"}
|
||||
{:error, "Impossible to authenticate, either your email or password are invalid."}
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -104,7 +104,7 @@ defmodule MobilizonWeb.Schema do
|
||||
end
|
||||
|
||||
def plugins do
|
||||
[Absinthe.Middleware.Dataloader | Absinthe.Plugin.defaults()]
|
||||
[Absinthe.Middleware.Dataloader] ++ Absinthe.Plugin.defaults()
|
||||
end
|
||||
|
||||
@desc """
|
||||
@ -175,6 +175,11 @@ defmodule MobilizonWeb.Schema do
|
||||
resolve(&Resolvers.Person.find_person/3)
|
||||
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"
|
||||
field :categories, non_null(list_of(:category)) do
|
||||
arg(:page, :integer, default_value: 1)
|
||||
@ -201,7 +206,7 @@ defmodule MobilizonWeb.Schema do
|
||||
arg(:publish_at, :datetime)
|
||||
arg(:online_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))
|
||||
|
||||
resolve(&Resolvers.Event.create_event/3)
|
||||
@ -273,6 +278,16 @@ defmodule MobilizonWeb.Schema do
|
||||
resolve(&Resolvers.User.change_default_actor/3)
|
||||
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"
|
||||
field :create_group, :group do
|
||||
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
|
||||
import Absinthe.Resolution.Helpers, only: [dataloader: 1]
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Events
|
||||
|
||||
import_types(MobilizonWeb.Schema.Actors.FollowerType)
|
||||
import_types(MobilizonWeb.Schema.EventType)
|
||||
|
||||
@desc "An ActivityPub actor"
|
||||
interface :actor do
|
||||
field(:id, :id, description: "Internal ID for this actor")
|
||||
field(:url, :string, description: "The ActivityPub actor's URL")
|
||||
field(:type, :actor_type, description: "The type of Actor (Person, Group,…)")
|
||||
field(:name, :string, description: "The actor's displayed name")
|
||||
@ -51,6 +53,9 @@ defmodule MobilizonWeb.Schema.ActorInterface do
|
||||
|
||||
%Actor{type: :Group}, _ ->
|
||||
:group
|
||||
|
||||
_, _ ->
|
||||
nil
|
||||
end)
|
||||
end
|
||||
|
||||
|
@ -12,6 +12,7 @@ defmodule MobilizonWeb.Schema.Actors.GroupType do
|
||||
object :group do
|
||||
interfaces([:actor])
|
||||
|
||||
field(:id, :id, description: "Internal ID for this group")
|
||||
field(:url, :string, description: "The ActivityPub actor's URL")
|
||||
field(:type, :actor_type, description: "The type of Actor (Person, Group,…)")
|
||||
field(:name, :string, description: "The actor's displayed name")
|
||||
|
@ -5,12 +5,14 @@ defmodule MobilizonWeb.Schema.Actors.PersonType do
|
||||
use Absinthe.Schema.Notation
|
||||
import Absinthe.Resolution.Helpers, only: [dataloader: 1]
|
||||
import_types(MobilizonWeb.Schema.UserType)
|
||||
alias Mobilizon.Events
|
||||
|
||||
@desc """
|
||||
Represents a person identity
|
||||
"""
|
||||
object :person do
|
||||
interfaces([:actor])
|
||||
field(:id, :id, description: "Internal ID for this person")
|
||||
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")
|
||||
|
@ -6,6 +6,7 @@ defmodule MobilizonWeb.Schema.CommentType do
|
||||
|
||||
@desc "A comment"
|
||||
object :comment do
|
||||
field(:id, :id, description: "Internal ID for this comment")
|
||||
field(:uuid, :uuid)
|
||||
field(:url, :string)
|
||||
field(:local, :boolean)
|
||||
|
@ -3,6 +3,7 @@ defmodule MobilizonWeb.Schema.EventType do
|
||||
Schema representation for Event
|
||||
"""
|
||||
use Absinthe.Schema.Notation
|
||||
alias Mobilizon.Actors
|
||||
import Absinthe.Resolution.Helpers, only: [dataloader: 1]
|
||||
import_types(MobilizonWeb.Schema.AddressType)
|
||||
import_types(MobilizonWeb.Schema.Events.ParticipantType)
|
||||
@ -10,6 +11,7 @@ defmodule MobilizonWeb.Schema.EventType do
|
||||
|
||||
@desc "An event"
|
||||
object :event do
|
||||
field(:id, :id, description: "Internal ID for this event")
|
||||
field(:uuid, :uuid, description: "The Event UUID")
|
||||
field(:url, :string, description: "The ActivityPub Event URL")
|
||||
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(:phone_address, :phone_address, description: "Phone address for the event")
|
||||
|
||||
field(:organizer_actor, :person,
|
||||
field(:organizer_actor, :actor,
|
||||
resolve: dataloader(Actors),
|
||||
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
|
||||
# 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
|
||||
category =
|
||||
if is_nil(ical_event.categories) do
|
||||
|
@ -118,7 +118,8 @@ defmodule Mobilizon.Service.ActivityPub.Utils do
|
||||
"organizer_actor_id" => actor_id,
|
||||
"begins_on" => object["begins_on"],
|
||||
"category_id" => Events.get_category_by_title(object["category"]).id,
|
||||
"url" => object["id"]
|
||||
"url" => object["id"],
|
||||
"uuid" => object["uuid"]
|
||||
}
|
||||
end
|
||||
|
||||
|
@ -21,10 +21,17 @@ actor = insert(:actor, user: user)
|
||||
# Insert a second actor account for the same user
|
||||
actor2 = insert(:actor, user: user)
|
||||
|
||||
# Make actor organize an event
|
||||
# Make actor organize a few events
|
||||
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
|
||||
group = insert(:actor, type: :Group)
|
||||
|
@ -95,25 +95,29 @@ defmodule Mobilizon.Factory do
|
||||
|
||||
def event_factory do
|
||||
actor = build(:actor)
|
||||
start = Timex.now()
|
||||
uuid = Ecto.UUID.generate()
|
||||
|
||||
%Mobilizon.Events.Event{
|
||||
title: sequence("Ceci est un événement"),
|
||||
description: "Ceci est une description avec une première phrase assez longue,
|
||||
puis sur une seconde ligne",
|
||||
begins_on: nil,
|
||||
ends_on: nil,
|
||||
begins_on: start,
|
||||
ends_on: Timex.shift(start, hours: 2),
|
||||
organizer_actor: actor,
|
||||
category: build(:category),
|
||||
physical_address: build(:address),
|
||||
visibility: :public,
|
||||
url: "@#{actor.url}/#{Ecto.UUID.generate()}"
|
||||
url: "#{actor.url}/#{uuid}",
|
||||
uuid: uuid
|
||||
}
|
||||
end
|
||||
|
||||
def participant_factory do
|
||||
%Mobilizon.Events.Participant{
|
||||
event: build(:event),
|
||||
actor: build(:actor)
|
||||
actor: build(:actor),
|
||||
role: 0
|
||||
}
|
||||
end
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user