PronounsPage/components/PersonalEventListInput.vue
Adaline Simonian 23a3862ca0
test: introduce snapshot-based smoke tests
- Adds a new test suite with Docker-based smoke tests for all locales.
  Can be run using the ./smoketest.sh script.
- Replaces all calls to Math.random() with a new helper that returns 0.5
  in snapshot testing mode, ensuring deterministic snapshots.
- Similarly replaces all calls to new Date() and Date.now() with new
  helpers that return a fixed date in snapshot testing mode.
- Replaces checks against NODE_ENV with APP_ENV, to ensure that the
  bundles can be built with Nuxt for testing without losing code that
  would otherwise be stripped out by production optimizations.
- Adds a database init script that can be used to initialize the
  database with a single admin user and a long-lived JWT token for use
  in automation tests.
- Adds a JWT decoding/encoding CLI tool for debugging JWTs.

Note: Snapshots are not checked in, and must be generated manually. See
test/__snapshots__/.gitignore for more information.
2025-02-02 23:11:19 -08:00

138 lines
3.8 KiB
Vue

<script setup lang="ts">
import datepicker from '@vuepic/vue-datepicker';
import type { CustomEvent } from '~/src/calendar/helpers.ts';
import { newDate } from '~/src/helpers.ts';
defineProps<{
maxitems: number;
}>();
const { $translator: translator } = useNuxtApp();
const config = useConfig();
const { isDark } = useDark();
const today = newDate();
const modelValue = defineModel<CustomEvent[]>();
const showIconSelector = ref<number | false>(false);
const maxDaysInMonths: Record<number, number> = {
1: 31,
2: 29,
3: 31,
4: 30,
5: 31,
6: 30,
7: 31,
8: 31,
9: 30,
10: 31,
11: 30,
12: 31,
};
const formatDate = (date: Date): string => {
return translator.translate(`calendar.dates.${date.getMonth() + 1}`, { day: date.getDate().toString() });
};
const prototype = () => {
return { name: '', month: '', day: '', comment: '' };
};
const validation = (v: CustomEvent): string | null => {
if (JSON.stringify(v) === JSON.stringify(prototype())) {
return null;
}
if (!v.name) {
return 'profile.calendar.customEvents.validation.missingName';
}
if (!v.month || !v.day) {
return 'profile.calendar.customEvents.validation.missingDate';
}
if (v.day < 1 || v.day > maxDaysInMonths[v.month]) {
return 'profile.calendar.customEvents.validation.invalidDate';
}
return null;
};
</script>
<template>
<ListInput
v-model="modelValue"
:prototype="prototype()"
:maxitems="maxitems"
disable-sorting
>
<template #default="s">
<datepicker
:model-value="new Date(today.getFullYear(), s.val.month - 1, s.val.day)"
:format="formatDate"
auto-apply
:enable-time-picker="false"
:clearable="false"
no-today
vertical
:dark="isDark"
:locale="config.locale"
@update:model-value="(date: Date) => {
s.val.month = date.getMonth() + 1;
s.val.day = date.getDate();
}"
/>
<button
type="button"
:class="['btn', 'btn-outline-secondary', showIconSelector === s.i ? 'btn-secondary text-white border' : '']"
@click="showIconSelector = showIconSelector === s.i ? false : s.i"
>
<Icon :v="s.val.icon" />
</button>
<input
v-model="s.val.name"
class="form-control"
required
maxlength="24"
:placeholder="$t('profile.calendar.customEvents.name')"
@keyup="s.update(s.val)"
@paste="$nextTick(() => s.update(s.val))"
@change="s.update(s.val)"
>
<input
v-model="s.val.comment"
class="form-control"
maxlength="128"
:placeholder="$t('profile.calendar.customEvents.comment')"
@keyup="s.update(s.val)"
@paste="$nextTick(() => s.update(s.val))"
@change="s.update(s.val)"
>
<IconSelector
v-if="showIconSelector === s.i"
class="hanging shadow shadow-lg border"
@change="s.update({ ...s.val, icon: $event }); showIconSelector = false"
/>
</template>
<template #validation="s">
<p v-if="validation(s.val)" class="small text-danger">
<Icon v="exclamation-triangle" />
<span class="ml-1">{{ $t(validation(s.val)!) }}</span>
</p>
</template>
</ListInput>
</template>
<style lang="scss" scoped>
.hanging {
position: absolute;
top: 100%;
left: 0;
width: 100%;
max-width: 500px;
z-index: 5000;
}
</style>