Add timezone handling

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
Thomas Citharel
2021-10-10 16:25:50 +02:00
parent eba3c70c9b
commit d58ca5743d
49 changed files with 1218 additions and 429 deletions

View File

@@ -44,9 +44,10 @@
:placeholder="$t('Type or select a date…')"
icon="calendar-today"
:locale="$i18n.locale"
v-model="event.beginsOn"
v-model="beginsOn"
horizontal-time-picker
editable
:tz-offset="tzOffset(beginsOn)"
:datepicker="{
id: 'begins-on-field',
'aria-next-label': $t('Next month'),
@@ -62,9 +63,10 @@
:placeholder="$t('Type or select a date…')"
icon="calendar-today"
:locale="$i18n.locale"
v-model="event.endsOn"
v-model="endsOn"
horizontal-time-picker
:min-datetime="event.beginsOn"
:min-datetime="beginsOn"
:tz-offset="tzOffset(endsOn)"
editable
:datepicker="{
id: 'ends-on-field',
@@ -75,12 +77,14 @@
</b-datetimepicker>
</b-field>
<!-- <b-switch v-model="endsOnNull">{{ $t('No end date') }}</b-switch>-->
<b-button type="is-text" @click="dateSettingsIsOpen = true">
{{ $t("Date parameters") }}
</b-button>
<full-address-auto-complete v-model="event.physicalAddress" />
<full-address-auto-complete
v-model="eventPhysicalAddress"
:user-timezone="userActualTimezone"
/>
<div class="field">
<label class="label">{{ $t("Description") }}</label>
@@ -332,9 +336,45 @@
<form action>
<div class="modal-card" style="width: auto">
<header class="modal-card-head">
<p class="modal-card-title">{{ $t("Date and time settings") }}</p>
<h3 class="modal-card-title">{{ $t("Date and time settings") }}</h3>
</header>
<section class="modal-card-body">
<p>
{{
$t(
"Event timezone will default to the timezone of the event's address if there is one, or to your own timezone setting."
)
}}
</p>
<b-field :label="$t('Timezone')" label-for="timezone" expanded>
<b-select
:placeholder="$t('Select a timezone')"
:loading="!config"
v-model="timezone"
id="timezone"
>
<optgroup
:label="group"
v-for="(groupTimezones, group) in timezones"
:key="group"
>
<option
v-for="timezone in groupTimezones"
:value="`${group}/${timezone}`"
:key="timezone"
>
{{ sanitizeTimezone(timezone) }}
</option>
</optgroup>
</b-select>
<b-button
:disabled="!timezone"
@click="timezone = null"
class="reset-area"
icon-left="close"
:title="$t('Clear timezone field')"
/>
</b-field>
<b-field :label="$t('Event page settings')">
<b-switch v-model="eventOptions.showStartTime">{{
$t("Show the time when the event begins")
@@ -514,6 +554,7 @@ section {
<script lang="ts">
import { Component, Prop, Vue, Watch } from "vue-property-decorator";
import { getTimezoneOffset } from "date-fns-tz";
import PictureUpload from "@/components/PictureUpload.vue";
import EditorComponent from "@/components/Editor.vue";
import TagInput from "@/components/Event/TagInput.vue";
@@ -541,6 +582,7 @@ import {
} from "../../graphql/event";
import {
EventModel,
IEditableEvent,
IEvent,
removeTypeName,
toEditJSON,
@@ -566,7 +608,7 @@ import {
} from "../../utils/image";
import RouteName from "../../router/name";
import "intersection-observer";
import { CONFIG } from "../../graphql/config";
import { CONFIG_EDIT_EVENT } from "../../graphql/config";
import { IConfig } from "../../types/config.model";
import {
ApolloCache,
@@ -575,6 +617,9 @@ import {
} from "@apollo/client/core";
import cloneDeep from "lodash/cloneDeep";
import { IEventOptions } from "@/types/event-options.model";
import { USER_SETTINGS } from "@/graphql/user";
import { IUser } from "@/types/current-user.model";
import { IAddress } from "@/types/address.model";
const DEFAULT_LIMIT_NUMBER_OF_PLACES = 10;
@@ -591,7 +636,8 @@ const DEFAULT_LIMIT_NUMBER_OF_PLACES = 10;
},
apollo: {
currentActor: CURRENT_ACTOR_CLIENT,
config: CONFIG,
loggedUser: USER_SETTINGS,
config: CONFIG_EDIT_EVENT,
identities: IDENTITIES,
event: {
query: FETCH_EVENT,
@@ -643,9 +689,11 @@ export default class EditEvent extends Vue {
currentActor!: IActor;
event: IEvent = new EventModel();
loggedUser!: IUser;
unmodifiedEvent: IEvent = new EventModel();
event: IEditableEvent = new EventModel();
unmodifiedEvent: IEditableEvent = new EventModel();
identities: IActor[] = [];
@@ -671,8 +719,6 @@ export default class EditEvent extends Vue {
dateSettingsIsOpen = false;
endsOnNull = false;
saving = false;
displayNameAndUsername = displayNameAndUsername;
@@ -908,7 +954,7 @@ export default class EditEvent extends Vue {
*/
private postCreateOrUpdate(store: any, updateEvent: IEvent) {
const resultEvent: IEvent = { ...updateEvent };
console.log(resultEvent);
console.debug("resultEvent", resultEvent);
if (!updateEvent.draft) {
store.writeQuery({
query: EVENT_PERSON_PARTICIPATION,
@@ -984,6 +1030,23 @@ export default class EditEvent extends Vue {
...toEditJSON(new EventModel(this.event)),
options: this.eventOptions,
};
console.debug(this.event.beginsOn?.toISOString());
// if (this.event.beginsOn && this.timezone) {
// console.debug(
// "begins on should be",
// zonedTimeToUtc(this.event.beginsOn, this.timezone).toISOString()
// );
// }
// if (this.event.beginsOn && this.timezone) {
// res.beginsOn = zonedTimeToUtc(
// this.event.beginsOn,
// this.timezone
// ).toISOString();
// }
const organizerActor = this.event.organizerActor?.id
? this.event.organizerActor
: this.organizerActor;
@@ -995,10 +1058,6 @@ export default class EditEvent extends Vue {
: null;
res = { ...res, attributedToId };
if (this.endsOnNull) {
res.endsOn = null;
}
if (this.pictureFile) {
const pictureObj = buildFileVariable(this.pictureFile, "picture");
res = { ...res, ...pictureObj };
@@ -1119,13 +1178,16 @@ export default class EditEvent extends Vue {
);
}
get beginsOn(): Date {
get beginsOn(): Date | null {
// if (this.timezone && this.event.beginsOn) {
// return utcToZonedTime(this.event.beginsOn, this.timezone);
// }
return this.event.beginsOn;
}
@Watch("beginsOn", { deep: true })
onBeginsOnChanged(beginsOn: string): void {
if (!this.event.endsOn) return;
set beginsOn(beginsOn: Date | null) {
this.event.beginsOn = beginsOn;
if (!this.event.endsOn || !beginsOn) return;
const dateBeginsOn = new Date(beginsOn);
const dateEndsOn = new Date(this.event.endsOn);
if (dateEndsOn < dateBeginsOn) {
@@ -1137,13 +1199,94 @@ export default class EditEvent extends Vue {
}
}
/**
* In event endsOn datepicker, we lock starting with the day before the beginsOn date
*/
get minDateForEndsOn(): Date {
const minDate = new Date(this.event.beginsOn);
minDate.setDate(minDate.getDate() - 1);
return minDate;
get endsOn(): Date | null {
// if (this.event.endsOn && this.timezone) {
// return utcToZonedTime(this.event.endsOn, this.timezone);
// }
return this.event.endsOn;
}
set endsOn(endsOn: Date | null) {
this.event.endsOn = endsOn;
}
get timezones(): Record<string, string[]> {
if (!this.config || !this.config.timezones) return {};
return this.config.timezones.reduce(
(acc: { [key: string]: Array<string> }, val: string) => {
const components = val.split("/");
const [prefix, suffix] = [
components.shift() as string,
components.join("/"),
];
const pushOrCreate = (
acc2: { [key: string]: Array<string> },
prefix2: string,
suffix2: string
) => {
// eslint-disable-next-line no-param-reassign
(acc2[prefix2] = acc2[prefix2] || []).push(suffix2);
return acc2;
};
if (suffix) {
return pushOrCreate(acc, prefix, suffix);
}
return pushOrCreate(acc, this.$t("Other") as string, prefix);
},
{}
);
}
// eslint-disable-next-line class-methods-use-this
sanitizeTimezone(timezone: string): string {
return timezone
.split("_")
.join(" ")
.replace("St ", "St. ")
.split("/")
.join(" - ");
}
get timezone(): string | null {
return this.event.options.timezone;
}
set timezone(timezone: string | null) {
this.event.options = {
...this.event.options,
timezone,
};
}
get userTimezone(): string | undefined {
return this.loggedUser?.settings?.timezone;
}
get userActualTimezone(): string {
if (this.userTimezone) {
return this.userTimezone;
}
return Intl.DateTimeFormat().resolvedOptions().timeZone;
}
tzOffset(date: Date): number {
if (this.timezone && date) {
const eventUTCOffset = getTimezoneOffset(this.timezone, date);
const localUTCOffset = getTimezoneOffset(this.userActualTimezone);
return (eventUTCOffset - localUTCOffset) / (60 * 1000);
}
return 0;
}
get eventPhysicalAddress(): IAddress | null {
return this.event.physicalAddress;
}
set eventPhysicalAddress(address: IAddress | null) {
if (address && address.timezone) {
this.timezone = address.timezone;
}
this.event.physicalAddress = address;
}
}
</script>

View File

@@ -303,6 +303,8 @@
v-if="event && config"
:event="event"
:config="config"
:user="loggedUser"
@showMapModal="showMap = true"
/>
</div>
</aside>
@@ -458,6 +460,22 @@
</section>
</div>
</b-modal>
<b-modal
class="map-modal"
v-if="event.physicalAddress && event.physicalAddress.geom"
:active.sync="showMap"
has-modal-card
full-screen
:can-cancel="['escape', 'outside']"
>
<template #default="props">
<event-map
:routingType="routingType"
:address="event.physicalAddress"
@close="props.close"
/>
</template>
</b-modal>
</div>
</div>
</template>
@@ -508,11 +526,14 @@ import Subtitle from "../../components/Utils/Subtitle.vue";
import Tag from "../../components/Tag.vue";
import EventMetadataSidebar from "../../components/Event/EventMetadataSidebar.vue";
import EventBanner from "../../components/Event/EventBanner.vue";
import EventMap from "../../components/Event/EventMap.vue";
import PopoverActorCard from "../../components/Account/PopoverActorCard.vue";
import { IParticipant } from "../../types/participant.model";
import { ApolloCache, FetchResult } from "@apollo/client/core";
import { IEventMetadataDescription } from "@/types/event-metadata";
import { eventMetaDataList } from "../../services/EventMetadata";
import { USER_SETTINGS } from "@/graphql/user";
import { IUser } from "@/types/current-user.model";
// noinspection TypeScriptValidateTypes
@Component({
@@ -529,6 +550,7 @@ import { eventMetaDataList } from "../../services/EventMetadata";
PopoverActorCard,
EventBanner,
EventMetadataSidebar,
EventMap,
ShareEventModal: () =>
import(
/* webpackChunkName: "shareEventModal" */ "../../components/Event/ShareEventModal.vue"
@@ -567,9 +589,8 @@ import { eventMetaDataList } from "../../services/EventMetadata";
this.handleErrors(graphQLErrors);
},
},
currentActor: {
query: CURRENT_ACTOR_CLIENT,
},
currentActor: CURRENT_ACTOR_CLIENT,
loggedUser: USER_SETTINGS,
participations: {
query: EVENT_PERSON_PARTICIPATION,
fetchPolicy: "cache-and-network",
@@ -646,6 +667,8 @@ export default class Event extends EventMixin {
person!: IPerson;
loggedUser!: IUser;
participations: IParticipant[] = [];
oldParticipationRole!: string;
@@ -1130,6 +1153,12 @@ export default class Event extends EventMixin {
return acc;
}, {});
}
showMap = false;
get routingType(): string | undefined {
return this.config?.maps?.routing?.type;
}
}
</script>
<style lang="scss" scoped>