mirror of
https://gitlab.com/PronounsPage/PronounsPage.git
synced 2025-09-25 05:54:25 -04:00
Merge branch 'nouns-nb-p' into 'main'
nouns nonbinary column and responsive design See merge request PronounsPage/PronounsPage!541
This commit is contained in:
commit
459c1629fb
@ -262,6 +262,30 @@ form[inert] {
|
||||
}
|
||||
}
|
||||
|
||||
.btn-concise {
|
||||
white-space: nowrap;
|
||||
}
|
||||
@include media-breakpoint-up('md', $grid-breakpoints) {
|
||||
.list-btn-concise {
|
||||
min-width: 3rem;
|
||||
|
||||
li {
|
||||
height: 2.5rem;
|
||||
}
|
||||
}
|
||||
.btn-concise {
|
||||
position: absolute;
|
||||
|
||||
.btn-label {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&:hover .btn-label {
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.graph {
|
||||
width: 100%;
|
||||
height: 400px;
|
||||
|
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<Table :data="abuseReports" :columns="abuseReports.length && abuseReports[0].snapshot ? 5 : 4">
|
||||
<Table :data="abuseReports">
|
||||
<template #header>
|
||||
<div class="bold text-nowrap">
|
||||
Suspicious account
|
||||
|
@ -106,7 +106,7 @@ const remove = async (entry: InclusiveEntry): Promise<void> => {
|
||||
@submit-clicked="form?.focus()"
|
||||
/>
|
||||
|
||||
<Table ref="dictionarytable" :data="visibleEntries" :columns="3" :marked="(el) => !el.approved" fixed>
|
||||
<Table ref="dictionarytable" :data="visibleEntries" :marked="(el) => !el.approved" fixed>
|
||||
<template #header>
|
||||
<div class="bold text-nowrap">
|
||||
<Icon v="comment-times" />
|
||||
@ -369,28 +369,4 @@ const remove = async (entry: InclusiveEntry): Promise<void> => {
|
||||
/ 1fr 1fr 2.5fr 3em;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-concise {
|
||||
white-space: nowrap;
|
||||
}
|
||||
@include media-breakpoint-up('md', $grid-breakpoints) {
|
||||
.list-btn-concise {
|
||||
min-width: 3rem;
|
||||
|
||||
li {
|
||||
height: 2.5rem;
|
||||
}
|
||||
}
|
||||
.btn-concise {
|
||||
position: absolute;
|
||||
|
||||
.btn-label {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&:hover .btn-label {
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -11,7 +11,6 @@ type VPage = {
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
data: T[];
|
||||
columns: number;
|
||||
perPage?: number;
|
||||
marked?: (element: T) => boolean;
|
||||
fixed?: boolean;
|
||||
|
@ -128,7 +128,7 @@ const year = buildCalendar(runtimeConfig.public.baseUrl).getCurrentYear()!;
|
||||
@submit-clicked="form?.focus()"
|
||||
/>
|
||||
|
||||
<Table ref="dictionarytable" :data="visibleEntries" :columns="1" fixed :marked="(el) => !el.approved">
|
||||
<Table ref="dictionarytable" :data="visibleEntries" fixed :marked="(el) => !el.approved">
|
||||
<template #row="s">
|
||||
<template v-if="s">
|
||||
<div>
|
||||
@ -231,29 +231,6 @@ const year = buildCalendar(runtimeConfig.public.baseUrl).getCurrentYear()!;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-concise {
|
||||
white-space: nowrap;
|
||||
}
|
||||
@include media-breakpoint-up('md', $grid-breakpoints) {
|
||||
.list-btn-concise {
|
||||
min-width: 3rem;
|
||||
|
||||
li {
|
||||
height: 2.5rem;
|
||||
}
|
||||
}
|
||||
.btn-concise {
|
||||
position: absolute;
|
||||
|
||||
.btn-label {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&:hover .btn-label {
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
}
|
||||
@include media-breakpoint-down('md', $grid-breakpoints) {
|
||||
.cell-wide {
|
||||
min-width: 90vw;
|
||||
|
@ -1,9 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import type { ComponentExposed } from 'vue-component-type-helpers';
|
||||
|
||||
import type Table from '~/components/Table.vue';
|
||||
import type NounsSubmitForm from '~/components/nouns/NounsSubmitForm.vue';
|
||||
import { Noun, genders } from '~/src/classes.ts';
|
||||
import { Noun } from '~/src/classes.ts';
|
||||
import type { NounRaw } from '~/src/classes.ts';
|
||||
import { buildDict } from '~/src/helpers.ts';
|
||||
|
||||
@ -15,7 +12,7 @@ const { $translator: translator } = useNuxtApp();
|
||||
const config = useConfig();
|
||||
const filter = useFilterWithCategory();
|
||||
|
||||
const dictionarytable = useTemplateRef<ComponentExposed<typeof Table>>('dictionarytable');
|
||||
const dictionarytable = useTemplateRef('dictionarytable');
|
||||
watch(filter, () => {
|
||||
if (dictionarytable.value) {
|
||||
dictionarytable.value.reset();
|
||||
@ -113,84 +110,71 @@ defineExpose({ loadNouns });
|
||||
@submit-clicked="form?.focus()"
|
||||
/>
|
||||
|
||||
<Table ref="dictionarytable" :data="visibleNouns" :columns="3" :marked="(el) => !el.approved" fixed>
|
||||
<template #header>
|
||||
<div v-for="gender in genders" :key="gender" class="d-none d-md-block bold">
|
||||
<NounsGenderLabel :gender="gender" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #row="{ el: noun }">
|
||||
<NounsDictionaryEntry
|
||||
:noun="noun"
|
||||
:base="noun.base && nouns[noun.base] ? nouns[noun.base] : undefined"
|
||||
>
|
||||
<template #buttons>
|
||||
<ul class="d-flex flex-wrap flex-md-column list-unstyled list-btn-concise mb-0">
|
||||
<template v-if="$isGranted('nouns')">
|
||||
<li v-if="noun.author" class="small">
|
||||
<nuxt-link
|
||||
:to="`/@${noun.author}`"
|
||||
class="btn btn-concise btn-outline-dark btn-sm m-1"
|
||||
>
|
||||
<Icon v="user" />
|
||||
<span class="btn-label">
|
||||
<T>crud.author</T><T>quotation.colon</T>
|
||||
@{{ noun.author }}
|
||||
</span>
|
||||
</nuxt-link>
|
||||
</li>
|
||||
<li v-if="!noun.approved">
|
||||
<button class="btn btn-concise btn-success btn-sm m-1" @click="approve(noun)">
|
||||
<Icon v="check" />
|
||||
<span class="btn-label"><T>crud.approve</T></span>
|
||||
</button>
|
||||
</li>
|
||||
<li v-else @click="hide(noun)">
|
||||
<button class="btn btn-concise btn-outline-secondary btn-sm m-1">
|
||||
<Icon v="times" />
|
||||
<span class="btn-label"><T>crud.hide</T></span>
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button class="btn btn-concise btn-outline-danger btn-sm m-1" @click="remove(noun)">
|
||||
<Icon v="trash" />
|
||||
<span class="btn-label"><T>crud.remove</T></span>
|
||||
</button>
|
||||
</li>
|
||||
</template>
|
||||
<li>
|
||||
<button class="btn btn-concise btn-outline-primary btn-sm m-1" @click="edit(noun)">
|
||||
<Icon v="pen" />
|
||||
<span class="btn-label">
|
||||
<T v-if="$isGranted('nouns')">crud.edit</T>
|
||||
<T v-else>nouns.edit</T>
|
||||
</span>
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
:href="`/api/nouns/${noun.id}.png`"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
class="btn btn-concise btn-outline-primary btn-sm m-1"
|
||||
>
|
||||
<Icon v="image" />
|
||||
<span class="btn-label">
|
||||
<T>nouns.image</T>
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<NounsTable
|
||||
ref="dictionarytable"
|
||||
:class="[config.nouns.nonbinary ? 'nouns-table-nonbinary' : '']"
|
||||
:nouns="visibleNouns"
|
||||
:marked="(el) => !el.approved"
|
||||
>
|
||||
<template #buttons="{ noun }">
|
||||
<ul class="d-flex flex-wrap flex-md-column list-unstyled list-btn-concise mb-0">
|
||||
<template v-if="$isGranted('nouns')">
|
||||
<li v-if="noun.author" class="small">
|
||||
<nuxt-link
|
||||
:to="`/@${noun.author}`"
|
||||
class="btn btn-concise btn-outline-dark btn-sm m-1"
|
||||
>
|
||||
<Icon v="user" />
|
||||
<span class="btn-label">
|
||||
<T>crud.author</T><T>quotation.colon</T>
|
||||
@{{ noun.author }}
|
||||
</span>
|
||||
</nuxt-link>
|
||||
</li>
|
||||
<li v-if="!noun.approved">
|
||||
<button class="btn btn-concise btn-success btn-sm m-1" @click="approve(noun)">
|
||||
<Icon v="check" />
|
||||
<span class="btn-label"><T>crud.approve</T></span>
|
||||
</button>
|
||||
</li>
|
||||
<li v-else @click="hide(noun)">
|
||||
<button class="btn btn-concise btn-outline-secondary btn-sm m-1">
|
||||
<Icon v="times" />
|
||||
<span class="btn-label"><T>crud.hide</T></span>
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button class="btn btn-concise btn-outline-danger btn-sm m-1" @click="remove(noun)">
|
||||
<Icon v="trash" />
|
||||
<span class="btn-label"><T>crud.remove</T></span>
|
||||
</button>
|
||||
</li>
|
||||
</template>
|
||||
</NounsDictionaryEntry>
|
||||
<li>
|
||||
<button class="btn btn-concise btn-outline-primary btn-sm m-1" @click="edit(noun)">
|
||||
<Icon v="pen" />
|
||||
<span class="btn-label">
|
||||
<T v-if="$isGranted('nouns')">crud.edit</T>
|
||||
<T v-else>nouns.edit</T>
|
||||
</span>
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
:href="`/api/nouns/${noun.id}.png`"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
class="btn btn-concise btn-outline-primary btn-sm m-1"
|
||||
>
|
||||
<Icon v="image" />
|
||||
<span class="btn-label">
|
||||
<T>nouns.image</T>
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<template #empty>
|
||||
<Icon v="search" />
|
||||
<T>nouns.empty</T>
|
||||
</template>
|
||||
</Table>
|
||||
</NounsTable>
|
||||
|
||||
<AdPlaceholder :phkey="['content-1', 'content-mobile-1']" />
|
||||
|
||||
@ -201,53 +185,3 @@ defineExpose({ loadNouns });
|
||||
</template>
|
||||
</Loading>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "assets/variables";
|
||||
|
||||
:deep(.row-header) {
|
||||
grid-template-columns: 1fr 1fr 1fr 3em;
|
||||
}
|
||||
|
||||
:deep(.row-content) {
|
||||
grid:
|
||||
"mascLabel masc mascPl"
|
||||
"femLabel fem femPl"
|
||||
"neutrLabel neutr neutrPl"
|
||||
"sources sources sources"
|
||||
"buttons buttons buttons"
|
||||
/ auto 1fr 1fr;
|
||||
|
||||
@include media-breakpoint-up('md', $grid-breakpoints) {
|
||||
grid:
|
||||
"masc fem neutr buttons"
|
||||
"mascPl femPl neutrPl buttons"
|
||||
"sources sources sources buttons"
|
||||
/ 1fr 1fr 1fr 3em;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-concise {
|
||||
white-space: nowrap;
|
||||
}
|
||||
@include media-breakpoint-up('md', $grid-breakpoints) {
|
||||
.list-btn-concise {
|
||||
min-width: 3rem;
|
||||
|
||||
li {
|
||||
height: 2.5rem;
|
||||
}
|
||||
}
|
||||
.btn-concise {
|
||||
position: absolute;
|
||||
|
||||
.btn-label {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&:hover .btn-label {
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -1,26 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
import type { genders } from '~/src/classes.ts';
|
||||
import { type Gender, iconNamesByGender, longIdentifierByGender } from '~/src/nouns.ts';
|
||||
|
||||
const props = defineProps<{
|
||||
gender: typeof genders[number];
|
||||
gender: Gender;
|
||||
concise?: boolean;
|
||||
}>();
|
||||
|
||||
const iconName = computed((): string => {
|
||||
const iconNames = {
|
||||
masc: 'mars',
|
||||
fem: 'venus',
|
||||
neutr: 'neuter',
|
||||
};
|
||||
return iconNames[props.gender];
|
||||
return iconNamesByGender[props.gender];
|
||||
});
|
||||
const longIdentifier = computed((): string => {
|
||||
const longIdentifiers = {
|
||||
masc: 'masculine',
|
||||
fem: 'feminine',
|
||||
neutr: 'neuter',
|
||||
};
|
||||
return longIdentifiers[props.gender];
|
||||
return longIdentifierByGender[props.gender];
|
||||
});
|
||||
</script>
|
||||
|
||||
|
@ -1,9 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import type { genders, MinimalNoun } from '~/src/classes.ts';
|
||||
import type { MinimalNoun } from '~/src/classes.ts';
|
||||
import type { Gender } from '~/src/nouns.ts';
|
||||
|
||||
defineProps<{
|
||||
noun: MinimalNoun;
|
||||
gender: typeof genders[number];
|
||||
gender: Gender;
|
||||
plural?: boolean;
|
||||
}>();
|
||||
|
||||
|
@ -1,17 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
import type { Config } from '~/locale/config.ts';
|
||||
import { genders } from '~/src/classes.ts';
|
||||
import type { Noun, MinimalNoun } from '~/src/classes.ts';
|
||||
import { abbreviations } from '~/src/data.ts';
|
||||
import { availableGenders } from '~/src/nouns.ts';
|
||||
import type { Gender } from '~/src/nouns.ts';
|
||||
|
||||
const emptyForm = (config: Config): MinimalNoun => {
|
||||
return {
|
||||
masc: [''],
|
||||
fem: [''],
|
||||
neutr: [''],
|
||||
nb: [''],
|
||||
mascPl: config.nouns.pluralsRequired ? [''] : [],
|
||||
femPl: config.nouns.pluralsRequired ? [''] : [],
|
||||
neutrPl: config.nouns.pluralsRequired ? [''] : [],
|
||||
nbPl: config.nouns.pluralsRequired ? [''] : [],
|
||||
categories: [],
|
||||
sources: [],
|
||||
base: null,
|
||||
@ -37,8 +40,8 @@ const templateBase = ref('');
|
||||
const templateFilter = ref('');
|
||||
const templateVisible = ref(false);
|
||||
|
||||
const canRemoveWord = (gender: typeof genders[number], plural: boolean): boolean => {
|
||||
return genders.filter((otherGender) => {
|
||||
const canRemoveWord = (gender: Gender, plural: boolean): boolean => {
|
||||
return availableGenders(config).filter((otherGender) => {
|
||||
return otherGender !== gender && form.value[plural ? `${otherGender}Pl` as const : otherGender].length > 0;
|
||||
}).length > 1 || form.value[plural ? `${gender}Pl` as const : gender].length > 1;
|
||||
};
|
||||
@ -73,9 +76,11 @@ const edit = (word: Noun): void => {
|
||||
masc: word.masc,
|
||||
fem: word.fem,
|
||||
neutr: word.neutr,
|
||||
nb: word.nb,
|
||||
mascPl: word.mascPl,
|
||||
femPl: word.femPl,
|
||||
neutrPl: word.neutrPl,
|
||||
nbPl: word.nbPl,
|
||||
categories: word.categories,
|
||||
sources: word.sources,
|
||||
base: word.id,
|
||||
@ -109,10 +114,10 @@ const { data: sourcesKeys } = await useFetch('/api/sources/keys', { lazy: true,
|
||||
</div>
|
||||
<form v-else @submit.prevent="submit">
|
||||
<div class="row">
|
||||
<div v-if="config.nouns.plurals" class="col-12 col-md text-nowrap mt-md-4">
|
||||
<div v-if="config.nouns.plurals" class="col-12 col-md-2 text-nowrap mt-md-4">
|
||||
<label><strong>⋅ <T>nouns.singular</T></strong></label>
|
||||
</div>
|
||||
<div v-for="gender in genders" :key="gender" class="col-6 col-sm">
|
||||
<div v-for="gender in availableGenders(config)" :key="gender" class="col-6 col-sm">
|
||||
<label><strong><NounsGenderLabel :gender="gender" /></strong></label>
|
||||
<ListInput
|
||||
v-model="form[gender]"
|
||||
@ -122,10 +127,10 @@ const { data: sourcesKeys } = await useFetch('/api/sources/keys', { lazy: true,
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="config.nouns.plurals" class="row">
|
||||
<div class="col-12 col-md text-nowrap">
|
||||
<div class="col-12 col-md-2 text-nowrap">
|
||||
<label><strong>⁖ <T>nouns.plural</T></strong></label>
|
||||
</div>
|
||||
<div v-for="gender in genders" :key="gender" class="col-6 col-sm">
|
||||
<div v-for="gender in availableGenders(config)" :key="gender" class="col-6 col-sm">
|
||||
<label class="d-md-none"><strong><NounsGenderLabel :gender="gender" /></strong></label>
|
||||
<ListInput
|
||||
v-model="form[`${gender}Pl`]"
|
||||
|
84
components/nouns/NounsTable.vue
Normal file
84
components/nouns/NounsTable.vue
Normal file
@ -0,0 +1,84 @@
|
||||
<script setup lang="ts" generic="T extends Partial<Noun> & MinimalNoun & { id: string }">
|
||||
import type { MinimalNoun, Noun } from '~/src/classes.ts';
|
||||
import { availableGenders } from '~/src/nouns.ts';
|
||||
|
||||
defineProps<{
|
||||
nouns: T[];
|
||||
marked?: (element: T) => boolean;
|
||||
}>();
|
||||
|
||||
const config = useConfig();
|
||||
|
||||
const table = useTemplateRef('table');
|
||||
defineExpose({
|
||||
reset() {
|
||||
table.value?.reset();
|
||||
},
|
||||
focus() {
|
||||
table.value?.focus();
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Table ref="table" :data="nouns" :marked fixed>
|
||||
<template #header>
|
||||
<div v-for="gender in availableGenders(config)" :key="gender" class="d-none d-md-block bold">
|
||||
<NounsGenderLabel :gender="gender" />
|
||||
</div>
|
||||
</template>
|
||||
<template #row="{ el: noun }">
|
||||
<NounsTableEntry :noun>
|
||||
<template #buttons>
|
||||
<ul class="list-unstyled list-btn-concise">
|
||||
<slot name="buttons" :noun="noun"></slot>
|
||||
</ul>
|
||||
</template>
|
||||
</NounsTableEntry>
|
||||
</template>
|
||||
|
||||
<template #empty>
|
||||
<Icon v="search" />
|
||||
<T>nouns.empty</T>
|
||||
</template>
|
||||
</Table>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "assets/variables";
|
||||
|
||||
:deep(.row-header) {
|
||||
grid-template-columns: 1fr 1fr 1fr 3em;
|
||||
|
||||
.nouns-table-nonbinary & {
|
||||
grid-template-columns: 1fr 1fr 1fr 1fr 3em;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.row-content) {
|
||||
grid:
|
||||
"mascLabel masc mascPl"
|
||||
"femLabel fem femPl"
|
||||
"neutrLabel neutr neutrPl"
|
||||
"nbLabel nb nbPl"
|
||||
"sources sources sources"
|
||||
"buttons buttons buttons"
|
||||
/ auto 1fr 1fr;
|
||||
|
||||
@include media-breakpoint-up('md', $grid-breakpoints) {
|
||||
grid:
|
||||
"masc fem neutr buttons"
|
||||
"mascPl femPl neutrPl buttons"
|
||||
"sources sources sources buttons"
|
||||
/ 1fr 1fr 1fr 3em;
|
||||
|
||||
.nouns-table-nonbinary & {
|
||||
grid:
|
||||
"masc fem neutr nb buttons"
|
||||
"mascPl femPl neutrPl nbPl buttons"
|
||||
"sources sources sources sources buttons"
|
||||
/ 1fr 1fr 1fr 1fr 3em;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,8 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { genders, type Noun } from '~/src/classes.ts';
|
||||
import type { MinimalNoun, Noun } from '~/src/classes.ts';
|
||||
import { genders } from '~/src/nouns.ts';
|
||||
|
||||
const props = defineProps<{
|
||||
noun: Noun;
|
||||
noun: Partial<Noun> & MinimalNoun;
|
||||
base?: Noun;
|
||||
}>();
|
||||
|
||||
@ -43,7 +44,7 @@ const numerus = computed(() => {
|
||||
</small>
|
||||
</div>
|
||||
</template>
|
||||
<div v-if="noun.sourcesData.length" style="grid-area: sources">
|
||||
<div v-if="noun.sourcesData?.length" style="grid-area: sources">
|
||||
<p><strong><T>sources.referenced</T><T>quotation.colon</T></strong></p>
|
||||
<ul class="list-unstyled mb-0">
|
||||
<li v-for="source in noun.sourcesData" :key="source.id">
|
@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { genders, gendersWithNumerus } from '~/src/classes.ts';
|
||||
import type { MinimalNoun } from '~/src/classes.ts';
|
||||
import { nounTemplates } from '~/src/data.ts';
|
||||
import { gendersWithNumerus } from '~/src/nouns.ts';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
templateBase?: string;
|
||||
@ -30,27 +30,11 @@ const templates = computed((): (MinimalNoun & { id: string })[] => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Table :data="templates" :columns="3" fixed>
|
||||
<template #header>
|
||||
<th v-for="gender in genders" :key="gender" class="text-nowrap">
|
||||
<NounsGenderLabel :gender="gender" />
|
||||
</th>
|
||||
<th></th>
|
||||
<NounsTable :nouns="templates">
|
||||
<template #buttons="{ noun }">
|
||||
<ul class="list-unstyled list-btn-concise">
|
||||
<slot name="buttons" :template="noun"></slot>
|
||||
</ul>
|
||||
</template>
|
||||
<template #row="{ el: template }">
|
||||
<td v-for="gender in genders" :key="gender">
|
||||
<NounsItem :noun="template" :gender="gender" />
|
||||
</td>
|
||||
<th>
|
||||
<ul class="list-unstyled list-btn-concise">
|
||||
<slot name="buttons" :template="template"></slot>
|
||||
</ul>
|
||||
</th>
|
||||
</template>
|
||||
|
||||
<template #empty>
|
||||
<Icon v="search" />
|
||||
<T>nouns.empty</T>
|
||||
</template>
|
||||
</Table>
|
||||
</NounsTable>
|
||||
</template>
|
||||
|
@ -213,6 +213,7 @@ nouns:
|
||||
feminineShort: 'fem.'
|
||||
neuter: 'neutral'
|
||||
neuterShort: 'neutr.'
|
||||
nonbinary: 'nonbinary'
|
||||
|
||||
singular: 'singular'
|
||||
singularShort: 'sing.'
|
||||
|
@ -365,6 +365,10 @@ interface NounsConfig {
|
||||
* whether the dictionary should be collapsed by default
|
||||
*/
|
||||
collapsable: boolean;
|
||||
/**
|
||||
* whether to have a nonbinary column
|
||||
*/
|
||||
nonbinary?: boolean;
|
||||
/**
|
||||
* whether nouns have plural forms
|
||||
*/
|
||||
|
@ -1,4 +1,4 @@
|
||||
import type { gendersWithNumerus } from '~/src/classes.ts';
|
||||
import type { gendersWithNumerus } from '~/src/nouns.ts';
|
||||
|
||||
export type PronounData<M extends string> = {
|
||||
key: string;
|
||||
|
@ -224,6 +224,7 @@ nouns:
|
||||
feminineShort: 'fem.'
|
||||
neuter: 'neutral'
|
||||
neuterShort: 'neutr.'
|
||||
nonbinary: 'nonbinary'
|
||||
|
||||
singular: 'singular'
|
||||
singularShort: 'sing.'
|
||||
|
@ -100,6 +100,7 @@ nouns:
|
||||
enabled: true
|
||||
route: 'substantivoj'
|
||||
collapsable: false
|
||||
nonbinary: true
|
||||
plurals: false
|
||||
pluralsRequired: false
|
||||
categories: []
|
||||
|
@ -6,8 +6,9 @@ import templates from './dukatywy.tsv';
|
||||
|
||||
import useConfig from '~/composables/useConfig.ts';
|
||||
import type { NounTemplatesData } from '~/locale/data.ts';
|
||||
import { gendersWithNumerus, Noun, NounDeclension, SourceLibrary } from '~/src/classes.ts';
|
||||
import { Noun, NounDeclension, SourceLibrary } from '~/src/classes.ts';
|
||||
import type { NounRaw, Source } from '~/src/classes.ts';
|
||||
import { gendersWithNumerus } from '~/src/nouns.ts';
|
||||
|
||||
const dukajDeclension = new NounDeclension({
|
||||
M: 'u',
|
||||
|
@ -6,8 +6,9 @@ import templates from './iksatywy.tsv';
|
||||
|
||||
import useConfig from '~/composables/useConfig.ts';
|
||||
import type { NounTemplatesData } from '~/locale/data.ts';
|
||||
import { gendersWithNumerus, Noun, NounDeclension, SourceLibrary } from '~/src/classes.ts';
|
||||
import { Noun, NounDeclension, SourceLibrary } from '~/src/classes.ts';
|
||||
import type { NounRaw, Source } from '~/src/classes.ts';
|
||||
import { gendersWithNumerus } from '~/src/nouns.ts';
|
||||
|
||||
const xDeclension = new NounDeclension({
|
||||
M: 'x',
|
||||
|
8
migrations/085-nouns-nb.sql
Normal file
8
migrations/085-nouns-nb.sql
Normal file
@ -0,0 +1,8 @@
|
||||
-- Up
|
||||
|
||||
ALTER TABLE nouns
|
||||
ADD COLUMN nb TEXT AFTER neutr NOT NULL DEFAULT '';
|
||||
ALTER TABLE nouns
|
||||
ADD COLUMN nbPl TEXT AFTER neutrPl NOT NULL DEFAULT '';
|
||||
|
||||
-- Down
|
@ -13,6 +13,8 @@ definePageMeta({
|
||||
},
|
||||
});
|
||||
|
||||
const NounsNav = useLocaleComponent('nouns', 'NounsNav');
|
||||
|
||||
const { $translator: translator } = useNuxtApp();
|
||||
useSimpleHead({
|
||||
title: translator.translate('nouns.headerLonger'),
|
||||
|
@ -16,9 +16,10 @@ import { rootDir } from '~/server/paths.ts';
|
||||
import { parsePronounGroups, parsePronouns, shortForVariant } from '~/src/buildPronoun.ts';
|
||||
import { buildCalendar } from '~/src/calendar/calendar.ts';
|
||||
import { Day } from '~/src/calendar/helpers.ts';
|
||||
import { genders, gendersWithNumerus, PronounLibrary } from '~/src/classes.ts';
|
||||
import { PronounLibrary } from '~/src/classes.ts';
|
||||
import forbidden from '~/src/forbidden.ts';
|
||||
import { clearLinkedText, buildImageUrl } from '~/src/helpers.ts';
|
||||
import { genders, gendersWithNumerus } from '~/src/nouns.ts';
|
||||
import parseMarkdown from '~/src/parseMarkdown.ts';
|
||||
import { normaliseQuery, type SearchDocument, validateQuery } from '~/src/search.ts';
|
||||
import { Translator } from '~/src/translator.ts';
|
||||
|
@ -15,6 +15,8 @@ import { registerLocaleFont } from '../localeFont.ts';
|
||||
import type { SourceRow } from './sources.ts';
|
||||
import type { UserRow } from './user.ts';
|
||||
|
||||
import { availableGenders, iconUnicodesByGender, longIdentifierByGender } from '~/src/nouns.ts';
|
||||
|
||||
const translations = loadSuml('translations') as Translations;
|
||||
|
||||
interface NounRow {
|
||||
@ -22,9 +24,11 @@ interface NounRow {
|
||||
masc: string;
|
||||
fem: string;
|
||||
neutr: string;
|
||||
nb: string;
|
||||
mascPl: string;
|
||||
femPl: string;
|
||||
neutrPl: string;
|
||||
nbPl: string;
|
||||
approved: number;
|
||||
base_id: string | null;
|
||||
locale: string;
|
||||
@ -128,7 +132,7 @@ router.get('/nouns/search/:term', handleErrorAsync(async (req, res) => {
|
||||
WHERE n.locale = ${global.config.locale}
|
||||
AND n.approved >= ${req.isGranted('nouns') ? 0 : 1}
|
||||
AND n.deleted = 0
|
||||
AND (n.masc like ${term} OR n.fem like ${term} OR n.neutr like ${term} OR n.mascPl like ${term} OR n.femPl like ${term} OR n.neutrPl like ${term})
|
||||
AND (n.masc like ${term} OR n.fem like ${term} OR n.neutr like ${term} OR n.nb like ${term} OR n.mascPl like ${term} OR n.femPl like ${term} OR n.neutrPl like ${term} OR n.nbPl like ${term})
|
||||
ORDER BY n.approved, n.masc
|
||||
`)));
|
||||
}));
|
||||
@ -140,11 +144,11 @@ router.post('/nouns/submit', handleErrorAsync(async (req, res) => {
|
||||
|
||||
const id = ulid();
|
||||
await req.db.get(SQL`
|
||||
INSERT INTO nouns (id, masc, fem, neutr, mascPl, femPl, neutrPl, categories, sources, approved, base_id, locale, author_id)
|
||||
INSERT INTO nouns (id, masc, fem, neutr, nb, mascPl, femPl, neutrPl, nbPl, categories, sources, approved, base_id, locale, author_id)
|
||||
VALUES (
|
||||
${id},
|
||||
${req.body.masc.join('|')}, ${req.body.fem.join('|')}, ${req.body.neutr.join('|')},
|
||||
${req.body.mascPl.join('|')}, ${req.body.femPl.join('|')}, ${req.body.neutrPl.join('|')},
|
||||
${req.body.masc.join('|')}, ${req.body.fem.join('|')}, ${req.body.neutr.join('|')}, ${req.body.nb.join('|')},
|
||||
${req.body.mascPl.join('|')}, ${req.body.femPl.join('|')}, ${req.body.neutrPl.join('|')}, ${req.body.nbPl.join('|')},
|
||||
${req.body.categories.join('|')},
|
||||
${req.body.sources ? req.body.sources.join(',') : null},
|
||||
0, ${req.body.base}, ${global.config.locale}, ${req.user ? req.user.id : null}
|
||||
@ -223,8 +227,10 @@ router.get('/nouns/:id.png', async (req, res) => {
|
||||
return res.status(404).json({ error: 'Not found' });
|
||||
}
|
||||
|
||||
const genders = availableGenders(global.config);
|
||||
|
||||
let maxItems = 0;
|
||||
(['masc', 'fem', 'neutr'] as const).forEach((form) => {
|
||||
genders.forEach((form) => {
|
||||
let items = 0;
|
||||
for (const key of ['', 'Pl'] as const) {
|
||||
items += noun[`${form}${key}`].split('|').filter((x) => x.length).length;
|
||||
@ -235,7 +241,7 @@ router.get('/nouns/:id.png', async (req, res) => {
|
||||
});
|
||||
|
||||
const padding = 48;
|
||||
const width = 1200;
|
||||
const width = genders.length * 400;
|
||||
const height = padding * 2.5 + (maxItems + 1) * 48 + padding;
|
||||
const mime = 'image/png';
|
||||
|
||||
@ -250,21 +256,22 @@ router.get('/nouns/:id.png', async (req, res) => {
|
||||
|
||||
context.font = `bold 64pt '${fontName}'`;
|
||||
|
||||
for (const [column, key, icon] of [[0, 'masculine', '\uf222'], [1, 'feminine', '\uf221'], [2, 'neuter', '\uf22c']] as const) {
|
||||
genders.forEach((gender, column) => {
|
||||
context.font = 'regular 24pt FontAwesome';
|
||||
context.fillText(icon, column * (width - 2 * padding) / 3 + padding, padding * 1.5);
|
||||
context.fillText(iconUnicodesByGender[gender], column * (width - 2 * padding) / genders.length + padding, padding * 1.5);
|
||||
|
||||
context.font = `bold 24pt '${fontName}'`;
|
||||
context.fillText(translations.nouns[key], column * (width - 2 * padding) / 3 + padding + 36, padding * 1.5);
|
||||
}
|
||||
const header = translations.nouns[longIdentifierByGender[gender]];
|
||||
context.fillText(header, column * (width - 2 * padding) / genders.length + padding + 36, padding * 1.5);
|
||||
});
|
||||
|
||||
context.font = `regular 24pt '${fontName}'`;
|
||||
(['masc', 'fem', 'neutr'] as const).forEach((form, column) => {
|
||||
genders.forEach((form, column) => {
|
||||
let i = 0;
|
||||
for (const [key, symbol] of [['', '⋅'], ['Pl', '⁖']] as const) {
|
||||
noun[`${form}${key}`].split('|').filter((x) => x.length)
|
||||
.forEach((part) => {
|
||||
context.fillText(`${symbol} ${part}`, column * (width - 2 * padding) / 3 + padding, padding * 2.5 + i * 48);
|
||||
context.fillText(`${symbol} ${part}`, column * (width - 2 * padding) / genders.length + padding, padding * 2.5 + i * 48);
|
||||
i++;
|
||||
});
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ import { buildDict, buildList, capitalise, escapeControlSymbols, escapePronuncia
|
||||
import type { Translator } from './translator.ts';
|
||||
|
||||
import type { NounTemplatesData } from '~/locale/data.ts';
|
||||
import { gendersWithNumerus } from '~/src/nouns.ts';
|
||||
|
||||
export class ExamplePart {
|
||||
variable: boolean;
|
||||
@ -935,9 +936,11 @@ export interface NounRaw {
|
||||
masc: string;
|
||||
fem: string;
|
||||
neutr: string;
|
||||
nb?: string;
|
||||
mascPl: string;
|
||||
femPl: string;
|
||||
neutrPl: string;
|
||||
nbPl?: string;
|
||||
categories?: string | null;
|
||||
sources?: string | null;
|
||||
sourcesData?: SourceRaw[];
|
||||
@ -947,17 +950,16 @@ export interface NounRaw {
|
||||
declension?: NounDeclension | null;
|
||||
}
|
||||
|
||||
export const genders = ['masc', 'fem', 'neutr'] as const;
|
||||
export const gendersWithNumerus = ['masc', 'fem', 'neutr', 'mascPl', 'femPl', 'neutrPl'] as const;
|
||||
|
||||
export class Noun implements Entry {
|
||||
id: string;
|
||||
masc: string[];
|
||||
fem: string[];
|
||||
neutr: string[];
|
||||
nb: string[];
|
||||
mascPl: string[];
|
||||
femPl: string[];
|
||||
neutrPl: string[];
|
||||
nbPl: string[];
|
||||
categories: string[];
|
||||
sources: string[];
|
||||
sourcesData: Source[];
|
||||
@ -967,16 +969,19 @@ export class Noun implements Entry {
|
||||
declension: NounDeclension | null;
|
||||
|
||||
constructor(config: Config, {
|
||||
id, masc, fem, neutr, mascPl, femPl, neutrPl, categories = null, sources = null, sourcesData = [],
|
||||
id, masc, fem, neutr, nb = '', mascPl, femPl, neutrPl, nbPl = '',
|
||||
categories = null, sources = null, sourcesData = [],
|
||||
approved = true, base_id = null, author = null, declension = null,
|
||||
}: NounRaw) {
|
||||
this.id = id;
|
||||
this.masc = masc ? masc.split('|') : [];
|
||||
this.fem = fem ? fem.split('|') : [];
|
||||
this.neutr = neutr ? neutr.split('|') : [];
|
||||
this.nb = nb ? nb.split('|') : [];
|
||||
this.mascPl = mascPl ? mascPl.split('|') : [];
|
||||
this.femPl = femPl ? femPl.split('|') : [];
|
||||
this.neutrPl = neutrPl ? neutrPl.split('|') : [];
|
||||
this.nbPl = nbPl ? nbPl.split('|') : [];
|
||||
this.categories = categories?.split('|') ?? [];
|
||||
this.sources = sources ? sources.split(',') : [];
|
||||
this.sourcesData = sourcesData.filter((s) => !!s).map((s) => new Source(config, s));
|
||||
@ -1032,17 +1037,21 @@ export class NounTemplate {
|
||||
masc: string[];
|
||||
fem: string[];
|
||||
neutr: string[];
|
||||
nb: string[];
|
||||
mascPl: string[];
|
||||
femPl: string[];
|
||||
neutrPl: string[];
|
||||
nbPl: string[];
|
||||
|
||||
constructor(masc: string[], fem: string[], neutr: string[], mascPl: string[], femPl: string[], neutrPl: string[]) {
|
||||
constructor(masc: string[], fem: string[], neutr: string[], nb: string[], mascPl: string[], femPl: string[], neutrPl: string[], nbPl: string[]) {
|
||||
this.masc = masc;
|
||||
this.fem = fem;
|
||||
this.neutr = neutr;
|
||||
this.nb = nb;
|
||||
this.mascPl = mascPl;
|
||||
this.femPl = femPl;
|
||||
this.neutrPl = neutrPl;
|
||||
this.nbPl = nbPl;
|
||||
}
|
||||
|
||||
static from(data: NounTemplatesData): NounTemplate {
|
||||
@ -1050,9 +1059,11 @@ export class NounTemplate {
|
||||
data.masc?.split('/') ?? [],
|
||||
data.fem?.split('/') ?? [],
|
||||
data.neutr?.split('/') ?? [],
|
||||
data.nb?.split('/') ?? [],
|
||||
data.mascPl?.split('/') ?? [],
|
||||
data.femPl?.split('/') ?? [],
|
||||
data.neutrPl?.split('/') ?? [],
|
||||
data.nbPl?.split('/') ?? [],
|
||||
);
|
||||
}
|
||||
|
||||
@ -1061,9 +1072,11 @@ export class NounTemplate {
|
||||
masc: this.masc.map((e) => e.replace('-', stem)),
|
||||
fem: this.fem.map((e) => e.replace('-', stem)),
|
||||
neutr: this.neutr.map((e) => e.replace('-', stem)),
|
||||
nb: this.nb.map((e) => e.replace('-', stem)),
|
||||
mascPl: this.mascPl.map((e) => e.replace('-', stem)),
|
||||
femPl: this.femPl.map((e) => e.replace('-', stem)),
|
||||
neutrPl: this.neutrPl.map((e) => e.replace('-', stem)),
|
||||
nbPl: this.nbPl.map((e) => e.replace('-', stem)),
|
||||
categories: [],
|
||||
sources: [],
|
||||
base: null,
|
||||
|
@ -146,6 +146,10 @@ export function listMissingTranslations(
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!config.nouns.nonbinary && keyMatches('nouns.nonbinary')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (config.nouns.categories?.length === 0 && keyMatches('nouns.categories')) {
|
||||
return false;
|
||||
}
|
||||
|
29
src/nouns.ts
Normal file
29
src/nouns.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import type { Config } from '~/locale/config.ts';
|
||||
|
||||
export const genders = ['masc', 'fem', 'neutr', 'nb'] as const;
|
||||
export type Gender = typeof genders[number];
|
||||
|
||||
export const gendersWithNumerus = ['masc', 'fem', 'neutr', 'nb', 'mascPl', 'femPl', 'neutrPl', 'nbPl'] as const;
|
||||
|
||||
export const availableGenders = (config: Config): Gender[] => {
|
||||
return genders.filter((gender) => gender !== 'nb' || config.nouns.nonbinary);
|
||||
};
|
||||
|
||||
export const iconNamesByGender: Record<Gender, string> = {
|
||||
masc: 'mars',
|
||||
fem: 'venus',
|
||||
neutr: 'neuter',
|
||||
nb: 'transgender-alt',
|
||||
};
|
||||
export const iconUnicodesByGender: Record<Gender, string> = {
|
||||
masc: '\uf222',
|
||||
fem: '\uf221',
|
||||
neutr: '\uf22c',
|
||||
nb: '\uf225',
|
||||
};
|
||||
export const longIdentifierByGender: Record<Gender, string> = {
|
||||
masc: 'masculine',
|
||||
fem: 'feminine',
|
||||
neutr: 'neuter',
|
||||
nb: 'nonbinary',
|
||||
};
|
@ -5,10 +5,11 @@ import type { Config } from '../../locale/config.ts';
|
||||
import type { NounTemplatesData, PronounGroupData, PronounExamplesData, PronounData } from '../../locale/data.ts';
|
||||
import allLocales from '../../locale/locales.ts';
|
||||
import { loadSumlFromBase } from '../../server/loader.ts';
|
||||
import { Example, gendersWithNumerus } from '../../src/classes.ts';
|
||||
import { Example } from '../../src/classes.ts';
|
||||
import { loadTsv } from '../../src/tsv.ts';
|
||||
|
||||
import { normaliseKey } from '~/src/buildPronoun.ts';
|
||||
import { gendersWithNumerus } from '~/src/nouns.ts';
|
||||
|
||||
const __dirname = new URL('.', import.meta.url).pathname;
|
||||
|
||||
@ -152,7 +153,10 @@ describe.each(allLocales)('data files of $code', ({ code }) => {
|
||||
test('nouns/nounTemplates.tsv have exactly one hyphen as placeholder for root', () => {
|
||||
for (const template of nounTemplates) {
|
||||
for (const genderWithNumerus of gendersWithNumerus) {
|
||||
expect(template[genderWithNumerus]).toMatch(/^(?:[^-]*-[^-]*(?:\/|$))+/);
|
||||
const actual = template[genderWithNumerus];
|
||||
if (typeof actual === 'string') {
|
||||
expect(actual).toMatch(/^(?:[^-]*-[^-]*(?:\/|$))+/);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
Loading…
x
Reference in New Issue
Block a user