(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}; 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> </button>
</p> </p>
</div> </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"> <TabsNav
<p class="mb-0 narrow-message"> :tabs="['general', 'cards', 'socials', 'circles', 'backup']"
<Icon :v="messageIcon" /> pills
<T :params="messageParams">{{ message }}</T> showheaders
</p> navclass="mb-3 border-bottom-0"
</div> >
<template #general-header>
<form :disabled="savingUsername" @submit.prevent="changeUsername"> <Icon v="user" />
<h3 class="h6"> <T>user.headerLong</T>
<T>user.account.changeUsername.header</T> </template>
</h3> <template #general>
<input <div class="card mb-3">
v-model="username" <div class="card-body d-flex flex-column flex-md-row">
type="text" <div class="mx-2 text-center">
class="form-control mb-3" <p v-if="$isGranted('panel') || $isGranted('users')">
required <nuxt-link to="/admin" class="badge bg-primary text-white">
minlength="4" <Icon v="collective-logo.svg" class="inverted" />
maxlength="16" <T>contact.team.member</T>
> </nuxt-link>
<p v-if="usernameError" class="small text-danger"> </p>
<Icon v="exclamation-triangle" /> <p class="mb-0">
<span class="ml-1">{{ usernameError }}</span> <Avatar :user="$user()" validate />
</p> </p>
<div class="d-none d-md-block mt-3"> <div>
<button class="btn btn-outline-primary" :disabled="username === user.username"> <p class="mt-3 mb-1">
<T>user.account.changeUsername.action</T> <strong><T>user.avatar.change</T><T>quotation.colon</T></strong>
</button> </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>
<div class="d-block-force d-md-none mt-3"> <div class="mx-2 flex-grow-1">
<button class="btn btn-outline-primary w-100" :disabled="username === user.username"> <Alert type="danger" :message="error" />
<T>user.account.changeUsername.action</T>
</button>
</div>
</form>
<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"> <form :disabled="savingUsername" @submit.prevent="changeUsername">
<h3 class="h6"> <h3 class="h6">
<T>user.account.changeEmail.header</T> <T>user.account.changeUsername.header</T>
</h3> </h3>
<div v-if="!changeEmailAuthId" class=""> <input
<input v-model="email" type="email" class="form-control mb-3" required> v-model="username"
<div class="d-flex flex-column flex-md-row"> type="text"
<Captcha v-if="showCaptcha" v-model="captchaToken" /> class="form-control mb-3"
<div :class="['d-none', 'd-md-block', showCaptcha ? 'ms-3' : '']"> required
<button class="btn btn-outline-primary" :disabled="!canChangeEmail"> minlength="4"
<T>user.account.changeEmail.action</T> 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> </button>
</div> </div>
<div class="d-block-force d-md-none mt-3"> <div class="d-block-force d-md-none mt-3">
<button class="btn btn-outline-primary w-100" :disabled="!canChangeEmail"> <button class="btn btn-outline-primary w-100" :disabled="username === user.username">
<T>user.account.changeEmail.action</T> <T>user.account.changeUsername.action</T>
</button> </button>
</div> </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>
<div v-else class="input-group mb-3"> </div>
<input </div>
ref="code"
v-model="code" <AdPlaceholder :phkey="['content-0', 'content-mobile-0']" />
type="text"
class="form-control text-center" <section class="mt-5">
placeholder="000000" <a href="#" class="btn btn-outline-danger" @click.prevent="logout">
autofocus <Icon v="sign-out" />
required <T>user.logout</T>
minlength="0" @{{ $user().username }}
maxlength="6" </a>
inputmode="numeric"
pattern="[0-9]{6}" <a href="#" class="btn btn-outline-danger" @click.prevent="deleteAccount">
autocomplete="one-time-code" <Icon v="trash-alt" />
> <T>user.deleteAccount</T>
<button class="btn btn-outline-primary"> @{{ $user().username }}
<Icon v="key" /> </a>
<T>user.code.action</T>
</button> <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> </div>
</form> </template>
</div> <ul v-if="socialConnections !== undefined" class="list-group">
</div> <li v-for="(providerOptions, provider) in socialProviders" :key="provider" :class="['list-group-item', socialConnections[provider] !== undefined ? 'profile-current' : '']">
</div> <SocialConnection
:provider="provider"
<AdPlaceholder :phkey="['content-0', 'content-mobile-0']" /> :provider-options="providerOptions"
:connection="socialConnections[provider]"
<Loading :value="profiles"> @disconnected="socialConnections[provider] = undefined"
<template #header> @setAvatar="setAvatar"
<h3 class="h4"> />
<T>profile.list</T><T>quotation.colon</T> </li>
</h3> <li :class="['list-group-item', $user().mfa ? 'profile-current' : '']">
<MfaConnection />
</li>
</ul>
</Loading>
</template> </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']" /> <template #circles-header>
<Icon v="heart-circle" />
<Loading :value="socialConnections"> <T>profile.circles.header</T>
<template #header> </template>
<h3 class="h4"> <template #circles>
<T>user.socialConnection.list</T><T>quotation.colon</T> <h5><T>profile.circles.yourMentions.header</T><T>quotation.colon</T></h5>
</h3> <CircleMentions />
<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> </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 /> <template #backup-header>
<Icon v="copy"/>
<CardsBackup v-if="!$user().bannedReason" /> <T>profile.backup.headerShort</T>
</template>
<section class="mt-5"> <template #backup>
<a href="#" class="badge bg-light text-dark border" @click.prevent="logout"> <CardsBackup v-if="!$user().bannedReason" />
<Icon v="sign-out" /> </template>
<T>user.logout</T> </TabsNav>
@{{ $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>
<AdPlaceholder :phkey="['content-2', 'content-mobile-2']" /> <AdPlaceholder :phkey="['content-2', 'content-mobile-2']" />

View File

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

View File

@ -1,10 +1,5 @@
<template> <template>
<Loading :value="circleMentions"> <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"> <p class="small text-muted">
<T>profile.circles.yourMentions.description</T> <T>profile.circles.yourMentions.description</T>
</p> </p>

View File

@ -6,17 +6,17 @@
<Icon v="id-card" /> <Icon v="id-card" />
<T>profile.show</T> <T>profile.show</T>
</LocaleLink> </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" /> <Icon v="edit" />
<T>profile.edit</T> <T>profile.edit</T>
</LocaleLink> </LocaleLink>
<Spinner v-if="deleting" /> <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" /> <Icon v="trash-alt" />
</a> </a>
</span> </span>
<span v-else> <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" /> <Icon v="plus-circle" />
<T>profile.init</T> <T>profile.init</T>
</LocaleLink> </LocaleLink>

View File

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

View File

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

View File

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

View File

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

View File

@ -466,6 +466,7 @@ user:
headerLong: 'Via konto' headerLong: 'Via konto'
tokenExpired: 'Token has expired. Please refresh the website and try again.' tokenExpired: 'Token has expired. Please refresh the website and try again.'
login: 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.' 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)' placeholder: 'Email (or username, if you''re already registered)'
action: 'Ensaluti' action: 'Ensaluti'

View File

@ -748,6 +748,7 @@ profile:
relationship: 'Relación (p. ej. "pareja", "mejor amigx")' relationship: 'Relación (p. ej. "pareja", "mejor amigx")'
backup: backup:
header: 'Copia de seguridad de tarjetas' header: 'Copia de seguridad de tarjetas'
headerShort: 'Copia'
export: export:
action: 'Generar copia de seguridad' action: 'Generar copia de seguridad'
success: 'Se está generando un archivo de copia de seguridad; la descarga debería comenzar en breve' 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' deleteAccount: 'Usuń konto'
deleteAccountConfirm: 'Czy na pewno chcesz usunąć swoje konto? Ta operacja jest nieodwracalna!' deleteAccountConfirm: 'Czy na pewno chcesz usunąć swoje konto? Ta operacja jest nieodwracalna!'
socialConnection: socialConnection:
header: 'Metody logowania'
list: 'Połączenia z social mediami' list: 'Połączenia z social mediami'
connect: 'Połącz' connect: 'Połącz'
refresh: 'Odśwież' refresh: 'Odśwież'
@ -1665,6 +1666,7 @@ profile:
pronouns: 'Zamieść zaimki bezpośrednio w linku' pronouns: 'Zamieść zaimki bezpośrednio w linku'
backup: backup:
header: 'Kopia zapasowa wizytówek' header: 'Kopia zapasowa wizytówek'
headerShort: 'Kopia zapasowa'
export: export:
action: 'Ściągnij kopię' action: 'Ściągnij kopię'
success: 'Plik kopii zapasowej jest generowany, zaraz zacznie się ściąganie pliku' 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' local: 'Linkar para esta versão de linguagem'
backup: backup:
header: 'Backup de cartões' header: 'Backup de cartões'
headerShort: 'Backup'
import: import:
action: 'Recuperar backup' action: 'Recuperar backup'
success: 'Seu backup foi restaurado com sucesso! A página será atualizada.' 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' altExample: 'Un exemplu cum ar putea fi structurat un text alternativ'
backup: backup:
header: 'Back-up' header: 'Back-up'
headerShort: 'Back-up'
export: export:
action: 'Generează back-up' 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' 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/' atAlternative: 'o kepeken nimi /u/'
backup: backup:
header: 'awen lipu' header: 'awen lipu'
headerShort: 'awen lipu'
export: export:
action: 'o awen e lipu sina' action: 'o awen e lipu sina'
flagsCustomForm: flagsCustomForm: