Files
habitica/website/client/src/components/userMenu/profile.vue
SabreCat a9757b2d74 Squashed commit of the following:
commit 3aba0abedd
Author: SabreCat <sabe@habitica.com>
Date:   Mon Oct 2 20:51:20 2023 -0500

    fix(router): use state to pass modal launch info

commit 541eadd319
Merge: c0bb56c8c2 89fff49d02
Author: SabreCat <sabe@habitica.com>
Date:   Mon Oct 2 20:12:40 2023 -0500

    Merge branch 'release' into report-profile-modal

commit c0bb56c8c2
Author: SabreCat <sabe@habitica.com>
Date:   Wed Sep 27 16:15:28 2023 -0500

    test(profiles): add integrations

commit 9b644e9ad8
Author: SabreCat <sabe@habitica.com>
Date:   Tue Sep 26 17:17:22 2023 -0500

    fix(profile): adjust margin

commit bfefe5dfa9
Author: SabreCat <sabe@habitica.com>
Date:   Tue Sep 26 17:12:24 2023 -0500

    fix(profiles): moar layout fixes

commit 8f211ee3e2
Author: SabreCat <sabe@habitica.com>
Date:   Mon Sep 25 17:32:04 2023 -0500

    fix(profile): fix admin actions
    Correct "user is banned" banner
    Fix bouncing modal
    Add "Days" smart plural
    Fix leaky CSS on Market page
    Refactor some redundant functions

commit b1d23ec88b
Merge: ee9709a9e1 a63cc84779
Author: SabreCat <sabe@habitica.com>
Date:   Mon Sep 25 15:37:54 2023 -0500

    Merge branch 'release' into report-profile-modal

commit ee9709a9e1
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Mon Sep 18 16:30:30 2023 -0400

    WIP(profile): add banned banner, toggle switches now toggle, add "days" to Next Login Reward

commit f80928a895
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Mon Sep 18 13:43:34 2023 -0400

    update(node): update node modules

commit 1d552f7e80
Author: SabreCat <sabe@habitica.com>
Date:   Fri Sep 15 16:52:22 2023 -0500

    fix(import): remove empty import

commit f55d74a95d
Author: SabreCat <sabe@habitica.com>
Date:   Fri Sep 15 16:39:50 2023 -0500

    refactor(profiles): remove email feature
    also still more visual cleanup of profile modal

commit 311c743284
Author: SabreCat <sabe@habitica.com>
Date:   Fri Sep 15 15:44:56 2023 -0500

    refactor(profile): remove page view

commit f8632bf50d
Merge: ec85159c65 9e25360102
Author: SabreCat <sabe@habitica.com>
Date:   Fri Sep 15 15:23:21 2023 -0500

    Merge branch 'release' into report-profile-modal

commit ec85159c65
Author: SabreCat <sabe@habitica.com>
Date:   Mon Sep 11 22:53:14 2023 -0500

    feat(profiles): load modal instead of page?

commit 9986082914
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Fri Sep 8 14:49:57 2023 -0400

    WIP(profile): fixed a comment, woohoo

commit 6262a9ba0c
Merge: ae2b614df2 ea2b007b1a
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Fri Sep 8 13:40:23 2023 -0400

    Merge remote-tracking branch 'origin/report-profile-modal' into report-profile-modal

commit ea2b007b1a
Author: SabreCat <sabe@habitica.com>
Date:   Thu Sep 7 16:54:19 2023 -0500

    fix(profile): focus behavior

commit ae2b614df2
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Thu Sep 7 17:47:08 2023 -0400

    WIP(profile): styling updates

commit 2e0723f1b9
Author: SabreCat <sabe@habitica.com>
Date:   Thu Sep 7 15:37:59 2023 -0500

    feat(moderation): unflag profile
    Also a few stylistic tweaks

commit edcf8113de
Author: SabreCat <sabe@habitica.com>
Date:   Wed Sep 6 16:39:02 2023 -0500

    WIP(profile): dropdown draft

commit 0691483d63
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Wed Sep 6 16:33:30 2023 -0400

    WIP(profile): Styling and string updates

commit 7e9d57d10a
Author: SabreCat <sabe@habitica.com>
Date:   Wed Sep 6 11:40:31 2023 -0500

    feat(profile): functional dropdown buttons

commit a2989b2833
Merge: af6575e40c e072d7c09c
Author: SabreCat <sabe@habitica.com>
Date:   Wed Sep 6 10:04:57 2023 -0500

    Merge branch 'release' into report-profile-modal

commit af6575e40c
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Wed Sep 6 11:01:05 2023 -0400

    WIP(profile): comment cleanup

commit 7b1de37202
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Tue Sep 5 17:22:14 2023 -0400

    WIP(profile): remove shadowban tooltip

commit d1177c32b9
Merge: 321a01b081 31f821021b
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Tue Sep 5 17:02:40 2023 -0400

    Merge branch 'sabrecat/report-profile' into report-profile-modal

commit 321a01b081
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Fri Sep 1 16:14:36 2023 -0400

    WIP(profile): close button finally workinating

commit e143d36d28
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Fri Sep 1 15:52:38 2023 -0400

    WIP(profile): close icon moved to profile.vue

commit 31f821021b
Merge: a8f5e25d38 8957c5c009
Author: SabreCat <sabe@habitica.com>
Date:   Fri Sep 1 14:52:31 2023 -0500

    Merge branch 'report-profile-modal' into sabrecat/report-profile

commit 8957c5c009
Merge: d340f06a22 0aec3866a4
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Fri Sep 1 15:38:12 2023 -0400

    Merge remote-tracking branch 'origin/report-profile-modal' into report-profile-modal

commit d340f06a22
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Fri Sep 1 15:37:57 2023 -0400

    WIP(profile): fixed user not found error

commit 0aec3866a4
Merge: b01f323b14 ac7c8e0eb6
Author: Natalie <78037386+CuriousMagpie@users.noreply.github.com>
Date:   Fri Sep 1 15:28:58 2023 -0400

    Merge branch 'HabitRPG:develop' into report-profile-modal

commit a8f5e25d38
Author: SabreCat <sabe@habitica.com>
Date:   Thu Aug 31 17:02:07 2023 -0500

    feat(community): basic "report profile"

commit b01f323b14
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Thu Aug 31 17:42:12 2023 -0400

    WIP(profile): removed refactoring crud, located where close icon should be (profileModal.vue)

commit ce7d51a20c
Merge: 010f2299f0 ac7c8e0eb6
Author: SabreCat <sabe@habitica.com>
Date:   Thu Aug 31 14:20:37 2023 -0500

    Merge branch 'release' into sabrecat/report-profile

commit 18b41acd94
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Thu Aug 31 12:23:41 2023 -0400

    WIP(profile): moar buttonz

commit 9387b3a6bc
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Wed Aug 30 17:21:36 2023 -0400

    WIP(profile): buttons

commit b3ea48c4f5
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Fri Aug 25 15:52:41 2023 -0400

    WIP(profile): work on achievement component

commit a1ceb2ea75
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Fri Aug 25 14:39:12 2023 -0400

    WIP(profile): create achievements component

commit 4a24d9b80b
Merge: 8fe263a377 1e05297e96
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Wed Aug 23 13:14:39 2023 -0400

    Merge branch 'develop' into report-profile-modal

commit 1e05297e96
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Wed Aug 23 13:12:52 2023 -0400

    package updates

commit 8fe263a377
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Wed Aug 23 12:12:36 2023 -0400

    update(dependencies): ran npm install to update dependencies

commit 190fe048a1
Merge: 3ea48ab5cb fa83d1a9cf
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Wed Aug 23 11:52:08 2023 -0400

    Merge branch 'develop' into report-profile-modal

commit 3ea48ab5cb
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Fri Aug 11 17:12:31 2023 -0400

    WIP(user profile): dropdown menu and toggles and colors oh my

commit c301a2b460
Merge: 1da6af11b5 647b27c55f
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Fri Aug 11 12:40:07 2023 -0400

    Merge branch 'develop' into report-profile-modal

commit 1da6af11b5
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Thu Aug 10 16:50:07 2023 -0400

    WIP(user profile): moved some CSS classes out of unscoped and into the scoped section, started on toggle buttons

commit dd55cbc928
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Wed Aug 9 15:38:46 2023 -0400

    WIP(user profile): workin on the hamburger (kebab?) menu

commit 3834093207
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Tue Aug 8 14:14:40 2023 -0400

    WIP(user profiles): working on the drop down menu

commit f2be588195
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Mon Aug 7 16:10:30 2023 -0400

    WIP(user profile): options menu

commit 010f2299f0
Author: SabreCat <sabe@habitica.com>
Date:   Mon Aug 7 11:49:04 2023 -0500

    fix(lint): eof and const

commit 4551dbf4b3
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Fri Aug 4 15:34:05 2023 -0400

    WIP(user profile): styling the top portion of the modal

commit 19a9fe3644
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Thu Aug 3 15:06:51 2023 -0400

    WIP(user profile): adding buttons

commit dfdb305b1c
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Wed Aug 2 14:41:20 2023 -0400

    WIP(user profile): layout

commit ded4eee693
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Wed Aug 2 12:04:02 2023 -0400

    WIP(user profile): start flex grid & tidy up CSS

commit aaca48be32
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Fri Jul 28 16:44:06 2023 -0400

    WIP(user profile): mostly css updates

commit e531985b87
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Thu Jul 27 16:49:44 2023 -0400

    WIP(user profile): one infinitesimal change that's hardly worth the electricity it's made from

commit eb4021fcc7
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Wed Jul 26 16:33:05 2023 -0400

    feat(content): upgrade profile page

commit 1b25394f3e
Merge: c50cee0d88 8558dcc3a8
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Wed Jul 26 11:50:12 2023 -0400

    Merge branch 'develop' into report-profile-modal

commit c50cee0d88
Author: SabreCat <sabe@habitica.com>
Date:   Wed Jul 12 16:32:25 2023 -0500

    fix(flagging): debug params issue
    Also add and document the "source" body param

commit 55848c58be
Author: SabreCat <sabe@habitica.com>
Date:   Mon Jul 10 16:24:20 2023 -0500

    WIP(members): basic report a user API

commit dda6180792
Author: SabreCat <sabe@habitica.com>
Date:   Thu Jul 6 10:05:07 2023 -0500

    fix(lint): remove console.info
2023-10-03 13:29:26 -05:00

1330 lines
35 KiB
Vue

<template>
<div>
<div
v-if="!user && userLoaded"
>
<error404 />
</div>
<div
v-else-if="userLoaded"
class="profile mt-n3"
>
<!-- HEADER -->
<div class="header">
<div
class="avatar mr-auto"
>
<member-details
:member="user"
:class-badge-position="'hidden'"
class="mx-4"
/>
</div>
</div>
<!-- PAGE STATE CHANGES -->
<div class="state-pages pt-3">
<div
v-if="userBlocked"
class="blocked-banned text-center mb-3 mx-4 py-2 px-3"
v-html="$t('blockedUser')"
>
</div>
<div
v-if="hasPermission(userLoggedIn, 'moderator') && hero.auth.blocked"
class="blocked-banned mb-3 mx-4 py-2 px-3
d-flex align-items-middle justify-content-center"
>
<div
v-once
class="svg-icon icon-16 color my-auto ml-auto mr-2"
v-html="icons.block"
></div>
<div
class="my-auto mr-auto"
v-html="$t('bannedUser')"
>
</div>
</div>
<div class="text-center nav">
<div
class="nav-item"
:class="{active: selectedPage === 'profile'}"
@click="selectPage('profile')"
>
{{ $t('profile') }}
</div>
<div
class="nav-item"
:class="{active: selectedPage === 'stats'}"
@click="selectPage('stats')"
>
{{ $t('stats') }}
</div>
<div
class="nav-item"
:class="{active: selectedPage === 'achievements'}"
@click="selectPage('achievements')"
>
{{ $t('achievements') }}
</div>
</div>
</div>
<!-- SHOW PROFILE -->
<div
v-show="selectedPage === 'profile'"
v-if="user.profile"
id="userProfile"
class="standard-page"
>
<!-- PROFILE STUFF -->
<div
v-if="!editing"
class="flex-container"
>
<div class="flex-left">
<div class="about mb-0">
<h2>{{ $t('about') }}</h2>
</div>
<div class="flex-left">
<div class="about profile-section">
<p
v-if="user.profile.blurb"
v-markdown="user.profile.blurb"
class="markdown"
></p>
<p v-else>
{{ $t('noDescription') }}
</p>
</div>
</div>
<div class="photo profile-section">
<h2>{{ $t('photo') }}</h2>
<img
v-if="user.profile.imageUrl"
class="img-rendering-auto"
:src="user.profile.imageUrl"
>
<p v-else>
{{ $t('noPhoto') }}
</p>
</div>
</div>
<div class="ml-auto">
<button
v-if="user._id === userLoggedIn._id"
class="btn btn-primary flex-right edit-profile"
@click="editing = !editing"
>
{{ $t('editProfile') }}
</button>
<span
v-else-if="user._id !== userLoggedIn._id"
class="flex-right d-flex justify-content-between"
>
<router-link
:to="{ path: '/private-messages', query: { uuid: user._id } }"
replace
>
<button
class="btn btn-primary send-message"
>
{{ $t('sendMessage') }}
</button>
</router-link>
<!-- KEBAB MENU DROPDOWN -->
<b-dropdown
right="right"
toggle-class="with-icon"
class="mx-auto"
:no-caret="true"
>
<template v-slot:button-content>
<span
v-once
class="svg-icon dots-icon with-icon"
v-html="icons.dots"
>
</span>
</template>
<!-- SEND GIFT -->
<b-dropdown-item
class="selectListItem"
@click="openSendGemsModal()"
>
<span class="with-icon">
<span
v-once
class="svg-icon icon-16 color"
v-html="icons.gift"
></span>
<span
v-once
class="send-gift"
>
{{ $t('sendGift') }}
</span>
</span>
</b-dropdown-item>
<!-- REPORT PLAYER -->
<b-dropdown-item
class="selectListItem"
:class="{ disabled: !canReport }"
:disabled="!canReport"
@click="reportPlayer()"
>
<span class="with-icon">
<span
v-once
class="svg-icon icon-16 color"
v-html="icons.report"
></span>
<span v-once>
{{ $t('reportPlayer') }}
</span>
</span>
</b-dropdown-item>
<!-- BLOCK PLAYER -->
<b-dropdown-item
v-if="!userBlocked"
class="selectListItem block-ban"
@click.native.capture.stop="blockUser()"
>
<span class="with-icon">
<span
v-once
class="svg-icon icon-16 color"
v-html="icons.block"
></span>
<span v-once>
{{ $t('blockPlayer') }}
</span>
</span>
</b-dropdown-item>
<b-dropdown-item
v-else
class="selectListItem block-ban"
@click.native.capture.stop="unblockUser()"
>
<span class="with-icon">
<span
v-once
class="svg-icon icon-16 color"
v-html="icons.block"
></span>
<span v-once>
{{ $t('unblock') }}
</span>
</span>
</b-dropdown-item>
<!-- REST OF DROPDOWN ONLY VISIBLE IF ADMIN -->
<div
v-if="hasPermission(userLoggedIn, 'moderator')"
>
<!-- ADMIN TOOLS HEADER -->
<div
class="admin-tools-divider"
>
<span v-once>
<strong>{{ $t('adminTools') }}</strong>
</span>
</div>
<!-- ADMIN PANEL -->
<b-dropdown-item
v-if="hasPermission(userLoggedIn, 'userSupport')"
class="selectListItem"
@click="openAdminPanel()"
>
<span class="with-icon">
<span
v-once
class="svg-icon icon-16 color"
v-html="icons.crown"
></span>
<span v-once>
{{ $t('viewAdminPanel') }}
</span>
</span>
</b-dropdown-item>
<!-- BAN USER -->
<b-dropdown-item
v-if="!hero.auth.blocked"
class="selectListItem block-ban"
@click.native.capture.stop="adminToggleBan()"
>
<span class="with-icon">
<span
v-once
class="svg-icon icon-16 color"
v-html="icons.block"
></span>
<span v-once>
{{ $t('banPlayer') }}
</span>
</span>
</b-dropdown-item>
<b-dropdown-item
v-else
class="selectListItem block-ban"
@click.native.capture.stop="adminToggleBan()"
>
<span class="with-icon">
<span
v-once
class="svg-icon icon-16 color"
v-html="icons.block"
></span>
<span v-once>
{{ $t('unbanPlayer') }}
</span>
</span>
</b-dropdown-item>
<!-- SHADOW MUTE PLAYER WITH TOGGLE -->
<b-dropdown-item
class="selectListItem"
@click.native.capture.stop="adminToggleShadowMute()"
>
<span class="with-icon">
<span
v-once
class="svg-icon icon-16 color"
v-html="icons.shadowMute"
></span>
<span
v-once
class="admin-action"
>
{{ $t('shadowMute') }}
</span>
<toggle-switch
v-model="hero.flags.chatShadowMuted"
class="toggle-switch-outer ml-auto"
@change.native.capture.stop="adminToggleShadowMute()"
/>
</span>
</b-dropdown-item>
<!-- MUTE PLAYER WITH TOGGLE -->
<b-dropdown-item
class="selectListItem"
@click.native.capture.stop="adminToggleChatRevoke()"
>
<span class="with-icon">
<span
v-once
class="svg-icon icon-16 color"
v-html="icons.mute"
></span>
<span v-once>
{{ $t('mutePlayer') }}
</span>
<toggle-switch
v-model="hero.flags.chatRevoked"
class="toggle-switch-outer ml-auto"
@change.native.capture.stop="adminToggleChatRevoke()"
/>
</span>
</b-dropdown-item>
</div>
</b-dropdown>
</span>
<!-- ACCOUNT DATES, LOG IN COUNTER -->
<div class="info profile-section">
<div class="info-item">
<div class="info-item-label">
{{ $t('joined') }}:
</div>
<div class="info-item-value">
{{ userJoinedDate }}
</div>
</div>
<div class="info-item">
<div class="info-item-label">
{{ $t('totalLogins') }}:
</div>
<div class="info-item-value">
{{ user.loginIncentives }}
</div>
</div>
<div class="info-item">
<div class="info-item-label">
{{ $t('latestCheckin') }}:
</div>
<div class="info-item-value">
{{ userLastLoggedIn }}
</div>
</div>
<div class="info-item">
<div class="info-item-label">
{{ $t('nextReward') }}:
</div>
<div class="info-item-value">
{{ getNextIncentive() }} {{ getNextIncentive() === 1 ? $t('day') : $t('days') }}
</div>
</div>
</div>
</div>
</div>
<!-- EDITING PROFILE -->
<div
v-if="editing"
class="row"
>
<h1>{{ $t('editProfile') }}</h1>
<div class="">
<div
class="alert alert-info alert-sm"
v-html="$t('communityGuidelinesWarning', managerEmail)"
></div>
<!-- TODO use photo-upload instead: https://groups.google.com/forum/?fromgroups=#!topic/derbyjs/xMmADvxBOak-->
<div class="form-group">
<label>{{ $t('displayName') }}</label>
<input
v-model="editingProfile.name"
class="form-control"
type="text"
:placeholder="$t('fullName')"
>
</div>
<div class="form-group">
<label>{{ $t('photoUrl') }}</label>
<input
v-model="editingProfile.imageUrl"
class="form-control"
type="url"
:placeholder="$t('imageUrl')"
>
</div>
<div class="form-group">
<label>{{ $t('about') }}</label>
<textarea
v-model="editingProfile.blurb"
class="form-control"
rows="5"
:placeholder="$t('displayBlurbPlaceholder')"
></textarea>
<!-- include ../../shared/formatting-help-->
</div>
</div>
<div class=" text-center">
<button
class="btn btn-primary"
@click="save()"
>
{{ $t("save") }}
</button>
<button
class="btn btn-secondary"
@click="editing = false"
>
{{ $t("cancel") }}
</button>
</div>
</div>
</div>
<!-- ACHIEVEMENTS -->
<div
v-show="selectedPage === 'achievements'"
v-if="user.achievements"
id="achievements"
class="standard-page container "
>
<div
v-for="(category, key) in achievements"
:key="key"
class="row category-row d-flex flex-column"
>
<h3 class="text-center">
{{ $t(`${key}Achievs`) }}
</h3>
<div class="">
<div class="row achievements-row justify-content-center">
<div
v-for="(achievement, achievKey) in achievementsCategory(key, category)"
:key="achievKey"
class="achievement-wrapper col text-center"
>
<div
:id="achievKey + '-achievement'"
class="box achievement-container"
:class="{'achievement-unearned': !achievement.earned}"
>
<b-popover
:target="'#' + achievKey + '-achievement'"
triggers="hover"
placement="top"
>
<h4 class="popover-content-title">
{{ achievement.title }}
</h4>
<div
class="popover-content-text"
v-html="achievement.text"
></div>
</b-popover>
<div
v-if="achievement.earned"
class="achievement"
:class="achievement.icon + '2x'"
>
<div
v-if="achievement.optionalCount"
class="counter badge badge-pill stack-count"
>
{{ achievement.optionalCount }}
</div>
</div>
<div
v-if="!achievement.earned"
class="achievement achievement-unearned achievement-unearned2x"
></div>
</div>
</div>
</div>
<div
v-if="achievementsCategories[key].number > 5"
class="btn btn-flat btn-show-more"
@click="toggleAchievementsCategory(key)"
>
{{ achievementsCategories[key].open ?
$t('hideAchievements', {category: $t(`${key}Achievs`)}) :
$t('showAllAchievements', {category: $t(`${key}Achievs`)})
}}
</div>
</div>
</div>
<hr class="">
<div class="row">
<div
v-if="user.achievements.challenges"
class="col-12 col-md-6"
>
<div class="achievement-icon achievement-karaoke-2x"></div>
<h3 class="text-center">
{{ $t('challengesWon') }}
</h3>
<div
v-for="chal in user.achievements.challenges"
:key="chal"
class="achievement-list-item"
>
<span v-markdown="chal"></span>
</div>
</div>
<div
v-if="user.achievements.quests"
class="col-12 col-md-6"
>
<div class="achievement-icon achievement-alien2x"></div>
<h3 class="text-center">
{{ $t('questsCompleted') }}
</h3>
<div
v-for="(value, key) in user.achievements.quests"
:key="key"
class="achievement-list-item d-flex justify-content-between"
>
<span>{{ content.quests[key].text() }}</span>
<span
v-if="value > 1"
class="badge badge-pill stack-count"
>
{{ value }}
</span>
</div>
</div>
</div>
</div>
<!-- STATS -->
<div>
<profileStats
v-show="selectedPage === 'stats'"
v-if="user.preferences"
:user="user"
:show-allocation="showAllocation()"
/>
</div>
</div>
</div>
</template>
<style lang="scss" >
@import '~@/assets/scss/colors.scss';
#userProfile {
.dropdown-menu {
margin-left: -48px;
width: 210px;
}
.dropdown-item {
svg {
color: $gray-50;
}
&.disabled {
color: $gray-50 !important;
opacity: 0.75;
svg {
color: $gray-50;
opacity: 0.75;
}
}
&:not(.disabled):hover, &:not(.disabled):focus {
a, svg {
color: $purple-300;
}
}
}
.drawer-toggle-icon {
position: absolute;
right: 16px;
bottom: 0;
&.closed {
top: 10px;
}
}
.toggle-switch-outer {
margin-bottom: 2px;
}
.selectListItem {
&:not(.disabled):hover svg {
color: $purple-300;
}
&.block-ban {
&:hover, .dropdown-item:hover {
color: $maroon-50 !important;
background-color: rgba($red-500, 0.25) !important;
svg {
color: $maroon-50;
}
}
&:focus, .dropdown-item:focus {
color: $maroon-50 !important;
svg {
color: $maroon-50;
}
&:active, .dropdown-item:active {
color: $gray-50 !important;
}
}
}
}
}
.profile {
.member-details {
background-color: $white;
}
.avatar-container {
padding-top: 16px;
}
.progress-container > .progress {
background-color: $gray-500 !important;
border-radius: 2px;
height: 16px !important;
min-width: 375px !important;
vertical-align: middle !important;
.progress-bar {
height: 16px !important;
}
}
.progress-container > .svg-icon {
width: 28px;
margin-top: -2px !important;
height: 28px;
margin-right: 8px;
}
.profile-first-row,
.progress-container {
margin-left: 4px;
margin-top: 4px;
.small-text {
color: $gray-50;
}
}
.character-name {
color: $gray-50;
font-weight: bold;
height: 24px;
line-height: 1.71;
margin-bottom: 0px;
}
small {
color: $gray-50;
font-size: 0.75em;
line-height: 1.33;
}
}
</style>
<style lang="scss" scoped>
@import '~@/assets/scss/colors.scss';
.avatar {
width: fit-content;
}
.header {
width: 100%;
}
.markdown p {
padding-bottom: 24px;
}
.standard-page {
background-color: $gray-700;
border-bottom-left-radius: 10px;
border-bottom-right-radius: 10px;
padding-top: 0px;
}
.flex-container {
display: flex;
flex-direction: row;
padding-top: 18px;
}
.flex-left {
width: 424px;
}
.flex-right {
width: 188px;
}
.admin-profile-actions {
margin-bottom: 48px;
.admin-action {
color: $red-500;
cursor: pointer;
}
}
.message-icon svg {
height: 11px;
margin-top: 1px;
}
.dots-icon {
height: 16px;
width: 4px;
}
.toggle-switch-outer {
margin-bottom: 2px;
}
.photo {
img {
max-width: 100%;
}
}
.header {
h1 {
color: $gray-50;
margin-bottom: 0.2rem;
}
h4 {
color: $gray-100;
}
}
.blocked-banned {
background-color: $maroon-100;
border-radius: 4px;
color: $white;
line-height: 1.71;
svg {
color: $white;
}
}
.state-pages {
background-color: $gray-700;
margin-left: 0px;
margin-right: 0px;
width: 100%;
}
.nav {
font-size: 0.75rem;
font-weight: bold;
justify-content: center;
min-height: 40px;
padding-top: 16px;
width: 100%;
}
.nav-item {
color: $gray-50;
display: inline-block;
margin: 0 8px 8px 6px;
}
.nav-item:hover, .nav-item.active {
border-bottom: 2px solid $purple-300;
color: $purple-300;
cursor: pointer;
}
.name {
color: $gray-200;
font-size: 16px;
}
.white {
background: $white;
border: 1px solid transparent;
border-radius: 2px;
box-shadow: 0 2px 2px 0 rgba(26, 24, 29, 0.15), 0 1px 4px 0 rgba(26, 24, 29, 0.1);
}
.item-wrapper {
h3 {
text-align: center;
}
}
.profile-section {
h2 {
color: $gray-50;
margin-bottom: 20px !important;
overflow: hidden;
size: 1.125em;
}
}
.about {
line-height: 1.71;
}
.edit-profile {
font-size: 1em;
margin-left: 24px;
padding: 4px 16px;
width: 188px;
}
.send-message {
font-size: 1em;
line-height: 1.71;
margin-left: 24px;
margin-right: 8px;
margin-top: 0px;
width: 148px;
}
.dot-menu {
height: 32px;
margin-right: -24px;
width: 32px;
}
.send-gift {
margin-top: 3px;
}
.admin-tools-divider {
color: $gray-50;
cursor: default;
background-color: $gray-700;
font-size: 0.875em;
line-height: 1.71;
padding: 4px 12px;
height: 32px;
}
.info {
margin-top: 16px;
line-height: 1.71;
size: 0.875em;
width: 212px;
.info-item {
color: $gray-50;
margin-bottom: 4px;
margin-left: 24px;
.info-item-label {
display: inline-block;
font-weight: bold;
}
.info-item-value {
float: right;
}
}
}
.achievement {
margin: 0 auto;
}
.box {
border: dotted 1px #c3c0c7;
border-radius: 2px;
height: 92px;
width: 94px;
}
#achievements {
.category-row {
margin-bottom: 34px;
justify-content: center;
&:last-child {
margin-bottom: 0px;
}
}
.achievements-row {
margin: 0 auto;
max-width: 590px;
}
.achievement-wrapper {
margin-left: 12px;
margin-right: 12px;
max-width: 94px;
min-width: 94px;
padding: 0px;
width: 94px;
}
.box {
background: $white;
margin: 0 auto;
margin-bottom: 16px;
padding-top: 20px;
}
hr {
margin-bottom: 48px;
margin-top: 48px;
}
.box.achievement-unearned {
background-color: $gray-600;
}
.counter.badge {
background-color: $orange-100;
color: $white;
max-height: 24px;
position: absolute;
right: -8px;
top: -12.8px;
}
.achievement-icon {
margin: 0 auto;
}
.achievement-list-item {
border-top: 1px solid $gray-500;
padding-bottom: 12px;
padding-top: 11px;
&:last-child {
border-bottom: 1px solid $gray-500;
}
.badge {
background: $gray-600;
color: $gray-300;
height: fit-content;
margin-right: 8px;
}
}
}
@media (max-width: 550px) {
.member-details {
flex-direction: column;
}
.member-details .avatar {
margin-bottom: 15px;
}
}
</style>
// eslint-disable-next-line vue/component-tags-order
<script>
import moment from 'moment';
import axios from 'axios';
import each from 'lodash/each';
import cloneDeep from 'lodash/cloneDeep';
import toggleSwitch from '../ui/toggleSwitch';
import { mapState } from '@/libs/store';
import MemberDetails from '../memberDetails';
import markdown from '@/directives/markdown';
import achievementsLib from '@/../../common/script/libs/achievements';
import Content from '@/../../common/script/content';
import profileStats from './profileStats';
import message from '@/assets/svg/message.svg';
import gift from '@/assets/svg/gift.svg';
import block from '@/assets/svg/block.svg';
import positive from '@/assets/svg/positive.svg';
import dots from '@/assets/svg/dots.svg';
import megaphone from '@/assets/svg/broken-megaphone.svg';
import lock from '@/assets/svg/lock.svg';
import challenge from '@/assets/svg/challenge.svg';
import member from '@/assets/svg/member-icon.svg';
import staff from '@/assets/svg/tier-staff.svg';
import report from '@/assets/svg/report.svg';
import crown from '@/assets/svg/crown.svg';
import mute from '@/assets/svg/mute.svg';
import shadowMute from '@/assets/svg/shadow-mute.svg';
import error404 from '../404';
import externalLinks from '../../mixins/externalLinks';
import { userCustomStateMixin } from '../../mixins/userState';
// @TODO: EMAILS.COMMUNITY_MANAGER_EMAIL
const COMMUNITY_MANAGER_EMAIL = 'admin@habitica.com';
export default {
directives: {
markdown,
},
components: {
MemberDetails,
profileStats,
error404,
toggleSwitch,
},
mixins: [externalLinks, userCustomStateMixin('userLoggedIn')],
props: ['userId', 'startingPage'],
data () {
return {
icons: Object.freeze({
message,
block,
positive,
gift,
dots,
megaphone,
challenge,
lock,
member,
staff,
report,
crown,
mute,
shadowMute,
}),
userIdToMessage: '',
editing: false,
editingProfile: {
name: '',
imageUrl: '',
blurb: '',
},
hero: {},
managerEmail: {
hrefBlankCommunityManagerEmail: `<a href="mailto:${COMMUNITY_MANAGER_EMAIL}">${COMMUNITY_MANAGER_EMAIL}</a>`,
},
selectedPage: 'profile',
achievements: {},
achievementsCategories: {}, // number, open
content: Content,
user: null,
userLoaded: false,
oldTitle: null,
isOpened: true,
};
},
computed: {
...mapState({
flatGear: 'content.gear.flat',
}),
userJoinedDate () {
return moment(this.user.auth.timestamps.created)
.format(this.userLoggedIn.preferences.dateFormat.toUpperCase());
},
userLastLoggedIn () {
return moment(this.user.auth.timestamps.loggedin)
.format(this.userLoggedIn.preferences.dateFormat.toUpperCase());
},
equippedItems () {
return this.user.items.gear.equipped;
},
costumeItems () {
return this.user.items.gear.costume;
},
classText () {
const classTexts = {
warrior: this.$t('warrior'),
wizard: this.$t('mage'),
rogue: this.$t('rogue'),
healer: this.$t('healer'),
};
return classTexts[this.user.stats.class];
},
startingPageOption () {
return this.$store.state.profileOptions.startingPage;
},
hasClass () {
return this.$store.getters['members:hasClass'](this.userLoggedIn);
},
isOpen () {
// Open status is a number so we can tell if the value was passed
if (this.openStatus !== undefined) return this.openStatus === 1;
return this.isOpened;
},
userBlocked () {
return this.userLoggedIn.inbox.blocks.indexOf(this.user._id) !== -1;
},
// userBanned () {
// return this.userLoggedIn.auth.blocked.valueOf(this.user._id.auth.blocked);
// },
canReport () {
if (!this.user || !this.user.profile || !this.user.profile.flags) {
return true;
}
return Boolean(this.hasPermission(this.userLoggedIn, 'moderator')
|| !this.user.profile.flags[this.userLoggedIn._id]);
},
},
watch: {
startingPage () {
this.selectedPage = this.startingPage;
},
async userId () {
this.loadUser();
},
userLoggedIn () {
this.loadUser();
},
},
mounted () {
this.loadUser();
this.oldTitle = this.$store.state.title;
this.handleExternalLinks();
this.selectPage(this.startingPage);
this.$root.$on('habitica:report-profile-result', () => {
this.loadUser();
});
},
updated () {
this.handleExternalLinks();
},
beforeDestroy () {
if (this.oldTitle) {
this.$store.dispatch('common:setTitle', {
fullTitle: this.oldTitle,
});
}
this.$root.$off('habitica:report-profile-result');
},
methods: {
async loadUser () {
let user = null;
// Reset editing when user is changed. Move to watch or is this good?
this.editing = false;
this.hero = {};
this.userLoaded = false;
const profileUserId = this.userId;
if (profileUserId && profileUserId !== this.userLoggedIn._id) {
const response = await this.$store.dispatch('members:fetchMember', {
memberId: profileUserId,
unpack: false,
});
if (response.response && response.response.status === 404) {
user = null;
this.$store.dispatch('snackbars:add', {
title: 'Habitica',
text: this.$t('messageDeletedUser'),
type: 'error',
timeout: false,
});
} else if (response.status && response.status === 200) {
user = response.data.data;
}
} else {
user = this.userLoggedIn;
}
if (user) {
this.editingProfile.name = user.profile.name;
this.editingProfile.imageUrl = user.profile.imageUrl;
this.editingProfile.blurb = user.profile.blurb;
if (!user.achievements.quests) user.achievements.quests = {};
if (!user.achievements.challenges) user.achievements.challenges = {};
// @TODO: this common code should handle the above
this.achievements = achievementsLib.getAchievementsForProfile(user);
const achievementsCategories = {};
Object.keys(this.achievements).forEach(category => {
achievementsCategories[category] = {
open: false,
number: Object.keys(this.achievements[category].achievements).length,
};
});
this.achievementsCategories = achievementsCategories;
// @TODO For some reason markdown doesn't seem to be handling numbers or maybe undefined?
user.profile.blurb = user.profile.blurb ? `${user.profile.blurb}` : '';
this.user = user;
}
if (this.hasPermission(this.userLoggedIn, 'moderator')) {
this.hero = await this.$store.dispatch('hall:getHero', { uuid: this.user._id });
}
this.userLoaded = true;
},
selectPage (page) {
this.selectedPage = page || 'profile';
window.history.replaceState(null, null, '');
this.$store.dispatch('common:setTitle', {
section: this.$t('user'),
subSection: this.$t(this.startingPage),
});
},
getNextIncentive () {
const currentLoginDay = Content.loginIncentives[this.user.loginIncentives];
if (!currentLoginDay) return 0;
const previousRewardDay = currentLoginDay.prevRewardKey || 0;
const { nextRewardAt } = currentLoginDay;
return ((nextRewardAt - previousRewardDay));
},
save () {
const values = {};
const edits = cloneDeep(this.editingProfile);
each(edits, (value, key) => {
// Using toString because we need to compare two arrays (websites)
const curVal = this.user.profile[key];
if (!curVal || value.toString() !== curVal.toString()) {
values[`profile.${key}`] = value;
this.$set(this.user.profile, key, value);
}
});
this.$store.dispatch('user:set', values);
this.editing = false;
},
blockUser () {
this.userLoggedIn.inbox.blocks.push(this.user._id);
axios.post(`/api/v4/user/block/${this.user._id}`);
},
unblockUser () {
const index = this.userLoggedIn.inbox.blocks.indexOf(this.user._id);
this.userLoggedIn.inbox.blocks.splice(index, 1);
axios.post(`/api/v4/user/block/${this.user._id}`);
},
openSendGemsModal () {
this.$store.state.giftModalOptions.startingPage = 'buyGems';
this.$root.$emit('habitica::send-gift', this.user);
},
adminToggleShadowMute () {
if (!this.hero.flags) {
this.hero.flags = {};
this.hero.flags.chatShadowMuted = true;
} else {
this.hero.flags.chatShadowMuted = !this.hero.flags.chatShadowMuted;
}
this.$store.dispatch('hall:updateHero', { heroDetails: this.hero });
},
adminToggleChatRevoke () {
if (!this.hero.flags) {
this.hero.flags = {};
this.hero.flags.chatRevoked = true;
} else {
this.hero.flags.chatRevoked = !this.hero.flags.chatRevoked;
}
this.$store.dispatch('hall:updateHero', { heroDetails: this.hero });
},
adminToggleBan () {
this.hero.auth.blocked = !this.hero.auth.blocked;
this.$store.dispatch('hall:updateHero', { heroDetails: this.hero });
},
showAllocation () {
return this.user._id === this.userLoggedIn._id && this.hasClass;
},
achievementsCategory (categoryKey, category) {
const achievementsKeys = Object.keys(category.achievements);
if (this.achievementsCategories[categoryKey].open === true) {
return category.achievements;
}
const fiveAchievements = achievementsKeys.slice(0, 5);
const categoryAchievements = {};
fiveAchievements.forEach(key => {
categoryAchievements[key] = category.achievements[key];
});
return categoryAchievements;
},
toggleAchievementsCategory (categoryKey) {
const status = this.achievementsCategories[categoryKey].open;
this.achievementsCategories[categoryKey].open = !status;
},
toggle () {
this.isOpened = !this.isOpen;
this.$emit('toggled', this.isOpened);
},
open () {
this.isOpened = true;
this.$emit('toggled', this.isOpened);
},
reportPlayer () {
this.$root.$emit('habitica::report-profile', {
memberId: this.user._id,
displayName: this.user.profile.name,
username: this.user.auth.local.username,
});
},
openAdminPanel () {
this.$router.push(`/admin-panel/${this.hero._id}`);
},
},
};
</script>