aboutsummaryrefslogtreecommitdiffstats
path: root/core/src/views/Profile.vue
diff options
context:
space:
mode:
Diffstat (limited to 'core/src/views/Profile.vue')
-rw-r--r--core/src/views/Profile.vue531
1 files changed, 531 insertions, 0 deletions
diff --git a/core/src/views/Profile.vue b/core/src/views/Profile.vue
new file mode 100644
index 00000000000..d16a3661b22
--- /dev/null
+++ b/core/src/views/Profile.vue
@@ -0,0 +1,531 @@
+<!--
+ - @copyright Copyright (c) 2021 Christopher Ng <chrng8@gmail.com>
+ -
+ - @author Christopher Ng <chrng8@gmail.com>
+ - @author Julius Härtl <jus@bitgrid.net>
+ -
+ - @license GNU AGPL version 3 or any later version
+ -
+ - This program is free software: you can redistribute it and/or modify
+ - it under the terms of the GNU Affero General Public License as
+ - published by the Free Software Foundation, either version 3 of the
+ - License, or (at your option) any later version.
+ -
+ - This program is distributed in the hope that it will be useful,
+ - but WITHOUT ANY WARRANTY; without even the implied warranty of
+ - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ - GNU Affero General Public License for more details.
+ -
+ - You should have received a copy of the GNU Affero General Public License
+ - along with this program. If not, see <http://www.gnu.org/licenses/>.
+ -
+ -->
+
+<template>
+ <div class="profile">
+ <div class="profile__header">
+ <div class="profile__header__container">
+ <div class="profile__header__container__placeholder" />
+ <h2 class="profile__header__container__displayname">
+ {{ displayname || userId }}
+ <a v-if="isCurrentUser"
+ class="primary profile__header__container__edit-button"
+ :href="settingsUrl">
+ <PencilIcon
+ class="pencil-icon"
+ decorative
+ title=""
+ :size="16" />
+ {{ t('core', 'Edit Profile') }}
+ </a>
+ </h2>
+ <div v-if="status.icon || status.message"
+ class="profile__header__container__status-text"
+ :class="{ interactive: isCurrentUser }"
+ @click.prevent.stop="openStatusModal">
+ {{ status.icon }} {{ status.message }}
+ </div>
+ </div>
+ </div>
+
+ <div class="profile__content">
+ <div class="profile__sidebar">
+ <Avatar
+ class="avatar"
+ :class="{ interactive: isCurrentUser }"
+ :user="userId"
+ :size="180"
+ :show-user-status="true"
+ :show-user-status-compact="false"
+ :disable-menu="true"
+ :disable-tooltip="true"
+ :is-no-user="!isUserAvatarVisible"
+ @click.native.prevent.stop="openStatusModal" />
+
+ <div class="user-actions">
+ <!-- When a tel: URL is opened with target="_blank", a blank new tab is opened which is inconsistent with the handling of other URLs so we set target="_self" for the phone action -->
+ <PrimaryActionButton v-if="primaryAction"
+ class="user-actions__primary"
+ :href="primaryAction.target"
+ :icon="primaryAction.icon"
+ :target="primaryAction.id === 'phone' ? '_self' :'_blank'">
+ {{ primaryAction.title }}
+ </PrimaryActionButton>
+ <div class="user-actions__other">
+ <!-- FIXME Remove inline styles after https://github.com/nextcloud/nextcloud-vue/issues/2315 is fixed -->
+ <Actions v-for="action in middleActions"
+ :key="action.id"
+ :default-icon="action.icon"
+ style="
+ background-position: 14px center;
+ background-size: 16px;
+ background-repeat: no-repeat;"
+ :style="{
+ backgroundImage: `url(${action.icon})`,
+ ...(colorMainBackground === '#181818' && { filter: 'invert(1)' })
+ }">
+ <ActionLink
+ :close-after-click="true"
+ :icon="action.icon"
+ :href="action.target"
+ :target="action.id === 'phone' ? '_self' :'_blank'">
+ {{ action.title }}
+ </ActionLink>
+ </Actions>
+ <template v-if="otherActions">
+ <Actions v-for="action in otherActions"
+ :key="action.id"
+ :force-menu="true">
+ <ActionLink
+ :class="{ 'icon-invert': colorMainBackground === '#181818' }"
+ :close-after-click="true"
+ :icon="action.icon"
+ :href="action.target"
+ :target="action.id === 'phone' ? '_self' :'_blank'">
+ {{ action.title }}
+ </ActionLink>
+ </Actions>
+ </template>
+ </div>
+ </div>
+ </div>
+
+ <div class="profile__blocks">
+ <div v-if="organisation || role || address" class="profile__blocks-details">
+ <div v-if="organisation || role" class="detail">
+ <p>{{ organisation }} <span v-if="organisation && role">•</span> {{ role }}</p>
+ </div>
+ <div v-if="address" class="detail">
+ <p>
+ <MapMarkerIcon
+ class="map-icon"
+ decorative
+ title=""
+ :size="16" />
+ {{ address }}
+ </p>
+ </div>
+ </div>
+ <template v-if="headline || biography">
+ <div v-if="headline" class="profile__blocks-headline">
+ <h3>{{ headline }}</h3>
+ </div>
+ <div v-if="biography" class="profile__blocks-biography">
+ <p>{{ biography }}</p>
+ </div>
+ </template>
+ <template v-else>
+ <div class="profile__blocks-empty-info">
+ <AccountIcon
+ decorative
+ title=""
+ fill-color="var(--color-text-maxcontrast)"
+ :size="60" />
+ <h3>{{ displayname || userId }} {{ t('core', 'hasn\'t added any info yet') }}</h3>
+ <p>{{ t('core', 'The headline and about section will show up here') }}</p>
+ </div>
+ </template>
+ </div>
+ </div>
+ </div>
+</template>
+
+<script>
+import { getCurrentUser } from '@nextcloud/auth'
+import { subscribe, unsubscribe } from '@nextcloud/event-bus'
+import { loadState } from '@nextcloud/initial-state'
+import { generateUrl } from '@nextcloud/router'
+import { showError } from '@nextcloud/dialogs'
+
+import Avatar from '@nextcloud/vue/dist/Components/Avatar'
+import Actions from '@nextcloud/vue/dist/Components/Actions'
+import ActionLink from '@nextcloud/vue/dist/Components/ActionLink'
+import MapMarkerIcon from 'vue-material-design-icons/MapMarker'
+import PencilIcon from 'vue-material-design-icons/Pencil'
+import AccountIcon from 'vue-material-design-icons/Account'
+
+import PrimaryActionButton from '../components/Profile/PrimaryActionButton'
+
+const status = loadState('core', 'status', {})
+const {
+ userId,
+ displayname,
+ address,
+ organisation,
+ role,
+ headline,
+ biography,
+ actions,
+ isUserAvatarVisible,
+} = loadState('core', 'profileParameters', {
+ userId: null,
+ displayname: null,
+ address: null,
+ organisation: null,
+ role: null,
+ headline: null,
+ biography: null,
+ actions: [],
+ isUserAvatarVisible: false,
+})
+
+export default {
+ name: 'Profile',
+
+ components: {
+ AccountIcon,
+ ActionLink,
+ Actions,
+ Avatar,
+ MapMarkerIcon,
+ PencilIcon,
+ PrimaryActionButton,
+ },
+
+ data() {
+ return {
+ status,
+ userId,
+ displayname,
+ address,
+ organisation,
+ role,
+ headline,
+ biography,
+ actions,
+ isUserAvatarVisible,
+ }
+ },
+
+ computed: {
+ isCurrentUser() {
+ return getCurrentUser()?.uid === this.userId
+ },
+
+ allActions() {
+ return this.actions
+ },
+
+ primaryAction() {
+ if (this.allActions.length) {
+ return this.allActions[0]
+ }
+ return null
+ },
+
+ middleActions() {
+ if (this.allActions.slice(1, 4).length) {
+ return this.allActions.slice(1, 4)
+ }
+ return null
+ },
+
+ otherActions() {
+ if (this.allActions.slice(4).length) {
+ return this.allActions.slice(4)
+ }
+ return null
+ },
+
+ settingsUrl() {
+ return generateUrl('/settings/user')
+ },
+
+ colorMainBackground() {
+ // For some reason the returned string has prepended whitespace
+ return getComputedStyle(document.body).getPropertyValue('--color-main-background').trim()
+ },
+ },
+
+ mounted() {
+ subscribe('user_status:status.updated', this.handleStatusUpdate)
+ },
+
+ beforeDestroy() {
+ unsubscribe('user_status:status.updated', this.handleStatusUpdate)
+ },
+
+ methods: {
+ handleStatusUpdate(status) {
+ this.status = status
+ },
+
+ openStatusModal() {
+ const statusMenuItem = document.querySelector('.user-status-menu-item__toggle')
+ // Changing the user status is only enabled if you are the current user
+ if (this.isCurrentUser) {
+ if (statusMenuItem) {
+ statusMenuItem.click()
+ } else {
+ showError(t('core', 'Error opening the user status modal, try hard refreshing the page'))
+ }
+ }
+ },
+ },
+}
+</script>
+
+<style lang="scss">
+// Override header styles
+#header {
+ background-color: transparent !important;
+ background-image: none !important;
+}
+
+#content {
+ padding-top: 0px;
+}
+</style>
+
+<style lang="scss" scoped>
+$profile-max-width: 1024px;
+$content-max-width: 640px;
+
+.profile {
+ width: 100%;
+
+ &__header {
+ position: sticky;
+ height: 190px;
+ top: -40px;
+
+ &__container {
+ align-self: flex-end;
+ width: 100%;
+ max-width: $profile-max-width;
+ margin: 0 auto;
+ display: grid;
+ grid-template-rows: max-content max-content;
+ grid-template-columns: 240px 1fr;
+ justify-content: center;
+
+ &__placeholder {
+ grid-row: 1 / 3;
+ }
+
+ &__displayname, &__status-text {
+ color: var(--color-primary-text);
+ }
+
+ &__displayname {
+ width: $content-max-width;
+ height: 45px;
+ margin-top: 128px;
+ // Override the global style declaration
+ margin-bottom: 0;
+ font-size: 30px;
+ display: flex;
+ align-items: center;
+ cursor: text;
+
+ &:not(:last-child) {
+ margin-top: 100px;
+ margin-bottom: 4px;
+ }
+ }
+
+ &__edit-button {
+ border: none;
+ margin-left: 18px;
+ margin-top: 2px;
+ color: var(--color-primary-element);
+ background-color: var(--color-primary-text);
+ box-shadow: 0 0 0 2px var(--color-primary-text);
+ border-radius: var(--border-radius-pill);
+ padding: 0 18px;
+ font-size: var(--default-font-size);
+ height: 44px;
+ line-height: 44px;
+ font-weight: bold;
+
+ &:hover {
+ color: var(--color-primary-text);
+ background-color: var(--color-primary-element-light);
+ }
+
+ .pencil-icon {
+ display: inline-block;
+ vertical-align: middle;
+ margin-top: 2px;
+ }
+ }
+
+ &__status-text {
+ width: max-content;
+ max-width: $content-max-width;
+ padding: 5px 10px;
+ margin-left: -14px;
+ margin-top: 2px;
+
+ &.interactive {
+ cursor: pointer;
+
+ &:hover {
+ background-color: var(--color-main-background);
+ color: var(--color-main-text);
+ border-radius: var(--border-radius-pill);
+ font-weight: bold;
+ box-shadow: 0 3px 6px var(--color-box-shadow);
+ }
+ }
+ }
+ }
+ }
+
+ &__sidebar {
+ position: sticky;
+ top: var(--header-height);
+ align-self: flex-start;
+ padding-top: 20px;
+ min-width: 220px;
+ margin: -150px 20px 0 0;
+
+ // Specificity hack is needed to override Avatar component styles
+ &::v-deep .avatar.avatardiv, h2 {
+ text-align: center;
+ margin: auto;
+ display: block;
+ padding: 8px;
+ }
+
+ &::v-deep .avatar.avatardiv:not(.avatardiv--unknown) {
+ background-color: var(--color-main-background) !important;
+ box-shadow: none;
+ }
+
+ &::v-deep .avatar.avatardiv {
+ .avatardiv__user-status {
+ right: 14px;
+ bottom: 14px;
+ width: 34px;
+ height: 34px;
+ background-size: 28px;
+ border: none;
+ // Styles when custom status icon and status text are set
+ background-color: var(--color-main-background);
+ line-height: 34px;
+ font-size: 20px;
+ }
+ }
+
+ &::v-deep .avatar.interactive.avatardiv {
+ .avatardiv__user-status {
+ cursor: pointer;
+
+ &:hover {
+ box-shadow: 0 3px 6px var(--color-box-shadow);
+ }
+ }
+ }
+ }
+
+ &__content {
+ max-width: $profile-max-width;
+ margin: 0 auto;
+ display: flex;
+ width: 100%;
+ }
+
+ &__blocks {
+ margin: 18px 0 80px 0;
+ display: grid;
+ gap: 16px 0;
+ width: $content-max-width;
+
+ p, h3 {
+ overflow-wrap: anywhere;
+ }
+
+ &-details {
+ display: flex;
+ flex-direction: column;
+ gap: 2px 0;
+
+ .detail {
+ display: inline-block;
+ color: var(--color-text-maxcontrast);
+
+ p .map-icon {
+ display: inline-block;
+ vertical-align: middle;
+ }
+ }
+ }
+
+ &-headline {
+ margin-top: 10px;
+
+ h3 {
+ font-weight: bold;
+ font-size: 20px;
+ margin: 0;
+ }
+ }
+
+ &-biography {
+ white-space: pre-line;
+ }
+
+ h3, p {
+ cursor: text;
+ }
+
+ &-empty-info {
+ margin-top: 80px;
+ margin-right: 100px;
+ display: flex;
+ flex-direction: column;
+ text-align: center;
+
+ h3 {
+ font-weight: bold;
+ font-size: 18px;
+ margin: 8px 0;
+ }
+ }
+ }
+}
+
+.user-actions {
+ display: flex;
+ flex-direction: column;
+ gap: 8px 0;
+ margin-top: 20px;
+
+ &__primary {
+ margin: 0 auto;
+ }
+
+ &__other {
+ display: flex;
+ justify-content: center;
+ gap: 0 4px;
+ }
+}
+
+.icon-invert {
+ &::v-deep .action-link__icon {
+ filter: invert(1);
+ }
+}
+</style>