124
js/src/components/Address/AddressInfo.vue
Normal file
124
js/src/components/Address/AddressInfo.vue
Normal file
@@ -0,0 +1,124 @@
|
||||
<template>
|
||||
<address>
|
||||
<b-icon
|
||||
v-if="showIcon"
|
||||
:icon="address.poiInfos.poiIcon.icon"
|
||||
size="is-medium"
|
||||
class="icon"
|
||||
/>
|
||||
<p>
|
||||
<span
|
||||
class="addressDescription"
|
||||
:title="address.poiInfos.name"
|
||||
v-if="address.poiInfos.name"
|
||||
>
|
||||
{{ address.poiInfos.name }}
|
||||
</span>
|
||||
<br v-if="address.poiInfos.name" />
|
||||
<span class="has-text-grey-dark">
|
||||
{{ address.poiInfos.alternativeName }}
|
||||
</span>
|
||||
<br />
|
||||
<small
|
||||
v-if="
|
||||
userTimezoneDifferent &&
|
||||
longShortTimezoneNamesDifferent &&
|
||||
timezoneLongNameValid
|
||||
"
|
||||
class="has-text-grey-dark"
|
||||
>
|
||||
🌐
|
||||
{{
|
||||
$t("{timezoneLongName} ({timezoneShortName})", {
|
||||
timezoneLongName,
|
||||
timezoneShortName,
|
||||
})
|
||||
}}
|
||||
</small>
|
||||
<small v-else-if="userTimezoneDifferent" class="has-text-grey-dark">
|
||||
🌐 {{ timezoneShortName }}
|
||||
</small>
|
||||
</p>
|
||||
</address>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { IAddress } from "@/types/address.model";
|
||||
import { PropType } from "vue";
|
||||
import { Component, Prop, Vue } from "vue-property-decorator";
|
||||
|
||||
@Component
|
||||
export default class AddressInfo extends Vue {
|
||||
@Prop({ required: true, type: Object as PropType<IAddress> })
|
||||
address!: IAddress;
|
||||
|
||||
@Prop({ required: false, default: false, type: Boolean }) showIcon!: boolean;
|
||||
@Prop({ required: false, default: false, type: Boolean })
|
||||
showTimezone!: boolean;
|
||||
@Prop({ required: false, type: String }) userTimezone!: string;
|
||||
|
||||
get userTimezoneDifferent(): boolean {
|
||||
return (
|
||||
this.userTimezone != undefined &&
|
||||
this.address.timezone != undefined &&
|
||||
this.userTimezone !== this.address.timezone
|
||||
);
|
||||
}
|
||||
|
||||
get longShortTimezoneNamesDifferent(): boolean {
|
||||
return (
|
||||
this.timezoneLongName != undefined &&
|
||||
this.timezoneShortName != undefined &&
|
||||
this.timezoneLongName !== this.timezoneShortName
|
||||
);
|
||||
}
|
||||
|
||||
get timezoneLongName(): string | undefined {
|
||||
return this.timezoneName("long");
|
||||
}
|
||||
|
||||
get timezoneShortName(): string | undefined {
|
||||
return this.timezoneName("short");
|
||||
}
|
||||
|
||||
get timezoneLongNameValid(): boolean {
|
||||
return (
|
||||
this.timezoneLongName != undefined && !this.timezoneLongName.match(/UTC/)
|
||||
);
|
||||
}
|
||||
|
||||
private timezoneName(format: "long" | "short"): string | undefined {
|
||||
return this.extractTimezone(
|
||||
new Intl.DateTimeFormat(undefined, {
|
||||
timeZoneName: format,
|
||||
timeZone: this.address.timezone,
|
||||
}).formatToParts()
|
||||
);
|
||||
}
|
||||
|
||||
private extractTimezone(
|
||||
parts: Intl.DateTimeFormatPart[]
|
||||
): string | undefined {
|
||||
return parts.find((part) => part.type === "timeZoneName")?.value;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
address {
|
||||
font-style: normal;
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
|
||||
span.addressDescription {
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
flex: 1 0 auto;
|
||||
min-width: 100%;
|
||||
max-width: 4rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
span.icon {
|
||||
padding-right: 1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -18,64 +18,97 @@
|
||||
</docs>
|
||||
|
||||
<template>
|
||||
<span v-if="!endsOn">{{
|
||||
beginsOn | formatDateTimeString(showStartTime)
|
||||
}}</span>
|
||||
<span v-else-if="isSameDay() && showStartTime && showEndTime">
|
||||
{{
|
||||
<p v-if="!endsOn">
|
||||
<span>{{
|
||||
formatDateTimeString(beginsOn, timezoneToShow, showStartTime)
|
||||
}}</span>
|
||||
<br />
|
||||
<b-switch
|
||||
size="is-small"
|
||||
v-model="showLocalTimezone"
|
||||
v-if="differentFromUserTimezone"
|
||||
>
|
||||
{{ singleTimeZone }}
|
||||
</b-switch>
|
||||
</p>
|
||||
<p v-else-if="isSameDay() && showStartTime && showEndTime">
|
||||
<span>{{
|
||||
$t("On {date} from {startTime} to {endTime}", {
|
||||
date: formatDate(beginsOn),
|
||||
startTime: formatTime(beginsOn),
|
||||
endTime: formatTime(endsOn),
|
||||
startTime: formatTime(beginsOn, timezoneToShow),
|
||||
endTime: formatTime(endsOn, timezoneToShow),
|
||||
})
|
||||
}}
|
||||
</span>
|
||||
<span v-else-if="isSameDay() && !showStartTime && showEndTime">
|
||||
{{
|
||||
$t("On {date} ending at {endTime}", {
|
||||
date: formatDate(beginsOn),
|
||||
endTime: formatTime(endsOn),
|
||||
})
|
||||
}}
|
||||
</span>
|
||||
<span v-else-if="isSameDay() && showStartTime && !showEndTime">
|
||||
}}</span>
|
||||
<br />
|
||||
<b-switch
|
||||
size="is-small"
|
||||
v-model="showLocalTimezone"
|
||||
v-if="differentFromUserTimezone"
|
||||
>
|
||||
{{ singleTimeZone }}
|
||||
</b-switch>
|
||||
</p>
|
||||
<p v-else-if="isSameDay() && showStartTime && !showEndTime">
|
||||
{{
|
||||
$t("On {date} starting at {startTime}", {
|
||||
date: formatDate(beginsOn),
|
||||
startTime: formatTime(beginsOn),
|
||||
})
|
||||
}}
|
||||
</span>
|
||||
<span v-else-if="isSameDay()">{{
|
||||
$t("On {date}", { date: formatDate(beginsOn) })
|
||||
}}</span>
|
||||
<span v-else-if="endsOn && showStartTime && showEndTime">
|
||||
{{
|
||||
$t("From the {startDate} at {startTime} to the {endDate} at {endTime}", {
|
||||
startDate: formatDate(beginsOn),
|
||||
startTime: formatTime(beginsOn),
|
||||
endDate: formatDate(endsOn),
|
||||
endTime: formatTime(endsOn),
|
||||
})
|
||||
}}
|
||||
</span>
|
||||
<span v-else-if="endsOn && showStartTime">
|
||||
{{
|
||||
$t("From the {startDate} at {startTime} to the {endDate}", {
|
||||
startDate: formatDate(beginsOn),
|
||||
startTime: formatTime(beginsOn),
|
||||
endDate: formatDate(endsOn),
|
||||
})
|
||||
}}
|
||||
</span>
|
||||
<span v-else-if="endsOn">
|
||||
</p>
|
||||
<p v-else-if="isSameDay()">
|
||||
{{ $t("On {date}", { date: formatDate(beginsOn) }) }}
|
||||
</p>
|
||||
<p v-else-if="endsOn && showStartTime && showEndTime">
|
||||
<span>
|
||||
{{
|
||||
$t(
|
||||
"From the {startDate} at {startTime} to the {endDate} at {endTime}",
|
||||
{
|
||||
startDate: formatDate(beginsOn),
|
||||
startTime: formatTime(beginsOn, timezoneToShow),
|
||||
endDate: formatDate(endsOn),
|
||||
endTime: formatTime(endsOn, timezoneToShow),
|
||||
}
|
||||
)
|
||||
}}
|
||||
</span>
|
||||
<br />
|
||||
<b-switch
|
||||
size="is-small"
|
||||
v-model="showLocalTimezone"
|
||||
v-if="differentFromUserTimezone"
|
||||
>
|
||||
{{ multipleTimeZones }}
|
||||
</b-switch>
|
||||
</p>
|
||||
<p v-else-if="endsOn && showStartTime">
|
||||
<span>
|
||||
{{
|
||||
$t("From the {startDate} at {startTime} to the {endDate}", {
|
||||
startDate: formatDate(beginsOn),
|
||||
startTime: formatTime(beginsOn, timezoneToShow),
|
||||
endDate: formatDate(endsOn),
|
||||
})
|
||||
}}
|
||||
</span>
|
||||
<br />
|
||||
<b-switch
|
||||
size="is-small"
|
||||
v-model="showLocalTimezone"
|
||||
v-if="differentFromUserTimezone"
|
||||
>
|
||||
{{ singleTimeZone }}
|
||||
</b-switch>
|
||||
</p>
|
||||
<p v-else-if="endsOn">
|
||||
{{
|
||||
$t("From the {startDate} to the {endDate}", {
|
||||
startDate: formatDate(beginsOn),
|
||||
endDate: formatDate(endsOn),
|
||||
})
|
||||
}}
|
||||
</span>
|
||||
</p>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from "vue-property-decorator";
|
||||
@@ -90,14 +123,47 @@ export default class EventFullDate extends Vue {
|
||||
|
||||
@Prop({ required: false, default: true }) showEndTime!: boolean;
|
||||
|
||||
@Prop({ required: false }) timezone!: string;
|
||||
|
||||
@Prop({ required: false }) userTimezone!: string;
|
||||
|
||||
showLocalTimezone = true;
|
||||
|
||||
get timezoneToShow(): string {
|
||||
if (this.showLocalTimezone) {
|
||||
return this.timezone;
|
||||
}
|
||||
return this.userActualTimezone;
|
||||
}
|
||||
|
||||
get userActualTimezone(): string {
|
||||
if (this.userTimezone) {
|
||||
return this.userTimezone;
|
||||
}
|
||||
return Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
}
|
||||
|
||||
formatDate(value: Date): string | undefined {
|
||||
if (!this.$options.filters) return undefined;
|
||||
return this.$options.filters.formatDateString(value);
|
||||
}
|
||||
|
||||
formatTime(value: Date): string | undefined {
|
||||
formatTime(value: Date, timezone: string): string | undefined {
|
||||
if (!this.$options.filters) return undefined;
|
||||
return this.$options.filters.formatTimeString(value);
|
||||
return this.$options.filters.formatTimeString(value, timezone || undefined);
|
||||
}
|
||||
|
||||
formatDateTimeString(
|
||||
value: Date,
|
||||
timezone: string,
|
||||
showTime: boolean
|
||||
): string | undefined {
|
||||
if (!this.$options.filters) return undefined;
|
||||
return this.$options.filters.formatDateTimeString(
|
||||
value,
|
||||
timezone,
|
||||
showTime
|
||||
);
|
||||
}
|
||||
|
||||
isSameDay(): boolean {
|
||||
@@ -106,5 +172,35 @@ export default class EventFullDate extends Vue {
|
||||
new Date(this.endsOn).toDateString();
|
||||
return this.endsOn !== undefined && sameDay;
|
||||
}
|
||||
|
||||
get differentFromUserTimezone(): boolean {
|
||||
return (
|
||||
!!this.timezone &&
|
||||
!!this.userActualTimezone &&
|
||||
this.timezone !== this.userActualTimezone
|
||||
);
|
||||
}
|
||||
|
||||
get singleTimeZone(): string {
|
||||
if (this.showLocalTimezone) {
|
||||
return this.$t("Local time ({timezone})", {
|
||||
timezone: this.timezoneToShow,
|
||||
}) as string;
|
||||
}
|
||||
return this.$t("Time in your timezone ({timezone})", {
|
||||
timezone: this.timezoneToShow,
|
||||
}) as string;
|
||||
}
|
||||
|
||||
get multipleTimeZones(): string {
|
||||
if (this.showLocalTimezone) {
|
||||
return this.$t("Local time ({timezone})", {
|
||||
timezone: this.timezoneToShow,
|
||||
}) as string;
|
||||
}
|
||||
return this.$t("Times in your timezone ({timezone})", {
|
||||
timezone: this.timezoneToShow,
|
||||
}) as string;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
175
js/src/components/Event/EventMap.vue
Normal file
175
js/src/components/Event/EventMap.vue
Normal file
@@ -0,0 +1,175 @@
|
||||
<template>
|
||||
<div class="modal-card">
|
||||
<header class="modal-card-head">
|
||||
<button type="button" class="delete" @click="$emit('close')" />
|
||||
</header>
|
||||
<div class="modal-card-body">
|
||||
<section class="map">
|
||||
<map-leaflet
|
||||
:coords="physicalAddress.geom"
|
||||
:marker="{
|
||||
text: physicalAddress.fullName,
|
||||
icon: physicalAddress.poiInfos.poiIcon.icon,
|
||||
}"
|
||||
/>
|
||||
</section>
|
||||
<section class="columns is-centered map-footer">
|
||||
<div class="column is-half has-text-centered">
|
||||
<p class="address">
|
||||
<i class="mdi mdi-map-marker"></i>
|
||||
{{ physicalAddress.fullName }}
|
||||
</p>
|
||||
<p class="getting-there">{{ $t("Getting there") }}</p>
|
||||
<div
|
||||
class="buttons"
|
||||
v-if="
|
||||
addressLinkToRouteByCar ||
|
||||
addressLinkToRouteByBike ||
|
||||
addressLinkToRouteByFeet
|
||||
"
|
||||
>
|
||||
<a
|
||||
class="button"
|
||||
target="_blank"
|
||||
v-if="addressLinkToRouteByFeet"
|
||||
:href="addressLinkToRouteByFeet"
|
||||
>
|
||||
<i class="mdi mdi-walk"></i
|
||||
></a>
|
||||
<a
|
||||
class="button"
|
||||
target="_blank"
|
||||
v-if="addressLinkToRouteByBike"
|
||||
:href="addressLinkToRouteByBike"
|
||||
>
|
||||
<i class="mdi mdi-bike"></i
|
||||
></a>
|
||||
<a
|
||||
class="button"
|
||||
target="_blank"
|
||||
v-if="addressLinkToRouteByTransit"
|
||||
:href="addressLinkToRouteByTransit"
|
||||
>
|
||||
<i class="mdi mdi-bus"></i
|
||||
></a>
|
||||
<a
|
||||
class="button"
|
||||
target="_blank"
|
||||
v-if="addressLinkToRouteByCar"
|
||||
:href="addressLinkToRouteByCar"
|
||||
>
|
||||
<i class="mdi mdi-car"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { Address, IAddress } from "@/types/address.model";
|
||||
import { RoutingTransportationType, RoutingType } from "@/types/enums";
|
||||
import { PropType } from "vue";
|
||||
import { Component, Vue, Prop } from "vue-property-decorator";
|
||||
|
||||
const RoutingParamType = {
|
||||
[RoutingType.OPENSTREETMAP]: {
|
||||
[RoutingTransportationType.FOOT]: "engine=fossgis_osrm_foot",
|
||||
[RoutingTransportationType.BIKE]: "engine=fossgis_osrm_bike",
|
||||
[RoutingTransportationType.TRANSIT]: null,
|
||||
[RoutingTransportationType.CAR]: "engine=fossgis_osrm_car",
|
||||
},
|
||||
[RoutingType.GOOGLE_MAPS]: {
|
||||
[RoutingTransportationType.FOOT]: "dirflg=w",
|
||||
[RoutingTransportationType.BIKE]: "dirflg=b",
|
||||
[RoutingTransportationType.TRANSIT]: "dirflg=r",
|
||||
[RoutingTransportationType.CAR]: "driving",
|
||||
},
|
||||
};
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
"map-leaflet": () =>
|
||||
import(/* webpackChunkName: "map" */ "../../components/Map.vue"),
|
||||
},
|
||||
})
|
||||
export default class EventMap extends Vue {
|
||||
@Prop({ type: Object as PropType<IAddress> }) address!: IAddress;
|
||||
@Prop({ type: String }) routingType!: RoutingType;
|
||||
|
||||
get physicalAddress(): Address | null {
|
||||
if (!this.address) return null;
|
||||
|
||||
return new Address(this.address);
|
||||
}
|
||||
|
||||
makeNavigationPath(
|
||||
transportationType: RoutingTransportationType
|
||||
): string | undefined {
|
||||
const geometry = this.physicalAddress?.geom;
|
||||
if (geometry) {
|
||||
/**
|
||||
* build urls to routing map
|
||||
*/
|
||||
if (!RoutingParamType[this.routingType][transportationType]) {
|
||||
return;
|
||||
}
|
||||
|
||||
const urlGeometry = geometry.split(";").reverse().join(",");
|
||||
|
||||
switch (this.routingType) {
|
||||
case RoutingType.GOOGLE_MAPS:
|
||||
return `https://maps.google.com/?saddr=Current+Location&daddr=${urlGeometry}&${
|
||||
RoutingParamType[this.routingType][transportationType]
|
||||
}`;
|
||||
case RoutingType.OPENSTREETMAP:
|
||||
default: {
|
||||
const bboxX = geometry.split(";").reverse()[0];
|
||||
const bboxY = geometry.split(";").reverse()[1];
|
||||
return `https://www.openstreetmap.org/directions?from=&to=${urlGeometry}&${
|
||||
RoutingParamType[this.routingType][transportationType]
|
||||
}#map=14/${bboxX}/${bboxY}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get addressLinkToRouteByCar(): undefined | string {
|
||||
return this.makeNavigationPath(RoutingTransportationType.CAR);
|
||||
}
|
||||
|
||||
get addressLinkToRouteByBike(): undefined | string {
|
||||
return this.makeNavigationPath(RoutingTransportationType.BIKE);
|
||||
}
|
||||
|
||||
get addressLinkToRouteByFeet(): undefined | string {
|
||||
return this.makeNavigationPath(RoutingTransportationType.FOOT);
|
||||
}
|
||||
|
||||
get addressLinkToRouteByTransit(): undefined | string {
|
||||
return this.makeNavigationPath(RoutingTransportationType.TRANSIT);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.modal-card-head {
|
||||
justify-content: flex-end;
|
||||
button.delete {
|
||||
margin-right: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
section.map {
|
||||
height: calc(100% - 8rem);
|
||||
width: calc(100% - 20px);
|
||||
}
|
||||
|
||||
section.map-footer {
|
||||
p.address {
|
||||
margin: 1rem auto;
|
||||
}
|
||||
div.buttons {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -11,7 +11,7 @@
|
||||
<b-button
|
||||
type="is-text"
|
||||
class="map-show-button"
|
||||
@click="showMap = !showMap"
|
||||
@click="$emit('showMapModal', true)"
|
||||
v-if="physicalAddress.geom"
|
||||
>
|
||||
{{ $t("Show map") }}
|
||||
@@ -24,6 +24,8 @@
|
||||
:beginsOn="event.beginsOn"
|
||||
:show-start-time="event.options.showStartTime"
|
||||
:show-end-time="event.options.showEndTime"
|
||||
:timezone="event.options.timezone"
|
||||
:userTimezone="userTimezone"
|
||||
:endsOn="event.endsOn"
|
||||
/>
|
||||
</event-metadata-block>
|
||||
@@ -130,91 +132,12 @@
|
||||
>
|
||||
<span v-else>{{ extra.value }}</span>
|
||||
</event-metadata-block>
|
||||
<b-modal
|
||||
class="map-modal"
|
||||
v-if="physicalAddress && physicalAddress.geom"
|
||||
:active.sync="showMap"
|
||||
has-modal-card
|
||||
full-screen
|
||||
>
|
||||
<div class="modal-card">
|
||||
<header class="modal-card-head">
|
||||
<button type="button" class="delete" @click="showMap = false" />
|
||||
</header>
|
||||
<div class="modal-card-body">
|
||||
<section class="map">
|
||||
<map-leaflet
|
||||
:coords="physicalAddress.geom"
|
||||
:marker="{
|
||||
text: physicalAddress.fullName,
|
||||
icon: physicalAddress.poiInfos.poiIcon.icon,
|
||||
}"
|
||||
/>
|
||||
</section>
|
||||
<section class="columns is-centered map-footer">
|
||||
<div class="column is-half has-text-centered">
|
||||
<p class="address">
|
||||
<i class="mdi mdi-map-marker"></i>
|
||||
{{ physicalAddress.fullName }}
|
||||
</p>
|
||||
<p class="getting-there">{{ $t("Getting there") }}</p>
|
||||
<div
|
||||
class="buttons"
|
||||
v-if="
|
||||
addressLinkToRouteByCar ||
|
||||
addressLinkToRouteByBike ||
|
||||
addressLinkToRouteByFeet
|
||||
"
|
||||
>
|
||||
<a
|
||||
class="button"
|
||||
target="_blank"
|
||||
v-if="addressLinkToRouteByFeet"
|
||||
:href="addressLinkToRouteByFeet"
|
||||
>
|
||||
<i class="mdi mdi-walk"></i
|
||||
></a>
|
||||
<a
|
||||
class="button"
|
||||
target="_blank"
|
||||
v-if="addressLinkToRouteByBike"
|
||||
:href="addressLinkToRouteByBike"
|
||||
>
|
||||
<i class="mdi mdi-bike"></i
|
||||
></a>
|
||||
<a
|
||||
class="button"
|
||||
target="_blank"
|
||||
v-if="addressLinkToRouteByTransit"
|
||||
:href="addressLinkToRouteByTransit"
|
||||
>
|
||||
<i class="mdi mdi-bus"></i
|
||||
></a>
|
||||
<a
|
||||
class="button"
|
||||
target="_blank"
|
||||
v-if="addressLinkToRouteByCar"
|
||||
:href="addressLinkToRouteByCar"
|
||||
>
|
||||
<i class="mdi mdi-car"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</b-modal>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { Address } from "@/types/address.model";
|
||||
import { IConfig } from "@/types/config.model";
|
||||
import {
|
||||
EventMetadataKeyType,
|
||||
EventMetadataType,
|
||||
RoutingTransportationType,
|
||||
RoutingType,
|
||||
} from "@/types/enums";
|
||||
import { EventMetadataKeyType, EventMetadataType } from "@/types/enums";
|
||||
import { IEvent } from "@/types/event.model";
|
||||
import { PropType } from "vue";
|
||||
import { Component, Prop, Vue } from "vue-property-decorator";
|
||||
@@ -224,11 +147,13 @@ import EventMetadataBlock from "./EventMetadataBlock.vue";
|
||||
import EventFullDate from "./EventFullDate.vue";
|
||||
import PopoverActorCard from "../Account/PopoverActorCard.vue";
|
||||
import ActorCard from "../../components/Account/ActorCard.vue";
|
||||
import AddressInfo from "../../components/Address/AddressInfo.vue";
|
||||
import {
|
||||
IEventMetadata,
|
||||
IEventMetadataDescription,
|
||||
} from "@/types/event-metadata";
|
||||
import { eventMetaDataList } from "../../services/EventMetadata";
|
||||
import { IUser } from "@/types/current-user.model";
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
@@ -236,15 +161,14 @@ import { eventMetaDataList } from "../../services/EventMetadata";
|
||||
EventFullDate,
|
||||
PopoverActorCard,
|
||||
ActorCard,
|
||||
"map-leaflet": () =>
|
||||
import(/* webpackChunkName: "map" */ "../../components/Map.vue"),
|
||||
AddressInfo,
|
||||
},
|
||||
})
|
||||
export default class EventMetadataSidebar extends Vue {
|
||||
@Prop({ type: Object as PropType<IEvent>, required: true }) event!: IEvent;
|
||||
@Prop({ type: Object as PropType<IConfig>, required: true }) config!: IConfig;
|
||||
|
||||
showMap = false;
|
||||
@Prop({ required: true }) user!: IUser | undefined;
|
||||
@Prop({ required: false, default: false }) showMap!: boolean;
|
||||
|
||||
RouteName = RouteName;
|
||||
|
||||
@@ -255,21 +179,6 @@ export default class EventMetadataSidebar extends Vue {
|
||||
EventMetadataType = EventMetadataType;
|
||||
EventMetadataKeyType = EventMetadataKeyType;
|
||||
|
||||
RoutingParamType = {
|
||||
[RoutingType.OPENSTREETMAP]: {
|
||||
[RoutingTransportationType.FOOT]: "engine=fossgis_osrm_foot",
|
||||
[RoutingTransportationType.BIKE]: "engine=fossgis_osrm_bike",
|
||||
[RoutingTransportationType.TRANSIT]: null,
|
||||
[RoutingTransportationType.CAR]: "engine=fossgis_osrm_car",
|
||||
},
|
||||
[RoutingType.GOOGLE_MAPS]: {
|
||||
[RoutingTransportationType.FOOT]: "dirflg=w",
|
||||
[RoutingTransportationType.BIKE]: "dirflg=b",
|
||||
[RoutingTransportationType.TRANSIT]: "dirflg=r",
|
||||
[RoutingTransportationType.CAR]: "driving",
|
||||
},
|
||||
};
|
||||
|
||||
get physicalAddress(): Address | null {
|
||||
if (!this.event.physicalAddress) return null;
|
||||
|
||||
@@ -286,50 +195,6 @@ export default class EventMetadataSidebar extends Vue {
|
||||
});
|
||||
}
|
||||
|
||||
makeNavigationPath(
|
||||
transportationType: RoutingTransportationType
|
||||
): string | undefined {
|
||||
const geometry = this.physicalAddress?.geom;
|
||||
if (geometry) {
|
||||
const routingType = this.config.maps.routing.type;
|
||||
/**
|
||||
* build urls to routing map
|
||||
*/
|
||||
if (!this.RoutingParamType[routingType][transportationType]) {
|
||||
return;
|
||||
}
|
||||
|
||||
const urlGeometry = geometry.split(";").reverse().join(",");
|
||||
|
||||
switch (routingType) {
|
||||
case RoutingType.GOOGLE_MAPS:
|
||||
return `https://maps.google.com/?saddr=Current+Location&daddr=${urlGeometry}&${this.RoutingParamType[routingType][transportationType]}`;
|
||||
case RoutingType.OPENSTREETMAP:
|
||||
default: {
|
||||
const bboxX = geometry.split(";").reverse()[0];
|
||||
const bboxY = geometry.split(";").reverse()[1];
|
||||
return `https://www.openstreetmap.org/directions?from=&to=${urlGeometry}&${this.RoutingParamType[routingType][transportationType]}#map=14/${bboxX}/${bboxY}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get addressLinkToRouteByCar(): undefined | string {
|
||||
return this.makeNavigationPath(RoutingTransportationType.CAR);
|
||||
}
|
||||
|
||||
get addressLinkToRouteByBike(): undefined | string {
|
||||
return this.makeNavigationPath(RoutingTransportationType.BIKE);
|
||||
}
|
||||
|
||||
get addressLinkToRouteByFeet(): undefined | string {
|
||||
return this.makeNavigationPath(RoutingTransportationType.FOOT);
|
||||
}
|
||||
|
||||
get addressLinkToRouteByTransit(): undefined | string {
|
||||
return this.makeNavigationPath(RoutingTransportationType.TRANSIT);
|
||||
}
|
||||
|
||||
urlToHostname(url: string): string | null {
|
||||
try {
|
||||
return new URL(url).hostname;
|
||||
@@ -362,6 +227,10 @@ export default class EventMetadataSidebar extends Vue {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get userTimezone(): string | undefined {
|
||||
return this.user?.settings?.timezone;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@@ -391,50 +260,6 @@ div.address-wrapper {
|
||||
.map-show-button {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
address {
|
||||
font-style: normal;
|
||||
flex-wrap: wrap;
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
|
||||
span.addressDescription {
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
flex: 1 0 auto;
|
||||
min-width: 100%;
|
||||
max-width: 4rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
:not(.addressDescription) {
|
||||
flex: 1;
|
||||
min-width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.map-modal {
|
||||
.modal-card-head {
|
||||
justify-content: flex-end;
|
||||
button.delete {
|
||||
margin-right: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
section.map {
|
||||
height: calc(100% - 8rem);
|
||||
width: calc(100% - 20px);
|
||||
}
|
||||
|
||||
section.map-footer {
|
||||
p.address {
|
||||
margin: 1rem auto;
|
||||
}
|
||||
div.buttons {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,72 +1,89 @@
|
||||
<template>
|
||||
<div class="address-autocomplete">
|
||||
<b-field
|
||||
:label-for="id"
|
||||
expanded
|
||||
:message="fieldErrors"
|
||||
:type="{ 'is-danger': fieldErrors.length }"
|
||||
>
|
||||
<template slot="label">
|
||||
{{ actualLabel }}
|
||||
<b-button
|
||||
v-if="canShowLocateMeButton && !gettingLocation"
|
||||
size="is-small"
|
||||
icon-right="map-marker"
|
||||
@click="locateMe"
|
||||
:title="$t('Use my location')"
|
||||
/>
|
||||
<span
|
||||
class="is-size-6 has-text-weight-normal"
|
||||
v-else-if="gettingLocation"
|
||||
>{{ $t("Getting location") }}</span
|
||||
>
|
||||
</template>
|
||||
<b-autocomplete
|
||||
:data="addressData"
|
||||
v-model="queryText"
|
||||
:placeholder="$t('e.g. 10 Rue Jangot')"
|
||||
field="fullName"
|
||||
:loading="isFetching"
|
||||
@typing="fetchAsyncData"
|
||||
icon="map-marker"
|
||||
<div class="address-autocomplete columns is-desktop">
|
||||
<div class="column">
|
||||
<b-field
|
||||
:label-for="id"
|
||||
expanded
|
||||
@select="updateSelected"
|
||||
v-bind="$attrs"
|
||||
:id="id"
|
||||
:message="fieldErrors"
|
||||
:type="{ 'is-danger': fieldErrors.length }"
|
||||
>
|
||||
<template #default="{ option }">
|
||||
<b-icon :icon="option.poiInfos.poiIcon.icon" />
|
||||
<b>{{ option.poiInfos.name }}</b
|
||||
><br />
|
||||
<small>{{ option.poiInfos.alternativeName }}</small>
|
||||
<template slot="label">
|
||||
{{ actualLabel }}
|
||||
<b-button
|
||||
v-if="canShowLocateMeButton && !gettingLocation"
|
||||
size="is-small"
|
||||
icon-right="map-marker"
|
||||
@click="locateMe"
|
||||
:title="$t('Use my location')"
|
||||
/>
|
||||
<span
|
||||
class="is-size-6 has-text-weight-normal"
|
||||
v-else-if="gettingLocation"
|
||||
>{{ $t("Getting location") }}</span
|
||||
>
|
||||
</template>
|
||||
<template slot="empty">
|
||||
<span v-if="isFetching">{{ $t("Searching…") }}</span>
|
||||
<div v-else-if="queryText.length >= 3" class="is-enabled">
|
||||
<span>{{ $t('No results for "{queryText}"', { queryText }) }}</span>
|
||||
<span>{{
|
||||
$t(
|
||||
"You can try another search term or drag and drop the marker on the map",
|
||||
{
|
||||
queryText,
|
||||
}
|
||||
)
|
||||
}}</span>
|
||||
<!-- <p class="control" @click="openNewAddressModal">-->
|
||||
<!-- <button type="button" class="button is-primary">{{ $t('Add') }}</button>-->
|
||||
<!-- </p>-->
|
||||
</div>
|
||||
</template>
|
||||
</b-autocomplete>
|
||||
<b-button
|
||||
:disabled="!queryText"
|
||||
@click="resetAddress"
|
||||
class="reset-area"
|
||||
icon-left="close"
|
||||
:title="$t('Clear address field')"
|
||||
/>
|
||||
</b-field>
|
||||
<div class="map" v-if="selected && selected.geom && selected.poiInfos">
|
||||
<b-autocomplete
|
||||
:data="addressData"
|
||||
v-model="queryText"
|
||||
:placeholder="$t('e.g. 10 Rue Jangot')"
|
||||
field="fullName"
|
||||
:loading="isFetching"
|
||||
@typing="fetchAsyncData"
|
||||
icon="map-marker"
|
||||
expanded
|
||||
@select="updateSelected"
|
||||
v-bind="$attrs"
|
||||
:id="id"
|
||||
>
|
||||
<template #default="{ option }">
|
||||
<b-icon :icon="option.poiInfos.poiIcon.icon" />
|
||||
<b>{{ option.poiInfos.name }}</b
|
||||
><br />
|
||||
<small>{{ option.poiInfos.alternativeName }}</small>
|
||||
</template>
|
||||
<template slot="empty">
|
||||
<span v-if="isFetching">{{ $t("Searching…") }}</span>
|
||||
<div v-else-if="queryText.length >= 3" class="is-enabled">
|
||||
<span>{{
|
||||
$t('No results for "{queryText}"', { queryText })
|
||||
}}</span>
|
||||
<span>{{
|
||||
$t(
|
||||
"You can try another search term or drag and drop the marker on the map",
|
||||
{
|
||||
queryText,
|
||||
}
|
||||
)
|
||||
}}</span>
|
||||
<!-- <p class="control" @click="openNewAddressModal">-->
|
||||
<!-- <button type="button" class="button is-primary">{{ $t('Add') }}</button>-->
|
||||
<!-- </p>-->
|
||||
</div>
|
||||
</template>
|
||||
</b-autocomplete>
|
||||
<b-button
|
||||
:disabled="!queryText"
|
||||
@click="resetAddress"
|
||||
class="reset-area"
|
||||
icon-left="close"
|
||||
:title="$t('Clear address field')"
|
||||
/>
|
||||
</b-field>
|
||||
<div class="card" v-if="selected.originId || selected.url">
|
||||
<div class="card-content">
|
||||
<address-info
|
||||
:address="selected"
|
||||
:show-icon="true"
|
||||
:show-timezone="true"
|
||||
:user-timezone="userTimezone"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="map column"
|
||||
v-if="selected && selected.geom && selected.poiInfos"
|
||||
>
|
||||
<map-leaflet
|
||||
:coords="selected.geom"
|
||||
:marker="{
|
||||
@@ -126,14 +143,19 @@ import { Component, Prop, Watch, Mixins } from "vue-property-decorator";
|
||||
import { LatLng } from "leaflet";
|
||||
import { Address, IAddress } from "../../types/address.model";
|
||||
import AddressAutoCompleteMixin from "../../mixins/AddressAutoCompleteMixin";
|
||||
import AddressInfo from "../../components/Address/AddressInfo.vue";
|
||||
|
||||
@Component({
|
||||
inheritAttrs: false,
|
||||
components: {
|
||||
AddressInfo,
|
||||
},
|
||||
})
|
||||
export default class FullAddressAutoComplete extends Mixins(
|
||||
AddressAutoCompleteMixin
|
||||
) {
|
||||
@Prop({ required: false, default: "" }) label!: string;
|
||||
@Prop({ required: false }) userTimezone!: string;
|
||||
|
||||
addressModalActive = false;
|
||||
|
||||
|
||||
@@ -187,7 +187,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue, Watch } from "vue-property-decorator";
|
||||
import { Component, Ref, Vue, Watch } from "vue-property-decorator";
|
||||
import Logo from "@/components/Logo.vue";
|
||||
import { GraphQLError } from "graphql";
|
||||
import { loadLanguageAsync } from "@/utils/i18n";
|
||||
@@ -259,6 +259,13 @@ export default class NavBar extends Vue {
|
||||
|
||||
displayName = displayName;
|
||||
|
||||
@Ref("user-dropdown") userDropDown!: any;
|
||||
|
||||
toggleMenu(): void {
|
||||
console.debug("called toggleMenu");
|
||||
this.userDropDown.showMenu();
|
||||
}
|
||||
|
||||
@Watch("currentActor")
|
||||
async initializeListOfIdentities(): Promise<void> {
|
||||
if (!this.currentUser.isLoggedIn) return;
|
||||
|
||||
@@ -14,10 +14,11 @@ function formatDateString(value: string): string {
|
||||
});
|
||||
}
|
||||
|
||||
function formatTimeString(value: string): string {
|
||||
function formatTimeString(value: string, timeZone: string): string {
|
||||
return parseDateTime(value).toLocaleTimeString(locale(), {
|
||||
hour: "numeric",
|
||||
minute: "numeric",
|
||||
timeZone,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -55,6 +56,7 @@ const SHORT_TIME_FORMAT_OPTIONS: DateTimeFormatOptions = {
|
||||
|
||||
function formatDateTimeString(
|
||||
value: string,
|
||||
timeZone: string | undefined = undefined,
|
||||
showTime = true,
|
||||
dateFormat = "long"
|
||||
): string {
|
||||
@@ -66,6 +68,7 @@ function formatDateTimeString(
|
||||
options = {
|
||||
...options,
|
||||
...(isLongFormat ? LONG_TIME_FORMAT_OPTIONS : SHORT_TIME_FORMAT_OPTIONS),
|
||||
timeZone,
|
||||
};
|
||||
}
|
||||
const format = new Intl.DateTimeFormat(locale(), options);
|
||||
|
||||
@@ -13,6 +13,7 @@ export const ADDRESS_FRAGMENT = gql`
|
||||
type
|
||||
url
|
||||
originId
|
||||
timezone
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
@@ -96,6 +96,31 @@ export const CONFIG = gql`
|
||||
}
|
||||
`;
|
||||
|
||||
export const CONFIG_EDIT_EVENT = gql`
|
||||
query EditEventConfig {
|
||||
config {
|
||||
timezones
|
||||
features {
|
||||
groups
|
||||
}
|
||||
anonymous {
|
||||
participation {
|
||||
allowed
|
||||
validation {
|
||||
email {
|
||||
enabled
|
||||
confirmationRequired
|
||||
}
|
||||
captcha {
|
||||
enabled
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const TERMS = gql`
|
||||
query Terms($locale: String) {
|
||||
config {
|
||||
|
||||
@@ -46,6 +46,7 @@ const EVENT_OPTIONS_FRAGMENT = gql`
|
||||
anonymousParticipation
|
||||
showStartTime
|
||||
showEndTime
|
||||
timezone
|
||||
offers {
|
||||
price
|
||||
priceCurrency
|
||||
|
||||
@@ -147,6 +147,17 @@ export const USER_SETTINGS = gql`
|
||||
${USER_SETTINGS_FRAGMENT}
|
||||
`;
|
||||
|
||||
export const LOGGED_USER_TIMEZONE = gql`
|
||||
query LoggedUserTimezone {
|
||||
loggedUser {
|
||||
id
|
||||
settings {
|
||||
timezone
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const SET_USER_SETTINGS = gql`
|
||||
mutation SetUserSettings(
|
||||
$timezone: String
|
||||
|
||||
@@ -1158,5 +1158,47 @@
|
||||
"Who can post a comment?": "Who can post a comment?",
|
||||
"Does the event needs to be confirmed later or is it cancelled?": "Does the event needs to be confirmed later or is it cancelled?",
|
||||
"When the post is private, you'll need to share the link around.": "When the post is private, you'll need to share the link around.",
|
||||
"Reset": "Reset"
|
||||
}
|
||||
"Reset": "Reset",
|
||||
"Local time ({timezone})": "Local time ({timezone})",
|
||||
"Time in your timezone ({timezone})": "Time in your timezone ({timezone})",
|
||||
"Export": "Export",
|
||||
"Times in your timezone ({timezone})": "Times in your timezone ({timezone})",
|
||||
"Skip to main": "Skip to main",
|
||||
"Comment body": "Comment body",
|
||||
"has loaded": "has loaded",
|
||||
"Follows": "Follows",
|
||||
"Event description body": "Event description body",
|
||||
"Event timezone will default to the timezone of the event's address if there is one, or to your own timezone setting.": "Event timezone will default to the timezone of the event's address if there is one, or to your own timezone setting.",
|
||||
"Clear timezone field": "Clear timezone field",
|
||||
"Group description body": "Group description body",
|
||||
"Moderation logs": "Moderation logs",
|
||||
"Post body": "Post body",
|
||||
"{group} posts": "{group} posts",
|
||||
"{group}'s todolists": "{group}'s todolists",
|
||||
"Validating email": "Validating email",
|
||||
"Redirecting to Mobilizon": "Redirecting to Mobilizon",
|
||||
"Reset password": "Reset password",
|
||||
"First steps": "First steps",
|
||||
"Validating account": "Validating account",
|
||||
"Navigated to {pageTitle}": "Navigated to {pageTitle}",
|
||||
"Confirm participation": "Confirm participation",
|
||||
"Participation with account": "Participation with account",
|
||||
"Participation without account": "Participation without account",
|
||||
"Unlogged participation": "Unlogged participation",
|
||||
"Discussions list": "Discussions list",
|
||||
"Create discussion": "Create discussion",
|
||||
"Tag search": "Tag search",
|
||||
"Homepage": "Homepage",
|
||||
"About instance": "About instance",
|
||||
"Privacy": "Privacy",
|
||||
"Interact": "Interact",
|
||||
"Account settings": "Account settings",
|
||||
"Admin dashboard": "Admin dashboard",
|
||||
"Admin settings": "Admin settings",
|
||||
"Group profiles": "Group profiles",
|
||||
"Reports list": "Reports list",
|
||||
"Create identity": "Create identity",
|
||||
"Resent confirmation email": "Resent confirmation email",
|
||||
"Send password reset": "Send password reset",
|
||||
"Email validate": "Email validate"
|
||||
}
|
||||
@@ -1262,5 +1262,47 @@
|
||||
"{profile} updated the member {member}.": "{profile} a mis à jour le ou la membre {member}.",
|
||||
"{title} ({count} todos)": "{title} ({count} todos)",
|
||||
"{username} was invited to {group}": "{username} a été invité à {group}",
|
||||
"© The OpenStreetMap Contributors": "© Les Contributeur⋅ices OpenStreetMap"
|
||||
}
|
||||
"© The OpenStreetMap Contributors": "© Les Contributeur⋅ices OpenStreetMap",
|
||||
"Local time ({timezone})": "Heure locale ({timezone})",
|
||||
"Time in your timezone ({timezone})": "Heure dans votre fuseau horaire ({timezone})",
|
||||
"Export": "Export",
|
||||
"Times in your timezone ({timezone})": "Heures dans votre fuseau horaire ({timezone})",
|
||||
"has loaded": "a chargé",
|
||||
"Skip to main": "",
|
||||
"Navigated to {pageTitle}": "Navigué vers {pageTitle}",
|
||||
"Comment body": "Corps du commentaire",
|
||||
"Confirm participation": "Confirmer la participation",
|
||||
"Participation with account": "Participation avec compte",
|
||||
"Participation without account": "Participation sans compte",
|
||||
"Unlogged participation": "Participation non connecté⋅e",
|
||||
"Discussions list": "Liste des discussions",
|
||||
"Create discussion": "Créer une discussion",
|
||||
"Tag search": "Recherche par tag",
|
||||
"Homepage": "Page d'accueil",
|
||||
"About instance": "À propos de l'instance",
|
||||
"Privacy": "Vie privée",
|
||||
"Interact": "Interagir",
|
||||
"Redirecting to Mobilizon": "Redirection vers Mobilizon",
|
||||
"First steps": "",
|
||||
"Account settings": "",
|
||||
"Admin dashboard": "",
|
||||
"Admin settings": "",
|
||||
"Group profiles": "",
|
||||
"Reports list": "",
|
||||
"Moderation logs": "",
|
||||
"Create identity": "",
|
||||
"Resent confirmation email": "",
|
||||
"Send password reset": "",
|
||||
"Email validate": "",
|
||||
"Validating account": "",
|
||||
"Follows": "",
|
||||
"Event description body": "",
|
||||
"Event timezone will default to the timezone of the event's address if there is one, or to your own timezone setting.": "Le fuseau horaire de l'événement sera mis par défaut au fuseau horaire de l'addresse de l'événement s'il y en a une, ou bien à votre propre paramètre de fuseau horaire.",
|
||||
"Clear timezone field": "",
|
||||
"Group description body": "",
|
||||
"Post body": "Corps du billet",
|
||||
"{group} posts": "Billets de {group}",
|
||||
"{group}'s todolists": "Liste de tâches de {group}",
|
||||
"Validating email": "",
|
||||
"Reset password": ""
|
||||
}
|
||||
@@ -13,6 +13,7 @@ export interface IAddress {
|
||||
geom?: string;
|
||||
url?: string;
|
||||
originId?: string;
|
||||
timezone?: string;
|
||||
}
|
||||
|
||||
export interface IPoiInfo {
|
||||
@@ -44,20 +45,23 @@ export class Address implements IAddress {
|
||||
|
||||
geom?: string = "";
|
||||
|
||||
timezone?: string = "";
|
||||
|
||||
constructor(hash?: IAddress) {
|
||||
if (!hash) return;
|
||||
|
||||
this.id = hash.id;
|
||||
this.description = hash.description;
|
||||
this.street = hash.street;
|
||||
this.locality = hash.locality;
|
||||
this.postalCode = hash.postalCode;
|
||||
this.region = hash.region;
|
||||
this.country = hash.country;
|
||||
this.description = hash.description?.trim();
|
||||
this.street = hash.street?.trim();
|
||||
this.locality = hash.locality?.trim();
|
||||
this.postalCode = hash.postalCode?.trim();
|
||||
this.region = hash.region?.trim();
|
||||
this.country = hash.country?.trim();
|
||||
this.type = hash.type;
|
||||
this.geom = hash.geom;
|
||||
this.url = hash.url;
|
||||
this.originId = hash.originId;
|
||||
this.timezone = hash.timezone;
|
||||
}
|
||||
|
||||
get poiInfos(): IPoiInfo {
|
||||
|
||||
@@ -26,6 +26,7 @@ export interface IEventOptions {
|
||||
showParticipationPrice: boolean;
|
||||
showStartTime: boolean;
|
||||
showEndTime: boolean;
|
||||
timezone: string | null;
|
||||
}
|
||||
|
||||
export class EventOptions implements IEventOptions {
|
||||
@@ -54,4 +55,6 @@ export class EventOptions implements IEventOptions {
|
||||
showStartTime = true;
|
||||
|
||||
showEndTime = true;
|
||||
|
||||
timezone = null;
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ interface IEventEditJSON {
|
||||
id?: string;
|
||||
title: string;
|
||||
description: string;
|
||||
beginsOn: string;
|
||||
beginsOn: string | null;
|
||||
endsOn: string | null;
|
||||
status: EventStatus;
|
||||
visibility: EventVisibility;
|
||||
@@ -92,6 +92,9 @@ export interface IEvent {
|
||||
toEditJSON(): IEventEditJSON;
|
||||
}
|
||||
|
||||
export interface IEditableEvent extends Omit<IEvent, "beginsOn"> {
|
||||
beginsOn: Date | null;
|
||||
}
|
||||
export class EventModel implements IEvent {
|
||||
id?: string;
|
||||
|
||||
@@ -158,7 +161,7 @@ export class EventModel implements IEvent {
|
||||
|
||||
metadata: IEventMetadata[] = [];
|
||||
|
||||
constructor(hash?: IEvent) {
|
||||
constructor(hash?: IEvent | IEditableEvent) {
|
||||
if (!hash) return;
|
||||
|
||||
this.id = hash.id;
|
||||
@@ -170,8 +173,14 @@ export class EventModel implements IEvent {
|
||||
this.slug = hash.slug;
|
||||
this.description = hash.description || "";
|
||||
|
||||
this.beginsOn = new Date(hash.beginsOn);
|
||||
if (hash.endsOn) this.endsOn = new Date(hash.endsOn);
|
||||
if (hash.beginsOn) {
|
||||
this.beginsOn = new Date(hash.beginsOn);
|
||||
}
|
||||
if (hash.endsOn) {
|
||||
this.endsOn = new Date(hash.endsOn);
|
||||
} else {
|
||||
this.endsOn = null;
|
||||
}
|
||||
|
||||
this.publishAt = new Date(hash.publishAt);
|
||||
|
||||
@@ -217,12 +226,12 @@ export function removeTypeName(entity: any): any {
|
||||
return entity;
|
||||
}
|
||||
|
||||
export function toEditJSON(event: IEvent): IEventEditJSON {
|
||||
export function toEditJSON(event: IEditableEvent): IEventEditJSON {
|
||||
return {
|
||||
id: event.id,
|
||||
title: event.title,
|
||||
description: event.description,
|
||||
beginsOn: event.beginsOn.toISOString(),
|
||||
beginsOn: event.beginsOn ? event.beginsOn.toISOString() : null,
|
||||
endsOn: event.endsOn ? event.endsOn.toISOString() : null,
|
||||
status: event.status,
|
||||
visibility: event.visibility,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user