PronounsPage/components/Profile.vue
2025-02-15 14:27:00 +01:00

324 lines
14 KiB
Vue

<script setup lang="ts">
import { useNuxtApp } from 'nuxt/app';
import useConfig from '~/composables/useConfig.ts';
import useMainPronoun from '~/composables/useMainPronoun.ts';
import useSpelling from '~/composables/useSpelling.ts';
import type { TermsEntryRaw } from '~/src/classes.ts';
import { loadCalendar, loadPronounLibrary } from '~/src/data.ts';
import { buildFlags } from '~/src/flags.ts';
import { buildImageUrl } from '~/src/helpers.ts';
import type { Profile, UserWithProfiles } from '~/src/profile.ts';
const props = withDefaults(defineProps<{
user: UserWithProfiles;
profile: Partial<Profile>;
terms?: TermsEntryRaw[] | null;
isStatic?: boolean;
expandLinks?: boolean;
}>(), {
terms: null,
});
const { $translator: translator } = useNuxtApp();
const { convertName } = useSpelling();
const config = useConfig();
const pronounLibrary = await loadPronounLibrary(config);
const { mainPronoun, pronounOpinions } = useMainPronoun(pronounLibrary, toRef(props, 'profile'), translator);
const normaliseUrl = (url: string): string | null => {
try {
return new URL(url).toString();
} catch {
return null;
}
};
const allFlags = buildFlags(config.locale);
const minAge = config.ageLimit || 13;
const year = (await loadCalendar()).getCurrentYear();
const countFlags = computed(() => {
return (props.profile.flags?.length ?? 0) + (props.profile.customFlags?.length ?? 0);
});
const manyFlagsLayout = computed(() => {
return countFlags.value > 36 || countFlags.value === 0 || !hasDescriptionColumn.value;
});
const hasDescriptionColumn = computed(() => {
return (props.profile.age && props.profile.age > minAge) ||
props.profile.description?.trim().length ||
props.user.team ||
props.profile.timezone
;
});
const mainRowCount = computed(() => {
let c = 0;
if (props.profile.names?.length) {
c++;
}
if (props.profile.pronouns?.length && config.pronouns.enabled) {
c++;
}
if (props.profile.links?.length) {
c++;
}
return c;
});
const usedOpinions = computed(() => {
return new Set([
...(props.profile.names ?? []).map((r) => r.opinion),
...(props.profile.pronouns ?? []).map((r) => r.opinion),
...(props.profile.words ?? []).flat().map((c) => c.values)
.flat()
.map((r) => r.opinion),
]);
});
</script>
<template>
<div>
<div class="mb-3 d-flex justify-content-between flex-column flex-md-row">
<div class="mw-50">
<div class="text-nowrap d-flex align-items-center">
<Avatar :user="user" class="me-3" />
<div>
<h2>
@{{ user.username }}
</h2>
<p v-if="user.team || profile.teamName || profile.footerName" class="mb-2">
<nuxt-link :to="{ name: 'team' }" class="badge bg-primary text-white">
<Icon v="collective-logo.svg" class="inverted" />
<T>contact.team.member</T>
</nuxt-link>
</p>
</div>
</div>
</div>
<div class="flex-grow-1 text-lg-end">
<slot></slot>
</div>
</div>
<template v-if="profile.access">
<section class="row">
<div v-if="hasDescriptionColumn" :class="['col-12', manyFlagsLayout ? '' : 'col-lg-6']">
<div v-if="profile.description" class="mb-3">
<p v-for="line in profile.description.split('\n')" class="mb-1" style="word-break: break-word;">
<Spelling escape :text="line" :markdown="profile.markdown" />
</p>
</div>
<p v-if="profile.age && profile.age >= minAge">
<Icon v="birthday-cake" />
<T v-if="$te('profile.age')">profile.age</T><T v-else>profile.birthday</T><T>quotation.colon</T>
{{ profile.age }}
</p>
<Timezone v-if="profile.timezone" :value="profile.timezone" :is-static="isStatic" />
</div>
<div v-if="profile.flags?.length || profile.customFlags?.length" :class="['col-12', manyFlagsLayout ? '' : 'col-lg-6']">
<ClientOnly>
<ExpandableList
:values="[...(profile.flags ?? []).filter(flag => allFlags[flag]), ...profile.customFlags]"
:limit="32"
:reduced-limit="8"
class="list-inline"
item-class="list-inline-item p-1"
:is-static="isStatic"
:expand="expandLinks"
>
<template #default="s">
<Flag
v-if="typeof s.el === 'string'"
:termkey="allFlags[s.el].display"
:name="$translateForPronoun(allFlags[s.el].display, mainPronoun)"
:alt="$t(`flags_alt.${s.el.replace(/'/g, '*').replace(/ /g, '_')}`)"
:img="`/flags/${s.el}.png`"
:terms="terms || []"
:asterisk="allFlags[s.el].asterisk"
/>
<Flag
v-else
:termkey="s.el.name"
:name="s.el.name"
:alt="s.el.alt || ''"
:img="buildImageUrl($config.public.cloudfront, s.el.value, 'flag')"
:terms="terms || []"
custom
:description="s.el.description"
:customlink="s.el.link"
/>
</template>
</ExpandableList>
</ClientOnly>
</div>
</section>
<section class="row">
<div v-if="profile.names?.length" :class="['col-6', mainRowCount === 3 ? 'col-lg-4' : 'col-lg-6']">
<h3>
<Icon v="signature" />
<T>profile.names</T>
</h3>
<ExpandableList :values="profile.names" :limit="16" class="list-unstyled" :is-static="isStatic" :expand="expandLinks">
<template #default="s">
<Opinion
:word="convertName(s.el.value)"
:opinion="s.el.opinion"
:escape="false"
:markdown="profile.markdown"
:pronunciation="s.el.pronunciation"
:link="config.locale === 'tok' && config.pronouns.enabled ? `${config.pronouns.prefix}/${s.el.value}` : null"
:custom-opinions="profile.opinions"
/>
</template>
</ExpandableList>
</div>
<div
v-if="profile.pronouns?.length && config.pronouns.enabled"
:class="['col-6', mainRowCount === 3 ? 'col-lg-4' : 'col-lg-6']"
>
<h3>
<Icon v="tags" />
<T>profile.pronouns</T>
</h3>
<ExpandableList :values="pronounOpinions" :limit="16" class="list-unstyled" :is-static="isStatic" :expand="expandLinks">
<template #default="s">
<Opinion
:word="s.el.short.options.join(s.el.short.glue)"
:opinion="s.el.opinion"
:link="`${config.pronouns.prefix || ''}/${s.el.link}`"
:custom-opinions="profile.opinions"
/>
</template>
</ExpandableList>
</div>
<div v-if="profile.links?.length" :class="['col-12', mainRowCount === 3 ? 'col-lg-4' : 'col-lg-6']">
<h3>
<Icon v="link" />
<T>profile.links</T>
</h3>
<ExpandableList :values="profile.links" :limit="16" class="list-unstyled" :is-static="isStatic" :expand="expandLinks">
<template #default="s">
<ProfileLink
:link="s.el"
:expand="isStatic"
:verified-links="profile.verifiedLinks || {}"
:metadata="profile.linksMetadata?.[normaliseUrl(s.el) ?? '']"
/>
</template>
</ExpandableList>
</div>
</section>
<section v-if="(profile.words ?? []).map(w => w.values.length).reduce((a, b) => a + b, 0) > 0" class="clearfix">
<h3>
<Icon v="scroll-old" />
<T>profile.words</T>
</h3>
<div class="row">
<template v-for="column in profile.words">
<div v-if="column.values.length" class="col-6 col-lg-3">
<h4 v-if="column.header" class="h6">
<Spelling :text="column.header" :markdown="profile.markdown" />
</h4>
<ExpandableList
:values="column.values"
:limit="16"
class="list-unstyled"
:is-static="isStatic"
:expand="expandLinks"
>
<template #default="s">
<Opinion
:word="s.el.value"
:opinion="s.el.opinion"
:custom-opinions="profile.opinions"
:markdown="profile.markdown"
/>
</template>
</ExpandableList>
</div>
</template>
</div>
</section>
<section v-if="(profile.events?.length ?? 0) + (profile.customEvents?.length ?? 0) > 0 && !isStatic && year" class="clearfix">
<h3>
<Icon v="calendar" />
<T>profile.calendar.header</T>
</h3>
<PersonalCalendar :year="year" :events="[...profile.events, ...profile.customEvents]" />
</section>
<section v-if="(profile.circle?.length ?? 0) > 0 && !isStatic" class="clearfix">
<h3>
<Icon v="heart-circle" />
<T>profile.circles.header</T>
</h3>
<div class="row">
<div v-for="connection in profile.circle" class="col-12 col-lg-4 pt-2 pb-2">
<Avatar :user="connection" :src="connection.avatar" class="float-start me-2" dsize="4rem" />
<h4>
<LocaleLink :link="`/@${connection.username}`" :locale="connection.locale">
@{{ connection.username }}
</LocaleLink>
<Tooltip v-if="connection.circleMutual" :text="$t('profile.circles.mutual')" class="small">
<Icon v="shield-check" set="s" />
</Tooltip>
</h4>
<p>{{ connection.relationship }}</p>
</div>
</div>
</section>
<section>
<OpinionLegend :custom="profile.opinions" :used="usedOpinions" />
<ul v-if="!isStatic" class="list-inline text-muted small text-center">
<li v-if="profile.id" class="list-inline-item">
<T>profile.creation</T><T>quotation.colon</T>
{{ $date($ulidTime(profile.id)) }}
</li>
<li v-if="profile.lastUpdate" class="list-inline-item">
<T>profile.lastUpdate</T><T>quotation.colon</T>
{{ $date($ulidTime(profile.lastUpdate)) }}
</li>
</ul>
</section>
</template>
<section v-else class="py-5">
<div class="alert alert-info text-center">
<p class="h5">
<Icon v="info-circle" />
<T>profile.visibility.accessDenied</T>
</p>
<p class="mb-0 pt-2">
<nuxt-link :to="{ name: 'user' }" class="btn btn-primary">
<Icon v="sign-in-alt" />
<T>user.login.action</T>
</nuxt-link>
</p>
</div>
</section>
</div>
</template>
<style lang="scss" scoped>
.avatar {
width: 100%;
max-width: 5rem;
max-height: 5rem;
}
.mw-50 {
min-width: 50%;
}
</style>