Skip to content

Commit 1318734

Browse files
authored
Merge pull request activist-org#1328 from nicki182/1291_selector_view_events
1291 selector view events
2 parents 7d667ed + a58c0d8 commit 1318734

37 files changed

+1065
-695
lines changed
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<!-- SPDX-License-Identifier: AGPL-3.0-or-later -->
2+
<template>
3+
<div v-for="event in events" class="space-y-6 pb-6 pt-3 md:pt-4">
4+
<CardSearchResultEvent :isPrivate="false" :event="event" />
5+
</div>
6+
</template>
7+
8+
<script setup lang="ts">
9+
import type { Event } from "~/types/events/event";
10+
11+
defineProps<{
12+
events: Event[];
13+
}>();
14+
</script>
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<!-- SPDX-License-Identifier: AGPL-3.0-or-later -->
2+
<template>
3+
<div class="space-y-6 pb-6 pt-3 md:pt-4">
4+
<MediaMapEvents
5+
v-if="events.length"
6+
class="h-[calc(50vh-1rem)] w-full"
7+
:events="events"
8+
/>
9+
</div>
10+
</template>
11+
12+
<script setup lang="ts">
13+
import type { Event } from "~/types/events/event";
14+
15+
defineProps<{
16+
events: Event[];
17+
}>();
18+
</script>

frontend/components/form/FormViewSelector.vue

Lines changed: 34 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -2,26 +2,31 @@
22
<template>
33
<RadioGroup
44
v-model="value"
5-
class="flex h-11 w-full items-center divide-x-2 divide-primary-text"
5+
class="flex h-10 w-full px-1"
66
:aria-label="$t('i18n.components.form_view_selector.title_aria_label')"
77
>
88
<RadioGroupOption
9-
v-for="option in viewOptions"
10-
:key="option"
9+
v-for="(option, idx) in options"
10+
:key="option.key"
1111
v-slot="{ checked }"
12-
:name="option"
13-
:value="option"
14-
as="template"
12+
class="flex flex-1"
13+
:name="option.label || ''"
14+
:value="option.value"
1515
>
1616
<button
17-
class="h-full flex-1 border-y-2 border-primary-text first:rounded-l-xl first:border-l-2 last:rounded-r-xl last:!border-r-2"
18-
:class="checked ? 'bg-menu-selection' : 'bg-layer-0'"
19-
:aria-label="$t(viewAriaLabelsDict[option])"
17+
class="flex-1 rounded-none"
18+
:class="{
19+
'style-menu-option-cta': checked,
20+
'style-menu-option bg-layer-0': !checked,
21+
'rounded-l-lg': idx === 0,
22+
'rounded-r-lg': idx === options.length - 1,
23+
}"
24+
:aria-label="$t(option.aria_label)"
2025
>
2126
<Icon
22-
class="h-full w-auto p-2"
23-
:class="checked ? 'text-primary-text' : 'text-distinct-text'"
24-
:name="viewTypeIcons[option]"
27+
:name="option.content as string"
28+
class="h-6 w-6"
29+
:aria-hidden="true"
2530
/>
2631
</button>
2732
</RadioGroupOption>
@@ -31,58 +36,26 @@
3136
<script setup lang="ts">
3237
import { RadioGroup, RadioGroupOption } from "@headlessui/vue";
3338
34-
import { ViewType } from "~/types/view-types";
39+
type Option = {
40+
value: string | number | boolean | Record<string, unknown> | undefined;
41+
key: string;
42+
content: HTMLElement | string;
43+
aria_label: string;
44+
label?: string;
45+
isIcon?: boolean;
46+
};
3547
36-
const props = defineProps({
37-
modelValue: {
38-
type: String as PropType<ViewType>,
39-
required: true,
40-
},
41-
option1: {
42-
type: String as PropType<ViewType>,
43-
required: true,
44-
},
45-
option2: {
46-
type: String as PropType<ViewType>,
47-
required: true,
48-
},
49-
option3: {
50-
type: String as PropType<ViewType>,
51-
required: false,
52-
},
53-
});
48+
const props = defineProps<{
49+
modelValue: string | number | boolean | Record<string, unknown> | undefined;
50+
options: Option[];
51+
}>();
5452
55-
const emit = defineEmits(["update:modelValue"]);
53+
const emit = defineEmits<{
54+
(e: "update:modelValue", value: typeof props.modelValue): void;
55+
}>();
5656
5757
const value = computed({
58-
get() {
59-
return props.modelValue;
60-
},
61-
set(value: ViewType) {
62-
emit("update:modelValue", value);
63-
},
64-
});
65-
66-
const viewOptions = computed(() => {
67-
const options = [props.option1, props.option2];
68-
if (props.option3) {
69-
options.push(props.option3);
70-
}
71-
return options;
58+
get: () => props.modelValue,
59+
set: (val) => emit("update:modelValue", val),
7260
});
73-
74-
// Dictionary is used to assure that the full keys are present and picked up by the i18n checks.
75-
const viewAriaLabelsDict = {
76-
map: "i18n.components.form_view_selector.view_as_map_aria_label",
77-
list: "i18n.components.form_view_selector.view_as_list_aria_label",
78-
calendar: "i18n.components.form_view_selector.view_as_calendar_aria_label",
79-
grid: "i18n.components.form_view_selector.view_as_grid_aria_label",
80-
};
81-
82-
const viewTypeIcons: Record<ViewType, string> = {
83-
[ViewType.MAP]: "bi-pin-map-fill",
84-
[ViewType.LIST]: "bi-list-ul",
85-
[ViewType.GRID]: "bi-grid-3x2-gap-fill",
86-
[ViewType.CALENDAR]: "bi-calendar-date",
87-
};
8861
</script>

frontend/components/media/MediaMap.vue renamed to frontend/components/media/map/MediaMap.vue

Lines changed: 32 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -9,23 +9,27 @@
99
<script setup lang="ts">
1010
import type { LayerSpecification } from "maplibre-gl";
1111
12-
import { useMap } from "@/composables/useMap";
1312
import "maplibre-gl/dist/maplibre-gl.css";
1413
15-
import type { Location } from "~/types/content/location";
16-
import type { Event, EventType } from "~/types/events/event";
17-
1814
import { useClusterMap } from "~/composables/useClusterMap";
15+
import { useMap } from "~/composables/useMap";
16+
import { usePointerMap } from "~/composables/usePointerMap";
1917
import { useRouting } from "~/composables/useRoutingMap";
20-
import { MapType } from "~/types/map";
18+
import {
19+
MapType,
20+
type ClusterProperties,
21+
type Pointer,
22+
type PointerCluster,
23+
type PopupContent,
24+
} from "~/types/map";
2125
2226
const props = defineProps<{
23-
eventNames?: string[];
24-
eventTypes?: EventType[];
25-
eventLocations?: Location[];
26-
events?: Event[];
27+
pointer?: Pointer;
2728
type: MapType;
28-
ids?: string[];
29+
pointers?: PointerCluster[];
30+
clusterProperties?: ClusterProperties;
31+
clusterTooltipCreate?: (pointer: unknown) => PopupContent;
32+
pointerTooltipCreate?: (pointer: Pointer) => PopupContent;
2933
}>();
3034
3135
const { createMap, isWebglSupported, addDefaultControls } = useMap();
@@ -41,9 +45,6 @@ const i18n = useI18n();
4145
const colorMode = useColorMode();
4246
const { setMapLayers, setMap } = useRouting();
4347
44-
const attendLabelKey = "i18n.components._global.attend";
45-
const attendLabel = i18n.t(attendLabelKey) as string;
46-
4748
const isTouchDevice =
4849
// Note: `maxTouchPoints` isn't recognized by TS. Safe to ignore.
4950
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
@@ -81,7 +82,6 @@ const mapLayers: LayerSpecification[] = [
8182
];
8283
8384
// MARK: Map Creation
84-
8585
onMounted(() => {
8686
if (!isWebglSupported()) {
8787
alert(i18n.t("i18n.components.media_map.maplibre_gl_alert"));
@@ -92,22 +92,28 @@ onMounted(() => {
9292
setMap(map);
9393
9494
if (props.type === MapType.POINT) {
95-
createMapForPointerTypeMap(
95+
if (!props.pointer) {
96+
console.error("Pointer is required for MapType.POINT");
97+
return;
98+
}
99+
const pointer: Pointer = props.pointer;
100+
createMapForPointerTypeMap(map, pointer, isTouchDevice);
101+
}
102+
if (props.type === MapType.CLUSTER) {
103+
if (!props.pointers || props.pointers.length === 0) {
104+
console.error("Pointers are required for MapType.CLUSTER");
105+
return;
106+
}
107+
const { pointers } = props;
108+
createMapForClusterTypeMap(
96109
map,
97-
{
98-
name: props.eventNames ? props.eventNames[0] : "",
99-
type: props.eventTypes ? props.eventTypes[0] : "learn",
100-
location: props.eventLocations
101-
? props.eventLocations[0]
102-
: ({} as Location),
103-
id: props.ids ? props.ids[0] : "",
104-
},
110+
pointers || [],
105111
isTouchDevice,
106-
attendLabel
112+
props.clusterProperties as ClusterProperties,
113+
props?.clusterTooltipCreate as (pointer: unknown) => PopupContent,
114+
props?.pointerTooltipCreate as (pointer: unknown) => PopupContent
107115
);
108116
}
109-
if (props.type === MapType.CLUSTER)
110-
createMapForClusterTypeMap(map, props.events || [], isTouchDevice);
111117
}
112118
});
113119
</script>
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
<!-- SPDX-License-Identifier: AGPL-3.0-or-later -->
2+
<template>
3+
<MediaMap
4+
class="h-[17.5rem] w-full"
5+
:pointer="pointer"
6+
:type="MapType.POINT"
7+
/>
8+
</template>
9+
10+
<script setup lang="ts">
11+
import type { Event, EventType } from "~/types/events/event";
12+
13+
import { MapType, type Pointer } from "~/types/map";
14+
import { colorByType } from "~/utils/mapUtils";
15+
const organizationIcon = `/icons/map/tooltip_organization.png`;
16+
const calendarIcon = `/icons/map/tooltip_datetime.png`;
17+
const locationIcon = `/icons/map/tooltip_location.png`;
18+
const props = defineProps<{
19+
event: Event;
20+
}>();
21+
const { event } = props;
22+
const buildExpandedTooltip = () => {
23+
const root = document.createElement("div");
24+
root.className = "w-[220px] cursor-pointer font-sans";
25+
26+
let tooltipClass = "";
27+
if (event.type === "learn") {
28+
tooltipClass =
29+
"overflow-hidden bg-white rounded-sm border-l-8 border-l-[#2176AE]";
30+
} else {
31+
tooltipClass =
32+
"overflow-hidden bg-white rounded-sm border-l-8 border-l-[#BA3D3B]";
33+
}
34+
const url = "";
35+
const organization = "Organization"; // replace with actual organization name
36+
const datetime = "Date and Time"; // replace with actual date and time
37+
38+
root.innerHTML = `
39+
<a href="${url}" class="no-underline">
40+
<div class="${tooltipClass}">
41+
<div class="px-3 py-1">
42+
<h3 class="font-display text-base text-black font-bold mb-2 leading-tight">${event.name}</h3>
43+
44+
<div class="flex items-center text-xs text-black mb-1.5 font-semibold space-x-2">
45+
<img src="${organizationIcon}"/>
46+
<span>${organization}</span>
47+
</div>
48+
49+
<div class="flex items-center text-xs text-black mb-1.5 font-semibold space-x-2">
50+
<img src="${calendarIcon}"/>
51+
<span>${datetime}</span>
52+
</div>
53+
54+
<div class="flex items-start text-xs text-black mb-1.5 font-semibold space-x-2">
55+
<img src="${locationIcon}"/>
56+
<span>${event.offlineLocation?.displayName.split(",").slice(0, 3).join(", ")}</span>
57+
</div>
58+
</div>
59+
</div>
60+
</a>
61+
`;
62+
63+
return root;
64+
};
65+
const pointer: Pointer = {
66+
id: event.id,
67+
color: colorByType[event.type as EventType],
68+
location: event.offlineLocation || {
69+
displayName: event.name,
70+
lat: "0",
71+
lon: "0",
72+
id: "",
73+
bbox: ["0", "0", "0", "0"],
74+
},
75+
popup: buildExpandedTooltip(),
76+
};
77+
</script>

0 commit comments

Comments
 (0)