Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: initial implementation of showing clustered responses #119

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
3 changes: 2 additions & 1 deletion apps/frontend/locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -331,10 +331,11 @@
"updateResponse": "Antwort bearbeiten"
},
"addAPoint": "Klicken Sie dort auf die Karte, wo Sie einen Ort ergänzen möchten",
"addLocation": "Neuen Ort eingeben",
"addLocation": "Neuen Eintrag eingeben",
"confirmHuman": "Bitte bestätigen Sie, dass Sie ein Mensch sind",
"contactDetails": "Kontaktdaten",
"ended": "Dieser Callout ist seit dem {date} beendet.",
"entryOf": "Eintrag {no} von {total}",
"form": {
"email": "Ihre E-Mail-Adresse",
"guestFieldsMissing": "Bitte geben Sie uns Ihre Kontaktdaten.",
Expand Down
3 changes: 2 additions & 1 deletion apps/frontend/locales/de@informal.json
Original file line number Diff line number Diff line change
Expand Up @@ -331,10 +331,11 @@
"updateResponse": "Antwort bearbeiten"
},
"addAPoint": "Klicken Sie dort auf die Karte, wo Sie einen Ort ergänzen möchten",
"addLocation": "Neuen Ort eingeben",
"addLocation": "Neuen Eintrag eingeben",
"confirmHuman": "Bitte bestätige, dass du ein Mensch bist",
"contactDetails": "Kontaktdaten",
"ended": "Dieser Callout ist seit dem {date} beendet.",
"entryOf": "Eintrag {no} von {total}",
"form": {
"email": "Deine E-Mail-Adresse",
"guestFieldsMissing": "Bitte gib' uns Deine Kontaktdaten. ",
Expand Down
1 change: 1 addition & 0 deletions apps/frontend/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,7 @@
"confirmHuman": "Please confirm you are human",
"contactDetails": "Contact details",
"ended": "This callout ended on {date}",
"entryOf": "Entry {no} of {total}",
"form": {
"email": "Your email",
"guestFieldsMissing": "Please tell us your contact details",
Expand Down
117 changes: 117 additions & 0 deletions apps/frontend/src/components/pages/callouts/CalloutResponse.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
<template>
<div v-if="callout.responseViewSchema">
<h2 class="mb-4 font-title text-2xl font-bold">{{ response.title }}</h2>
<div
v-if="response.photos.length > 0"
class="relative -mx-4 mb-4 overflow-hidden"
>
<ul
class="flex items-center transition-transform"
:style="{ transform: `translateX(${currentPhotoIndex * -100}%)` }"
>
<li
v-for="photo in response.photos"
:key="photo.url"
class="w-full flex-none p-4"
>
<img
class="max-h-[300px] w-full object-contain"
:style="{ filter: callout.responseViewSchema.imageFilter }"
:src="photo.url + '?w=600&h=600'"
/>
</li>
</ul>
<div
v-if="response.photos.length > 1"
class="absolute inset-x-0 top-1/2 flex -translate-y-1/2 transform justify-between text-2xl font-bold"
>
<div>
<button
v-show="currentPhotoIndex > 0"
class="flex h-10 w-10 items-center justify-center rounded-full bg-primary text-white"
@click="currentPhotoIndex--"
>
<font-awesome-icon :icon="faChevronLeft" />
</button>
</div>
<div>
<button
v-show="currentPhotoIndex < response.photos.length - 1"
class="flex h-10 w-10 items-center justify-center rounded-full bg-primary text-white"
@click="currentPhotoIndex++"
>
<font-awesome-icon :icon="faChevronRight" />
</button>
</div>
</div>
</div>

<CalloutForm
:key="response.number"
:callout="viewOnlyCallout"
:answers="response.answers"
readonly
all-slides
no-bg
:style="'simple'"
/>

<ul
v-if="callout.responseViewSchema.links.length > 0"
class="mt-8 columns-2 gap-4 border-t border-t-primary pt-8"
>
<li
v-for="link in callout.responseViewSchema.links"
:key="link.url"
class="break-inside-avoid"
>
<a
class="block font-title font-bold text-link underline"
:href="link.url"
target="_blank"
>
{{ link.text }}
</a>
</li>
</ul>
</div>
</template>
<script lang="ts" setup>
import {
filterComponents,
type GetCalloutDataWith,
type GetCalloutResponseMapData,
} from '@beabee/beabee-common';
import {
faChevronLeft,
faChevronRight,
} from '@fortawesome/free-solid-svg-icons';
import { computed, ref, watch } from 'vue';
import CalloutForm from './CalloutForm.vue';

const props = defineProps<{
callout: GetCalloutDataWith<'form' | 'responseViewSchema'>;
response: GetCalloutResponseMapData;
}>();

const currentPhotoIndex = ref(0);

// Don't show admin-only fields (they would always be empty as the API doesn't return their answers)
const viewOnlyCallout = computed(() => ({
...props.callout,
formSchema: {
...props.callout.formSchema,
slides: props.callout.formSchema.slides.map((slide) => ({
...slide,
components: filterComponents(slide.components, (c) => !c.adminOnly),
})),
},
}));

watch(
() => props.response,
() => {
currentPhotoIndex.value = 0;
}
);
</script>
151 changes: 50 additions & 101 deletions apps/frontend/src/components/pages/callouts/CalloutShowResponsePanel.vue
Original file line number Diff line number Diff line change
@@ -1,123 +1,72 @@
<template>
<CalloutSidePanel :show="!!response" @close="$emit('close')">
<div v-if="response && callout.responseViewSchema">
<h2 class="mb-4 font-title text-2xl font-bold">{{ response.title }}</h2>
<div
v-if="response.photos.length > 0"
class="relative -mx-4 mb-4 overflow-hidden"
>
<ul
class="flex items-center transition-transform"
:style="{ transform: `translateX(${currentPhotoIndex * -100}%)` }"
>
<li
v-for="photo in response.photos"
:key="photo.url"
class="w-full flex-none p-4"
>
<img
class="max-h-[300px] w-full object-contain"
:style="{ filter: callout.responseViewSchema.imageFilter }"
:src="photo.url + '?w=600&h=600'"
/>
</li>
</ul>
<div
v-if="response.photos.length > 1"
class="absolute inset-x-0 top-1/2 flex -translate-y-1/2 transform justify-between text-2xl font-bold"
>
<div>
<button
v-show="currentPhotoIndex > 0"
class="flex h-10 w-10 items-center justify-center rounded-full bg-primary text-white"
@click="currentPhotoIndex--"
>
<font-awesome-icon :icon="faChevronLeft" />
</button>
</div>
<div>
<button
v-show="currentPhotoIndex < response.photos.length - 1"
class="flex h-10 w-10 items-center justify-center rounded-full bg-primary text-white"
@click="currentPhotoIndex++"
>
<font-awesome-icon :icon="faChevronRight" />
</button>
</div>
</div>
</div>
<CalloutSidePanel :show="!!currentResponse" @close="$emit('close')">
<div v-if="responses.length > 1" class="mb-4 flex items-center gap-4">
<AppButtonGroup>
<AppButton
variant="link"
:icon="faArrowLeft"
:disabled="responseIndex === 0"
@click="changeResponse(-1)"
/>
<AppButton
variant="link"
:icon="faArrowRight"
:disabled="responseIndex === responses.length - 1"
@click="changeResponse(1)"
/>
</AppButtonGroup>

<CalloutForm
:key="response.number"
:callout="viewOnlyCallout"
:answers="response.answers"
readonly
all-slides
no-bg
:style="'simple'"
/>

<ul
v-if="callout.responseViewSchema.links.length > 0"
class="mt-8 columns-2 gap-4 border-t border-t-primary pt-8"
>
<li
v-for="link in callout.responseViewSchema.links"
:key="link.url"
class="break-inside-avoid"
>
<a
class="block font-title font-bold text-link underline"
:href="link.url"
target="_blank"
>
{{ link.text }}
</a>
</li>
</ul>
<i18n-t keypath="callout.entryOf" tag="span">
<template #no>
<b>{{ n(responseIndex + 1) }}</b>
</template>
<template #total>
<b>{{ n(responses.length) }}</b>
</template>
</i18n-t>
</div>

<CalloutResponse
v-if="currentResponse"
:callout="callout"
:response="currentResponse"
/>
</CalloutSidePanel>
</template>

<script lang="ts" setup>
import {
faChevronLeft,
faChevronRight,
} from '@fortawesome/free-solid-svg-icons';
import { computed, ref, watch } from 'vue';
import { faArrowLeft, faArrowRight } from '@fortawesome/free-solid-svg-icons';
import { computed } from 'vue';

import CalloutSidePanel from './CalloutSidePanel.vue';
import CalloutForm from './CalloutForm.vue';
import {
filterComponents,
type GetCalloutDataWith,
type GetCalloutResponseMapData,
} from '@beabee/beabee-common';
import CalloutResponse from './CalloutResponse.vue';
import AppButton from '@components/button/AppButton.vue';
import AppButtonGroup from '@components/button/AppButtonGroup.vue';
import { useI18n } from 'vue-i18n';

defineEmits<(e: 'close') => void>();
defineEmits<{ (e: 'close'): void }>();
const props = defineProps<{
callout: GetCalloutDataWith<'form' | 'responseViewSchema'>;
response?: GetCalloutResponseMapData;
responses: GetCalloutResponseMapData[];
}>();

const currentPhotoIndex = ref(0);
const currentResponseNumber = defineModel<number>('currentResponseNumber', {
default: 0,
});

// Don't show admin-only fields (they would always be empty as the API doesn't return their answers)
const viewOnlyCallout = computed(() => ({
...props.callout,
formSchema: {
...props.callout.formSchema,
slides: props.callout.formSchema.slides.map((slide) => ({
...slide,
components: filterComponents(slide.components, (c) => !c.adminOnly),
})),
},
}));
const { n } = useI18n();

watch(
() => props.response,
() => {
currentPhotoIndex.value = 0;
}
const responseIndex = computed(() =>
props.responses.findIndex((r) => r.number === currentResponseNumber.value)
);
const currentResponse = computed(() => props.responses[responseIndex.value]);

function changeResponse(inc: number) {
const newIndex = responseIndex.value + inc;
currentResponseNumber.value = props.responses[newIndex].number;
}
</script>
2 changes: 1 addition & 1 deletion apps/frontend/src/pages/callouts/[id]/gallery.vue
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ meta:

<CalloutShowResponsePanel
:callout="callout"
:response="selectedResponse"
:responses="selectedResponse ? [selectedResponse] : []"
@close="
router.push({ ...route, hash: '' });
introOpen = false;
Expand Down
Loading
Loading