Allow to add metadata to an event
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
@@ -2,7 +2,18 @@
|
||||
<div>
|
||||
<h2>{{ title }}</h2>
|
||||
<div class="eventMetadataBlock">
|
||||
<b-icon v-if="icon" :icon="icon" size="is-medium" />
|
||||
<!-- Custom icons -->
|
||||
<span
|
||||
class="icon is-medium"
|
||||
v-if="icon && icon.substring(0, 7) === 'mz:icon'"
|
||||
>
|
||||
<img
|
||||
:src="`/img/${icon.substring(8)}_monochrome.svg`"
|
||||
width="32"
|
||||
height="32"
|
||||
/>
|
||||
</span>
|
||||
<b-icon v-else-if="icon" :icon="icon" size="is-medium" />
|
||||
<p :class="{ 'padding-left': icon }">
|
||||
<slot></slot>
|
||||
</p>
|
||||
@@ -36,6 +47,13 @@ div.eventMetadataBlock {
|
||||
|
||||
&.padding-left {
|
||||
padding: 0 20px;
|
||||
|
||||
a {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
140
js/src/components/Event/EventMetadataItem.vue
Normal file
140
js/src/components/Event/EventMetadataItem.vue
Normal file
@@ -0,0 +1,140 @@
|
||||
<template>
|
||||
<div class="card card-content">
|
||||
<div class="media">
|
||||
<div class="media-left">
|
||||
<img
|
||||
v-if="
|
||||
metadataItem.icon && metadataItem.icon.substring(0, 7) === 'mz:icon'
|
||||
"
|
||||
:src="`/img/${metadataItem.icon.substring(8)}_monochrome.svg`"
|
||||
width="24"
|
||||
height="24"
|
||||
/>
|
||||
|
||||
<b-icon v-else-if="metadataItem.icon" :icon="metadataItem.icon" />
|
||||
<b-icon v-else icon="help-circle" />
|
||||
</div>
|
||||
<div class="media-content">
|
||||
<b>{{ metadataItem.title || metadataItem.label }}</b>
|
||||
<br />
|
||||
<small>
|
||||
{{ metadataItem.description }}
|
||||
</small>
|
||||
<div
|
||||
v-if="
|
||||
metadataItem.type === EventMetadataType.STRING &&
|
||||
metadataItem.keyType === EventMetadataKeyType.CHOICE &&
|
||||
metadataItem.choices
|
||||
"
|
||||
>
|
||||
<b-field v-for="(value, key) in metadataItem.choices" :key="key">
|
||||
<b-radio v-model="metadataItemValue" :native-value="key">{{
|
||||
value
|
||||
}}</b-radio>
|
||||
</b-field>
|
||||
</div>
|
||||
<b-field
|
||||
v-else-if="
|
||||
metadataItem.type === EventMetadataType.STRING &&
|
||||
metadataItem.keyType == EventMetadataKeyType.URL
|
||||
"
|
||||
>
|
||||
<b-input
|
||||
@blur="validatePattern"
|
||||
ref="urlInput"
|
||||
type="url"
|
||||
:pattern="
|
||||
metadataItem.pattern ? metadataItem.pattern.source : undefined
|
||||
"
|
||||
:validation-message="$t(`This URL doesn't seem to be valid`)"
|
||||
required
|
||||
v-model="metadataItemValue"
|
||||
:placeholder="metadataItem.placeholder"
|
||||
/>
|
||||
</b-field>
|
||||
<b-field v-else-if="metadataItem.type === EventMetadataType.STRING">
|
||||
<b-input
|
||||
v-model="metadataItemValue"
|
||||
:placeholder="metadataItem.placeholder"
|
||||
/>
|
||||
</b-field>
|
||||
<b-field v-else-if="metadataItem.type === EventMetadataType.INTEGER">
|
||||
<b-numberinput v-model="metadataItemValue" />
|
||||
</b-field>
|
||||
<b-field v-else-if="metadataItem.type === EventMetadataType.BOOLEAN">
|
||||
<b-checkbox v-model="metadataItemValue">
|
||||
{{
|
||||
metadataItemValue === "true"
|
||||
? metadataItem.choices["true"]
|
||||
: metadataItem.choices["false"]
|
||||
}}
|
||||
</b-checkbox>
|
||||
</b-field>
|
||||
</div>
|
||||
<b-button
|
||||
icon-left="close"
|
||||
@click="$emit('removeItem', metadataItem.key)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { EventMetadataKeyType, EventMetadataType } from "@/types/enums";
|
||||
import { IEventMetadataDescription } from "@/types/event-metadata";
|
||||
import { PropType } from "vue";
|
||||
import { Component, Prop, Ref, Vue } from "vue-property-decorator";
|
||||
|
||||
@Component
|
||||
export default class EventMetadataItem extends Vue {
|
||||
@Prop({ type: Object as PropType<IEventMetadataDescription>, required: true })
|
||||
value!: IEventMetadataDescription;
|
||||
|
||||
EventMetadataType = EventMetadataType;
|
||||
EventMetadataKeyType = EventMetadataKeyType;
|
||||
|
||||
@Ref("urlInput") readonly urlInput!: any;
|
||||
|
||||
get metadataItem(): IEventMetadataDescription {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
get metadataItemValue(): string {
|
||||
return this.metadataItem.value;
|
||||
}
|
||||
|
||||
set metadataItemValue(value: string) {
|
||||
if (this.validate(value)) {
|
||||
this.$emit("input", { ...this.metadataItem, value: value.toString() });
|
||||
}
|
||||
}
|
||||
|
||||
validatePattern(): void {
|
||||
this.urlInput.checkHtml5Validity();
|
||||
}
|
||||
|
||||
private validate(value: string): boolean {
|
||||
if (this.metadataItem.keyType === EventMetadataKeyType.URL) {
|
||||
try {
|
||||
const url = new URL(value);
|
||||
if (!["http:", "https:", "mailto:"].includes(url.protocol))
|
||||
return false;
|
||||
if (this.metadataItem.pattern) {
|
||||
return value.match(this.metadataItem.pattern) !== null;
|
||||
}
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.card .media {
|
||||
align-items: center;
|
||||
|
||||
& > button {
|
||||
margin-left: 1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
206
js/src/components/Event/EventMetadataList.vue
Normal file
206
js/src/components/Event/EventMetadataList.vue
Normal file
@@ -0,0 +1,206 @@
|
||||
<template>
|
||||
<section>
|
||||
<div class="mb-4">
|
||||
<div v-for="(item, index) in metadata" :key="item.key" class="my-2">
|
||||
<event-metadata-item
|
||||
:value="metadata[index]"
|
||||
@input="updateSingleMetadata"
|
||||
@removeItem="removeItem"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<b-field grouped :label="$t('Find or add an element')">
|
||||
<b-autocomplete
|
||||
expanded
|
||||
v-model="search"
|
||||
ref="autocomplete"
|
||||
:data="filteredDataArray"
|
||||
group-field="category"
|
||||
group-options="items"
|
||||
open-on-focus
|
||||
:placeholder="$t('e.g. Accessibility, Twitch, PeerTube')"
|
||||
@select="(option) => addElement(option)"
|
||||
>
|
||||
<template slot-scope="props">
|
||||
<div class="media">
|
||||
<div class="media-left">
|
||||
<img
|
||||
v-if="
|
||||
props.option.icon &&
|
||||
props.option.icon.substring(0, 7) === 'mz:icon'
|
||||
"
|
||||
:src="`/img/${props.option.icon.substring(8)}_monochrome.svg`"
|
||||
width="24"
|
||||
height="24"
|
||||
/>
|
||||
<b-icon v-else-if="props.option.icon" :icon="props.option.icon" />
|
||||
<b-icon v-else icon="help-circle" />
|
||||
</div>
|
||||
<div class="media-content">
|
||||
<b>{{ props.option.label }}</b>
|
||||
<br />
|
||||
<small>
|
||||
{{ props.option.description }}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #empty>{{
|
||||
$t("No results for {search}", { search })
|
||||
}}</template>
|
||||
</b-autocomplete>
|
||||
<p class="control">
|
||||
<b-button @click="showNewElementModal = true">
|
||||
{{ $t("Add new…") }}
|
||||
</b-button>
|
||||
</p>
|
||||
</b-field>
|
||||
<b-modal has-modal-card v-model="showNewElementModal">
|
||||
<div class="modal-card">
|
||||
<header class="modal-card-head">
|
||||
<button
|
||||
type="button"
|
||||
class="delete"
|
||||
@click="showNewElementModal = false"
|
||||
/>
|
||||
</header>
|
||||
<div class="modal-card-body">
|
||||
<form @submit="addNewElement">
|
||||
<b-field :label="$t('Element title')">
|
||||
<b-input v-model="newElement.title" />
|
||||
</b-field>
|
||||
<b-field :label="$t('Element value')">
|
||||
<b-input v-model="newElement.value" />
|
||||
</b-field>
|
||||
<b-button type="is-primary" native-type="submit">{{
|
||||
$t("Add")
|
||||
}}</b-button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</b-modal>
|
||||
</section>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import {
|
||||
IEventMetadata,
|
||||
IEventMetadataDescription,
|
||||
} from "@/types/event-metadata";
|
||||
import cloneDeep from "lodash/cloneDeep";
|
||||
import { PropType } from "vue";
|
||||
import { Component, Prop, Vue } from "vue-property-decorator";
|
||||
import EventMetadataItem from "./EventMetadataItem.vue";
|
||||
import { eventMetaDataList } from "../../services/EventMetadata";
|
||||
import { EventMetadataCategories, EventMetadataType } from "@/types/enums";
|
||||
|
||||
type GroupedIEventMetadata = Array<{
|
||||
category: string;
|
||||
items: IEventMetadata[];
|
||||
}>;
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
EventMetadataItem,
|
||||
},
|
||||
})
|
||||
export default class EventMetadataList extends Vue {
|
||||
@Prop({ type: Array as PropType<Array<IEventMetadata>>, required: true })
|
||||
value!: IEventMetadata[];
|
||||
|
||||
newElement = {
|
||||
title: "",
|
||||
value: "",
|
||||
};
|
||||
|
||||
search = "";
|
||||
|
||||
data: IEventMetadataDescription[] = eventMetaDataList;
|
||||
|
||||
showNewElementModal = false;
|
||||
|
||||
get metadata(): IEventMetadata[] {
|
||||
return this.value.map((val) => {
|
||||
const def = this.data.find((dat) => dat.key === val.key);
|
||||
return {
|
||||
...def,
|
||||
...val,
|
||||
};
|
||||
}) as any[];
|
||||
}
|
||||
|
||||
set metadata(metadata: IEventMetadata[]) {
|
||||
this.$emit("input", metadata);
|
||||
}
|
||||
|
||||
localizedCategories: Record<EventMetadataCategories, string> = {
|
||||
[EventMetadataCategories.ACCESSIBILITY]: this.$t("Accessibility") as string,
|
||||
[EventMetadataCategories.LIVE]: this.$t("Live") as string,
|
||||
[EventMetadataCategories.REPLAY]: this.$t("Replay") as string,
|
||||
[EventMetadataCategories.TOOLS]: this.$t("Tools") as string,
|
||||
[EventMetadataCategories.SOCIAL]: this.$t("Social") as string,
|
||||
[EventMetadataCategories.DETAILS]: this.$t("Details") as string,
|
||||
[EventMetadataCategories.BOOKING]: this.$t("Booking") as string,
|
||||
};
|
||||
|
||||
get filteredDataArray(): GroupedIEventMetadata {
|
||||
return this.data
|
||||
.filter((option) => {
|
||||
return (
|
||||
option.label
|
||||
.toString()
|
||||
.toLowerCase()
|
||||
.indexOf(this.search.toLowerCase()) >= 0
|
||||
);
|
||||
})
|
||||
.filter(({ key }) => {
|
||||
return !this.metadata.map(({ key: key2 }) => key2).includes(key);
|
||||
})
|
||||
.reduce(
|
||||
(acc: GroupedIEventMetadata, current: IEventMetadataDescription) => {
|
||||
const group = acc.find(
|
||||
(elem) =>
|
||||
elem.category === this.localizedCategories[current.category]
|
||||
);
|
||||
if (group) {
|
||||
group.items.push(current);
|
||||
} else {
|
||||
acc.push({
|
||||
category: this.localizedCategories[current.category],
|
||||
items: [current],
|
||||
});
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
[]
|
||||
);
|
||||
}
|
||||
|
||||
updateSingleMetadata(element: IEventMetadataDescription): void {
|
||||
const metadataClone = cloneDeep(this.metadata);
|
||||
const index = metadataClone.findIndex((elem) => elem.key === element.key);
|
||||
metadataClone.splice(index, 1, element);
|
||||
this.$emit("input", metadataClone);
|
||||
}
|
||||
|
||||
removeItem(itemKey: string): void {
|
||||
const metadataClone = cloneDeep(this.metadata);
|
||||
const index = metadataClone.findIndex((elem) => elem.key === itemKey);
|
||||
metadataClone.splice(index, 1);
|
||||
this.$emit("input", metadataClone);
|
||||
}
|
||||
|
||||
addElement(element: IEventMetadata): void {
|
||||
this.metadata = [...this.metadata, element];
|
||||
}
|
||||
|
||||
addNewElement(e: Event): void {
|
||||
e.preventDefault();
|
||||
this.addElement({
|
||||
...this.newElement,
|
||||
type: EventMetadataType.STRING,
|
||||
key: `mz:plain:${(Math.random() + 1).toString(36).substring(7)}`,
|
||||
});
|
||||
this.showNewElementModal = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
450
js/src/components/Event/EventMetadataSidebar.vue
Normal file
450
js/src/components/Event/EventMetadataSidebar.vue
Normal file
@@ -0,0 +1,450 @@
|
||||
<template>
|
||||
<div>
|
||||
<event-metadata-block
|
||||
:title="$t('Location')"
|
||||
:icon="physicalAddress ? physicalAddress.poiInfos.poiIcon.icon : 'earth'"
|
||||
>
|
||||
<div class="address-wrapper">
|
||||
<span v-if="!physicalAddress">{{ $t("No address defined") }}</span>
|
||||
<div class="address" v-if="physicalAddress">
|
||||
<div>
|
||||
<address>
|
||||
<p
|
||||
class="addressDescription"
|
||||
:title="physicalAddress.poiInfos.name"
|
||||
>
|
||||
{{ physicalAddress.poiInfos.name }}
|
||||
</p>
|
||||
<p class="has-text-grey-dark">
|
||||
{{ physicalAddress.poiInfos.alternativeName }}
|
||||
</p>
|
||||
</address>
|
||||
</div>
|
||||
<span
|
||||
class="map-show-button"
|
||||
@click="showMap = !showMap"
|
||||
v-if="physicalAddress.geom"
|
||||
>{{ $t("Show map") }}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</event-metadata-block>
|
||||
<event-metadata-block :title="$t('Date and time')" icon="calendar">
|
||||
<event-full-date
|
||||
:beginsOn="event.beginsOn"
|
||||
:show-start-time="event.options.showStartTime"
|
||||
:show-end-time="event.options.showEndTime"
|
||||
:endsOn="event.endsOn"
|
||||
/>
|
||||
</event-metadata-block>
|
||||
<event-metadata-block
|
||||
class="metadata-organized-by"
|
||||
:title="$t('Organized by')"
|
||||
>
|
||||
<popover-actor-card
|
||||
:actor="event.organizerActor"
|
||||
v-if="!event.attributedTo"
|
||||
>
|
||||
<actor-card :actor="event.organizerActor" />
|
||||
</popover-actor-card>
|
||||
<router-link
|
||||
v-if="event.attributedTo"
|
||||
:to="{
|
||||
name: RouteName.GROUP,
|
||||
params: {
|
||||
preferredUsername: usernameWithDomain(event.attributedTo),
|
||||
},
|
||||
}"
|
||||
>
|
||||
<popover-actor-card
|
||||
:actor="event.attributedTo"
|
||||
v-if="
|
||||
!event.attributedTo || !event.options.hideOrganizerWhenGroupEvent
|
||||
"
|
||||
>
|
||||
<actor-card :actor="event.attributedTo" />
|
||||
</popover-actor-card>
|
||||
</router-link>
|
||||
|
||||
<popover-actor-card
|
||||
:actor="contact"
|
||||
v-for="contact in event.contacts"
|
||||
:key="contact.id"
|
||||
>
|
||||
<actor-card :actor="contact" />
|
||||
</popover-actor-card>
|
||||
</event-metadata-block>
|
||||
<event-metadata-block
|
||||
v-if="event.onlineAddress && urlToHostname(event.onlineAddress)"
|
||||
icon="link"
|
||||
:title="$t('Website')"
|
||||
>
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer ugc"
|
||||
:href="event.onlineAddress"
|
||||
:title="
|
||||
$t('View page on {hostname} (in a new window)', {
|
||||
hostname: urlToHostname(event.onlineAddress),
|
||||
})
|
||||
"
|
||||
>{{ simpleURL(event.onlineAddress) }}</a
|
||||
>
|
||||
</event-metadata-block>
|
||||
<event-metadata-block
|
||||
v-for="extra in extraMetadata"
|
||||
:title="extra.title || extra.label"
|
||||
:icon="extra.icon"
|
||||
:key="extra.key"
|
||||
>
|
||||
<span
|
||||
v-if="
|
||||
((extra.type == EventMetadataType.STRING &&
|
||||
extra.keyType == EventMetadataKeyType.CHOICE) ||
|
||||
extra.type === EventMetadataType.BOOLEAN) &&
|
||||
extra.choices &&
|
||||
extra.choices[extra.value]
|
||||
"
|
||||
>
|
||||
{{ extra.choices[extra.value] }}
|
||||
</span>
|
||||
<a
|
||||
v-else-if="
|
||||
extra.type == EventMetadataType.STRING &&
|
||||
extra.keyType == EventMetadataKeyType.URL
|
||||
"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer ugc"
|
||||
:href="extra.value"
|
||||
:title="
|
||||
$t('View page on {hostname} (in a new window)', {
|
||||
hostname: urlToHostname(extra.value),
|
||||
})
|
||||
"
|
||||
>{{ simpleURL(extra.value) }}</a
|
||||
>
|
||||
<a
|
||||
v-else-if="
|
||||
extra.type == EventMetadataType.STRING &&
|
||||
extra.keyType == EventMetadataKeyType.HANDLE
|
||||
"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer ugc"
|
||||
:href="accountURL(extra)"
|
||||
:title="
|
||||
$t('View account on {hostname} (in a new window)', {
|
||||
hostname: urlToHostname(accountURL(extra)),
|
||||
})
|
||||
"
|
||||
>{{ extra.value }}</a
|
||||
>
|
||||
<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 { IEvent } from "@/types/event.model";
|
||||
import { PropType } from "vue";
|
||||
import { Component, Prop, Vue } from "vue-property-decorator";
|
||||
import RouteName from "../../router/name";
|
||||
import { usernameWithDomain } from "../../types/actor";
|
||||
import EventMetadataBlock from "./EventMetadataBlock.vue";
|
||||
import EventFullDate from "./EventFullDate.vue";
|
||||
import PopoverActorCard from "../Account/PopoverActorCard.vue";
|
||||
import ActorCard from "../../components/Account/ActorCard.vue";
|
||||
import {
|
||||
IEventMetadata,
|
||||
IEventMetadataDescription,
|
||||
} from "@/types/event-metadata";
|
||||
import { eventMetaDataList } from "../../services/EventMetadata";
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
EventMetadataBlock,
|
||||
EventFullDate,
|
||||
PopoverActorCard,
|
||||
ActorCard,
|
||||
"map-leaflet": () =>
|
||||
import(/* webpackChunkName: "map" */ "../../components/Map.vue"),
|
||||
},
|
||||
})
|
||||
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;
|
||||
|
||||
RouteName = RouteName;
|
||||
|
||||
usernameWithDomain = usernameWithDomain;
|
||||
|
||||
eventMetaDataList = eventMetaDataList;
|
||||
|
||||
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;
|
||||
|
||||
return new Address(this.event.physicalAddress);
|
||||
}
|
||||
|
||||
get extraMetadata(): IEventMetadata[] {
|
||||
return this.event.metadata.map((val) => {
|
||||
const def = eventMetaDataList.find((dat) => dat.key === val.key);
|
||||
return {
|
||||
...def,
|
||||
...val,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
simpleURL(url: string): string | null {
|
||||
try {
|
||||
const uri = new URL(url);
|
||||
return `${this.removeWWW(uri.hostname)}${uri.pathname}${uri.search}${
|
||||
uri.hash
|
||||
}`;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private removeWWW(string: string): string {
|
||||
return string.replace(/^www./, "");
|
||||
}
|
||||
|
||||
accountURL(extra: IEventMetadataDescription): string | undefined {
|
||||
switch (extra.key) {
|
||||
case "mz:social:twitter:account": {
|
||||
const handle =
|
||||
extra.value[0] === "@" ? extra.value.slice(1) : extra.value;
|
||||
return `https://twitter.com/${handle}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
::v-deep .metadata-organized-by {
|
||||
.v-popover.popover .trigger {
|
||||
width: 100%;
|
||||
.media-content {
|
||||
width: calc(100% - 32px - 1rem);
|
||||
max-width: 80vw;
|
||||
|
||||
p.has-text-grey-dark {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
div.address-wrapper {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-wrap: wrap;
|
||||
|
||||
div.address {
|
||||
flex: 1;
|
||||
|
||||
.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>
|
||||
55
js/src/components/Event/Integrations/PeerTube.vue
Normal file
55
js/src/components/Event/Integrations/PeerTube.vue
Normal file
@@ -0,0 +1,55 @@
|
||||
<template>
|
||||
<div class="peertube">
|
||||
<div class="peertube-video" v-if="videoDetails">
|
||||
<iframe
|
||||
width="100%"
|
||||
height="100%"
|
||||
sandbox="allow-same-origin allow-scripts allow-popups"
|
||||
:src="`https://${videoDetails.host}/videos/embed/${videoDetails.uuid}`"
|
||||
frameborder="0"
|
||||
allowfullscreen
|
||||
></iframe>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { IEventMetadataDescription } from "@/types/event-metadata";
|
||||
import { PropType } from "vue";
|
||||
import { Component, Prop, Vue } from "vue-property-decorator";
|
||||
|
||||
@Component
|
||||
export default class PeerTubeIntegration extends Vue {
|
||||
@Prop({ type: Object as PropType<IEventMetadataDescription>, required: true })
|
||||
metadata!: IEventMetadataDescription;
|
||||
|
||||
get videoDetails(): { host: string; uuid: string } | null {
|
||||
if (this.metadata.pattern) {
|
||||
const matches = this.metadata.pattern.exec(this.metadata.value);
|
||||
if (matches && matches[1] && matches[2]) {
|
||||
return { host: matches[1], uuid: matches[2] };
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
get origin(): string {
|
||||
return window.location.hostname;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.peertube {
|
||||
.peertube-video {
|
||||
padding-top: 56.25%;
|
||||
position: relative;
|
||||
height: 0;
|
||||
|
||||
iframe {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
56
js/src/components/Event/Integrations/Twitch.vue
Normal file
56
js/src/components/Event/Integrations/Twitch.vue
Normal file
@@ -0,0 +1,56 @@
|
||||
<template>
|
||||
<div class="twitch">
|
||||
<div class="twitch-video" v-if="channelName">
|
||||
<iframe
|
||||
:src="`https://player.twitch.tv/?channel=${channelName}&parent=${origin}&autoplay=false`"
|
||||
frameborder="0"
|
||||
scrolling="no"
|
||||
allowfullscreen="true"
|
||||
height="100%"
|
||||
width="100%"
|
||||
>
|
||||
</iframe>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { IEventMetadataDescription } from "@/types/event-metadata";
|
||||
import { PropType } from "vue";
|
||||
import { Component, Prop, Vue } from "vue-property-decorator";
|
||||
|
||||
@Component
|
||||
export default class TwitchIntegration extends Vue {
|
||||
@Prop({ type: Object as PropType<IEventMetadataDescription>, required: true })
|
||||
metadata!: IEventMetadataDescription;
|
||||
|
||||
get channelName(): string | null {
|
||||
if (this.metadata.pattern) {
|
||||
const matches = this.metadata.pattern.exec(this.metadata.value);
|
||||
if (matches && matches[1]) {
|
||||
return matches[1];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
get origin(): string {
|
||||
return window.location.hostname;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.twitch {
|
||||
.twitch-video {
|
||||
padding-top: 56.25%;
|
||||
position: relative;
|
||||
height: 0;
|
||||
|
||||
iframe {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
56
js/src/components/Event/Integrations/YouTube.vue
Normal file
56
js/src/components/Event/Integrations/YouTube.vue
Normal file
@@ -0,0 +1,56 @@
|
||||
<template>
|
||||
<div class="youtube">
|
||||
<div class="youtube-video" v-if="videoID">
|
||||
<iframe
|
||||
width="100%"
|
||||
height="100%"
|
||||
:src="`https://www.youtube.com/embed/${videoID}`"
|
||||
title="YouTube video player"
|
||||
frameborder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowfullscreen
|
||||
></iframe>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { IEventMetadataDescription } from "@/types/event-metadata";
|
||||
import { PropType } from "vue";
|
||||
import { Component, Prop, Vue } from "vue-property-decorator";
|
||||
|
||||
@Component
|
||||
export default class YouTubeIntegration extends Vue {
|
||||
@Prop({ type: Object as PropType<IEventMetadataDescription>, required: true })
|
||||
metadata!: IEventMetadataDescription;
|
||||
|
||||
get videoID(): string | null {
|
||||
if (this.metadata.pattern) {
|
||||
const matches = this.metadata.pattern.exec(this.metadata.value);
|
||||
if (matches && matches[1]) {
|
||||
return matches[1];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
get origin(): string {
|
||||
return window.location.hostname;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.youtube {
|
||||
.youtube-video {
|
||||
padding-top: 56.25%;
|
||||
position: relative;
|
||||
height: 0;
|
||||
|
||||
iframe {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user