@@ -3,7 +3,7 @@
|
||||
:to="{
|
||||
name: 'SEARCH',
|
||||
query: {
|
||||
eventCategory: category.key,
|
||||
categoryOneOf: [category.key],
|
||||
contentType: 'EVENTS',
|
||||
radius: undefined,
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<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 } }"
|
||||
>
|
||||
<div class="bg-secondary rounded-lg">
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
<template>
|
||||
<article class="bg-white dark:bg-mbz-purple mb-5 mt-4 p-0">
|
||||
<div
|
||||
class="bg-mbz-yellow-2 flex p-2 text-violet-title rounded-t-lg"
|
||||
dir="auto"
|
||||
>
|
||||
<article class="bg-white dark:bg-mbz-purple mb-5 mt-4 pb-2 md:p-0">
|
||||
<div class="bg-yellow-2 flex p-2 text-violet-title rounded-t-lg" dir="auto">
|
||||
<figure
|
||||
class="image is-24x24 ltr:pr-1 rtl:pl-1"
|
||||
v-if="participation.actor.avatar"
|
||||
@@ -21,7 +18,7 @@
|
||||
</div>
|
||||
<div class="list-card flex flex-col relative">
|
||||
<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="h-36 relative w-full">
|
||||
@@ -52,24 +49,24 @@
|
||||
</router-link>
|
||||
</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">
|
||||
<b-tag
|
||||
<Tag
|
||||
variant="info"
|
||||
class="mr-1 mb-1"
|
||||
size="is-medium"
|
||||
size="medium"
|
||||
v-if="participation.event.status === EventStatus.TENTATIVE"
|
||||
>
|
||||
{{ $t("Tentative") }}
|
||||
</b-tag>
|
||||
<b-tag
|
||||
{{ t("Tentative") }}
|
||||
</Tag>
|
||||
<Tag
|
||||
variant="danger"
|
||||
class="mr-1 mb-1"
|
||||
size="is-medium"
|
||||
size="medium"
|
||||
v-if="participation.event.status === EventStatus.CANCELLED"
|
||||
>
|
||||
{{ $t("Cancelled") }}
|
||||
</b-tag>
|
||||
{{ t("Cancelled") }}
|
||||
</Tag>
|
||||
<router-link
|
||||
:to="{
|
||||
name: RouteName.EVENT,
|
||||
@@ -96,7 +93,7 @@
|
||||
"
|
||||
>
|
||||
<Video />
|
||||
<span>{{ $t("Online") }}</span>
|
||||
<span>{{ t("Online") }}</span>
|
||||
</div>
|
||||
<div class="flex gap-1">
|
||||
<figure class="" v-if="actorAvatarURL">
|
||||
@@ -122,7 +119,7 @@
|
||||
<!-- Less than 10 seats left -->
|
||||
<span class="has-text-danger" v-if="lastSeatsLeft">
|
||||
{{
|
||||
$t("{number} seats left", {
|
||||
t("{number} seats left", {
|
||||
number: seatsLeft,
|
||||
})
|
||||
}}
|
||||
@@ -133,7 +130,7 @@
|
||||
"
|
||||
>
|
||||
{{
|
||||
$t(
|
||||
t(
|
||||
"{available}/{capacity} available places",
|
||||
{
|
||||
available:
|
||||
@@ -149,7 +146,7 @@
|
||||
</span>
|
||||
<span v-else>
|
||||
{{
|
||||
$t(
|
||||
t(
|
||||
"{count} participants",
|
||||
{
|
||||
count: participation.event.participantStats.participant,
|
||||
@@ -170,7 +167,7 @@
|
||||
"
|
||||
>
|
||||
{{
|
||||
$t(
|
||||
t(
|
||||
"{count} requests waiting",
|
||||
{
|
||||
count: participation.event.participantStats.notApproved,
|
||||
@@ -183,10 +180,13 @@
|
||||
</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>
|
||||
<o-button icon-right="dots-horizontal">
|
||||
{{ $t("Actions") }}
|
||||
{{ t("Actions") }}
|
||||
</o-button>
|
||||
</template>
|
||||
<o-dropdown-item
|
||||
@@ -208,7 +208,7 @@
|
||||
"
|
||||
>
|
||||
<Pencil />
|
||||
{{ $t("Edit") }}
|
||||
{{ t("Edit") }}
|
||||
</div>
|
||||
</o-dropdown-item>
|
||||
|
||||
@@ -226,7 +226,7 @@
|
||||
"
|
||||
>
|
||||
<ContentDuplicate />
|
||||
{{ $t("Duplicate") }}
|
||||
{{ t("Duplicate") }}
|
||||
</div>
|
||||
</o-dropdown-item>
|
||||
|
||||
@@ -241,7 +241,7 @@
|
||||
>
|
||||
<div @click="openDeleteEventModalWrapper" class="flex gap-1">
|
||||
<Delete />
|
||||
{{ $t("Delete") }}
|
||||
{{ t("Delete") }}
|
||||
</div>
|
||||
</o-dropdown-item>
|
||||
|
||||
@@ -264,7 +264,7 @@
|
||||
"
|
||||
>
|
||||
<AccountMultiplePlus />
|
||||
{{ $t("Manage participations") }}
|
||||
{{ t("Manage participations") }}
|
||||
</div>
|
||||
</o-dropdown-item>
|
||||
|
||||
@@ -277,7 +277,7 @@
|
||||
}"
|
||||
>
|
||||
<ViewCompact />
|
||||
{{ $t("View event page") }}
|
||||
{{ t("View event page") }}
|
||||
</router-link>
|
||||
</o-dropdown-item>
|
||||
</o-dropdown>
|
||||
@@ -288,12 +288,11 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
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 {
|
||||
IEvent,
|
||||
IEventCardOptions,
|
||||
organizer,
|
||||
organizerAvatarUrl,
|
||||
organizerDisplayName,
|
||||
} from "@/types/event.model";
|
||||
@@ -319,6 +318,7 @@ import { useI18n } from "vue-i18n";
|
||||
import { Dialog } from "@/plugins/dialog";
|
||||
import { Snackbar } from "@/plugins/snackbar";
|
||||
import { useDeleteEvent } from "@/composition/apollo/event";
|
||||
import Tag from "@/components/Tag.vue";
|
||||
|
||||
const defaultOptions: IEventCardOptions = {
|
||||
hideDate: true,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<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
|
||||
class="flex flex-col h-full"
|
||||
|
||||
@@ -4,35 +4,49 @@
|
||||
name: RouteName.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" />
|
||||
</figure>
|
||||
<div class="py-2 pl-2">
|
||||
<div class="flex gap-1 mb-2">
|
||||
<div class="">
|
||||
<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>
|
||||
<AccountGroup v-else :size="48" />
|
||||
<AccountGroup v-else :size="64" />
|
||||
</div>
|
||||
<div class="">
|
||||
<h3 class="text-2xl" dir="auto">
|
||||
<div class="px-1">
|
||||
<h3
|
||||
class="text-2xl leading-5 line-clamp-3 font-bold text-violet-3 dark:text-white"
|
||||
dir="auto"
|
||||
>
|
||||
{{ displayName(group) }}
|
||||
</h3>
|
||||
<span class="is-6 has-text-grey-dark group-federated-username">
|
||||
<span>
|
||||
{{ `@${usernameWithDomain(group)}` }}
|
||||
</span>
|
||||
</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>
|
||||
<inline-address
|
||||
v-if="group.physicalAddress && addressFullName(group.physicalAddress)"
|
||||
:physicalAddress="group.physicalAddress"
|
||||
/>
|
||||
<p class="flex">
|
||||
<p class="flex gap-1">
|
||||
<Account />
|
||||
{{
|
||||
t(
|
||||
@@ -58,10 +72,18 @@ import { addressFullName } from "@/types/address.model";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import AccountGroup from "vue-material-design-icons/AccountGroup.vue";
|
||||
import Account from "vue-material-design-icons/Account.vue";
|
||||
import { htmlToText } from "@/utils/html";
|
||||
import { computed } from "vue";
|
||||
|
||||
defineProps<{
|
||||
group: IGroup;
|
||||
}>();
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
group: IGroup;
|
||||
showSummary: boolean;
|
||||
}>(),
|
||||
{ showSummary: true }
|
||||
);
|
||||
|
||||
const { t } = useI18n({ useScope: "global" });
|
||||
|
||||
const saneSummary = computed(() => htmlToText(props.group.summary));
|
||||
</script>
|
||||
|
||||
@@ -62,7 +62,7 @@
|
||||
<div
|
||||
class="mt-3 prose dark:prose-invert lg:prose-xl line-clamp-2"
|
||||
v-if="member.parent.summary"
|
||||
v-html="flattenHTMLParagraphs(member.parent.summary)"
|
||||
v-html="htmlToText(member.parent.summary)"
|
||||
/>
|
||||
</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 AccountCircle from "vue-material-design-icons/AccountCircle.vue";
|
||||
import Tag from "@/components/Tag.vue";
|
||||
import { flattenHTMLParagraphs } from "@/utils/html";
|
||||
import { htmlToText } from "@/utils/html";
|
||||
|
||||
defineProps<{
|
||||
member: IMember;
|
||||
|
||||
@@ -57,13 +57,11 @@ const imageOpacity = computed(() =>
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
console.debug("on lazy image mounted");
|
||||
observer.value = new IntersectionObserver((entries) => {
|
||||
isIntersecting.value = entries[0].isIntersecting;
|
||||
});
|
||||
|
||||
if (wrapper.value) {
|
||||
console.debug("starting observing");
|
||||
observer.value.observe(wrapper.value);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
</h2>
|
||||
|
||||
<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"
|
||||
@click="emit('doGeoLoc')"
|
||||
>
|
||||
@@ -21,7 +21,7 @@
|
||||
<div class="hidden sm:block" v-show="showScrollLeftButton">
|
||||
<button
|
||||
@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>
|
||||
</button>
|
||||
@@ -38,7 +38,7 @@
|
||||
<div class="hidden sm:block" v-show="showScrollRightButton">
|
||||
<button
|
||||
@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>
|
||||
</button>
|
||||
@@ -47,9 +47,8 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, inject, onMounted, onUnmounted, ref } from "vue";
|
||||
import { onMounted, onUnmounted, ref } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { LocationType } from "../../types/user-location.model";
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
@@ -62,14 +61,6 @@ const emit = defineEmits(["doGeoLoc"]);
|
||||
|
||||
const { t } = useI18n({ useScope: "global" });
|
||||
|
||||
const userLocationInjection = inject<{
|
||||
userLocation: LocationType;
|
||||
}>("userLocation");
|
||||
|
||||
const isIPLocation = computed(
|
||||
() => userLocationInjection?.userLocation.isIPLocation
|
||||
);
|
||||
|
||||
const showScrollRightButton = ref(true);
|
||||
const showScrollLeftButton = ref(false);
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
<template>
|
||||
<close-content
|
||||
class="container mx-auto px-2"
|
||||
v-show="loadingEvents || (events && events.total > 0)"
|
||||
:suggestGeoloc="suggestGeoloc"
|
||||
v-on="attrs"
|
||||
@doGeoLoc="emit('doGeoLoc')"
|
||||
>
|
||||
<template #title>
|
||||
<template v-if="userLocationName">
|
||||
@@ -25,13 +27,7 @@
|
||||
:key="event.uuid"
|
||||
/>
|
||||
<more-content
|
||||
v-if="
|
||||
userLocation &&
|
||||
userLocationName &&
|
||||
userLocation.lat &&
|
||||
userLocation.lon &&
|
||||
userLocation.picture
|
||||
"
|
||||
v-if="userLocationName && userLocation?.lat && userLocation?.lon"
|
||||
:to="{
|
||||
name: 'SEARCH',
|
||||
query: {
|
||||
@@ -39,10 +35,10 @@
|
||||
lat: userLocation.lat?.toString(),
|
||||
lon: userLocation.lon?.toString(),
|
||||
contentType: 'EVENTS',
|
||||
distance: '25',
|
||||
distance: '25_km',
|
||||
},
|
||||
}"
|
||||
:picture="userLocation.picture"
|
||||
:picture="userLocation?.picture"
|
||||
>
|
||||
{{
|
||||
t("View more events around {position}", {
|
||||
@@ -65,115 +61,33 @@ import { useQuery } from "@vue/apollo-composable";
|
||||
import EventCard from "../Event/EventCard.vue";
|
||||
import { Paginate } from "@/types/paginate";
|
||||
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 { coordsToGeoHash } from "@/utils/location";
|
||||
|
||||
const props = defineProps<{ userLocation: LocationType }>();
|
||||
const emit = defineEmits(["doGeoLoc"]);
|
||||
|
||||
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 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(() => {
|
||||
return userLocation.value?.name;
|
||||
return props.userLocation?.name;
|
||||
});
|
||||
|
||||
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 suggestGeoloc = computed(() => props.userLocation?.isIPLocation);
|
||||
|
||||
const { result: eventsResult, loading: loadingEvents } = useQuery<{
|
||||
searchEvents: Paginate<IEvent>;
|
||||
}>(SEARCH_EVENTS, {
|
||||
location: geohash,
|
||||
}>(SEARCH_EVENTS, () => ({
|
||||
location: coordsToGeoHash(props.userLocation.lat, props.userLocation.lon),
|
||||
beginsOn: new Date(),
|
||||
endsOn: undefined,
|
||||
radius: 25,
|
||||
eventPage: 1,
|
||||
limit: EVENT_PAGE_LIMIT,
|
||||
type: "IN_PERSON",
|
||||
});
|
||||
}));
|
||||
|
||||
const events = computed(
|
||||
() => eventsResult.value?.searchEvents ?? { elements: [], total: 0 }
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
<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>
|
||||
{{
|
||||
$t("Popular groups nearby {position}", {
|
||||
position: userLocationName,
|
||||
})
|
||||
}}
|
||||
<template v-if="userLocationName">
|
||||
{{
|
||||
t("Popular groups nearby {position}", {
|
||||
position: userLocationName,
|
||||
})
|
||||
}}
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ t("Popular groups close to you") }}
|
||||
</template>
|
||||
</template>
|
||||
<template #content>
|
||||
<!-- <skeleton-group-result
|
||||
@@ -14,32 +24,32 @@
|
||||
:key="i"
|
||||
v-show="loadingGroups"
|
||||
/> -->
|
||||
<group-result
|
||||
<group-card
|
||||
v-for="group in selectedGroups"
|
||||
:key="group.id"
|
||||
class="scroll-ml-6 snap-center shrink-0 first:pl-8 last:pr-8 w-[18rem]"
|
||||
:group="group"
|
||||
:view-mode="'column'"
|
||||
:minimal="true"
|
||||
:has-border="true"
|
||||
:showSummary="false"
|
||||
/>
|
||||
|
||||
<more-content
|
||||
v-if="userLocation"
|
||||
v-if="userLocationName"
|
||||
:to="{
|
||||
name: 'SEARCH',
|
||||
query: {
|
||||
locationName: userLocationName,
|
||||
lat: userLocation.lat,
|
||||
lon: userLocation.lon,
|
||||
lat: userLocation.lat?.toString(),
|
||||
lon: userLocation.lon?.toString(),
|
||||
contentType: 'GROUPS',
|
||||
distance: currentDistance,
|
||||
distance: '25_km',
|
||||
},
|
||||
}"
|
||||
:picture="userLocation.picture"
|
||||
>
|
||||
{{
|
||||
$t("View more groups around {position}", {
|
||||
t("View more groups around {position}", {
|
||||
position: userLocationName,
|
||||
})
|
||||
}}
|
||||
@@ -48,8 +58,7 @@
|
||||
</close-content>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, inject, reactive, computed, ref } from "vue";
|
||||
<script lang="ts" setup>
|
||||
// import SkeletonGroupResult from "../../components/result/SkeletonGroupResult.vue";
|
||||
import sampleSize from "lodash/sampleSize";
|
||||
import { LocationType } from "../../types/user-location.model";
|
||||
@@ -57,39 +66,36 @@ import MoreContent from "./MoreContent.vue";
|
||||
import CloseContent from "./CloseContent.vue";
|
||||
import { IGroup } from "@/types/actor";
|
||||
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({
|
||||
components: {
|
||||
MoreContent,
|
||||
CloseContent,
|
||||
// SkeletonGroupResult,
|
||||
},
|
||||
apollo: {
|
||||
groups: {
|
||||
query: SEARCH_GROUPS,
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const userLocationInjection = inject<{
|
||||
userLocation: LocationType;
|
||||
}>("userLocation");
|
||||
const props = defineProps<{ userLocation: LocationType }>();
|
||||
const emit = defineEmits(["doGeoLoc"]);
|
||||
|
||||
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(
|
||||
() => userLocationInjection?.userLocation?.name
|
||||
);
|
||||
const selectedGroups = computed(() => sampleSize(groups.value?.elements, 5));
|
||||
|
||||
return {
|
||||
selectedGroups,
|
||||
userLocation: userLocationInjection?.userLocation,
|
||||
userLocationName,
|
||||
currentDistance,
|
||||
};
|
||||
},
|
||||
});
|
||||
const userLocationName = computed(() => props?.userLocation?.name);
|
||||
</script>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<template>
|
||||
<close-content
|
||||
class="container mx-auto px-2"
|
||||
v-show="loadingEvents || (events && events.total > 0)"
|
||||
:suggestGeoloc="false"
|
||||
v-on="attrs"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<router-link
|
||||
: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">
|
||||
<img
|
||||
@@ -63,7 +63,7 @@ import { computed } from "vue";
|
||||
import { CategoryPictureLicencing } from "../Categories/constants";
|
||||
|
||||
const props = defineProps<{
|
||||
to: { name: string; query: Record<string, string> };
|
||||
to: { name: string; query: Record<string, string | undefined> };
|
||||
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>
|
||||
<span
|
||||
class="rounded-md my-1 truncate text-sm text-violet-title capitalize px-2 py-1"
|
||||
:class="typeClasses"
|
||||
class="rounded-md my-1 truncate text-sm text-violet-title px-2 py-1"
|
||||
:class="[typeClasses, capitalize]"
|
||||
>
|
||||
<slot />
|
||||
</span>
|
||||
@@ -12,8 +12,9 @@ import { computed } from "vue";
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
variant?: "info" | "danger" | "warning" | "light";
|
||||
capitalize: boolean;
|
||||
}>(),
|
||||
{ variant: "light" }
|
||||
{ variant: "light", capitalize: false }
|
||||
);
|
||||
|
||||
const typeClasses = computed(() => {
|
||||
@@ -23,6 +24,8 @@ const typeClasses = computed(() => {
|
||||
return "bg-purple-3 dark:text-violet-3";
|
||||
case "info":
|
||||
return "bg-mbz-info dark:text-white";
|
||||
case "warning":
|
||||
return "bg-yellow-1";
|
||||
case "danger":
|
||||
return "bg-mbz-danger dark:text-white";
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<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
|
||||
v-if="icon"
|
||||
:icon="icon"
|
||||
@@ -15,7 +15,7 @@
|
||||
class="tag"
|
||||
role="button"
|
||||
:aria-label="ariaCloseLabel"
|
||||
:tabindex="tabstop ? 0 : false"
|
||||
:tabindex="tabstop ? 0 : undefined"
|
||||
:disabled="disabled"
|
||||
:class="[
|
||||
size,
|
||||
@@ -35,7 +35,7 @@
|
||||
/> -->
|
||||
</a>
|
||||
</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
|
||||
v-if="icon"
|
||||
:icon="icon"
|
||||
@@ -53,7 +53,7 @@
|
||||
class="delete is-small"
|
||||
:class="closeType"
|
||||
:disabled="disabled"
|
||||
:tabindex="tabstop ? 0 : false"
|
||||
:tabindex="tabstop ? 0 : undefined"
|
||||
@click="close"
|
||||
@keyup.delete.prevent="close"
|
||||
/>
|
||||
@@ -64,7 +64,7 @@ const props = withDefaults(
|
||||
defineProps<{
|
||||
attached?: boolean;
|
||||
closable?: boolean;
|
||||
type?: string;
|
||||
variant?: string;
|
||||
size?: string;
|
||||
rounded?: boolean;
|
||||
disabled?: boolean;
|
||||
@@ -227,6 +227,8 @@ const icons: Record<string, () => Promise<any>> = {
|
||||
import(`../../../node_modules/vue-material-design-icons/LanDisconnect.vue`),
|
||||
CloudQuestion: () =>
|
||||
import(`../../../node_modules/vue-material-design-icons/CloudQuestion.vue`),
|
||||
Filter: () =>
|
||||
import(`../../../node_modules/vue-material-design-icons/Filter.vue`),
|
||||
};
|
||||
|
||||
const props = withDefaults(
|
||||
|
||||
Reference in New Issue
Block a user