mirror of
https://gitlab.com/PronounsPage/PronounsPage.git
synced 2025-09-30 00:28:02 -04:00
712 lines
31 KiB
Vue
712 lines
31 KiB
Vue
<template>
|
|
<Page v-if="$config.profile.editorEnabled">
|
|
<MustLogin v-if="!$user()" />
|
|
<div v-else>
|
|
<AdPlaceholder :phkey="['header', null]" />
|
|
<div class="mb-3 d-flex justify-content-between flex-column flex-md-row">
|
|
<h2 class="text-nowrap">
|
|
<Avatar :user="$user()" />
|
|
@{{ $user()?.username }}
|
|
</h2>
|
|
<div>
|
|
<nuxt-link :to="/* @ts-ignore */ `/@${$user().username}`" class="btn btn-outline-primary btn-sm">
|
|
<Icon v="id-card" />
|
|
<T>profile.show</T>
|
|
</nuxt-link>
|
|
</div>
|
|
</div>
|
|
|
|
<form :class="[saving ? 'saving' : '']" @submit.prevent="save">
|
|
<TabsNav
|
|
:tabs="['opinions', 'names', $config.pronouns.enabled ? 'pronouns' : undefined, 'description', 'flags', 'links', 'birthday', 'timezone', 'words', 'circle', 'calendar', 'sensitive', $isGranted() ? 'admin' : undefined]"
|
|
pills
|
|
showheaders
|
|
navclass="mb-3 border-bottom-0"
|
|
>
|
|
<template #admin-header>
|
|
<Icon v="user-cog" />
|
|
Team section
|
|
</template>
|
|
<template #admin>
|
|
<p class="small text-muted mb-0">
|
|
This will be shown on the “Team” page.
|
|
If you leave it empty, you won't show up there (for this language version).
|
|
You can use a different display name in different language versions.
|
|
Please only add yourself here, if you're actually working on <strong>this language version</strong>.
|
|
</p>
|
|
|
|
<div class="form-group">
|
|
<label for="teamName">Team page display name:</label>
|
|
<input v-model="teamName" class="form-control" name="teamName" maxlength="64">
|
|
<PropagateCheckbox v-if="otherProfiles > 0" field="teamName" :before="beforeChanges.teamName" :after="teamName" @change="propagateChanged" />
|
|
</div>
|
|
|
|
<hr>
|
|
|
|
<p class="small text-muted mb-0">
|
|
If you feel that you've contributed to this language version enough to get credited in the footer
|
|
(not saying how much that is, that's on you to decide 😉),
|
|
then add your name and areas here (in the local language!).
|
|
Please only add yourself here, if you're actually working on <strong>this language version</strong>.
|
|
The team as a whole will be credited in the footer either way.
|
|
</p>
|
|
|
|
<div class="form-group">
|
|
<label for="footerName">Footer display name:</label>
|
|
<input v-model="footerName" class="form-control" name="footerName" maxlength="64">
|
|
<PropagateCheckbox v-if="otherProfiles > 0" field="footerName" :before="beforeChanges.footerName" :after="footerName" @change="propagateChanged" />
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="footerAreas">Areas responsible for / contributing to:</label>
|
|
<ListInput v-model="footerAreas" />
|
|
</div>
|
|
|
|
<template v-if="$te('contact.team.credentials')">
|
|
<hr>
|
|
|
|
<p class="small text-muted mb-0">
|
|
This will be displayed on the team page in the "Credentials" section.
|
|
You might want to put here your full legal name here, but it's not required
|
|
(you can leave this field empty).
|
|
</p>
|
|
|
|
<div class="form-group">
|
|
<label for="credentials">Credentials:</label>
|
|
<ListInput v-model="credentials" />
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="credentials">Credentials level:</label>
|
|
<select v-model="credentialsLevel" class="form-select">
|
|
<option :value="null"></option>
|
|
<option :value="1">
|
|
Higher education, but irrelevant field
|
|
</option>
|
|
<option :value="2">
|
|
Bachelor (not completed yet)
|
|
</option>
|
|
<option :value="3">
|
|
Bachelor
|
|
</option>
|
|
<option :value="4">
|
|
Master (not completed yet)
|
|
</option>
|
|
<option :value="5">
|
|
Master
|
|
</option>
|
|
<option :value="6">
|
|
PhD (not completed yet)
|
|
</option>
|
|
<option :value="7">
|
|
PhD
|
|
</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="credentials">Name for credentials:</label>
|
|
<input v-model="credentialsName" class="form-control" placeholder="(not required)">
|
|
</div>
|
|
</template>
|
|
</template>
|
|
|
|
<template #opinions-header>
|
|
<Icon v="comment-smile" />
|
|
<T>profile.opinions.header</T>
|
|
</template>
|
|
<template #opinions>
|
|
<LegendOpinionListInput v-model="defaultOpinions" readonly class="mb-0" />
|
|
<LegendOpinionListInput v-model="opinions" :maxitems="10" />
|
|
</template>
|
|
|
|
<template #names-header>
|
|
<Icon v="signature" />
|
|
<T>profile.names</T>
|
|
</template>
|
|
<template #names>
|
|
<p v-if="$te('profile.namesInfo')" class="small text-muted">
|
|
<T>profile.namesInfo</T>
|
|
</p>
|
|
<OpinionListInput
|
|
v-model="names"
|
|
:prototype="{ value: '', opinion: 'meh', pronunciation: null }"
|
|
:custom-opinions="opinions"
|
|
:maxitems="128"
|
|
:maxlength="$config.profile.longNames ? 255 : 32"
|
|
>
|
|
<template #additional="s">
|
|
<PronunciationInput v-model="s.val.pronunciation" />
|
|
</template>
|
|
</OpinionListInput>
|
|
<InlineMarkdownInstructions v-model="markdown" />
|
|
<PropagateCheckbox v-if="otherProfiles > 0" field="names" :before="beforeChanges.names" :after="names" @change="propagateChanged" />
|
|
</template>
|
|
|
|
<template #pronouns-header>
|
|
<Icon v="tags" />
|
|
<T>profile.pronouns</T>
|
|
</template>
|
|
<template #pronouns>
|
|
<div v-if="$t('profile.pronounsInfo')" class="alert alert-info">
|
|
<p class="small mb-0">
|
|
<Icon v="info-circle" />
|
|
<T>profile.pronounsInfo</T>
|
|
</p>
|
|
</div>
|
|
<OpinionListInput v-model="pronouns" :validation="validatePronoun" :custom-opinions="opinions" :maxitems="128" :maxlength="192" />
|
|
</template>
|
|
|
|
<template #description-header>
|
|
<Icon v="comment-edit" />
|
|
<T>profile.description</T>
|
|
</template>
|
|
<template #description>
|
|
<textarea v-model="description" class="form-control form-control-sm" maxlength="1024" rows="8"></textarea>
|
|
<InlineMarkdownInstructions v-model="markdown" />
|
|
</template>
|
|
|
|
<template #flags-header>
|
|
<Icon v="flag" />
|
|
<T>profile.flags</T>
|
|
</template>
|
|
<template #flags>
|
|
<p class="small text-muted mb-0">
|
|
<T>profile.flagsInfo</T>
|
|
</p>
|
|
<ButtonList v-slot="s" v-model="flags" :options="allFlagsOptions">
|
|
<Flag
|
|
:name="s.desc.split('|')[0]"
|
|
:alt="s.desc.split('|')[1]"
|
|
:img="`/flags/${s.v}.png`"
|
|
:asterisk="allFlags[s.v].asterisk"
|
|
/>
|
|
</ButtonList>
|
|
<PropagateCheckbox v-if="otherProfiles > 0" field="flags" :before="beforeChanges.flags" :after="flags" @change="propagateChanged" />
|
|
|
|
<details class="form-group border rounded" :open="customFlags.length > 0">
|
|
<summary class="px-3 py-2">
|
|
<T>profile.flagsCustom</T>
|
|
</summary>
|
|
<div class="border-top">
|
|
<CustomFlagsWidget v-model="customFlags" sizes="flag" :maxitems="128" />
|
|
</div>
|
|
</details>
|
|
<PropagateCheckbox v-if="otherProfiles > 0" field="customFlags" :before="beforeChanges.customFlags" :after="customFlags" @change="propagateChanged" />
|
|
<Answer question="flags" small />
|
|
</template>
|
|
|
|
<template #links-header>
|
|
<Icon v="link" />
|
|
<T>profile.links</T>
|
|
</template>
|
|
<template #links>
|
|
<ListInput v-model="links" :maxitems="128">
|
|
<template #default="s">
|
|
<input
|
|
v-model="s.val"
|
|
type="url"
|
|
class="form-control"
|
|
required
|
|
@keyup="s.update(s.val)"
|
|
@paste="$nextTick(() => s.update(s.val))"
|
|
@change="s.update(s.val)"
|
|
>
|
|
</template>
|
|
<template #validation="s">
|
|
<p v-if="s.val && !isValidLink(s.val)" class="small text-danger">
|
|
<Icon v="exclamation-triangle" />
|
|
<span class="ml-1">{{ $t('crud.validation.invalidLink') }}</span>
|
|
</p>
|
|
</template>
|
|
</ListInput>
|
|
<PropagateCheckbox v-if="otherProfiles > 0" field="links" :before="beforeChanges.links" :after="links" @change="propagateChanged" />
|
|
<p class="small text-muted mb-0">
|
|
<Icon v="ad" />
|
|
<T>profile.linksRecommended</T>
|
|
<a v-for="provider in recommendedLinkProviders()" :href="provider.homepage" target="_blank" rel="noopener">
|
|
<Icon :v="provider.icon" :set="provider.iconSet || 'l'" />
|
|
{{ provider.name }}
|
|
</a>
|
|
<T>profile.linksRecommendedAfter</T>
|
|
😉
|
|
</p>
|
|
<p v-if="$te('profile.linksWarning')" class="small text-muted mt-2 mb-0">
|
|
<Icon v="exclamation-triangle" />
|
|
<T>profile.linksWarning</T>
|
|
</p>
|
|
</template>
|
|
|
|
<template #birthday-header>
|
|
<Icon v="birthday-cake" />
|
|
<T>profile.birthday</T>
|
|
</template>
|
|
<template #birthday>
|
|
<p class="small text-muted">
|
|
<T>profile.birthdayInfo</T>
|
|
</p>
|
|
<div class="input-group mb-3">
|
|
<ClientOnly>
|
|
<datepicker
|
|
v-model="birthday"
|
|
inline
|
|
:disabled-dates="disabledDates"
|
|
:open-date="disabledDates.from"
|
|
:initial-view="birthday !== null ? 'day' : 'year'"
|
|
/>
|
|
</ClientOnly>
|
|
</div>
|
|
<PropagateCheckbox v-if="otherProfiles > 0" field="birthday" :before="beforeChanges.birthday" :after="birthday" @change="propagateChanged" />
|
|
<button v-if="birthday !== null" type="button" class="btn btn-outline-danger btn-sm" @click="birthday = null">
|
|
<Icon v="times" />
|
|
<T>crud.remove</T>
|
|
</button>
|
|
</template>
|
|
|
|
<template #timezone-header>
|
|
<Icon v="clock" />
|
|
<T>profile.timezone.header</T>
|
|
</template>
|
|
<template #timezone>
|
|
<p class="small text-muted">
|
|
<T>profile.timezone.info</T>
|
|
</p>
|
|
<TimezoneSelect v-model="timezone" />
|
|
<PropagateCheckbox v-if="otherProfiles > 0" field="timezone" :before="beforeChanges.timezone" :after="timezone" @change="propagateChanged" />
|
|
</template>
|
|
|
|
<template #words-header>
|
|
<Icon v="scroll-old" />
|
|
<T>profile.words</T>
|
|
</template>
|
|
<template #words>
|
|
<template v-for="i in [0, 1, 2, 3]">
|
|
<h4 class="h5">
|
|
<T>profile.column</T> {{ i + 1 }}
|
|
</h4>
|
|
<input v-model="words[i].header" class="form-control form-control-sm mb-2" :placeholder="$t('profile.wordsColumnHeader')" maxlength="36">
|
|
<OpinionListInput v-model="words[i].values" group="words" :custom-opinions="opinions" :maxitems="128" />
|
|
</template>
|
|
<button type="button" class="btn btn-outline-warning btn-sm" @click.prevent="resetWords">
|
|
<T>profile.editor.defaults</T>
|
|
</button>
|
|
<InlineMarkdownInstructions v-model="markdown" />
|
|
</template>
|
|
|
|
<template #circle-header>
|
|
<Icon v="heart-circle" />
|
|
<T>profile.circles.header</T>
|
|
</template>
|
|
<template #circle>
|
|
<p class="small text-muted">
|
|
<T>profile.circles.info</T>
|
|
</p>
|
|
|
|
<CircleListInput v-model="circle" :maxitems="16" />
|
|
|
|
<CircleMentions />
|
|
</template>
|
|
|
|
<template #sensitive-header>
|
|
<Icon v="engine-warning" />
|
|
<T>profile.sensitive.header</T>
|
|
</template>
|
|
<template #sensitive>
|
|
<p class="small text-muted">
|
|
<T>profile.sensitive.info</T>
|
|
</p>
|
|
|
|
<ListInput v-model="sensitive" :maxlength="64" :maxitems="16" />
|
|
</template>
|
|
|
|
<template #calendar-header>
|
|
<Icon v="calendar" />
|
|
<T>profile.calendar.header</T>
|
|
</template>
|
|
<template #calendar>
|
|
<p class="small text-muted">
|
|
<T>profile.calendar.info</T>
|
|
</p>
|
|
|
|
<section class="my-5">
|
|
<p class="h5">
|
|
<T>profile.calendar.customEvents.header</T><T>quotation.colon</T>
|
|
</p>
|
|
<PersonalEventListInput v-model="customEvents" :maxitems="100" />
|
|
</section>
|
|
|
|
<section class="my-5">
|
|
<p class="h5">
|
|
<T>profile.calendar.publicEvents.header</T><T>quotation.colon</T>
|
|
</p>
|
|
<!-- @vue-ignore -->
|
|
<PersonalCalendar
|
|
:year="year"
|
|
:events="events"
|
|
class="my-3"
|
|
remove-button
|
|
:maxitems="100"
|
|
@delete="(d) => events = events.filter(e => e !== d)"
|
|
/>
|
|
<PropagateCheckbox v-if="otherProfiles > 0" field="events" :before="beforeChanges.events" :after="events" @change="propagateChanged" />
|
|
<!-- @vue-ignore -->
|
|
<CalendarEventsList :year="year" add-button @add="(e) => events.push(e)" />
|
|
</section>
|
|
</template>
|
|
</TabsNav>
|
|
|
|
<section>
|
|
<button class="btn btn-primary w-100" type="submit">
|
|
<Icon v="save" />
|
|
<T>profile.editor.save</T>
|
|
</button>
|
|
</section>
|
|
|
|
<AdPlaceholder :phkey="['content-1', 'content-mobile-1']" />
|
|
</form>
|
|
</div>
|
|
</Page>
|
|
</template>
|
|
|
|
<script lang="ts">
|
|
import { head, buildList, isValidLink } from '../src/helpers.ts';
|
|
import { pronouns, pronounLibrary } from '../src/data.ts';
|
|
import { buildPronoun } from '../src/buildPronoun.ts';
|
|
import { buildFlags } from '../src/flags.ts';
|
|
import link from '../plugins/link.ts';
|
|
import { birthdateRange, formatDate, parseDate } from '../src/birthdate.ts';
|
|
import opinions from '../src/opinions.ts';
|
|
import { calendar } from '../src/calendar/calendar.ts';
|
|
import type { Config } from '../locale/config.ts';
|
|
import type { Translator } from '../src/translator.ts';
|
|
import type { Pronoun } from '../src/classes.ts';
|
|
import type { Flag } from '../src/flags.ts';
|
|
import type { Opinion } from '../src/opinions.ts';
|
|
import type { OpinionFormValue, Profile, SaveProfilePayload, WordCategory } from '../src/profile.ts';
|
|
|
|
interface ProfileFormValue extends Omit<Profile, 'birthday' | 'linksMetadata' | 'verifiedLinks' | 'opinions' |
|
|
'card' | 'cardDark' | 'lastUpdate'> {
|
|
birthday: Date | null;
|
|
opinions: OpinionFormValue[];
|
|
}
|
|
|
|
const defaultWords = (config: Config): WordCategory[] => {
|
|
if (!config.profile.enabled || !config.profile.editorEnabled) {
|
|
return [];
|
|
}
|
|
return config.profile.defaultWords.map(({ header, values }) => {
|
|
return {
|
|
header,
|
|
values: values.map((v) => {
|
|
return { value: v.replace(/"/g, '\''), opinion: 'meh' };
|
|
}),
|
|
};
|
|
});
|
|
};
|
|
|
|
function coerceWords(words: WordCategory[]): WordCategory[] {
|
|
for (let i = 0; i < 4; i++) {
|
|
words[i] = words[i] || {
|
|
header: null,
|
|
values: [],
|
|
};
|
|
}
|
|
return words;
|
|
}
|
|
|
|
function fixArrayObject<T>(arrayObject: Record<string, T> | T[]): T[] {
|
|
return Array.isArray(arrayObject) ? arrayObject : Object.values(arrayObject);
|
|
}
|
|
|
|
const opinionsToForm = (
|
|
opinions: Record<string, Opinion>,
|
|
translator: Translator,
|
|
): OpinionFormValue[] => buildList(function*() {
|
|
for (const [key, options] of Object.entries(opinions)) {
|
|
yield {
|
|
key,
|
|
icon: options.icon,
|
|
description: options.description || translator.get(`profile.opinion.${key}`),
|
|
colour: options.colour || '',
|
|
style: options.style || '',
|
|
};
|
|
}
|
|
});
|
|
|
|
const buildProfile = (
|
|
profiles: Record<string, Profile>,
|
|
config: Config,
|
|
translator: Translator,
|
|
): ProfileFormValue => {
|
|
// card in this locale exists
|
|
for (const locale in profiles) {
|
|
if (!profiles.hasOwnProperty(locale)) {
|
|
continue;
|
|
}
|
|
if (locale === config.locale) {
|
|
const profile = profiles[locale];
|
|
return {
|
|
names: profile.names,
|
|
pronouns: profile.pronouns,
|
|
description: profile.description,
|
|
birthday: parseDate(profile.birthday),
|
|
timezone: profile.timezone,
|
|
links: profile.links,
|
|
flags: profile.flags,
|
|
customFlags: fixArrayObject(profile.customFlags),
|
|
words: coerceWords(profile.words),
|
|
teamName: profile.teamName,
|
|
footerName: profile.footerName,
|
|
footerAreas: profile.footerAreas,
|
|
credentials: profile.credentials,
|
|
credentialsLevel: profile.credentialsLevel,
|
|
credentialsName: profile.credentialsName,
|
|
opinions: opinionsToForm(profile.opinions || {}, translator),
|
|
circle: profile.circle,
|
|
sensitive: profile.sensitive,
|
|
markdown: profile.markdown,
|
|
events: profile.events,
|
|
customEvents: profile.customEvents,
|
|
};
|
|
}
|
|
}
|
|
|
|
// card in this locale doesn't exist yet, but we can copy some non-language-specific fields from another card
|
|
for (const locale in profiles) {
|
|
if (!profiles.hasOwnProperty(locale)) {
|
|
continue;
|
|
}
|
|
const profile = profiles[locale];
|
|
return {
|
|
names: profile.names,
|
|
pronouns: [],
|
|
description: '',
|
|
birthday: parseDate(profile.birthday),
|
|
timezone: profile.timezone,
|
|
links: profile.links,
|
|
flags: profile.flags.filter((f) => !f.startsWith('-')),
|
|
customFlags: fixArrayObject(profile.customFlags),
|
|
words: [...defaultWords(config)],
|
|
teamName: profile.teamName,
|
|
footerName: profile.footerName,
|
|
footerAreas: [],
|
|
credentials: [],
|
|
credentialsLevel: null,
|
|
credentialsName: null,
|
|
opinions: opinionsToForm(profile.opinions || {}, translator),
|
|
circle: profile.circle,
|
|
sensitive: [],
|
|
markdown: profile.markdown,
|
|
events: [],
|
|
customEvents: [],
|
|
};
|
|
}
|
|
|
|
// no cards in other languages available, start with a fresh one
|
|
return {
|
|
names: [],
|
|
pronouns: [],
|
|
description: '',
|
|
birthday: null,
|
|
timezone: null,
|
|
links: [],
|
|
flags: [],
|
|
customFlags: [],
|
|
words: [...defaultWords(config)],
|
|
teamName: '',
|
|
footerName: '',
|
|
footerAreas: [],
|
|
credentials: [],
|
|
credentialsLevel: null,
|
|
credentialsName: null,
|
|
opinions: [],
|
|
circle: [],
|
|
sensitive: [],
|
|
markdown: false,
|
|
events: [],
|
|
customEvents: [],
|
|
};
|
|
};
|
|
|
|
type AsyncData = ProfileFormValue & { beforeChanges: ProfileFormValue, otherProfiles: number } | Record<string, never>;
|
|
|
|
export default link.extend({
|
|
components: {
|
|
datepicker: () => {
|
|
if (!process.client) {
|
|
return null;
|
|
}
|
|
return import('vuejs-datepicker');
|
|
},
|
|
},
|
|
async asyncData({ app, store }): Promise<AsyncData> {
|
|
if (!store.state.user) {
|
|
return {};
|
|
}
|
|
|
|
const profiles = (await app.$axios.$get(`/profile/get/${encodeURIComponent(store.state.user.username)}?version=2&props=flags,pronouns,names,age,timezone,links,customFlags,team,opinions,circle&lprops[${app.$config.locale}]=all`, { headers: {
|
|
authorization: `Bearer ${store.state.token}`,
|
|
} })).profiles;
|
|
|
|
const profile = buildProfile(profiles, app.$config, app.$translator);
|
|
|
|
return {
|
|
...profile,
|
|
beforeChanges: JSON.parse(JSON.stringify(profile)),
|
|
otherProfiles: Object.keys(profiles).filter((locale) => locale !== app.$config.locale).length,
|
|
};
|
|
},
|
|
data() {
|
|
const bdayRange = birthdateRange(this.$config);
|
|
return {
|
|
saving: false,
|
|
disabledDates: {
|
|
to: bdayRange.min,
|
|
from: bdayRange.max,
|
|
},
|
|
propagate: [] as string[],
|
|
defaultOpinions: opinionsToForm(opinions, this.$translator),
|
|
isValidLink,
|
|
year: calendar.getCurrentYear(),
|
|
};
|
|
},
|
|
head() {
|
|
return head({
|
|
title: this.$t('profile.editor.header'),
|
|
}, this.$translator);
|
|
},
|
|
computed: {
|
|
mainPronoun(): Pronoun | null {
|
|
if (!this.$config.profile.editorEnabled || !this.$config.profile.flags?.defaultPronoun) {
|
|
return null;
|
|
}
|
|
let mainPronoun = buildPronoun(pronouns, this.$config.profile.flags?.defaultPronoun, this.$config, this.$translator);
|
|
let mainOpinion = -1;
|
|
for (const { value: pronoun, opinion } of this.pronouns) {
|
|
const opinionValue = opinions[opinion]?.value || 0;
|
|
if (opinionValue > mainOpinion) {
|
|
const p = this.normaliseAndBuildPronoun(pronoun);
|
|
if (p) {
|
|
mainPronoun = p;
|
|
mainOpinion = opinionValue;
|
|
}
|
|
}
|
|
}
|
|
|
|
return mainPronoun;
|
|
},
|
|
allFlags(): Record<string, Flag> {
|
|
return buildFlags(this.$config.locale);
|
|
},
|
|
allFlagsOptions(): Record<string, string> {
|
|
const entries = Object.entries(this.allFlags).map(([flagName, flag]) => {
|
|
return [flagName, `${this.$translateForPronoun(flag.display, this.mainPronoun)}|${flag.display}`];
|
|
});
|
|
entries.sort((a, b) => a[1].localeCompare(b[1]));
|
|
return Object.fromEntries(entries);
|
|
},
|
|
},
|
|
mounted() {
|
|
if (process.client && !this.$user()) {
|
|
window.sessionStorage.setItem('after-login', window.location.pathname);
|
|
this.$router.push(`/${this.$config.user.route}`);
|
|
}
|
|
},
|
|
methods: {
|
|
async save(): Promise<void> {
|
|
this.saving = true;
|
|
try {
|
|
await this.$post('/profile/save', {
|
|
username: this.$user()!.username,
|
|
|
|
opinions: this.opinions,
|
|
names: this.names,
|
|
pronouns: this.pronouns,
|
|
description: this.description,
|
|
birthday: formatDate(this.birthday),
|
|
timezone: this.timezone,
|
|
links: [...this.links],
|
|
flags: [...this.flags],
|
|
customFlags: [...fixArrayObject(this.customFlags)],
|
|
words: this.words,
|
|
circle: this.circle,
|
|
sensitive: this.sensitive,
|
|
markdown: this.markdown,
|
|
events: this.events,
|
|
customEvents: this.customEvents,
|
|
|
|
teamName: this.teamName,
|
|
footerName: this.footerName,
|
|
footerAreas: this.footerAreas,
|
|
credentials: this.credentials,
|
|
credentialsLevel: this.credentialsLevel,
|
|
credentialsName: this.credentialsName,
|
|
|
|
propagate: this.propagate,
|
|
} satisfies SaveProfilePayload);
|
|
this.$router.push(`/@${this.$user()!.username}`);
|
|
} finally {
|
|
this.saving = false;
|
|
}
|
|
},
|
|
normalisePronoun(pronoun: string): string | null {
|
|
try {
|
|
return decodeURIComponent(pronoun
|
|
.toLowerCase()
|
|
.trim()
|
|
.replace(new RegExp(`^${this.$base}`), '')
|
|
.replace(new RegExp(`^${this.$base.replace(/^https?:\/\//, '')}`), '')
|
|
.replace(new RegExp('^/'), ''));
|
|
} catch {
|
|
return null;
|
|
}
|
|
},
|
|
normaliseAndBuildPronoun(pronoun: string): Pronoun | null {
|
|
return buildPronoun(pronouns, this.normalisePronoun(pronoun), this.$config, this.$translator);
|
|
},
|
|
validatePronoun(pronoun: string): string | null {
|
|
const normalisedPronoun = this.normalisePronoun(pronoun);
|
|
if (!normalisedPronoun) {
|
|
return 'profile.pronounsNotFound';
|
|
}
|
|
return this.validateAnyPronoun(normalisedPronoun) ||
|
|
this.$config.pronouns.null && this.$config.pronouns.null.routes && this.$config.pronouns.null.routes.includes(normalisedPronoun) ||
|
|
this.$config.pronouns.mirror && this.$config.pronouns.mirror.route === normalisedPronoun ||
|
|
buildPronoun(pronouns, normalisedPronoun, this.$config, this.$translator)
|
|
? null
|
|
: 'profile.pronounsNotFound';
|
|
},
|
|
validateAnyPronoun(pronoun: string): boolean {
|
|
const prefix = `${this.$config.pronouns.any}:`;
|
|
return pronoun === this.$config.pronouns.any ||
|
|
pronoun.startsWith(prefix) && !!pronounLibrary.byKey()[pronoun.substring(prefix.length)];
|
|
},
|
|
async resetWords(): Promise<void> {
|
|
await this.$confirm();
|
|
|
|
this.words = [...defaultWords(this.$config)];
|
|
},
|
|
propagateChanged(field: string, checked: boolean): void {
|
|
this.propagate = this.propagate.filter((f) => f !== field);
|
|
if (checked) {
|
|
this.propagate.push(field);
|
|
}
|
|
},
|
|
},
|
|
});
|
|
</script>
|
|
|
|
<style lang="scss" scoped>
|
|
.avatar {
|
|
width: 100%;
|
|
max-width: 5rem;
|
|
max-height: 5rem;
|
|
}
|
|
.saving {
|
|
opacity: .5;
|
|
}
|
|
section.form-group {
|
|
margin-bottom: 5rem;
|
|
}
|
|
</style>
|