@digital-blueprint/lunchlottery-app
Version:
[GitHub Repository](https://github.com/digital-blueprint/lunchlottery-app) | [npmjs package](https://www.npmjs.com/package/@digital-blueprint/lunchlottery-app) | [Unpkg CDN](https://unpkg.com/browse/@digital-blueprint/lunchlottery-app/)
630 lines (565 loc) • 23.7 kB
JavaScript
import {css, html} from 'lit';
import {ScopedElementsMixin} from '@dbp-toolkit/common';
import * as commonUtils from '@dbp-toolkit/common/utils';
import {Icon, Button} from '@dbp-toolkit/common';
import * as commonStyles from '@dbp-toolkit/common/styles';
import {FORM_IDENTIFIER} from './constants.js';
import DBPLunchlotteryLitElement from './dbp-lunchlottery-lit-element';
import {classMap} from 'lit/directives/class-map.js';
import {replacePlaceholders} from './utils.js';
const FORM_AVAILABILITY_INIT = 'i';
const FORM_AVAILABILITY_AVAILABLE = 'a';
const FORM_AVAILABILITY_UNAVAILABLE = 'u';
class LunchLotteryRegister extends ScopedElementsMixin(DBPLunchlotteryLitElement) {
constructor() {
super();
this.entryPointUrl = null;
this.identifier = null;
this.firstName = null;
this.lastName = null;
this.dates = null;
this.email = null;
this.organizations = [];
this.organizationIds = [];
this.preferredLanguage = null;
this.formAvailability = FORM_AVAILABILITY_INIT;
this.possibleDates = [];
this.possibleDatesContainer = null;
this.loadingPerson = false;
this.loadingOrganizations = false;
this.loadingForm = false;
this.isPostingSubmission = false;
this.wasSubmissionSuccessful = false;
this.disableForm = false;
}
static get scopedElements() {
return {
'dbp-icon': Icon,
'dbp-button': Button,
};
}
static get properties() {
return {
...super.properties,
entryPointUrl: {type: String, attribute: 'entry-point-url'},
identifier: {type: String, attribute: false},
firstName: {type: String, attribute: false},
lastName: {type: String, attribute: false},
dates: {type: Array, attribute: false},
email: {type: String, attribute: false},
organizationIds: {type: Array, attribute: false},
organizations: {type: Array, attribute: false},
formAvailability: {type: String, attribute: false},
language: {type: String, attribute: false},
container: {type: Object},
possibleDates: {type: Array, attribute: false},
possibleDatesContainer: {type: Object},
loadingPerson: {type: Boolean, attribute: false},
loadingOrganizations: {type: Boolean, attribute: false},
loadingForm: {type: Boolean, attribute: false},
isPostingSubmission: {type: Boolean, attribute: false},
wasSubmissionSuccessful: {type: Boolean, attribute: false},
disableForm: {type: Boolean, attribute: false},
};
}
initialize() {
super.initialize();
this.fetchPerson();
this.fetchForm();
}
update(changedProperties) {
super.update(changedProperties);
changedProperties.forEach((oldValue, propName) => {
switch (propName) {
case 'lang':
this.fetchOrganizations();
break;
}
});
}
isLoadingData() {
return this.loadingPerson || this.loadingOrganizations || this.loadingForm;
}
async fetchPerson() {
try {
this.loadingPerson = true;
const response = await fetch(
this.entryPointUrl +
'/base/people/' +
encodeURIComponent(this.auth['user-id']) +
'?includeLocal=email,staffAt',
{
headers: {
'Content-Type': 'application/ld+json',
Authorization: 'Bearer ' + this.auth.token,
},
},
);
if (!response.ok) {
this.disableForm = true;
this.handleErrorResponse(response);
} else {
const data = await response.json();
this.identifier = data.identifier;
this.firstName = data.givenName;
this.lastName = data.familyName;
this.email = data.localData.email ?? '';
this.organizationIds = data.localData.staffAt ?? [];
await this.fetchOrganizations();
}
} finally {
this.loadingPerson = false;
}
}
/**
* Get the names for the organization IDs
*/
async fetchOrganizations() {
try {
this.loadingOrganizations = true;
this.organizations = [];
let organizations = [];
for (let orgId of this.organizationIds) {
let response = await fetch(
this.entryPointUrl +
'/base/organizations/' +
encodeURIComponent(orgId) +
'?includeLocal=code',
{
headers: {
'Content-Type': 'application/ld+json',
Authorization: 'Bearer ' + this.auth.token,
'Accept-Language': this.lang,
},
},
);
if (!response.ok) {
if (response.status === 404) {
organizations.push({
code: orgId,
name: this._i18n.t('unknown-org', {id: orgId}),
});
} else {
this.disableForm = true;
this.handleErrorResponse(response);
}
} else {
const data = await response.json();
organizations.push({
code: data.localData.code ?? orgId,
name: data.name,
});
}
}
this.organizations = organizations;
} finally {
this.loadingOrganizations = false;
}
}
async fetchForm() {
try {
this.loadingForm = true;
this.possibleDates = [];
this.formAvailability = FORM_AVAILABILITY_INIT;
const response = await fetch(
this.entryPointUrl + '/formalize/forms/' + encodeURIComponent(FORM_IDENTIFIER),
{
headers: {
'Content-Type': 'application/ld+json',
Authorization: 'Bearer ' + this.auth.token,
},
},
);
if (!response.ok) {
this.disableForm = true;
this.handleErrorResponse(response);
} else {
const formData = await response.json();
const decodedDataFeedSchema = JSON.parse(formData['dataFeedSchema']);
this.possibleDates =
decodedDataFeedSchema['properties']['possibleDates']['items']['enum'];
console.log('poss dates ', decodedDataFeedSchema['properties']['possibleDates']);
const start = new Date(formData['availabilityStarts']);
const end = new Date(formData['availabilityEnds']);
const current = new Date();
this.formAvailability =
start.getTime() < current.getTime() && end.getTime() > current.getTime()
? FORM_AVAILABILITY_AVAILABLE
: FORM_AVAILABILITY_UNAVAILABLE;
if (this.possibleDates && this.possibleDates.length != 0)
this.createPossibleDatesContainer();
}
} finally {
this.loadingForm = false;
}
}
createPossibleDatesContainer() {
const i18n = this._i18n;
let possibleDatesContainer = document.createElement('div');
console.log('poss dates ', this.possibleDates);
if (!this.possibleDates) return;
this.possibleDates.forEach((date_string) => {
console.log('date string', date_string);
const date = new Date(date_string);
let box = document.createElement('input');
//create checkbox
box.type = 'checkbox';
box.value = date_string;
//create checkbox label
let label = document.createElement('label');
const formatter = new Intl.DateTimeFormat(i18n.language, {
weekday: 'long',
day: 'numeric',
month: 'long',
hour: '2-digit',
minute: '2-digit',
});
let complete_date = formatter.format(date);
//append data to DOM
label.appendChild(document.createTextNode(complete_date));
let option = document.createElement('div');
box.classList.add('date');
option.appendChild(label);
label.prepend(box);
possibleDatesContainer.appendChild(option);
});
this.possibleDatesContainer = possibleDatesContainer;
return this.possibleDatesContainer;
}
async submitRegistration() {
try {
this.isPostingSubmission = true;
if (this.wasSubmissionSuccessful) {
return;
}
let language = null;
this.shadowRoot.querySelectorAll('.language').forEach((element) => {
if (element.checked) {
language = element.value;
}
});
const dates = [];
this.shadowRoot.querySelectorAll('.date').forEach((element) => {
if (element.checked) {
dates.push(element.value);
}
});
this.dates = dates;
let agreement = null;
this.shadowRoot.querySelectorAll('.agreement').forEach((element) => {
if (element.checked) {
agreement = element.value === 'true';
}
});
//Check language field
if (!language) {
this._('#lang-error').hidden = false;
return;
} else {
this._('#lang-error').hidden = true;
}
//Check dates field
if (dates.length === 0) {
this._('#dates-error').hidden = false;
return;
} else {
this._('#dates-error').hidden = true;
}
//Check agreement field
if (agreement === null) {
this._('#agreement-error').hidden = false;
return;
} else {
this._('#agreement-error').hidden = true;
}
let data = {
identifier: this.identifier,
givenName: this.firstName,
familyName: this.lastName,
email: this.email,
organizationIds: this.organizations.map((org) => org.code),
organizationNames: this.organizations.map((org) => org.name),
preferredLanguage: language,
possibleDates: dates,
privacyConsent: agreement,
};
let body = {
form: '/formalize/forms/' + FORM_IDENTIFIER,
dataFeedElement: JSON.stringify(data),
};
const options = {
method: 'POST',
headers: {
'Content-Type': 'application/ld+json',
Authorization: 'Bearer ' + this.auth.token,
},
body: JSON.stringify(body),
};
const response = await this.httpGetAsync(
this.entryPointUrl + '/formalize/submissions',
options,
);
if (!response.ok) {
this.handleErrorResponse(response);
} else {
this.wasSubmissionSuccessful = true;
}
return response;
} finally {
this.isPostingSubmission = false;
}
}
async buttonClickHandler() {
setTimeout(() => {
this.shadowRoot.querySelectorAll('dbp-button').forEach((element) => {
element.stop();
});
}, 1000);
await this.submitRegistration();
this.shadowRoot.querySelectorAll('.date').forEach((element) => {
if (this.dates.includes(element.value)) element.checked = true;
});
}
static get styles() {
return [
commonStyles.getThemeCSS(),
commonStyles.getGeneralCSS(),
commonStyles.getActivityCSS(),
commonStyles.getNotificationCSS(),
css`
.hidden {
display: none;
}
a {
color: blue;
}
textarea {
width: 100%;
resize: none;
}
.textField {
width: 100%;
}
.input-error {
background-color: var(--dbp-warning-surface);
padding-bottom: calc(0.375em - 1px);
padding-left: 0.75em;
padding-right: 0.75em;
padding-top: calc(0.375em - 1px);
color: var(--dbp-on-warning-surface);
}
.error-container {
margin-top: 3px;
}
.submit-button {
display: flex;
justify-content: flex-end;
}
`,
];
}
languageClick(e) {
this.preferredLanguage = e.target.value;
}
_onLoginClicked(e) {
this.sendSetPropertyEvent('requested-login-status', 'logged-in');
e.preventDefault();
}
render() {
const isFormUnavailable = this.formAvailability === FORM_AVAILABILITY_UNAVAILABLE;
const i18n = this._i18n;
const isPostingSubmission = this.isPostingSubmission;
const showSpinner =
(this.isAuthPending() || this.isLoadingData()) &&
!isFormUnavailable &&
!this.wasSubmissionSuccessful;
const showForm =
this.isLoggedIn() &&
!this.isLoadingData() &&
!isFormUnavailable &&
!this.wasSubmissionSuccessful &&
!this.disableForm;
const showFormUnavailable = this.isLoggedIn() && isFormUnavailable;
const showSuccessText = this.isLoggedIn() && this.wasSubmissionSuccessful;
return html`
<div class="control full-size-spinner ${classMap({hidden: !showSpinner})}">
<span class="loading">
<dbp-mini-spinner text=${i18n.t('loading')}></dbp-mini-spinner>
</span>
</div>
<div class="${classMap({hidden: !showForm})}">
<slot name="activity-description">
<p>
${replacePlaceholders(i18n.t('register.description-text'), {
link: html`
<a
target="_blank"
rel="noopener"
href=${i18n.t('further-information')}>
${i18n.t('register.description-link-text')}
</a>
`,
})}
</p>
<br />
</slot>
<form id="registration-form">
<div class="field field--firstname">
<label class="label" for="reg-firstname">${i18n.t('name.first')}</label>
<div class="control">
<input
id="reg-firstname"
type="text"
class="textField"
value="${this.firstName}"
disabled />
</div>
</div>
<div class="field field--lastname">
<label class="label" for="reg-lastname">${i18n.t('name.last')}</label>
<div class="control">
<input
id="reg-lastname"
type="text"
class="textField"
value="${this.lastName}"
disabled />
</div>
</div>
<div class="field field--organization">
<label class="label" for="reg-organization">
${i18n.t('organization')}
</label>
<div class="control">
<input
id="reg-organization"
type="text"
class="textField"
value="${this.organizations.map((org) => org.name).join(', ')}"
disabled />
</div>
</div>
<div class="field field--email">
<label class="label" for="reg-email">${i18n.t('email')}</label>
<div class="control">
<input
id="reg-email"
type="email"
class="textField"
value="${this.email}"
disabled />
</div>
</div>
<div class="field field--language">
<label class="label">${i18n.t('languages.label')}</label>
<div class="control">
<div>
<input
type="radio"
class="language"
id="language-german"
name="language"
value="de" />
<label for="language-german">${i18n.t('languages.german')}</label>
</div>
<div>
<input
type="radio"
class="language"
id="language-english"
name="language"
value="en" />
<label for="language-english">${i18n.t('languages.english')}</label>
</div>
<div>
<input
type="radio"
class="language"
id="language-both"
name="language"
value="both" />
<label for="language-both">${i18n.t('languages.both')}</label>
</div>
</div>
<div class="error-container">
<span class="input-error" id="lang-error" hidden>
${i18n.t('errors.language')}
</span>
</div>
</div>
<div class="field field--dates">
<label class="label">${i18n.t('date.label')}</label>
<div class="control">${this.createPossibleDatesContainer()}</div>
<div class="error-container">
<span class="input-error" id="dates-error" hidden>
${i18n.t('errors.date')}
</span>
</div>
</div>
<div class="field field--agreement">
<label class="label">
${replacePlaceholders(i18n.t('agreement.label'), {
link: html`
<a
target="_blank"
rel="noopener"
href=${i18n.t('agreement.privacy-policy')}>
${i18n.t('agreement.link-text')}
</a>
`,
})}
</label>
<div class="control">
<div>
<input
type="radio"
class="agreement"
id="yes"
name="agree"
value="true" />
<label for="yes">${i18n.t('agreement.yes')}</label>
</div>
<div>
<input
type="radio"
class="agreement"
id="no"
name="agree"
value="false" />
<label for="no">${i18n.t('agreement.no')}</label>
</div>
<div class="error-container">
<span class="input-error" id="agreement-error" hidden>
${i18n.t('errors.agreement')}
</span>
</div>
</div>
</div>
<div class="submit-button">
<dbp-button
value="Primary"
@click="${this.buttonClickHandler}"
type="is-primary"
class="${classMap({disabled: isPostingSubmission})}">
${i18n.t('submit')}
</dbp-button>
</div>
</form>
</div>
<div class="availability ${classMap({hidden: !showFormUnavailable})}">
<p>${i18n.t('availability')}</p>
</div>
<div class="registration-success ${classMap({hidden: !showSuccessText})}">
<h2><b>${i18n.t('register.registration-success-text')}</b></h2>
</div>
<div
class="notification is-warning ${classMap({
hidden: this.isLoggedIn() || this.isAuthPending(),
})}">
${i18n.t('error-login-message')}
<a href="#" @click="${this._onLoginClicked}">${i18n.t('error-login-link')}</a>
</div>
`;
}
}
commonUtils.defineCustomElement('dbp-lunchlottery-register', LunchLotteryRegister);