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'; ...@@ -8,7 +8,6 @@ import { login, isLoggedIn, checkLogin } from './models/auth';
import studydocList from './views/studydocs/studydocList'; import studydocList from './views/studydocs/studydocList';
import studydocNew from './views/studydocs/studydocNew'; import studydocNew from './views/studydocs/studydocNew';
import eventList from './views/events/eventList'; import eventList from './views/events/eventList';
import eventDetails from './views/events/eventDetails';
import profile from './views/profile'; import profile from './views/profile';
import layout from './views/layout'; import layout from './views/layout';
import frontpage from './views/frontpage'; import frontpage from './views/frontpage';
...@@ -93,7 +92,7 @@ Raven.context(() => { ...@@ -93,7 +92,7 @@ Raven.context(() => {
}, },
{ {
url: '/:language/events/:eventId', url: '/:language/events/:eventId',
view: vnode => m(eventDetails, vnode.attrs), view: vnode => m(eventList, vnode.attrs),
}, },
{ {
url: '/:language/jobs', url: '/:language/jobs',
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
"values": { "values": {
"language.de": "Deutsch", "language.de": "Deutsch",
"language.en": "Englisch", "language.en": "Englisch",
"loading": "Laden...",
"AMIV": "AMIV", "AMIV": "AMIV",
"About AMIV": "Über den AMIV", "About AMIV": "Über den AMIV",
"Board": "Vorstand", "Board": "Vorstand",
...@@ -34,16 +35,14 @@ ...@@ -34,16 +35,14 @@
"withdraw": "austragen", "withdraw": "austragen",
"username": "Benutzername", "username": "Benutzername",
"password": "Passwort", "password": "Passwort",
"event.title": "TItel",
"event.start_time": "Start Zeit",
"event.signup_count": "Anmeldungen",
"event.spots": "Freie Plätze",
"regular_member": "Ordentliches Mitglied", "regular_member": "Ordentliches Mitglied",
"extraordinary_member": "Ausserordentliches Mitglied", "extraordinary_member": "Ausserordentliches Mitglied",
"honorary_member": "Ehrenmitglied", "honorary_member": "Ehrenmitglied",
"no image": "Kein Bild verfügbar.", "no image": "Kein Bild verfügbar.",
"no description": "Keine Beschreibung verfügbar.", "no description": "Keine Beschreibung verfügbar.",
"translation unavailable": "Übersetzung nicht verfügbar. Zeige text in %{shown_language}", "translation unavailable": "Übersetzung nicht verfügbar. Zeige text in %{shown_language}",
"email": "Email",
"email_invalid": "Ungültige Email-Adresse",
"search": "Suchen", "search": "Suchen",
"frontpage.whats_hot": "Was ist brandaktuell?", "frontpage.whats_hot": "Was ist brandaktuell?",
"frontpage.social_media": "Folge uns auf Social Media", "frontpage.social_media": "Folge uns auf Social Media",
...@@ -95,6 +94,21 @@ ...@@ -95,6 +94,21 @@
"events.price": "Preis", "events.price": "Preis",
"events.free": "Gratis", "events.free": "Gratis",
"events.small_fee": "Kleine Teilnahmegebühr", "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.contact_information": "Kontakt-Informationen",
"companies.email": "Email", "companies.email": "Email",
"companies.phone": "Telefon", "companies.phone": "Telefon",
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
"values": { "values": {
"language.de": "German", "language.de": "German",
"language.en": "English", "language.en": "English",
"loading": "Loading...",
"AMIV": "AMIV", "AMIV": "AMIV",
"About AMIV": "About AMIV", "About AMIV": "About AMIV",
"Board": "Board", "Board": "Board",
...@@ -34,16 +35,14 @@ ...@@ -34,16 +35,14 @@
"withdraw": "withdraw", "withdraw": "withdraw",
"username": "Username", "username": "Username",
"password": "Password", "password": "Password",
"event.title": "Title",
"event.start_time": "Starting time",
"event.signup_count": "Signup count",
"event.spots": "Spots",
"regular_member": "regular member", "regular_member": "regular member",
"extraordinary_member": "extraordinary member", "extraordinary_member": "extraordinary member",
"honorary_member": "honorary member", "honorary_member": "honorary member",
"no image": "No image available.", "no image": "No image available.",
"no description": "No description available.", "no description": "No description available.",
"translation unavailable": "Translation not available. Showing text in %{shown_language}", "translation unavailable": "Translation not available. Showing text in %{shown_language}",
"email": "Email",
"email_invalid": "Not a valid email address",
"search": "Search", "search": "Search",
"frontpage.whats_hot": "What's HOT right now?", "frontpage.whats_hot": "What's HOT right now?",
"frontpage.social_media": "Join us on social media!", "frontpage.social_media": "Join us on social media!",
...@@ -95,6 +94,21 @@ ...@@ -95,6 +94,21 @@
"events.price": "Price", "events.price": "Price",
"events.free": "Free", "events.free": "Free",
"events.small_fee": "Small fee", "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.contact_information": "Contact information",
"companies.email": "Email", "companies.email": "Email",
"companies.phone": "Phone", "companies.phone": "Phone",
......
...@@ -25,7 +25,7 @@ export class Event { ...@@ -25,7 +25,7 @@ export class Event {
const queryString = m.buildQueryString({ const queryString = m.buildQueryString({
where: JSON.stringify({ where: JSON.stringify({
user: getUserId(), user: getUserId(),
event: this.getSelectedEvent()._id, event: this._id,
}), }),
}); });
...@@ -37,27 +37,28 @@ export class Event { ...@@ -37,27 +37,28 @@ export class Event {
}, },
}); });
if (response._items.length === 1) { if (response._items.length === 1) {
[this.signup] = response._items; [this._signup] = response._items;
} else { } else {
this.signup = undefined; this._signup = undefined;
} }
this.signupLoaded = true; this.signupLoaded = true;
return this.signup; return this._signup;
} }
/** /**
* Checks if the signup data has been loaded. * Checks if the signup data has been loaded.
* @return {Boolean} * @return {Boolean}
*/ */
hasSignupLoaded() { get hasSignupDataLoaded() {
return this.signupLoaded; return this.signupLoaded;
} }
/** /**
* Get signup data of the authenticated user. * Get signup data of the authenticated user.
* @return {object} signup data
*/ */
getSignup() { get signupData() {
return this.signup; return this._signup;
} }
/** /**
...@@ -65,17 +66,17 @@ export class Event { ...@@ -65,17 +66,17 @@ export class Event {
* @return {Promise} * @return {Promise}
*/ */
async signoff() { async signoff() {
if (!this.signup) return; if (!this._signup) return;
await m.request({ await m.request({
method: 'DELETE', method: 'DELETE',
url: `${apiUrl}/eventsignups/${this.signup._id}`, url: `${apiUrl}/eventsignups/${this._signup._id}`,
headers: { headers: {
Authorization: getToken(), 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 { ...@@ -85,15 +86,15 @@ export class Event {
* @return {Promise} * @return {Promise}
*/ */
async signup(additionalFields, email = '') { async signup(additionalFields, email = '') {
let additionalFieldsString = ''; let additionalFieldsString;
if (this.selectedEvent.additional_fields) { if (this.additional_fields) {
additionalFieldsString = JSON.stringify(additionalFields); additionalFieldsString = JSON.stringify(additionalFields);
} }
if (this.signup) { if (this._signup) {
this._updateSignup(additionalFieldsString); return this._updateSignup(additionalFieldsString);
} }
this._createSignup(additionalFieldsString, email); return this._createSignup(additionalFieldsString, email);
} }
async _createSignup(additionalFieldsString, email = '') { async _createSignup(additionalFieldsString, email = '') {
...@@ -110,7 +111,7 @@ export class Event { ...@@ -110,7 +111,7 @@ export class Event {
throw new Error('Signup not allowed'); throw new Error('Signup not allowed');
} }
this.signup = await m.request({ this._signup = await m.request({
method: 'POST', method: 'POST',
url: `${apiUrl}/eventsignups`, url: `${apiUrl}/eventsignups`,
data, data,
...@@ -118,20 +119,22 @@ export class Event { ...@@ -118,20 +119,22 @@ export class Event {
Authorization: getToken(), Authorization: getToken(),
}, },
}); });
return this._signup;
} }
async _updateSignup(additionalFieldsString) { async _updateSignup(additionalFieldsString) {
this.signup = await m.request({ this._signup = await m.request({
method: 'PATCH', method: 'PATCH',
url: `${apiUrl}/eventsignups/${this.signup._id}`, url: `${apiUrl}/eventsignups/${this._signup._id}`,
data: { data: {
additional_fields: additionalFieldsString, additional_fields: additionalFieldsString,
}, },
headers: { headers: {
Authorization: getToken(), Authorization: getToken(),
'If-Match': this.signup._etag, 'If-Match': this._signup._etag,
}, },
}); });
return this._signup;
} }
} }
...@@ -140,28 +143,95 @@ export class EventController { ...@@ -140,28 +143,95 @@ export class EventController {
this.query = query || {}; this.query = query || {};
// state pointer that is counted up every time the table is refreshed so // state pointer that is counted up every time the table is refreshed so
// we can tell infinite scroll that the data-version has changed. // 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() { 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 { return {
item, item,
pageData: pageNum => this.getPageData(pageNum), before,
pageKey: pageNum => `${pageNum}-${this.stateCounter()}`, 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. // for some reason this is called before the object is instantiated.
// check this and return nothing const query = Object.assign({}, this.query, additionalQuery);
const query = Object.assign({}, this.query); query.where = query.where || {};
query.max_results = 10; query.where.show_website = true;
query.where.time_advertising_start = { $lt: date };
query.max_results = query.max_results || 10;
query.page = pageNum; 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 // Parse query such that the backend understands it
const parsedQuery = {}; const parsedQuery = {};
Object.keys(query).forEach(key => { Object.keys(query).forEach(key => {
...@@ -177,10 +247,12 @@ export class EventController { ...@@ -177,10 +247,12 @@ export class EventController {
}, },
}); });
return response._items.map(event => { return response._items.map(event => {
const otherLanguage = currentLanguage() === 'en' ? 'de' : 'en';
const newEvent = Object.assign({}, event); const newEvent = Object.assign({}, event);
newEvent.title = newEvent[`title_${currentLanguage()}`]; newEvent.title = newEvent[`title_${currentLanguage()}`] || newEvent[`title_${otherLanguage}`];
newEvent.description = newEvent[`description_${currentLanguage()}`]; newEvent.description =
return Event(newEvent); newEvent[`description_${currentLanguage()}`] || newEvent[`description_${otherLanguage}`];
return new Event(newEvent);
}); });
} }
...@@ -193,7 +265,8 @@ export class EventController { ...@@ -193,7 +265,8 @@ export class EventController {
* Load a specific event * Load a specific event
* @param {String} eventId * @param {String} eventId
*/ */
static async loadEvent(eventId) { async loadEvent(eventId) {
const otherLanguage = currentLanguage() === 'en' ? 'de' : 'en';
const event = await m.request({ const event = await m.request({
method: 'GET', method: 'GET',
url: `${apiUrl}/events/${eventId}`, url: `${apiUrl}/events/${eventId}`,
...@@ -201,8 +274,20 @@ export class EventController { ...@@ -201,8 +274,20 @@ export class EventController {
Authorization: getToken(), Authorization: getToken(),
}, },
}); });
event.title = event[`title_${currentLanguage()}`]; event.title = event[`title_${currentLanguage()}`] || event[`title_${otherLanguage}`];
event.description = event[`description_${currentLanguage()}`]; event.description =
return Event(event); 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 m from 'mithril';
import marked from 'marked'; import marked from 'marked';
import * as EmailValidator from 'email-validator'; import * as EmailValidator from 'email-validator';
import * as events from '../../models/events';
import { log } from '../../models/log'; import { log } from '../../models/log';
import { isLoggedIn } from '../../models/auth'; import { isLoggedIn } from '../../models/auth';
import inputGroup from '../form/inputGroup'; import inputGroup from '../form/inputGroup';
import { Button } from '../../components'; import { Button } from '../../components';
import JSONSchemaForm from '../form/jsonSchemaForm'; import JSONSchemaForm from '../form/jsonSchemaForm';
import { i18n } from '../../models/language';
class EventSignupForm extends JSONSchemaForm { class EventSignupForm extends JSONSchemaForm {
oninit(vnode) { 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.email = '';
this.emailErrors = []; this.emailErrors = [];
this.emailValid = false; this.emailValid = false;
if (isLoggedIn()) { if (isLoggedIn()) {
events.loadSignupForSelectedEvent().then(() => { this.event.loadSignup().then(() => {
if (typeof events.getSignupForSelectedEvent() !== 'undefined') { if (this.event.signupData) {
this.data = JSON.parse(events.getSignupForSelectedEvent().additional_fields) || {}; this.data = JSON.parse(this.event.signupData.additional_fields) || {};
} }
}); });
} }
} }
signup() { async signup() {
events try {
.signupForSelectedEvent(super.getValue(), this.email) await this.event.signup(super.getValue(), this.email);
.then(() => log('Successfully signed up for the event!')) } catch (err) {
.catch(() => log('Could not sign up of the event!')); log(err);
}
} }
signoff() { signoff() {
events.signoffForSelectedEvent(); try {
this.event.signoff();
} catch (err) {
log(err);
}
this.validate(); this.validate();
} }
view() { view() {
// do not render anything if there is no data yet
if (typeof events.getSelectedEvent() === 'undefined') return m('');
if (isLoggedIn()) { if (isLoggedIn()) {
// do not render form if there is no signup data of the current user // 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'));