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:
Thomas Citharel 2019-01-21 15:08:22 +01:00
parent 759a740625
commit 90fd0ff6b6
No known key found for this signature in database
GPG key ID: A061B9DDE0CA0773
79 changed files with 3482 additions and 3369 deletions

View file

@ -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

File diff suppressed because it is too large Load diff

View file

@ -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",

View 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);
}

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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&#45;&#45;text">{{ event.organizer.username }}</span></router-link> organises {{ event.title }} <span v-if="event.address.addressLocality">in {{ event.address.addressLocality }}</span> on the {{ event.startDate | formatDate }}.</p> -->
<v-card-text v-if="event.description">
<vue-markdown :source="event.description"></vue-markdown>
</v-card-text>
</v-flex>
<!-- <v-flex md2>
<p v-if="actorIsOrganizer()">
Vous êtes organisateur de cet événement.
</p>
<div v-else>
<p v-if="actorIsParticipant()">
Vous avez annoncé aller à cet événement.
</p>
<p v-else>Vous y allez ?
<span class="text--darken-2 grey--text">{{ event.participants.length }} personnes y vont.</span>
</p>
</div>
<v-card-actions v-if="!actorIsOrganizer()">
<v-btn v-if="!actorIsParticipant()" @click="joinEvent" color="success"><v-icon>check</v-icon> Join</v-btn>
<v-btn v-if="actorIsParticipant()" @click="leaveEvent" color="error">Leave</v-btn>
</v-card-actions>
</v-flex> -->
</v-layout>
</v-container>
<v-divider></v-divider>
<v-container>
<v-layout row wrap>
<v-flex xs12 md4 order-md1>
<v-layout
column
fill-height
>
<v-list two-line>
<v-list-tile>
<v-list-tile-action>
<v-icon color="indigo">access_time</v-icon>
</v-list-tile-action>
<v-list-tile-content>
<v-list-tile-title>{{ event.begins_on | formatDate }}</v-list-tile-title>
<v-list-tile-sub-title>{{ event.ends_on | formatDate }}</v-list-tile-sub-title>
</v-list-tile-content>
</v-list-tile>
<v-divider inset></v-divider>
<v-list-tile>
<v-list-tile-action>
<v-icon color="indigo">place</v-icon>
</v-list-tile-action>
<v-list-tile-content>
<v-list-tile-title>
{{ 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>

View 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}"
>&nbsp;%{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>

View file

@ -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>

View file

@ -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"-->
<!--&gt;-->
<!--</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)"-->
<!--&gt;</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>

View file

@ -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>

View 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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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() {

View file

@ -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>

View file

@ -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
}
}
`

View file

@ -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
)
}
`;

View file

@ -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 ""

View file

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

View file

@ -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 ""

View file

@ -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"

View file

@ -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;

View file

@ -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 },
},

View 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;
}

View 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;
}

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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
View 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
View 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>

View 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
View 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>

View 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>

View 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>

View 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>

View 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>

View file

@ -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)

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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"),

View file

@ -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"],

View file

@ -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

View file

@ -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

View file

@ -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")

View file

@ -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

View file

@ -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")

View file

@ -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")

View file

@ -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)

View file

@ -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)"
)

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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