279 lines
7.3 KiB
TypeScript
279 lines
7.3 KiB
TypeScript
import Option from './option';
|
|
import User from './user';
|
|
import { TrackedArray } from 'tracked-built-ins';
|
|
import { NotFoundError, apiUrl } from '../utils/api';
|
|
import { decrypt, encrypt } from '../utils/encryption';
|
|
import answersForAnswerType from '../utils/answers-for-answer-type';
|
|
import fetch from 'fetch';
|
|
import config from 'croodle/config/environment';
|
|
import type { SelectionInput } from './selection';
|
|
|
|
const DAY_STRING_LENGTH = 10; // 'YYYY-MM-DD'.length
|
|
|
|
export type AnswerType = 'YesNo' | 'YesNoMaybe' | 'FreeText';
|
|
type OptionInput = { title: string }[];
|
|
export type PollType = 'FindADate' | 'MakeAPoll';
|
|
type PollInput = {
|
|
anonymousUser: boolean;
|
|
answerType: AnswerType;
|
|
creationDate: string;
|
|
description: string;
|
|
expirationDate: string;
|
|
forceAnswer: boolean;
|
|
id: string;
|
|
options: OptionInput;
|
|
pollType: PollType;
|
|
timezone: string | null;
|
|
title: string;
|
|
users: User[];
|
|
version: string;
|
|
};
|
|
|
|
export default class Poll {
|
|
// Is participation without user name possibile?
|
|
anonymousUser: boolean;
|
|
|
|
// YesNo, YesNoMaybe or Freetext
|
|
answerType: AnswerType;
|
|
|
|
// ISO-8601 combined date and time string in UTC
|
|
creationDate: string;
|
|
|
|
// poll's description
|
|
description: string;
|
|
|
|
// ISO 8601 date + time string in UTC
|
|
expirationDate: string;
|
|
|
|
// Must all options been answered?
|
|
forceAnswer: boolean;
|
|
|
|
// ID of the poll
|
|
id: string;
|
|
|
|
// array of poll's options
|
|
options: Option[];
|
|
|
|
// FindADate or MakeAPoll
|
|
pollType: 'FindADate' | 'MakeAPoll';
|
|
|
|
// timezone poll got created in (like "Europe/Berlin")
|
|
timezone: string | null;
|
|
|
|
// polls title
|
|
title: string;
|
|
|
|
// participants of the poll
|
|
users: TrackedArray<User>;
|
|
|
|
// Croodle version poll got created with
|
|
version: string;
|
|
|
|
get answers() {
|
|
const { answerType } = this;
|
|
return answersForAnswerType(answerType);
|
|
}
|
|
|
|
get hasTimes() {
|
|
if (this.isMakeAPoll) {
|
|
return false;
|
|
}
|
|
|
|
return this.options.some((option) => {
|
|
return option.title.length > DAY_STRING_LENGTH;
|
|
});
|
|
}
|
|
|
|
get isFindADate() {
|
|
return this.pollType === 'FindADate';
|
|
}
|
|
|
|
get isFreeText() {
|
|
return this.answerType === 'FreeText';
|
|
}
|
|
|
|
get isMakeAPoll() {
|
|
return this.pollType === 'MakeAPoll';
|
|
}
|
|
|
|
constructor({
|
|
anonymousUser,
|
|
answerType,
|
|
creationDate,
|
|
description,
|
|
expirationDate,
|
|
forceAnswer,
|
|
id,
|
|
options,
|
|
pollType,
|
|
timezone,
|
|
title,
|
|
users,
|
|
version,
|
|
}: PollInput) {
|
|
this.anonymousUser = anonymousUser;
|
|
this.answerType = answerType;
|
|
this.creationDate = creationDate;
|
|
this.description = description;
|
|
this.expirationDate = expirationDate;
|
|
this.forceAnswer = forceAnswer;
|
|
this.id = id;
|
|
this.options = options.map((option) => new Option(option));
|
|
this.pollType = pollType;
|
|
this.timezone = timezone;
|
|
this.title = title;
|
|
this.users = new TrackedArray(users);
|
|
this.version = version;
|
|
}
|
|
|
|
static async load(id: string, passphrase: string) {
|
|
const url = apiUrl(`polls/${id}`);
|
|
|
|
// TODO: Handle network connectivity error
|
|
const response = await fetch(url);
|
|
|
|
if (!response.ok) {
|
|
if (response.status === 404) {
|
|
throw new NotFoundError(
|
|
`A poll with ID ${id} could not be found at the server.`,
|
|
);
|
|
} else {
|
|
throw new Error(
|
|
`Unexpected server-side error. Server responsed with ${response.status} (${response.statusText})`,
|
|
);
|
|
}
|
|
}
|
|
|
|
// TODO: Handle malformed server response
|
|
const payload = (await response.json()) as {
|
|
poll: {
|
|
anonymousUser: string;
|
|
answerType: string;
|
|
creationDate: string;
|
|
description: string;
|
|
expirationDate: string;
|
|
forceAnswer: string;
|
|
id: string;
|
|
options: string;
|
|
pollType: string;
|
|
timezone: string;
|
|
title: string;
|
|
users: {
|
|
creationDate: string;
|
|
id: string;
|
|
name: string;
|
|
selections: string;
|
|
version: string;
|
|
}[];
|
|
version: string;
|
|
};
|
|
};
|
|
|
|
return new Poll({
|
|
anonymousUser: decrypt(payload.poll.anonymousUser, passphrase) as boolean,
|
|
answerType: decrypt(payload.poll.answerType, passphrase) as AnswerType,
|
|
creationDate: decrypt(payload.poll.creationDate, passphrase) as string,
|
|
description: decrypt(payload.poll.description, passphrase) as string,
|
|
expirationDate: decrypt(
|
|
payload.poll.expirationDate,
|
|
passphrase,
|
|
) as string,
|
|
forceAnswer: decrypt(payload.poll.forceAnswer, passphrase) as boolean,
|
|
id: payload.poll.id,
|
|
options: decrypt(payload.poll.options, passphrase) as OptionInput,
|
|
pollType: decrypt(payload.poll.pollType, passphrase) as PollType,
|
|
timezone: decrypt(payload.poll.timezone, passphrase) as string | null,
|
|
title: decrypt(payload.poll.title, passphrase) as string,
|
|
users: payload.poll.users.map((user) => {
|
|
return new User({
|
|
creationDate: decrypt(user.creationDate, passphrase) as string,
|
|
id: user.id,
|
|
name: decrypt(user.name, passphrase) as string,
|
|
selections: decrypt(user.selections, passphrase) as SelectionInput[],
|
|
version: user.version,
|
|
});
|
|
}),
|
|
version: payload.poll.version,
|
|
});
|
|
}
|
|
|
|
static async create(
|
|
{
|
|
anonymousUser,
|
|
answerType,
|
|
description,
|
|
expirationDate,
|
|
forceAnswer,
|
|
options,
|
|
pollType,
|
|
title,
|
|
}: {
|
|
anonymousUser: boolean;
|
|
answerType: AnswerType;
|
|
description: string;
|
|
expirationDate: string;
|
|
forceAnswer: boolean;
|
|
options: OptionInput;
|
|
pollType: PollType;
|
|
title: string;
|
|
},
|
|
passphrase: string,
|
|
) {
|
|
const creationDate = new Date().toISOString();
|
|
const version = config.APP['version'] as string;
|
|
const timezone =
|
|
pollType === 'FindADate' &&
|
|
options.some(({ title }) => {
|
|
return title.length >= 'YYYY-MM-DDTHH:mm'.length;
|
|
})
|
|
? Intl.DateTimeFormat().resolvedOptions().timeZone
|
|
: null;
|
|
|
|
const payload = {
|
|
poll: {
|
|
anonymousUser: encrypt(anonymousUser, passphrase),
|
|
answerType: encrypt(answerType, passphrase),
|
|
creationDate: encrypt(creationDate, passphrase),
|
|
description: encrypt(description, passphrase),
|
|
expirationDate: encrypt(expirationDate, passphrase),
|
|
forceAnswer: encrypt(forceAnswer, passphrase),
|
|
options: encrypt(options, passphrase),
|
|
pollType: encrypt(pollType, passphrase),
|
|
serverExpirationDate: expirationDate,
|
|
timezone: encrypt(timezone, passphrase),
|
|
title: encrypt(title, passphrase),
|
|
version,
|
|
},
|
|
};
|
|
|
|
// TODO: Handle network connectivity issue
|
|
const response = await fetch(apiUrl('polls'), {
|
|
body: JSON.stringify(payload),
|
|
method: 'POST',
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(
|
|
`Unexpected server-side error. Server responded with ${response.status} (${response.statusText})`,
|
|
);
|
|
}
|
|
const responseDocument = await response.json();
|
|
const { id } = responseDocument.poll;
|
|
|
|
return new Poll({
|
|
anonymousUser,
|
|
answerType,
|
|
creationDate,
|
|
description,
|
|
expirationDate,
|
|
forceAnswer,
|
|
id,
|
|
options,
|
|
pollType,
|
|
timezone,
|
|
title,
|
|
users: [],
|
|
version,
|
|
});
|
|
}
|
|
}
|