This repository has been archived on 2024-07-22. You can view files and clone it, but cannot push or open issues or pull requests.
Zaimki/routes/profileEditor.vue

432 lines
17 KiB
Vue

<template>
<MustLogin v-if="!$user()"/>
<div v-else>
<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="`/@${$user().username}`" class="btn btn-outline-primary btn-sm">
<Icon v="id-card"/>
<T>profile.show</T>
</nuxt-link>
</div>
</div>
<form @submit.prevent="save" :class="[saving ? 'saving' : '']">
<div v-if="$isGranted()" class="border border-primary rounded p-4">
<h3 class="h4 mb-3">
<Icon v="user-cog"/>
Admin section
</h3>
<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.
</p>
<div class="form-group">
<label for="teamName">Team page display name:</label>
<input class="form-control" name="teamName" maxlength="36" v-model="teamName"/>
</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!).
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 class="form-control" name="footerName" maxlength="36" v-model="footerName"/>
</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>
</div>
<section>
<OpinionLegend/>
</section>
<section class="form-group">
<h3 class="h4">
<Icon v="signature"/>
<T>profile.names</T>
</h3>
<OpinionListInput v-model="names"/>
</section>
<section class="form-group">
<h3 class="h4">
<Icon v="link"/>
<T>profile.pronouns</T>
</h3>
<div 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"/>
</section>
<section class="form-group">
<h3 class="h4">
<Icon v="comment-edit"/>
<T>profile.description</T>
</h3>
<textarea class="form-control form-control-sm" v-model="description" maxlength="256" rows="8"/>
</section>
<section class="form-group">
<h3 class="h4">
<Icon v="flag"/>
<T>profile.flags</T>
</h3>
<p class="small text-muted mb-0">
<T>profile.flagsInfo</T>
</p>
<ButtonList v-model="flags" :options="allFlags" v-slot="s">
<Flag :name="s.desc.split('|')[0]" :alt="s.desc.split('|')[1]" :img="`/flags/${s.v}.png`"/>
</ButtonList>
<details class="form-group border rounded" :open="Object.keys(customFlags).length > 0">
<summary class="px-3 py-2">
<T>profile.flagsCustom</T>
</summary>
<div class="border-top">
<ImageWidgetRich v-model="customFlags" sizes="flag"/>
</div>
</details>
<Answer question="flags" small/>
</section>
<section class="form-group">
<h3 class="h4">
<Icon v="link"/>
<T>profile.links</T>
</h3>
<ListInput v-model="links" v-slot="s">
<input v-model="s.val" type="url" class="form-control" @keyup="s.update(s.val)" @paste="s.update(s.val)" @change="s.update(s.val)" required/>
</ListInput>
<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 silent>profile.linksRecommendedAfter</T>
😉
</p>
</section>
<section class="form-group">
<h3 class="h4">
<Icon v="birthday-cake"/>
<T>profile.birthday</T>
<button type="button" class="btn btn-outline-danger btn-sm" v-if="birthday !== null" @click="birthday = null">
<Icon v="times"/>
<T>crud.remove</T>
</button>
</h3>
<p class="small text-muted">
<T>profile.birthdayInfo</T>
</p>
<div class="input-group mb-3">
<datepicker v-model="birthday" inline :disabled-dates="disabledDates" initial-view="year"/>
</div>
</section>
<section class="form-group">
<div class="h4 d-flex justify-content-between">
<h3 class="h4">
<Icon v="scroll-old"/>
<T>profile.words</T>
</h3>
<button type="button" class="btn btn-outline-warning btn-sm" @click.prevent="resetWords">
<T>profile.editor.defaults</T>
</button>
</div>
<template v-for="i in [0, 1, 2, 3]">
<h4 class="h5">
<T>profile.column</T> {{i + 1}}
</h4>
<OpinionListInput v-model="words[i]" group="words"/>
</template>
</section>
<section>
<button class="btn btn-primary w-100" type="submit">
<Icon v="save"/>
<T>profile.editor.save</T>
</button>
</section>
</form>
</div>
</template>
<script>
import {head, dictToList, listToDict, buildList, buildDict} from "../src/helpers";
import { pronouns } from "~/src/data";
import { buildPronoun } from "../src/buildPronoun";
import config from '../data/config.suml';
import link from '../plugins/link';
import {minBirthdate, maxBirthdate, formatDate} from '../src/birthdate';
const defaultWords = config.profile.defaultWords.map(c => buildList(function* () {
for (let word of c) {
yield {key: word, value: 0};
}
}))
export default {
mixins: [link],
data() {
return {
saving: false,
disabledDates: {
to: minBirthdate,
from: maxBirthdate,
},
};
},
async asyncData({ app, store }) {
if (!store.state.user) {
return {};
}
const profiles = (await app.$axios.$get(`/profile/get/${encodeURIComponent(store.state.user.username)}`, { headers: {
authorization: 'Bearer ' + store.state.token,
} })).profiles;
for (let locale in profiles) {
if (!profiles.hasOwnProperty(locale)) {
continue;
}
if (locale === app.context.env.LOCALE) {
const profile = profiles[locale];
return {
names: dictToList(profile.names),
pronouns: dictToList(profile.pronouns),
description: profile.description,
birthday: profile.birthday,
links: Object.keys(profile.links).length ? profile.links : [],
flags: profile.flags,
customFlags: profile.customFlags,
words: profile.words.map(x => dictToList(x)),
teamName: profile.teamName,
footerName: profile.footerName,
footerAreas: profile.footerAreas,
credentials: profile.credentials,
credentialsLevel: profile.credentialsLevel,
credentialsName: profile.credentialsName,
};
}
}
for (let locale in profiles) {
if (!profiles.hasOwnProperty(locale)) {
continue;
}
const profile = profiles[locale];
return {
names: dictToList(profile.names),
pronouns: [],
description: '',
birthday: profile.birthday,
links: Object.keys(profile.links).length ? profile.links : [],
flags: profile.flags.filter(f => !f.startsWith('-')),
customFlags: profile.customFlags,
words: [...defaultWords],
teamName: profile.teamName,
footerName: profile.footerName,
footerAreas: [],
credentials: [],
credentialsLevel: null,
credentialsName: null,
};
}
return {
names: [],
pronouns: [],
description: '',
birthday: null,
links: [],
flags: [],
customFlags: {},
words: [...defaultWords],
teamName: '',
footerName: '',
footerAreas: [],
credentials: [],
credentialsLevel: null,
credentialsName: null,
};
},
mounted() {
if (process.client && !this.$user()) {
window.sessionStorage.setItem('after-login', window.location.pathname);
this.$router.push('/' + this.config.user.route);
}
},
methods: {
async save() {
this.saving = true;
try {
await this.$post(`/profile/save`, {
names: listToDict(this.names),
pronouns: listToDict(this.pronouns),
description: this.description,
birthday: this.birthday ? formatDate(this.birthday) : null,
links: [...this.links],
flags: [...this.flags],
customFlags: {...this.customFlags},
words: this.words.map(x => listToDict(x)),
teamName: this.teamName,
footerName: this.footerName,
footerAreas: this.footerAreas,
credentials: this.credentials,
credentialsLevel: this.credentialsLevel,
credentialsName: this.credentialsName,
});
this.$router.push(`/@${this.$user().username}`);
} finally {
this.saving = false;
}
},
normalisePronoun(pronoun) {
return decodeURIComponent(
pronoun
.toLowerCase()
.trim()
.replace(new RegExp('^' + this.$base), '')
.replace(new RegExp('^' + this.$base.replace(/^https?:\/\//, '')), '')
.replace(new RegExp('^/'), '')
);
},
normaliseAndBuildPronoun(pronoun) {
return buildPronoun(pronouns, this.normalisePronoun(pronoun));
},
validatePronoun(pronoun) {
pronoun = this.normalisePronoun(pronoun);
return pronoun === this.config.pronouns.any
|| (this.config.pronouns.null && this.config.pronouns.null.routes && this.config.pronouns.null.routes.includes(pronoun))
|| (this.config.pronouns.mirror && this.config.pronouns.mirror.route === pronoun)
|| buildPronoun(pronouns, pronoun)
? null
: 'profile.pronounsNotFound'
},
async resetWords() {
await this.$confirm();
this.words = [...defaultWords];
}
},
computed: {
mainPronoun() {
let mainPronoun = buildPronoun(pronouns, this.config.profile.flags.defaultPronoun);
let mainOpinion = -1;
for (let {key: pronoun, value: opinion} of this.pronouns) {
if (opinion === 2) {
opinion = 0.5;
}
if (opinion > mainOpinion) {
const p = this.normaliseAndBuildPronoun(pronoun);
if (p) {
mainPronoun = p;
mainOpinion = opinion;
}
}
}
return mainPronoun;
},
allFlags() {
const that = this;
const flags = buildList(function*() {
for (let key in process.env.FLAGS) {
if (!process.env.FLAGS.hasOwnProperty(key)) { continue; }
yield [
key,
(key.startsWith('-') ? process.env.FLAGS[key] : that.$translateForPronoun(process.env.FLAGS[key], that.mainPronoun))
+ '|' + process.env.FLAGS[key]
];
}
});
flags.sort((a, b) => a[1].localeCompare(b[1]));
return buildDict(function *() {
for (let [key, display] of flags) {
yield [key, display];
}
});
},
},
head() {
return head({
title: this.$t('profile.editor.header'),
});
},
}
</script>
<style lang="scss" scoped>
.avatar {
width: 100%;
max-width: 5rem;
max-height: 5rem;
}
.saving {
opacity: .5;
}
section.form-group {
margin-bottom: 5rem;
}
</style>