(ts) migrate sources components to composition API with typescript

This commit is contained in:
Valentyne Stigloher 2024-10-31 23:13:13 +01:00
parent 50a9c7afbe
commit 0b602028ee
7 changed files with 226 additions and 285 deletions

View File

@ -62,7 +62,7 @@
<p><strong><T>sources.referenced</T><T>quotation.colon</T></strong></p>
<ul class="list-unstyled">
<li v-for="source in s.el.sourcesData">
<Source :source="source" />
<SourceItem :source="source" />
</li>
</ul>
</div>

View File

@ -1,3 +1,24 @@
<script setup lang="ts">
import { getPronoun } from '~/src/buildPronoun.ts';
import type { Pronoun, Source } from '~/src/classes.ts';
import { pronouns } from '~/src/data.ts';
const props = defineProps<{
pronoun?: Pronoun;
sources: Record<string, Source[] | undefined>;
}>();
const { $translator: translator } = useNuxtApp();
const glue = ` ${translator.translate('pronouns.or')} `;
const visibleSources = computed((): Record<string, Source[]> => {
return Object.fromEntries(
Object.entries(props.sources).filter(([_, sources]) => sources),
) as Record<string, Source[]>;
});
</script>
<template>
<div v-if="Object.keys(visibleSources).length">
<h2 class="h4">
@ -25,33 +46,3 @@
</section>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import type { PropType } from 'vue';
import { getPronoun } from '../src/buildPronoun.ts';
import type { Source } from '../src/classes.ts';
import { pronouns } from '../src/data.ts';
export default defineComponent({
props: {
pronoun: { },
sources: { required: true, type: Object as PropType<Record<string, Source[] | undefined>> },
},
data() {
return {
pronouns,
getPronoun,
glue: ` ${this.$t('pronouns.or')} `,
};
},
computed: {
visibleSources(): Record<string, Source[]> {
return Object.fromEntries(
Object.entries(this.sources).filter(([_, sources]) => sources),
) as Record<string, Source[]>;
},
},
});
</script>

View File

@ -1,3 +1,48 @@
<script setup lang="ts">
import { LazyHydrationWrapper } from 'vue3-lazy-hydration';
import type { Source } from '~/src/classes.ts';
import { pronounLibrary } from '~/src/data.ts';
const props = defineProps<{
source: Source;
manage?: boolean;
}>();
const emit = defineEmits<{
'edit-source': [Source];
}>();
const { $translator: translator } = useNuxtApp();
const config = useConfig();
const dialogue = useDialogue();
const deleted = ref(false);
const versionsShown = ref(false);
const showSpoiler = ref(false);
const approve = async () => {
await dialogue.postWithAlertOnError(`/api/sources/approve/${props.source.id}`);
props.source.approved = true;
props.source.base_id = null;
};
const hide = async () => {
await dialogue.postWithAlertOnError(`/api/sources/hide/${props.source.id}`);
props.source.approved = false;
};
const remove = async () => {
await dialogue.confirm(translator.translate('crud.removeConfirm'), 'danger');
await dialogue.postWithAlertOnError(`/api/sources/remove/${props.source.id}`);
deleted.value = true;
};
const addMarks = (t: string) => {
return t.replace(/\[\[/g, '<mark>').replace(/]]/g, '</mark>');
};
</script>
<template>
<LazyHydrationWrapper when-visible>
<div v-if="!deleted" class="my-2 clearfix">
@ -44,7 +89,7 @@
</a>
</li>
<li class="list-inline-item">
<a href="#" class="badge bg-light text-dark border border-primary btn-sm m-1" @click.prevent="$emit('edit-source', source)">
<a href="#" class="badge bg-light text-dark border border-primary btn-sm m-1" @click.prevent="emit('edit-source', source)">
<Icon v="pen" />
<span class="btn-label">
<T>crud.edit</T>
@ -101,7 +146,7 @@
>{{ $locales[version.locale].name }}</a>:
</strong>
</h4>
<Source :source="version" />
<SourceItem :source="version" />
</li>
</template>
</ul>
@ -110,60 +155,6 @@
</LazyHydrationWrapper>
</template>
<script>
import { LazyHydrationWrapper } from 'vue3-lazy-hydration';
import useConfig from '../composables/useConfig.ts';
import useDialogue from '../composables/useDialogue.ts';
import { pronounLibrary } from '../src/data.ts';
export default {
components: { LazyHydrationWrapper },
props: {
source: { required: true },
manage: { type: Boolean },
},
emits: ['edit-source'],
setup() {
return {
config: useConfig(),
dialogue: useDialogue(),
};
},
data() {
return {
pronounLibrary,
deleted: false,
versionsShown: false,
showSpoiler: false,
};
},
methods: {
async approve() {
await this.dialogue.postWithAlertOnError(`/api/sources/approve/${this.source.id}`);
this.source.approved = true;
this.source.base = null;
this.$forceUpdate();
},
async hide() {
await this.dialogue.postWithAlertOnError(`/api/sources/hide/${this.source.id}`);
this.source.approved = false;
this.$forceUpdate();
},
async remove() {
await this.dialogue.confirm(this.$t('crud.removeConfirm'), 'danger');
await this.dialogue.postWithAlertOnError(`/api/sources/remove/${this.source.id}`);
this.deleted = true;
this.$forceUpdate();
},
addMarks(t) {
return t.replace(/\[\[/g, '<mark>').replace(/]]/g, '</mark>');
},
},
};
</script>
<style lang="scss" scoped>
@import "assets/variables";

View File

@ -1,3 +1,42 @@
<script setup lang="ts">
import type { Source, Filter, Pronoun } from '~/src/classes.ts';
import { makeId } from '~/src/helpers.ts';
const props = defineProps<{
sources: Source[];
pronoun?: Pronoun;
filter?: Filter;
manage?: boolean;
}>();
const emit = defineEmits< {
'edit-source': [Source];
}>();
const sourcesUnique = computed(() => {
const sourcesMap: Record<string, Source> = {};
for (const source of props.sources) {
sourcesMap[source.id] = source;
}
return Object.values(sourcesMap);
});
const edit = (source: Source) => {
// TODO it should be possible to do it nicer
emit('edit-source', source);
};
const visibleSources = computed(() => {
return sourcesUnique.value.filter((source) => !props.filter || source.matches(props.filter));
});
const notEmpty = computed(() => {
return visibleSources.value.length > 0;
});
const listId = makeId(6);
</script>
<template>
<div v-if="notEmpty">
<slot></slot>
@ -8,59 +47,8 @@
<SourcesChart :sources="sources" :label="pronoun ? pronoun.name() : ''" />
<ul class="list-unstyled">
<li v-for="source in visibleSources" :key="`${source.id}-${listId}`" class="my-2 clearfix">
<Source :source="source" :manage="manage" @edit-source="edit" />
<SourceItem :source="source" :manage="manage" @edit-source="edit" />
</li>
</ul>
</div>
</template>
<script>
import { makeId } from '../src/helpers.ts';
export default {
props: {
sources: { required: true },
pronoun: { },
filter: { default: '' },
filterType: { default: '' },
manage: { type: Boolean },
},
emits: ['edit-source'],
data() {
const sourcesMap = {};
for (const source of this.sources) {
sourcesMap[source.id] = source;
}
return {
listId: makeId(6),
sourcesUnique: Object.values(sourcesMap),
};
},
computed: {
visibleSources() {
return this.sourcesUnique.filter(this.isVisible);
},
notEmpty() {
return this.visibleSources.length > 0;
},
},
methods: {
isVisible(source) {
if (this.filterType && this.filterType !== source.type) {
return false;
}
if (this.filter) {
return source.index.includes(this.filter.toLowerCase());
}
return true;
},
edit(source) {
// TODO it should be possible to do it nicer
this.$emit('edit-source', source);
},
},
};
</script>

View File

@ -1,3 +1,47 @@
<script setup lang="ts">
import type { SourceRaw, Source } from '~/src/classes.ts';
const props = defineProps<{
sources: (SourceRaw | Source)[] | null;
label: string;
}>();
const open = ref(false);
const publishDates = computed(() => {
if (props.sources === null) {
return null;
}
const dates: Record<number, number> = {};
let count = 0;
let min, max;
for (const source of props.sources) {
if (source.year) {
if (dates[source.year] === undefined) {
dates[source.year] = 0;
}
dates[source.year]++;
count++;
if (min === undefined || source.year < min) {
min = source.year;
}
if (max === undefined || source.year > max) {
max = source.year;
}
}
}
if (Object.keys(dates).length < 2 || count < 5 || min === undefined || max === undefined) {
return null;
}
for (let i = min + 1; i < max; i++) {
if (dates[i] === undefined) {
dates[i] = 0;
}
}
return dates;
});
</script>
<template>
<div v-if="publishDates !== null && $isGranted('sources')" class="card">
<a class="card-header cursor-pointer" href="#" @click.prevent="open = !open">
@ -14,51 +58,3 @@
</div>
</div>
</template>
<script>
export default {
props: {
sources: { required: true },
label: { required: true },
},
data() {
return {
open: false,
};
},
computed: {
publishDates() {
if (this.sources === null) {
return null;
}
const dates = {};
let count = 0;
let min, max;
for (const source of this.sources) {
if (source.year) {
if (dates[source.year] === undefined) {
dates[source.year] = 0;
}
dates[source.year]++;
count++;
if (min === undefined || source.year < min) {
min = source.year;
}
if (max === undefined || source.year > max) {
max = source.year;
}
}
}
if (Object.keys(dates).length < 2 || count < 5) {
return null;
}
for (let i = min + 1; i < max; i++) {
if (dates[i] === undefined) {
dates[i] = 0;
}
}
return dates;
},
},
};
</script>

View File

@ -1,3 +1,68 @@
<script setup lang="ts">
import type SourceSubmitForm from '~/components/SourceSubmitForm.vue';
import { Source, SourceLibrary } from '~/src/classes.ts';
import type { SourceRaw } from '~/src/classes.ts';
import { pronouns, pronounLibrary } from '~/src/data.ts';
definePageMeta({
translatedPaths: (config) => translatedPathByConfigModule(config.sources),
});
const { $translator: translator } = useNuxtApp();
const config = useConfig();
useSimpleHead({
title: translator.translate('sources.headerLonger'),
description: translator.translate('sources.subheader'),
}, translator);
const filter = useFilterWithCategory();
const { data: sources } = await useFetch<SourceRaw[]>('/api/sources', { lazy: true });
const sourceLibrary = computed(() => {
if (sources.value === null) {
return undefined;
}
return new SourceLibrary(config, sources.value);
});
const tocShown = ref(false);
const glue = ` ${translator.translate('pronouns.or')} `;
const submitShown = ref(false);
const tocPronounGroups = computed(() => {
const pronounGroups = pronounLibrary.split((pronoun) => {
if (sourceLibrary.value === undefined) {
return false;
}
return sourceLibrary.value.getForPronoun(pronoun.canonicalName).length > 0;
}, false);
return [...pronounGroups].filter(([_, groupPronouns]) => groupPronouns.length > 0);
});
const categories = computed(() => {
return Object.entries(Source.TYPES)
.filter(([type]) => type)
.map(([type, icon]) => ({
key: type,
text: translator.translate(`sources.type.${type}`),
icon,
}));
});
const toId = (str: string): string => {
return str.replace(/\//g, '-').replace(/&/g, '_');
};
const form = useTemplateRef<InstanceType<typeof SourceSubmitForm>>('form');
const edit = (source: Source) => {
submitShown.value = true;
nextTick(() => {
form.value?.edit(source);
});
};
</script>
<template>
<Page v-if="config.sources.enabled">
<h2>
@ -96,8 +161,7 @@
<SourceList
:sources="sourceLibrary.getForPronoun(pronoun.canonicalName)"
:pronoun="pronoun"
:filter="filter.text"
:filter-type="filter.category"
:filter="filter"
manage
@edit-source="edit"
>
@ -116,8 +180,7 @@
<section v-if="sourceLibrary.getForPronoun(multiple).length">
<SourceList
:sources="sourceLibrary.getForPronoun(multiple)"
:filter="filter.text"
:filter-type="filter.category"
:filter="filter"
manage
@edit-source="edit"
>
@ -134,8 +197,7 @@
<section v-if="sourceLibrary.getForPronoun('', pronounLibrary)">
<SourceList
:sources="sourceLibrary.getForPronoun('', pronounLibrary)"
:filter="filter.text"
:filter-type="filter.category"
:filter="filter"
manage
@edit-source="edit"
>
@ -150,95 +212,3 @@
<AdPlaceholder :phkey="['content-1', 'content-mobile-1']" />
</Page>
</template>
<script lang="ts">
import { useNuxtApp, useFetch } from 'nuxt/app';
import { defineComponent, computed } from 'vue';
import type SourceSubmitForm from '../components/SourceSubmitForm.vue';
import useConfig from '../composables/useConfig.ts';
import useSimpleHead from '../composables/useSimpleHead.ts';
import { Source, SourceLibrary } from '../src/classes.ts';
import type { SourceRaw } from '../src/classes.ts';
import { pronouns, pronounLibrary } from '../src/data.ts';
interface Refs {
form: InstanceType<typeof SourceSubmitForm> | undefined;
}
export default defineComponent({
async setup() {
definePageMeta({
translatedPaths: (config) => translatedPathByConfigModule(config.sources),
});
const { $translator: translator } = useNuxtApp();
const config = useConfig();
useSimpleHead({
title: translator.translate('sources.headerLonger'),
description: translator.translate('sources.subheader'),
}, translator);
const filter = useFilterWithCategory();
const { data: sources } = await useFetch<SourceRaw[]>('/api/sources', { lazy: true });
const sourceLibrary = computed(() => {
if (sources.value === null) {
return undefined;
}
return new SourceLibrary(config, sources.value);
});
return {
config,
filter,
sources,
sourceLibrary,
};
},
data() {
return {
pronouns,
pronounLibrary,
tocShown: false,
sourceTypes: Source.TYPES,
glue: ` ${this.$t('pronouns.or')} `,
submitShown: false,
};
},
computed: {
$tRefs(): Refs {
return this.$refs as unknown as Refs;
},
tocPronounGroups() {
const pronounGroups = this.pronounLibrary.split((pronoun) => {
if (this.sourceLibrary === undefined) {
return false;
}
return this.sourceLibrary.getForPronoun(pronoun.canonicalName).length > 0;
}, false);
return [...pronounGroups].filter(([_, groupPronouns]) => groupPronouns.length > 0);
},
categories() {
return Object.entries(Source.TYPES)
.filter(([type]) => type)
.map(([type, icon]) => ({
key: type,
text: this.$t(`sources.type.${type}`),
icon,
}));
},
},
methods: {
toId(str: string): string {
return str.replace(/\//g, '-').replace(/&/g, '_');
},
edit(source: Source) {
this.submitShown = true;
this.$nextTick(() => {
this.$tRefs.form?.edit(source);
});
},
},
});
</script>

View File

@ -273,6 +273,11 @@ export class Source {
icon(): string {
return Source.TYPES[this.type];
}
matches(filter: Filter) {
return (!filter.text || !!this.index?.includes(filter.text.toLowerCase())) &&
(!filter.category || this.type === filter.category);
}
}
export class SourceLibrary {