all developments of milestone 1

This commit is contained in:
setop
2024-04-10 12:36:21 +00:00
parent a78dc261e5
commit 7030d56864
266 changed files with 5391 additions and 2609 deletions

View File

@@ -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 {

View File

@@ -168,7 +168,7 @@ const props = withDefaults(
defineProps<{
modelValue: IComment;
currentActor: IPerson;
canReport: boolean;
canReport?: boolean;
}>(),
{ canReport: false }
);

View File

@@ -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(

View File

@@ -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));
}

View File

@@ -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

View File

@@ -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);

View 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>

View File

@@ -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>

View 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>

View 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>

View File

@@ -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;

View File

@@ -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<{

View File

@@ -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 {

View File

@@ -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: [] }

View File

@@ -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;

View File

@@ -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();

View File

@@ -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";

View File

@@ -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>

View File

@@ -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";

View File

@@ -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" });

View File

@@ -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";

View File

@@ -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);

View File

@@ -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 }>();

View File

@@ -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`

View File

@@ -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">;

View File

@@ -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();

View File

@@ -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,
};

View File

@@ -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

View File

@@ -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 {

View File

@@ -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
}

View File

@@ -6,6 +6,7 @@ export const EVENT_OPTIONS_FRAGMENT = gql`
remainingAttendeeCapacity
showRemainingAttendeeCapacity
anonymousParticipation
hideNumberOfParticipants
showStartTime
showEndTime
timezone

View File

@@ -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

View File

@@ -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",

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -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();

View File

@@ -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",

View File

@@ -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,

View File

@@ -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;

View File

@@ -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[];

View File

@@ -134,6 +134,8 @@ export enum SearchTabs {
export enum ContentType {
ALL = "ALL",
EVENTS = "EVENTS",
SHORTEVENTS = "SHORTEVENTS",
LONGEVENTS = "LONGEVENTS",
GROUPS = "GROUPS",
}

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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
View 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
),
});
}

View File

@@ -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;
}

View File

@@ -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);

View File

@@ -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";

View File

@@ -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";

View File

@@ -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";

View File

@@ -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";

View File

@@ -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();

View File

@@ -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(

View File

@@ -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();

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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";

View File

@@ -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";

View File

@@ -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";

View File

@@ -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";

View File

@@ -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,

View File

@@ -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

View File

@@ -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";

View File

@@ -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" });

View File

@@ -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";

View File

@@ -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";

View File

@@ -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";

View File

@@ -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,
},

View File

@@ -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);

View File

@@ -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";

View File

@@ -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";

View 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>

View File

@@ -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;

View File

@@ -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")

View File

@@ -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;

View File

@@ -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>

View File

@@ -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;

View File

@@ -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";

View File

@@ -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";

View File

@@ -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";

View File

@@ -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(

View File

@@ -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";

View File

@@ -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>

View File

@@ -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";

View File

@@ -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";

View File

@@ -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";

View File

@@ -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" });

View File

@@ -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";

View File

@@ -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,

View File

@@ -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";

View File

@@ -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";

View File

@@ -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";

View File

@@ -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";

View File

@@ -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";

View File

@@ -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";

View File

@@ -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";

View File

@@ -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";

View File

@@ -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,

View File

@@ -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";

View File

@@ -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