(user)(ui) revamp the account page

This commit is contained in:
Andrea Vos 2024-04-16 20:55:39 -05:00
parent 0f21051a08
commit 44e656744b
15 changed files with 240 additions and 197 deletions

View File

@ -318,3 +318,8 @@ body:not(.reduced-colours) {
border-left: 3px solid #{$value};
}
}
.badge-lg {
--bs-badge-font-size: 0.95em;
//--bs-badge-font-weight: normal;
}

View File

@ -33,204 +33,235 @@
</button>
</p>
</div>
<div class="card mb-3">
<div class="card-body d-flex flex-column flex-md-row">
<div class="mx-2 text-center">
<p v-if="$isGranted('panel') || $isGranted('users')">
<nuxt-link to="/admin" class="badge bg-primary text-white">
<Icon v="collective-logo.svg" class="inverted" />
<T>contact.team.member</T>
</nuxt-link>
</p>
<p class="mb-0">
<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">
<a href="https://gravatar.com" target="_blank" rel="noopener" class="small">
<Icon v="external-link" />
Gravatar
</a>
</div>
<div v-else class="mt-3">
Gravatar:
<a href="#" @click.prevent="setAvatar('gravatar')">
<Avatar :user="$user()" :src="gravatar($user())" dsize="2rem" />
</a>
</div>
<div v-if="$user().avatarSource">
<a href="#" class="small" @click.prevent="setAvatar(null)">
<Icon v="trash" />
<T>crud.remove</T>
</a>
</div>
<ImageUploader small sizes="avatar" @uploaded="uploaded" />
<p class="small my-2 avatar-social-hint">
<T>user.avatar.social</T>
</p>
</div>
</div>
<div class="mx-2 flex-grow-1">
<Alert type="danger" :message="error" />
<div v-if="message" class="alert alert-success">
<p class="mb-0 narrow-message">
<Icon :v="messageIcon" />
<T :params="messageParams">{{ message }}</T>
</p>
</div>
<form :disabled="savingUsername" @submit.prevent="changeUsername">
<h3 class="h6">
<T>user.account.changeUsername.header</T>
</h3>
<input
v-model="username"
type="text"
class="form-control mb-3"
required
minlength="4"
maxlength="16"
>
<p v-if="usernameError" class="small text-danger">
<Icon v="exclamation-triangle" />
<span class="ml-1">{{ usernameError }}</span>
</p>
<div class="d-none d-md-block mt-3">
<button class="btn btn-outline-primary" :disabled="username === user.username">
<T>user.account.changeUsername.action</T>
</button>
<TabsNav
:tabs="['general', 'cards', 'socials', 'circles', 'backup']"
pills
showheaders
navclass="mb-3 border-bottom-0"
>
<template #general-header>
<Icon v="user" />
<T>user.headerLong</T>
</template>
<template #general>
<div class="card mb-3">
<div class="card-body d-flex flex-column flex-md-row">
<div class="mx-2 text-center">
<p v-if="$isGranted('panel') || $isGranted('users')">
<nuxt-link to="/admin" class="badge bg-primary text-white">
<Icon v="collective-logo.svg" class="inverted" />
<T>contact.team.member</T>
</nuxt-link>
</p>
<p class="mb-0">
<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">
<a href="https://gravatar.com" target="_blank" rel="noopener" class="small">
<Icon v="external-link" />
Gravatar
</a>
</div>
<div v-else class="mt-3">
Gravatar:
<a href="#" @click.prevent="setAvatar('gravatar')">
<Avatar :user="$user()" :src="gravatar($user())" dsize="2rem" />
</a>
</div>
<div v-if="$user().avatarSource">
<a href="#" class="small" @click.prevent="setAvatar(null)">
<Icon v="trash" />
<T>crud.remove</T>
</a>
</div>
<ImageUploader small sizes="avatar" @uploaded="uploaded" />
<p class="small my-2 avatar-social-hint">
<T>user.avatar.social</T>
</p>
</div>
</div>
<div class="d-block-force d-md-none mt-3">
<button class="btn btn-outline-primary w-100" :disabled="username === user.username">
<T>user.account.changeUsername.action</T>
</button>
</div>
</form>
<div class="mx-2 flex-grow-1">
<Alert type="danger" :message="error" />
<hr>
<div v-if="message" class="alert alert-success">
<p class="mb-0 narrow-message">
<Icon :v="messageIcon" />
<T :params="messageParams">{{ message }}</T>
</p>
</div>
<form :disabled="savingEmail" @submit.prevent="changeEmail">
<h3 class="h6">
<T>user.account.changeEmail.header</T>
</h3>
<div v-if="!changeEmailAuthId" class="">
<input v-model="email" type="email" class="form-control mb-3" required>
<div class="d-flex flex-column flex-md-row">
<Captcha v-if="showCaptcha" v-model="captchaToken" />
<div :class="['d-none', 'd-md-block', showCaptcha ? 'ms-3' : '']">
<button class="btn btn-outline-primary" :disabled="!canChangeEmail">
<T>user.account.changeEmail.action</T>
<form :disabled="savingUsername" @submit.prevent="changeUsername">
<h3 class="h6">
<T>user.account.changeUsername.header</T>
</h3>
<input
v-model="username"
type="text"
class="form-control mb-3"
required
minlength="4"
maxlength="16"
>
<p v-if="usernameError" class="small text-danger">
<Icon v="exclamation-triangle" />
<span class="ml-1">{{ usernameError }}</span>
</p>
<div class="d-none d-md-block mt-3">
<button class="btn btn-outline-primary" :disabled="username === user.username">
<T>user.account.changeUsername.action</T>
</button>
</div>
<div class="d-block-force d-md-none mt-3">
<button class="btn btn-outline-primary w-100" :disabled="!canChangeEmail">
<T>user.account.changeEmail.action</T>
<button class="btn btn-outline-primary w-100" :disabled="username === user.username">
<T>user.account.changeUsername.action</T>
</button>
</div>
</div>
</form>
<hr>
<form :disabled="savingEmail" @submit.prevent="changeEmail">
<h3 class="h6">
<T>user.account.changeEmail.header</T>
</h3>
<div v-if="!changeEmailAuthId" class="">
<input v-model="email" type="email" class="form-control mb-3" required>
<div class="d-flex flex-column flex-md-row">
<Captcha v-if="showCaptcha" v-model="captchaToken" />
<div :class="['d-none', 'd-md-block', showCaptcha ? 'ms-3' : '']">
<button class="btn btn-outline-primary" :disabled="!canChangeEmail">
<T>user.account.changeEmail.action</T>
</button>
</div>
<div class="d-block-force d-md-none mt-3">
<button class="btn btn-outline-primary w-100" :disabled="!canChangeEmail">
<T>user.account.changeEmail.action</T>
</button>
</div>
</div>
</div>
<div v-else class="input-group mb-3">
<input
ref="code"
v-model="code"
type="text"
class="form-control text-center"
placeholder="000000"
autofocus
required
minlength="0"
maxlength="6"
inputmode="numeric"
pattern="[0-9]{6}"
autocomplete="one-time-code"
>
<button class="btn btn-outline-primary">
<Icon v="key" />
<T>user.code.action</T>
</button>
</div>
</form>
</div>
<div v-else class="input-group mb-3">
<input
ref="code"
v-model="code"
type="text"
class="form-control text-center"
placeholder="000000"
autofocus
required
minlength="0"
maxlength="6"
inputmode="numeric"
pattern="[0-9]{6}"
autocomplete="one-time-code"
>
<button class="btn btn-outline-primary">
<Icon v="key" />
<T>user.code.action</T>
</button>
</div>
</div>
<AdPlaceholder :phkey="['content-0', 'content-mobile-0']" />
<section class="mt-5">
<a href="#" class="btn btn-outline-danger" @click.prevent="logout">
<Icon v="sign-out" />
<T>user.logout</T>
@{{ $user().username }}
</a>
<a href="#" class="btn btn-outline-danger" @click.prevent="deleteAccount">
<Icon v="trash-alt" />
<T>user.deleteAccount</T>
@{{ $user().username }}
</a>
<a v-if="impersonationActive" href="#" class="btn btn-outline-primary" @click.prevent="stopImpersonation">
<Icon v="user-secret" />
Stop impersonation
</a>
</section>
</template>
<template #cards-header>
<Icon v="id-card" />
<T>profile.list</T>
</template>
<template #cards>
<Loading :value="profiles">
<ul v-if="profiles !== undefined" class="list-group">
<li
v-for="locale in Object.keys($locales)"
:key="locale"
:class="['list-group-item', locale === $config.locale ? 'profile-current' : '']"
>
<ProfileOverview :username="username" :profile="profiles[locale]" :locale="locale" @update="setProfiles" />
</li>
</ul>
</Loading>
<AdPlaceholder :phkey="['content-1', 'content-mobile-1']" />
</template>
<template #socials-header>
<Icon v="sign-in-alt"/>
<T>user.socialConnection.header</T>
</template>
<template #socials>
<Loading :value="socialConnections">
<template #header>
<div class="form-check form-switch my-2">
<label>
<input v-model="socialLookup" class="form-check-input" type="checkbox">
<T>user.socialLookup</T>
<br>
<small><T>user.socialLookupWhy</T></small>
</label>
</div>
</form>
</div>
</div>
</div>
<AdPlaceholder :phkey="['content-0', 'content-mobile-0']" />
<Loading :value="profiles">
<template #header>
<h3 class="h4">
<T>profile.list</T><T>quotation.colon</T>
</h3>
</template>
<ul v-if="socialConnections !== undefined" class="list-group">
<li v-for="(providerOptions, provider) in socialProviders" :key="provider" :class="['list-group-item', socialConnections[provider] !== undefined ? 'profile-current' : '']">
<SocialConnection
:provider="provider"
:provider-options="providerOptions"
:connection="socialConnections[provider]"
@disconnected="socialConnections[provider] = undefined"
@setAvatar="setAvatar"
/>
</li>
<li :class="['list-group-item', $user().mfa ? 'profile-current' : '']">
<MfaConnection />
</li>
</ul>
</Loading>
</template>
<ul v-if="profiles !== undefined" class="list-group">
<li
v-for="locale in Object.keys($locales)"
:key="locale"
:class="['list-group-item', locale === $config.locale ? 'profile-current' : '']"
>
<ProfileOverview :username="username" :profile="profiles[locale]" :locale="locale" @update="setProfiles" />
</li>
</ul>
</Loading>
<AdPlaceholder :phkey="['content-1', 'content-mobile-1']" />
<Loading :value="socialConnections">
<template #header>
<h3 class="h4">
<T>user.socialConnection.list</T><T>quotation.colon</T>
</h3>
<div class="form-check form-switch my-2">
<label>
<input v-model="socialLookup" class="form-check-input" type="checkbox">
<T>user.socialLookup</T>
<br>
<small><T>user.socialLookupWhy</T></small>
</label>
</div>
<template #circles-header>
<Icon v="heart-circle" />
<T>profile.circles.header</T>
</template>
<template #circles>
<h5><T>profile.circles.yourMentions.header</T><T>quotation.colon</T></h5>
<CircleMentions />
</template>
<ul v-if="socialConnections !== undefined" class="list-group">
<li v-for="(providerOptions, provider) in socialProviders" :key="provider" :class="['list-group-item', socialConnections[provider] !== undefined ? 'profile-current' : '']">
<SocialConnection
:provider="provider"
:provider-options="providerOptions"
:connection="socialConnections[provider]"
@disconnected="socialConnections[provider] = undefined"
@setAvatar="setAvatar"
/>
</li>
<li :class="['list-group-item', $user().mfa ? 'profile-current' : '']">
<MfaConnection />
</li>
</ul>
</Loading>
<CircleMentions />
<CardsBackup v-if="!$user().bannedReason" />
<section class="mt-5">
<a href="#" class="badge bg-light text-dark border" @click.prevent="logout">
<Icon v="sign-out" />
<T>user.logout</T>
@{{ $user().username }}
</a>
<a href="#" class="badge bg-light text-dark border" @click.prevent="deleteAccount">
<Icon v="trash-alt" />
<T>user.deleteAccount</T>
@{{ $user().username }}
</a>
<a v-if="impersonationActive" href="#" class="badge bg-light text-dark border border-primary" @click.prevent="stopImpersonation">
<Icon v="user-secret" />
Stop impersonation
</a>
</section>
<template #backup-header>
<Icon v="copy"/>
<T>profile.backup.headerShort</T>
</template>
<template #backup>
<CardsBackup v-if="!$user().bannedReason" />
</template>
</TabsNav>
<AdPlaceholder :phkey="['content-2', 'content-mobile-2']" />

View File

@ -1,8 +1,8 @@
<template>
<section>
<h3 class="h4">
<h5>
<T>profile.backup.header</T>
</h3>
</h5>
<ul class="list-inline">
<li class="list-inline-item">
<a

View File

@ -1,10 +1,5 @@
<template>
<Loading :value="circleMentions">
<template #header>
<h3 class="h4">
<T>profile.circles.yourMentions.header</T><T>quotation.colon</T>
</h3>
</template>
<p class="small text-muted">
<T>profile.circles.yourMentions.description</T>
</p>

View File

@ -6,17 +6,17 @@
<Icon v="id-card" />
<T>profile.show</T>
</LocaleLink>
<LocaleLink :locale="locale" link="/editor" class="badge bg-light text-dark border">
<LocaleLink :locale="locale" link="/editor" class="badge badge-lg bg-light text-dark border">
<Icon v="edit" />
<T>profile.edit</T>
</LocaleLink>
<Spinner v-if="deleting" />
<a v-else href="#" class="badge bg-light text-dark" :aria-label="$t('profile.delete')" @click.prevent="removeProfile(locale)">
<a v-else href="#" class="badge badge-lg bg-light text-dark" :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 bg-light text-dark border">
<LocaleLink :locale="locale" link="/editor" class="badge badge-lg bg-light text-dark border">
<Icon v="plus-circle" />
<T>profile.init</T>
</LocaleLink>

View File

@ -31,7 +31,7 @@
<Icon v="link" />
</button>
</form>
<button v-else class="badge bg-light text-dark border" @click="formShown = true">
<button v-else class="badge badge-lg bg-light text-dark border" @click="formShown = true">
<Icon v="link" />
<T>user.socialConnection.connect</T>
</button>
@ -39,7 +39,7 @@
<a
v-else
:href="providerOptions.redirectViaHome ? `${homeUrl}/api/user/social-redirect/${provider}/${$config.locale}` : `/api/connect/${provider}`"
class="badge bg-light text-dark border"
class="badge badge-lg bg-light text-dark border"
>
<Icon v="link" />
<T>user.socialConnection.connect</T>
@ -57,13 +57,13 @@
<br class="d-md-none">
<a
:href="(providerOptions.redirectViaHome ? `${homeUrl}/api/user/social-redirect/${provider}/${$config.locale}` : `/api/connect/${provider}`) + (providerOptions.instanceRequired ? `?instance=${connection.name.split('@').slice(-1)[0]}` : '')"
class="badge bg-light text-dark border"
class="badge badge-lg bg-light text-dark border"
>
<Icon v="sync" />
<T>user.socialConnection.refresh</T>
</a>
<Spinner v-if="disconnecting" />
<a v-else href="#" class="badge bg-light text-dark" @click.prevent="disconnect">
<a v-else href="#" class="badge badge-lg bg-light text-dark" @click.prevent="disconnect">
<Icon v="unlink" />
<T>user.socialConnection.disconnect</T>
</a>

View File

@ -600,6 +600,7 @@ user:
deleteAccount: 'Delete account'
deleteAccountConfirm: 'Are you sure you want to remove your account? This will be irreversible!'
socialConnection:
header: 'Login methods'
list: 'Social media connections'
connect: 'Connect'
refresh: 'Refresh'
@ -814,6 +815,7 @@ profile:
pronouns: 'Include pronouns'
backup:
header: 'Cards backup'
headerShort: 'Cards backup'
export:
action: 'Generate backup'
success: 'A backup file is being generated, download should begin shortly'

View File

@ -760,6 +760,7 @@ profile:
pronouns: 'Pronomen einfügen'
backup:
header: 'Visitenkartensicherung'
headerShort: 'Ensicherung'
export:
action: 'Sicherung erstellen'
success: 'Eine Sicherungsdatei wurde erstellt, der Download sollte in Kürze beginnen'

View File

@ -776,6 +776,7 @@ user:
deleteAccount: 'Delete account'
deleteAccountConfirm: 'Are you sure you want to remove your account? This will be irreversible!'
socialConnection:
header: 'Login methods'
list: 'Social media connections'
connect: 'Connect'
refresh: 'Refresh'
@ -1003,6 +1004,7 @@ profile:
pronouns: 'Include pronouns'
backup:
header: 'Cards backup'
headerShort: 'Backup'
export:
action: 'Generate backup'
success: 'A backup file is being generated, download should begin shortly'

View File

@ -466,6 +466,7 @@ user:
headerLong: 'Via konto'
tokenExpired: 'Token has expired. Please refresh the website and try again.'
login:
methods: 'Login methods'
help: 'To log in or create an account you can either use the social media buttons or enter your email in the field below and then confirm the code you will have received in your mailbox.'
placeholder: 'Email (or username, if you''re already registered)'
action: 'Ensaluti'

View File

@ -748,6 +748,7 @@ profile:
relationship: 'Relación (p. ej. "pareja", "mejor amigx")'
backup:
header: 'Copia de seguridad de tarjetas'
headerShort: 'Copia'
export:
action: 'Generar copia de seguridad'
success: 'Se está generando un archivo de copia de seguridad; la descarga debería comenzar en breve'

View File

@ -1457,6 +1457,7 @@ user:
deleteAccount: 'Usuń konto'
deleteAccountConfirm: 'Czy na pewno chcesz usunąć swoje konto? Ta operacja jest nieodwracalna!'
socialConnection:
header: 'Metody logowania'
list: 'Połączenia z social mediami'
connect: 'Połącz'
refresh: 'Odśwież'
@ -1665,6 +1666,7 @@ profile:
pronouns: 'Zamieść zaimki bezpośrednio w linku'
backup:
header: 'Kopia zapasowa wizytówek'
headerShort: 'Kopia zapasowa'
export:
action: 'Ściągnij kopię'
success: 'Plik kopii zapasowej jest generowany, zaraz zacznie się ściąganie pliku'

View File

@ -734,6 +734,7 @@ profile:
local: 'Linkar para esta versão de linguagem'
backup:
header: 'Backup de cartões'
headerShort: 'Backup'
import:
action: 'Recuperar backup'
success: 'Seu backup foi restaurado com sucesso! A página será atualizada.'

View File

@ -943,6 +943,7 @@ profile:
altExample: 'Un exemplu cum ar putea fi structurat un text alternativ'
backup:
header: 'Back-up'
headerShort: 'Back-up'
export:
action: 'Generează back-up'
success: 'Un fișier de back-up este în curs de generare, descărcarea ar trebui să înceapă în curând'

View File

@ -340,6 +340,7 @@ profile:
atAlternative: 'o kepeken nimi /u/'
backup:
header: 'awen lipu'
headerShort: 'awen lipu'
export:
action: 'o awen e lipu sina'
flagsCustomForm: