forked from potsda.mn/mobilizon
Merge branch 'event-metadata' into 'master'
Allow to add metadata to an event Closes #410 See merge request framasoft/mobilizon!1017
This commit is contained in:
commit
887ac38b96
|
@ -201,7 +201,7 @@ pages:
|
|||
- mkdir -p /kaniko/.docker
|
||||
- echo "{\"auths\":{\"$CI_REGISTRY\":{\"auth\":\"$CI_REGISTRY_AUTH\",\"email\":\"$CI_REGISTRY_EMAIL\"}}}" > /kaniko/.docker/config.json
|
||||
script:
|
||||
- /kaniko/executor --cache=true --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/docker/production/Dockerfile --destination $DOCKER_IMAGE_NAME --build-arg VCS_REF=$CI_VCS_REF --build-arg BUILD_DATE=$CI_JOB_TIMESTAMP
|
||||
- /kaniko/executor --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/docker/production/Dockerfile --destination $DOCKER_IMAGE_NAME --build-arg VCS_REF=$CI_VCS_REF --build-arg BUILD_DATE=$CI_JOB_TIMESTAMP
|
||||
|
||||
build-docker-master:
|
||||
<<: *docker
|
||||
|
|
1
js/public/img/fediverse_monochrome.svg
Normal file
1
js/public/img/fediverse_monochrome.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="742.753" height="742.753" viewBox="0 0 557.065 557.065"><path style="stroke:none;fill-rule:nonzero;fill:#636363;fill-opacity:1" d="M135.848 206.352c-4.887 9.359-12.715 17.152-22.098 21.996L235.066 350.14l29.25-14.825zm160.023 160.64-29.25 14.824 61.473 61.711c4.886-9.359 12.719-17.156 22.105-21.996zm0 0"/><path style="stroke:none;fill-rule:nonzero;fill:#878787;fill-opacity:1" d="m436.234 254.543-68.68 34.809 5.063 32.39 77.711-39.383c-7.39-7.543-12.387-17.398-14.094-27.816zM327.68 309.559l-162.39 82.3c7.39 7.54 12.386 17.395 14.093 27.817l153.363-77.727zm0 0"/><path style="stroke:none;fill-rule:nonzero;fill:#636363;fill-opacity:1" d="m275.457 106.828-78.36 152.977 23.133 23.226 82.97-161.969c-10.41-1.761-20.243-6.804-27.743-14.234zm-98.742 192.766-39.692 77.488c10.41 1.758 20.239 6.805 27.743 14.23l35.086-68.496zm0 0"/><path style="stroke:none;fill-rule:nonzero;fill:#5c5c5c;fill-opacity:1" d="M113.074 228.688a51.922 51.922 0 0 1-25.808 5.398 52.012 52.012 0 0 1-4.989-.524l23.176 148.247a51.976 51.976 0 0 1 25.813-5.395c1.668.094 3.332.266 4.984.52zm0 0"/><path style="stroke:none;fill-rule:nonzero;fill:#575757;fill-opacity:1" d="M179.508 420.41c.527 3.438.71 6.93.539 10.406a51.888 51.888 0 0 1-5.45 20.387l148.22 23.781a51.814 51.814 0 0 1-.54-10.406 51.852 51.852 0 0 1 5.45-20.383zm0 0"/><path style="stroke:none;fill-rule:nonzero;fill:#878787;fill-opacity:1" d="m450.852 282.898-68.414 133.563c10.41 1.762 20.242 6.805 27.742 14.238l68.414-133.562c-10.41-1.762-20.242-6.805-27.742-14.239zm0 0"/><path style="stroke:none;fill-rule:nonzero;fill:#ccc;fill-opacity:1" d="M357.543 93.996c-4.887 9.363-12.719 17.156-22.106 22l105.95 106.36c4.886-9.36 12.718-17.157 22.101-22zm0 0"/><path style="stroke:none;fill-rule:nonzero;fill:#8f8f8f;fill-opacity:1" d="m260.84 78.473-133.93 67.875c7.39 7.539 12.383 17.394 14.094 27.812l133.93-67.875c-7.391-7.539-12.387-17.394-14.094-27.812zm74.355 37.648a52.01 52.01 0 0 1-26.238 5.61 51.5 51.5 0 0 1-4.52-.473l11.864 75.969 32.37 5.191zm-12 125.27 28.051 179.613a51.909 51.909 0 0 1 25.434-5.211c1.812.105 3.617.3 5.406.594l-26.52-169.805zm0 0"/><path style="stroke:none;fill-rule:nonzero;fill:#ababab;fill-opacity:1" d="M141.098 174.73a51.84 51.84 0 0 1 .57 10.575 51.878 51.878 0 0 1-5.371 20.234l76.027 12.211 14.942-29.18zm130.304 20.926-14.945 29.184 179.633 28.85a51.828 51.828 0 0 1-.52-10.289 51.863 51.863 0 0 1 5.512-20.492zm0 0"/><path style="stroke:none;fill-rule:nonzero;fill:#c2c2c2;fill-opacity:.995968" d="M358.672 72.691c-1.414 25.907-23.555 45.762-49.461 44.348-25.902-1.41-45.758-23.55-44.348-49.457 1.414-25.902 23.555-45.758 49.461-44.348 25.903 1.414 45.758 23.555 44.348 49.457zm0 0"/><path style="stroke:none;fill-rule:nonzero;fill:#b3b3b3;fill-opacity:.995968" d="M534.066 248.766c-1.41 25.906-23.554 45.761-49.457 44.347-25.906-1.41-45.761-23.55-44.347-49.457 1.41-25.902 23.55-45.758 49.457-44.347 25.902 1.41 45.758 23.554 44.347 49.457zm0 0"/><path style="stroke:none;fill-rule:nonzero;fill:#7d7d7d;fill-opacity:.995968" d="M420.773 469.941c-1.414 25.903-23.554 45.758-49.457 44.348-25.906-1.41-45.761-23.555-44.351-49.457 1.414-25.902 23.555-45.758 49.46-44.348 25.903 1.41 45.759 23.555 44.348 49.457zm0 0"/><path style="stroke:none;fill-rule:nonzero;fill:#4a4a4a;fill-opacity:.995968" d="M175.355 430.563c-1.41 25.902-23.55 45.757-49.457 44.347-25.902-1.414-45.757-23.555-44.347-49.457 1.414-25.906 23.554-45.762 49.457-44.351 25.906 1.414 45.762 23.554 44.347 49.46zm0 0"/><path style="stroke:none;fill-rule:nonzero;fill:#4d4d4d;fill-opacity:.995968" d="M136.977 185.047c-1.41 25.902-23.555 45.758-49.457 44.348-25.907-1.41-45.758-23.555-44.348-49.458 1.41-25.902 23.555-45.757 49.457-44.347 25.902 1.41 45.758 23.555 44.348 49.457zm0 0"/></svg>
|
After Width: | Height: | Size: 3.7 KiB |
1
js/public/img/peertube_monochrome.svg
Normal file
1
js/public/img/peertube_monochrome.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="2799 -911 16 22"><g data-name="Artboard – 1"><g data-name="Symbol 3 – 1"><g data-name="Group 44"><path d="M2799-911v11l8-5" data-name="Path 4"/><path d="M2799-900v11l8-6" data-name="Path 5"/><path d="M2807-905v10l8-5" data-name="Path 6"/><path fill="transparent" d="M2807-895v-10l-8 5z" data-name="Path 7"/></g></g></g></svg>
|
After Width: | Height: | Size: 378 B |
1
js/public/img/sign_language_monochrome.svg
Normal file
1
js/public/img/sign_language_monochrome.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg height="100px" width="100px" fill="#000000" version="1.0" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 100 84.922" enable-background="new 0 0 100 84.922" xml:space="preserve"><path d="M50.29,42.212"/><path d="M50.29,42.212"/><path d="M50.29,42.212"/><path d="M95.223,22.145c-5.377,3.135-8.271,4.414-13.844,4.125c-2.098-0.024-3.207-0.917-5.281-0.724 c-1.93,0.072-2.918,0.796-4.849,1.447c-2.966,0.917-4.583,1.737-7.694,2.702c-1.977,0.555-3.23,0.579-5.281,1.278 c-1.76,0.748-0.916,0.145-2.146,1.567c-1.592,1.761-1.206,3.136-2.701,6.15c-1.856,3.956-1.641,4.486-0.579,4.969 c-0.699,2.025-0.867,3.377-0.699,5.571c0.192,1.664-0.506,2.821,1.278,4.147c2.437,1.81,7.55,6.054,8.853,6.125 c1.953-0.071,7.646,0.146,10.539-1.855c0.652-0.41,0.845-0.82,0.289,0.725c-0.119,0.192-7.211,5.715-7.982,5.69 c-5.463,0.176-6.511,0.431-6.611,0.556c-0.628-0.243-4.812-5.237-7.377-3.402c-0.99,0.604-1.448,1.183-1.835,2.269 c0.338,0.699,1.931,1.81,3.4,2.726c2.123,1.352,2.315,2.772,4.438,3.281c2.58,0.577,5.33,0.191,6.969,0.408 c0.629,0.169,6.32-1.495,9.865-3.4c3.28-1.688-4.51,3.256-6.416,5.692c-5.74,1.81-4.123,1.882-6.85,2.437 c-2.532,0.578-2.412,0.434-4.148,0.988c-1.785,0.434-3.256,2.87-1.566,4.147c1.013,0.845,4.173-0.337,7.283-0.435 c1.375-0.022,6.994-2.604,8.272-3.279c3.185-1.713,5.522-4.076,7.404-5.428c2.604-1.807-2.942,4.994-5.282,7.84 c-0.675,0.918-0.988,1.109-1.832,2.291c-1.398,1.811,0.385,4.56,1.277,4.125c1.037-0.385,1.713-2.459,2.846-3.135 c0.58-0.314,2.123-2.582,3.57-4.125c1.061-1.205,1.833-1.736,2.846-2.99c1.713-2.123,2.074-3.738,3.57-6.006 c0.916-1.183,1.566-1.543,2.41-2.99c4.463-8.441,3.16-12.229,7.43-18.258c1.303-1.785,2.773-2.22,4.848-3.281 C101.566,30.031,95.175,22.169,95.223,22.145z M74.529,44.528c-1.014,3.545-0.916,3.955-2.846,5.281 c-2.34,1.785-2.461,0.434-7.838,3.425c0,0-2.22-2.315-5.717-3.425c0.023,0.049-0.941-4.486-2.846-6.006 c4.533-3.449,4.412-6.366,5.137-6.27c1.616,0.145,4.198,0.965,8.973-0.844C72.143,41.923,75.471,41.657,74.529,44.528z"/><path d="M47.3,18.863c-2.122-1.423-2.339-2.846-4.438-3.304c-2.58-0.627-5.354-0.241-6.971-0.555 c-0.627-0.072-6.318,1.592-9.84,3.425c-3.328,1.761,4.486-3.184,6.416-5.716c5.716-1.712,4.076-1.785,6.85-2.412 c2.508-0.506,2.387-0.362,4.124-0.868c1.761-0.482,3.256-2.895,1.567-4.269c-1.037-0.772-4.172,0.41-7.26,0.434 C36.35,5.696,30.729,8.277,29.451,9c-3.184,1.664-5.523,4.028-7.404,5.282c-2.629,1.906,2.942-4.872,5.282-7.694 c0.675-0.965,0.989-1.158,1.856-2.291c1.352-1.857-0.41-4.607-1.277-4.269c-1.062,0.482-1.736,2.556-3.016,3.28 c-0.458,0.266-2.002,2.533-3.424,4.125c-1.062,1.158-1.834,1.688-2.847,3.015c-1.736,2.05-2.074,3.666-3.569,5.837 c-0.916,1.302-1.592,1.64-2.412,3.135C8.179,27.813,9.457,31.6,5.212,37.533c-1.302,1.905-2.773,2.315-4.848,3.425 c-1.93,14.037,4.438,21.876,4.413,21.828c5.379-3.062,8.249-4.342,13.845-4.147c2.099,0.12,3.208,1.012,5.282,0.866 c1.93-0.119,2.918-0.844,4.848-1.422c2.967-0.988,4.582-1.81,7.549-2.727c2.123-0.604,3.377-0.627,5.428-1.422 c1.76-0.652,0.916-0.049,2.146-1.424c1.567-1.809,1.205-3.184,2.557-6.127c2.002-4.027,1.784-4.558,0.723-4.992 c0.676-2.074,0.869-3.4,0.699-5.571c-0.217-1.688,0.507-2.87-1.277-4.125c-2.437-1.881-7.55-6.126-8.828-6.15 c-1.978,0.024-7.67-0.193-10.564,1.712c-0.65,0.506-0.844,0.917-0.289-0.555c0.121-0.266,7.212-5.789,7.983-5.861 c5.059-0.108,6.332-0.315,6.573-0.429c0.5,0.013,4.804,5.243,7.417,3.42c0.965-0.651,1.447-1.23,1.857-2.267 C50.364,20.817,48.772,19.708,47.3,18.863z M28.318,35.121c2.315-1.712,2.459-0.362,7.838-3.28c0-0.073,2.219,2.243,5.717,3.28 c-0.024,0.024,0.94,4.558,2.846,6.126c-4.535,3.401-4.414,6.32-5.138,6.271c-1.617-0.193-4.197-1.014-8.972,0.725 c-2.774-5.162-6.078-4.873-5.138-7.864C26.484,36.954,26.364,36.52,28.318,35.121z"/></svg>
|
After Width: | Height: | Size: 3.7 KiB |
|
@ -165,12 +165,13 @@ function doMerge<T = any>(
|
|||
args: Record<string, any> | null
|
||||
): Array<T> {
|
||||
const merged = existing && Array.isArray(existing) ? existing.slice(0) : [];
|
||||
const previous = incoming && Array.isArray(incoming) ? incoming.slice(0) : [];
|
||||
let res;
|
||||
if (args) {
|
||||
// Assume an page of 1 if args.page omitted.
|
||||
const { page = 1, limit = 10 } = args;
|
||||
for (let i = 0; i < incoming.length; ++i) {
|
||||
merged[(page - 1) * limit + i] = incoming[i];
|
||||
for (let i = 0; i < previous.length; ++i) {
|
||||
merged[(page - 1) * limit + i] = previous[i];
|
||||
}
|
||||
res = merged;
|
||||
} else {
|
||||
|
@ -178,7 +179,7 @@ function doMerge<T = any>(
|
|||
// to receive any arguments, so you might prefer to throw an
|
||||
// exception here, instead of recovering by appending incoming
|
||||
// onto the existing array.
|
||||
res = [...merged, ...incoming];
|
||||
res = [...merged, ...previous];
|
||||
// eslint-disable-next-line no-underscore-dangle
|
||||
res = uniqBy(res, (elem: any) => elem.__ref);
|
||||
}
|
||||
|
|
|
@ -250,7 +250,9 @@ export default class EditorComponent extends Vue {
|
|||
Mention.configure(MentionOptions),
|
||||
CustomImage,
|
||||
Underline,
|
||||
Link,
|
||||
Link.configure({
|
||||
HTMLAttributes: { target: "_blank", rel: "noopener noreferrer ugc" },
|
||||
}),
|
||||
CharacterCount.configure({
|
||||
limit: this.maxSize,
|
||||
}),
|
||||
|
|
|
@ -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>
|
|
@ -171,6 +171,12 @@ const FULL_EVENT_FRAGMENT = gql`
|
|||
options {
|
||||
...EventOptions
|
||||
}
|
||||
metadata {
|
||||
key
|
||||
title
|
||||
value
|
||||
type
|
||||
}
|
||||
}
|
||||
${ADDRESS_FRAGMENT}
|
||||
${TAG_FRAGMENT}
|
||||
|
@ -326,6 +332,7 @@ export const EDIT_EVENT = gql`
|
|||
$physicalAddress: AddressInput
|
||||
$options: EventOptionsInput
|
||||
$contacts: [Contact]
|
||||
$metadata: EventMetadataInput
|
||||
) {
|
||||
updateEvent(
|
||||
eventId: $id
|
||||
|
@ -347,6 +354,7 @@ export const EDIT_EVENT = gql`
|
|||
physicalAddress: $physicalAddress
|
||||
options: $options
|
||||
contacts: $contacts
|
||||
metadata: $metadata
|
||||
) {
|
||||
...FullEvent
|
||||
}
|
||||
|
|
|
@ -1072,5 +1072,55 @@
|
|||
"+ Create a post": "+ Create a post",
|
||||
"Edited {relative_time} ago": "Edited {relative_time} ago",
|
||||
"Members-only post": "Members-only post",
|
||||
"This post is accessible only for members. You have access to it for moderation purposes only because you are an instance moderator.": "This post is accessible only for members. You have access to it for moderation purposes only because you are an instance moderator."
|
||||
"This post is accessible only for members. You have access to it for moderation purposes only because you are an instance moderator.": "This post is accessible only for members. You have access to it for moderation purposes only because you are an instance moderator.",
|
||||
"Find or add an element": "Find or add an element",
|
||||
"e.g. Accessibility, Twitch, PeerTube": "e.g. Accessibility, Twitch, PeerTube",
|
||||
"Add new…": "Add new…",
|
||||
"No results for {search}": "No results for {search}",
|
||||
"Wheelchair accessibility": "Wheelchair accessibility",
|
||||
"Whether the event is accessible with a wheelchair": "Whether the event is accessible with a wheelchair",
|
||||
"Not accessible with a wheelchair": "Not accessible with a wheelchair",
|
||||
"Partially accessible with a wheelchair": "Partially accessible with a wheelchair",
|
||||
"Fully accessible with a wheelchair": "Fully accessible with a wheelchair",
|
||||
"YouTube replay": "YouTube replay",
|
||||
"The URL where the event live can be watched again after it has ended": "The URL where the event live can be watched again after it has ended",
|
||||
"Twitch replay": "Twitch replay",
|
||||
"PeerTube replay": "PeerTube replay",
|
||||
"PeerTube live": "PeerTube live",
|
||||
"The URL where the event can be watched live": "The URL where the event can be watched live",
|
||||
"Twitch live": "Twitch live",
|
||||
"YouTube live": "YouTube live",
|
||||
"Event metadata": "Event metadata",
|
||||
"Framadate poll": "Framadate poll",
|
||||
"The URL of a poll where the choice for the event date is happening": "The URL of a poll where the choice for the event date is happening",
|
||||
"View account on {hostname} (in a new window)": "View account on {hostname} (in a new window)",
|
||||
"Twitter account": "Twitter account",
|
||||
"A twitter account handle to follow for event updates": "A twitter account handle to follow for event updates",
|
||||
"Fediverse account": "Fediverse account",
|
||||
"A fediverse account URL to follow for event updates": "A fediverse account URL to follow for event updates",
|
||||
"Element title": "Element title",
|
||||
"Element value": "Element value",
|
||||
"Subtitles": "Subtitles",
|
||||
"Whether the event live video is subtitled": "Whether the event live video is subtitled",
|
||||
"The event live video contains subtitles": "The event live video contains subtitles",
|
||||
"The event live video does not contain subtitles": "The event live video does not contain subtitles",
|
||||
"Sign Language": "Sign Language",
|
||||
"Whether the event is interpreted in sign language": "Whether the event is interpreted in sign language",
|
||||
"The event has a sign language interpreter": "The event has a sign language interpreter",
|
||||
"The event hasn't got a sign language interpreter": "The event hasn't got a sign language interpreter",
|
||||
"Online ticketing": "Online ticketing",
|
||||
"An URL to an external ticketing platform": "An URL to an external ticketing platform",
|
||||
"Price sheet": "Price sheet",
|
||||
"A link to a page presenting the price options": "A link to a page presenting the price options",
|
||||
"Integrate this event with 3rd-party tools and show metadata for the event.": "Integrate this event with 3rd-party tools and show metadata for the event.",
|
||||
"This URL doesn't seem to be valid": "This URL doesn't seem to be valid",
|
||||
"Schedule": "Schedule",
|
||||
"A link to a page presenting the event schedule": "A link to a page presenting the event schedule",
|
||||
"Accessibility": "Accessibility",
|
||||
"Live": "Live",
|
||||
"Replay": "Replay",
|
||||
"Tools": "Tools",
|
||||
"Social": "Social",
|
||||
"Details": "Details",
|
||||
"Booking": "Booking"
|
||||
}
|
||||
|
|
|
@ -1163,5 +1163,55 @@
|
|||
"+ Create a post": "+ Créer un billet",
|
||||
"Edited {relative_time} ago": "Édité il y a {relative_time}",
|
||||
"Members-only post": "Billet reservé aux membres",
|
||||
"This post is accessible only for members. You have access to it for moderation purposes only because you are an instance moderator.": "Ce billet est accessible uniquement aux membres. Vous y avez accès à des fins de modération car vous êtes modérateur⋅ice de l'instance."
|
||||
"This post is accessible only for members. You have access to it for moderation purposes only because you are an instance moderator.": "Ce billet est accessible uniquement aux membres. Vous y avez accès à des fins de modération car vous êtes modérateur⋅ice de l'instance.",
|
||||
"Find or add an element": "Trouver ou ajouter un élément",
|
||||
"e.g. Accessibility, Twitch, PeerTube": "par ex. Accessibilité, Framadate, PeerTube",
|
||||
"Add new…": "Ajouter un nouvel élément…",
|
||||
"No results for {search}": "Pas de résultats pour {search}",
|
||||
"Wheelchair accessibility": "Accessibilité aux fauteuils roulants",
|
||||
"Whether the event is accessible with a wheelchair": "Si l'événement est accessible avec un fauteuil roulant",
|
||||
"Not accessible with a wheelchair": "Non accessible avec un fauteuil roulant",
|
||||
"Partially accessible with a wheelchair": "Partiellement accessible avec un fauteuil roulant",
|
||||
"Fully accessible with a wheelchair": "Entièrement accessible avec un fauteuil roulant",
|
||||
"YouTube replay": "Replay sur YouTube",
|
||||
"The URL where the event live can be watched again after it has ended": "L'URL où le direct de l'événement peut être visionné à nouveau une fois terminé",
|
||||
"Twitch replay": "Replay sur Twitch",
|
||||
"PeerTube replay": "Replay sur PeerTube",
|
||||
"PeerTube live": "Direct sur PeerTube",
|
||||
"The URL where the event can be watched live": "L'URL où l'événement peut être visionné en direct",
|
||||
"Twitch live": "Direct sur Twitch",
|
||||
"YouTube live": "Direct sur YouTube",
|
||||
"Event metadata": "Métadonnées de l'événement",
|
||||
"Framadate poll": "Sondage Framadate",
|
||||
"The URL of a poll where the choice for the event date is happening": "L'URL d'un sondage où la date de l'événement doit être choisie",
|
||||
"View account on {hostname} (in a new window)": "Voir le compte sur {hostname} (dans une nouvelle fenêtre)",
|
||||
"Twitter account": "Compte Twitter",
|
||||
"A twitter account handle to follow for event updates": "Un compte sur Twitter à suivre pour les mises à jour de l'événement",
|
||||
"Fediverse account": "Compte fediverse",
|
||||
"A fediverse account URL to follow for event updates": "Un compte sur le fediverse à suivre pour les mises à jour de l'événement",
|
||||
"Element title": "Titre de l'élement",
|
||||
"Element value": "Valeur de l'élement",
|
||||
"Subtitles": "Sous-titres",
|
||||
"Whether the event live video is subtitled": "Si le direct vidéo de l'événement est sous-titré",
|
||||
"The event live video contains subtitles": "Le direct vidéo de l'événement contient des sous-titres",
|
||||
"The event live video does not contain subtitles": "Le direct vidéo de l'événement ne contient pas de sous-titres",
|
||||
"Sign Language": "Langue des signes",
|
||||
"Whether the event is interpreted in sign language": "Si l'événement est interprété en langue des signes",
|
||||
"The event has a sign language interpreter": "L'événement a un interprète en langue des signes",
|
||||
"The event hasn't got a sign language interpreter": "L'événement n'a pas d'interprète en langue des signes",
|
||||
"Online ticketing": "Billetterie en ligne",
|
||||
"An URL to an external ticketing platform": "Une URL vers une plateforme de billetterie externe",
|
||||
"Price sheet": "Feuille des prix",
|
||||
"A link to a page presenting the price options": "Un lien vers une page présentant la tarification",
|
||||
"Integrate this event with 3rd-party tools and show metadata for the event.": "Intégrer cet événement avec des outils tiers et afficher des métadonnées pour l'événement.",
|
||||
"This URL doesn't seem to be valid": "Cette URL ne semble pas être valide",
|
||||
"Schedule": "Programme",
|
||||
"A link to a page presenting the event schedule": "Un lien vers une page présentant le programme de l'événement",
|
||||
"Accessibility": "Accessibilité",
|
||||
"Live": "Direct",
|
||||
"Replay": "Rattrapage",
|
||||
"Tools": "Outils",
|
||||
"Social": "Social",
|
||||
"Details": "Détails",
|
||||
"Booking": "Réservations"
|
||||
}
|
||||
|
|
|
@ -193,13 +193,4 @@ export default class EventMixin extends mixins(Vue) {
|
|||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
urlToHostname(url: string): string | null {
|
||||
try {
|
||||
return new URL(url).hostname;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
212
js/src/services/EventMetadata.ts
Normal file
212
js/src/services/EventMetadata.ts
Normal file
|
@ -0,0 +1,212 @@
|
|||
import {
|
||||
EventMetadataType,
|
||||
EventMetadataKeyType,
|
||||
EventMetadataCategories,
|
||||
} from "@/types/enums";
|
||||
import { IEventMetadataDescription } from "@/types/event-metadata";
|
||||
import { i18n } from "@/utils/i18n";
|
||||
|
||||
export const eventMetaDataList: IEventMetadataDescription[] = [
|
||||
{
|
||||
icon: "wheelchair-accessibility",
|
||||
key: "mz:accessibility:wheelchairAccessible",
|
||||
label: i18n.t("Wheelchair accessibility") as string,
|
||||
description: i18n.t(
|
||||
"Whether the event is accessible with a wheelchair"
|
||||
) as string,
|
||||
value: "",
|
||||
type: EventMetadataType.STRING,
|
||||
keyType: EventMetadataKeyType.CHOICE,
|
||||
choices: {
|
||||
no: i18n.t("Not accessible with a wheelchair") as string,
|
||||
partially: i18n.t("Partially accessible with a wheelchair") as string,
|
||||
fully: i18n.t("Fully accessible with a wheelchair") as string,
|
||||
},
|
||||
category: EventMetadataCategories.ACCESSIBILITY,
|
||||
},
|
||||
{
|
||||
icon: "subtitles",
|
||||
key: "mz:accessibility:live:subtitle",
|
||||
label: i18n.t("Subtitles") as string,
|
||||
description: i18n.t("Whether the event live video is subtitled") as string,
|
||||
value: "",
|
||||
type: EventMetadataType.BOOLEAN,
|
||||
keyType: EventMetadataKeyType.PLAIN,
|
||||
choices: {
|
||||
true: i18n.t("The event live video contains subtitles") as string,
|
||||
false: i18n.t(
|
||||
"The event live video does not contain subtitles"
|
||||
) as string,
|
||||
},
|
||||
category: EventMetadataCategories.ACCESSIBILITY,
|
||||
},
|
||||
{
|
||||
icon: "mz:icon:sign_language",
|
||||
key: "mz:accessibility:live:sign_language",
|
||||
label: i18n.t("Sign Language") as string,
|
||||
description: i18n.t(
|
||||
"Whether the event is interpreted in sign language"
|
||||
) as string,
|
||||
value: "",
|
||||
type: EventMetadataType.BOOLEAN,
|
||||
keyType: EventMetadataKeyType.PLAIN,
|
||||
choices: {
|
||||
true: i18n.t("The event has a sign language interpreter") as string,
|
||||
false: i18n.t(
|
||||
"The event hasn't got a sign language interpreter"
|
||||
) as string,
|
||||
},
|
||||
category: EventMetadataCategories.ACCESSIBILITY,
|
||||
},
|
||||
{
|
||||
icon: "youtube",
|
||||
key: "mz:replay:youtube:url",
|
||||
label: i18n.t("YouTube replay") as string,
|
||||
description: i18n.t(
|
||||
"The URL where the event live can be watched again after it has ended"
|
||||
) as string,
|
||||
value: "",
|
||||
type: EventMetadataType.STRING,
|
||||
keyType: EventMetadataKeyType.URL,
|
||||
pattern:
|
||||
/http(?:s?):\/\/(?:www\.)?youtu(?:be\.com\/watch\?v=|\.be\/)([\w\-_]*)(&(amp;)?[\w?=]*)?/,
|
||||
category: EventMetadataCategories.REPLAY,
|
||||
},
|
||||
// {
|
||||
// icon: "twitch",
|
||||
// key: "mz:replay:twitch:url",
|
||||
// label: i18n.t("Twitch replay") as string,
|
||||
// description: i18n.t(
|
||||
// "The URL where the event live can be watched again after it has ended"
|
||||
// ) as string,
|
||||
// value: "",
|
||||
// type: EventMetadataType.STRING,
|
||||
// },
|
||||
{
|
||||
icon: "mz:icon:peertube",
|
||||
key: "mz:replay:peertube:url",
|
||||
label: i18n.t("PeerTube replay") as string,
|
||||
description: i18n.t(
|
||||
"The URL where the event live can be watched again after it has ended"
|
||||
) as string,
|
||||
value: "",
|
||||
type: EventMetadataType.STRING,
|
||||
keyType: EventMetadataKeyType.URL,
|
||||
pattern: /^https?:\/\/([^/]+)\/(?:videos\/(?:watch|embed)|w)\/([^/]+)$/,
|
||||
category: EventMetadataCategories.REPLAY,
|
||||
},
|
||||
{
|
||||
icon: "mz:icon:peertube",
|
||||
key: "mz:live:peertube:url",
|
||||
label: i18n.t("PeerTube live") as string,
|
||||
description: i18n.t(
|
||||
"The URL where the event can be watched live"
|
||||
) as string,
|
||||
value: "",
|
||||
type: EventMetadataType.STRING,
|
||||
keyType: EventMetadataKeyType.URL,
|
||||
pattern: /^https?:\/\/([^/]+)\/(?:videos\/(?:watch|embed)|w)\/([^/]+)$/,
|
||||
category: EventMetadataCategories.LIVE,
|
||||
},
|
||||
{
|
||||
icon: "twitch",
|
||||
key: "mz:live:twitch:url",
|
||||
label: i18n.t("Twitch live") as string,
|
||||
description: i18n.t(
|
||||
"The URL where the event can be watched live"
|
||||
) as string,
|
||||
value: "",
|
||||
type: EventMetadataType.STRING,
|
||||
keyType: EventMetadataKeyType.URL,
|
||||
placeholder: "https://www.twitch.tv/",
|
||||
pattern: /^(?:https?:\/\/)?(?:www\.|go\.)?twitch\.tv\/([a-z0-9_]+)($|\?)/,
|
||||
category: EventMetadataCategories.LIVE,
|
||||
},
|
||||
{
|
||||
icon: "youtube",
|
||||
key: "mz:live:youtube:url",
|
||||
label: i18n.t("YouTube live") as string,
|
||||
description: i18n.t(
|
||||
"The URL where the event can be watched live"
|
||||
) as string,
|
||||
value: "",
|
||||
type: EventMetadataType.STRING,
|
||||
keyType: EventMetadataKeyType.URL,
|
||||
pattern:
|
||||
/http(?:s?):\/\/(?:www\.)?youtu(?:be\.com\/watch\?v=|\.be\/)([\w\-_]*)(&(amp;)?[\w?=]*)?/,
|
||||
category: EventMetadataCategories.LIVE,
|
||||
},
|
||||
{
|
||||
icon: "calendar-check",
|
||||
key: "mz:poll:framadate:url",
|
||||
label: i18n.t("Framadate poll") as string,
|
||||
description: i18n.t(
|
||||
"The URL of a poll where the choice for the event date is happening"
|
||||
) as string,
|
||||
value: "",
|
||||
placeholder: "https://framadate.org/",
|
||||
type: EventMetadataType.STRING,
|
||||
keyType: EventMetadataKeyType.URL,
|
||||
category: EventMetadataCategories.TOOLS,
|
||||
},
|
||||
{
|
||||
icon: "twitter",
|
||||
key: "mz:social:twitter:account",
|
||||
label: i18n.t("Twitter account") as string,
|
||||
description: i18n.t(
|
||||
"A twitter account handle to follow for event updates"
|
||||
) as string,
|
||||
value: "",
|
||||
placeholder: "@JoinMobilizon",
|
||||
type: EventMetadataType.STRING,
|
||||
keyType: EventMetadataKeyType.HANDLE,
|
||||
category: EventMetadataCategories.SOCIAL,
|
||||
},
|
||||
{
|
||||
icon: "mz:icon:fediverse",
|
||||
key: "mz:social:fediverse:account_url",
|
||||
label: i18n.t("Fediverse account") as string,
|
||||
description: i18n.t(
|
||||
"A fediverse account URL to follow for event updates"
|
||||
) as string,
|
||||
value: "",
|
||||
placeholder: "https://framapiaf.org/@mobilizon",
|
||||
type: EventMetadataType.STRING,
|
||||
keyType: EventMetadataKeyType.URL,
|
||||
category: EventMetadataCategories.SOCIAL,
|
||||
},
|
||||
{
|
||||
icon: "ticket-confirmation",
|
||||
key: "mz:ticket:external_url",
|
||||
label: i18n.t("Online ticketing") as string,
|
||||
description: i18n.t("An URL to an external ticketing platform") as string,
|
||||
value: "",
|
||||
type: EventMetadataType.STRING,
|
||||
keyType: EventMetadataKeyType.URL,
|
||||
category: EventMetadataCategories.BOOKING,
|
||||
},
|
||||
{
|
||||
icon: "cash",
|
||||
key: "mz:ticket:price_url",
|
||||
label: i18n.t("Price sheet") as string,
|
||||
description: i18n.t(
|
||||
"A link to a page presenting the price options"
|
||||
) as string,
|
||||
value: "",
|
||||
type: EventMetadataType.STRING,
|
||||
keyType: EventMetadataKeyType.URL,
|
||||
category: EventMetadataCategories.DETAILS,
|
||||
},
|
||||
{
|
||||
icon: "calendar-text",
|
||||
key: "mz:schedule_url",
|
||||
label: i18n.t("Schedule") as string,
|
||||
description: i18n.t(
|
||||
"A link to a page presenting the event schedule"
|
||||
) as string,
|
||||
value: "",
|
||||
type: EventMetadataType.STRING,
|
||||
keyType: EventMetadataKeyType.URL,
|
||||
category: EventMetadataCategories.DETAILS,
|
||||
},
|
||||
];
|
|
@ -251,3 +251,27 @@ export enum SortDirection {
|
|||
ASC = "ASC",
|
||||
DESC = "DESC",
|
||||
}
|
||||
|
||||
export enum EventMetadataType {
|
||||
STRING = "STRING",
|
||||
INTEGER = "INTEGER",
|
||||
FLOAT = "FLOAT",
|
||||
BOOLEAN = "BOOLEAN",
|
||||
}
|
||||
|
||||
export enum EventMetadataKeyType {
|
||||
PLAIN = "PLAIN",
|
||||
URL = "URL",
|
||||
CHOICE = "CHOICE",
|
||||
HANDLE = "HANDLE",
|
||||
}
|
||||
|
||||
export enum EventMetadataCategories {
|
||||
ACCESSIBILITY = "ACCESSIBILITY",
|
||||
LIVE = "LIVE",
|
||||
REPLAY = "REPLAY",
|
||||
SOCIAL = "SOCIAL",
|
||||
TOOLS = "TOOLS",
|
||||
DETAILS = "DETAILS",
|
||||
BOOKING = "BOOKING",
|
||||
}
|
||||
|
|
23
js/src/types/event-metadata.ts
Normal file
23
js/src/types/event-metadata.ts
Normal file
|
@ -0,0 +1,23 @@
|
|||
import {
|
||||
EventMetadataCategories,
|
||||
EventMetadataKeyType,
|
||||
EventMetadataType,
|
||||
} from "./enums";
|
||||
|
||||
export interface IEventMetadata {
|
||||
key: string;
|
||||
title?: string;
|
||||
value: string;
|
||||
type: EventMetadataType;
|
||||
}
|
||||
|
||||
export interface IEventMetadataDescription extends IEventMetadata {
|
||||
icon?: string;
|
||||
placeholder?: string;
|
||||
description: string;
|
||||
choices?: Record<string, string>;
|
||||
keyType: EventMetadataKeyType;
|
||||
pattern?: RegExp;
|
||||
label: string;
|
||||
category: EventMetadataCategories;
|
||||
}
|
|
@ -10,6 +10,7 @@ import type { IParticipant } from "./participant.model";
|
|||
import { EventOptions } from "./event-options.model";
|
||||
import type { IEventOptions } from "./event-options.model";
|
||||
import { EventJoinOptions, EventStatus, EventVisibility } from "./enums";
|
||||
import { IEventMetadata } from "./event-metadata";
|
||||
|
||||
export interface IEventCardOptions {
|
||||
hideDate: boolean;
|
||||
|
@ -49,6 +50,7 @@ interface IEventEditJSON {
|
|||
tags: string[];
|
||||
options: IEventOptions;
|
||||
contacts: { id?: string }[];
|
||||
metadata: IEventMetadata[];
|
||||
}
|
||||
|
||||
export interface IEvent {
|
||||
|
@ -84,6 +86,7 @@ export interface IEvent {
|
|||
|
||||
tags: ITag[];
|
||||
options: IEventOptions;
|
||||
metadata: IEventMetadata[];
|
||||
contacts: IActor[];
|
||||
|
||||
toEditJSON(): IEventEditJSON;
|
||||
|
@ -153,6 +156,8 @@ export class EventModel implements IEvent {
|
|||
|
||||
options: IEventOptions = new EventOptions();
|
||||
|
||||
metadata: IEventMetadata[] = [];
|
||||
|
||||
constructor(hash?: IEvent) {
|
||||
if (!hash) return;
|
||||
|
||||
|
@ -193,6 +198,7 @@ export class EventModel implements IEvent {
|
|||
this.contacts = hash.contacts;
|
||||
|
||||
this.tags = hash.tags;
|
||||
this.metadata = hash.metadata;
|
||||
if (hash.options) this.options = hash.options;
|
||||
}
|
||||
|
||||
|
@ -212,6 +218,12 @@ export class EventModel implements IEvent {
|
|||
phoneAddress: this.phoneAddress,
|
||||
physicalAddress: this.removeTypeName(this.physicalAddress),
|
||||
options: this.removeTypeName(this.options),
|
||||
metadata: this.metadata.map(({ key, value, type, title }) => ({
|
||||
key,
|
||||
value,
|
||||
type,
|
||||
title,
|
||||
})),
|
||||
attributedToId:
|
||||
this.attributedTo && this.attributedTo.id ? this.attributedTo.id : null,
|
||||
contacts: this.contacts.map(({ id }) => ({
|
||||
|
|
|
@ -122,6 +122,15 @@
|
|||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<subtitle>{{ $t("Event metadata") }}</subtitle>
|
||||
<p>
|
||||
{{
|
||||
$t(
|
||||
"Integrate this event with 3rd-party tools and show metadata for the event."
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
<event-metadata-list v-model="event.metadata" />
|
||||
<subtitle>{{ $t("Who can view this event and participate") }}</subtitle>
|
||||
<div class="field">
|
||||
<b-radio
|
||||
|
@ -451,6 +460,7 @@ import PictureUpload from "@/components/PictureUpload.vue";
|
|||
import EditorComponent from "@/components/Editor.vue";
|
||||
import TagInput from "@/components/Event/TagInput.vue";
|
||||
import FullAddressAutoComplete from "@/components/Event/FullAddressAutoComplete.vue";
|
||||
import EventMetadataList from "@/components/Event/EventMetadataList.vue";
|
||||
import IdentityPickerWrapper from "@/views/Account/IdentityPickerWrapper.vue";
|
||||
import Subtitle from "@/components/Utils/Subtitle.vue";
|
||||
import { Route } from "vue-router";
|
||||
|
@ -515,6 +525,7 @@ const DEFAULT_LIMIT_NUMBER_OF_PLACES = 10;
|
|||
TagInput,
|
||||
PictureUpload,
|
||||
Editor: EditorComponent,
|
||||
EventMetadataList,
|
||||
},
|
||||
apollo: {
|
||||
currentActor: CURRENT_ACTOR_CLIENT,
|
||||
|
|
|
@ -294,104 +294,11 @@
|
|||
<div class="event-description-wrapper">
|
||||
<aside class="event-metadata">
|
||||
<div class="sticky">
|
||||
<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-sidebar
|
||||
v-if="event && config"
|
||||
:event="event"
|
||||
:config="config"
|
||||
/>
|
||||
</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"
|
||||
:href="event.onlineAddress"
|
||||
:title="
|
||||
$t('View page on {hostname} (in a new window)', {
|
||||
hostname: urlToHostname(event.onlineAddress),
|
||||
})
|
||||
"
|
||||
>{{ urlToHostname(event.onlineAddress) }}</a
|
||||
>
|
||||
</event-metadata-block>
|
||||
</div>
|
||||
</aside>
|
||||
<div class="event-description-comments">
|
||||
|
@ -408,6 +315,14 @@
|
|||
/>
|
||||
</div>
|
||||
</section>
|
||||
<section class="integration-wrappers">
|
||||
<component
|
||||
v-for="(metadata, integration) in integrations"
|
||||
:is="integration"
|
||||
:key="integration"
|
||||
:metadata="metadata"
|
||||
/>
|
||||
</section>
|
||||
<section class="comments" ref="commentsObserver">
|
||||
<a href="#comments">
|
||||
<subtitle id="comments">{{ $t("Comments") }}</subtitle>
|
||||
|
@ -531,80 +446,6 @@
|
|||
</section>
|
||||
</div>
|
||||
</b-modal>
|
||||
<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>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -618,8 +459,6 @@ import {
|
|||
EventVisibility,
|
||||
MemberRole,
|
||||
ParticipantRole,
|
||||
RoutingTransportationType,
|
||||
RoutingType,
|
||||
} from "@/types/enums";
|
||||
import {
|
||||
EVENT_PERSON_PARTICIPATION,
|
||||
|
@ -636,7 +475,6 @@ import { IActor, IPerson, Person, usernameWithDomain } from "../../types/actor";
|
|||
import { GRAPHQL_API_ENDPOINT } from "../../api/_entrypoint";
|
||||
import DateCalendarIcon from "../../components/Event/DateCalendarIcon.vue";
|
||||
import EventCard from "../../components/Event/EventCard.vue";
|
||||
import EventFullDate from "../../components/Event/EventFullDate.vue";
|
||||
import ReportModal from "../../components/Report/ReportModal.vue";
|
||||
import { IReport } from "../../types/report.model";
|
||||
import { CREATE_REPORT } from "../../graphql/report";
|
||||
|
@ -644,7 +482,6 @@ import EventMixin from "../../mixins/event";
|
|||
import IdentityPicker from "../Account/IdentityPicker.vue";
|
||||
import ParticipationSection from "../../components/Participation/ParticipationSection.vue";
|
||||
import RouteName from "../../router/name";
|
||||
import { Address } from "../../types/address.model";
|
||||
import CommentTree from "../../components/Comment/CommentTree.vue";
|
||||
import "intersection-observer";
|
||||
import { CONFIG } from "../../graphql/config";
|
||||
|
@ -657,19 +494,18 @@ import {
|
|||
import { IConfig } from "../../types/config.model";
|
||||
import Subtitle from "../../components/Utils/Subtitle.vue";
|
||||
import Tag from "../../components/Tag.vue";
|
||||
import EventMetadataBlock from "../../components/Event/EventMetadataBlock.vue";
|
||||
import EventMetadataSidebar from "../../components/Event/EventMetadataSidebar.vue";
|
||||
import EventBanner from "../../components/Event/EventBanner.vue";
|
||||
import ActorCard from "../../components/Account/ActorCard.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";
|
||||
|
||||
// noinspection TypeScriptValidateTypes
|
||||
@Component({
|
||||
components: {
|
||||
EventMetadataBlock,
|
||||
Subtitle,
|
||||
EventFullDate,
|
||||
EventCard,
|
||||
BIcon,
|
||||
DateCalendarIcon,
|
||||
|
@ -678,15 +514,25 @@ import { ApolloCache, FetchResult } from "@apollo/client/core";
|
|||
ParticipationSection,
|
||||
CommentTree,
|
||||
Tag,
|
||||
ActorCard,
|
||||
PopoverActorCard,
|
||||
EventBanner,
|
||||
"map-leaflet": () =>
|
||||
import(/* webpackChunkName: "map" */ "../../components/Map.vue"),
|
||||
EventMetadataSidebar,
|
||||
ShareEventModal: () =>
|
||||
import(
|
||||
/* webpackChunkName: "shareEventModal" */ "../../components/Event/ShareEventModal.vue"
|
||||
),
|
||||
"integration-twitch": () =>
|
||||
import(
|
||||
/* webpackChunkName: "twitchIntegration" */ "../../components/Event/Integrations/Twitch.vue"
|
||||
),
|
||||
"integration-peertube": () =>
|
||||
import(
|
||||
/* webpackChunkName: "PeerTubeIntegration" */ "../../components/Event/Integrations/PeerTube.vue"
|
||||
),
|
||||
"integration-youtube": () =>
|
||||
import(
|
||||
/* webpackChunkName: "YouTubeIntegration" */ "../../components/Event/Integrations/YouTube.vue"
|
||||
),
|
||||
},
|
||||
apollo: {
|
||||
event: {
|
||||
|
@ -783,8 +629,6 @@ export default class Event extends EventMixin {
|
|||
|
||||
oldParticipationRole!: string;
|
||||
|
||||
showMap = false;
|
||||
|
||||
isReportModalActive = false;
|
||||
|
||||
isShareModalActive = false;
|
||||
|
@ -813,65 +657,6 @@ export default class Event extends EventMixin {
|
|||
|
||||
messageForConfirmation = "";
|
||||
|
||||
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",
|
||||
},
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
get eventTitle(): undefined | string {
|
||||
if (!this.event) return undefined;
|
||||
return this.event.title;
|
||||
|
@ -1262,12 +1047,6 @@ export default class Event extends EventMixin {
|
|||
);
|
||||
}
|
||||
|
||||
get physicalAddress(): Address | null {
|
||||
if (!this.event.physicalAddress) return null;
|
||||
|
||||
return new Address(this.event.physicalAddress);
|
||||
}
|
||||
|
||||
async anonymousParticipationConfirmed(): Promise<boolean> {
|
||||
return isParticipatingInThisEvent(this.uuid);
|
||||
}
|
||||
|
@ -1302,6 +1081,32 @@ export default class Event extends EventMixin {
|
|||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
metadataToComponent: Record<string, string> = {
|
||||
"mz:live:twitch:url": "integration-twitch",
|
||||
"mz:live:peertube:url": "integration-peertube",
|
||||
"mz:live:youtube:url": "integration-youtube",
|
||||
};
|
||||
|
||||
get integrations(): Record<string, IEventMetadataDescription> {
|
||||
return this.event.metadata
|
||||
.map((val) => {
|
||||
const def = eventMetaDataList.find((dat) => dat.key === val.key);
|
||||
return {
|
||||
...def,
|
||||
...val,
|
||||
};
|
||||
})
|
||||
.reduce((acc: Record<string, IEventMetadataDescription>, metadata) => {
|
||||
const component = this.metadataToComponent[metadata.key];
|
||||
if (component !== undefined) {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
acc[component] = metadata;
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
|
@ -1402,60 +1207,6 @@ div.sidebar {
|
|||
top: 50px;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
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%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
span.online-address {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
::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.event-description-comments {
|
||||
|
@ -1547,29 +1298,6 @@ a.participations-link {
|
|||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.no-border {
|
||||
border: 0;
|
||||
cursor: auto;
|
||||
|
|
|
@ -118,7 +118,7 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from "vue-property-decorator";
|
||||
import { Component, Prop, Vue, Watch } from "vue-property-decorator";
|
||||
import { Route } from "vue-router";
|
||||
import { ICurrentUser } from "@/types/current-user.model";
|
||||
import { LoginError, LoginErrorCode } from "@/types/enums";
|
||||
|
@ -269,6 +269,13 @@ export default class Login extends Vue {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Watch("currentUser")
|
||||
redirectToHomepageIfAlreadyLoggedIn(): Promise<Route> | void {
|
||||
if (this.currentUser.isLoggedIn) {
|
||||
return this.$router.push("/");
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
@ -103,6 +103,7 @@ defmodule Mobilizon.GraphQL.Schema.EventType do
|
|||
field(:updated_at, :datetime, description: "When the event was last updated")
|
||||
field(:inserted_at, :datetime, description: "When the event was created")
|
||||
field(:options, :event_options, description: "The event options")
|
||||
field(:metadata, list_of(:event_metadata), description: "A key-value list of metadata")
|
||||
end
|
||||
|
||||
@desc "The list of visibility options for an event"
|
||||
|
@ -290,6 +291,26 @@ defmodule Mobilizon.GraphQL.Schema.EventType do
|
|||
)
|
||||
end
|
||||
|
||||
enum :event_metadata_type do
|
||||
value(:string, description: "A string")
|
||||
value(:integer, description: "An integer")
|
||||
value(:boolean, description: "A boolean")
|
||||
end
|
||||
|
||||
object :event_metadata do
|
||||
field(:key, :string, description: "The key for the metadata")
|
||||
field(:title, :string, description: "The title for the metadata")
|
||||
field(:value, :string, description: "The value for the metadata")
|
||||
field(:type, :event_metadata_type, description: "The metadata type")
|
||||
end
|
||||
|
||||
input_object :event_metadata_input do
|
||||
field(:key, non_null(:string), description: "The key for the metadata")
|
||||
field(:title, :string, description: "The title for the metadata")
|
||||
field(:value, non_null(:string), description: "The value for the metadata")
|
||||
field(:type, :event_metadata_type, description: "The metadata type")
|
||||
end
|
||||
|
||||
@desc """
|
||||
A event contact
|
||||
"""
|
||||
|
@ -372,6 +393,7 @@ defmodule Mobilizon.GraphQL.Schema.EventType do
|
|||
arg(:category, :string, default_value: "meeting", description: "The event's category")
|
||||
arg(:physical_address, :address_input, description: "The event's physical address")
|
||||
arg(:options, :event_options_input, description: "The event options")
|
||||
arg(:metadata, list_of(:event_metadata_input), description: "The event metadata")
|
||||
|
||||
arg(:draft, :boolean,
|
||||
default_value: false,
|
||||
|
@ -419,6 +441,7 @@ defmodule Mobilizon.GraphQL.Schema.EventType do
|
|||
arg(:category, :string, description: "The event's category")
|
||||
arg(:physical_address, :address_input, description: "The event's physical address")
|
||||
arg(:options, :event_options_input, description: "The event options")
|
||||
arg(:metadata, list_of(:event_metadata_input), description: "The event metadata")
|
||||
arg(:draft, :boolean, description: "Whether or not the event is a draft")
|
||||
arg(:contacts, list_of(:contact), default_value: [], description: "The events contacts")
|
||||
|
||||
|
|
|
@ -16,6 +16,7 @@ defmodule Mobilizon.Events.Event do
|
|||
alias Mobilizon.Discussions.Comment
|
||||
|
||||
alias Mobilizon.Events.{
|
||||
EventMetadata,
|
||||
EventOptions,
|
||||
EventParticipantStats,
|
||||
EventStatus,
|
||||
|
@ -108,6 +109,7 @@ defmodule Mobilizon.Events.Event do
|
|||
|
||||
embeds_one(:options, EventOptions, on_replace: :delete)
|
||||
embeds_one(:participant_stats, EventParticipantStats, on_replace: :update)
|
||||
embeds_many(:metadata, EventMetadata, on_replace: :delete)
|
||||
belongs_to(:organizer_actor, Actor, foreign_key: :organizer_actor_id)
|
||||
belongs_to(:attributed_to, Actor, foreign_key: :attributed_to_id)
|
||||
belongs_to(:physical_address, Address, on_replace: :nilify)
|
||||
|
@ -151,6 +153,7 @@ defmodule Mobilizon.Events.Event do
|
|||
defp common_changeset(%Changeset{} = changeset, attrs) do
|
||||
changeset
|
||||
|> cast_embed(:options)
|
||||
|> cast_embed(:metadata)
|
||||
|> put_assoc(:contacts, Map.get(attrs, :contacts, []))
|
||||
|> put_assoc(:media, Map.get(attrs, :media, []))
|
||||
|> put_tags(attrs)
|
||||
|
|
45
lib/mobilizon/events/event_metadata.ex
Normal file
45
lib/mobilizon/events/event_metadata.ex
Normal file
|
@ -0,0 +1,45 @@
|
|||
defmodule Mobilizon.Events.EventMetadata do
|
||||
@moduledoc """
|
||||
Participation stats on event
|
||||
"""
|
||||
|
||||
use Ecto.Schema
|
||||
import Ecto.Changeset
|
||||
import EctoEnum
|
||||
|
||||
defenum(EventMetadataTypeEnum, string: 0, integer: 1, boolean: 2)
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
key: String.t(),
|
||||
value: String.t()
|
||||
}
|
||||
|
||||
@required_attrs [
|
||||
:key,
|
||||
:value
|
||||
]
|
||||
|
||||
@optional_attrs [
|
||||
:title,
|
||||
:type
|
||||
]
|
||||
|
||||
@attrs @required_attrs ++ @optional_attrs
|
||||
|
||||
@primary_key false
|
||||
@derive Jason.Encoder
|
||||
embedded_schema do
|
||||
field(:key, :string)
|
||||
field(:title, :string)
|
||||
field(:value, :string)
|
||||
field(:type, EventMetadataTypeEnum, default: :string)
|
||||
end
|
||||
|
||||
@doc false
|
||||
@spec changeset(t, map) :: Ecto.Changeset.t()
|
||||
def changeset(%__MODULE__{} = event_metadata, attrs) do
|
||||
event_metadata
|
||||
|> cast(attrs, @attrs)
|
||||
|> validate_required(@required_attrs)
|
||||
end
|
||||
end
|
|
@ -36,6 +36,11 @@ defmodule Mobilizon.Service.Formatter.DefaultScrubbler do
|
|||
"ugc"
|
||||
])
|
||||
|
||||
# Rel attributes are separated by spaces
|
||||
Meta.allow_tag_with_this_attribute_values(:a, "rel", [
|
||||
"noopener noreferrer ugc"
|
||||
])
|
||||
|
||||
Meta.allow_tag_with_these_attributes(:a, ["name", "title", "target"])
|
||||
|
||||
Meta.allow_tag_with_these_attributes(:abbr, ["title"])
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
defmodule Mobilizon.Storage.Repo.Migrations.AddMetadataForEvents do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
alter table(:events) do
|
||||
add(:metadata, :map)
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in a new issue