Commit 61f42e48 authored by Sandro Lutz's avatar Sandro Lutz
Browse files

Add ininite scroll to events page

parent 72c218e7
......@@ -8,7 +8,6 @@ import { login, isLoggedIn, checkLogin } from './models/auth';
import studydocList from './views/studydocs/studydocList';
import studydocNew from './views/studydocs/studydocNew';
import eventList from './views/events/eventList';
import eventDetails from './views/events/eventDetails';
import profile from './views/profile';
import layout from './views/layout';
import frontpage from './views/frontpage';
......@@ -93,7 +92,7 @@ Raven.context(() => {
},
{
url: '/:language/events/:eventId',
view: vnode => m(eventDetails, vnode.attrs),
view: vnode => m(eventList, vnode.attrs),
},
{
url: '/:language/jobs',
......
......@@ -2,6 +2,7 @@
"values": {
"language.de": "Deutsch",
"language.en": "Englisch",
"loading": "Laden...",
"AMIV": "AMIV",
"About AMIV": "Über den AMIV",
"Board": "Vorstand",
......@@ -34,16 +35,14 @@
"withdraw": "austragen",
"username": "Benutzername",
"password": "Passwort",
"event.title": "TItel",
"event.start_time": "Start Zeit",
"event.signup_count": "Anmeldungen",
"event.spots": "Freie Plätze",
"regular_member": "Ordentliches Mitglied",
"extraordinary_member": "Ausserordentliches Mitglied",
"honorary_member": "Ehrenmitglied",
"no image": "Kein Bild verfügbar.",
"no description": "Keine Beschreibung verfügbar.",
"translation unavailable": "Übersetzung nicht verfügbar. Zeige text in %{shown_language}",
"email": "Email",
"email_invalid": "Ungültige Email-Adresse",
"search": "Suchen",
"frontpage.whats_hot": "Was ist brandaktuell?",
"frontpage.social_media": "Folge uns auf Social Media",
......@@ -95,6 +94,21 @@
"events.price": "Preis",
"events.free": "Gratis",
"events.small_fee": "Kleine Teilnahmegebühr",
"events.not_found": "Event nichr gefunden",
"events.signed_up": "Du hast dich für diesen Event angemeldet.",
"events.no_registration": "Keine Anmeldung erforderlich",
"events.registration_over": "Das Registrierungs-Fenster ist geschlossen.",
"events.registrations_starts_at": "Die Registrierungs-Periode startet am %{time}",
"events.%n_spots_available": [
[-1, -1, "Plätze verfügbar"],
[0, 0, "Keine Plätze verfügbar"],
[1, 1, "%n Platz verfügbar"],
[2, null, "%n Plätze verfügbar"]
],
"events.update_data": "Alktualisiere deine Daten im unteren Formular.",
"events.amiv_members_only": "Dieser Event ist nur für AMIV Mitglieder.",
"events.signup": "anmelden",
"events.delete_signup": "Anmeldung löschen",
"companies.contact_information": "Kontakt-Informationen",
"companies.email": "Email",
"companies.phone": "Telefon",
......
......@@ -2,6 +2,7 @@
"values": {
"language.de": "German",
"language.en": "English",
"loading": "Loading...",
"AMIV": "AMIV",
"About AMIV": "About AMIV",
"Board": "Board",
......@@ -34,16 +35,14 @@
"withdraw": "withdraw",
"username": "Username",
"password": "Password",
"event.title": "Title",
"event.start_time": "Starting time",
"event.signup_count": "Signup count",
"event.spots": "Spots",
"regular_member": "regular member",
"extraordinary_member": "extraordinary member",
"honorary_member": "honorary member",
"no image": "No image available.",
"no description": "No description available.",
"translation unavailable": "Translation not available. Showing text in %{shown_language}",
"email": "Email",
"email_invalid": "Not a valid email address",
"search": "Search",
"frontpage.whats_hot": "What's HOT right now?",
"frontpage.social_media": "Join us on social media!",
......@@ -95,6 +94,21 @@
"events.price": "Price",
"events.free": "Free",
"events.small_fee": "Small fee",
"events.not_found": "Event not found",
"events.signed_up": "You signed up for this event.",
"events.no_registration": "No registration required",
"events.registration_over": "The registration period is over.",
"events.registrations_starts_at": "The registration starts at %{time}",
"events.%n_spots_available": [
[-1, -1, "Spots available"],
[0, 0, "No spots available"],
[1, 1, "%n spot available"],
[2, null, "%n spots available"]
],
"events.update_data": "Update your data below.",
"events.amiv_members_only": "This event is for AMIV members only.",
"events.signup": "signup",
"events.delete_signup": "delete signup",
"companies.contact_information": "Contact information",
"companies.email": "Email",
"companies.phone": "Phone",
......
......@@ -25,7 +25,7 @@ export class Event {
const queryString = m.buildQueryString({
where: JSON.stringify({
user: getUserId(),
event: this.getSelectedEvent()._id,
event: this._id,
}),
});
......@@ -37,27 +37,28 @@ export class Event {
},
});
if (response._items.length === 1) {
[this.signup] = response._items;
[this._signup] = response._items;
} else {
this.signup = undefined;
this._signup = undefined;
}
this.signupLoaded = true;
return this.signup;
return this._signup;
}
/**
* Checks if the signup data has been loaded.
* @return {Boolean}
*/
hasSignupLoaded() {
get hasSignupDataLoaded() {
return this.signupLoaded;
}
/**
* Get signup data of the authenticated user.
* @return {object} signup data
*/
getSignup() {
return this.signup;
get signupData() {
return this._signup;
}
/**
......@@ -65,17 +66,17 @@ export class Event {
* @return {Promise}
*/
async signoff() {
if (!this.signup) return;
if (!this._signup) return;
await m.request({
method: 'DELETE',
url: `${apiUrl}/eventsignups/${this.signup._id}`,
url: `${apiUrl}/eventsignups/${this._signup._id}`,
headers: {
Authorization: getToken(),
'If-Match': this.signup._etag,
'If-Match': this._signup._etag,
},
});
this.signup = undefined;
this._signup = undefined;
}
/**
......@@ -85,15 +86,15 @@ export class Event {
* @return {Promise}
*/
async signup(additionalFields, email = '') {
let additionalFieldsString = '';
if (this.selectedEvent.additional_fields) {
let additionalFieldsString;
if (this.additional_fields) {
additionalFieldsString = JSON.stringify(additionalFields);
}
if (this.signup) {
this._updateSignup(additionalFieldsString);
if (this._signup) {
return this._updateSignup(additionalFieldsString);
}
this._createSignup(additionalFieldsString, email);
return this._createSignup(additionalFieldsString, email);
}
async _createSignup(additionalFieldsString, email = '') {
......@@ -110,7 +111,7 @@ export class Event {
throw new Error('Signup not allowed');
}
this.signup = await m.request({
this._signup = await m.request({
method: 'POST',
url: `${apiUrl}/eventsignups`,
data,
......@@ -118,20 +119,22 @@ export class Event {
Authorization: getToken(),
},
});
return this._signup;
}
async _updateSignup(additionalFieldsString) {
this.signup = await m.request({
this._signup = await m.request({
method: 'PATCH',
url: `${apiUrl}/eventsignups/${this.signup._id}`,
url: `${apiUrl}/eventsignups/${this._signup._id}`,
data: {
additional_fields: additionalFieldsString,
},
headers: {
Authorization: getToken(),
'If-Match': this.signup._etag,
'If-Match': this._signup._etag,
},
});
return this._signup;
}
}
......@@ -140,28 +143,95 @@ export class EventController {
this.query = query || {};
// state pointer that is counted up every time the table is refreshed so
// we can tell infinite scroll that the data-version has changed.
this.stateCounter = Stream(0);
this._stateCounter = Stream(0);
}
get stateCounter() {
return this._stateCounter();
}
refresh() {
this.stateCounter(this.stateCounter() + 1);
this._stateCounter(this.stateCounter + 1);
}
infiniteScrollParams(item) {
infiniteScrollParams(item, before) {
const date = `${new Date().toISOString().split('.')[0]}Z`;
return {
item,
pageData: pageNum => this.getPageData(pageNum),
pageKey: pageNum => `${pageNum}-${this.stateCounter()}`,
before,
pageData: pageNum =>
this.getPageData(pageNum, {
where: {
time_advertising_end: { $lt: date },
$and: [
{ $or: [{ time_start: null }, { time_start: { $lt: date } }] },
{ $or: [{ time_end: null }, { time_end: { $lt: date } }] },
],
},
}),
pageKey: pageNum => `${pageNum}-${this.stateCounter}`,
};
}
async getPageData(pageNum) {
/**
* Get page data according to saved query
* @param {number} pageNum
*/
async getPageData(pageNum, additionalQuery = {}) {
const date = `${new Date().toISOString().split('.')[0]}Z`;
// for some reason this is called before the object is instantiated.
// check this and return nothing
const query = Object.assign({}, this.query);
query.max_results = 10;
const query = Object.assign({}, this.query, additionalQuery);
query.where = query.where || {};
query.where.show_website = true;
query.where.time_advertising_start = { $lt: date };
query.max_results = query.max_results || 10;
query.page = pageNum;
return EventController._getData(query);
}
/**
* Get all events with their registration open
*/
async getWithOpenRegistration() {
const date = `${new Date().toISOString().split('.')[0]}Z`;
// for some reason this is called before the object is instantiated.
const query = Object.assign({}, this.query);
query.where = query.where || {};
query.where.show_website = true;
query.where.time_register_start = { $lt: date };
query.where.time_register_end = { $gt: date };
return EventController._getData(query);
}
/**
* Get all upcoming events
* @param {Boolean} skipRegistrationOpen skip events which have their registration open
*/
async getUpcoming(skipRegistrationOpen = false) {
const date = `${new Date().toISOString().split('.')[0]}Z`;
// for some reason this is called before the object is instantiated.
const query = Object.assign({}, this.query);
query.where = query.where || {};
query.where.show_website = true;
if (!skipRegistrationOpen) {
query.where.time_start = { $gt: date };
query.where.time_advertising_end = { $gt: date };
} else {
query.where.time_start = { $gt: date };
query.where.time_advertising_end = { $gt: date };
query.where.$or = [
{ time_register_end: { $lt: date } },
{ time_register_start: { $gt: date } },
];
}
query.where.time_advertising_start = { $lt: date };
return EventController._getData(query);
}
static async _getData(query) {
// Parse query such that the backend understands it
const parsedQuery = {};
Object.keys(query).forEach(key => {
......@@ -177,10 +247,12 @@ export class EventController {
},
});
return response._items.map(event => {
const otherLanguage = currentLanguage() === 'en' ? 'de' : 'en';
const newEvent = Object.assign({}, event);
newEvent.title = newEvent[`title_${currentLanguage()}`];
newEvent.description = newEvent[`description_${currentLanguage()}`];
return Event(newEvent);
newEvent.title = newEvent[`title_${currentLanguage()}`] || newEvent[`title_${otherLanguage}`];
newEvent.description =
newEvent[`description_${currentLanguage()}`] || newEvent[`description_${otherLanguage}`];
return new Event(newEvent);
});
}
......@@ -193,7 +265,8 @@ export class EventController {
* Load a specific event
* @param {String} eventId
*/
static async loadEvent(eventId) {
async loadEvent(eventId) {
const otherLanguage = currentLanguage() === 'en' ? 'de' : 'en';
const event = await m.request({
method: 'GET',
url: `${apiUrl}/events/${eventId}`,
......@@ -201,8 +274,20 @@ export class EventController {
Authorization: getToken(),
},
});
event.title = event[`title_${currentLanguage()}`];
event.description = event[`description_${currentLanguage()}`];
return Event(event);
event.title = event[`title_${currentLanguage()}`] || event[`title_${otherLanguage}`];
event.description =
event[`description_${currentLanguage()}`] || event[`description_${otherLanguage}`];
if (!event.show_website) {
throw new Error('Event not found');
}
this._selectedEvent = new Event(event);
return this._selectedEvent;
}
/**
* Get the previously loaded event
*/
get selectedEvent() {
return this._selectedEvent;
}
}
import m from 'mithril';
import marked from 'marked';
import * as EmailValidator from 'email-validator';
import * as events from '../../models/events';
import { log } from '../../models/log';
import { isLoggedIn } from '../../models/auth';
import inputGroup from '../form/inputGroup';
import { Button } from '../../components';
import JSONSchemaForm from '../form/jsonSchemaForm';
import { i18n } from '../../models/language';
class EventSignupForm extends JSONSchemaForm {
oninit(vnode) {
super.oninit(vnode);
this.event = vnode.attrs.event;
super.oninit(
Object.assign({}, vnode, {
attrs: {
schema:
this.event.additional_fields === undefined
? undefined
: JSON.parse(this.event.additional_fields),
},
})
);
this.email = '';
this.emailErrors = [];
this.emailValid = false;
if (isLoggedIn()) {
events.loadSignupForSelectedEvent().then(() => {
if (typeof events.getSignupForSelectedEvent() !== 'undefined') {
this.data = JSON.parse(events.getSignupForSelectedEvent().additional_fields) || {};
this.event.loadSignup().then(() => {
if (this.event.signupData) {
this.data = JSON.parse(this.event.signupData.additional_fields) || {};
}
});
}
}
signup() {
events
.signupForSelectedEvent(super.getValue(), this.email)
.then(() => log('Successfully signed up for the event!'))
.catch(() => log('Could not sign up of the event!'));
async signup() {
try {
await this.event.signup(super.getValue(), this.email);
} catch (err) {
log(err);
}
}
signoff() {
events.signoffForSelectedEvent();
try {
this.event.signoff();
} catch (err) {
log(err);
}
this.validate();
}
view() {
// do not render anything if there is no data yet
if (typeof events.getSelectedEvent() === 'undefined') return m('');
if (isLoggedIn()) {
// do not render form if there is no signup data of the current user
if (!events.signupForSelectedEventHasLoaded()) return m('span', 'Loading...');
if (!this.event.hasSignupDataLoaded) return m('span', i18n('loading'));
const elements = this.renderFormElements();
elements.push(this._renderSignupButton());
if (typeof events.getSignupForSelectedEvent() !== 'undefined') {
elements.unshift(m('div', 'You have already signed up. Update your data below.'));
if (!this.event.signupData || (this.event.signupData && this.event.additional_fields)) {
elements.push(this._renderSignupButton());
}
if (this.event.signupData) {
elements.unshift(
m(
'div',
`${i18n('events.signed_up')} ${
this.event.additional_fields ? i18n('events.update_data') : ''
}`
)
);
elements.push(this._renderSignoffButton());
}
return m('form', elements);
} else if (events.getSelectedEvent().allow_email_signup) {
return m('form', { onsubmit: () => false }, elements);
} else if (this.event.allow_email_signup) {
const elements = this.renderFormElements();
elements.push(this._renderEmailField());
elements.push(this._renderSignupButton());
return m('form', elements);
}
return m('div', 'This event is for AMIV members only.');
return m('div', i18n('events.amiv_members_only'));
}
isValid() {
......@@ -69,7 +90,7 @@ class EventSignupForm extends JSONSchemaForm {
_renderEmailField() {
return m(inputGroup, {
name: 'email',
title: 'Email',
title: i18n('email'),
args: {
type: 'text',
},
......@@ -83,7 +104,7 @@ class EventSignupForm extends JSONSchemaForm {
this.emailErrors = [];
} else {
this.emailValid = false;
this.emailErrors = ['Not a valid email address'];
this.emailErrors = [i18n('email_invalid')];
}
},
getErrors: () => this.emailErrors,
......@@ -94,7 +115,7 @@ class EventSignupForm extends JSONSchemaForm {
_renderSignupButton() {
return m(Button, {
name: 'signup',
label: 'Signup',
label: i18n('events.signup'),
active: super.isValid(),
events: {
onclick: () => this.signup(),
......@@ -105,7 +126,7 @@ class EventSignupForm extends JSONSchemaForm {
_renderSignoffButton() {
return m(Button, {
name: 'signoff',
label: 'Delete signup',
label: i18n('events.delete_signup'),
active: true,
events: {
onclick: () => this.signoff(),
......@@ -115,43 +136,43 @@ class EventSignupForm extends JSONSchemaForm {
}
export default class EventDetails {
static oninit(vnode) {
events.selectEvent(vnode.attrs.eventId);
oninit(vnode) {
this.controller = vnode.attrs.controller;
}
static view() {
if (typeof events.getSelectedEvent() === 'undefined') {
return m('');
view() {
const event = this.controller.selectedEvent;
if (!event) {
return m('h1', i18n('events.not_found'));
}
let eventSignupForm;
const now = new Date();
const registerStart = new Date(events.getSelectedEvent().time_register_start);
const registerEnd = new Date(events.getSelectedEvent().time_register_end);
const registerStart = new Date(event.time_register_start);
const registerEnd = new Date(event.time_register_end);
if (registerStart <= now) {
if (registerEnd >= now) {
eventSignupForm = m(EventSignupForm, {
schema:
events.getSelectedEvent().additional_fields === undefined
? undefined
: JSON.parse(events.getSelectedEvent().additional_fields),
});
eventSignupForm = m(EventSignupForm, { event });
} else {
let participantNotice = '';
if (events.getSignupForSelectedEvent() !== 'undefined') {
participantNotice = m('span', 'You signed up for this event.');
if (event.hasSignupDataLoaded && event.signupData) {
participantNotice = m('span', i18n('events.signed_up'));
}
eventSignupForm = m('div', ['The registration period is over.', participantNotice]);
eventSignupForm = m('div', [i18n('events.registration_over'), participantNotice]);
}
} else {
eventSignupForm = m('div', `The registration starts at ${registerStart}`);
eventSignupForm = m('div', i18n('events.registration_starts_at', { time: registerStart }));
}
return m('div', [
m('h1', events.getSelectedEvent().title_de),
m('span', events.getSelectedEvent().time_start),
m('span', events.getSelectedEvent().signup_count),
m('span', events.getSelectedEvent().spots),
m('p', m.trust(marked(events.getSelectedEvent().description_de))),
return m('div.event-details', [
m('h1', event.title),
m('div', event.time_start),
m(
'div',
event.spots === undefined
? i18n('events.no_registration')
: i18n('events.%n_spots_available', event.spots - event.signup_count)
),
m('p', m.trust(marked(event.description))),
eventSignupForm,
]);
}
......
import m from 'mithril';
import infinite from 'mithril-infinite';
import { i18n, currentLanguage } from '../../models/language';
import * as events from '../../models/events';
import { EventController } from '../../models/events';
import { FilterView } from '../../components';
import { log } from '../../models/log';
import EventDetails from './eventDetails';