2024-09-12 10:11:25 +02:00

412 lines
16 KiB
Vue

<template>
<Page>
<CommunityNav />
<h2>
<Icon v="user-chart" />
<T>census.headerLong</T>
</h2>
<template v-if="q === null">
<section v-if="$isGranted('census')">
<div class="alert alert-info">
<a href="/api/census/export" class="btn btn-outline-secondary btn-sm float-end">
<Icon v="download" />
</a>
<p>{{ countResponses.all }} <T>census.replies</T></p>
<ul>
<li>{{ countResponses.nonbinary }} <T>census.repliesNonbinary</T></li>
<li>{{ countResponses.usable }} <T>census.repliesUsable</T></li>
<li>
<nuxt-link :to="`/${config.census.route}/admin`">
{{ countResponses.awaiting }} <T>census.repliesAwaiting</T>
</nuxt-link>
</li>
</ul>
<ChartSet name="useful responses" :data="countResponses.graphs" init="cumulative" class="mb-3" />
</div>
</section>
<section class="row">
<div class="col-12 col-lg-6">
<T>census.description</T>
<T
v-if="open"
:params="{
questions: questions.length,
start: start.setLocale(config.locale).toLocaleString(DateTime.DATE_SHORT),
end: end.setLocale(config.locale).toLocaleString(DateTime.DATE_SHORT),
}"
>
census.descriptionOpen
</T>
<T v-else>census.descriptionClosed</T>
</div>
<div class="col-12 col-lg-6">
<div v-if="Object.keys(config.census.results).length > 0" class="alert alert-info">
<ul class="list-unstyled mb-0">
<li v-for="(text, link) in config.census.results" class="h5 m-3">
<router-link v-if="typeof text === 'string'" :to="`/blog/${link}`">
<Icon v="file-chart-line" />
{{ text }}
</router-link>
<div v-else>
<router-link :to="`/blog/${link}`">
<Icon v="file-chart-line" />
{{ text._ }}
</router-link>
<ul class="list-unstyled small">
<template v-for="(subText, subLink) in text">
<li v-if="subLink !== '_'">
<span style="display: inline-block; width: 1.5em"></span>
<router-link :to="`/blog/${subLink}`">
{{ subText }}
</router-link>
</li>
</template>
</ul>
</div>
</li>
</ul>
</div>
<CensusShare :text="$t('census.headerLong')" />
<CensusSubscribe />
</div>
</section>
<section v-if="open">
<div v-if="finished" class="alert alert-success">
<Icon v="badge-check" :size="2" class="float-start me-2 mt-2" />
<T>census.finished</T>
</div>
<template v-else>
<div class="form-group">
<div class="form-check">
<label class="form-check-label small">
<input v-model="agreement" type="checkbox" class="form-check-input">
<T>census.agree</T>
</label>
</div>
</div>
<div class="form-group">
<button class="btn btn-primary btn-lg" :disabled="!agreement" @click="startSurvey">
<T>census.start</T>
</button>
</div>
</template>
</section>
</template>
<template v-else-if="q < questions.length">
<div class="progress my-3">
<div
class="progress-bar"
role="progressbar"
:style="`width: ${progress}%`"
:aria-valuenow="q"
aria-valuemin="0"
:aria-valuemax="questions.length"
>
{{ q }}/{{ questions.length }}
</div>
</div>
<p class="h4 mt-5 mb-3">
{{ q + 1 }}. {{ question.question }}
</p>
<div v-if="question.instruction" class="alert alert-info small">
<p v-for="(line, i) in question.instruction" :class="i === question.instruction.length - 1 ? 'mb-0' : ''">
<LinkedText :text="line" />
</p>
</div>
<div v-if="question.cw" class="form-check form-switch my-2">
<label>
<input v-model="cw" class="form-check-input" type="checkbox">
<Icon v="engine-warning" />
<T>census.cw.switch</T>
</label>
</div>
<form ref="questionform" @submit.prevent="q++">
<div v-if="question.type === 'radio'" :class="['form-group', question.optionsSorted.length > 10 ? 'multi-column' : '']">
<div v-for="[option, help] in question.optionsSorted" class="form-check mb-2">
<label class="form-check-label small">
<input
v-model="answers[q]"
type="radio"
class="form-check-input"
:name="`question${q}`"
:value="option"
required
>
<span v-if="question.cw && question.cw.includes(option) && !cw" class="badge text-bg-light"><T>census.cw.label</T></span>
<span v-else>{{ option }}</span>
<span v-if="help" class="text-muted">({{ help }})</span>
</label>
</div>
</div>
<div v-else-if="question.type === 'checkbox'" :class="['form-group', question.optionsSorted.length > 10 ? 'multi-column' : '']">
<div v-for="[option, help] in question.optionsSorted" class="form-check mb-2">
<label class="form-check-label small">
<input v-model="answers[q]" type="checkbox" class="form-check-input" :value="option">
<span v-if="question.cw && question.cw.includes(option) && !cw" class="badge text-bg-light"><T>census.cw.label</T></span>
<span v-else>{{ option }}</span>
<span v-if="help" class="text-muted">({{ help }})</span>
</label>
</div>
</div>
<div v-else-if="question.type === 'text'" class="form-group">
<input v-model="answers[q]" type="text" class="form-control" required>
</div>
<div v-else-if="question.type === 'number'" class="form-group">
<input
v-model="answers[q]"
type="number"
class="form-control"
:min="question.min"
:max="question.max"
required
>
</div>
<div v-else-if="question.type === 'textarea'" class="form-group">
<textarea v-model="answers[q]" class="form-control"></textarea>
</div>
<div v-if="question.writein" class="form-group">
<input v-model="writins[q]" type="text" class="form-control form-control-sm" :placeholder="$t('census.writein')">
</div>
</form>
<div class="btn-group w-100">
<button class="btn btn-outline-primary" :disabled="q === 0" @click="q--">
<Icon v="arrow-alt-left" />
<T>census.prev</T>
</button>
<button class="btn btn-primary" :disabled="!stepValid" @click="q++">
<T>census.next</T>
<Icon v="arrow-alt-right" />
</button>
</div>
<div v-if="$user() && $user().username === 'andrea'" class="mt-4 btn-group w-100">
<button v-for="(question, i) in questions" :class="['btn', q === i ? 'btn-primary' : 'btn-outline-primary']" :disabled="q === i" @click="q = i">
{{ i }}
</button>
</div>
</template>
<template v-else>
<div class="progress my-3">
<div
class="progress-bar"
role="progressbar"
:style="`width: ${progress}%`"
:aria-valuenow="q"
aria-valuemin="0"
:aria-valuemax="questions.length"
>
{{ q }}/{{ questions.length }}
</div>
</div>
<section>
<div class="alert alert-success">
<Icon v="badge-check" :size="2" class="float-start me-2 mt-2" />
<T>census.finished</T>
</div>
</section>
<section class="row">
<div class="col-12 col-lg-6">
<CensusShare :text="$t('census.shareButton')" />
</div>
<div class="col-12 col-lg-6">
<CensusSubscribe />
</div>
</section>
</template>
</Page>
</template>
<script>
import { buildDict, shuffle } from '~/src/helpers.ts';
import { DateTime } from 'luxon';
import useConfig from '~/composables/useConfig.ts';
import useDialogue from '~/composables/useDialogue.ts';
export default {
async beforeRouteLeave(to, from, next) {
if (this.q !== null && this.q < this.questions.length) {
try {
await this.dialogue.confirm(this.$t('census.leave'));
} catch {
next(false);
return;
}
}
next();
},
async setup() {
definePageMeta({
translatedPaths: (config) => translatedPathByConfigModule(config.census),
});
const { $translator: translator } = useNuxtApp();
const dialogue = useDialogue();
useSimpleHead({
title: translator.translate('census.headerLong'),
description: translator.translate('census.description')[0],
banner: 'img-local/census/census-banner.png',
}, translator);
const { data: finished } = useFetch('/api/census/finished');
const { data: countResponses } = useFetch('/api/census/count');
await Promise.all([finished, countResponses]);
return {
config: useConfig(),
dialogue: dialogue,
finished,
countResponses,
};
},
data() {
const questions = this.config.census.questions.map((q) => {
q.optionsSorted = q.randomise
? [
...q.optionsFirst || [],
...shuffle(q.options),
...q.optionsLast || [],
]
: q.options;
return q;
});
return {
agreement: false,
q: null,
questions,
answers: buildDict(function* () {
let i = 0;
for (const question of questions) {
yield [i, question.type === 'checkbox' ? [] : null];
i++;
}
}),
writins: buildDict(function* () {
let i = 0;
for (const _question of questions) {
yield [i, ''];
i++;
}
}),
DateTime,
cw: false,
};
},
computed: {
progress() {
return Math.round(100 * (this.q || 0) / this.questions.length);
},
question() {
return this.questions[this.q];
},
stepValid() {
if (!this.question) {
return false;
}
if (this.writins[this.q] !== '') {
return true;
}
if (this.question.optional) {
return true;
}
if (this.question.type === 'radio') {
return this.answers[this.q] !== undefined && this.answers[this.q] !== null;
}
if (this.question.type === 'checkbox') {
return this.answers[this.q] !== undefined && this.answers[this.q].length > 0;
}
if (this.question.type === 'number') {
const v = parseInt(this.answers[this.q]);
return this.answers[this.q] !== '' && v >= this.question.min && v <= this.question.max;
}
if (this.question.type === 'text' || this.question.type === 'textarea') {
return this.answers[this.q] !== '';
}
return true;
},
start() {
return DateTime.fromISO(this.config.census.start).toLocal();
},
end() {
return DateTime.fromISO(this.config.census.end).toLocal();
},
open() {
const now = DateTime.utc().setZone(this.config.format.timezone);
return now >= this.start && now <= this.end;
},
finishedEditionKey() {
return `census-${this.config.census.edition}-finished`;
},
},
watch: {
async q(newValue, oldValue) {
if (this.question && this.question.conditionalOn) {
const conditionAnswer = this.answers[this.question.conditionalOn];
const conditionFullfilled = Array.isArray(conditionAnswer)
? conditionAnswer.filter((a) => this.question.conditionalValue.includes(a)).length > 0
: conditionAnswer === this.question.conditionalValue;
if (!conditionFullfilled) {
if (newValue > oldValue) {
this.q++;
} else {
this.q--;
}
return;
}
}
if (this.q === this.questions.length) {
await this.dialogue.postWithAlertOnError('/api/census/submit', {
answers: JSON.stringify(this.answers),
writins: JSON.stringify(this.writins),
});
this.finished = true;
window.localStorage.setItem(this.finishedEditionKey, `${this.config.census.edition}`);
}
this.$nextTick(() => {
if (this.$refs.questionform) {
this.$refs.questionform.querySelector('input,textarea').focus();
}
});
},
},
async mounted() {
if (process.client && !this.$user()) {
const finishedEdition = window.localStorage.getItem(this.finishedEditionKey) || 0;
this.finished = parseInt(finishedEdition) === parseInt(this.config.census.edition);
if (!this.finished) {
this.finished = await $fetch('/api/census/finished');
}
}
},
methods: {
startSurvey() {
this.q = 0;
},
},
};
</script>
<style lang="scss">
@import "assets/variables";
.multi-column {
columns: 2;
}
@include media-breakpoint-up('md', $grid-breakpoints) {
.multi-column {
columns: 3;
}
}
</style>