Commit 76ee384b authored by Sandro Lutz's avatar Sandro Lutz
Browse files

Add PaginationController

parent 0b2f184b
......@@ -61,14 +61,18 @@ import { Button, Checkbox, Dropdown, TextField } from '../components';
*
* Default behavior of buttons is to trigger `onchange`.
*/
export default class FilterViewComponent {
constructor() {
this.values = {};
}
export default class FilterViewComponent {
oninit(vnode) {
this.onchange = vnode.attrs.onchange;
this.notify();
if (vnode.attrs.values) {
this.values = vnode.attrs.values;
} else {
this.values = {};
vnode.attrs.fields.forEach(field => {
this.values[field.key] = field.default || '';
});
}
}
notify() {
......
......@@ -95,10 +95,11 @@
"events.free": "Gratis",
"events.small_fee": "Kleine Teilnahmegebühr",
"events.not_found": "Event nichr gefunden",
"events.no_selection": "Kein Event ausgewählt",
"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.registration_over": "Das Anmeldefenster ist geschlossen.",
"events.registration_starts_at": "Das Anmeldefenster öffnet am %{time}",
"events.%n_spots_available": [
[-1, -1, "Plätze verfügbar"],
[0, 0, "Keine Plätze verfügbar"],
......@@ -109,6 +110,8 @@
"events.amiv_members_only": "Dieser Event ist nur für AMIV Mitglieder.",
"events.signup": "anmelden",
"events.delete_signup": "Anmeldung löschen",
"events.loading": "Laden...",
"events.load_more": "Mehr Events laden",
"companies.contact_information": "Kontakt-Informationen",
"companies.email": "Email",
"companies.phone": "Telefon",
......
......@@ -95,10 +95,11 @@
"events.free": "Free",
"events.small_fee": "Small fee",
"events.not_found": "Event not found",
"events.no_selection": "No event selected",
"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.registration_starts_at": "The registration starts at %{time}",
"events.%n_spots_available": [
[-1, -1, "Spots available"],
[0, 0, "No spots available"],
......@@ -109,6 +110,8 @@
"events.amiv_members_only": "This event is for AMIV members only.",
"events.signup": "signup",
"events.delete_signup": "delete signup",
"events.loading": "Loading...",
"events.load_more": "Load more events",
"companies.contact_information": "Contact information",
"companies.email": "Email",
"companies.phone": "Phone",
......
import m from 'mithril';
import Stream from 'mithril/stream';
import { apiUrl } from 'config';
import { getToken, getUserId, isLoggedIn } from './auth';
import { currentLanguage } from './language';
import PaginationController from './pagination';
/**
* Event class
*/
export class Event {
/**
* Constructor
*
* @param {object} event object loaded from the API
*/
constructor(event) {
// Expose all properties of `event`
Object.keys(event).forEach(key => {
......@@ -17,6 +22,7 @@ export class Event {
/**
* Load the signup data of the authenticated user.
*
* @return {Promise}
*/
async loadSignup() {
......@@ -47,6 +53,7 @@ export class Event {
/**
* Checks if the signup data has been loaded.
*
* @return {Boolean}
*/
get hasSignupDataLoaded() {
......@@ -55,6 +62,7 @@ export class Event {
/**
* Get signup data of the authenticated user.
*
* @return {object} signup data
*/
get signupData() {
......@@ -63,6 +71,7 @@ export class Event {
/**
* Sign off the authenticated user from this event.
*
* @return {Promise}
*/
async signoff() {
......@@ -81,6 +90,7 @@ export class Event {
/**
* Sign up the authenticated user for this event.
*
* @param {*} additionalFields
* @param {string} email email address (required if not logged in!)
* @return {Promise}
......@@ -138,125 +148,131 @@ export class Event {
}
}
export class EventController {
constructor(query = {}) {
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);
/**
* EventListController class (inherited from `PaginationController`)
*
* Used to handle a list of a specific type of event (e.g. all past events)
*/
export class EventListController extends PaginationController {
constructor(query = {}, additionalQuery = {}) {
super('events', query, additionalQuery);
}
get stateCounter() {
return this._stateCounter();
async _loadData(query) {
const items = await super._loadData(query);
return items.map(event => {
const otherLanguage = currentLanguage() === 'en' ? 'de' : 'en';
const newEvent = Object.assign({}, event);
newEvent.title = newEvent[`title_${currentLanguage()}`] || newEvent[`title_${otherLanguage}`];
newEvent.description =
newEvent[`description_${currentLanguage()}`] || newEvent[`description_${otherLanguage}`];
return new Event(newEvent);
});
}
}
refresh() {
this._stateCounter(this.stateCounter + 1);
}
/**
* EventController class
*
* Managing multiple type of event lists and handling of the currently selected event.
*/
export class EventController {
/**
* Constructor
*
* @param {object} query initial query
* @param {boolean} upcomingSkipRegistrationOpen if `true`, skip all events with open registration in upcoming event list
*/
constructor(query = {}, upcomingSkipRegistrationOpen = false) {
this.query = query;
this._pastEvents = new EventListController(query, () => {
const date = `${new Date().toISOString().split('.')[0]}Z`;
return {
where: {
time_advertising_end: { $lt: date },
$and: [
{ $or: [{ time_start: null }, { time_start: { $lt: date } }] },
{ $or: [{ time_end: null }, { time_end: { $lt: date } }] },
],
},
};
});
infiniteScrollParams(item, before) {
const date = `${new Date().toISOString().split('.')[0]}Z`;
return {
item,
before,
pageData: pageNum =>
this.getPageData(pageNum, {
let upcomingAdditionalQuery;
if (upcomingSkipRegistrationOpen) {
upcomingAdditionalQuery = () => {
const date = `${new Date().toISOString().split('.')[0]}Z`;
return {
where: {
time_advertising_end: { $lt: date },
$and: [
{ $or: [{ time_start: null }, { time_start: { $lt: date } }] },
{ $or: [{ time_end: null }, { time_end: { $lt: date } }] },
],
show_website: true,
time_start: { $gt: date },
time_advertising_start: { $lt: date },
$or: [{ time_register_end: { $lt: date } }, { time_register_start: { $gt: date } }],
},
}),
pageKey: pageNum => `${pageNum}-${this.stateCounter}`,
};
}
/**
* 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.
const query = JSON.parse(JSON.stringify(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;
};
};
} else {
upcomingAdditionalQuery = () => {
const date = `${new Date().toISOString().split('.')[0]}Z`;
return {
where: {
show_website: true,
time_start: { $gt: date },
time_advertising_start: { $lt: date },
},
};
};
}
this._upcomingEvents = new EventListController(query, upcomingAdditionalQuery);
return EventController._getData(query);
this._openRegistrationEvents = new EventListController(query, () => {
const date = `${new Date().toISOString().split('.')[0]}Z`;
return {
where: {
show_website: true,
time_register_start: { $lt: date },
time_register_end: { $gt: date },
},
};
});
}
/**
* 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 = JSON.parse(JSON.stringify(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 };
/** Set a new query used by all EventListController to load events */
async setQuery(query) {
const newQuery = JSON.stringify(query || {});
const oldQuery = JSON.stringify(this.query);
return EventController._getData(query);
}
if (newQuery === oldQuery) return false;
/**
* 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 = JSON.parse(JSON.stringify(this.query || {}));
query.where = query.where || {};
query.where.show_website = true;
if (!skipRegistrationOpen) {
query.where.time_start = { $gt: date };
} else {
query.where.time_start = { $gt: date };
query.where.$or = [
{ time_register_end: { $lt: date } },
{ time_register_start: { $gt: date } },
];
}
query.where.time_advertising_start = { $lt: date };
this.query = JSON.parse(newQuery);
this.openRegistrationEvents.setQuery(this.query);
this.upcomingEvents.setQuery(this.query);
this.pastEvents.setQuery(this.query);
await this.refresh();
return true;
}
return EventController._getData(query);
/** Refresh all event data */
async refresh() {
await this.openRegistrationEvents.loadAll();
await this.upcomingEvents.loadAll();
await this.pastEvents.loadPageData(1);
}
static async _getData(query) {
// Parse query such that the backend understands it
const parsedQuery = {};
Object.keys(query).forEach(key => {
parsedQuery[key] = key === 'sort' ? query[key] : JSON.stringify(query[key]);
});
const queryString = m.buildQueryString(parsedQuery);
/** Get EventListController for all events with open registration window */
get openRegistrationEvents() {
return this._openRegistrationEvents;
}
const response = await m.request({
method: 'GET',
url: `${apiUrl}/events?${queryString}`,
headers: {
Authorization: getToken(),
},
});
return response._items.map(event => {
const otherLanguage = currentLanguage() === 'en' ? 'de' : 'en';
const newEvent = Object.assign({}, event);
newEvent.title = newEvent[`title_${currentLanguage()}`] || newEvent[`title_${otherLanguage}`];
newEvent.description =
newEvent[`description_${currentLanguage()}`] || newEvent[`description_${otherLanguage}`];
return new Event(newEvent);
});
/** Get EventListController for all upcoming events */
get upcomingEvents() {
return this._upcomingEvents;
}
setQuery(query) {
this.query = JSON.parse(JSON.stringify(query || {}));
this.refresh();
/** Get EventListController for all past events */
get pastEvents() {
return this._pastEvents;
}
/**
......@@ -272,12 +288,12 @@ export class EventController {
Authorization: getToken(),
},
});
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');
}
event.title = event[`title_${currentLanguage()}`] || event[`title_${otherLanguage}`];
event.description =
event[`description_${currentLanguage()}`] || event[`description_${otherLanguage}`];
this._selectedEvent = new Event(event);
return this._selectedEvent;
}
......
import m from 'mithril';
import { apiUrl } from 'config';
import { getToken } from './auth';
/**
* PaginationController class
*
* This is a generic class for different API resources.
*/
export default class PaginationController {
/**
* Constructor
*
* @param {*} resource resource name (e.g. `events`)
* @param {*} query initial query
* @param {*} additionalQuery additional query to be added to every query
* @public
*/
constructor(resource, query = {}, additionalQuery = {}) {
this.resource = resource;
this.query = query;
this.additionalQuery = additionalQuery;
this._lastLoadedPage = 0;
this._totalPages = 1;
}
/**
* Get the total number of pages available to load
*
* @return {int}
* @public
*/
get totalPages() {
return this._totalPages;
}
/**
* Get last loaded page number
*
* @return {int}
* @public
*/
get lastLoadedPage() {
return this._lastLoadedPage;
}
/**
* Set a new query to load the configured resource
*
* @public
*/
setQuery(query) {
this.query = JSON.parse(JSON.stringify(query || {}));
this._pages = [];
this._lastLoadedPage = 0;
this._totalPages = 1;
}
/**
* Load all pages of this resource query
*
* @return {Promise}
* @public
*/
async loadAll() {
let currentPage = 1;
const promiseList = [];
while (currentPage <= this._totalPages) {
promiseList.push(this.loadPageData(currentPage));
currentPage += 1;
}
await Promise.all(promiseList);
}
/**
* Map over all loaded pages (missing pages are skipped!)
*
* @param {function} callback function called with every loaded page
* @public
*/
map(callback) {
return this._pages.map(page => callback(page.items));
}
/**
* Get data of a specific page
*
* @param {int} pageNum page number to load the data from
* @return {Promise}
* @public
*/
async getPageData(pageNum) {
if (
this._pages[pageNum] &&
new Date() < new Date(this._pages[pageNum].datetime.getTime() + 60000)
) {
return this._pages[pageNum].items;
}
await this.loadPageData(pageNum);
return this._pages[pageNum].items;
}
/**
* Load data of a specific page (This function does not return any data!)
*
* @param {int} pageNum page number to load the data from
* @return {Promise}
* @public
*/
async loadPageData(pageNum) {
let additionalQuery;
if (typeof this.additionalQuery === 'function') {
additionalQuery = this.additionalQuery();
} else {
({ additionalQuery } = this);
}
const date = `${new Date().toISOString().split('.')[0]}Z`;
const query = JSON.parse(
JSON.stringify(
Object.assign({}, this.query, additionalQuery, {
where: Object.assign({}, this.query.where, additionalQuery.where),
})
)
);
if (
this.query.where &&
this.query.where.$or &&
additionalQuery.where &&
additionalQuery.where.$or
) {
query.where.$and = query.where.$and || [];
query.where.$and.push(
JSON.parse(JSON.stringify({ $or: this.query.where.$or })),
JSON.parse(JSON.stringify({ $or: additionalQuery.where.$or }))
);
delete query.where.$or;
}
if (this.query.where && this.query.where.$and) {
query.where.$and = query.where.$and.concat(JSON.parse(JSON.stringify(this.query.where.$and)));
}
if (additionalQuery.where && additionalQuery.where.$and) {
query.where.$and = query.where.$and.concat(
JSON.parse(JSON.stringify(additionalQuery.where.$and))
);
}
query.where.show_website = true;
query.where.time_advertising_start = { $lt: date };
query.max_results = query.max_results || 10;
query.page = pageNum;
const data = await this._loadData(query);
this._pages[pageNum] = { datetime: new Date(), items: data };
if (this._lastLoadedPage < pageNum) {
this._lastLoadedPage = pageNum;
}
}
/**
* Load data with the given query from the API
*
* @param {object} query
* @private
*/
async _loadData(query) {
// Parse query such that the backend understands it
const parsedQuery = {};
Object.keys(query).forEach(key => {
parsedQuery[key] = key === 'sort' ? query[key] : JSON.stringify(query[key]);
});
const queryString = m.buildQueryString(parsedQuery);
const response = await m.request({
method: 'GET',
url: `${apiUrl}/${this.resource}?${queryString}`,
headers: {
Authorization: getToken(),
},
});
this._totalPages = Math.ceil(response._meta.total / response._meta.max_results);
return response._items;
}
}
......@@ -27,7 +27,7 @@ class EventSignupForm extends JSONSchemaForm {
if (isLoggedIn()) {
this.event.loadSignup().then(() => {
if (this.event.signupData) {
this.data = JSON.parse(this.event.signupData.additional_fields) || {};
this.data = JSON.parse(this.event.signupData.additional_fields || '{}');
}
});
}
......
import m from 'mithril';
import infinite from 'mithril-infinite';
import { i18n, currentLanguage } from '../../models/language';
import { EventController } from '../../models/events';
import { FilterView } from '../../components';
import { log } from '../../models/log';
import EventDetails from './eventDetails';
const controller = new EventController({}, true);
const stickyPositionTop = { filterView: 0, detailsView: 0 };
let lastScrollPosition = 0;
let filterValues;
let listState = 'loading';
let loadMoreState = 'idle';
let eventLoaded = false;
function renderEventListItem(event, className = '') {
return m(
'div',
......@@ -19,153 +26,223 @@ function renderEventListItem(event, className = '') {
);
}
/**
* EventPromotionList
*
* Used to show upcoming events and events with open registration
*/
class EventPromotionList {