all developments of milestone 1
This commit is contained in:
@@ -110,7 +110,13 @@ body {
|
||||
@apply border-red-500;
|
||||
}
|
||||
.input-icon-right {
|
||||
right: 0.5rem;
|
||||
@apply right-2;
|
||||
}
|
||||
.input-iconspace-left {
|
||||
@apply pl-8;
|
||||
}
|
||||
.input-iconspace-right {
|
||||
@apply pr-8;
|
||||
}
|
||||
.input[type="text"]:disabled,
|
||||
.input[type="email"]:disabled {
|
||||
|
||||
@@ -168,7 +168,7 @@ const props = withDefaults(
|
||||
defineProps<{
|
||||
modelValue: IComment;
|
||||
currentActor: IPerson;
|
||||
canReport: boolean;
|
||||
canReport?: boolean;
|
||||
}>(),
|
||||
{ canReport: false }
|
||||
);
|
||||
|
||||
@@ -2,21 +2,6 @@
|
||||
<div class="container mx-auto" id="error-wrapper">
|
||||
<div class="">
|
||||
<section>
|
||||
<div class="text-center">
|
||||
<picture>
|
||||
<source
|
||||
:srcset="`/img/pics/error-480w.webp 1x, /img/pics/error-1024w.webp 2x`"
|
||||
type="image/webp"
|
||||
/>
|
||||
<img
|
||||
:src="`/img/pics/error-480w.webp`"
|
||||
alt=""
|
||||
width="480"
|
||||
height="312"
|
||||
loading="lazy"
|
||||
/>
|
||||
</picture>
|
||||
</div>
|
||||
<o-notification variant="danger" class="">
|
||||
<h1>
|
||||
{{
|
||||
@@ -108,7 +93,7 @@ import { IAnalyticsConfig, IConfig } from "@/types/config.model";
|
||||
import { computed, defineAsyncComponent, ref } from "vue";
|
||||
import { useQuery, useQueryLoading } from "@vue/apollo-composable";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
import { useAnalytics } from "@/composition/apollo/config";
|
||||
import { INSTANCE_NAME } from "@/graphql/config";
|
||||
const SentryFeedback = defineAsyncComponent(
|
||||
|
||||
@@ -4,7 +4,11 @@
|
||||
:class="{ small }"
|
||||
:style="`--small: ${smallStyle}`"
|
||||
>
|
||||
<div class="datetime-container-header" />
|
||||
<div class="datetime-container-header">
|
||||
<time :datetime="dateObj.toISOString()" class="weekday">{{
|
||||
weekday
|
||||
}}</time>
|
||||
</div>
|
||||
<div class="datetime-container-content">
|
||||
<time :datetime="dateObj.toISOString()" class="day block font-semibold">{{
|
||||
day
|
||||
@@ -38,6 +42,10 @@ const day = computed<string>(() =>
|
||||
dateObj.value.toLocaleString(undefined, { day: "numeric" })
|
||||
);
|
||||
|
||||
const weekday = computed<string>(() =>
|
||||
dateObj.value.toLocaleString(undefined, { weekday: "short" })
|
||||
);
|
||||
|
||||
const smallStyle = computed<string>(() => (props.small ? "1.2" : "2"));
|
||||
</script>
|
||||
|
||||
@@ -51,6 +59,12 @@ div.datetime-container {
|
||||
height: calc(10px * var(--small));
|
||||
background: #f3425f;
|
||||
}
|
||||
.datetime-container-header .weekday {
|
||||
font-size: calc(9px * var(--small));
|
||||
font-weight: bold;
|
||||
vertical-align: top;
|
||||
line-height: calc(9px * var(--small));
|
||||
}
|
||||
.datetime-container-content {
|
||||
height: calc(30px * var(--small));
|
||||
}
|
||||
|
||||
@@ -23,7 +23,10 @@
|
||||
<div class="flex flex-col gap-1 mt-1">
|
||||
<p
|
||||
class="inline-flex gap-2 ml-auto"
|
||||
v-if="event.joinOptions !== EventJoinOptions.EXTERNAL"
|
||||
v-if="
|
||||
event.joinOptions !== EventJoinOptions.EXTERNAL &&
|
||||
!event.options.hideNumberOfParticipants
|
||||
"
|
||||
>
|
||||
<TicketConfirmationOutline />
|
||||
<router-link
|
||||
|
||||
@@ -12,6 +12,31 @@
|
||||
class="rounded-lg"
|
||||
:class="{ 'sm:w-full sm:max-w-[20rem]': mode === 'row' }"
|
||||
>
|
||||
<div
|
||||
class="-mt-3 h-0 mb-3 ltr:ml-0 rtl:mr-0 block relative z-10"
|
||||
:class="{
|
||||
'sm:hidden': mode === 'row',
|
||||
'calendar-simple': !isDifferentBeginsEndsDate,
|
||||
'calendar-double': isDifferentBeginsEndsDate,
|
||||
}"
|
||||
>
|
||||
<date-calendar-icon
|
||||
:small="true"
|
||||
v-if="!mergedOptions.hideDate"
|
||||
:date="event.beginsOn.toString()"
|
||||
/>
|
||||
<MenuDown
|
||||
:small="true"
|
||||
class="left-3 relative"
|
||||
v-if="!mergedOptions.hideDate && isDifferentBeginsEndsDate"
|
||||
/>
|
||||
<date-calendar-icon
|
||||
:small="true"
|
||||
v-if="!mergedOptions.hideDate && isDifferentBeginsEndsDate"
|
||||
:date="event.endsOn?.toString()"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<figure class="block relative pt-40">
|
||||
<lazy-image-wrapper
|
||||
:picture="event.picture"
|
||||
@@ -49,20 +74,29 @@
|
||||
<div class="p-2 flex-auto" :class="{ 'sm:flex-1': mode === 'row' }">
|
||||
<div class="relative flex flex-col h-full">
|
||||
<div
|
||||
class="-mt-3 h-0 flex mb-3 ltr:ml-0 rtl:mr-0 items-end self-start"
|
||||
class="-mt-3 h-0 flex mb-3 ltr:ml-0 rtl:mr-0 items-end self-end"
|
||||
:class="{ 'sm:hidden': mode === 'row' }"
|
||||
>
|
||||
<date-calendar-icon
|
||||
<start-time-icon
|
||||
:small="true"
|
||||
v-if="!mergedOptions.hideDate"
|
||||
v-if="!mergedOptions.hideDate && event.options.showStartTime"
|
||||
:date="event.beginsOn.toString()"
|
||||
/>
|
||||
</div>
|
||||
<span
|
||||
class="text-gray-700 dark:text-white font-semibold hidden"
|
||||
:class="{ 'sm:block': mode === 'row' }"
|
||||
v-if="!isDifferentBeginsEndsDate"
|
||||
>{{ formatDateTimeWithCurrentLocale }}</span
|
||||
>
|
||||
<span
|
||||
class="text-gray-700 dark:text-white font-semibold hidden"
|
||||
:class="{ 'sm:block': mode === 'row' }"
|
||||
v-if="isDifferentBeginsEndsDate"
|
||||
>{{ formatBeginsOnDateWithCurrentLocale }}
|
||||
<ArrowRightThin :small="true" style="display: ruby" />
|
||||
{{ formatEndsOnDateWithCurrentLocale }}</span
|
||||
>
|
||||
<div class="w-full flex flex-col justify-between h-full">
|
||||
<h2
|
||||
class="mt-0 mb-2 text-2xl line-clamp-3 font-bold text-violet-3 dark:text-white"
|
||||
@@ -152,6 +186,16 @@
|
||||
</div>
|
||||
</LinkOrRouterLink>
|
||||
</template>
|
||||
<style scoped>
|
||||
.calendar-simple {
|
||||
bottom: -117px;
|
||||
left: 5px;
|
||||
}
|
||||
.calendar-double {
|
||||
bottom: -45px;
|
||||
left: 5px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {
|
||||
@@ -161,6 +205,9 @@ import {
|
||||
organizerAvatarUrl,
|
||||
} from "@/types/event.model";
|
||||
import DateCalendarIcon from "@/components/Event/DateCalendarIcon.vue";
|
||||
import StartTimeIcon from "@/components/Event/StartTimeIcon.vue";
|
||||
import ArrowRightThin from "vue-material-design-icons/ArrowRightThin.vue";
|
||||
import MenuDown from "vue-material-design-icons/MenuDown.vue";
|
||||
import LazyImageWrapper from "@/components/Image/LazyImageWrapper.vue";
|
||||
import { EventStatus } from "@/types/enums";
|
||||
import RouteName from "../../router/name";
|
||||
@@ -170,7 +217,7 @@ import { computed, inject } from "vue";
|
||||
import MobilizonTag from "@/components/TagElement.vue";
|
||||
import AccountCircle from "vue-material-design-icons/AccountCircle.vue";
|
||||
import Video from "vue-material-design-icons/Video.vue";
|
||||
import { formatDateTimeForEvent } from "@/utils/datetime";
|
||||
import { formatDateForEvent, formatDateTimeForEvent } from "@/utils/datetime";
|
||||
import type { Locale } from "date-fns";
|
||||
import LinkOrRouterLink from "../core/LinkOrRouterLink.vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
@@ -212,6 +259,28 @@ const actorAvatarURL = computed<string | null>(() =>
|
||||
|
||||
const dateFnsLocale = inject<Locale>("dateFnsLocale");
|
||||
|
||||
const isDifferentBeginsEndsDate = computed(() => {
|
||||
if (!dateFnsLocale) return;
|
||||
const beginsOnStr = formatDateForEvent(
|
||||
new Date(props.event.beginsOn),
|
||||
dateFnsLocale
|
||||
);
|
||||
const endsOnStr = props.event.endsOn
|
||||
? formatDateForEvent(new Date(props.event.endsOn), dateFnsLocale)
|
||||
: null;
|
||||
return endsOnStr && endsOnStr != beginsOnStr;
|
||||
});
|
||||
|
||||
const formatBeginsOnDateWithCurrentLocale = computed(() => {
|
||||
if (!dateFnsLocale) return;
|
||||
return formatDateForEvent(new Date(props.event.beginsOn), dateFnsLocale);
|
||||
});
|
||||
|
||||
const formatEndsOnDateWithCurrentLocale = computed(() => {
|
||||
if (!dateFnsLocale) return;
|
||||
return formatDateForEvent(new Date(props.event.endsOn), dateFnsLocale);
|
||||
});
|
||||
|
||||
const formatDateTimeWithCurrentLocale = computed(() => {
|
||||
if (!dateFnsLocale) return;
|
||||
return formatDateTimeForEvent(new Date(props.event.beginsOn), dateFnsLocale);
|
||||
|
||||
51
src/components/Event/StartTimeIcon.vue
Normal file
51
src/components/Event/StartTimeIcon.vue
Normal file
@@ -0,0 +1,51 @@
|
||||
<template>
|
||||
<div
|
||||
class="starttime-container flex flex-col rounded-lg text-center justify-center overflow-hidden items-stretch bg-white dark:bg-gray-700 text-violet-3 dark:text-white"
|
||||
:class="{ small }"
|
||||
:style="`--small: ${smallStyle}`"
|
||||
>
|
||||
<div class="starttime-container-content font-semibold">
|
||||
<Clock class="clock-icon" /><time :datetime="dateObj.toISOString()">{{
|
||||
time
|
||||
}}</time>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { computed } from "vue";
|
||||
import Clock from "vue-material-design-icons/ClockTimeTenOutline.vue";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
date: string;
|
||||
small?: boolean;
|
||||
}>(),
|
||||
{ small: false }
|
||||
);
|
||||
|
||||
const dateObj = computed<Date>(() => new Date(props.date));
|
||||
|
||||
const time = computed<string>(() =>
|
||||
dateObj.value.toLocaleTimeString(undefined, {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
})
|
||||
);
|
||||
|
||||
const smallStyle = computed<string>(() => (props.small ? "0.9" : "2"));
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
div.starttime-container {
|
||||
width: auto;
|
||||
box-shadow: 0 0 12px rgba(0, 0, 0, 0.2);
|
||||
padding: 0.25rem 0.25rem;
|
||||
font-size: calc(1rem * var(--small));
|
||||
}
|
||||
|
||||
.clock-icon {
|
||||
vertical-align: middle;
|
||||
padding-right: 0.2rem;
|
||||
display: inline-block;
|
||||
}
|
||||
</style>
|
||||
@@ -14,7 +14,8 @@
|
||||
</p>
|
||||
</template>
|
||||
<o-taginput
|
||||
v-model="tagsStrings"
|
||||
:modelValue="tagsStrings"
|
||||
@update:modelValue="updateTags"
|
||||
:data="filteredTags"
|
||||
:allow-autocomplete="true"
|
||||
:allow-new="true"
|
||||
@@ -34,7 +35,7 @@
|
||||
<script lang="ts" setup>
|
||||
import differenceBy from "lodash/differenceBy";
|
||||
import { ITag } from "../../types/tag.model";
|
||||
import { computed, onBeforeMount, ref } from "vue";
|
||||
import { computed, onBeforeMount, ref, watch } from "vue";
|
||||
import HelpCircleOutline from "vue-material-design-icons/HelpCircleOutline.vue";
|
||||
import { useFetchTags } from "@/composition/apollo/tags";
|
||||
import { FILTER_TAGS } from "@/graphql/tags";
|
||||
@@ -44,6 +45,10 @@ const props = defineProps<{
|
||||
modelValue: ITag[];
|
||||
}>();
|
||||
|
||||
const propsValue = computed(() => props.modelValue);
|
||||
|
||||
const tagsStrings = ref<string[]>([]);
|
||||
|
||||
const emit = defineEmits(["update:modelValue"]);
|
||||
|
||||
const text = ref("");
|
||||
@@ -77,7 +82,7 @@ const getFilteredTags = async (newText: string): Promise<void> => {
|
||||
};
|
||||
|
||||
const filteredTags = computed((): ITag[] => {
|
||||
return differenceBy(tags.value, props.modelValue, "id").filter(
|
||||
return differenceBy(tags.value, propsValue.value, "id").filter(
|
||||
(option) =>
|
||||
option.title.toString().toLowerCase().indexOf(text.value.toLowerCase()) >=
|
||||
0 ||
|
||||
@@ -86,19 +91,19 @@ const filteredTags = computed((): ITag[] => {
|
||||
);
|
||||
});
|
||||
|
||||
const tagsStrings = computed({
|
||||
get(): string[] {
|
||||
return props.modelValue.map((tag: ITag) => tag.title);
|
||||
},
|
||||
set(newTagsStrings: string[]) {
|
||||
console.debug("tagsStrings", newTagsStrings);
|
||||
const tagEntities = newTagsStrings.map((tag: string | ITag) => {
|
||||
if (typeof tag !== "string") {
|
||||
return tag;
|
||||
}
|
||||
return { title: tag, slug: tag } as ITag;
|
||||
});
|
||||
emit("update:modelValue", tagEntities);
|
||||
},
|
||||
watch(props.modelValue, (newValue, oldValue) => {
|
||||
if (newValue != oldValue) {
|
||||
tagsStrings.value = propsValue.value.map((tag: ITag) => tag.title);
|
||||
}
|
||||
});
|
||||
|
||||
const updateTags = (newTagsStrings: string[]) => {
|
||||
const tagEntities = newTagsStrings.map((tag: string | ITag) => {
|
||||
if (typeof tag !== "string") {
|
||||
return tag;
|
||||
}
|
||||
return { title: tag, slug: tag } as ITag;
|
||||
});
|
||||
emit("update:modelValue", tagEntities);
|
||||
};
|
||||
</script>
|
||||
|
||||
228
src/components/FullCalendar/EventsAgenda.vue
Normal file
228
src/components/FullCalendar/EventsAgenda.vue
Normal file
@@ -0,0 +1,228 @@
|
||||
<template>
|
||||
<FullCalendar
|
||||
ref="calendarRef"
|
||||
:options="calendarOptions"
|
||||
class="agenda-view"
|
||||
/>
|
||||
|
||||
<div v-if="listOfEventsByDate.date" class="my-4">
|
||||
<b v-text="formatDateString(listOfEventsByDate.date)" />
|
||||
|
||||
<div v-if="listOfEventsByDate.events.length > 0">
|
||||
<div
|
||||
v-for="(event, index) in listOfEventsByDate.events"
|
||||
v-bind:key="index"
|
||||
>
|
||||
<div class="scroll-ml-6 snap-center shrink-0 my-4">
|
||||
<EventCard :event="event.event.extendedProps.event" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<EmptyContent v-else icon="calendar" :inline="true">
|
||||
<span>
|
||||
{{ t("No events found") }}
|
||||
</span>
|
||||
</EmptyContent>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { locale } from "@/utils/i18n";
|
||||
import { computed, ref } from "vue";
|
||||
import { useLazyQuery } from "@vue/apollo-composable";
|
||||
import { IEvent } from "@/types/event.model";
|
||||
import { Paginate } from "@/types/paginate";
|
||||
import { SEARCH_CALENDAR_EVENTS } from "@/graphql/search";
|
||||
import FullCalendar from "@fullcalendar/vue3";
|
||||
import { EventSegment } from "@fullcalendar/core";
|
||||
import dayGridPlugin from "@fullcalendar/daygrid";
|
||||
import interactionPlugin from "@fullcalendar/interaction";
|
||||
import {
|
||||
formatDateISOStringWithoutTime,
|
||||
formatDateString,
|
||||
} from "@/filters/datetime";
|
||||
import EventCard from "../Event/EventCard.vue";
|
||||
import EmptyContent from "../Utils/EmptyContent.vue";
|
||||
|
||||
const { t } = useI18n({ useScope: "global" });
|
||||
|
||||
const calendarRef = ref();
|
||||
|
||||
const lastSelectedDate = ref<string | undefined>(new Date().toISOString());
|
||||
|
||||
const listOfEventsByDate = ref<{ events: EventSegment[]; date?: string }>({
|
||||
events: [],
|
||||
date: undefined,
|
||||
});
|
||||
|
||||
const showEventsByDate = (dateStr: string) => {
|
||||
dateStr = formatDateISOStringWithoutTime(dateStr);
|
||||
const moreLinkElement = document.querySelectorAll(
|
||||
`td[data-date='${dateStr}'] a.fc-more-link`
|
||||
)[0] as undefined | HTMLElement;
|
||||
|
||||
if (moreLinkElement) {
|
||||
moreLinkElement.click();
|
||||
} else {
|
||||
listOfEventsByDate.value = {
|
||||
events: [],
|
||||
date: dateStr,
|
||||
};
|
||||
}
|
||||
|
||||
calendarRef.value.getApi().select(dateStr);
|
||||
};
|
||||
|
||||
if (window.location.hash.length) {
|
||||
lastSelectedDate.value = formatDateISOStringWithoutTime(
|
||||
window.location.hash.replace("#_", "")
|
||||
);
|
||||
} else {
|
||||
lastSelectedDate.value = formatDateISOStringWithoutTime(
|
||||
new Date().toISOString()
|
||||
);
|
||||
}
|
||||
|
||||
const { load: searchEventsLoad, refetch: searchEventsRefetch } = useLazyQuery<{
|
||||
searchEvents: Paginate<IEvent>;
|
||||
}>(SEARCH_CALENDAR_EVENTS);
|
||||
|
||||
const calendarOptions = computed((): object => {
|
||||
return {
|
||||
plugins: [dayGridPlugin, interactionPlugin],
|
||||
initialView: "dayGridMonth",
|
||||
initialDate: lastSelectedDate.value,
|
||||
events: async (
|
||||
info: { start: Date; end: Date; startStr: string; endStr: string },
|
||||
successCallback: (arg: object[]) => unknown,
|
||||
failureCallback: (err: string) => unknown
|
||||
) => {
|
||||
const queryVars = {
|
||||
limit: 999,
|
||||
beginsOn: info.start,
|
||||
endsOn: info.end,
|
||||
};
|
||||
|
||||
const result =
|
||||
(await searchEventsLoad(undefined, queryVars)) ||
|
||||
(await searchEventsRefetch(queryVars))?.data;
|
||||
|
||||
if (!result) {
|
||||
failureCallback("failed to fetch calendar events");
|
||||
return;
|
||||
}
|
||||
|
||||
successCallback(
|
||||
(result.searchEvents.elements ?? []).map((event: IEvent) => {
|
||||
return {
|
||||
id: event.id,
|
||||
title: event.title,
|
||||
start: event.beginsOn,
|
||||
end: event.endsOn,
|
||||
startStr: event.beginsOn,
|
||||
endStr: event.endsOn,
|
||||
url: event.url,
|
||||
extendedProps: {
|
||||
event: event,
|
||||
},
|
||||
};
|
||||
})
|
||||
);
|
||||
},
|
||||
nextDayThreshold: "09:00:00",
|
||||
dayMaxEventRows: 0,
|
||||
moreLinkClassNames: "bg-mbz-yellow dark:bg-mbz-purple dark:text-white",
|
||||
moreLinkContent: (arg: { num: number; text: string }) => {
|
||||
return "+" + arg.num.toString();
|
||||
},
|
||||
contentHeight: "auto",
|
||||
eventClassNames: "line-clamp-3 bg-mbz-yellow dark:bg-mbz-purple",
|
||||
headerToolbar: {
|
||||
left: "prev,next,customTodayButton",
|
||||
center: "",
|
||||
right: "title",
|
||||
},
|
||||
locale: locale,
|
||||
firstDay: 1,
|
||||
buttonText: {
|
||||
today: t("Today"),
|
||||
month: t("Month"),
|
||||
week: t("Week"),
|
||||
day: t("Day"),
|
||||
list: t("List"),
|
||||
},
|
||||
customButtons: {
|
||||
customTodayButton: {
|
||||
text: t("Today"),
|
||||
click: () => {
|
||||
calendarRef.value.getApi().today();
|
||||
lastSelectedDate.value = formatDateISOStringWithoutTime(
|
||||
new Date().toISOString()
|
||||
);
|
||||
},
|
||||
},
|
||||
},
|
||||
dateClick: (info: { dateStr: string }) => {
|
||||
showEventsByDate(info.dateStr);
|
||||
},
|
||||
moreLinkClick: (info: {
|
||||
date: Date;
|
||||
allSegs: EventSegment[];
|
||||
hiddenSegs: EventSegment[];
|
||||
jsEvent: object;
|
||||
}) => {
|
||||
listOfEventsByDate.value = {
|
||||
events: info.allSegs,
|
||||
date: info.date.toISOString(),
|
||||
};
|
||||
|
||||
if (info.allSegs.length) {
|
||||
window.location.hash =
|
||||
"_" + formatDateISOStringWithoutTime(info.date.toISOString());
|
||||
}
|
||||
|
||||
return "none";
|
||||
},
|
||||
moreLinkDidMount: (arg: { el: Element }) => {
|
||||
if (
|
||||
lastSelectedDate.value &&
|
||||
arg.el.closest(`td[data-date='${lastSelectedDate.value}']`)
|
||||
) {
|
||||
showEventsByDate(lastSelectedDate.value);
|
||||
lastSelectedDate.value = undefined;
|
||||
}
|
||||
},
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.agenda-view .fc-button {
|
||||
font-size: 0.8rem !important;
|
||||
}
|
||||
|
||||
.agenda-view .fc-toolbar-title {
|
||||
font-size: 1rem !important;
|
||||
}
|
||||
|
||||
.agenda-view .fc-daygrid-day-events {
|
||||
min-height: 1.1rem !important;
|
||||
margin-bottom: 0.2rem !important;
|
||||
margin-left: 0.1rem !important;
|
||||
}
|
||||
|
||||
.agenda-view .fc-more-link {
|
||||
pointer-events: none !important;
|
||||
}
|
||||
|
||||
.clock-icon {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.time {
|
||||
font-size: 0.95rem !important;
|
||||
}
|
||||
</style>
|
||||
104
src/components/FullCalendar/EventsCalendar.vue
Normal file
104
src/components/FullCalendar/EventsCalendar.vue
Normal file
@@ -0,0 +1,104 @@
|
||||
<template>
|
||||
<FullCalendar ref="calendarRef" :options="calendarOptions">
|
||||
<template v-slot:eventContent="arg">
|
||||
<span
|
||||
class="text-violet-3 dark:text-white font-bold m-2"
|
||||
:title="arg.event.title"
|
||||
>
|
||||
{{ arg.event.title }}
|
||||
</span>
|
||||
</template>
|
||||
</FullCalendar>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { locale } from "@/utils/i18n";
|
||||
import { computed, ref } from "vue";
|
||||
import { useLazyQuery } from "@vue/apollo-composable";
|
||||
import { IEvent } from "@/types/event.model";
|
||||
import { Paginate } from "@/types/paginate";
|
||||
import { SEARCH_CALENDAR_EVENTS } from "@/graphql/search";
|
||||
import FullCalendar from "@fullcalendar/vue3";
|
||||
import dayGridPlugin from "@fullcalendar/daygrid";
|
||||
import interactionPlugin from "@fullcalendar/interaction";
|
||||
|
||||
const calendarRef = ref();
|
||||
|
||||
const { t } = useI18n({ useScope: "global" });
|
||||
|
||||
const { load: searchEventsLoad, refetch: searchEventsRefetch } = useLazyQuery<{
|
||||
searchEvents: Paginate<IEvent>;
|
||||
}>(SEARCH_CALENDAR_EVENTS);
|
||||
|
||||
const calendarOptions = computed((): object => {
|
||||
return {
|
||||
plugins: [dayGridPlugin, interactionPlugin],
|
||||
initialView: "dayGridMonth",
|
||||
events: async (
|
||||
info: { start: Date; end: Date; startStr: string; endStr: string },
|
||||
successCallback: (arg: object[]) => unknown,
|
||||
failureCallback: (err: string) => unknown
|
||||
) => {
|
||||
const queryVars = {
|
||||
limit: 999,
|
||||
beginsOn: info.start,
|
||||
endsOn: info.end,
|
||||
};
|
||||
|
||||
const result =
|
||||
(await searchEventsLoad(undefined, queryVars)) ||
|
||||
(await searchEventsRefetch(queryVars))?.data;
|
||||
|
||||
if (!result) {
|
||||
failureCallback("failed to fetch calendar events");
|
||||
return;
|
||||
}
|
||||
|
||||
successCallback(
|
||||
(result.searchEvents.elements ?? []).map((event: IEvent) => {
|
||||
return {
|
||||
id: event.id,
|
||||
title: event.title,
|
||||
start: event.beginsOn,
|
||||
end: event.endsOn,
|
||||
startStr: event.beginsOn,
|
||||
endStr: event.endsOn,
|
||||
url: `/events/${event.uuid}`,
|
||||
extendedProps: {
|
||||
event: event,
|
||||
},
|
||||
};
|
||||
})
|
||||
);
|
||||
},
|
||||
nextDayThreshold: "09:00:00",
|
||||
dayMaxEventRows: 5,
|
||||
moreLinkClassNames: "bg-mbz-yellow dark:bg-mbz-purple dark:text-white p-2",
|
||||
moreLinkContent: (arg: { num: number; text: string }) => {
|
||||
return "+" + arg.num.toString();
|
||||
},
|
||||
eventClassNames: "line-clamp-3 bg-mbz-yellow dark:bg-mbz-purple",
|
||||
headerToolbar: {
|
||||
left: "prev,next,today",
|
||||
center: "title",
|
||||
right: "dayGridWeek,dayGridMonth", // user can switch between the two
|
||||
},
|
||||
locale: locale,
|
||||
firstDay: 1,
|
||||
buttonText: {
|
||||
today: t("Today"),
|
||||
month: t("Month"),
|
||||
week: t("Week"),
|
||||
day: t("Day"),
|
||||
list: t("List"),
|
||||
},
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.fc-popover-header {
|
||||
color: black !important;
|
||||
}
|
||||
</style>
|
||||
@@ -16,7 +16,7 @@ import { useGroup } from "@/composition/apollo/group";
|
||||
import { displayName } from "@/types/actor";
|
||||
import { computed } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
|
||||
const props = defineProps<{
|
||||
preferredUsername: string;
|
||||
|
||||
@@ -3,42 +3,12 @@
|
||||
<h1 class="dark:text-white font-bold">
|
||||
{{ config.slogan ?? t("Gather ⋅ Organize ⋅ Mobilize") }}
|
||||
</h1>
|
||||
<i18n-t
|
||||
keypath="Join {instance}, a Mobilizon instance"
|
||||
tag="p"
|
||||
class="dark:text-white"
|
||||
>
|
||||
<template #instance>
|
||||
<b>{{ config.name }}</b>
|
||||
</template>
|
||||
</i18n-t>
|
||||
|
||||
<p class="dark:text-white mb-2">{{ config.description }}</p>
|
||||
<!-- We don't invite to find other instances yet -->
|
||||
<!-- <p v-if="!config.registrationsOpen">
|
||||
{{ t("This instance isn't opened to registrations, but you can register on other instances.") }}
|
||||
</p>-->
|
||||
<div class="flex flex-wrap gap-2 items-center">
|
||||
<o-button
|
||||
variant="primary"
|
||||
tag="router-link"
|
||||
:to="{ name: RouteName.REGISTER }"
|
||||
v-if="config.registrationsOpen"
|
||||
>{{ t("Create an account") }}</o-button
|
||||
>
|
||||
<!-- We don't invite to find other instances yet -->
|
||||
<!-- <o-button v-else variant="link" tag="a" href="https://joinmastodon.org">{{ t('Find an instance') }}</o-button> -->
|
||||
<router-link
|
||||
:to="{ name: RouteName.ABOUT }"
|
||||
class="py-2.5 px-5 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded-lg border border-gray-200 hover:bg-gray-100 hover:text-violet-title focus:z-10 focus:ring-4 focus:ring-gray-200 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700"
|
||||
>
|
||||
{{ t("Learn more about {instance}", { instance: config.name }) }}
|
||||
</router-link>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { IConfig } from "@/types/config.model";
|
||||
import RouteName from "@/router/name";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
defineProps<{
|
||||
|
||||
@@ -11,8 +11,11 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed } from "vue";
|
||||
import { IMedia } from "@/types/media.model";
|
||||
import { useDefaultPicture } from "@/composition/apollo/config";
|
||||
import LazyImage from "../Image/LazyImage.vue";
|
||||
|
||||
const { defaultPicture } = useDefaultPicture();
|
||||
|
||||
const DEFAULT_CARD_URL = "/img/mobilizon_default_card.png";
|
||||
const DEFAULT_BLURHASH = "MCHKI4El-P-U}+={R-WWoes,Iu-P=?R,xD";
|
||||
const DEFAULT_WIDTH = 630;
|
||||
@@ -38,6 +41,9 @@ const props = withDefaults(
|
||||
|
||||
const pictureOrDefault = computed(() => {
|
||||
if (props.picture === null) {
|
||||
if (defaultPicture?.value?.url) {
|
||||
return defaultPicture.value;
|
||||
}
|
||||
return DEFAULT_PICTURE;
|
||||
}
|
||||
return {
|
||||
|
||||
@@ -6,18 +6,7 @@
|
||||
v-on="attrs"
|
||||
>
|
||||
<template #title>
|
||||
{{ t("Last published events") }}
|
||||
</template>
|
||||
<template #subtitle>
|
||||
<i18n-t
|
||||
class="text-slate-700 dark:text-slate-300"
|
||||
tag="p"
|
||||
keypath="On {instance} and other federated instances"
|
||||
>
|
||||
<template #instance>
|
||||
<b>{{ instanceName }}</b>
|
||||
</template>
|
||||
</i18n-t>
|
||||
{{ t("Agenda") }}
|
||||
</template>
|
||||
<template #content>
|
||||
<skeleton-event-result
|
||||
@@ -69,8 +58,8 @@ const attrs = useAttrs();
|
||||
const { result: resultEvents, loading: loadingEvents } = useQuery<{
|
||||
events: Paginate<IEvent>;
|
||||
}>(FETCH_EVENTS, {
|
||||
orderBy: EventSortField.INSERTED_AT,
|
||||
direction: SortDirection.DESC,
|
||||
orderBy: EventSortField.BEGINS_ON,
|
||||
direction: SortDirection.ASC,
|
||||
});
|
||||
const events = computed(
|
||||
() => resultEvents.value?.events ?? { total: 0, elements: [] }
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<template>
|
||||
<svg
|
||||
class="bg-white dark:bg-zinc-900 dark:fill-white"
|
||||
v-if="!instanceLogoUrl"
|
||||
class="bg-white dark:bg-zinc-900 dark:fill-white max-h-12"
|
||||
:class="{ 'bg-gray-900': invert }"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 248.16 46.78"
|
||||
@@ -30,9 +31,14 @@
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
<img v-else alt="" class="max-h-12 w-auto" :src="instanceLogoUrl" />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useInstanceLogoUrl } from "@/composition/apollo/config";
|
||||
|
||||
const { instanceLogoUrl } = useInstanceLogoUrl();
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
invert?: boolean;
|
||||
|
||||
@@ -3,9 +3,7 @@
|
||||
class="bg-white border-gray-200 px-2 sm:px-4 py-2.5 dark:bg-zinc-900"
|
||||
id="navbar"
|
||||
>
|
||||
<div
|
||||
class="container mx-auto flex flex-wrap items-center mx-auto gap-2 sm:gap-4"
|
||||
>
|
||||
<div class="container mx-auto flex flex-wrap items-center gap-2 sm:gap-4">
|
||||
<router-link
|
||||
:to="{ name: RouteName.HOME }"
|
||||
class="flex items-center"
|
||||
@@ -181,34 +179,92 @@
|
||||
<ul
|
||||
class="flex flex-col md:flex-row md:space-x-8 mt-2 md:mt-0 md:font-lightbold"
|
||||
>
|
||||
<li v-if="currentActor?.id">
|
||||
<search-fields
|
||||
v-if="showMobileMenu"
|
||||
class="m-auto w-auto"
|
||||
v-model:search="search"
|
||||
v-model:location="location"
|
||||
/>
|
||||
|
||||
<li class="m-auto" v-if="islongEvents">
|
||||
<router-link
|
||||
:to="{
|
||||
name: RouteName.SEARCH,
|
||||
query: { contentType: 'SHORTEVENTS' },
|
||||
}"
|
||||
class="block py-2 pr-4 pl-3 text-zinc-700 border-b border-gray-100 hover:bg-zinc-50 md:hover:bg-transparent md:border-0 md:hover:text-mbz-purple-700 md:p-0 dark:text-zinc-400 md:dark:hover:text-white dark:hover:bg-zinc-700 dark:hover:text-white md:dark:hover:bg-transparent dark:border-gray-700"
|
||||
>{{ t("Events") }}</router-link
|
||||
>
|
||||
</li>
|
||||
<li class="m-auto" v-else>
|
||||
<router-link
|
||||
:to="{ name: RouteName.SEARCH, query: { contentType: 'EVENTS' } }"
|
||||
class="block py-2 pr-4 pl-3 text-zinc-700 border-b border-gray-100 hover:bg-zinc-50 md:hover:bg-transparent md:border-0 md:hover:text-mbz-purple-700 md:p-0 dark:text-zinc-400 md:dark:hover:text-white dark:hover:bg-zinc-700 dark:hover:text-white md:dark:hover:bg-transparent dark:border-gray-700"
|
||||
>{{ t("Events") }}</router-link
|
||||
>
|
||||
</li>
|
||||
<li class="m-auto" v-if="islongEvents">
|
||||
<router-link
|
||||
:to="{
|
||||
name: RouteName.SEARCH,
|
||||
query: { contentType: 'LONGEVENTS' },
|
||||
}"
|
||||
class="block py-2 pr-4 pl-3 text-zinc-700 border-b border-gray-100 hover:bg-zinc-50 md:hover:bg-transparent md:border-0 md:hover:text-mbz-purple-700 md:p-0 dark:text-zinc-400 md:dark:hover:text-white dark:hover:bg-zinc-700 dark:hover:text-white md:dark:hover:bg-transparent dark:border-gray-700"
|
||||
>{{ t("Activities") }}</router-link
|
||||
>
|
||||
</li>
|
||||
<li class="m-auto">
|
||||
<router-link
|
||||
:to="{ name: RouteName.SEARCH, query: { contentType: 'GROUPS' } }"
|
||||
class="block py-2 pr-4 pl-3 text-zinc-700 border-b border-gray-100 hover:bg-zinc-50 md:hover:bg-transparent md:border-0 md:hover:text-mbz-purple-700 md:p-0 dark:text-zinc-400 md:dark:hover:text-white dark:hover:bg-zinc-700 dark:hover:text-white md:dark:hover:bg-transparent dark:border-gray-700"
|
||||
>{{ t("Groups") }}</router-link
|
||||
>
|
||||
</li>
|
||||
<li class="m-auto">
|
||||
<router-link
|
||||
:to="{ name: RouteName.EVENT_CALENDAR }"
|
||||
class="block relative py-2 pr-4 pl-3 text-zinc-700 border-b border-gray-100 hover:bg-zinc-50 md:hover:bg-transparent md:border-0 md:hover:text-mbz-purple-700 md:p-0 dark:text-zinc-400 md:dark:hover:text-white dark:hover:bg-zinc-700 dark:hover:text-white md:dark:hover:bg-transparent dark:border-gray-700"
|
||||
>{{ t("Calendar")
|
||||
}}<span class="absolute right-0 text-sm"
|
||||
><br />(beta)</span
|
||||
></router-link
|
||||
>
|
||||
</li>
|
||||
<li class="m-auto" v-if="currentActor?.id">
|
||||
<router-link
|
||||
:to="{ name: RouteName.MY_EVENTS }"
|
||||
class="block py-2 pr-4 pl-3 text-zinc-700 border-b border-gray-100 hover:bg-zinc-50 md:hover:bg-transparent md:border-0 md:hover:text-mbz-purple-700 md:p-0 dark:text-zinc-400 md:dark:hover:text-white dark:hover:bg-zinc-700 dark:hover:text-white md:dark:hover:bg-transparent dark:border-gray-700"
|
||||
>{{ t("My events") }}</router-link
|
||||
>
|
||||
</li>
|
||||
<li v-if="currentActor?.id">
|
||||
<li class="m-auto" v-if="currentActor?.id">
|
||||
<router-link
|
||||
:to="{ name: RouteName.MY_GROUPS }"
|
||||
class="block py-2 pr-4 pl-3 text-zinc-700 border-b border-gray-100 hover:bg-zinc-50 md:hover:bg-transparent md:border-0 md:hover:text-mbz-purple-700 md:p-0 dark:text-zinc-400 md:dark:hover:text-white dark:hover:bg-zinc-700 dark:hover:text-white md:dark:hover:bg-transparent dark:border-gray-700"
|
||||
>{{ t("My groups") }}</router-link
|
||||
>
|
||||
</li>
|
||||
<li v-if="!currentActor?.id">
|
||||
<li class="m-auto" v-if="!currentActor?.id">
|
||||
<router-link
|
||||
:to="{ name: RouteName.LOGIN }"
|
||||
class="block py-2 pr-4 pl-3 text-zinc-700 border-b border-gray-100 hover:bg-zinc-50 md:hover:bg-transparent md:border-0 md:hover:text-mbz-purple-700 md:p-0 dark:text-zinc-400 md:dark:hover:text-white dark:hover:bg-zinc-700 dark:hover:text-white md:dark:hover:bg-transparent dark:border-gray-700"
|
||||
>{{ t("Login") }}</router-link
|
||||
>
|
||||
</li>
|
||||
<li v-if="!currentActor?.id && canRegister">
|
||||
<li class="m-auto" v-if="!currentActor?.id && canRegister">
|
||||
<router-link
|
||||
:to="{ name: RouteName.REGISTER }"
|
||||
class="block py-2 pr-4 pl-3 text-zinc-700 border-b border-gray-100 hover:bg-zinc-50 md:hover:bg-transparent md:border-0 md:hover:text-mbz-purple-700 md:p-0 dark:text-zinc-400 md:dark:hover:text-white dark:hover:bg-zinc-700 dark:hover:text-white md:dark:hover:bg-transparent dark:border-gray-700"
|
||||
>{{ t("Register") }}</router-link
|
||||
>
|
||||
</li>
|
||||
|
||||
<search-fields
|
||||
v-if="!showMobileMenu"
|
||||
class="m-auto w-auto"
|
||||
v-model:search="search"
|
||||
v-model:location="location"
|
||||
/>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@@ -219,7 +275,7 @@
|
||||
import MobilizonLogo from "@/components/MobilizonLogo.vue";
|
||||
import { ICurrentUserRole } from "@/types/enums";
|
||||
import { logout } from "../utils/auth";
|
||||
import { IPerson, displayName } from "../types/actor";
|
||||
import { displayName } from "../types/actor";
|
||||
import RouteName from "../router/name";
|
||||
import { computed, onMounted, ref, watch } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
@@ -234,7 +290,10 @@ import {
|
||||
import { useLazyQuery, useMutation } from "@vue/apollo-composable";
|
||||
import { UPDATE_DEFAULT_ACTOR } from "@/graphql/actor";
|
||||
import { changeIdentity } from "@/utils/identity";
|
||||
import { useRegistrationConfig } from "@/composition/apollo/config";
|
||||
import {
|
||||
useRegistrationConfig,
|
||||
useIsLongEvents,
|
||||
} from "@/composition/apollo/config";
|
||||
import { useOruga } from "@oruga-ui/oruga-next";
|
||||
import {
|
||||
UNREAD_ACTOR_CONVERSATIONS,
|
||||
@@ -242,6 +301,8 @@ import {
|
||||
} from "@/graphql/user";
|
||||
import { ICurrentUser } from "@/types/current-user.model";
|
||||
|
||||
const { islongEvents } = useIsLongEvents();
|
||||
|
||||
const { currentUser } = useCurrentUserClient();
|
||||
const { currentActor } = useCurrentActorClient();
|
||||
|
||||
|
||||
@@ -90,7 +90,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
import { computed, inject, ref } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useMutation } from "@vue/apollo-composable";
|
||||
|
||||
@@ -1,21 +1,8 @@
|
||||
<template>
|
||||
<footer
|
||||
class="bg-violet-2 color-secondary flex flex-col items-center py-2 px-3"
|
||||
class="bg-violet-2 color-secondary flex flex-col items-center py-3 px-3"
|
||||
ref="footer"
|
||||
>
|
||||
<picture class="flex max-w-xl">
|
||||
<source
|
||||
:srcset="`/img/pics/footer_${random}-1024w.webp 1x, /img/pics/footer_${random}-1920w.webp 2x`"
|
||||
type="image/webp"
|
||||
/>
|
||||
<img
|
||||
:src="`/img/pics/footer_${random}-1024w.webp`"
|
||||
alt=""
|
||||
width="1024"
|
||||
height="428"
|
||||
loading="lazy"
|
||||
/>
|
||||
</picture>
|
||||
<ul
|
||||
class="inline-flex flex-wrap justify-around gap-3 text-lg text-white underline decoration-yellow-1"
|
||||
>
|
||||
@@ -92,15 +79,11 @@ import { saveLocaleData } from "@/utils/auth";
|
||||
import { loadLanguageAsync } from "@/utils/i18n";
|
||||
import RouteName from "../router/name";
|
||||
import langs from "../i18n/langs.json";
|
||||
import { computed, watch } from "vue";
|
||||
import { watch } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const { locale, t } = useI18n({ useScope: "global" });
|
||||
|
||||
const random = computed((): number => {
|
||||
return Math.floor(Math.random() * 4) + 1;
|
||||
});
|
||||
|
||||
watch(locale, async () => {
|
||||
if (locale) {
|
||||
console.debug("Setting locale from footer");
|
||||
@@ -113,3 +96,9 @@ const isLangSelected = (lang: string): boolean => {
|
||||
return lang === locale.value;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
footer > ul > li {
|
||||
margin: auto 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -45,7 +45,7 @@ import { LEAVE_EVENT } from "../../graphql/event";
|
||||
import { computed, ref, watchEffect } from "vue";
|
||||
import { useMutation } from "@vue/apollo-composable";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
import { IActor } from "@/types/actor";
|
||||
import { IEvent } from "@/types/event.model";
|
||||
import { useAnonymousActorId } from "@/composition/apollo/config";
|
||||
|
||||
@@ -70,7 +70,7 @@ import { CONFIRM_PARTICIPATION } from "../../graphql/event";
|
||||
import { computed, ref, watchEffect } from "vue";
|
||||
import { useMutation } from "@vue/apollo-composable";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
|
||||
const { t } = useI18n({ useScope: "global" });
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<script lang="ts" setup>
|
||||
import RedirectWithAccount from "@/components/Utils/RedirectWithAccount.vue";
|
||||
import { useFetchEvent } from "@/composition/apollo/event";
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
import { computed } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
|
||||
@@ -146,7 +146,7 @@ import { useFetchEventBasic } from "@/composition/apollo/event";
|
||||
import { useAnonymousActorId } from "@/composition/apollo/config";
|
||||
import { computed, reactive, ref } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
import { useMutation } from "@vue/apollo-composable";
|
||||
|
||||
const error = ref<boolean | string>(false);
|
||||
|
||||
@@ -99,7 +99,7 @@ import { useFetchEvent } from "@/composition/apollo/event";
|
||||
import { useAnonymousParticipationConfig } from "@/composition/apollo/config";
|
||||
import { computed } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const props = defineProps<{ uuid: string }>();
|
||||
|
||||
@@ -173,6 +173,8 @@ const icons: Record<string, () => Promise<any>> = {
|
||||
import(
|
||||
`../../../node_modules/vue-material-design-icons/CalendarRemove.vue`
|
||||
),
|
||||
CalendarStar: () =>
|
||||
import(`../../../node_modules/vue-material-design-icons/CalendarStar.vue`),
|
||||
FileDocumentEdit: () =>
|
||||
import(
|
||||
`../../../node_modules/vue-material-design-icons/FileDocumentEdit.vue`
|
||||
|
||||
@@ -4,11 +4,15 @@ import {
|
||||
ANONYMOUS_ACTOR_ID,
|
||||
ANONYMOUS_PARTICIPATION_CONFIG,
|
||||
ANONYMOUS_REPORTS_CONFIG,
|
||||
DEFAULT_PICTURE,
|
||||
DEMO_MODE,
|
||||
LONG_EVENTS,
|
||||
EVENT_CATEGORIES,
|
||||
EVENT_PARTICIPANTS,
|
||||
FEATURES,
|
||||
GEOCODING_AUTOCOMPLETE,
|
||||
COLORS,
|
||||
INSTANCE_LOGO,
|
||||
LOCATION,
|
||||
MAPS_TILES,
|
||||
REGISTRATIONS,
|
||||
@@ -76,6 +80,36 @@ export function useInstanceName() {
|
||||
return { instanceName, error, loading };
|
||||
}
|
||||
|
||||
export function useInstanceLogoUrl() {
|
||||
const { result, error, loading } = useQuery<{
|
||||
config: Pick<IConfig, "instanceLogo">;
|
||||
}>(INSTANCE_LOGO);
|
||||
|
||||
const instanceLogoUrl = computed(
|
||||
() => result.value?.config?.instanceLogo?.url
|
||||
);
|
||||
return { instanceLogoUrl, error, loading };
|
||||
}
|
||||
|
||||
export function useColors() {
|
||||
const { result, error, loading } = useQuery<{
|
||||
config: Pick<IConfig, "primaryColor" | "secondaryColor">;
|
||||
}>(COLORS);
|
||||
|
||||
const primaryColor = computed(() => result.value?.config?.primaryColor);
|
||||
const secondaryColor = computed(() => result.value?.config?.secondaryColor);
|
||||
return { primaryColor, secondaryColor, error, loading };
|
||||
}
|
||||
|
||||
export function useDefaultPicture() {
|
||||
const { result, error, loading } = useQuery<{
|
||||
config: Pick<IConfig, "defaultPicture">;
|
||||
}>(DEFAULT_PICTURE);
|
||||
|
||||
const defaultPicture = computed(() => result.value?.config?.defaultPicture);
|
||||
return { defaultPicture, error, loading };
|
||||
}
|
||||
|
||||
export function useAnonymousActorId() {
|
||||
const { result, error, loading } = useQuery<{
|
||||
config: Pick<IConfig, "anonymous">;
|
||||
@@ -188,6 +222,15 @@ export function useIsDemoMode() {
|
||||
return { isDemoMode, error, loading };
|
||||
}
|
||||
|
||||
export function useIsLongEvents() {
|
||||
const { result, error, loading } = useQuery<{
|
||||
config: Pick<IConfig, "longEvents">;
|
||||
}>(LONG_EVENTS);
|
||||
|
||||
const islongEvents = computed(() => result.value?.config.longEvents);
|
||||
return { islongEvents, error, loading };
|
||||
}
|
||||
|
||||
export function useAnalytics() {
|
||||
const { result, error, loading } = useQuery<{
|
||||
config: Pick<IConfig, "analytics">;
|
||||
|
||||
@@ -4,6 +4,12 @@ export const useHost = (): string => {
|
||||
return window.location.hostname;
|
||||
};
|
||||
|
||||
export const useDefaultMaxSize = (): number | undefined => {
|
||||
const { uploadLimits } = useUploadLimits();
|
||||
|
||||
return uploadLimits.value?.default;
|
||||
};
|
||||
|
||||
export const useAvatarMaxSize = (): number | undefined => {
|
||||
const { uploadLimits } = useUploadLimits();
|
||||
|
||||
|
||||
@@ -4,6 +4,10 @@ function parseDateTime(value: string): Date {
|
||||
return new Date(value);
|
||||
}
|
||||
|
||||
function formatDateISOStringWithoutTime(value: string): string {
|
||||
return parseDateTime(value).toISOString().split("T")[0];
|
||||
}
|
||||
|
||||
function formatDateString(value: string): string {
|
||||
return parseDateTime(value).toLocaleString(locale(), {
|
||||
weekday: "long",
|
||||
@@ -76,4 +80,9 @@ function formatDateTimeString(
|
||||
|
||||
const locale = () => i18n.global.locale.replace("_", "-");
|
||||
|
||||
export { formatDateString, formatTimeString, formatDateTimeString };
|
||||
export {
|
||||
formatDateISOStringWithoutTime,
|
||||
formatDateString,
|
||||
formatTimeString,
|
||||
formatDateTimeString,
|
||||
};
|
||||
|
||||
@@ -195,6 +195,23 @@ export const ADMIN_SETTINGS_FRAGMENT = gql`
|
||||
instanceLongDescription
|
||||
instanceSlogan
|
||||
contact
|
||||
instanceLogo {
|
||||
id
|
||||
url
|
||||
name
|
||||
}
|
||||
instanceFavicon {
|
||||
id
|
||||
url
|
||||
name
|
||||
}
|
||||
defaultPicture {
|
||||
id
|
||||
url
|
||||
name
|
||||
}
|
||||
primaryColor
|
||||
secondaryColor
|
||||
instanceTerms
|
||||
instanceTermsType
|
||||
instanceTermsUrl
|
||||
@@ -223,6 +240,11 @@ export const SAVE_ADMIN_SETTINGS = gql`
|
||||
$instanceLongDescription: String
|
||||
$instanceSlogan: String
|
||||
$contact: String
|
||||
$instanceLogo: MediaInput
|
||||
$instanceFavicon: MediaInput
|
||||
$defaultPicture: MediaInput
|
||||
$primaryColor: String
|
||||
$secondaryColor: String
|
||||
$instanceTerms: String
|
||||
$instanceTermsType: InstanceTermsType
|
||||
$instanceTermsUrl: String
|
||||
@@ -239,6 +261,11 @@ export const SAVE_ADMIN_SETTINGS = gql`
|
||||
instanceLongDescription: $instanceLongDescription
|
||||
instanceSlogan: $instanceSlogan
|
||||
contact: $contact
|
||||
instanceLogo: $instanceLogo
|
||||
instanceFavicon: $instanceFavicon
|
||||
defaultPicture: $defaultPicture
|
||||
primaryColor: $primaryColor
|
||||
secondaryColor: $secondaryColor
|
||||
instanceTerms: $instanceTerms
|
||||
instanceTermsType: $instanceTermsType
|
||||
instanceTermsUrl: $instanceTermsUrl
|
||||
|
||||
@@ -10,8 +10,24 @@ export const CONFIG = gql`
|
||||
registrationsOpen
|
||||
registrationsAllowlist
|
||||
demoMode
|
||||
longEvents
|
||||
countryCode
|
||||
languages
|
||||
primaryColor
|
||||
secondaryColor
|
||||
instanceLogo {
|
||||
url
|
||||
}
|
||||
defaultPicture {
|
||||
id
|
||||
url
|
||||
name
|
||||
metadata {
|
||||
width
|
||||
height
|
||||
blurhash
|
||||
}
|
||||
}
|
||||
eventCategories {
|
||||
id
|
||||
label
|
||||
@@ -425,6 +441,14 @@ export const DEMO_MODE = gql`
|
||||
}
|
||||
`;
|
||||
|
||||
export const LONG_EVENTS = gql`
|
||||
query LongEvents {
|
||||
config {
|
||||
longEvents
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const ANALYTICS = gql`
|
||||
query Analytics {
|
||||
config {
|
||||
@@ -454,6 +478,42 @@ export const SEARCH_CONFIG = gql`
|
||||
}
|
||||
`;
|
||||
|
||||
export const INSTANCE_LOGO = gql`
|
||||
query InstanceLogo {
|
||||
config {
|
||||
instanceLogo {
|
||||
url
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const COLORS = gql`
|
||||
query Colors {
|
||||
config {
|
||||
primaryColor
|
||||
secondaryColor
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const DEFAULT_PICTURE = gql`
|
||||
query DefaultPicture {
|
||||
config {
|
||||
defaultPicture {
|
||||
id
|
||||
url
|
||||
name
|
||||
metadata {
|
||||
width
|
||||
height
|
||||
blurhash
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const REGISTRATIONS = gql`
|
||||
query Registrations {
|
||||
config {
|
||||
|
||||
@@ -59,40 +59,6 @@ const FULL_EVENT_FRAGMENT = gql`
|
||||
tags {
|
||||
...TagFragment
|
||||
}
|
||||
relatedEvents {
|
||||
id
|
||||
uuid
|
||||
title
|
||||
beginsOn
|
||||
endsOn
|
||||
status
|
||||
language
|
||||
picture {
|
||||
id
|
||||
url
|
||||
name
|
||||
metadata {
|
||||
width
|
||||
height
|
||||
blurhash
|
||||
}
|
||||
}
|
||||
physicalAddress {
|
||||
...AdressFragment
|
||||
}
|
||||
organizerActor {
|
||||
...ActorFragment
|
||||
}
|
||||
attributedTo {
|
||||
...ActorFragment
|
||||
}
|
||||
options {
|
||||
...EventOptions
|
||||
}
|
||||
tags {
|
||||
...TagFragment
|
||||
}
|
||||
}
|
||||
options {
|
||||
...EventOptions
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ export const EVENT_OPTIONS_FRAGMENT = gql`
|
||||
remainingAttendeeCapacity
|
||||
showRemainingAttendeeCapacity
|
||||
anonymousParticipation
|
||||
hideNumberOfParticipants
|
||||
showStartTime
|
||||
showEndTime
|
||||
timezone
|
||||
|
||||
@@ -33,6 +33,7 @@ export const SEARCH_EVENTS_AND_GROUPS = gql`
|
||||
$searchTarget: SearchTarget
|
||||
$beginsOn: DateTime
|
||||
$endsOn: DateTime
|
||||
$longevents: Boolean
|
||||
$bbox: String
|
||||
$zoom: Int
|
||||
$eventPage: Int
|
||||
@@ -54,6 +55,7 @@ export const SEARCH_EVENTS_AND_GROUPS = gql`
|
||||
searchTarget: $searchTarget
|
||||
beginsOn: $beginsOn
|
||||
endsOn: $endsOn
|
||||
longevents: $longevents
|
||||
bbox: $bbox
|
||||
zoom: $zoom
|
||||
page: $eventPage
|
||||
@@ -67,6 +69,7 @@ export const SEARCH_EVENTS_AND_GROUPS = gql`
|
||||
title
|
||||
uuid
|
||||
beginsOn
|
||||
endsOn
|
||||
picture {
|
||||
id
|
||||
url
|
||||
@@ -152,6 +155,7 @@ export const SEARCH_EVENTS = gql`
|
||||
$endsOn: DateTime
|
||||
$eventPage: Int
|
||||
$limit: Int
|
||||
$longevents: Boolean
|
||||
) {
|
||||
searchEvents(
|
||||
location: $location
|
||||
@@ -164,6 +168,7 @@ export const SEARCH_EVENTS = gql`
|
||||
endsOn: $endsOn
|
||||
page: $eventPage
|
||||
limit: $limit
|
||||
longevents: $longevents
|
||||
) {
|
||||
total
|
||||
elements {
|
||||
@@ -201,6 +206,56 @@ export const SEARCH_EVENTS = gql`
|
||||
${ACTOR_FRAGMENT}
|
||||
`;
|
||||
|
||||
export const SEARCH_CALENDAR_EVENTS = gql`
|
||||
query SearchEvents(
|
||||
$beginsOn: DateTime
|
||||
$endsOn: DateTime
|
||||
$eventPage: Int
|
||||
$limit: Int
|
||||
) {
|
||||
searchEvents(
|
||||
beginsOn: $beginsOn
|
||||
endsOn: $endsOn
|
||||
page: $eventPage
|
||||
limit: $limit
|
||||
) {
|
||||
total
|
||||
elements {
|
||||
id
|
||||
title
|
||||
uuid
|
||||
beginsOn
|
||||
endsOn
|
||||
picture {
|
||||
id
|
||||
url
|
||||
}
|
||||
status
|
||||
tags {
|
||||
...TagFragment
|
||||
}
|
||||
physicalAddress {
|
||||
...AdressFragment
|
||||
}
|
||||
organizerActor {
|
||||
...ActorFragment
|
||||
}
|
||||
attributedTo {
|
||||
...ActorFragment
|
||||
}
|
||||
options {
|
||||
...EventOptions
|
||||
}
|
||||
__typename
|
||||
}
|
||||
}
|
||||
}
|
||||
${EVENT_OPTIONS_FRAGMENT}
|
||||
${TAG_FRAGMENT}
|
||||
${ADDRESS_FRAGMENT}
|
||||
${ACTOR_FRAGMENT}
|
||||
`;
|
||||
|
||||
export const SEARCH_GROUPS = gql`
|
||||
query SearchGroups(
|
||||
$location: String
|
||||
|
||||
@@ -168,6 +168,7 @@
|
||||
"By transit": "Mit öffentlichen Verkehrsmitteln",
|
||||
"By {group}": "Von {group}",
|
||||
"By {username}": "Von {username}",
|
||||
"Calendar": "Kalender",
|
||||
"Can be an email or a link, or just plain text.": "Dies kann eine E-Mail-Adresse oder ein Link sein. Oder einfach ein Freitext.",
|
||||
"Cancel": "Abbrechen",
|
||||
"Cancel anonymous participation": "Anonyme Teilnahme stornieren",
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
"Back to previous page": "Back to previous page",
|
||||
"Before you can login, you need to click on the link inside it to validate your account.": "Before you can login, you need to click on the link inside it to validate your account.",
|
||||
"By {username}": "By {username}",
|
||||
"Calendar": "Calendar",
|
||||
"Cancel anonymous participation": "Cancel anonymous participation",
|
||||
"Cancel creation": "Cancel creation",
|
||||
"Cancel edition": "Cancel edition",
|
||||
@@ -204,6 +205,7 @@
|
||||
"No events found": "No events found",
|
||||
"No group found": "No group found",
|
||||
"No groups found": "No groups found",
|
||||
"No activities found": "No activities found",
|
||||
"No instance follows your instance yet.": "No instance follows your instance yet.",
|
||||
"No instance to approve|Approve instance|Approve {number} instances": "No instance to approve|Approve instance|Approve {number} instances",
|
||||
"No instance to reject|Reject instance|Reject {number} instances": "No instance to reject|Reject instance|Reject {number} instances",
|
||||
@@ -416,6 +418,14 @@
|
||||
"No one is participating|One person participating|{going} people participating": "No one is participating|One person participating|{going} people participating",
|
||||
"Date and time": "Date and time",
|
||||
"Location": "Location",
|
||||
"Logo": "Logo",
|
||||
"Logo of the instance. Defaults to the upstream Mobilizon logo.": "Logo of the instance. Defaults to the upstream Mobilizon logo.",
|
||||
"Favicon": "Favicon",
|
||||
"Browser tab icon and PWA icon of the instance. Defaults to the upstream Mobilizon icon.": "Browser tab icon and PWA icon of the instance. Defaults to the upstream Mobilizon icon.",
|
||||
"Default Picture": "Default Picture",
|
||||
"Default picture when an event or group doesn't have one.": "Default picture when an event or group doesn't have one.",
|
||||
"Primary Color": "Primary Color",
|
||||
"Secondary Color": "Secondary Color",
|
||||
"No resources selected": "No resources selected|One resources selected|{count} resources selected",
|
||||
"You have been invited by {invitedBy} to the following group:": "You have been invited by {invitedBy} to the following group:",
|
||||
"Accept": "Accept",
|
||||
@@ -1394,6 +1404,7 @@
|
||||
"Reported content": "Reported content",
|
||||
"No results found": "No results found",
|
||||
"{eventsCount} events found": "No events found|One event found|{eventsCount} events found",
|
||||
"{eventsCount} activities found": "No activities found|One activity found|{eventsCount} activities found",
|
||||
"{groupsCount} groups found": "No groups found|One group found|{groupsCount} groups found",
|
||||
"{resultsCount} results found": "No results found|On result found|{resultsCount} results found",
|
||||
"Loading map": "Loading map",
|
||||
@@ -1644,5 +1655,6 @@
|
||||
"Only instances with an application actor can be followed": "Only instances with an application actor can be followed",
|
||||
"Domain or instance name": "Domain or instance name",
|
||||
"You need to enter a text": "You need to enter a text",
|
||||
"Error while adding tag: {error}": "Error while adding tag: {error}"
|
||||
}
|
||||
"Error while adding tag: {error}": "Error while adding tag: {error}",
|
||||
"From this instance only": "From this instance only"
|
||||
}
|
||||
|
||||
@@ -627,6 +627,14 @@
|
||||
"Local times ({timezone})": "Heures locales ({timezone})",
|
||||
"Locality": "Commune",
|
||||
"Location": "Lieu",
|
||||
"Logo": "Logo",
|
||||
"Logo of the instance. Defaults to the upstream Mobilizon logo.": "Logo de l'instance.",
|
||||
"Favicon": "Favicon",
|
||||
"Browser tab icon and PWA icon of the instance. Defaults to the upstream Mobilizon icon.": "Icône de l'onglet du navigateur et de la progressive web app.",
|
||||
"Default Picture": "Image par défaut",
|
||||
"Default picture when an event or group doesn't have one.": "Image par défaut quand un évènement ou groupe n'en a pas.",
|
||||
"Primary Color": "Couleur primaire",
|
||||
"Secondary Color": "Couleur secondaire",
|
||||
"Log in": "Se connecter",
|
||||
"Log out": "Se déconnecter",
|
||||
"Login": "Se connecter",
|
||||
@@ -716,6 +724,7 @@
|
||||
"No end date": "Pas de date de fin",
|
||||
"No event found at this address": "Aucun événement trouvé à cette addresse",
|
||||
"No events found": "Aucun événement trouvé",
|
||||
"No activities found": "Aucun activité trouvé",
|
||||
"No events found for {search}": "Aucun événement trouvé pour {search}",
|
||||
"No follower matches the filters": "Aucun·e abonné·e ne correspond aux filtres",
|
||||
"No group found": "Aucun groupe trouvé",
|
||||
@@ -1544,6 +1553,7 @@
|
||||
"{count} participants": "Aucun·e participant·e | Un·e participant·e | {count} participant·e·s",
|
||||
"{count} requests waiting": "Une demande en attente|{count} demandes en attente",
|
||||
"{eventsCount} events found": "Aucun événement trouvé|Un événement trouvé|{eventsCount} événements trouvés",
|
||||
"{eventsCount} activities found": "Aucune activité trouvé|Une activité trouvé|{eventsCount} activités trouvés",
|
||||
"{folder} - Resources": "{folder} - Ressources",
|
||||
"{groupsCount} groups found": "Aucun groupe trouvé|Un groupe trouvé|{groupsCount} groupes trouvés",
|
||||
"{group} activity timeline": "Timeline de l'activité de {group}",
|
||||
@@ -1638,5 +1648,6 @@
|
||||
"Only instances with an application actor can be followed": "Seules les instances avec un acteur application peuvent être suivies",
|
||||
"Domain or instance name": "Domaine ou nom de l'instance",
|
||||
"You need to enter a text": "Vous devez entrer un texte",
|
||||
"Error while adding tag: {error}": "Erreur lors de l'ajout d'un tag : {error}"
|
||||
"Error while adding tag: {error}": "Erreur lors de l'ajout d'un tag : {error}",
|
||||
"From this instance only": "Depuis cette instance uniquement"
|
||||
}
|
||||
|
||||
15
src/main.ts
15
src/main.ts
@@ -57,6 +57,21 @@ apolloClient
|
||||
})
|
||||
.then(({ data: configData }) => {
|
||||
instanceName.value = configData.config?.name;
|
||||
|
||||
const primaryColor = configData.config?.primaryColor;
|
||||
if (primaryColor) {
|
||||
document.documentElement.style.setProperty(
|
||||
"--custom-primary",
|
||||
primaryColor
|
||||
);
|
||||
}
|
||||
const secondaryColor = configData.config?.secondaryColor;
|
||||
if (secondaryColor) {
|
||||
document.documentElement.style.setProperty(
|
||||
"--custom-secondary",
|
||||
secondaryColor
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
const head = createHead();
|
||||
|
||||
@@ -34,6 +34,8 @@ export const orugaConfig = {
|
||||
sizeClass: (size: string) => {
|
||||
return `input-size-${size}`;
|
||||
},
|
||||
iconLeftSpaceClass: "input-iconspace-left",
|
||||
iconRightSpaceClass: "input-iconspace-right",
|
||||
},
|
||||
taginput: {
|
||||
itemClass: "taginput-item",
|
||||
|
||||
@@ -7,9 +7,11 @@ const participations = () => import("@/views/Event/ParticipantsView.vue");
|
||||
const editEvent = () => import("@/views/Event/EditView.vue");
|
||||
const event = () => import("@/views/Event/EventView.vue");
|
||||
const myEvents = () => import("@/views/Event/MyEventsView.vue");
|
||||
const eventCalendar = () => import("@/views/Event/CalendarView.vue");
|
||||
|
||||
export enum EventRouteName {
|
||||
EVENT_LIST = "EventList",
|
||||
EVENT_CALENDAR = "EventCalendar",
|
||||
CREATE_EVENT = "CreateEvent",
|
||||
MY_EVENTS = "MyEvents",
|
||||
EDIT_EVENT = "EditEvent",
|
||||
@@ -26,6 +28,14 @@ export enum EventRouteName {
|
||||
}
|
||||
|
||||
export const eventRoutes: RouteRecordRaw[] = [
|
||||
{
|
||||
path: "/events/calendar",
|
||||
name: EventRouteName.EVENT_CALENDAR,
|
||||
component: eventCalendar,
|
||||
meta: {
|
||||
requiredAuth: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "/events/create",
|
||||
name: EventRouteName.CREATE_EVENT,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { IEvent } from "@/types/event.model";
|
||||
import type { IMedia } from "@/types/media.model";
|
||||
import type { IGroup } from "./actor";
|
||||
import { InstancePrivacyType, InstanceTermsType } from "./enums";
|
||||
|
||||
@@ -25,6 +26,10 @@ export interface IAdminSettings {
|
||||
instanceSlogan: string;
|
||||
instanceLongDescription: string;
|
||||
contact: string;
|
||||
instanceLogo: IMedia | null;
|
||||
defaultPicture: IMedia | null;
|
||||
primaryColor: string;
|
||||
secondaryColor: string;
|
||||
instanceTerms: string;
|
||||
instanceTermsType: InstanceTermsType;
|
||||
instanceTermsUrl: string | null;
|
||||
|
||||
@@ -37,10 +37,15 @@ export interface IConfig {
|
||||
longDescription: string;
|
||||
contact: string;
|
||||
slogan: string;
|
||||
instanceLogo: { url: string };
|
||||
defaultPicture: { url: string };
|
||||
primaryColor: string;
|
||||
secondaryColor: string;
|
||||
|
||||
registrationsOpen: boolean;
|
||||
registrationsAllowlist: boolean;
|
||||
demoMode: boolean;
|
||||
longEvents: boolean;
|
||||
countryCode: string;
|
||||
eventCategories: { id: string; label: string }[];
|
||||
languages: string[];
|
||||
|
||||
@@ -134,6 +134,8 @@ export enum SearchTabs {
|
||||
export enum ContentType {
|
||||
ALL = "ALL",
|
||||
EVENTS = "EVENTS",
|
||||
SHORTEVENTS = "SHORTEVENTS",
|
||||
LONGEVENTS = "LONGEVENTS",
|
||||
GROUPS = "GROUPS",
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ export interface IEventOptions {
|
||||
program: string;
|
||||
commentModeration: CommentModeration;
|
||||
showParticipationPrice: boolean;
|
||||
hideNumberOfParticipants: boolean;
|
||||
showStartTime: boolean;
|
||||
showEndTime: boolean;
|
||||
timezone: string | null;
|
||||
@@ -53,6 +54,8 @@ export class EventOptions implements IEventOptions {
|
||||
|
||||
showParticipationPrice = false;
|
||||
|
||||
hideNumberOfParticipants = false;
|
||||
|
||||
showStartTime = true;
|
||||
|
||||
showEndTime = true;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { Ref } from "vue";
|
||||
|
||||
export interface IMedia {
|
||||
id: string;
|
||||
url: string;
|
||||
@@ -21,3 +23,9 @@ export interface IMediaMetadata {
|
||||
height?: number;
|
||||
blurhash?: string;
|
||||
}
|
||||
|
||||
export interface IModifiableMedia {
|
||||
file: Ref<File | null>;
|
||||
firstHash: string | null;
|
||||
hash: string | null;
|
||||
}
|
||||
|
||||
@@ -69,10 +69,15 @@ function formatDateTimeForEvent(dateTime: Date, locale: Locale): string {
|
||||
return format(dateTime, "PPp", { locale });
|
||||
}
|
||||
|
||||
function formatDateForEvent(dateTime: Date, locale: Locale): string {
|
||||
return format(dateTime, "PP", { locale });
|
||||
}
|
||||
|
||||
export {
|
||||
localeMonthNames,
|
||||
localeShortWeekDayNames,
|
||||
formatBytes,
|
||||
roundToNearestMinute,
|
||||
formatDateTimeForEvent,
|
||||
formatDateForEvent,
|
||||
};
|
||||
|
||||
22
src/utils/head.ts
Normal file
22
src/utils/head.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { computed } from "vue";
|
||||
import { provideApolloClient, useQuery } from "@vue/apollo-composable";
|
||||
import { useHead as unHead } from "@unhead/vue";
|
||||
import { apolloClient } from "@/vue-apollo";
|
||||
import { IConfig } from "@/types/config.model";
|
||||
import { ABOUT } from "@/graphql/config";
|
||||
|
||||
const { result } = provideApolloClient(apolloClient)(() =>
|
||||
useQuery<{ config: Pick<IConfig, "name"> }>(ABOUT)
|
||||
);
|
||||
const instanceName = computed(() => result.value?.config?.name);
|
||||
|
||||
export function useHead(args: any) {
|
||||
return unHead({
|
||||
...args,
|
||||
title: computed(() =>
|
||||
args?.title?.value
|
||||
? `${args.title.value} - ${instanceName.value}`
|
||||
: instanceName.value
|
||||
),
|
||||
});
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { IMedia } from "@/types/media.model";
|
||||
import { IMedia, IModifiableMedia } from "@/types/media.model";
|
||||
import { ref, watch } from "vue";
|
||||
|
||||
export async function buildFileFromIMedia(
|
||||
obj: IMedia | null | undefined
|
||||
@@ -29,18 +30,83 @@ export function buildFileVariable(
|
||||
};
|
||||
}
|
||||
|
||||
export function readFileAsync(
|
||||
file: File
|
||||
): Promise<string | ArrayBuffer | null> {
|
||||
export function readFileAsync(file: File): Promise<ArrayBuffer | null> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onload = () => {
|
||||
resolve(reader.result);
|
||||
resolve(reader.result as ArrayBuffer);
|
||||
};
|
||||
|
||||
reader.onerror = reject;
|
||||
|
||||
reader.readAsBinaryString(file);
|
||||
reader.readAsArrayBuffer(file);
|
||||
});
|
||||
}
|
||||
|
||||
export async function fileHash(file: File): Promise<string | null> {
|
||||
const data = await readFileAsync(file);
|
||||
if (data === null) return null;
|
||||
const hash = await crypto.subtle.digest("SHA-1", data);
|
||||
const b64Hash = btoa(
|
||||
Array.from(new Uint8Array(hash))
|
||||
.map((b) => String.fromCharCode(b))
|
||||
.join("")
|
||||
);
|
||||
return b64Hash;
|
||||
}
|
||||
|
||||
export function initWrappedMedia(): IModifiableMedia {
|
||||
return {
|
||||
file: ref<File | null>(null),
|
||||
firstHash: null,
|
||||
hash: null,
|
||||
};
|
||||
}
|
||||
|
||||
export async function loadWrappedMedia(
|
||||
modifiableMedia: IModifiableMedia,
|
||||
media: IMedia | null
|
||||
) {
|
||||
watch(modifiableMedia.file, async () => {
|
||||
if (modifiableMedia.file.value) {
|
||||
modifiableMedia.hash = await fileHash(modifiableMedia.file.value);
|
||||
} else {
|
||||
modifiableMedia.hash = null;
|
||||
}
|
||||
});
|
||||
try {
|
||||
modifiableMedia.file.value = await buildFileFromIMedia(media);
|
||||
} catch (e) {
|
||||
console.error("catched error while building media", e);
|
||||
}
|
||||
if (modifiableMedia.file.value) {
|
||||
modifiableMedia.firstHash = await fileHash(modifiableMedia.file.value);
|
||||
}
|
||||
}
|
||||
|
||||
export function asMediaInput(
|
||||
mmedia: IModifiableMedia,
|
||||
name: string,
|
||||
fallbackId: number
|
||||
): any {
|
||||
const ret = {
|
||||
[name]: {},
|
||||
};
|
||||
if (mmedia.file.value) {
|
||||
if (mmedia.firstHash != mmedia.hash) {
|
||||
ret[name] = {
|
||||
media: {
|
||||
name: mmedia.file.value?.name,
|
||||
alt: "",
|
||||
file: mmedia.file.value,
|
||||
},
|
||||
};
|
||||
} else {
|
||||
ret[name] = {
|
||||
mediaId: fallbackId,
|
||||
};
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
@@ -123,7 +123,7 @@ import { IStatistics } from "../../types/statistics.model";
|
||||
import { useQuery } from "@vue/apollo-composable";
|
||||
import { computed } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
|
||||
const { result: configResult } = useQuery<{ config: IConfig }>(ABOUT);
|
||||
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useQuery } from "@vue/apollo-composable";
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
import { computed } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { ABOUT } from "../../graphql/config";
|
||||
|
||||
@@ -14,7 +14,7 @@ import { PRIVACY } from "@/graphql/config";
|
||||
import { IConfig } from "@/types/config.model";
|
||||
import { InstancePrivacyType } from "@/types/enums";
|
||||
import { useQuery } from "@vue/apollo-composable";
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
import { computed, watch } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
import { RULES } from "@/graphql/config";
|
||||
import { IConfig } from "@/types/config.model";
|
||||
import { useQuery } from "@vue/apollo-composable";
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
import { computed } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ import { TERMS } from "@/graphql/config";
|
||||
import { IConfig } from "@/types/config.model";
|
||||
import { InstanceTermsType } from "@/types/enums";
|
||||
import { useQuery } from "@vue/apollo-composable";
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
import { computed, watch } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
|
||||
@@ -113,7 +113,7 @@ import { useQuery } from "@vue/apollo-composable";
|
||||
import { computed } from "vue";
|
||||
import { useCurrentUserClient } from "@/composition/apollo/user";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
|
||||
const { currentUser } = useCurrentUserClient();
|
||||
|
||||
|
||||
@@ -140,7 +140,7 @@ import { useRouter } from "vue-router";
|
||||
import { registerAccount } from "@/composition/apollo/user";
|
||||
import { convertToUsername } from "@/utils/username";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
import { getValueFromMeta } from "@/utils/html";
|
||||
|
||||
const props = withDefaults(
|
||||
|
||||
@@ -224,7 +224,7 @@ import { Dialog } from "@/plugins/dialog";
|
||||
import { Notifier } from "@/plugins/notifier";
|
||||
import { AbsintheGraphQLErrors } from "@/types/errors.model";
|
||||
import { ICurrentUser } from "@/types/current-user.model";
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
|
||||
const { t } = useI18n({ useScope: "global" });
|
||||
const router = useRouter();
|
||||
|
||||
@@ -336,7 +336,7 @@ import EmptyContent from "../../components/Utils/EmptyContent.vue";
|
||||
import { ApolloCache, FetchResult } from "@apollo/client/core";
|
||||
import { useMutation, useQuery } from "@vue/apollo-composable";
|
||||
import { computed, inject } from "vue";
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
import { integerTransformer, useRouteQuery } from "vue-use-route-query";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import {
|
||||
|
||||
@@ -319,7 +319,7 @@ import { MemberRole } from "@/types/enums";
|
||||
import cloneDeep from "lodash/cloneDeep";
|
||||
import { useMutation, useQuery } from "@vue/apollo-composable";
|
||||
import { integerTransformer, useRouteQuery } from "vue-use-route-query";
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
import { computed } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import {
|
||||
|
||||
@@ -327,7 +327,7 @@ import { ADMIN_UPDATE_USER, LANGUAGES_CODES } from "@/graphql/admin";
|
||||
import { useMutation, useQuery } from "@vue/apollo-composable";
|
||||
import { ILanguage } from "@/types/admin.model";
|
||||
import { computed, inject, reactive, ref, watch } from "vue";
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { formatDateTimeString } from "@/filters/datetime";
|
||||
import { useRouter } from "vue-router";
|
||||
|
||||
@@ -90,7 +90,7 @@ import RouteName from "@/router/name";
|
||||
import { useQuery } from "@vue/apollo-composable";
|
||||
import { computed } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
import NumberDashboardTile from "@/components/Dashboard/NumberDashboardTile.vue";
|
||||
import LinkedNumberDashboardTile from "@/components/Dashboard/LinkedNumberDashboardTile.vue";
|
||||
import { InstanceFilterFollowStatus } from "@/types/enums";
|
||||
|
||||
@@ -119,7 +119,7 @@ import {
|
||||
useRouteQuery,
|
||||
} from "vue-use-route-query";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
import { computed } from "vue";
|
||||
import { Paginate } from "@/types/paginate";
|
||||
import { IGroup } from "@/types/actor";
|
||||
|
||||
@@ -235,7 +235,7 @@ import {
|
||||
import { useMutation, useQuery } from "@vue/apollo-composable";
|
||||
import { computed, inject, ref, watch } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
import CloudQuestion from "../../../node_modules/vue-material-design-icons/CloudQuestion.vue";
|
||||
import { Notifier } from "@/plugins/notifier";
|
||||
import MastodonLogo from "@/components/Share/MastodonLogo.vue";
|
||||
|
||||
@@ -103,7 +103,7 @@ import EmptyContent from "@/components/Utils/EmptyContent.vue";
|
||||
import { useQuery } from "@vue/apollo-composable";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { computed } from "vue";
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
import {
|
||||
useRouteQuery,
|
||||
booleanTransformer,
|
||||
|
||||
@@ -58,6 +58,70 @@
|
||||
</small>
|
||||
<o-input v-model="settingsToWrite.contact" id="instance-contact" />
|
||||
</div>
|
||||
<label class="field flex flex-col">
|
||||
<p>{{ t("Logo") }}</p>
|
||||
<small>
|
||||
{{
|
||||
t(
|
||||
"Logo of the instance. Defaults to the upstream Mobilizon logo."
|
||||
)
|
||||
}}
|
||||
</small>
|
||||
<picture-upload
|
||||
v-model:modelValue="instanceLogoFile"
|
||||
:defaultImage="settingsToWrite.instanceLogo"
|
||||
:textFallback="t('Logo')"
|
||||
:maxSize="maxSize"
|
||||
/>
|
||||
</label>
|
||||
<label class="field flex flex-col">
|
||||
<p>{{ t("Favicon") }}</p>
|
||||
<small>
|
||||
{{
|
||||
t(
|
||||
"Browser tab icon and PWA icon of the instance. Defaults to the upstream Mobilizon icon."
|
||||
)
|
||||
}}
|
||||
</small>
|
||||
<picture-upload
|
||||
v-model:modelValue="instanceFaviconFile"
|
||||
:defaultImage="settingsToWrite.instanceFavicon"
|
||||
:textFallback="t('Favicon')"
|
||||
:maxSize="maxSize"
|
||||
/>
|
||||
</label>
|
||||
<label class="field flex flex-col">
|
||||
<p>{{ t("Default Picture") }}</p>
|
||||
<small>
|
||||
{{ t("Default picture when an event or group doesn't have one.") }}
|
||||
</small>
|
||||
<picture-upload
|
||||
v-model:modelValue="defaultPictureFile"
|
||||
:defaultImage="settingsToWrite.defaultPicture"
|
||||
:textFallback="t('Default Picture')"
|
||||
:maxSize="maxSize"
|
||||
/>
|
||||
</label>
|
||||
<!-- piece of code to manage instance colors
|
||||
<div class="field flex flex-col">
|
||||
<label class="" for="primary-color">{{ t("Primary Color") }}</label>
|
||||
<o-input
|
||||
type="color"
|
||||
v-model="settingsToWrite.primaryColor"
|
||||
id="primary-color"
|
||||
/>
|
||||
</div>
|
||||
<div class="field flex flex-col">
|
||||
<label class="" for="secondary-color">{{
|
||||
t("Secondary Color")
|
||||
}}</label>
|
||||
<o-input
|
||||
type="color"
|
||||
v-model="settingsToWrite.secondaryColor"
|
||||
id="secondary-color"
|
||||
/>
|
||||
</div>
|
||||
-->
|
||||
<o-field :label="t('Allow registrations')">
|
||||
<o-switch v-model="settingsToWrite.registrationsOpen">
|
||||
<p
|
||||
@@ -389,15 +453,29 @@ import RouteName from "@/router/name";
|
||||
import { useMutation, useQuery } from "@vue/apollo-composable";
|
||||
import { ref, computed, watch, inject } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
import type { Notifier } from "@/plugins/notifier";
|
||||
|
||||
// Media upload related
|
||||
import PictureUpload from "@/components/PictureUpload.vue";
|
||||
import {
|
||||
initWrappedMedia,
|
||||
loadWrappedMedia,
|
||||
asMediaInput,
|
||||
} from "@/utils/image";
|
||||
import { useDefaultMaxSize } from "@/composition/config";
|
||||
|
||||
const defaultAdminSettings: IAdminSettings = {
|
||||
instanceName: "",
|
||||
instanceDescription: "",
|
||||
instanceSlogan: "",
|
||||
instanceLongDescription: "",
|
||||
contact: "",
|
||||
instanceLogo: null,
|
||||
instanceFavicon: null,
|
||||
defaultPicture: null,
|
||||
primaryColor: "",
|
||||
secondaryColor: "",
|
||||
instanceTerms: "",
|
||||
instanceTermsType: InstanceTermsType.DEFAULT,
|
||||
instanceTermsUrl: null,
|
||||
@@ -409,12 +487,30 @@ const defaultAdminSettings: IAdminSettings = {
|
||||
instanceLanguages: [],
|
||||
};
|
||||
|
||||
const { result: adminSettingsResult } = useQuery<{
|
||||
const { onResult: onAdminSettingsResult } = useQuery<{
|
||||
adminSettings: IAdminSettings;
|
||||
}>(ADMIN_SETTINGS);
|
||||
const adminSettings = computed(
|
||||
() => adminSettingsResult.value?.adminSettings ?? defaultAdminSettings
|
||||
);
|
||||
|
||||
const adminSettings = ref<IAdminSettings>();
|
||||
|
||||
onAdminSettingsResult(async ({ data }) => {
|
||||
if (!data) return;
|
||||
adminSettings.value =
|
||||
{
|
||||
...data.adminSettings,
|
||||
} ?? defaultAdminSettings;
|
||||
|
||||
loadWrappedMedia(instanceLogo, adminSettings.value.instanceLogo);
|
||||
loadWrappedMedia(instanceFavicon, adminSettings.value.instanceFavicon);
|
||||
loadWrappedMedia(defaultPicture, adminSettings.value.defaultPicture);
|
||||
});
|
||||
|
||||
const instanceLogo = initWrappedMedia();
|
||||
const { file: instanceLogoFile } = instanceLogo;
|
||||
const instanceFavicon = initWrappedMedia();
|
||||
const { file: instanceFaviconFile } = instanceFavicon;
|
||||
const defaultPicture = initWrappedMedia();
|
||||
const { file: defaultPictureFile } = defaultPicture;
|
||||
|
||||
const { result: languageResult } = useQuery<{ languages: ILanguage[] }>(
|
||||
LANGUAGES
|
||||
@@ -463,6 +559,9 @@ const {
|
||||
} = useMutation(SAVE_ADMIN_SETTINGS);
|
||||
|
||||
saveAdminSettingsDone(() => {
|
||||
instanceLogo.firstHash = instanceLogo.hash;
|
||||
instanceFavicon.firstHash = instanceFavicon.hash;
|
||||
defaultPicture.firstHash = defaultPicture.hash;
|
||||
notifier?.success(t("Admin settings successfully saved.") as string);
|
||||
});
|
||||
|
||||
@@ -472,11 +571,29 @@ saveAdminSettingsError((e) => {
|
||||
});
|
||||
|
||||
const updateSettings = async (): Promise<void> => {
|
||||
const variables = { ...settingsToWrite.value };
|
||||
console.debug("updating settings with variables", variables);
|
||||
const variables = {
|
||||
...settingsToWrite.value,
|
||||
...asMediaInput(
|
||||
instanceLogo,
|
||||
"instanceLogo",
|
||||
adminSettings.value?.instanceLogo?.id
|
||||
),
|
||||
...asMediaInput(
|
||||
instanceFavicon,
|
||||
"instanceFavicon",
|
||||
adminSettings.value?.instanceFavicon?.id
|
||||
),
|
||||
...asMediaInput(
|
||||
defaultPicture,
|
||||
"defaultPicture",
|
||||
adminSettings.value?.defaultPicture?.id
|
||||
),
|
||||
};
|
||||
saveAdminSettings(variables);
|
||||
};
|
||||
|
||||
const maxSize = useDefaultMaxSize();
|
||||
|
||||
const getFilteredLanguages = (text: string): void => {
|
||||
filteredLanguages.value = languages.value
|
||||
? languages.value
|
||||
|
||||
@@ -110,7 +110,7 @@ import { useQuery } from "@vue/apollo-composable";
|
||||
import { ILanguage } from "@/types/admin.model";
|
||||
import { computed, ref } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
import { integerTransformer, useRouteQuery } from "vue-use-route-query";
|
||||
import { formatDateTimeString } from "@/filters/datetime";
|
||||
|
||||
|
||||
@@ -103,7 +103,7 @@ import {
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useEventCategories } from "@/composition/apollo/config";
|
||||
import EmptyContent from "@/components/Utils/EmptyContent.vue";
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
|
||||
const { t } = useI18n({ useScope: "global" });
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@ import {
|
||||
import { PROFILE_CONVERSATIONS } from "@/graphql/event";
|
||||
import ConversationListItem from "../../components/Conversations/ConversationListItem.vue";
|
||||
import EmptyContent from "../../components/Utils/EmptyContent.vue";
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
import { IPerson } from "@/types/actor";
|
||||
import { useOruga } from "@oruga-ui/oruga-next";
|
||||
import { arrayTransformer } from "@/utils/route";
|
||||
|
||||
@@ -189,7 +189,7 @@ import {
|
||||
onMounted,
|
||||
onUnmounted,
|
||||
} from "vue";
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
import { useRouter } from "vue-router";
|
||||
import { useCurrentActorClient } from "../../composition/apollo/actor";
|
||||
import { AbsintheGraphQLError } from "../../types/errors.model";
|
||||
|
||||
@@ -70,7 +70,7 @@ import { useI18n } from "vue-i18n";
|
||||
import { useMutation } from "@vue/apollo-composable";
|
||||
import { IDiscussion } from "@/types/discussions";
|
||||
import { useRouter } from "vue-router";
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
import { Notifier } from "@/plugins/notifier";
|
||||
import { AbsintheGraphQLError } from "@/types/errors.model";
|
||||
|
||||
|
||||
@@ -98,7 +98,6 @@
|
||||
</form>
|
||||
</div>
|
||||
<discussion-comment
|
||||
class="border rounded-md p-2 mt-4"
|
||||
v-for="comment in discussion.comments.elements"
|
||||
:key="comment.id"
|
||||
:model-value="comment"
|
||||
@@ -172,7 +171,7 @@ import {
|
||||
computed,
|
||||
inject,
|
||||
} from "vue";
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
import { useRouter } from "vue-router";
|
||||
import { useCurrentActorClient } from "@/composition/apollo/actor";
|
||||
import { AbsintheGraphQLError } from "@/types/errors.model";
|
||||
@@ -180,6 +179,7 @@ import { MemberRole } from "@/types/enums";
|
||||
import { PERSON_MEMBERSHIPS } from "@/graphql/actor";
|
||||
import { Dialog } from "@/plugins/dialog";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { watch } from "vue";
|
||||
|
||||
const props = defineProps<{ slug: string }>();
|
||||
|
||||
@@ -188,6 +188,8 @@ const COMMENTS_PER_PAGE = 10;
|
||||
|
||||
const { currentActor } = useCurrentActorClient();
|
||||
|
||||
const slug = computed(() => props.slug);
|
||||
|
||||
const {
|
||||
result: discussionResult,
|
||||
onError: onDiscussionError,
|
||||
@@ -197,52 +199,55 @@ const {
|
||||
} = useQuery<{ discussion: IDiscussion }>(
|
||||
GET_DISCUSSION,
|
||||
() => ({
|
||||
slug: props.slug,
|
||||
slug: slug.value,
|
||||
page: page.value,
|
||||
limit: COMMENTS_PER_PAGE,
|
||||
}),
|
||||
() => ({
|
||||
enabled: props.slug !== undefined,
|
||||
enabled: slug.value !== undefined,
|
||||
})
|
||||
);
|
||||
|
||||
subscribeToMore({
|
||||
document: DISCUSSION_COMMENT_CHANGED,
|
||||
variables: () => ({
|
||||
slug: props.slug,
|
||||
page: page.value,
|
||||
limit: COMMENTS_PER_PAGE,
|
||||
}),
|
||||
updateQuery(
|
||||
previousResult: any,
|
||||
{ subscriptionData }: { subscriptionData: any }
|
||||
) {
|
||||
const previousDiscussion = previousResult.discussion;
|
||||
const lastComment =
|
||||
subscriptionData.data.discussionCommentChanged.lastComment;
|
||||
hasMoreComments.value = !previousDiscussion.comments.elements.some(
|
||||
(comment: IComment) => comment.id === lastComment.id
|
||||
);
|
||||
if (hasMoreComments.value) {
|
||||
return {
|
||||
discussion: {
|
||||
...previousDiscussion,
|
||||
lastComment: lastComment,
|
||||
comments: {
|
||||
elements: [
|
||||
...previousDiscussion.comments.elements.filter(
|
||||
({ id }: { id: string }) => id !== lastComment.id
|
||||
),
|
||||
lastComment,
|
||||
],
|
||||
total: previousDiscussion.comments.total + 1,
|
||||
watch(slug, (newSlug: string | undefined | null) => {
|
||||
if (!newSlug) return;
|
||||
subscribeToMore({
|
||||
document: DISCUSSION_COMMENT_CHANGED,
|
||||
variables: () => ({
|
||||
slug: slug.value,
|
||||
page: page.value,
|
||||
limit: COMMENTS_PER_PAGE,
|
||||
}),
|
||||
updateQuery(
|
||||
previousResult: any,
|
||||
{ subscriptionData }: { subscriptionData: any }
|
||||
) {
|
||||
const previousDiscussion = previousResult.discussion;
|
||||
const lastComment =
|
||||
subscriptionData.data.discussionCommentChanged.lastComment;
|
||||
hasMoreComments.value = !previousDiscussion.comments.elements.some(
|
||||
(comment: IComment) => comment.id === lastComment.id
|
||||
);
|
||||
if (hasMoreComments.value) {
|
||||
return {
|
||||
discussion: {
|
||||
...previousDiscussion,
|
||||
lastComment: lastComment,
|
||||
comments: {
|
||||
elements: [
|
||||
...previousDiscussion.comments.elements.filter(
|
||||
({ id }: { id: string }) => id !== lastComment.id
|
||||
),
|
||||
lastComment,
|
||||
],
|
||||
total: previousDiscussion.comments.total + 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return previousDiscussion;
|
||||
},
|
||||
return previousDiscussion;
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
const discussion = computed(() => discussionResult.value?.discussion);
|
||||
@@ -272,7 +277,7 @@ const { mutate: replyToDiscussionMutation } = useMutation<{
|
||||
}>({
|
||||
query: GET_DISCUSSION,
|
||||
variables: {
|
||||
slug: props.slug,
|
||||
slug: slug.value,
|
||||
page: page.value,
|
||||
},
|
||||
});
|
||||
@@ -281,7 +286,7 @@ const { mutate: replyToDiscussionMutation } = useMutation<{
|
||||
|
||||
store.writeQuery({
|
||||
query: GET_DISCUSSION,
|
||||
variables: { slug: props.slug, page: page.value },
|
||||
variables: { slug: slug.value, page: page.value },
|
||||
data: {
|
||||
discussion: {
|
||||
...discussionCached,
|
||||
@@ -324,7 +329,7 @@ const { mutate: updateComment } = useMutation<
|
||||
}>({
|
||||
query: GET_DISCUSSION,
|
||||
variables: {
|
||||
slug: props.slug,
|
||||
slug: slug.value,
|
||||
page: page.value,
|
||||
},
|
||||
});
|
||||
@@ -339,7 +344,7 @@ const { mutate: updateComment } = useMutation<
|
||||
}
|
||||
store.writeQuery({
|
||||
query: GET_DISCUSSION,
|
||||
variables: { slug: props.slug, page: page.value },
|
||||
variables: { slug: slug.value, page: page.value },
|
||||
data: { discussion: discussionCached },
|
||||
});
|
||||
},
|
||||
@@ -379,7 +384,7 @@ const loadMoreComments = async (): Promise<void> => {
|
||||
await fetchMore({
|
||||
// New variables
|
||||
variables: {
|
||||
slug: props.slug,
|
||||
slug: slug.value,
|
||||
page: page.value,
|
||||
limit: COMMENTS_PER_PAGE,
|
||||
},
|
||||
|
||||
@@ -83,7 +83,7 @@ import EmptyContent from "@/components/Utils/EmptyContent.vue";
|
||||
import { usePersonStatusGroup } from "@/composition/apollo/actor";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useRouteQuery, integerTransformer } from "vue-use-route-query";
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
import { computed } from "vue";
|
||||
|
||||
const page = useRouteQuery("page", 1, integerTransformer);
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ErrorCode } from "@/types/enums";
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useRouteQuery } from "vue-use-route-query";
|
||||
import { computed } from "vue";
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
import RouteName from "@/router/name";
|
||||
import { computed } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
import EventConversations from "../../components/Conversations/EventConversations.vue";
|
||||
import NewPrivateMessage from "../../components/Participation/NewPrivateMessage.vue";
|
||||
import { useFetchEvent } from "@/composition/apollo/event";
|
||||
|
||||
21
src/views/Event/CalendarView.vue
Normal file
21
src/views/Event/CalendarView.vue
Normal file
@@ -0,0 +1,21 @@
|
||||
<template>
|
||||
<div class="container mx-auto px-1 mb-6">
|
||||
<h1 v-if="!isMobile">
|
||||
{{ t("Calendar") }}
|
||||
</h1>
|
||||
|
||||
<div class="p-2">
|
||||
<EventsCalendar v-if="!isMobile" />
|
||||
<EventsAgenda v-else />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { useI18n } from "vue-i18n";
|
||||
import EventsAgenda from "@/components/FullCalendar/EventsAgenda.vue";
|
||||
import EventsCalendar from "@/components/FullCalendar/EventsCalendar.vue";
|
||||
|
||||
const { t } = useI18n({ useScope: "global" });
|
||||
|
||||
const isMobile = window.innerWidth < 760;
|
||||
</script>
|
||||
@@ -241,19 +241,36 @@
|
||||
{{ t('Page limited to my group (asks for auth)') }}
|
||||
</o-radio>
|
||||
</div>-->
|
||||
</section>
|
||||
<section class="my-4">
|
||||
<h2>
|
||||
{{ t("How to register") }}
|
||||
</h2>
|
||||
|
||||
<div class="field">
|
||||
<o-radio
|
||||
v-model="registerOption"
|
||||
name="registerOption"
|
||||
:native-value="RegisterOption.MOBILIZON"
|
||||
>{{ t("I want to manage the registration on Mobilizon") }}</o-radio
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<o-radio
|
||||
v-model="registerOption"
|
||||
name="registerOption"
|
||||
:native-value="RegisterOption.EXTERNAL"
|
||||
>{{
|
||||
t("I want to manage the registration with an external provider")
|
||||
}}</o-radio
|
||||
>
|
||||
</div>
|
||||
|
||||
<o-field
|
||||
:label="t('External registration')"
|
||||
v-if="features?.eventExternal"
|
||||
v-if="registerOption === RegisterOption.EXTERNAL"
|
||||
:label="t('URL')"
|
||||
>
|
||||
<o-switch v-model="externalParticipation">
|
||||
{{
|
||||
t("I want to manage the registration with an external provider")
|
||||
}}
|
||||
</o-switch>
|
||||
</o-field>
|
||||
|
||||
<o-field v-if="externalParticipation" :label="t('URL')">
|
||||
<o-input
|
||||
icon="link"
|
||||
type="url"
|
||||
@@ -264,7 +281,10 @@
|
||||
</o-field>
|
||||
|
||||
<o-field
|
||||
v-if="anonymousParticipationConfig?.allowed && !externalParticipation"
|
||||
v-if="
|
||||
anonymousParticipationConfig?.allowed &&
|
||||
registerOption === RegisterOption.MOBILIZON
|
||||
"
|
||||
:label="t('Anonymous participations')"
|
||||
>
|
||||
<o-switch v-model="eventOptions.anonymousParticipation">
|
||||
@@ -287,20 +307,35 @@
|
||||
|
||||
<o-field
|
||||
:label="t('Participation approval')"
|
||||
v-show="!externalParticipation"
|
||||
v-show="registerOption === RegisterOption.MOBILIZON"
|
||||
>
|
||||
<o-switch v-model="needsApproval">{{
|
||||
t("I want to approve every participation request")
|
||||
}}</o-switch>
|
||||
</o-field>
|
||||
|
||||
<o-field :label="t('Number of places')" v-show="!externalParticipation">
|
||||
<o-field
|
||||
:label="t('Showing participants')"
|
||||
v-show="registerOption === RegisterOption.MOBILIZON"
|
||||
>
|
||||
<o-switch v-model="hideParticipants">{{
|
||||
t("Hide the number of participants")
|
||||
}}</o-switch>
|
||||
</o-field>
|
||||
|
||||
<o-field
|
||||
:label="t('Number of places')"
|
||||
v-show="registerOption === RegisterOption.MOBILIZON"
|
||||
>
|
||||
<o-switch v-model="limitedPlaces">{{
|
||||
t("Limited number of places")
|
||||
}}</o-switch>
|
||||
</o-field>
|
||||
|
||||
<div class="" v-if="limitedPlaces && !externalParticipation">
|
||||
<div
|
||||
class=""
|
||||
v-if="limitedPlaces && registerOption === RegisterOption.MOBILIZON"
|
||||
>
|
||||
<o-field :label="t('Number of places')" label-for="number-of-places">
|
||||
<o-input
|
||||
type="number"
|
||||
@@ -635,7 +670,7 @@ import {
|
||||
import { useMutation } from "@vue/apollo-composable";
|
||||
import { Dialog } from "@/plugins/dialog";
|
||||
import { Notifier } from "@/plugins/notifier";
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
import { useOruga } from "@oruga-ui/oruga-next";
|
||||
import type { Locale } from "date-fns";
|
||||
import sortBy from "lodash/sortBy";
|
||||
@@ -1091,6 +1126,15 @@ const needsApproval = computed({
|
||||
},
|
||||
});
|
||||
|
||||
const hideParticipants = computed({
|
||||
get(): boolean {
|
||||
return event.value?.options.hideNumberOfParticipants;
|
||||
},
|
||||
set(value: boolean) {
|
||||
event.value.options.hideNumberOfParticipants = value;
|
||||
},
|
||||
});
|
||||
|
||||
const checkTitleLength = computed((): Array<string | undefined> => {
|
||||
return event.value.title.length > 80
|
||||
? ["info", t("The event title will be ellipsed.")]
|
||||
@@ -1359,12 +1403,19 @@ const orderedCategories = computed(() => {
|
||||
return sortBy(eventCategories.value, ["label"]);
|
||||
});
|
||||
|
||||
const externalParticipation = computed({
|
||||
const RegisterOption = {
|
||||
MOBILIZON: "mobilizon",
|
||||
EXTERNAL: "external",
|
||||
};
|
||||
|
||||
const registerOption = computed({
|
||||
get() {
|
||||
return event.value?.joinOptions === EventJoinOptions.EXTERNAL;
|
||||
return event.value?.joinOptions === EventJoinOptions.EXTERNAL
|
||||
? RegisterOption.EXTERNAL
|
||||
: RegisterOption.MOBILIZON;
|
||||
},
|
||||
set(newValue) {
|
||||
if (newValue === true) {
|
||||
if (newValue === RegisterOption.EXTERNAL) {
|
||||
event.value.joinOptions = EventJoinOptions.EXTERNAL;
|
||||
} else {
|
||||
event.value.joinOptions = EventJoinOptions.FREE;
|
||||
|
||||
@@ -18,6 +18,16 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="start-time-icon-wrapper relative"
|
||||
v-if="event?.beginsOn && event?.options.showStartTime"
|
||||
>
|
||||
<start-time-icon
|
||||
:date="event.beginsOn.toString()"
|
||||
class="absolute right-3 -top-16"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<section class="intro px-2 pt-4" dir="auto">
|
||||
<div class="flex flex-wrap gap-2 justify-end">
|
||||
<div class="flex-1 min-w-[300px]">
|
||||
@@ -289,6 +299,7 @@ import {
|
||||
usernameWithDomain,
|
||||
} from "@/types/actor";
|
||||
import DateCalendarIcon from "@/components/Event/DateCalendarIcon.vue";
|
||||
import StartTimeIcon from "@/components/Event/StartTimeIcon.vue";
|
||||
import SkeletonDateCalendarIcon from "@/components/Event/SkeletonDateCalendarIcon.vue";
|
||||
import Earth from "vue-material-design-icons/Earth.vue";
|
||||
import Link from "vue-material-design-icons/Link.vue";
|
||||
@@ -326,7 +337,7 @@ import {
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { Notifier } from "@/plugins/notifier";
|
||||
import { AbsintheGraphQLErrors } from "@/types/errors.model";
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
|
||||
const IntegrationTwitch = defineAsyncComponent(
|
||||
() => import("@/components/Event/Integrations/TwitchIntegration.vue")
|
||||
|
||||
@@ -116,7 +116,7 @@ import {
|
||||
useRouteQuery,
|
||||
} from "vue-use-route-query";
|
||||
import { MemberRole } from "@/types/enums";
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const EVENTS_PAGE_LIMIT = 10;
|
||||
|
||||
@@ -151,7 +151,6 @@
|
||||
true // !$apollo.loading
|
||||
"
|
||||
>
|
||||
<div class="img-container h-64 prose" />
|
||||
<div class="text-center prose dark:prose-invert max-w-full">
|
||||
<p>
|
||||
{{
|
||||
@@ -237,7 +236,7 @@ import {
|
||||
import { Locale } from "date-fns";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useRestrictions } from "@/composition/apollo/config";
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
|
||||
const EventParticipationCard = defineAsyncComponent(
|
||||
() => import("@/components/Event/EventParticipationCard.vue")
|
||||
@@ -500,24 +499,3 @@ useHead({
|
||||
title: computed(() => t("My events")),
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.not-found {
|
||||
.img-container {
|
||||
background-image: url("../../../img/pics/event_creation-480w.webp");
|
||||
|
||||
@media (min-resolution: 2dppx) {
|
||||
& {
|
||||
background-image: url("../../../img/pics/event_creation-1024w.webp");
|
||||
}
|
||||
}
|
||||
|
||||
max-width: 450px;
|
||||
height: 300px;
|
||||
box-shadow: 0 0 8px 8px white inset;
|
||||
background-size: cover;
|
||||
border-radius: 10px;
|
||||
margin: auto auto 1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -284,7 +284,7 @@ import Incognito from "vue-material-design-icons/Incognito.vue";
|
||||
import EmptyContent from "@/components/Utils/EmptyContent.vue";
|
||||
import { Notifier } from "@/plugins/notifier";
|
||||
import Tag from "@/components/TagElement.vue";
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
|
||||
const PARTICIPANTS_PER_PAGE = 10;
|
||||
const MESSAGE_ELLIPSIS_LENGTH = 130;
|
||||
|
||||
@@ -231,7 +231,7 @@ import {
|
||||
useHost,
|
||||
} from "@/composition/config";
|
||||
import { Notifier } from "@/plugins/notifier";
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
import { Openness, GroupVisibility } from "@/types/enums";
|
||||
import FullAddressAutoComplete from "@/components/Event/FullAddressAutoComplete.vue";
|
||||
|
||||
|
||||
@@ -126,7 +126,7 @@ import {
|
||||
useRouteQuery,
|
||||
} from "vue-use-route-query";
|
||||
import { computed, inject } from "vue";
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { usePersonStatusGroup } from "@/composition/apollo/actor";
|
||||
import { MemberRole } from "@/types/enums";
|
||||
|
||||
@@ -250,7 +250,7 @@ import {
|
||||
} from "@/graphql/member";
|
||||
import { usernameWithDomain, displayName, IGroup } from "@/types/actor";
|
||||
import EmptyContent from "@/components/Utils/EmptyContent.vue";
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useMutation, useQuery } from "@vue/apollo-composable";
|
||||
import { computed, inject, ref } from "vue";
|
||||
|
||||
@@ -208,7 +208,7 @@ import { DELETE_GROUP } from "@/graphql/group";
|
||||
import { useMutation } from "@vue/apollo-composable";
|
||||
import { useRouter } from "vue-router";
|
||||
import { Dialog } from "@/plugins/dialog";
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
import { Notifier } from "@/plugins/notifier";
|
||||
|
||||
const Editor = defineAsyncComponent(
|
||||
|
||||
@@ -706,7 +706,7 @@ import AccountMultiplePlus from "vue-material-design-icons/AccountMultiplePlus.v
|
||||
import Earth from "vue-material-design-icons/Earth.vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useCreateReport } from "@/composition/apollo/report";
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
import Discussions from "@/components/Group/Sections/DiscussionsSection.vue";
|
||||
import Resources from "@/components/Group/Sections/ResourcesSection.vue";
|
||||
import Posts from "@/components/Group/Sections/PostsSection.vue";
|
||||
|
||||
@@ -48,7 +48,6 @@
|
||||
>
|
||||
<div class="">
|
||||
<div class="">
|
||||
<div class="img-container" />
|
||||
<div class="text-center prose dark:prose-invert max-w-full">
|
||||
<p>
|
||||
{{ t("You are not part of any group.") }}
|
||||
@@ -92,7 +91,7 @@ import { useMutation, useQuery } from "@vue/apollo-composable";
|
||||
import { IUser } from "@/types/current-user.model";
|
||||
import { integerTransformer, useRouteQuery } from "vue-use-route-query";
|
||||
import { computed, inject } from "vue";
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useRouter } from "vue-router";
|
||||
import { Notifier } from "@/plugins/notifier";
|
||||
@@ -201,23 +200,4 @@ section {
|
||||
.group-member-card {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.not-found {
|
||||
.img-container {
|
||||
background-image: url("../../../img/pics/group-480w.webp");
|
||||
|
||||
@media (min-resolution: 2dppx) {
|
||||
& {
|
||||
background-image: url("../../../img/pics/group-1024w.webp");
|
||||
}
|
||||
}
|
||||
|
||||
max-width: 450px;
|
||||
height: 300px;
|
||||
box-shadow: 0 0 8px 8px white inset;
|
||||
background-size: cover;
|
||||
border-radius: 10px;
|
||||
margin: auto auto 1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
import RouteName from "@/router/name";
|
||||
import SettingMenuSection from "@/components/Settings/SettingMenuSection.vue";
|
||||
import SettingMenuItem from "@/components/Settings/SettingMenuItem.vue";
|
||||
|
||||
@@ -166,7 +166,7 @@ import SkeletonActivityItem from "../../components/Activity/SkeletonActivityItem
|
||||
import RouteName from "../../router/name";
|
||||
import TimelineText from "vue-material-design-icons/TimelineText.vue";
|
||||
import { useQuery } from "@vue/apollo-composable";
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
import { enumTransformer, useRouteQuery } from "vue-use-route-query";
|
||||
import { computed, defineAsyncComponent, ref } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
@@ -144,41 +144,6 @@
|
||||
<CloseGroups :userLocation="userLocation" @doGeoLoc="performGeoLocation()" />
|
||||
<OnlineEvents />
|
||||
<LastEvents v-if="instanceName" :instanceName="instanceName" />
|
||||
<!-- Unlogged content section -->
|
||||
<picture v-if="!currentUser?.isLoggedIn">
|
||||
<source
|
||||
media="(max-width: 799px)"
|
||||
:srcset="`/img/pics/homepage-480w.webp`"
|
||||
type="image/webp"
|
||||
/>
|
||||
|
||||
<source
|
||||
media="(max-width: 1024px)"
|
||||
:srcset="`/img/pics/homepage-1024w.webp`"
|
||||
type="image/webp"
|
||||
/>
|
||||
|
||||
<source
|
||||
media="(max-width: 1920px)"
|
||||
:srcset="`/img/pics/homepage-1920w.webp`"
|
||||
type="image/webp"
|
||||
/>
|
||||
|
||||
<source
|
||||
media="(min-width: 1921px)"
|
||||
:srcset="`/img/pics/homepage.webp`"
|
||||
type="image/webp"
|
||||
/>
|
||||
|
||||
<img
|
||||
:src="`/img/pics/homepage-1024w.webp`"
|
||||
width="3840"
|
||||
height="2719"
|
||||
alt=""
|
||||
loading="lazy"
|
||||
/>
|
||||
</picture>
|
||||
<presentation v-if="!currentUser?.isLoggedIn" />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
@@ -214,7 +179,6 @@ import {
|
||||
UPDATE_CURRENT_USER_LOCATION_CLIENT,
|
||||
} from "@/graphql/location";
|
||||
import { LocationType } from "@/types/user-location.model";
|
||||
import Presentation from "@/components/Home/MobilizonPresentation.vue";
|
||||
import CategoriesPreview from "@/components/Home/CategoriesPreview.vue";
|
||||
import UnloggedIntroduction from "@/components/Home/UnloggedIntroduction.vue";
|
||||
import SearchFields from "@/components/Home/SearchFields.vue";
|
||||
|
||||
@@ -38,7 +38,7 @@ import { computed, reactive } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useRouteQuery } from "vue-use-route-query";
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
|
||||
const router = useRouter();
|
||||
const { t } = useI18n({ useScope: "global" });
|
||||
|
||||
@@ -442,7 +442,7 @@ import { displayNameAndUsername, displayName } from "../../types/actor";
|
||||
import { Paginate } from "@/types/paginate";
|
||||
import { useQuery } from "@vue/apollo-composable";
|
||||
import { integerTransformer, useRouteQuery } from "vue-use-route-query";
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
import { computed } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { formatDateTimeString } from "@/filters/datetime";
|
||||
|
||||
@@ -95,7 +95,7 @@ import { Paginate } from "@/types/paginate";
|
||||
import debounce from "lodash/debounce";
|
||||
import { useQuery } from "@vue/apollo-composable";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
import { computed } from "vue";
|
||||
import {
|
||||
enumTransformer,
|
||||
|
||||
@@ -414,7 +414,7 @@ import { GraphQLError } from "graphql";
|
||||
import { ApolloCache, FetchResult } from "@apollo/client/core";
|
||||
import { useLazyQuery, useMutation, useQuery } from "@vue/apollo-composable";
|
||||
import { useCurrentActorClient } from "@/composition/apollo/actor";
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useRouter } from "vue-router";
|
||||
import { ref, computed, inject } from "vue";
|
||||
|
||||
@@ -77,7 +77,7 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useRouteQuery } from "vue-use-route-query";
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
import { computed, ref } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useQuery } from "@vue/apollo-composable";
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { DEVICE_ACTIVATION } from "@/graphql/application";
|
||||
import { useMutation } from "@vue/apollo-composable";
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
import { computed, reactive, ref, watch } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import AuthorizeApplication from "@/components/OAuth/AuthorizeApplication.vue";
|
||||
|
||||
@@ -2,20 +2,6 @@
|
||||
<section class="container mx-auto py-4 is-max-desktop max-w-2xl">
|
||||
<div class="">
|
||||
<div class="">
|
||||
<picture>
|
||||
<source
|
||||
:srcset="`/img/pics/error-480w.webp 1x, /img/pics/error-1024w.webp 2x`"
|
||||
type="image/webp"
|
||||
/>
|
||||
|
||||
<img
|
||||
:src="`/img/pics/error-480w.webp`"
|
||||
alt=""
|
||||
width="2616"
|
||||
height="1698"
|
||||
loading="lazy"
|
||||
/>
|
||||
</picture>
|
||||
<h1 class="text-4xl mb-3">
|
||||
{{ $t("The page you're looking for doesn't exist.") }}
|
||||
</h1>
|
||||
@@ -55,7 +41,7 @@
|
||||
</section>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
import { computed, ref } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useRouter } from "vue-router";
|
||||
|
||||
@@ -154,7 +154,7 @@ import {
|
||||
useCurrentActorClient,
|
||||
usePersonStatusGroup,
|
||||
} from "@/composition/apollo/actor";
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { computed, inject, onMounted, ref, watch } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
|
||||
@@ -83,7 +83,7 @@ import MultiPostListItem from "../../components/Post/MultiPostListItem.vue";
|
||||
import { useCurrentActorClient } from "@/composition/apollo/actor";
|
||||
import { useQuery } from "@vue/apollo-composable";
|
||||
import { computed } from "vue";
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
import { integerTransformer, useRouteQuery } from "vue-use-route-query";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { MemberRole } from "@/types/enums";
|
||||
|
||||
@@ -268,7 +268,7 @@ import { useMutation, useQuery } from "@vue/apollo-composable";
|
||||
import { computed, inject, ref } from "vue";
|
||||
import { IPost } from "@/types/post.model";
|
||||
import { DELETE_POST, FETCH_POST } from "@/graphql/post";
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
import { formatDateTimeString } from "@/filters/datetime";
|
||||
import { useRouter } from "vue-router";
|
||||
import { useCreateReport } from "@/composition/apollo/report";
|
||||
|
||||
@@ -245,7 +245,7 @@ import { computed, nextTick, reactive, ref, watch } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { integerTransformer, useRouteQuery } from "vue-use-route-query";
|
||||
import { useRouter } from "vue-router";
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
import { useResourceProviders } from "@/composition/apollo/config";
|
||||
import Folder from "vue-material-design-icons/Folder.vue";
|
||||
import Link from "vue-material-design-icons/Link.vue";
|
||||
|
||||
@@ -45,6 +45,16 @@
|
||||
:size="24"
|
||||
/>
|
||||
|
||||
<Calendar
|
||||
v-if="content.contentType === ContentType.SHORTEVENTS"
|
||||
:size="24"
|
||||
/>
|
||||
|
||||
<CalendarStar
|
||||
v-if="content.contentType === ContentType.LONGEVENTS"
|
||||
:size="24"
|
||||
/>
|
||||
|
||||
<AccountMultiple
|
||||
v-if="content.contentType === ContentType.GROUPS"
|
||||
:size="24"
|
||||
@@ -443,8 +453,15 @@
|
||||
class="hidden sm:flex items-center justify-between dark:text-slate-100 mb-2"
|
||||
>
|
||||
<p v-if="totalCount === 0">
|
||||
<span v-if="contentType === ContentType.EVENTS">{{
|
||||
t("No events found")
|
||||
<span
|
||||
v-if="
|
||||
contentType === ContentType.EVENTS ||
|
||||
contentType === ContentType.SHORTEVENTS
|
||||
"
|
||||
>{{ t("No events found") }}</span
|
||||
>
|
||||
<span v-else-if="contentType === ContentType.LONGEVENTS">{{
|
||||
t("No activities found")
|
||||
}}</span>
|
||||
<span v-else-if="contentType === ContentType.GROUPS">{{
|
||||
t("No groups found")
|
||||
@@ -452,7 +469,12 @@
|
||||
<span v-else>{{ t("No results found") }}</span>
|
||||
</p>
|
||||
<p v-else>
|
||||
<span v-if="contentType === 'EVENTS'">
|
||||
<span
|
||||
v-if="
|
||||
contentType === ContentType.EVENTS ||
|
||||
contentType === ContentType.SHORTEVENTS
|
||||
"
|
||||
>
|
||||
{{
|
||||
t(
|
||||
"{eventsCount} events found",
|
||||
@@ -461,7 +483,16 @@
|
||||
)
|
||||
}}
|
||||
</span>
|
||||
<span v-else-if="contentType === 'GROUPS'">
|
||||
<span v-else-if="contentType === ContentType.LONGEVENTS">
|
||||
{{
|
||||
t(
|
||||
"{eventsCount} activities found",
|
||||
{ eventsCount: searchEvents?.total },
|
||||
searchEvents?.total ?? 0
|
||||
)
|
||||
}}
|
||||
</span>
|
||||
<span v-else-if="contentType === ContentType.GROUPS">
|
||||
{{
|
||||
t(
|
||||
"{groupsCount} groups found",
|
||||
@@ -597,7 +628,13 @@
|
||||
:aria-current-label="t('Current page')"
|
||||
/>
|
||||
</template>
|
||||
<template v-else-if="contentType === ContentType.EVENTS">
|
||||
<template
|
||||
v-else-if="
|
||||
contentType === ContentType.EVENTS ||
|
||||
contentType === ContentType.SHORTEVENTS ||
|
||||
contentType === ContentType.LONGEVENTS
|
||||
"
|
||||
>
|
||||
<template v-if="searchLoading">
|
||||
<SkeletonEventResultList v-for="i in 8" :key="i" />
|
||||
</template>
|
||||
@@ -625,13 +662,23 @@
|
||||
>
|
||||
</o-pagination>
|
||||
</template>
|
||||
<EmptyContent v-else-if="searchLoading === false" icon="calendar">
|
||||
<EmptyContent
|
||||
v-else-if="searchLoading === false"
|
||||
:icon="
|
||||
contentType === ContentType.LONGEVENTS
|
||||
? 'calendar-star'
|
||||
: 'calendar'
|
||||
"
|
||||
>
|
||||
<span v-if="searchIsUrl">
|
||||
{{ t("No event found at this address") }}
|
||||
</span>
|
||||
<span v-else-if="!search">
|
||||
<span v-else-if="!search && contentType !== ContentType.LONGEVENTS">
|
||||
{{ t("No events found") }}
|
||||
</span>
|
||||
<span v-else-if="!search && contentType === ContentType.LONGEVENTS">
|
||||
{{ t("No activities found") }}
|
||||
</span>
|
||||
<i18n-t keypath="No events found for {search}" tag="span" v-else>
|
||||
<template #search>
|
||||
<b>{{ search }}</b>
|
||||
@@ -694,7 +741,7 @@
|
||||
icon="account-multiple"
|
||||
>
|
||||
<span v-if="!search">
|
||||
{{ t("No events found") }}
|
||||
{{ t("No groups found") }}
|
||||
</span>
|
||||
<i18n-t keypath="No groups found for {search}" tag="span" v-else>
|
||||
<template #search>
|
||||
@@ -767,10 +814,11 @@ import {
|
||||
booleanTransformer,
|
||||
} from "vue-use-route-query";
|
||||
import Calendar from "vue-material-design-icons/Calendar.vue";
|
||||
import CalendarStar from "vue-material-design-icons/CalendarStar.vue";
|
||||
import AccountMultiple from "vue-material-design-icons/AccountMultiple.vue";
|
||||
import Magnify from "vue-material-design-icons/Magnify.vue";
|
||||
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
import type { Locale } from "date-fns";
|
||||
import FilterSection from "@/components/Search/filters/FilterSection.vue";
|
||||
import { listShortDisjunctionFormatter } from "@/utils/listFormat";
|
||||
@@ -778,6 +826,7 @@ import langs from "@/i18n/langs.json";
|
||||
import {
|
||||
useEventCategories,
|
||||
useFeatures,
|
||||
useIsLongEvents,
|
||||
useSearchConfig,
|
||||
} from "@/composition/apollo/config";
|
||||
import { coordsToGeoHash } from "@/utils/location";
|
||||
@@ -892,7 +941,7 @@ const searchTarget = useRouteQuery(
|
||||
const mode = useRouteQuery("mode", ViewMode.LIST, enumTransformer(ViewMode));
|
||||
const sortBy = useRouteQuery(
|
||||
"sortBy",
|
||||
SortValues.MATCH_DESC,
|
||||
SortValues.START_TIME_ASC,
|
||||
enumTransformer(SortValues)
|
||||
);
|
||||
const bbox = useRouteQuery("bbox", undefined);
|
||||
@@ -904,6 +953,7 @@ const GROUP_PAGE_LIMIT = 16;
|
||||
|
||||
const { features } = useFeatures();
|
||||
const { eventCategories } = useEventCategories();
|
||||
const { islongEvents } = useIsLongEvents();
|
||||
|
||||
const orderedCategories = computed(() => {
|
||||
if (!eventCategories.value) return [];
|
||||
@@ -1017,20 +1067,41 @@ const searchIsUrl = computed((): boolean => {
|
||||
});
|
||||
|
||||
const contentTypeMapping = computed(() => {
|
||||
return [
|
||||
{
|
||||
contentType: "ALL",
|
||||
label: t("Everything"),
|
||||
},
|
||||
{
|
||||
contentType: "EVENTS",
|
||||
label: t("Events"),
|
||||
},
|
||||
{
|
||||
contentType: "GROUPS",
|
||||
label: t("Groups"),
|
||||
},
|
||||
];
|
||||
if (islongEvents.value) {
|
||||
return [
|
||||
{
|
||||
contentType: "ALL",
|
||||
label: t("Everything"),
|
||||
},
|
||||
{
|
||||
contentType: "SHORTEVENTS",
|
||||
label: t("Events"),
|
||||
},
|
||||
{
|
||||
contentType: "LONGEVENTS",
|
||||
label: t("Activities"),
|
||||
},
|
||||
{
|
||||
contentType: "GROUPS",
|
||||
label: t("Groups"),
|
||||
},
|
||||
];
|
||||
} else {
|
||||
return [
|
||||
{
|
||||
contentType: "ALL",
|
||||
label: t("Everything"),
|
||||
},
|
||||
{
|
||||
contentType: "EVENTS",
|
||||
label: t("Events"),
|
||||
},
|
||||
{
|
||||
contentType: "GROUPS",
|
||||
label: t("Groups"),
|
||||
},
|
||||
];
|
||||
}
|
||||
});
|
||||
|
||||
const eventDistance = computed(() => {
|
||||
@@ -1138,6 +1209,16 @@ const geoHashLocation = computed(() =>
|
||||
|
||||
const radius = computed(() => Number.parseInt(distance.value.slice(0, -3)));
|
||||
|
||||
const longEvents = computed(() => {
|
||||
if (contentType.value === ContentType.SHORTEVENTS) {
|
||||
return false;
|
||||
} else if (contentType.value === ContentType.LONGEVENTS) {
|
||||
return true;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
const totalCount = computed(() => {
|
||||
return (searchEvents.value?.total ?? 0) + (searchGroups.value?.total ?? 0);
|
||||
});
|
||||
@@ -1150,7 +1231,11 @@ const sortOptions = computed(() => {
|
||||
},
|
||||
];
|
||||
|
||||
if (contentType.value == ContentType.EVENTS) {
|
||||
if (
|
||||
contentType.value === ContentType.EVENTS ||
|
||||
contentType.value === ContentType.SHORTEVENTS ||
|
||||
contentType.value === ContentType.LONGEVENTS
|
||||
) {
|
||||
options.push(
|
||||
{
|
||||
key: SortValues.START_TIME_ASC,
|
||||
@@ -1171,7 +1256,7 @@ const sortOptions = computed(() => {
|
||||
);
|
||||
}
|
||||
|
||||
if (contentType.value == ContentType.GROUPS) {
|
||||
if (contentType.value === ContentType.GROUPS) {
|
||||
options.push({
|
||||
key: SortValues.MEMBER_COUNT_DESC,
|
||||
label: t("Number of members"),
|
||||
@@ -1282,6 +1367,12 @@ watch(
|
||||
case ContentType.EVENTS:
|
||||
eventPage.value = 1;
|
||||
break;
|
||||
case ContentType.SHORTEVENTS:
|
||||
eventPage.value = 1;
|
||||
break;
|
||||
case ContentType.LONGEVENTS:
|
||||
eventPage.value = 1;
|
||||
break;
|
||||
case ContentType.GROUPS:
|
||||
groupPage.value = 1;
|
||||
break;
|
||||
@@ -1298,6 +1389,7 @@ const { result: searchElementsResult, loading: searchLoading } = useQuery<{
|
||||
location: geoHashLocation.value,
|
||||
beginsOn: start.value,
|
||||
endsOn: end.value,
|
||||
longevents: longEvents.value,
|
||||
radius: geoHashLocation.value ? radius.value : undefined,
|
||||
eventPage:
|
||||
contentType.value === ContentType.ALL ? page.value : eventPage.value,
|
||||
|
||||
@@ -235,7 +235,7 @@ import { useLoggedUser } from "@/composition/apollo/user";
|
||||
import { Notifier } from "@/plugins/notifier";
|
||||
import { IAuthProvider } from "@/types/enums";
|
||||
import { useMutation } from "@vue/apollo-composable";
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
import { GraphQLError } from "graphql/error/GraphQLError";
|
||||
import { computed, inject, ref } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
@@ -82,7 +82,7 @@ import {
|
||||
REVOKED_AUTHORIZED_APPLICATION,
|
||||
} from "@/graphql/application";
|
||||
import { useMutation, useQuery } from "@vue/apollo-composable";
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useHead } from "@/utils/head";
|
||||
import { computed, inject } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import RouteName from "../../router/name";
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user