Merge branch 'little-style-tweaks' into 'main'

(style) little tweaks

See merge request PronounsPage/PronounsPage!519
This commit is contained in:
Valentyne Stigloher 2024-09-27 11:17:57 +00:00
commit 7afc2e9538
13 changed files with 143 additions and 133 deletions

View File

@ -323,9 +323,8 @@ body:not(.reduced-colours) {
}
}
.badge-lg {
--bs-badge-font-size: 0.95em;
//--bs-badge-font-weight: normal;
.badge-sm {
--bs-badge-font-size: 0.75rem;
}
.scroll-mt-7 {

View File

@ -56,13 +56,13 @@
</nuxt-link>
</p>
<p class="mb-0">
<Avatar :user="$user()" validate />
<Avatar :user="user" validate />
</p>
<div>
<p class="mt-3 mb-1">
<strong><T>user.avatar.change</T><T>quotation.colon</T></strong>
</p>
<div v-if="$user().avatarSource === 'gravatar'" class="mt-3">
<div v-if="user.avatarSource === 'gravatar'" class="mt-3">
<a href="https://gravatar.com" target="_blank" rel="noopener" class="small">
<Icon v="external-link" />
Gravatar
@ -71,10 +71,10 @@
<div v-else class="mt-3">
Gravatar:
<a href="#" @click.prevent="setAvatar('gravatar')">
<Avatar :user="$user()" :src="gravatar($user())" dsize="2rem" />
<Avatar :user="user" :src="gravatar(user)" dsize="2rem" />
</a>
</div>
<div v-if="$user().avatarSource">
<div v-if="user.avatarSource">
<a href="#" class="small" @click.prevent="setAvatar(null)">
<Icon v="trash" />
<T>crud.remove</T>
@ -176,7 +176,7 @@
<section class="mt-5">
<a href="#" class="btn btn-outline-danger" @click.prevent="logout">
<Icon v="sign-out" />
<T :params="{ username: $user().username }">user.logout</T>
<T :params="{ username: user.username }">user.logout</T>
</a>
<a
@ -191,7 +191,7 @@
<a href="#" class="btn btn-outline-danger" @click.prevent="deleteAccount">
<Icon v="trash-alt" />
<T :params="{ username: $user().username }">user.deleteAccount</T>
<T :params="{ username: user.username }">user.deleteAccount</T>
</a>
<a v-if="impersonationActive" href="#" class="btn btn-outline-primary" @click.prevent="stopImpersonation">
@ -247,7 +247,7 @@
@set-avatar="setAvatar"
/>
</li>
<li :class="['list-group-item', $user().mfa ? 'profile-current' : '']">
<li :class="['list-group-item', user.mfa ? 'profile-current' : '']">
<MfaConnection />
</li>
</ul>
@ -272,7 +272,7 @@
<T>profile.backup.headerShort</T>
</template>
<template #backup>
<CardsBackup v-if="!$user().bannedReason" />
<CardsBackup v-if="!user.bannedReason" />
</template>
</TabsNav>
@ -289,27 +289,32 @@
</section>
</template>
<script>
import { useNuxtApp, useCookie, useFetch } from 'nuxt/app';
<script lang="ts">
import { useCookie, useFetch } from 'nuxt/app';
import { longtimeCookieSetting } from '../src/cookieSettings.ts';
import { socialProviders } from '../src/socialProviders.ts';
import { gravatar } from '../src/helpers.ts';
import { mapState } from 'pinia';
import { usernameRegex } from '../src/username.ts';
import useConfig from '../composables/useConfig.ts';
import useDialogue from '../composables/useDialogue.ts';
import { useMainStore } from '../store/index.ts';
import type { Profile } from '~/src/profile.ts';
export default {
export default defineComponent({
async setup() {
const { $user } = useNuxtApp();
const dialogue = useDialogue();
const store = useMainStore();
const user = store.user;
if (user === null) {
throw 'no user';
}
const tokenCookie = useCookie('token', longtimeCookieSetting);
const impersonatorCookie = useCookie('impersonator');
const termsUpdateDismissed3Cookie = useCookie('termsUpdateDismissed3', longtimeCookieSetting);
const { data: profilesData } = useFetch(`/api/profile/get/${$user().username}?version=2&props=hide`, {
const { data: profilesData } = useFetch(`/api/profile/get/${user.username}?version=2&props=hide`, {
lazy: true,
});
const { data: socialConnections } = useFetch('/api/user/social-connections', {
@ -319,6 +324,11 @@ export default {
return {
config: useConfig(),
dialogue,
user,
username: ref(user.username),
email: ref(user.email),
socialLookup: ref(user.socialLookup),
accounts: store.accounts,
tokenCookie,
impersonatorCookie,
termsUpdateDismissed3Cookie,
@ -328,15 +338,12 @@ export default {
},
data() {
return {
username: this.$user().username,
email: this.$user().email,
message: '',
messageParams: {},
messageIcon: null,
messageIcon: null as string | null,
error: '',
changeEmailAuthId: null,
code: '',
code: '' as string | null,
socialProviders,
@ -354,10 +361,40 @@ export default {
showTermsUpdate: new Date() < new Date(2023, 3, 6) &&
!this.termsUpdateDismissed3Cookie,
socialLookup: this.$user().socialLookup,
};
},
computed: {
profiles() {
return this.profilesData?.profiles;
},
impersonationActive() {
return !!this.impersonatorCookie;
},
canChangeEmail() {
return this.email && this.captchaToken;
},
usernameError() {
if (!this.username.match(usernameRegex)) {
return this.$t('user.account.changeUsername.invalid');
}
if (this.username !== encodeURIComponent(this.username)) {
return this.$t('user.account.changeUsername.nonascii', { encoded: encodeURIComponent(this.username) });
}
return null;
},
},
watch: {
email(v) {
if (v !== this.user.email) {
this.showCaptcha = true;
}
},
async socialLookup(v) {
const response = await this.dialogue.postWithAlertOnError<any>('/api/user/set-social-lookup', { socialLookup: v });
this.$setToken(response.token);
},
},
async mounted() {
const user = await $fetch('/api/user/current');
if (user) {
@ -365,7 +402,7 @@ export default {
}
const redirectTo = window.sessionStorage.getItem('after-login');
if (this.$user() && redirectTo) {
if (this.user && redirectTo) {
window.sessionStorage.removeItem('after-login');
await this.$router.push(redirectTo);
}
@ -379,7 +416,7 @@ export default {
}
this.savingUsername = true;
try {
const response = await this.dialogue.postWithAlertOnError('/api/user/change-username', {
const response = await this.dialogue.postWithAlertOnError<any>('/api/user/change-username', {
username: this.username,
});
@ -388,9 +425,9 @@ export default {
return;
}
this.$removeToken(this.$user().username);
this.$removeToken(this.user.username);
this.$setToken(response.token);
this.username = this.$user().username;
this.username = this.user.username;
this.message = 'crud.saved';
this.messageParams = {};
this.messageIcon = 'check-circle';
@ -407,7 +444,7 @@ export default {
}
this.savingEmail = true;
try {
const response = await this.dialogue.postWithAlertOnError('/api/user/change-email', {
const response = await this.dialogue.postWithAlertOnError<any>('/api/user/change-email', {
email: this.email,
authId: this.changeEmailAuthId,
code: this.code,
@ -424,8 +461,8 @@ export default {
this.message = 'user.login.emailSent';
this.messageParams = { email: this.addBrackets(this.email) };
this.messageIcon = 'envelope-open-text';
this.$nextTick((_) => {
this.$refs.code.focus();
this.$nextTick(() => {
(this.$refs.code as HTMLInputElement).focus();
});
} else {
this.changeEmailAuthId = null;
@ -456,7 +493,7 @@ export default {
this.logoutInProgress = false;
setTimeout(() => window.location.reload(), 300);
},
setProfiles(profiles) {
setProfiles(profiles: Record<string, Partial<Profile>>) {
this.profiles = profiles;
},
async deleteAccount() {
@ -470,16 +507,16 @@ export default {
this.logout();
}
},
async setAvatar(source) {
const response = await this.dialogue.postWithAlertOnError('/api/user/set-avatar', { source });
async setAvatar(source: string | null) {
const response = await this.dialogue.postWithAlertOnError<any>('/api/user/set-avatar', { source });
this.$setToken(response.token);
},
async uploaded(ids) {
async uploaded(ids: string[]) {
await this.setAvatar(`${this.$config.public.cloudfront}/images/${ids[0]}-avatar.png`);
},
async stopImpersonation() {
this.$removeToken(this.$user().username);
this.$removeToken(this.user.username);
this.tokenCookie = this.impersonatorCookie;
this.impersonatorCookie = null;
setTimeout(() => window.location.reload(), 300);
@ -488,47 +525,11 @@ export default {
this.termsUpdateDismissed3Cookie = 'true';
this.showTermsUpdate = false;
},
addBrackets(str) {
addBrackets(str: string): string {
return str ? `(${str})` : '';
},
},
computed: {
...mapState(useMainStore, [
'user',
'accounts',
]),
profiles() {
return this.profilesData?.profiles;
},
impersonationActive() {
return !!this.impersonatorCookie;
},
canChangeEmail() {
return this.email && this.captchaToken;
},
usernameError() {
if (!this.username.match(usernameRegex)) {
return this.$t('user.account.changeUsername.invalid');
}
if (this.username !== encodeURIComponent(this.username)) {
return this.$t('user.account.changeUsername.nonascii', { encoded: encodeURIComponent(this.username) });
}
return null;
},
},
watch: {
email(v) {
if (v !== this.$user().email) {
this.showCaptcha = true;
}
},
async socialLookup(v) {
const response = await this.dialogue.postWithAlertOnError('/api/user/set-social-lookup', { socialLookup: v });
this.$setToken(response.token);
},
},
};
});
</script>
<style lang="scss" scoped>

View File

@ -5,6 +5,7 @@
<script lang="ts">
import { useRuntimeConfig } from 'nuxt/app';
import { defineComponent } from 'vue';
import type { PropType } from 'vue';
import useConfig from '../composables/useConfig.ts';
import useDark from '../composables/useDark.ts';
@ -23,7 +24,7 @@ declare global {
export default defineComponent({
props: {
modelValue: { default: '', type: String },
modelValue: { default: '', type: String as PropType<string | null> },
},
emits: ['update:modelValue'],
setup() {

View File

@ -14,9 +14,9 @@
import { defineComponent } from 'vue';
import type { PropType } from 'vue';
const buildQueue = (v: string | (string | undefined)[]): { value: string, fallbacks: (string | undefined)[] } => {
let values = Array.isArray(v) ? v : [v];
values = values.filter((x) => !!x);
const buildQueue = (v: string | (string | undefined)[] | null): { value: string, fallbacks: string[] } => {
const rawValues = Array.isArray(v) ? v : [v];
let values = rawValues.filter((x) => !!x) as string[];
if (!values.length) {
values = ['spacer'];
@ -30,7 +30,7 @@ const buildQueue = (v: string | (string | undefined)[]): { value: string, fallba
export default defineComponent({
props: {
v: { required: true, type: [String, Array] as PropType<string | (string | undefined)[]> },
v: { required: true, type: [String, Array] as PropType<string | (string | undefined)[] | null> },
set: { default: 'l', type: String as PropType<'l' | 's' | 'b'> },
size: { default: 1, type: Number },
inverse: { type: Boolean },

View File

@ -1,23 +1,23 @@
<template>
<div class="d-flex flex-column flex-md-row justify-content-between align-items-center">
<span class="my-2 me-3 text-nowrap">
<div class="d-md-flex justify-content-between align-items-center">
<h4 class="my-md-0">
<Icon v="mobile" />
<T>user.mfa.header</T>
</span>
</h4>
<Spinner v-if="requesting" size="2rem" />
<template v-else>
<div v-if="$user().mfa">
<span class="badge badge-lg bg-success">
<span class="badge bg-success fs-6">
<Icon v="shield-check" />
<T>user.mfa.enabled</T>
</span>
<button class="badge badge-lg bg-light text-dark border" @click.prevent="disable">
<button class="btn btn-light border" @click.prevent="disable">
<Icon v="unlink" />
<T>user.mfa.disable</T>
</button>
</div>
<div v-else-if="!secret">
<button class="badge badge-lg bg-light text-dark border" @click="getLink">
<button class="btn btn-light border" @click="getLink">
<Icon v="link" />
<T>user.mfa.enable</T>
</button>

View File

@ -1,26 +1,26 @@
<template>
<div class="d-flex justify-content-between align-items-center">
{{ $locales[locale].name }}
<span v-if="profile">
<LocaleLink :locale="locale" :link="`/@${username}`" class="badge badge-lg bg-primary text-white text-white">
<div class="d-md-flex justify-content-between align-items-center">
<h4 class="my-md-0">
{{ $locales[locale].name }}
</h4>
<div v-if="profile" class="d-flex gap-2">
<LocaleLink :locale="locale" :link="`/@${username}`" class="btn btn-primary">
<Icon v="id-card" />
<T>profile.show</T>
</LocaleLink>
<LocaleLink :locale="locale" link="/editor" class="badge badge-lg bg-light text-dark border">
<LocaleLink :locale="locale" link="/editor" class="btn btn-light border">
<Icon v="edit" />
<T>profile.edit</T>
</LocaleLink>
<Spinner v-if="deleting" />
<a v-else href="#" class="badge badge-lg bg-light text-dark" :aria-label="$t('profile.delete')" @click.prevent="removeProfile(locale)">
<a v-else href="#" class="btn btn-outline-danger" :aria-label="$t('profile.delete')" @click.prevent="removeProfile(locale)">
<Icon v="trash-alt" />
</a>
</span>
<span v-else>
<LocaleLink :locale="locale" link="/editor" class="badge badge-lg bg-light text-dark border">
<Icon v="plus-circle" />
<T>profile.init</T>
</LocaleLink>
</span>
</div>
<LocaleLink v-else :locale="locale" link="/editor" class="btn btn-light border">
<Icon v="plus-circle" />
<T>profile.init</T>
</LocaleLink>
</div>
</template>

View File

@ -1,21 +1,21 @@
<template>
<div class="d-flex flex-column flex-md-row justify-content-between align-items-center">
<span class="my-2">
<div class="d-md-flex justify-content-between align-items-center">
<h4 class="my-md-0 d-flex align-items-center gap-2">
<Icon
:v="providerOptions.icon || provider"
set="b"
:class="[providerOptions.icon && providerOptions.icon.endsWith('.png') ? 'mx-1 invertible' : '']"
/>
{{ providerOptions.name }}
<button v-if="providerOptions.deprecated" class="badge bg-light text-dark border border-warning" @click="depreciationNotice(providerOptions.deprecated)">
<button v-if="providerOptions.deprecated" class="badge badge-sm text-bg-light border border-warning" @click="depreciationNotice(providerOptions.deprecated)">
<Icon v="exclamation-triangle" />
<T>user.login.deprecated</T>
</button>
<button v-if="providerOptions.broken" class="badge bg-light text-dark border border-warning" @click="brokenNotice()">
<button v-if="providerOptions.broken" class="badge badge-sm text-bg-light border border-warning" @click="brokenNotice()">
<Icon v="exclamation-triangle" />
<T>user.login.broken</T>
</button>
</span>
</h4>
<span v-if="connection === undefined">
<template v-if="providerOptions.instanceRequired">
<form
@ -35,7 +35,7 @@
<Icon v="link" />
</button>
</form>
<button v-else class="badge badge-lg bg-light text-dark border" @click="formShown = true">
<button v-else class="btn btn-light border" @click="formShown = true">
<Icon v="link" />
<T>user.socialConnection.connect</T>
</button>
@ -43,7 +43,7 @@
<a
v-else
:href="buildSocialLoginConnectLink(config.locale, provider, providerOptions)"
class="badge badge-lg bg-light text-dark border"
class="btn btn-light border"
>
<Icon v="link" />
<T>user.socialConnection.connect</T>
@ -61,13 +61,13 @@
<br class="d-md-none">
<a
:href="buildSocialLoginConnectLink(config.locale, provider, providerOptions, connection.name.split('@').slice(-1)[0])"
class="badge badge-lg bg-light text-dark border"
class="btn btn-light border"
>
<Icon v="sync" />
<T>user.socialConnection.refresh</T>
</a>
<Spinner v-if="disconnecting" />
<a v-else href="#" class="badge badge-lg bg-light text-dark" @click.prevent="disconnect">
<a v-else href="#" class="btn btn-light border" @click.prevent="disconnect">
<Icon v="unlink" />
<T>user.socialConnection.disconnect</T>
</a>

View File

@ -63,9 +63,9 @@
</div>
<div class="col-12 col-lg-4">
<div class="form-group">
<label class="text-nowrap"><strong>
<T>terminology.images</T>
</strong></label>
<label>
<strong><T>terminology.images</T></strong>
</label>
<ImageWidget v-model="form.images" multiple sizes="flag" small-size="flag" big-size="flag" />
</div>
</div>

View File

@ -1,15 +1,16 @@
<template>
<Page>
<h2 class="d-flex justify-content-between">
<h2
class="d-flex justify-content-between align-items-start align-items-md-center
flex-column flex-md-row gap-2"
>
<span>
<Icon v="pen-nib" />
<T>links.blog</T>
</span>
<span>
<a href="/blog.atom" target="_blank" rel="noopener" class="btn btn-sm btn-outline-primary">
<T icon="rss">links.blogFeed</T>
</a>
</span>
<a href="/blog.atom" target="_blank" rel="noopener" class="btn btn-sm btn-outline-primary">
<T icon="rss">links.blogFeed</T>
</a>
</h2>
<AdPlaceholder :phkey="['content-0', 'content-mobile-0']" />
<Loading :value="posts">

View File

@ -3,17 +3,18 @@
<div v-if="year" :class="basic ? 'py-5' : ''">
<CommunityNav v-if="!basic" />
<h2 class="d-flex justify-content-between flex-column flex-md-row">
<h2
class="d-flex justify-content-between align-items-start align-items-md-center
flex-column flex-md-row gap-2"
>
<span>
<Icon v="calendar-star" />
<T>calendar.headerLong</T> <small class="text-muted">({{ year.year }})</small>
</span>
<span v-if="basic" class="h4 mt-2">
<nuxt-link :to="{ name: 'calendar' }">
<Logo />
<T>domain</T>/{{ config.calendar.route }}
</nuxt-link>
</span>
<nuxt-link v-if="basic" :to="{ name: 'calendar' }" class="fs-4">
<Logo />
<T>domain</T>/{{ config.calendar.route }}
</nuxt-link>
<span v-else class="btn-group">
<button :class="['btn', labels ? 'btn-outline-primary' : 'btn-primary']" @click="labels = false">
<Icon v="table" />
@ -41,7 +42,7 @@
</section>
<section v-else class="row pb-4">
<div v-for="i in 12" class="col-12 col-lg-3 py-3">
<div v-for="i in 12" class="col-12 col-sm-6 col-lg-3 py-3">
<h3 class="text-center">
<T>calendar.months.{{ i }}</T>
</h3>

View File

@ -3,18 +3,19 @@
<div v-if="year && year.eventsByDate[day.toString()] || basic" :class="basic ? 'py-5' : ''">
<CommunityNav v-if="!basic" />
<h2 class="d-flex justify-content-between">
<h2
class="d-flex justify-content-between align-items-start align-items-md-center
flex-column flex-md-row gap-2"
>
<span>
<Icon v="calendar-star" />
<nuxt-link :to="{ name: 'calendar' }"><T>calendar.headerLong</T></nuxt-link>
<small class="text-muted">({{ day }})</small>
</span>
<span v-if="basic" class="h4 mt-2">
<nuxt-link :to="{ name: 'calendar' }">
<Logo />
<T>domain</T>/{{ config.calendar.route }}
</nuxt-link>
</span>
<nuxt-link v-if="basic" :to="{ name: 'calendar' }" class="fs-4">
<Logo />
<T>domain</T>/{{ config.calendar.route }}
</nuxt-link>
</h2>
<AdPlaceholder v-if="!basic" phkey="main-0" />

View File

@ -2,7 +2,10 @@
<Page>
<NotFound v-if="pronounGroups === undefined" />
<div v-else>
<h2 class="d-flex justify-content-between">
<h2
class="d-flex justify-content-between align-items-start align-items-md-center
flex-column flex-md-row gap-2"
>
<span>
<Icon v="tag" />
<T>pronouns.intro</T><T>quotation.colon</T>

View File

@ -2,7 +2,10 @@
<Page>
<NotFound v-if="!selectedPronoun" />
<div v-else>
<h2 class="d-md-flex justify-content-between">
<h2
class="d-flex justify-content-between align-items-start align-items-md-center
flex-column flex-md-row gap-2"
>
<div>
<Icon v="tag" />
<T>pronouns.intro</T><T>quotation.colon</T>