Improve search
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
parent
444e0d6a0c
commit
baac00f678
@ -134,8 +134,6 @@ export default function buildCurrentUserResolver(
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
console.debug("updating current user location", data);
|
|
||||||
|
|
||||||
localCache.writeQuery({ data, query: CURRENT_USER_LOCATION_CLIENT });
|
localCache.writeQuery({ data, query: CURRENT_USER_LOCATION_CLIENT });
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -204,3 +204,35 @@ button.menubar__button {
|
|||||||
@apply text-white;
|
@apply text-white;
|
||||||
background: #363636;
|
background: #363636;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Pagination */
|
||||||
|
.pagination {
|
||||||
|
@apply flex items-center text-center justify-between;
|
||||||
|
}
|
||||||
|
.pagination-link {
|
||||||
|
@apply inline-flex items-center relative justify-center cursor-pointer rounded h-10 m-1 p-2 bg-white text-lg;
|
||||||
|
}
|
||||||
|
.pagination-list {
|
||||||
|
@apply flex items-center text-center list-none flex-wrap grow shrink justify-start;
|
||||||
|
}
|
||||||
|
.pagination-next,
|
||||||
|
.pagination-previous {
|
||||||
|
@apply px-3;
|
||||||
|
}
|
||||||
|
.pagination-link-current {
|
||||||
|
@apply bg-primary cursor-not-allowed pointer-events-none border-primary text-white;
|
||||||
|
}
|
||||||
|
.pagination-ellipsis {
|
||||||
|
@apply text-center m-1 text-gray-300;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Tabs */
|
||||||
|
.tabs-nav {
|
||||||
|
@apply flex items-center justify-start pb-0.5;
|
||||||
|
}
|
||||||
|
.tabs-nav-item-boxed {
|
||||||
|
@apply flex items-center justify-center px-2 py-2 rounded-t border-transparent;
|
||||||
|
}
|
||||||
|
.tabs-nav-item-active-boxed {
|
||||||
|
@apply bg-white border-gray-300 text-primary;
|
||||||
|
}
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
:to="{
|
:to="{
|
||||||
name: 'SEARCH',
|
name: 'SEARCH',
|
||||||
query: {
|
query: {
|
||||||
eventCategory: category.key,
|
categoryOneOf: [category.key],
|
||||||
contentType: 'EVENTS',
|
contentType: 'EVENTS',
|
||||||
radius: undefined,
|
radius: undefined,
|
||||||
},
|
},
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<router-link
|
<router-link
|
||||||
class="mbz-card max-w-xs shrink-0 w-[18rem] snap-center"
|
class="mbz-card max-w-xs shrink-0 w-[18rem] snap-center dark:bg-mbz-purple"
|
||||||
:to="{ name: RouteName.EVENT, params: { uuid: event.uuid } }"
|
:to="{ name: RouteName.EVENT, params: { uuid: event.uuid } }"
|
||||||
>
|
>
|
||||||
<div class="bg-secondary rounded-lg">
|
<div class="bg-secondary rounded-lg">
|
||||||
|
@ -1,9 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<article class="bg-white dark:bg-mbz-purple mb-5 mt-4 p-0">
|
<article class="bg-white dark:bg-mbz-purple mb-5 mt-4 pb-2 md:p-0">
|
||||||
<div
|
<div class="bg-yellow-2 flex p-2 text-violet-title rounded-t-lg" dir="auto">
|
||||||
class="bg-mbz-yellow-2 flex p-2 text-violet-title rounded-t-lg"
|
|
||||||
dir="auto"
|
|
||||||
>
|
|
||||||
<figure
|
<figure
|
||||||
class="image is-24x24 ltr:pr-1 rtl:pl-1"
|
class="image is-24x24 ltr:pr-1 rtl:pl-1"
|
||||||
v-if="participation.actor.avatar"
|
v-if="participation.actor.avatar"
|
||||||
@ -21,7 +18,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="list-card flex flex-col relative">
|
<div class="list-card flex flex-col relative">
|
||||||
<div
|
<div
|
||||||
class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-x-1.5 gapt-x-3 pb-2"
|
class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-6 gap-x-1.5 md:gap-y-3 gapt-x-3"
|
||||||
>
|
>
|
||||||
<div class="mr-0 ml-0">
|
<div class="mr-0 ml-0">
|
||||||
<div class="h-36 relative w-full">
|
<div class="h-36 relative w-full">
|
||||||
@ -52,24 +49,24 @@
|
|||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="list-card-content lg:col-span-2 flex-1 p-2">
|
<div class="list-card-content lg:col-span-4 flex-1 p-2">
|
||||||
<div class="flex items-center pt-2" dir="auto">
|
<div class="flex items-center pt-2" dir="auto">
|
||||||
<b-tag
|
<Tag
|
||||||
variant="info"
|
variant="info"
|
||||||
class="mr-1 mb-1"
|
class="mr-1 mb-1"
|
||||||
size="is-medium"
|
size="medium"
|
||||||
v-if="participation.event.status === EventStatus.TENTATIVE"
|
v-if="participation.event.status === EventStatus.TENTATIVE"
|
||||||
>
|
>
|
||||||
{{ $t("Tentative") }}
|
{{ t("Tentative") }}
|
||||||
</b-tag>
|
</Tag>
|
||||||
<b-tag
|
<Tag
|
||||||
variant="danger"
|
variant="danger"
|
||||||
class="mr-1 mb-1"
|
class="mr-1 mb-1"
|
||||||
size="is-medium"
|
size="medium"
|
||||||
v-if="participation.event.status === EventStatus.CANCELLED"
|
v-if="participation.event.status === EventStatus.CANCELLED"
|
||||||
>
|
>
|
||||||
{{ $t("Cancelled") }}
|
{{ t("Cancelled") }}
|
||||||
</b-tag>
|
</Tag>
|
||||||
<router-link
|
<router-link
|
||||||
:to="{
|
:to="{
|
||||||
name: RouteName.EVENT,
|
name: RouteName.EVENT,
|
||||||
@ -96,7 +93,7 @@
|
|||||||
"
|
"
|
||||||
>
|
>
|
||||||
<Video />
|
<Video />
|
||||||
<span>{{ $t("Online") }}</span>
|
<span>{{ t("Online") }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-1">
|
<div class="flex gap-1">
|
||||||
<figure class="" v-if="actorAvatarURL">
|
<figure class="" v-if="actorAvatarURL">
|
||||||
@ -122,7 +119,7 @@
|
|||||||
<!-- Less than 10 seats left -->
|
<!-- Less than 10 seats left -->
|
||||||
<span class="has-text-danger" v-if="lastSeatsLeft">
|
<span class="has-text-danger" v-if="lastSeatsLeft">
|
||||||
{{
|
{{
|
||||||
$t("{number} seats left", {
|
t("{number} seats left", {
|
||||||
number: seatsLeft,
|
number: seatsLeft,
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
@ -133,7 +130,7 @@
|
|||||||
"
|
"
|
||||||
>
|
>
|
||||||
{{
|
{{
|
||||||
$t(
|
t(
|
||||||
"{available}/{capacity} available places",
|
"{available}/{capacity} available places",
|
||||||
{
|
{
|
||||||
available:
|
available:
|
||||||
@ -149,7 +146,7 @@
|
|||||||
</span>
|
</span>
|
||||||
<span v-else>
|
<span v-else>
|
||||||
{{
|
{{
|
||||||
$t(
|
t(
|
||||||
"{count} participants",
|
"{count} participants",
|
||||||
{
|
{
|
||||||
count: participation.event.participantStats.participant,
|
count: participation.event.participantStats.participant,
|
||||||
@ -170,7 +167,7 @@
|
|||||||
"
|
"
|
||||||
>
|
>
|
||||||
{{
|
{{
|
||||||
$t(
|
t(
|
||||||
"{count} requests waiting",
|
"{count} requests waiting",
|
||||||
{
|
{
|
||||||
count: participation.event.participantStats.notApproved,
|
count: participation.event.participantStats.notApproved,
|
||||||
@ -183,10 +180,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<o-dropdown aria-role="list" class="text-center self-center">
|
<o-dropdown
|
||||||
|
aria-role="list"
|
||||||
|
class="text-center self-center md:col-span-2 lg:col-span-1"
|
||||||
|
>
|
||||||
<template #trigger>
|
<template #trigger>
|
||||||
<o-button icon-right="dots-horizontal">
|
<o-button icon-right="dots-horizontal">
|
||||||
{{ $t("Actions") }}
|
{{ t("Actions") }}
|
||||||
</o-button>
|
</o-button>
|
||||||
</template>
|
</template>
|
||||||
<o-dropdown-item
|
<o-dropdown-item
|
||||||
@ -208,7 +208,7 @@
|
|||||||
"
|
"
|
||||||
>
|
>
|
||||||
<Pencil />
|
<Pencil />
|
||||||
{{ $t("Edit") }}
|
{{ t("Edit") }}
|
||||||
</div>
|
</div>
|
||||||
</o-dropdown-item>
|
</o-dropdown-item>
|
||||||
|
|
||||||
@ -226,7 +226,7 @@
|
|||||||
"
|
"
|
||||||
>
|
>
|
||||||
<ContentDuplicate />
|
<ContentDuplicate />
|
||||||
{{ $t("Duplicate") }}
|
{{ t("Duplicate") }}
|
||||||
</div>
|
</div>
|
||||||
</o-dropdown-item>
|
</o-dropdown-item>
|
||||||
|
|
||||||
@ -241,7 +241,7 @@
|
|||||||
>
|
>
|
||||||
<div @click="openDeleteEventModalWrapper" class="flex gap-1">
|
<div @click="openDeleteEventModalWrapper" class="flex gap-1">
|
||||||
<Delete />
|
<Delete />
|
||||||
{{ $t("Delete") }}
|
{{ t("Delete") }}
|
||||||
</div>
|
</div>
|
||||||
</o-dropdown-item>
|
</o-dropdown-item>
|
||||||
|
|
||||||
@ -264,7 +264,7 @@
|
|||||||
"
|
"
|
||||||
>
|
>
|
||||||
<AccountMultiplePlus />
|
<AccountMultiplePlus />
|
||||||
{{ $t("Manage participations") }}
|
{{ t("Manage participations") }}
|
||||||
</div>
|
</div>
|
||||||
</o-dropdown-item>
|
</o-dropdown-item>
|
||||||
|
|
||||||
@ -277,7 +277,7 @@
|
|||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<ViewCompact />
|
<ViewCompact />
|
||||||
{{ $t("View event page") }}
|
{{ t("View event page") }}
|
||||||
</router-link>
|
</router-link>
|
||||||
</o-dropdown-item>
|
</o-dropdown-item>
|
||||||
</o-dropdown>
|
</o-dropdown>
|
||||||
@ -288,12 +288,11 @@
|
|||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import DateCalendarIcon from "@/components/Event/DateCalendarIcon.vue";
|
import DateCalendarIcon from "@/components/Event/DateCalendarIcon.vue";
|
||||||
import { EventStatus, EventVisibility, ParticipantRole } from "@/types/enums";
|
import { EventStatus, ParticipantRole } from "@/types/enums";
|
||||||
import { IParticipant } from "@/types/participant.model";
|
import { IParticipant } from "@/types/participant.model";
|
||||||
import {
|
import {
|
||||||
IEvent,
|
IEvent,
|
||||||
IEventCardOptions,
|
IEventCardOptions,
|
||||||
organizer,
|
|
||||||
organizerAvatarUrl,
|
organizerAvatarUrl,
|
||||||
organizerDisplayName,
|
organizerDisplayName,
|
||||||
} from "@/types/event.model";
|
} from "@/types/event.model";
|
||||||
@ -319,6 +318,7 @@ import { useI18n } from "vue-i18n";
|
|||||||
import { Dialog } from "@/plugins/dialog";
|
import { Dialog } from "@/plugins/dialog";
|
||||||
import { Snackbar } from "@/plugins/snackbar";
|
import { Snackbar } from "@/plugins/snackbar";
|
||||||
import { useDeleteEvent } from "@/composition/apollo/event";
|
import { useDeleteEvent } from "@/composition/apollo/event";
|
||||||
|
import Tag from "@/components/Tag.vue";
|
||||||
|
|
||||||
const defaultOptions: IEventCardOptions = {
|
const defaultOptions: IEventCardOptions = {
|
||||||
hideDate: true,
|
hideDate: true,
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="grid auto-rows-[1fr] gap-x-5 gap-y-8 grid-cols-[repeat(auto-fill,_minmax(250px,_1fr))]"
|
class="grid auto-rows-[1fr] gap-x-2 gap-y-4 md:gap-x-6 grid-cols-[repeat(auto-fill,_minmax(250px,_1fr))] justify-items-center"
|
||||||
>
|
>
|
||||||
<event-card
|
<event-card
|
||||||
class="flex flex-col h-full"
|
class="flex flex-col h-full"
|
||||||
|
@ -4,35 +4,49 @@
|
|||||||
name: RouteName.GROUP,
|
name: RouteName.GROUP,
|
||||||
params: { preferredUsername: usernameWithDomain(group) },
|
params: { preferredUsername: usernameWithDomain(group) },
|
||||||
}"
|
}"
|
||||||
class="card flex flex-col max-w-md bg-white dark:bg-mbz-purple dark:text-white rounded-lg shadow-lg"
|
class="card flex flex-col shrink-0 w-[18rem] bg-white dark:bg-mbz-purple dark:text-white rounded-lg shadow-lg"
|
||||||
>
|
>
|
||||||
<figure class="rounded-t-lg flex justify-center h-1/4">
|
<figure class="rounded-t-lg flex justify-center h-40">
|
||||||
<lazy-image-wrapper :picture="group.banner" :rounded="true" />
|
<lazy-image-wrapper :picture="group.banner" :rounded="true" />
|
||||||
</figure>
|
</figure>
|
||||||
<div class="py-2 pl-2">
|
<div class="py-2 pl-2">
|
||||||
<div class="flex gap-1 mb-2">
|
<div class="flex gap-1 mb-2">
|
||||||
<div class="">
|
<div class="">
|
||||||
<figure class="" v-if="group.avatar">
|
<figure class="" v-if="group.avatar">
|
||||||
<img class="rounded-xl" :src="group.avatar.url" alt="" />
|
<img
|
||||||
|
class="rounded-xl"
|
||||||
|
:src="group.avatar.url"
|
||||||
|
alt=""
|
||||||
|
height="64"
|
||||||
|
width="64"
|
||||||
|
/>
|
||||||
</figure>
|
</figure>
|
||||||
<AccountGroup v-else :size="48" />
|
<AccountGroup v-else :size="64" />
|
||||||
</div>
|
</div>
|
||||||
<div class="">
|
<div class="px-1">
|
||||||
<h3 class="text-2xl" dir="auto">
|
<h3
|
||||||
|
class="text-2xl leading-5 line-clamp-3 font-bold text-violet-3 dark:text-white"
|
||||||
|
dir="auto"
|
||||||
|
>
|
||||||
{{ displayName(group) }}
|
{{ displayName(group) }}
|
||||||
</h3>
|
</h3>
|
||||||
<span class="is-6 has-text-grey-dark group-federated-username">
|
<span>
|
||||||
{{ `@${usernameWithDomain(group)}` }}
|
{{ `@${usernameWithDomain(group)}` }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-2 line-clamp-3" dir="auto" v-html="group.summary" />
|
<div
|
||||||
|
class="mb-2 line-clamp-3"
|
||||||
|
dir="auto"
|
||||||
|
v-html="saneSummary"
|
||||||
|
v-if="showSummary"
|
||||||
|
/>
|
||||||
<div>
|
<div>
|
||||||
<inline-address
|
<inline-address
|
||||||
v-if="group.physicalAddress && addressFullName(group.physicalAddress)"
|
v-if="group.physicalAddress && addressFullName(group.physicalAddress)"
|
||||||
:physicalAddress="group.physicalAddress"
|
:physicalAddress="group.physicalAddress"
|
||||||
/>
|
/>
|
||||||
<p class="flex">
|
<p class="flex gap-1">
|
||||||
<Account />
|
<Account />
|
||||||
{{
|
{{
|
||||||
t(
|
t(
|
||||||
@ -58,10 +72,18 @@ import { addressFullName } from "@/types/address.model";
|
|||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
import AccountGroup from "vue-material-design-icons/AccountGroup.vue";
|
import AccountGroup from "vue-material-design-icons/AccountGroup.vue";
|
||||||
import Account from "vue-material-design-icons/Account.vue";
|
import Account from "vue-material-design-icons/Account.vue";
|
||||||
|
import { htmlToText } from "@/utils/html";
|
||||||
|
import { computed } from "vue";
|
||||||
|
|
||||||
defineProps<{
|
const props = withDefaults(
|
||||||
group: IGroup;
|
defineProps<{
|
||||||
}>();
|
group: IGroup;
|
||||||
|
showSummary: boolean;
|
||||||
|
}>(),
|
||||||
|
{ showSummary: true }
|
||||||
|
);
|
||||||
|
|
||||||
const { t } = useI18n({ useScope: "global" });
|
const { t } = useI18n({ useScope: "global" });
|
||||||
|
|
||||||
|
const saneSummary = computed(() => htmlToText(props.group.summary));
|
||||||
</script>
|
</script>
|
||||||
|
@ -62,7 +62,7 @@
|
|||||||
<div
|
<div
|
||||||
class="mt-3 prose dark:prose-invert lg:prose-xl line-clamp-2"
|
class="mt-3 prose dark:prose-invert lg:prose-xl line-clamp-2"
|
||||||
v-if="member.parent.summary"
|
v-if="member.parent.summary"
|
||||||
v-html="flattenHTMLParagraphs(member.parent.summary)"
|
v-html="htmlToText(member.parent.summary)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@ -95,7 +95,7 @@ import DotsHorizontal from "vue-material-design-icons/DotsHorizontal.vue";
|
|||||||
import AccountGroup from "vue-material-design-icons/AccountGroup.vue";
|
import AccountGroup from "vue-material-design-icons/AccountGroup.vue";
|
||||||
import AccountCircle from "vue-material-design-icons/AccountCircle.vue";
|
import AccountCircle from "vue-material-design-icons/AccountCircle.vue";
|
||||||
import Tag from "@/components/Tag.vue";
|
import Tag from "@/components/Tag.vue";
|
||||||
import { flattenHTMLParagraphs } from "@/utils/html";
|
import { htmlToText } from "@/utils/html";
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
member: IMember;
|
member: IMember;
|
||||||
|
@ -57,13 +57,11 @@ const imageOpacity = computed(() =>
|
|||||||
);
|
);
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
console.debug("on lazy image mounted");
|
|
||||||
observer.value = new IntersectionObserver((entries) => {
|
observer.value = new IntersectionObserver((entries) => {
|
||||||
isIntersecting.value = entries[0].isIntersecting;
|
isIntersecting.value = entries[0].isIntersecting;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (wrapper.value) {
|
if (wrapper.value) {
|
||||||
console.debug("starting observing");
|
|
||||||
observer.value.observe(wrapper.value);
|
observer.value.observe(wrapper.value);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
v-if="suggestGeoloc && isIPLocation"
|
v-if="suggestGeoloc"
|
||||||
class="inline-flex bg-primary rounded text-white flex-initial px-4 py-2 justify-center w-full md:w-min whitespace-nowrap"
|
class="inline-flex bg-primary rounded text-white flex-initial px-4 py-2 justify-center w-full md:w-min whitespace-nowrap"
|
||||||
@click="emit('doGeoLoc')"
|
@click="emit('doGeoLoc')"
|
||||||
>
|
>
|
||||||
@ -21,7 +21,7 @@
|
|||||||
<div class="hidden sm:block" v-show="showScrollLeftButton">
|
<div class="hidden sm:block" v-show="showScrollLeftButton">
|
||||||
<button
|
<button
|
||||||
@click="scrollLeft"
|
@click="scrollLeft"
|
||||||
class="absolute inset-y-0 my-auto z-10 rounded-full bg-white w-10 h-10 border border-shadowColor -left-5"
|
class="absolute inset-y-0 my-auto z-10 rounded-full bg-white dark:bg-transparent w-10 h-10 border border-shadowColor -left-5"
|
||||||
>
|
>
|
||||||
<div class=""><</div>
|
<div class=""><</div>
|
||||||
</button>
|
</button>
|
||||||
@ -38,7 +38,7 @@
|
|||||||
<div class="hidden sm:block" v-show="showScrollRightButton">
|
<div class="hidden sm:block" v-show="showScrollRightButton">
|
||||||
<button
|
<button
|
||||||
@click="scrollRight"
|
@click="scrollRight"
|
||||||
class="absolute inset-y-0 my-auto z-10 rounded-full bg-white w-10 h-10 border border-shadowColor -right-5"
|
class="absolute inset-y-0 my-auto z-10 rounded-full bg-white dark:bg-transparent w-10 h-10 border border-shadowColor -right-5"
|
||||||
>
|
>
|
||||||
<div class="">></div>
|
<div class="">></div>
|
||||||
</button>
|
</button>
|
||||||
@ -47,9 +47,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, inject, onMounted, onUnmounted, ref } from "vue";
|
import { onMounted, onUnmounted, ref } from "vue";
|
||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
import { LocationType } from "../../types/user-location.model";
|
|
||||||
|
|
||||||
withDefaults(
|
withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
@ -62,14 +61,6 @@ const emit = defineEmits(["doGeoLoc"]);
|
|||||||
|
|
||||||
const { t } = useI18n({ useScope: "global" });
|
const { t } = useI18n({ useScope: "global" });
|
||||||
|
|
||||||
const userLocationInjection = inject<{
|
|
||||||
userLocation: LocationType;
|
|
||||||
}>("userLocation");
|
|
||||||
|
|
||||||
const isIPLocation = computed(
|
|
||||||
() => userLocationInjection?.userLocation.isIPLocation
|
|
||||||
);
|
|
||||||
|
|
||||||
const showScrollRightButton = ref(true);
|
const showScrollRightButton = ref(true);
|
||||||
const showScrollLeftButton = ref(false);
|
const showScrollLeftButton = ref(false);
|
||||||
|
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<close-content
|
<close-content
|
||||||
|
class="container mx-auto px-2"
|
||||||
v-show="loadingEvents || (events && events.total > 0)"
|
v-show="loadingEvents || (events && events.total > 0)"
|
||||||
:suggestGeoloc="suggestGeoloc"
|
:suggestGeoloc="suggestGeoloc"
|
||||||
v-on="attrs"
|
v-on="attrs"
|
||||||
|
@doGeoLoc="emit('doGeoLoc')"
|
||||||
>
|
>
|
||||||
<template #title>
|
<template #title>
|
||||||
<template v-if="userLocationName">
|
<template v-if="userLocationName">
|
||||||
@ -25,13 +27,7 @@
|
|||||||
:key="event.uuid"
|
:key="event.uuid"
|
||||||
/>
|
/>
|
||||||
<more-content
|
<more-content
|
||||||
v-if="
|
v-if="userLocationName && userLocation?.lat && userLocation?.lon"
|
||||||
userLocation &&
|
|
||||||
userLocationName &&
|
|
||||||
userLocation.lat &&
|
|
||||||
userLocation.lon &&
|
|
||||||
userLocation.picture
|
|
||||||
"
|
|
||||||
:to="{
|
:to="{
|
||||||
name: 'SEARCH',
|
name: 'SEARCH',
|
||||||
query: {
|
query: {
|
||||||
@ -39,10 +35,10 @@
|
|||||||
lat: userLocation.lat?.toString(),
|
lat: userLocation.lat?.toString(),
|
||||||
lon: userLocation.lon?.toString(),
|
lon: userLocation.lon?.toString(),
|
||||||
contentType: 'EVENTS',
|
contentType: 'EVENTS',
|
||||||
distance: '25',
|
distance: '25_km',
|
||||||
},
|
},
|
||||||
}"
|
}"
|
||||||
:picture="userLocation.picture"
|
:picture="userLocation?.picture"
|
||||||
>
|
>
|
||||||
{{
|
{{
|
||||||
t("View more events around {position}", {
|
t("View more events around {position}", {
|
||||||
@ -65,115 +61,33 @@ import { useQuery } from "@vue/apollo-composable";
|
|||||||
import EventCard from "../Event/EventCard.vue";
|
import EventCard from "../Event/EventCard.vue";
|
||||||
import { Paginate } from "@/types/paginate";
|
import { Paginate } from "@/types/paginate";
|
||||||
import SkeletonEventResult from "../Event/SkeletonEventResult.vue";
|
import SkeletonEventResult from "../Event/SkeletonEventResult.vue";
|
||||||
import { CURRENT_USER_LOCATION_CLIENT } from "@/graphql/location";
|
|
||||||
import { ICurrentUser, IUser } from "@/types/current-user.model";
|
|
||||||
import { CURRENT_USER_CLIENT, USER_SETTINGS } from "@/graphql/user";
|
|
||||||
import { coordsToGeoHash, geoHashToCoords } from "@/utils/location";
|
|
||||||
import { REVERSE_GEOCODE } from "@/graphql/address";
|
|
||||||
import { IAddress } from "@/types/address.model";
|
|
||||||
import { CONFIG } from "@/graphql/config";
|
|
||||||
import { IConfig } from "@/types/config.model";
|
|
||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
|
import { coordsToGeoHash } from "@/utils/location";
|
||||||
|
|
||||||
|
const props = defineProps<{ userLocation: LocationType }>();
|
||||||
|
const emit = defineEmits(["doGeoLoc"]);
|
||||||
|
|
||||||
const EVENT_PAGE_LIMIT = 12;
|
const EVENT_PAGE_LIMIT = 12;
|
||||||
|
|
||||||
const { result: currentUserResult } = useQuery<{
|
|
||||||
currentUser: ICurrentUser;
|
|
||||||
}>(CURRENT_USER_CLIENT);
|
|
||||||
|
|
||||||
const currentUser = computed(() => currentUserResult.value?.currentUser);
|
|
||||||
|
|
||||||
const { result: userResult } = useQuery<{ loggedUser: IUser }>(
|
|
||||||
USER_SETTINGS,
|
|
||||||
{},
|
|
||||||
() => ({
|
|
||||||
enabled: currentUser.value?.isLoggedIn,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
const loggedUser = computed(() => userResult.value?.loggedUser);
|
|
||||||
|
|
||||||
const { result: configResult } = useQuery<{ config: IConfig }>(CONFIG);
|
|
||||||
|
|
||||||
const config = computed<IConfig | undefined>(() => configResult.value?.config);
|
|
||||||
|
|
||||||
const serverLocation = computed(() => config.value?.location);
|
|
||||||
|
|
||||||
const { t } = useI18n({ useScope: "global" });
|
const { t } = useI18n({ useScope: "global" });
|
||||||
const attrs = useAttrs();
|
const attrs = useAttrs();
|
||||||
|
|
||||||
const coords = computed(() => {
|
|
||||||
const userSettingsGeoHash =
|
|
||||||
loggedUser.value?.settings?.location?.geohash ?? undefined;
|
|
||||||
const userSettingsCoords = geoHashToCoords(userSettingsGeoHash);
|
|
||||||
|
|
||||||
if (userSettingsCoords) {
|
|
||||||
return { ...userSettingsCoords, isIPLocation: false };
|
|
||||||
}
|
|
||||||
|
|
||||||
return { ...serverLocation.value, isIPLocation: true };
|
|
||||||
});
|
|
||||||
|
|
||||||
const { result: reverseGeocodeResult } = useQuery<{
|
|
||||||
reverseGeocode: IAddress[];
|
|
||||||
}>(REVERSE_GEOCODE, coords, () => ({
|
|
||||||
enabled: coords.value?.longitude != undefined,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const userSettingsLocation = computed(() => {
|
|
||||||
const address = reverseGeocodeResult.value?.reverseGeocode[0];
|
|
||||||
const placeName = address?.locality ?? address?.region ?? address?.country;
|
|
||||||
return {
|
|
||||||
lat: coords.value?.latitude,
|
|
||||||
lon: coords.value?.longitude,
|
|
||||||
name: placeName,
|
|
||||||
picture: address?.pictureInfo,
|
|
||||||
isIPLocation: coords.value?.isIPLocation,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const { result: currentUserLocationResult } = useQuery<{
|
|
||||||
currentUserLocation: LocationType;
|
|
||||||
}>(CURRENT_USER_LOCATION_CLIENT);
|
|
||||||
const currentUserLocation = computed(() => {
|
|
||||||
return {
|
|
||||||
...currentUserLocationResult.value?.currentUserLocation,
|
|
||||||
isIPLocation: false,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const geohash = computed(() => {
|
|
||||||
return coordsToGeoHash(userLocation.value?.lat, userLocation.value?.lon);
|
|
||||||
});
|
|
||||||
|
|
||||||
const userLocationName = computed(() => {
|
const userLocationName = computed(() => {
|
||||||
return userLocation.value?.name;
|
return props.userLocation?.name;
|
||||||
});
|
});
|
||||||
|
const suggestGeoloc = computed(() => props.userLocation?.isIPLocation);
|
||||||
const userLocation = computed(() => {
|
|
||||||
if (
|
|
||||||
!userSettingsLocation.value ||
|
|
||||||
(userSettingsLocation.value?.isIPLocation &&
|
|
||||||
currentUserLocation.value?.name)
|
|
||||||
) {
|
|
||||||
return currentUserLocation.value;
|
|
||||||
}
|
|
||||||
return userSettingsLocation.value;
|
|
||||||
});
|
|
||||||
|
|
||||||
const suggestGeoloc = computed(() => userLocation.value?.isIPLocation);
|
|
||||||
|
|
||||||
const { result: eventsResult, loading: loadingEvents } = useQuery<{
|
const { result: eventsResult, loading: loadingEvents } = useQuery<{
|
||||||
searchEvents: Paginate<IEvent>;
|
searchEvents: Paginate<IEvent>;
|
||||||
}>(SEARCH_EVENTS, {
|
}>(SEARCH_EVENTS, () => ({
|
||||||
location: geohash,
|
location: coordsToGeoHash(props.userLocation.lat, props.userLocation.lon),
|
||||||
beginsOn: new Date(),
|
beginsOn: new Date(),
|
||||||
endsOn: undefined,
|
endsOn: undefined,
|
||||||
radius: 25,
|
radius: 25,
|
||||||
eventPage: 1,
|
eventPage: 1,
|
||||||
limit: EVENT_PAGE_LIMIT,
|
limit: EVENT_PAGE_LIMIT,
|
||||||
type: "IN_PERSON",
|
type: "IN_PERSON",
|
||||||
});
|
}));
|
||||||
|
|
||||||
const events = computed(
|
const events = computed(
|
||||||
() => eventsResult.value?.searchEvents ?? { elements: [], total: 0 }
|
() => eventsResult.value?.searchEvents ?? { elements: [], total: 0 }
|
||||||
|
@ -1,11 +1,21 @@
|
|||||||
<template>
|
<template>
|
||||||
<close-content v-show="loadingGroups || selectedGroups.length > 0">
|
<close-content
|
||||||
|
class="container mx-auto px-2"
|
||||||
|
v-show="loadingGroups || selectedGroups.length > 0"
|
||||||
|
@do-geo-loc="emit('doGeoLoc')"
|
||||||
|
:suggestGeoloc="userLocation.isIPLocation"
|
||||||
|
>
|
||||||
<template #title>
|
<template #title>
|
||||||
{{
|
<template v-if="userLocationName">
|
||||||
$t("Popular groups nearby {position}", {
|
{{
|
||||||
position: userLocationName,
|
t("Popular groups nearby {position}", {
|
||||||
})
|
position: userLocationName,
|
||||||
}}
|
})
|
||||||
|
}}
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
{{ t("Popular groups close to you") }}
|
||||||
|
</template>
|
||||||
</template>
|
</template>
|
||||||
<template #content>
|
<template #content>
|
||||||
<!-- <skeleton-group-result
|
<!-- <skeleton-group-result
|
||||||
@ -14,32 +24,32 @@
|
|||||||
:key="i"
|
:key="i"
|
||||||
v-show="loadingGroups"
|
v-show="loadingGroups"
|
||||||
/> -->
|
/> -->
|
||||||
<group-result
|
<group-card
|
||||||
v-for="group in selectedGroups"
|
v-for="group in selectedGroups"
|
||||||
:key="group.id"
|
:key="group.id"
|
||||||
class="scroll-ml-6 snap-center shrink-0 first:pl-8 last:pr-8 w-[18rem]"
|
|
||||||
:group="group"
|
:group="group"
|
||||||
:view-mode="'column'"
|
:view-mode="'column'"
|
||||||
:minimal="true"
|
:minimal="true"
|
||||||
:has-border="true"
|
:has-border="true"
|
||||||
|
:showSummary="false"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<more-content
|
<more-content
|
||||||
v-if="userLocation"
|
v-if="userLocationName"
|
||||||
:to="{
|
:to="{
|
||||||
name: 'SEARCH',
|
name: 'SEARCH',
|
||||||
query: {
|
query: {
|
||||||
locationName: userLocationName,
|
locationName: userLocationName,
|
||||||
lat: userLocation.lat,
|
lat: userLocation.lat?.toString(),
|
||||||
lon: userLocation.lon,
|
lon: userLocation.lon?.toString(),
|
||||||
contentType: 'GROUPS',
|
contentType: 'GROUPS',
|
||||||
distance: currentDistance,
|
distance: '25_km',
|
||||||
},
|
},
|
||||||
}"
|
}"
|
||||||
:picture="userLocation.picture"
|
:picture="userLocation.picture"
|
||||||
>
|
>
|
||||||
{{
|
{{
|
||||||
$t("View more groups around {position}", {
|
t("View more groups around {position}", {
|
||||||
position: userLocationName,
|
position: userLocationName,
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
@ -48,8 +58,7 @@
|
|||||||
</close-content>
|
</close-content>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts" setup>
|
||||||
import { defineComponent, inject, reactive, computed, ref } from "vue";
|
|
||||||
// import SkeletonGroupResult from "../../components/result/SkeletonGroupResult.vue";
|
// import SkeletonGroupResult from "../../components/result/SkeletonGroupResult.vue";
|
||||||
import sampleSize from "lodash/sampleSize";
|
import sampleSize from "lodash/sampleSize";
|
||||||
import { LocationType } from "../../types/user-location.model";
|
import { LocationType } from "../../types/user-location.model";
|
||||||
@ -57,39 +66,36 @@ import MoreContent from "./MoreContent.vue";
|
|||||||
import CloseContent from "./CloseContent.vue";
|
import CloseContent from "./CloseContent.vue";
|
||||||
import { IGroup } from "@/types/actor";
|
import { IGroup } from "@/types/actor";
|
||||||
import { SEARCH_GROUPS } from "@/graphql/search";
|
import { SEARCH_GROUPS } from "@/graphql/search";
|
||||||
|
import { useQuery } from "@vue/apollo-composable";
|
||||||
|
import { Paginate } from "@/types/paginate";
|
||||||
|
import { computed } from "vue";
|
||||||
|
import GroupCard from "@/components/Group/GroupCard.vue";
|
||||||
|
import { coordsToGeoHash } from "@/utils/location";
|
||||||
|
import { useI18n } from "vue-i18n";
|
||||||
|
|
||||||
export default defineComponent({
|
const props = defineProps<{ userLocation: LocationType }>();
|
||||||
components: {
|
const emit = defineEmits(["doGeoLoc"]);
|
||||||
MoreContent,
|
|
||||||
CloseContent,
|
|
||||||
// SkeletonGroupResult,
|
|
||||||
},
|
|
||||||
apollo: {
|
|
||||||
groups: {
|
|
||||||
query: SEARCH_GROUPS,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
setup() {
|
|
||||||
const userLocationInjection = inject<{
|
|
||||||
userLocation: LocationType;
|
|
||||||
}>("userLocation");
|
|
||||||
|
|
||||||
const groups = reactive<IGroup[]>([]);
|
const { t } = useI18n({ useScope: "global" });
|
||||||
|
|
||||||
const selectedGroups = computed(() => sampleSize(groups, 5));
|
const { result: groupsResult, loading: loadingGroups } = useQuery<{
|
||||||
|
searchGroups: Paginate<IGroup>;
|
||||||
|
}>(
|
||||||
|
SEARCH_GROUPS,
|
||||||
|
() => ({
|
||||||
|
location: coordsToGeoHash(props.userLocation.lat, props.userLocation.lon),
|
||||||
|
radius: 25,
|
||||||
|
page: 1,
|
||||||
|
limit: 12,
|
||||||
|
}),
|
||||||
|
() => ({ enabled: props.userLocation?.lat !== undefined })
|
||||||
|
);
|
||||||
|
|
||||||
const currentDistance = ref("25_km");
|
const groups = computed(
|
||||||
|
() => groupsResult.value?.searchGroups ?? { total: 0, elements: [] }
|
||||||
|
);
|
||||||
|
|
||||||
const userLocationName = computed(
|
const selectedGroups = computed(() => sampleSize(groups.value?.elements, 5));
|
||||||
() => userLocationInjection?.userLocation?.name
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
const userLocationName = computed(() => props?.userLocation?.name);
|
||||||
selectedGroups,
|
|
||||||
userLocation: userLocationInjection?.userLocation,
|
|
||||||
userLocationName,
|
|
||||||
currentDistance,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<close-content
|
<close-content
|
||||||
|
class="container mx-auto px-2"
|
||||||
v-show="loadingEvents || (events && events.total > 0)"
|
v-show="loadingEvents || (events && events.total > 0)"
|
||||||
:suggestGeoloc="false"
|
:suggestGeoloc="false"
|
||||||
v-on="attrs"
|
v-on="attrs"
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<router-link
|
<router-link
|
||||||
:to="to"
|
:to="to"
|
||||||
class="mbz-card flex flex-col items-center dark:border-gray-700 shadow-md md:flex-col snap-center shrink-0 first:pl-8 w-[18rem]"
|
class="mbz-card flex flex-col items-center dark:border-gray-700 shadow-md md:flex-col snap-center shrink-0 first:pl-8 w-[18rem] dark:bg-mbz-purple"
|
||||||
>
|
>
|
||||||
<div class="relative w-full group">
|
<div class="relative w-full group">
|
||||||
<img
|
<img
|
||||||
@ -63,7 +63,7 @@ import { computed } from "vue";
|
|||||||
import { CategoryPictureLicencing } from "../Categories/constants";
|
import { CategoryPictureLicencing } from "../Categories/constants";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
to: { name: string; query: Record<string, string> };
|
to: { name: string; query: Record<string, string | undefined> };
|
||||||
picture?: CategoryPictureLicencing & { url: string };
|
picture?: CategoryPictureLicencing & { url: string };
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
70
js/src/components/Search/filters/FilterSection.vue
Normal file
70
js/src/components/Search/filters/FilterSection.vue
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
<template>
|
||||||
|
<div class="border-b border-gray-200 dark:border-gray-500 py-6">
|
||||||
|
<h2 class="-my-3 flow-root">
|
||||||
|
<!-- Expand/collapse section button -->
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="py-3 w-full flex items-center justify-between text-gray-400 hover:text-gray-500 dark:text-slate-100 hover:dark:text-slate-200"
|
||||||
|
aria-controls="filter-section-mobile-0"
|
||||||
|
:aria-expanded="opened"
|
||||||
|
@click="$emit('update:opened', !opened)"
|
||||||
|
>
|
||||||
|
<div class="flex flex-wrap gap-1 flex-col items-start">
|
||||||
|
<span class="font-medium text-gray-900 dark:text-slate-100 text-left">
|
||||||
|
{{ title }}
|
||||||
|
</span>
|
||||||
|
<slot name="preview" />
|
||||||
|
</div>
|
||||||
|
<span class="ml-6 flex items-center">
|
||||||
|
<svg
|
||||||
|
v-show="!opened"
|
||||||
|
class="h-5 w-5"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M10 5a1 1 0 011 1v3h3a1 1 0 110 2h-3v3a1 1 0 11-2 0v-3H6a1 1 0 110-2h3V6a1 1 0 011-1z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<svg
|
||||||
|
v-show="opened"
|
||||||
|
class="h-5 w-5"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M5 10a1 1 0 011-1h8a1 1 0 110 2H6a1 1 0 01-1-1z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</h2>
|
||||||
|
<!-- Filter section, show/hide based on section state. -->
|
||||||
|
<transition>
|
||||||
|
<div v-show="opened" class="pt-2">
|
||||||
|
<slot name="options" />
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
defineProps({
|
||||||
|
title: {
|
||||||
|
required: true,
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
opened: {
|
||||||
|
required: true,
|
||||||
|
type: Boolean,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
defineEmits(["update:opened"]);
|
||||||
|
</script>
|
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<span
|
<span
|
||||||
class="rounded-md my-1 truncate text-sm text-violet-title capitalize px-2 py-1"
|
class="rounded-md my-1 truncate text-sm text-violet-title px-2 py-1"
|
||||||
:class="typeClasses"
|
:class="[typeClasses, capitalize]"
|
||||||
>
|
>
|
||||||
<slot />
|
<slot />
|
||||||
</span>
|
</span>
|
||||||
@ -12,8 +12,9 @@ import { computed } from "vue";
|
|||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
variant?: "info" | "danger" | "warning" | "light";
|
variant?: "info" | "danger" | "warning" | "light";
|
||||||
|
capitalize: boolean;
|
||||||
}>(),
|
}>(),
|
||||||
{ variant: "light" }
|
{ variant: "light", capitalize: false }
|
||||||
);
|
);
|
||||||
|
|
||||||
const typeClasses = computed(() => {
|
const typeClasses = computed(() => {
|
||||||
@ -23,6 +24,8 @@ const typeClasses = computed(() => {
|
|||||||
return "bg-purple-3 dark:text-violet-3";
|
return "bg-purple-3 dark:text-violet-3";
|
||||||
case "info":
|
case "info":
|
||||||
return "bg-mbz-info dark:text-white";
|
return "bg-mbz-info dark:text-white";
|
||||||
|
case "warning":
|
||||||
|
return "bg-yellow-1";
|
||||||
case "danger":
|
case "danger":
|
||||||
return "bg-mbz-danger dark:text-white";
|
return "bg-mbz-danger dark:text-white";
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="attached && closable" class="tags has-addons">
|
<div v-if="attached && closable" class="tags has-addons">
|
||||||
<span class="tag" :class="[type, size, { 'is-rounded': rounded }]">
|
<span class="tag" :class="[variant, size, { rounded }]">
|
||||||
<!-- <o-icon
|
<!-- <o-icon
|
||||||
v-if="icon"
|
v-if="icon"
|
||||||
:icon="icon"
|
:icon="icon"
|
||||||
@ -15,7 +15,7 @@
|
|||||||
class="tag"
|
class="tag"
|
||||||
role="button"
|
role="button"
|
||||||
:aria-label="ariaCloseLabel"
|
:aria-label="ariaCloseLabel"
|
||||||
:tabindex="tabstop ? 0 : false"
|
:tabindex="tabstop ? 0 : undefined"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
:class="[
|
:class="[
|
||||||
size,
|
size,
|
||||||
@ -35,7 +35,7 @@
|
|||||||
/> -->
|
/> -->
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<span v-else class="tag" :class="[type, size, { 'is-rounded': rounded }]">
|
<span v-else class="tag" :class="[variant, size, { 'is-rounded': rounded }]">
|
||||||
<!-- <o-icon
|
<!-- <o-icon
|
||||||
v-if="icon"
|
v-if="icon"
|
||||||
:icon="icon"
|
:icon="icon"
|
||||||
@ -53,7 +53,7 @@
|
|||||||
class="delete is-small"
|
class="delete is-small"
|
||||||
:class="closeType"
|
:class="closeType"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
:tabindex="tabstop ? 0 : false"
|
:tabindex="tabstop ? 0 : undefined"
|
||||||
@click="close"
|
@click="close"
|
||||||
@keyup.delete.prevent="close"
|
@keyup.delete.prevent="close"
|
||||||
/>
|
/>
|
||||||
@ -64,7 +64,7 @@ const props = withDefaults(
|
|||||||
defineProps<{
|
defineProps<{
|
||||||
attached?: boolean;
|
attached?: boolean;
|
||||||
closable?: boolean;
|
closable?: boolean;
|
||||||
type?: string;
|
variant?: string;
|
||||||
size?: string;
|
size?: string;
|
||||||
rounded?: boolean;
|
rounded?: boolean;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
@ -227,6 +227,8 @@ const icons: Record<string, () => Promise<any>> = {
|
|||||||
import(`../../../node_modules/vue-material-design-icons/LanDisconnect.vue`),
|
import(`../../../node_modules/vue-material-design-icons/LanDisconnect.vue`),
|
||||||
CloudQuestion: () =>
|
CloudQuestion: () =>
|
||||||
import(`../../../node_modules/vue-material-design-icons/CloudQuestion.vue`),
|
import(`../../../node_modules/vue-material-design-icons/CloudQuestion.vue`),
|
||||||
|
Filter: () =>
|
||||||
|
import(`../../../node_modules/vue-material-design-icons/Filter.vue`),
|
||||||
};
|
};
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
|
@ -11,7 +11,8 @@ export const SEARCH_EVENTS_AND_GROUPS = gql`
|
|||||||
$tags: String
|
$tags: String
|
||||||
$term: String
|
$term: String
|
||||||
$type: EventType
|
$type: EventType
|
||||||
$category: String
|
$categoryOneOf: [String]
|
||||||
|
$statusOneOf: [EventStatus]
|
||||||
$beginsOn: DateTime
|
$beginsOn: DateTime
|
||||||
$endsOn: DateTime
|
$endsOn: DateTime
|
||||||
$eventPage: Int
|
$eventPage: Int
|
||||||
@ -24,7 +25,8 @@ export const SEARCH_EVENTS_AND_GROUPS = gql`
|
|||||||
tags: $tags
|
tags: $tags
|
||||||
term: $term
|
term: $term
|
||||||
type: $type
|
type: $type
|
||||||
category: $category
|
categoryOneOf: $categoryOneOf
|
||||||
|
statusOneOf: $statusOneOf
|
||||||
beginsOn: $beginsOn
|
beginsOn: $beginsOn
|
||||||
endsOn: $endsOn
|
endsOn: $endsOn
|
||||||
page: $eventPage
|
page: $eventPage
|
||||||
@ -156,9 +158,7 @@ export const SEARCH_GROUPS = gql`
|
|||||||
query SearchGroups(
|
query SearchGroups(
|
||||||
$location: String
|
$location: String
|
||||||
$radius: Float
|
$radius: Float
|
||||||
$tags: String
|
|
||||||
$term: String
|
$term: String
|
||||||
$category: String
|
|
||||||
$groupPage: Int
|
$groupPage: Int
|
||||||
$limit: Int
|
$limit: Int
|
||||||
) {
|
) {
|
||||||
@ -188,8 +188,6 @@ export const SEARCH_GROUPS = gql`
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
${EVENT_OPTIONS_FRAGMENT}
|
|
||||||
${TAG_FRAGMENT}
|
|
||||||
${ADDRESS_FRAGMENT}
|
${ADDRESS_FRAGMENT}
|
||||||
${ACTOR_FRAGMENT}
|
${ACTOR_FRAGMENT}
|
||||||
`;
|
`;
|
||||||
|
@ -1365,5 +1365,23 @@
|
|||||||
"multitude of interconnected Mobilizon websites": "multitude of interconnected Mobilizon websites",
|
"multitude of interconnected Mobilizon websites": "multitude of interconnected Mobilizon websites",
|
||||||
"Mobilizon is a tool that helps you {find_create_organize_events}.": "Mobilizon is a tool that helps you {find_create_organize_events}.",
|
"Mobilizon is a tool that helps you {find_create_organize_events}.": "Mobilizon is a tool that helps you {find_create_organize_events}.",
|
||||||
"Ethical alternative to Facebook events, groups and pages, Mobilizon is a {tool_designed_to_serve_you}. Period.": "Ethical alternative to Facebook events, groups and pages, Mobilizon is a {tool_designed_to_serve_you}. Period.",
|
"Ethical alternative to Facebook events, groups and pages, Mobilizon is a {tool_designed_to_serve_you}. Period.": "Ethical alternative to Facebook events, groups and pages, Mobilizon is a {tool_designed_to_serve_you}. Period.",
|
||||||
"Mobilizon is not a giant platform, but a {multitude_of_interconnected_mobilizon_websites}.": "Mobilizon is not a giant platform, but a {multitude_of_interconnected_mobilizon_websites}."
|
"Mobilizon is not a giant platform, but a {multitude_of_interconnected_mobilizon_websites}.": "Mobilizon is not a giant platform, but a {multitude_of_interconnected_mobilizon_websites}.",
|
||||||
|
"translation": "",
|
||||||
|
"detail": "",
|
||||||
|
"Events close to you": "Events close to you",
|
||||||
|
"Popular groups close to you": "Popular groups close to you",
|
||||||
|
"View more events": "View more events",
|
||||||
|
"Hide filters": "Hide filters",
|
||||||
|
"Show filters": "Show filters",
|
||||||
|
"Online events": "Online events",
|
||||||
|
"Event date": "Event date",
|
||||||
|
"Distance": "Distance",
|
||||||
|
"{numberOfCategories} selected": "{numberOfCategories} selected",
|
||||||
|
"Event status": "Event status",
|
||||||
|
"Statuses": "Statuses",
|
||||||
|
"Languages": "Languages",
|
||||||
|
"{numberOfLanguages} selected": "{numberOfLanguages} selected",
|
||||||
|
"Apply filters": "Apply filters",
|
||||||
|
"Any distance": "Any distance",
|
||||||
|
"{number} kilometers": "{number} kilometers"
|
||||||
}
|
}
|
@ -1351,5 +1351,21 @@
|
|||||||
"multitude of interconnected Mobilizon websites": "multitude de sites web Mobilizon interconnectés",
|
"multitude of interconnected Mobilizon websites": "multitude de sites web Mobilizon interconnectés",
|
||||||
"Mobilizon is a tool that helps you {find_create_organize_events}.": "Mobilizon est un outil qui vous permet de {find_create_organize_events}.",
|
"Mobilizon is a tool that helps you {find_create_organize_events}.": "Mobilizon est un outil qui vous permet de {find_create_organize_events}.",
|
||||||
"Ethical alternative to Facebook events, groups and pages, Mobilizon is a {tool_designed_to_serve_you}. Period.": "Alternative éthique aux événements, groupes et pages Facebook, Mobilizon est un {tool_designed_to_serve_you}. Point.",
|
"Ethical alternative to Facebook events, groups and pages, Mobilizon is a {tool_designed_to_serve_you}. Period.": "Alternative éthique aux événements, groupes et pages Facebook, Mobilizon est un {tool_designed_to_serve_you}. Point.",
|
||||||
"Mobilizon is not a giant platform, but a {multitude_of_interconnected_mobilizon_websites}.": "Mobilizon n’est pas une plateforme géante, mais une {multitude_of_interconnected_mobilizon_websites}."
|
"Mobilizon is not a giant platform, but a {multitude_of_interconnected_mobilizon_websites}.": "Mobilizon n’est pas une plateforme géante, mais une {multitude_of_interconnected_mobilizon_websites}.",
|
||||||
|
"Events close to you": "Événements proches de vous",
|
||||||
|
"Popular groups close to you": "Groupes populaires proches de vous",
|
||||||
|
"View more events": "Voir plus d'événements",
|
||||||
|
"Hide filters": "Masquer les filtres",
|
||||||
|
"Show filters": "Afficher les filtres",
|
||||||
|
"Online events": "Événements en ligne",
|
||||||
|
"Event date": "Date de l'événement",
|
||||||
|
"Distance": "Distance",
|
||||||
|
"{numberOfCategories} selected": "{numberOfCategories} sélectionnées",
|
||||||
|
"Event status": "Statut de l'événement",
|
||||||
|
"Statuses": "Statuts",
|
||||||
|
"Languages": "Langues",
|
||||||
|
"{numberOfLanguages} selected": "{numberOfLanguages} sélectionnées",
|
||||||
|
"Apply filters": "Appliquer les filtres",
|
||||||
|
"Any distance": "N'importe quelle distance",
|
||||||
|
"{number} kilometers": "{number} kilomètres"
|
||||||
}
|
}
|
||||||
|
@ -20,7 +20,7 @@ import { dateFnsPlugin } from "./plugins/dateFns";
|
|||||||
import { dialogPlugin } from "./plugins/dialog";
|
import { dialogPlugin } from "./plugins/dialog";
|
||||||
import { snackbarPlugin } from "./plugins/snackbar";
|
import { snackbarPlugin } from "./plugins/snackbar";
|
||||||
import { notifierPlugin } from "./plugins/notifier";
|
import { notifierPlugin } from "./plugins/notifier";
|
||||||
import Tag from "./components/core/Tag.vue";
|
import Tag from "./components/core/CoreTag.vue";
|
||||||
import FloatingVue from "floating-vue";
|
import FloatingVue from "floating-vue";
|
||||||
import "floating-vue/dist/style.css";
|
import "floating-vue/dist/style.css";
|
||||||
import Oruga from "@oruga-ui/oruga-next";
|
import Oruga from "@oruga-ui/oruga-next";
|
||||||
|
@ -77,4 +77,25 @@ export const orugaConfig = {
|
|||||||
tableClass: "table",
|
tableClass: "table",
|
||||||
tdClass: "table-td",
|
tdClass: "table-td",
|
||||||
},
|
},
|
||||||
|
pagination: {
|
||||||
|
rootClass: "pagination",
|
||||||
|
simpleClass: "pagination-simple",
|
||||||
|
listClass: "pagination-list",
|
||||||
|
infoClass: "pagination-info",
|
||||||
|
linkClass: "pagination-link",
|
||||||
|
linkCurrentClass: "pagination-link-current",
|
||||||
|
linkDisabledClass: "pagination-link-disabled",
|
||||||
|
nextBtnClass: "pagination-next",
|
||||||
|
prevBtnClass: "pagination-previous",
|
||||||
|
ellipsisClass: "pagination-ellipsis",
|
||||||
|
},
|
||||||
|
tabs: {
|
||||||
|
rootClass: "tabs",
|
||||||
|
navTabsClass: "tabs-nav",
|
||||||
|
navTypeClass: "tabs-nav-",
|
||||||
|
navSizeClass: "tabs-nav-",
|
||||||
|
tabItemWrapperClass: "tabs-nav-item-wrapper",
|
||||||
|
itemHeaderTypeClass: "tabs-nav-item-",
|
||||||
|
itemHeaderActiveClass: "tabs-nav-item-active-",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
/* eslint-disable no-console */
|
|
||||||
|
|
||||||
import { register } from "register-service-worker";
|
import { register } from "register-service-worker";
|
||||||
|
|
||||||
if ("serviceWorker" in navigator) {
|
if ("serviceWorker" in navigator && import.meta.env.PROD) {
|
||||||
register(`${import.meta.env.BASE_URL}service-worker.js`, {
|
register(`${import.meta.env.BASE_URL}service-worker.js`, {
|
||||||
ready() {
|
ready() {
|
||||||
console.debug(
|
console.debug(
|
||||||
|
@ -130,6 +130,12 @@ export enum SearchTabs {
|
|||||||
GROUPS = 1,
|
GROUPS = 1,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum ContentType {
|
||||||
|
ALL = "ALL",
|
||||||
|
EVENTS = "EVENTS",
|
||||||
|
GROUPS = "GROUPS",
|
||||||
|
}
|
||||||
|
|
||||||
export enum ActorType {
|
export enum ActorType {
|
||||||
PERSON = "PERSON",
|
PERSON = "PERSON",
|
||||||
APPLICATION = "APPLICATION",
|
APPLICATION = "APPLICATION",
|
||||||
|
@ -2,6 +2,11 @@ export function nl2br(text: string): string {
|
|||||||
return text.replace(/(?:\r\n|\r|\n)/g, "<br>");
|
return text.replace(/(?:\r\n|\r|\n)/g, "<br>");
|
||||||
}
|
}
|
||||||
|
|
||||||
export function flattenHTMLParagraphs(html: string): string {
|
export function htmlToText(html: string) {
|
||||||
return html.replace("</p><p>", "<br />").replace(/<?\/p>/g, "");
|
const template = document.createElement("template");
|
||||||
|
const trimmedHTML = html.trim();
|
||||||
|
template.innerHTML = trimmedHTML;
|
||||||
|
const text = template.content.textContent;
|
||||||
|
template.remove();
|
||||||
|
return text;
|
||||||
}
|
}
|
||||||
|
@ -11,17 +11,10 @@ const localeInLocalStorage = getLocaleData();
|
|||||||
|
|
||||||
export const AVAILABLE_LANGUAGES = Object.keys(langs);
|
export const AVAILABLE_LANGUAGES = Object.keys(langs);
|
||||||
|
|
||||||
console.debug("localeInLocalStorage", localeInLocalStorage);
|
|
||||||
|
|
||||||
let language =
|
let language =
|
||||||
localeInLocalStorage ||
|
localeInLocalStorage ||
|
||||||
(document.documentElement.getAttribute("lang") as string);
|
(document.documentElement.getAttribute("lang") as string);
|
||||||
|
|
||||||
console.debug(
|
|
||||||
"localeInLocalStorage or fallback to lang html attribute",
|
|
||||||
language
|
|
||||||
);
|
|
||||||
|
|
||||||
language =
|
language =
|
||||||
language ||
|
language ||
|
||||||
((window.navigator as any).userLanguage || window.navigator.language).replace(
|
((window.navigator as any).userLanguage || window.navigator.language).replace(
|
||||||
@ -29,15 +22,11 @@ language =
|
|||||||
"_"
|
"_"
|
||||||
);
|
);
|
||||||
|
|
||||||
console.debug("language or fallback to window.navigator language", language);
|
|
||||||
|
|
||||||
export const locale =
|
export const locale =
|
||||||
language && Object.prototype.hasOwnProperty.call(langs, language)
|
language && Object.prototype.hasOwnProperty.call(langs, language)
|
||||||
? language
|
? language
|
||||||
: language.split("-")[0];
|
: language.split("-")[0];
|
||||||
|
|
||||||
console.debug("chosen locale", locale);
|
|
||||||
|
|
||||||
export const i18n = createI18n({
|
export const i18n = createI18n({
|
||||||
legacy: false,
|
legacy: false,
|
||||||
locale: locale, // set locale
|
locale: locale, // set locale
|
||||||
@ -52,12 +41,9 @@ export const i18n = createI18n({
|
|||||||
globalInjection: true,
|
globalInjection: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
console.debug("set VueI18n with default locale", DEFAULT_LOCALE);
|
|
||||||
|
|
||||||
const loadedLanguages = [DEFAULT_LOCALE];
|
const loadedLanguages = [DEFAULT_LOCALE];
|
||||||
|
|
||||||
function setI18nLanguage(lang: string): string {
|
function setI18nLanguage(lang: string): string {
|
||||||
console.debug("setting i18n locale to", lang);
|
|
||||||
i18n.global.locale = lang;
|
i18n.global.locale = lang;
|
||||||
setLanguageInDOM(lang);
|
setLanguageInDOM(lang);
|
||||||
return lang;
|
return lang;
|
||||||
@ -74,7 +60,6 @@ function setLanguageInDOM(lang: string): void {
|
|||||||
const direction = ["ar", "ae", "he", "fa", "ku", "ur"].includes(fixedLang)
|
const direction = ["ar", "ae", "he", "fa", "ku", "ur"].includes(fixedLang)
|
||||||
? "rtl"
|
? "rtl"
|
||||||
: "ltr";
|
: "ltr";
|
||||||
console.debug("setDirection with", [fixedLang, direction]);
|
|
||||||
html.setAttribute("dir", direction);
|
html.setAttribute("dir", direction);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -96,17 +81,14 @@ function vueI18NfileForLanguage(lang: string) {
|
|||||||
export async function loadLanguageAsync(lang: string): Promise<string> {
|
export async function loadLanguageAsync(lang: string): Promise<string> {
|
||||||
// If the same language
|
// If the same language
|
||||||
if (i18n.global.locale === lang) {
|
if (i18n.global.locale === lang) {
|
||||||
console.debug("already using language", lang);
|
|
||||||
return Promise.resolve(setI18nLanguage(lang));
|
return Promise.resolve(setI18nLanguage(lang));
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the language was already loaded
|
// If the language was already loaded
|
||||||
if (loadedLanguages.includes(lang)) {
|
if (loadedLanguages.includes(lang)) {
|
||||||
console.debug("language already loaded", lang);
|
|
||||||
return Promise.resolve(setI18nLanguage(lang));
|
return Promise.resolve(setI18nLanguage(lang));
|
||||||
}
|
}
|
||||||
// If the language hasn't been loaded yet
|
// If the language hasn't been loaded yet
|
||||||
console.debug("loading language", lang);
|
|
||||||
const newMessages = await import(
|
const newMessages = await import(
|
||||||
`../i18n/${vueI18NfileForLanguage(lang)}.json`
|
`../i18n/${vueI18NfileForLanguage(lang)}.json`
|
||||||
);
|
);
|
||||||
@ -115,9 +97,7 @@ export async function loadLanguageAsync(lang: string): Promise<string> {
|
|||||||
return setI18nLanguage(lang);
|
return setI18nLanguage(lang);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.debug("loading async locale", locale);
|
|
||||||
loadLanguageAsync(locale);
|
loadLanguageAsync(locale);
|
||||||
console.debug("loaded async locale", locale);
|
|
||||||
|
|
||||||
export function formatList(list: string[]): string {
|
export function formatList(list: string[]): string {
|
||||||
if (window.Intl && Intl.ListFormat) {
|
if (window.Intl && Intl.ListFormat) {
|
||||||
|
17
js/src/utils/listFormat.ts
Normal file
17
js/src/utils/listFormat.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
const shortConjunctionFormatter = new Intl.ListFormat(undefined, {
|
||||||
|
style: "short",
|
||||||
|
type: "conjunction",
|
||||||
|
});
|
||||||
|
|
||||||
|
const shortDisjunctionFormatter = new Intl.ListFormat(undefined, {
|
||||||
|
style: "short",
|
||||||
|
type: "disjunction",
|
||||||
|
});
|
||||||
|
|
||||||
|
export const listShortConjunctionFormatter = (list: Array<string>): string => {
|
||||||
|
return shortConjunctionFormatter.format(list);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const listShortDisjunctionFormatter = (list: Array<string>): string => {
|
||||||
|
return shortDisjunctionFormatter.format(list);
|
||||||
|
};
|
@ -66,7 +66,7 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="flex gap-1 items-center" dir="auto">
|
<p class="flex gap-1 items-center" dir="auto">
|
||||||
<tag v-if="eventCategory" class="category">{{
|
<tag v-if="eventCategory" class="category" capitalize>{{
|
||||||
eventCategory
|
eventCategory
|
||||||
}}</tag>
|
}}</tag>
|
||||||
<router-link
|
<router-link
|
||||||
@ -77,22 +77,22 @@
|
|||||||
<tag>{{ tag.title }}</tag>
|
<tag>{{ tag.title }}</tag>
|
||||||
</router-link>
|
</router-link>
|
||||||
</p>
|
</p>
|
||||||
<b-tag variant="warning" size="is-medium" v-if="event?.draft"
|
<tag variant="warning" size="is-medium" v-if="event?.draft"
|
||||||
>{{ t("Draft") }}
|
>{{ t("Draft") }}
|
||||||
</b-tag>
|
</tag>
|
||||||
<span
|
<span
|
||||||
class="event-status"
|
class="event-status"
|
||||||
v-if="event?.status !== EventStatus.CONFIRMED"
|
v-if="event?.status !== EventStatus.CONFIRMED"
|
||||||
>
|
>
|
||||||
<b-tag
|
<tag
|
||||||
variant="warning"
|
variant="warning"
|
||||||
v-if="event?.status === EventStatus.TENTATIVE"
|
v-if="event?.status === EventStatus.TENTATIVE"
|
||||||
>{{ t("Event to be confirmed") }}</b-tag
|
>{{ t("Event to be confirmed") }}</tag
|
||||||
>
|
>
|
||||||
<b-tag
|
<tag
|
||||||
variant="danger"
|
variant="danger"
|
||||||
v-if="event?.status === EventStatus.CANCELLED"
|
v-if="event?.status === EventStatus.CANCELLED"
|
||||||
>{{ t("Event cancelled") }}</b-tag
|
>{{ t("Event cancelled") }}</tag
|
||||||
>
|
>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
@ -79,15 +79,13 @@
|
|||||||
<section
|
<section
|
||||||
class="py-4"
|
class="py-4"
|
||||||
v-if="
|
v-if="
|
||||||
showUpcoming &&
|
showUpcoming && monthlyFutureEvents && monthlyFutureEvents.size > 0
|
||||||
monthlyFutureEvents &&
|
|
||||||
monthlyFutureEvents.length > 0
|
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<transition-group name="list" tag="p">
|
<transition-group name="list" tag="p">
|
||||||
<div
|
<div
|
||||||
class="mb-5"
|
class="mb-5"
|
||||||
v-for="month of monthlyFutureEvents()"
|
v-for="month of monthlyFutureEvents"
|
||||||
:key="month[0]"
|
:key="month[0]"
|
||||||
>
|
>
|
||||||
<span class="upcoming-month">{{ month[0] }}</span>
|
<span class="upcoming-month">{{ month[0] }}</span>
|
||||||
@ -336,7 +334,7 @@ const monthlyEvents = (
|
|||||||
}, new Map());
|
}, new Map());
|
||||||
};
|
};
|
||||||
|
|
||||||
const monthlyFutureEvents = (): Map<string, Eventable[]> => {
|
const monthlyFutureEvents = computed((): Map<string, Eventable[]> => {
|
||||||
let eventable = [] as Eventable[];
|
let eventable = [] as Eventable[];
|
||||||
if (showAttending.value) {
|
if (showAttending.value) {
|
||||||
eventable = [...eventable, ...futureParticipations.value];
|
eventable = [...eventable, ...futureParticipations.value];
|
||||||
@ -345,7 +343,7 @@ const monthlyFutureEvents = (): Map<string, Eventable[]> => {
|
|||||||
eventable = [...eventable, ...groupEvents.value];
|
eventable = [...eventable, ...groupEvents.value];
|
||||||
}
|
}
|
||||||
return monthlyEvents(eventable);
|
return monthlyEvents(eventable);
|
||||||
};
|
});
|
||||||
|
|
||||||
const monthlyPastParticipations = computed((): Map<string, Eventable[]> => {
|
const monthlyPastParticipations = computed((): Map<string, Eventable[]> => {
|
||||||
return monthlyEvents(pastParticipations.value.elements, true);
|
return monthlyEvents(pastParticipations.value.elements, true);
|
||||||
|
@ -1,226 +1,171 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<!-- <o-loading v-model:active="$apollo.loading" /> -->
|
||||||
<!-- <o-loading v-model:active="$apollo.loading" /> -->
|
<!-- Nice looking SVGs -->
|
||||||
<section class="mt-5 sm:mt-24">
|
<section class="mt-5 sm:mt-24">
|
||||||
<div class="-z-10 overflow-hidden">
|
<div class="-z-10 overflow-hidden">
|
||||||
<img
|
<img
|
||||||
alt=""
|
alt=""
|
||||||
src="/img/shape-1.svg"
|
src="/img/shape-1.svg"
|
||||||
class="-z-10 absolute left-[2%] top-36"
|
class="-z-10 absolute left-[2%] top-36"
|
||||||
width="300"
|
width="300"
|
||||||
/>
|
/>
|
||||||
<img
|
<img
|
||||||
alt=""
|
alt=""
|
||||||
src="/img/shape-2.svg"
|
src="/img/shape-2.svg"
|
||||||
class="-z-10 absolute left-[50%] top-[5%] -translate-x-2/4 opacity-60"
|
class="-z-10 absolute left-[50%] top-[5%] -translate-x-2/4 opacity-60"
|
||||||
width="800"
|
width="800"
|
||||||
/>
|
/>
|
||||||
<img
|
<img
|
||||||
alt=""
|
alt=""
|
||||||
src="/img/shape-3.svg"
|
src="/img/shape-3.svg"
|
||||||
class="-z-10 absolute top-0 right-36"
|
class="-z-10 absolute top-0 right-36"
|
||||||
width="200"
|
width="200"
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<unlogged-introduction :config="config" v-if="config && !isLoggedIn" />
|
|
||||||
<search-fields v-model:search="search" v-model:location="location" />
|
|
||||||
<categories-preview />
|
|
||||||
<div
|
|
||||||
id="recent_events"
|
|
||||||
class="container mx-auto section"
|
|
||||||
v-if="config && (!currentUser || !currentActor)"
|
|
||||||
>
|
|
||||||
<section class="events-recent px-2">
|
|
||||||
<h2 class="title">
|
|
||||||
{{ $t("Last published events") }}
|
|
||||||
</h2>
|
|
||||||
<p>
|
|
||||||
<i18n-t
|
|
||||||
tag="span"
|
|
||||||
keypath="On {instance} and other federated instances"
|
|
||||||
>
|
|
||||||
<template #instance>
|
|
||||||
<b>{{ config.name }}</b>
|
|
||||||
</template>
|
|
||||||
</i18n-t>
|
|
||||||
</p>
|
|
||||||
<div v-if="events.total > 0">
|
|
||||||
<multi-card :events="events.elements.slice(0, 6)" />
|
|
||||||
<span
|
|
||||||
class="block mt-2 text-right underline text-slate-700 dark:text-slate-300"
|
|
||||||
>
|
|
||||||
<router-link
|
|
||||||
:to="{ name: RouteName.SEARCH }"
|
|
||||||
class="hover:text-slate-800 hover:dark:text-slate-400"
|
|
||||||
>{{ $t("View everything") }} >></router-link
|
|
||||||
>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<o-notification v-else variant="danger">{{
|
|
||||||
$t("No events found")
|
|
||||||
}}</o-notification>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
id="picture"
|
|
||||||
v-if="config && (!currentUser || !currentUser.isLoggedIn)"
|
|
||||||
>
|
|
||||||
<div class="container mx-auto">
|
|
||||||
<close-events @doGeoLoc="performGeoLocation()" />
|
|
||||||
</div>
|
|
||||||
<div class="picture-container">
|
|
||||||
<picture>
|
|
||||||
<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>
|
|
||||||
</div>
|
|
||||||
<presentation />
|
|
||||||
</div>
|
|
||||||
<div class="container mx-auto" v-if="config && loggedUserSettings">
|
|
||||||
<section
|
|
||||||
v-if="
|
|
||||||
currentActor && currentActor.id && (welcomeBack || newRegisteredUser)
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<o-notification variant="info" v-if="welcomeBack">{{
|
|
||||||
$t("Welcome back {username}!", {
|
|
||||||
username: currentActor.displayName(),
|
|
||||||
})
|
|
||||||
}}</o-notification>
|
|
||||||
<o-notification variant="info" v-if="newRegisteredUser">{{
|
|
||||||
$t("Welcome to Mobilizon, {username}!", {
|
|
||||||
username: currentActor.displayName(),
|
|
||||||
})
|
|
||||||
}}</o-notification>
|
|
||||||
</section>
|
|
||||||
<!-- Your upcoming events -->
|
|
||||||
<section v-if="canShowMyUpcomingEvents">
|
|
||||||
<h2 class="dark:text-white text-2xl font-bold">
|
|
||||||
{{ $t("Your upcoming events") }}
|
|
||||||
</h2>
|
|
||||||
<div
|
|
||||||
v-for="row of goingToEvents"
|
|
||||||
class="text-slate-700 dark:text-slate-300"
|
|
||||||
:key="row[0]"
|
|
||||||
>
|
|
||||||
<p
|
|
||||||
class="date-component-container"
|
|
||||||
v-if="isInLessThanSevenDays(row[0])"
|
|
||||||
>
|
|
||||||
<span v-if="isToday(row[0])">{{
|
|
||||||
$t(
|
|
||||||
"You have one event today.",
|
|
||||||
{
|
|
||||||
count: row[1].size,
|
|
||||||
},
|
|
||||||
row[1].size
|
|
||||||
)
|
|
||||||
}}</span>
|
|
||||||
<span v-else-if="isTomorrow(row[0])">{{
|
|
||||||
$t(
|
|
||||||
"You have one event tomorrow.",
|
|
||||||
{
|
|
||||||
count: row[1].size,
|
|
||||||
},
|
|
||||||
row[1].size
|
|
||||||
)
|
|
||||||
}}</span>
|
|
||||||
<span v-else-if="isInLessThanSevenDays(row[0])">
|
|
||||||
{{
|
|
||||||
$t(
|
|
||||||
"You have one event in {days} days.",
|
|
||||||
{
|
|
||||||
count: row[1].size,
|
|
||||||
days: calculateDiffDays(row[0]),
|
|
||||||
},
|
|
||||||
row[1].size
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
<div>
|
|
||||||
<event-participation-card
|
|
||||||
v-for="participation in thisWeek(row)"
|
|
||||||
:key="participation[1].id"
|
|
||||||
:participation="participation[1]"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<span
|
|
||||||
class="block mt-2 text-right underline text-slate-700 dark:text-slate-300"
|
|
||||||
>
|
|
||||||
<router-link
|
|
||||||
:to="{ name: RouteName.MY_EVENTS }"
|
|
||||||
class="hover:text-slate-800 hover:dark:text-slate-400"
|
|
||||||
>{{ $t("View everything") }} >></router-link
|
|
||||||
>
|
|
||||||
</span>
|
|
||||||
</section>
|
|
||||||
<hr
|
|
||||||
role="presentation"
|
|
||||||
class="home-separator"
|
|
||||||
v-if="canShowMyUpcomingEvents && canShowFollowedGroupEvents"
|
|
||||||
/>
|
/>
|
||||||
<!-- Events from your followed groups -->
|
|
||||||
<section class="followActivity" v-if="canShowFollowedGroupEvents">
|
|
||||||
<h2 class="title">
|
|
||||||
{{ $t("Upcoming events from your groups") }}
|
|
||||||
</h2>
|
|
||||||
<p>{{ $t("That you follow or of which you are a member") }}</p>
|
|
||||||
<multi-card :events="filteredFollowedGroupsEvents" />
|
|
||||||
<span
|
|
||||||
class="block mt-2 text-right underline text-slate-700 dark:text-slate-300"
|
|
||||||
>
|
|
||||||
<router-link
|
|
||||||
class="hover:text-slate-800 hover:dark:text-slate-400"
|
|
||||||
:to="{
|
|
||||||
name: RouteName.MY_EVENTS,
|
|
||||||
query: {
|
|
||||||
showUpcoming: 'true',
|
|
||||||
showDrafts: 'false',
|
|
||||||
showAttending: 'false',
|
|
||||||
showMyGroups: 'true',
|
|
||||||
},
|
|
||||||
}"
|
|
||||||
>{{ $t("View everything") }} >></router-link
|
|
||||||
>
|
|
||||||
</span>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<CloseEvents @doGeoLoc="performGeoLocation()" />
|
|
||||||
<LastEvents :instanceName="config.name" class="mb-10" />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
|
<!-- Unlogged introduction -->
|
||||||
|
<unlogged-introduction :config="config" v-if="config && !isLoggedIn" />
|
||||||
|
<!-- Search fields -->
|
||||||
|
<search-fields v-model:search="search" v-model:location="location" />
|
||||||
|
<!-- Categories preview -->
|
||||||
|
<categories-preview />
|
||||||
|
<!-- Welcome back -->
|
||||||
|
<section v-if="currentActor?.id && (welcomeBack || newRegisteredUser)">
|
||||||
|
<o-notification variant="info" v-if="welcomeBack">{{
|
||||||
|
$t("Welcome back {username}!", {
|
||||||
|
username: displayName(currentActor),
|
||||||
|
})
|
||||||
|
}}</o-notification>
|
||||||
|
<o-notification variant="info" v-if="newRegisteredUser">{{
|
||||||
|
$t("Welcome to Mobilizon, {username}!", {
|
||||||
|
username: displayName(currentActor),
|
||||||
|
})
|
||||||
|
}}</o-notification>
|
||||||
|
</section>
|
||||||
|
<!-- Your upcoming events -->
|
||||||
|
<section v-if="canShowMyUpcomingEvents" class="container mx-auto">
|
||||||
|
<h2 class="dark:text-white font-bold">
|
||||||
|
{{ $t("Your upcoming events") }}
|
||||||
|
</h2>
|
||||||
|
<div
|
||||||
|
v-for="row of goingToEvents"
|
||||||
|
class="text-slate-700 dark:text-slate-300"
|
||||||
|
:key="row[0]"
|
||||||
|
>
|
||||||
|
<p class="date-component-container" v-if="isInLessThanSevenDays(row[0])">
|
||||||
|
<span v-if="isToday(row[0])">{{
|
||||||
|
$t(
|
||||||
|
"You have one event today.",
|
||||||
|
{
|
||||||
|
count: row[1].size,
|
||||||
|
},
|
||||||
|
row[1].size
|
||||||
|
)
|
||||||
|
}}</span>
|
||||||
|
<span v-else-if="isTomorrow(row[0])">{{
|
||||||
|
$t(
|
||||||
|
"You have one event tomorrow.",
|
||||||
|
{
|
||||||
|
count: row[1].size,
|
||||||
|
},
|
||||||
|
row[1].size
|
||||||
|
)
|
||||||
|
}}</span>
|
||||||
|
<span v-else-if="isInLessThanSevenDays(row[0])">
|
||||||
|
{{
|
||||||
|
$t(
|
||||||
|
"You have one event in {days} days.",
|
||||||
|
{
|
||||||
|
count: row[1].size,
|
||||||
|
days: calculateDiffDays(row[0]),
|
||||||
|
},
|
||||||
|
row[1].size
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<div>
|
||||||
|
<event-participation-card
|
||||||
|
v-for="participation in thisWeek(row)"
|
||||||
|
:key="participation[1].id"
|
||||||
|
:participation="participation[1]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
class="block mt-2 text-right underline text-slate-700 dark:text-slate-300"
|
||||||
|
>
|
||||||
|
<router-link
|
||||||
|
:to="{ name: RouteName.MY_EVENTS }"
|
||||||
|
class="hover:text-slate-800 hover:dark:text-slate-400"
|
||||||
|
>{{ $t("View everything") }} >></router-link
|
||||||
|
>
|
||||||
|
</span>
|
||||||
|
</section>
|
||||||
|
<!-- Events from your followed groups -->
|
||||||
|
<section class="followActivity" v-if="canShowFollowedGroupEvents">
|
||||||
|
<h2 class="title">
|
||||||
|
{{ $t("Upcoming events from your groups") }}
|
||||||
|
</h2>
|
||||||
|
<p>{{ $t("That you follow or of which you are a member") }}</p>
|
||||||
|
<multi-card :events="filteredFollowedGroupsEvents" />
|
||||||
|
<span
|
||||||
|
class="block mt-2 text-right underline text-slate-700 dark:text-slate-300"
|
||||||
|
>
|
||||||
|
<router-link
|
||||||
|
class="hover:text-slate-800 hover:dark:text-slate-400"
|
||||||
|
:to="{
|
||||||
|
name: RouteName.MY_EVENTS,
|
||||||
|
query: {
|
||||||
|
showUpcoming: 'true',
|
||||||
|
showDrafts: 'false',
|
||||||
|
showAttending: 'false',
|
||||||
|
showMyGroups: 'true',
|
||||||
|
},
|
||||||
|
}"
|
||||||
|
>{{ $t("View everything") }} >></router-link
|
||||||
|
>
|
||||||
|
</span>
|
||||||
|
</section>
|
||||||
|
<!-- Recent events -->
|
||||||
|
<CloseEvents @doGeoLoc="performGeoLocation()" :userLocation="userLocation" />
|
||||||
|
<CloseGroups :userLocation="userLocation" @doGeoLoc="performGeoLocation()" />
|
||||||
|
<LastEvents v-if="config" :instanceName="config.name" />
|
||||||
|
<!-- 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>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
@ -231,8 +176,7 @@ import { FETCH_EVENTS } from "../graphql/event";
|
|||||||
import EventParticipationCard from "../components/Event/EventParticipationCard.vue";
|
import EventParticipationCard from "../components/Event/EventParticipationCard.vue";
|
||||||
import MultiCard from "../components/Event/MultiCard.vue";
|
import MultiCard from "../components/Event/MultiCard.vue";
|
||||||
import { CURRENT_ACTOR_CLIENT } from "../graphql/actor";
|
import { CURRENT_ACTOR_CLIENT } from "../graphql/actor";
|
||||||
import { IPerson, Person } from "../types/actor";
|
import { IPerson, displayName } from "../types/actor";
|
||||||
import ngeohash from "ngeohash";
|
|
||||||
import {
|
import {
|
||||||
ICurrentUser,
|
ICurrentUser,
|
||||||
IUser,
|
IUser,
|
||||||
@ -246,6 +190,7 @@ import { CONFIG } from "../graphql/config";
|
|||||||
import { IConfig } from "../types/config.model";
|
import { IConfig } from "../types/config.model";
|
||||||
// import { IFollowedGroupEvent } from "../types/followedGroupEvent.model";
|
// import { IFollowedGroupEvent } from "../types/followedGroupEvent.model";
|
||||||
import CloseEvents from "@/components/Local/CloseEvents.vue";
|
import CloseEvents from "@/components/Local/CloseEvents.vue";
|
||||||
|
import CloseGroups from "@/components/Local/CloseGroups.vue";
|
||||||
import LastEvents from "@/components/Local/LastEvents.vue";
|
import LastEvents from "@/components/Local/LastEvents.vue";
|
||||||
import { computed, onMounted, reactive, watch } from "vue";
|
import { computed, onMounted, reactive, watch } from "vue";
|
||||||
import { useMutation, useQuery } from "@vue/apollo-composable";
|
import { useMutation, useQuery } from "@vue/apollo-composable";
|
||||||
@ -262,7 +207,7 @@ import CategoriesPreview from "@/components/Home/CategoriesPreview.vue";
|
|||||||
import UnloggedIntroduction from "@/components/Home/UnloggedIntroduction.vue";
|
import UnloggedIntroduction from "@/components/Home/UnloggedIntroduction.vue";
|
||||||
import SearchFields from "@/components/Home/SearchFields.vue";
|
import SearchFields from "@/components/Home/SearchFields.vue";
|
||||||
import { useHead } from "@vueuse/head";
|
import { useHead } from "@vueuse/head";
|
||||||
import InformationOutline from "vue-material-design-icons/InformationOutline.vue";
|
import { geoHashToCoords } from "@/utils/location";
|
||||||
|
|
||||||
const { result: resultEvents } = useQuery<{ events: Paginate<IEvent> }>(
|
const { result: resultEvents } = useQuery<{ events: Paginate<IEvent> }>(
|
||||||
FETCH_EVENTS,
|
FETCH_EVENTS,
|
||||||
@ -278,8 +223,8 @@ const events = computed(
|
|||||||
const { result: currentActorResult } = useQuery<{ currentActor: IPerson }>(
|
const { result: currentActorResult } = useQuery<{ currentActor: IPerson }>(
|
||||||
CURRENT_ACTOR_CLIENT
|
CURRENT_ACTOR_CLIENT
|
||||||
);
|
);
|
||||||
const currentActor = computed<Person | undefined>(
|
const currentActor = computed<IPerson | undefined>(
|
||||||
() => new Person(currentActorResult.value?.currentActor)
|
() => currentActorResult.value?.currentActor
|
||||||
);
|
);
|
||||||
|
|
||||||
const { result: currentUserResult } = useQuery<{
|
const { result: currentUserResult } = useQuery<{
|
||||||
@ -305,112 +250,6 @@ const followedGroupEvents = computed(
|
|||||||
() => userResult.value?.loggedUser?.followedGroupEvents
|
() => userResult.value?.loggedUser?.followedGroupEvents
|
||||||
);
|
);
|
||||||
|
|
||||||
const userSettingsLocation = computed(
|
|
||||||
() => loggedUser.value?.settings?.location
|
|
||||||
);
|
|
||||||
|
|
||||||
const { result: currentUserLocationResult } = useQuery<{
|
|
||||||
currentUserLocation: LocationType;
|
|
||||||
}>(CURRENT_USER_LOCATION_CLIENT);
|
|
||||||
|
|
||||||
const currentUserLocation = computed(() => {
|
|
||||||
return currentUserLocationResult.value?.currentUserLocation;
|
|
||||||
});
|
|
||||||
|
|
||||||
const { mutate: saveCurrentUserLocation } = useMutation<any, LocationType>(
|
|
||||||
UPDATE_CURRENT_USER_LOCATION_CLIENT
|
|
||||||
);
|
|
||||||
|
|
||||||
const reverseGeoCodeInformation = reactive<{
|
|
||||||
latitude: number | undefined;
|
|
||||||
longitude: number | undefined;
|
|
||||||
accuracy: number | undefined;
|
|
||||||
}>({
|
|
||||||
latitude: undefined,
|
|
||||||
longitude: undefined,
|
|
||||||
accuracy: undefined,
|
|
||||||
});
|
|
||||||
|
|
||||||
const { onResult: onReverseGeocodeResult } = useQuery<{
|
|
||||||
reverseGeocode: IAddress[];
|
|
||||||
}>(REVERSE_GEOCODE, reverseGeoCodeInformation, () => ({
|
|
||||||
enabled: reverseGeoCodeInformation.latitude !== undefined,
|
|
||||||
}));
|
|
||||||
|
|
||||||
onReverseGeocodeResult((result) => {
|
|
||||||
if (!result?.data) return;
|
|
||||||
const geoLocationInformation = result?.data?.reverseGeocode[0];
|
|
||||||
console.debug("geoLocationInformation", result.data);
|
|
||||||
const placeName =
|
|
||||||
geoLocationInformation.locality ??
|
|
||||||
geoLocationInformation.region ??
|
|
||||||
geoLocationInformation.country;
|
|
||||||
console.log("place name", placeName);
|
|
||||||
|
|
||||||
saveCurrentUserLocation({
|
|
||||||
lat: reverseGeoCodeInformation.latitude,
|
|
||||||
lon: reverseGeoCodeInformation.longitude,
|
|
||||||
accuracy: Math.round(reverseGeoCodeInformation.accuracy ?? 12) / 1000,
|
|
||||||
isIPLocation: false,
|
|
||||||
name: placeName,
|
|
||||||
picture: geoLocationInformation.pictureInfo,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const fetchAndSaveCurrentLocationName = async ({
|
|
||||||
coords: { latitude, longitude, accuracy },
|
|
||||||
}: GeolocationPosition) => {
|
|
||||||
console.debug(
|
|
||||||
"data found from navigator geocoding",
|
|
||||||
latitude,
|
|
||||||
longitude,
|
|
||||||
accuracy
|
|
||||||
);
|
|
||||||
reverseGeoCodeInformation.latitude = latitude;
|
|
||||||
reverseGeoCodeInformation.longitude = longitude;
|
|
||||||
reverseGeoCodeInformation.accuracy = accuracy;
|
|
||||||
};
|
|
||||||
|
|
||||||
const performGeoLocation = () => {
|
|
||||||
navigator.geolocation.getCurrentPosition(fetchAndSaveCurrentLocationName);
|
|
||||||
};
|
|
||||||
|
|
||||||
const GEOHASH_DEPTH = 9; // put enough accuracy, radius will be used anyway
|
|
||||||
|
|
||||||
const geohash = computed(
|
|
||||||
() => userSettingsLocation.value?.geohash ?? currentUserLocationGeoHash
|
|
||||||
);
|
|
||||||
|
|
||||||
const currentUserLocationGeoHash = computed(() => {
|
|
||||||
if (currentUserLocation.value?.lat && currentUserLocation.value?.lon) {
|
|
||||||
return ngeohash.encode(
|
|
||||||
currentUserLocation.value?.lat,
|
|
||||||
currentUserLocation.value?.lon,
|
|
||||||
GEOHASH_DEPTH
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
});
|
|
||||||
|
|
||||||
const radius = computed(
|
|
||||||
() => userSettingsLocation.value?.range ?? currentUserLocation.value?.accuracy
|
|
||||||
);
|
|
||||||
const locationName = computed(
|
|
||||||
() => userSettingsLocation.value?.name ?? currentUserLocation.value?.name
|
|
||||||
);
|
|
||||||
|
|
||||||
// const { result: closeEventsResult } = useQuery<{
|
|
||||||
// searchEvents: Paginate<IEvent>;
|
|
||||||
// }>(
|
|
||||||
// CLOSE_CONTENT,
|
|
||||||
// { location: geohash, radius },
|
|
||||||
// { enabled: geohash.value !== undefined }
|
|
||||||
// );
|
|
||||||
|
|
||||||
// const closeEvents = computed(
|
|
||||||
// () => closeEventsResult.value?.searchEvents || { total: 0, elements: [] }
|
|
||||||
// );
|
|
||||||
|
|
||||||
const currentUserParticipations = computed(
|
const currentUserParticipations = computed(
|
||||||
() => loggedUser.value?.participations.elements
|
() => loggedUser.value?.participations.elements
|
||||||
);
|
);
|
||||||
@ -499,16 +338,6 @@ const goingToEvents = computed<Map<string, Map<string, IParticipant>>>(() => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// const eventDeleted = (eventid: string): void => {
|
|
||||||
// currentUserParticipations = currentUserParticipations?.filter(
|
|
||||||
// (participation) => participation.event.id !== eventid
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
|
|
||||||
// viewEvent(event: IEvent): void {
|
|
||||||
// this.$router.push({ name: RouteName.EVENT, params: { uuid: event.uuid } });
|
|
||||||
// }
|
|
||||||
|
|
||||||
const loggedUserSettings = computed<IUserSettings | undefined>(() => {
|
const loggedUserSettings = computed<IUserSettings | undefined>(() => {
|
||||||
return loggedUser.value?.settings;
|
return loggedUser.value?.settings;
|
||||||
});
|
});
|
||||||
@ -517,14 +346,6 @@ const canShowMyUpcomingEvents = computed<boolean>(() => {
|
|||||||
return currentActor.value?.id != undefined && goingToEvents.value.size > 0;
|
return currentActor.value?.id != undefined && goingToEvents.value.size > 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
// const canShowCloseEvents = computed<boolean>(() => {
|
|
||||||
// return (
|
|
||||||
// loggedUser.value?.settings?.location != undefined &&
|
|
||||||
// closeEvents.value != undefined &&
|
|
||||||
// closeEvents.value?.total > 0
|
|
||||||
// );
|
|
||||||
// });
|
|
||||||
|
|
||||||
const canShowFollowedGroupEvents = computed<boolean>(() => {
|
const canShowFollowedGroupEvents = computed<boolean>(() => {
|
||||||
return filteredFollowedGroupsEvents.value.length > 0;
|
return filteredFollowedGroupsEvents.value.length > 0;
|
||||||
});
|
});
|
||||||
@ -562,18 +383,134 @@ watch(loggedUser, (loggedUserValue) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// metaInfo() {
|
|
||||||
// return {
|
|
||||||
// // eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// // @ts-ignore
|
|
||||||
// title: this.instanceName,
|
|
||||||
// titleTemplate: "%s | Mobilizon",
|
|
||||||
// };
|
|
||||||
// },
|
|
||||||
|
|
||||||
const isLoggedIn = computed(() => loggedUser.value?.id !== undefined);
|
const isLoggedIn = computed(() => loggedUser.value?.id !== undefined);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Geolocation stuff
|
||||||
|
*/
|
||||||
|
|
||||||
|
// The location hash saved in the user settings (should be the default)
|
||||||
|
const userSettingsLocationGeoHash = computed(
|
||||||
|
() => loggedUser.value?.settings?.location?.geohash
|
||||||
|
);
|
||||||
|
|
||||||
|
// The location provided by the server
|
||||||
|
const serverLocation = computed(() => config.value?.location);
|
||||||
|
|
||||||
|
// The coords from the user location or the server provided location
|
||||||
|
const coords = computed(() => {
|
||||||
|
const userSettingsCoords = geoHashToCoords(
|
||||||
|
userSettingsLocationGeoHash.value ?? undefined
|
||||||
|
);
|
||||||
|
|
||||||
|
if (userSettingsCoords) {
|
||||||
|
return { ...userSettingsCoords, isIPLocation: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ...serverLocation.value, isIPLocation: true };
|
||||||
|
});
|
||||||
|
|
||||||
|
const { result: reverseGeocodeResult } = useQuery<{
|
||||||
|
reverseGeocode: IAddress[];
|
||||||
|
}>(REVERSE_GEOCODE, coords, () => ({
|
||||||
|
enabled: coords.value?.longitude != undefined,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const userSettingsLocation = computed(() => {
|
||||||
|
const address = reverseGeocodeResult.value?.reverseGeocode[0];
|
||||||
|
const placeName = address?.locality ?? address?.region ?? address?.country;
|
||||||
|
return {
|
||||||
|
lat: coords.value?.latitude,
|
||||||
|
lon: coords.value?.longitude,
|
||||||
|
name: placeName,
|
||||||
|
picture: address?.pictureInfo,
|
||||||
|
isIPLocation: coords.value?.isIPLocation,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const { result: currentUserLocationResult } = useQuery<{
|
||||||
|
currentUserLocation: LocationType;
|
||||||
|
}>(CURRENT_USER_LOCATION_CLIENT);
|
||||||
|
|
||||||
|
// The user's location currently in the Apollo cache
|
||||||
|
const currentUserLocation = computed(() => {
|
||||||
|
return {
|
||||||
|
...(currentUserLocationResult.value?.currentUserLocation ?? {
|
||||||
|
lat: undefined,
|
||||||
|
lon: undefined,
|
||||||
|
accuracy: undefined,
|
||||||
|
isIPLocation: undefined,
|
||||||
|
name: undefined,
|
||||||
|
picture: undefined,
|
||||||
|
}),
|
||||||
|
isIPLocation: false,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const userLocation = computed(() => {
|
||||||
|
if (
|
||||||
|
!userSettingsLocation.value ||
|
||||||
|
(userSettingsLocation.value?.isIPLocation &&
|
||||||
|
currentUserLocation.value?.name)
|
||||||
|
) {
|
||||||
|
return currentUserLocation.value;
|
||||||
|
}
|
||||||
|
return userSettingsLocation.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
const { mutate: saveCurrentUserLocation } = useMutation<any, LocationType>(
|
||||||
|
UPDATE_CURRENT_USER_LOCATION_CLIENT
|
||||||
|
);
|
||||||
|
|
||||||
|
const reverseGeoCodeInformation = reactive<{
|
||||||
|
latitude: number | undefined;
|
||||||
|
longitude: number | undefined;
|
||||||
|
accuracy: number | undefined;
|
||||||
|
}>({
|
||||||
|
latitude: undefined,
|
||||||
|
longitude: undefined,
|
||||||
|
accuracy: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { onResult: onReverseGeocodeResult } = useQuery<{
|
||||||
|
reverseGeocode: IAddress[];
|
||||||
|
}>(REVERSE_GEOCODE, reverseGeoCodeInformation, () => ({
|
||||||
|
enabled: reverseGeoCodeInformation.latitude !== undefined,
|
||||||
|
}));
|
||||||
|
|
||||||
|
onReverseGeocodeResult((result) => {
|
||||||
|
if (!result?.data) return;
|
||||||
|
const geoLocationInformation = result?.data?.reverseGeocode[0];
|
||||||
|
const placeName =
|
||||||
|
geoLocationInformation.locality ??
|
||||||
|
geoLocationInformation.region ??
|
||||||
|
geoLocationInformation.country;
|
||||||
|
|
||||||
|
saveCurrentUserLocation({
|
||||||
|
lat: reverseGeoCodeInformation.latitude,
|
||||||
|
lon: reverseGeoCodeInformation.longitude,
|
||||||
|
accuracy: Math.round(reverseGeoCodeInformation.accuracy ?? 12) / 1000,
|
||||||
|
isIPLocation: false,
|
||||||
|
name: placeName,
|
||||||
|
picture: geoLocationInformation.pictureInfo,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const fetchAndSaveCurrentLocationName = async ({
|
||||||
|
coords: { latitude, longitude, accuracy },
|
||||||
|
}: GeolocationPosition) => {
|
||||||
|
reverseGeoCodeInformation.latitude = latitude;
|
||||||
|
reverseGeoCodeInformation.longitude = longitude;
|
||||||
|
reverseGeoCodeInformation.accuracy = accuracy;
|
||||||
|
};
|
||||||
|
|
||||||
|
const performGeoLocation = () => {
|
||||||
|
navigator.geolocation.getCurrentPosition(fetchAndSaveCurrentLocationName);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* View Head
|
||||||
|
*/
|
||||||
useHead({
|
useHead({
|
||||||
title: computed(() => instanceName.value ?? ""),
|
title: computed(() => instanceName.value ?? ""),
|
||||||
});
|
});
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -12,7 +12,6 @@ module.exports = {
|
|||||||
"mbz-yellow": "#ffd599",
|
"mbz-yellow": "#ffd599",
|
||||||
"mbz-purple": "#424056",
|
"mbz-purple": "#424056",
|
||||||
"mbz-bluegreen": "#1e7d97",
|
"mbz-bluegreen": "#1e7d97",
|
||||||
"mbz-yellow-2": "rgb(var(--color-mbz-yellow-2) / <alpha-value>)",
|
|
||||||
"violet-1": "#3a384c",
|
"violet-1": "#3a384c",
|
||||||
"violet-2": "#474467",
|
"violet-2": "#474467",
|
||||||
"violet-3": "#3c376e",
|
"violet-3": "#3c376e",
|
||||||
|
@ -12,7 +12,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Config do
|
|||||||
"""
|
"""
|
||||||
@spec get_config(any(), map(), Absinthe.Resolution.t()) :: {:ok, map()}
|
@spec get_config(any(), map(), Absinthe.Resolution.t()) :: {:ok, map()}
|
||||||
def get_config(_parent, _params, %{context: %{ip: ip}}) do
|
def get_config(_parent, _params, %{context: %{ip: ip}}) do
|
||||||
# ip = "2a01:e0a:184:2000:1112:e19d:9779:88c8"
|
ip = "2a01:e0a:184:2000:1112:e19d:9779:88c8"
|
||||||
geolix = Geolix.lookup(ip)
|
geolix = Geolix.lookup(ip)
|
||||||
|
|
||||||
country_code =
|
country_code =
|
||||||
|
@ -95,6 +95,14 @@ defmodule Mobilizon.GraphQL.Schema.SearchType do
|
|||||||
arg(:type, :event_type, description: "Whether the event is online or in person")
|
arg(:type, :event_type, description: "Whether the event is online or in person")
|
||||||
arg(:category, :string, description: "The category for the event")
|
arg(:category, :string, description: "The category for the event")
|
||||||
|
|
||||||
|
arg(:category_one_of, list_of(:string),
|
||||||
|
description: "The list of categories the event can be in"
|
||||||
|
)
|
||||||
|
|
||||||
|
arg(:status_one_of, list_of(:event_status),
|
||||||
|
description: "The list of statuses this event can have"
|
||||||
|
)
|
||||||
|
|
||||||
arg(:radius, :float,
|
arg(:radius, :float,
|
||||||
default_value: 50,
|
default_value: 50,
|
||||||
description: "Radius around the location to search in"
|
description: "Radius around the location to search in"
|
||||||
|
@ -530,6 +530,8 @@ defmodule Mobilizon.Events do
|
|||||||
|> events_for_begins_on(args)
|
|> events_for_begins_on(args)
|
||||||
|> events_for_ends_on(args)
|
|> events_for_ends_on(args)
|
||||||
|> events_for_category(args)
|
|> events_for_category(args)
|
||||||
|
|> events_for_categories(args)
|
||||||
|
|> events_for_statuses(args)
|
||||||
|> events_for_tags(args)
|
|> events_for_tags(args)
|
||||||
|> events_for_location(args)
|
|> events_for_location(args)
|
||||||
|> filter_online(args)
|
|> filter_online(args)
|
||||||
@ -1320,6 +1322,19 @@ defmodule Mobilizon.Events do
|
|||||||
|
|
||||||
defp events_for_category(query, _args), do: query
|
defp events_for_category(query, _args), do: query
|
||||||
|
|
||||||
|
@spec events_for_categories(Ecto.Queryable.t(), map()) :: Ecto.Query.t()
|
||||||
|
defp events_for_categories(query, %{category_one_of: category_one_of}) when length(category_one_of) > 0 do
|
||||||
|
where(query, [q], q.category in ^category_one_of)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp events_for_categories(query, _args), do: query
|
||||||
|
|
||||||
|
defp events_for_statuses(query, %{status_one_of: status_one_of}) when length(status_one_of) > 0 do
|
||||||
|
where(query, [q], q.status in ^status_one_of)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp events_for_statuses(query, _args), do: query
|
||||||
|
|
||||||
@spec events_for_tags(Ecto.Queryable.t(), map()) :: Ecto.Query.t()
|
@spec events_for_tags(Ecto.Queryable.t(), map()) :: Ecto.Query.t()
|
||||||
defp events_for_tags(query, %{tags: tags}) when is_valid_string(tags) do
|
defp events_for_tags(query, %{tags: tags}) when is_valid_string(tags) do
|
||||||
query
|
query
|
||||||
|
Loading…
Reference in New Issue
Block a user