mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-16 06:07:21 +01:00
Usernames Misc: Bulk Email and New Party Modal (#10898)
* feat(migrations): genericize bulk email * chore(migrations): archive one-offs * feat(usernames): change create party modal to copy username * fix(modal): styling * fix(modal): add Chrome clipboard implementation
This commit is contained in:
61
migrations/users/bulk-email.js
Normal file
61
migrations/users/bulk-email.js
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
/* eslint-disable no-console */
|
||||||
|
import { sendTxn } from '../../../website/server/libs/email';
|
||||||
|
import { model as User } from '../../website/server/models/user';
|
||||||
|
import moment from 'moment';
|
||||||
|
import nconf from 'nconf';
|
||||||
|
const BASE_URL = nconf.get('BASE_URL');
|
||||||
|
const EMAIL_SLUG = 'mandrill-email-slug'; // Set email template to send
|
||||||
|
const MIGRATION_NAME = 'bulk-email';
|
||||||
|
|
||||||
|
const progressCount = 1000;
|
||||||
|
let count = 0;
|
||||||
|
|
||||||
|
async function updateUser (user) {
|
||||||
|
count++;
|
||||||
|
|
||||||
|
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
|
||||||
|
|
||||||
|
sendTxn(
|
||||||
|
user,
|
||||||
|
EMAIL_SLUG,
|
||||||
|
[{name: 'BASE_URL', content: BASE_URL}] // Add variables from template
|
||||||
|
);
|
||||||
|
|
||||||
|
return await User.update({_id: user._id}, {$set: {migration: MIGRATION_NAME}}).exec();
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = async function processUsers () {
|
||||||
|
let query = {
|
||||||
|
migration: {$ne: MIGRATION_NAME},
|
||||||
|
'auth.timestamps.loggedin': {$gt: moment().subtract(2, 'weeks').toDate()}, // customize or remove to target different populations
|
||||||
|
};
|
||||||
|
|
||||||
|
const fields = {
|
||||||
|
_id: 1,
|
||||||
|
auth: 1,
|
||||||
|
preferences: 1,
|
||||||
|
profile: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
while (true) { // eslint-disable-line no-constant-condition
|
||||||
|
const users = await User // eslint-disable-line no-await-in-loop
|
||||||
|
.find(query)
|
||||||
|
.limit(250)
|
||||||
|
.sort({_id: 1})
|
||||||
|
.select(fields)
|
||||||
|
.lean()
|
||||||
|
.exec();
|
||||||
|
|
||||||
|
if (users.length === 0) {
|
||||||
|
console.warn('All appropriate users found and modified.');
|
||||||
|
console.warn(`\n${count} users processed\n`);
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
query._id = {
|
||||||
|
$gt: users[users.length - 1],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all(users.map(updateUser)); // eslint-disable-line no-await-in-loop
|
||||||
|
}
|
||||||
|
};
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 11 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 9.7 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 24 KiB |
BIN
website/client/assets/images/group@3x.png
Normal file
BIN
website/client/assets/images/group@3x.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 447 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 24 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 32 KiB |
@@ -1,100 +1,61 @@
|
|||||||
<template lang="pug">
|
<template lang="pug">
|
||||||
b-modal#create-party-modal(title="Empty", size='lg', hide-footer=true)
|
b-modal#create-party-modal(size='lg', hide-footer=true)
|
||||||
.header-wrap(slot="modal-header")
|
.header-wrap(slot="modal-header")
|
||||||
.quest_screen
|
.quest_screen
|
||||||
.row.heading
|
.row.heading
|
||||||
.col-12.text-center
|
.col-12.text-center.pr-5.pl-5
|
||||||
h2(v-once) {{$t('playInPartyTitle')}}
|
h2(v-once) {{ $t('playInPartyTitle') }}
|
||||||
p(v-once) {{$t('playInPartyDescription')}}
|
p.mb-4(v-once) {{ $t('playInPartyDescription') }}
|
||||||
|
button.btn.btn-primary(v-once, @click='createParty()') {{ $t('createParty') }}
|
||||||
.row.grey-row
|
.row.grey-row
|
||||||
.col-6.text-center
|
.col-12.text-center
|
||||||
.start-party
|
.join-party
|
||||||
h3(v-once) {{$t('startYourOwnPartyTitle')}}
|
h2(v-once) {{ $t('wantToJoinPartyTitle') }}
|
||||||
p(v-once) {{$t('startYourOwnPartyDescription')}}
|
p(v-html='$t("wantToJoinPartyDescription")')
|
||||||
button.btn.btn-primary(v-once, @click='createParty()') {{$t('createParty')}}
|
.form-group(@click='copyUsername')
|
||||||
.col-6
|
.d-flex.align-items-center
|
||||||
div.text-center
|
label.mr-3(v-once) {{ $t('username') }}
|
||||||
.join-party
|
.flex-grow-1
|
||||||
h3(v-once) {{$t('wantToJoinPartyTitle')}}
|
.input-group-prepend.input-group-text @
|
||||||
p(v-once) {{$t('wantToJoinPartyDescription')}}
|
.text {{ user.auth.local.username }}
|
||||||
button.btn.btn-primary(v-once, @click='shareUserId()') {{$t('shartUserId')}}
|
.svg-icon.copy-icon(v-html='icons.copy')
|
||||||
.share-userid-options(v-if="shareUserIdShown")
|
.small(v-once) {{ $t('copy') }}
|
||||||
.option-item(@click='copyUserId()')
|
|
||||||
.svg-icon(v-html="icons.copy")
|
|
||||||
input(type="text", v-model="user._id", id="userIdInput")
|
|
||||||
| Copy User ID
|
|
||||||
//.option-item(v-once)
|
|
||||||
.svg-icon(v-html="icons.greyBadge")
|
|
||||||
| {{$t('lookingForGroup')}}
|
|
||||||
//.option-item(v-once)
|
|
||||||
.svg-icon(v-html="icons.qrCode")
|
|
||||||
| {{$t('qrCode')}}
|
|
||||||
//.option-item(v-once)
|
|
||||||
.svg-icon.facebook(v-html="icons.facebook")
|
|
||||||
| Facebook
|
|
||||||
//.option-item(v-once)
|
|
||||||
.svg-icon(v-html="icons.twitter")
|
|
||||||
| Twitter
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
#create-party-modal .modal-body {
|
||||||
|
padding: 0rem 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
#create-party-modal .modal-dialog {
|
#create-party-modal .modal-dialog {
|
||||||
width: 684px;
|
width: 35.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
#create-party-modal .modal-header {
|
#create-party-modal .modal-header {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
border-bottom: 0px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss" scoped>
|
||||||
@import '~client/assets/scss/colors.scss';
|
@import '~client/assets/scss/colors.scss';
|
||||||
|
|
||||||
.heading {
|
.copy-icon {
|
||||||
margin-top: 1em;
|
width: 1rem;
|
||||||
margin-bottom: 1em;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-wrap {
|
.form-control[readonly] {
|
||||||
padding: 0;
|
background-color: $white;
|
||||||
color: #4e4a57;
|
color: $gray-50;
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
.quest_screen {
|
|
||||||
background-image: url('~client/assets/images/quest_screen.png');
|
|
||||||
background-size: cover;
|
|
||||||
width: 100%;
|
|
||||||
height: 246px;
|
|
||||||
margin-bottom: .5em;
|
|
||||||
border-radius: 2px 2px 0 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
color: #4f2a93;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.start-party {
|
.form-group {
|
||||||
background-image: url('~client/assets/images/basilist@3x.png');
|
background-color: $gray-600;
|
||||||
background-size: cover;
|
border-radius: 2px;
|
||||||
width: 122px;
|
border: solid 1px $gray-500;
|
||||||
height: 69px;
|
width: 90%;
|
||||||
margin: 0 auto;
|
margin: auto;
|
||||||
margin-bottom: 1em;
|
cursor: pointer;
|
||||||
}
|
|
||||||
|
|
||||||
.join-party {
|
|
||||||
background-image: url('~client/assets/images/party@3x.png');
|
|
||||||
background-size: cover;
|
|
||||||
width: 203px;
|
|
||||||
height: 66px;
|
|
||||||
margin: 0 auto;
|
|
||||||
margin-bottom: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-body {
|
|
||||||
padding-bottom: 0;
|
|
||||||
padding-top: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.grey-row {
|
.grey-row {
|
||||||
@@ -104,97 +65,130 @@ b-modal#create-party-modal(title="Empty", size='lg', hide-footer=true)
|
|||||||
border-radius: 0px 0px 2px 2px;
|
border-radius: 0px 0px 2px 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.share-userid-options {
|
h2 {
|
||||||
background-color: $white;
|
color: $gray-100;
|
||||||
border-radius: 2px;
|
}
|
||||||
width: 220px;
|
|
||||||
position: absolute;
|
|
||||||
top: 9em;
|
|
||||||
left: 4.8em;
|
|
||||||
box-shadow: 0 2px 2px 0 rgba(26, 24, 29, 0.16), 0 1px 4px 0 rgba(26, 24, 29, 0.12);
|
|
||||||
|
|
||||||
#userIdInput {
|
.header-wrap {
|
||||||
position: absolute;
|
padding: 0;
|
||||||
left: 1000rem;
|
color: #4e4a57;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.quest_screen {
|
||||||
|
background-image: url('~client/assets/images/group@3x.png');
|
||||||
|
background-size: cover;
|
||||||
|
width: 100%;
|
||||||
|
height: 246px;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
border-radius: 2px 2px 0 0;
|
||||||
|
image-rendering: optimizequality;
|
||||||
}
|
}
|
||||||
|
|
||||||
.option-item {
|
h2 {
|
||||||
padding: 1em;
|
|
||||||
|
|
||||||
.svg-icon {
|
|
||||||
margin-right: .5em;
|
|
||||||
width: 20px;
|
|
||||||
display: inline-block;
|
|
||||||
vertical-align: bottom;
|
|
||||||
}
|
|
||||||
|
|
||||||
.facebook svg {
|
|
||||||
width: 15px;
|
|
||||||
height: 15px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.option-item:hover {
|
|
||||||
background-color: $header-color;
|
|
||||||
color: $purple-200;
|
color: $purple-200;
|
||||||
cursor: pointer;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.heading {
|
||||||
|
margin-top: 1rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
input.form-control {
|
||||||
|
border: 0px;
|
||||||
|
padding-left: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-group-prepend {
|
||||||
|
margin-right: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-group-text {
|
||||||
|
background-color: $white;
|
||||||
|
border: 0px;
|
||||||
|
border-radius: 0px;
|
||||||
|
color: $gray-300;
|
||||||
|
padding: 0rem 0.1rem 0rem 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.join-party {
|
||||||
|
background-image: url('~client/assets/images/party.png');
|
||||||
|
background-size: cover;
|
||||||
|
width: 203px;
|
||||||
|
height: 66px;
|
||||||
|
margin: 0 auto;
|
||||||
|
margin-bottom: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
color: $gray-100;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 0rem;
|
||||||
|
margin-left: 1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-dialog .text {
|
||||||
|
min-height: 1rem;
|
||||||
|
margin: 0.75rem auto 0.75rem 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.small {
|
||||||
|
color: $gray-200;
|
||||||
|
margin: auto 0.5rem auto 0.25rem;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapState } from 'client/libs/store';
|
import { mapState } from 'client/libs/store';
|
||||||
import * as Analytics from 'client/libs/analytics';
|
import * as Analytics from 'client/libs/analytics';
|
||||||
|
import notifications from 'client/mixins/notifications';
|
||||||
|
|
||||||
import copyIcon from 'assets/svg/copy.svg';
|
import copyIcon from 'assets/svg/copy.svg';
|
||||||
import greyBadgeIcon from 'assets/svg/grey-badge.svg';
|
|
||||||
import qrCodeIcon from 'assets/svg/qrCode.svg';
|
|
||||||
import facebookIcon from 'assets/svg/facebook.svg';
|
|
||||||
import twitterIcon from 'assets/svg/twitter.svg';
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
icons: Object.freeze({
|
icons: Object.freeze({
|
||||||
copy: copyIcon,
|
copy: copyIcon,
|
||||||
greyBadge: greyBadgeIcon,
|
}),
|
||||||
qrCode: qrCodeIcon,
|
|
||||||
facebook: facebookIcon,
|
|
||||||
twitter: twitterIcon,
|
|
||||||
}),
|
|
||||||
shareUserIdShown: false,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
...mapState({user: 'user.data'}),
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
shareUserId () {
|
|
||||||
this.shareUserIdShown = !this.shareUserIdShown;
|
|
||||||
},
|
|
||||||
async createParty () {
|
|
||||||
let group = {
|
|
||||||
type: 'party',
|
|
||||||
};
|
};
|
||||||
group.name = this.$t('possessiveParty', {name: this.user.profile.name});
|
|
||||||
let party = await this.$store.dispatch('guilds:create', {group});
|
|
||||||
this.$store.state.party.data = party;
|
|
||||||
this.user.party._id = party._id;
|
|
||||||
|
|
||||||
Analytics.updateUser({
|
|
||||||
partyID: party._id,
|
|
||||||
partySize: 1,
|
|
||||||
});
|
|
||||||
|
|
||||||
this.$root.$emit('bv::hide::modal', 'create-party-modal');
|
|
||||||
this.$router.push('/party');
|
|
||||||
},
|
},
|
||||||
copyUserId () {
|
computed: {
|
||||||
const copyText = document.getElementById('userIdInput');
|
...mapState({user: 'user.data'}),
|
||||||
copyText.select();
|
|
||||||
document.execCommand('copy');
|
|
||||||
alert('User ID has been copied');
|
|
||||||
},
|
},
|
||||||
},
|
methods: {
|
||||||
};
|
async createParty () {
|
||||||
|
let group = {
|
||||||
|
type: 'party',
|
||||||
|
};
|
||||||
|
group.name = this.$t('possessiveParty', {name: this.user.profile.name});
|
||||||
|
let party = await this.$store.dispatch('guilds:create', {group});
|
||||||
|
this.$store.state.party.data = party;
|
||||||
|
this.user.party._id = party._id;
|
||||||
|
|
||||||
|
Analytics.updateUser({
|
||||||
|
partyID: party._id,
|
||||||
|
partySize: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.$root.$emit('bv::hide::modal', 'create-party-modal');
|
||||||
|
this.$router.push('/party');
|
||||||
|
},
|
||||||
|
copyUsername () {
|
||||||
|
if (navigator.clipboard) {
|
||||||
|
navigator.clipboard.writeText(this.user.auth.local.username);
|
||||||
|
} else {
|
||||||
|
let copyText = document.createElement('textarea');
|
||||||
|
copyText.value = this.user.auth.local.username;
|
||||||
|
document.body.appendChild(copyText);
|
||||||
|
copyText.select();
|
||||||
|
document.execCommand('copy');
|
||||||
|
document.body.removeChild(copyText);
|
||||||
|
}
|
||||||
|
this.text(this.$t('usernameCopied'));
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mixins: [notifications],
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -45,6 +45,7 @@
|
|||||||
"LFG": "To advertise your new Party or find one to join, go to the <%= linkStart %>Party Wanted (Looking for Group)<%= linkEnd %> Guild.",
|
"LFG": "To advertise your new Party or find one to join, go to the <%= linkStart %>Party Wanted (Looking for Group)<%= linkEnd %> Guild.",
|
||||||
"wantExistingParty": "Want to join an existing Party? Go to the <%= linkStart %>Party Wanted Guild<%= linkEnd %> and post this User ID:",
|
"wantExistingParty": "Want to join an existing Party? Go to the <%= linkStart %>Party Wanted Guild<%= linkEnd %> and post this User ID:",
|
||||||
"joinExistingParty": "Join Someone Else's Party",
|
"joinExistingParty": "Join Someone Else's Party",
|
||||||
|
"usernameCopied": "Username copied to clipboard.",
|
||||||
"needPartyToStartQuest": "Whoops! You need to <a href='http://habitica.wikia.com/wiki/Party' target='_blank'>create or join a Party</a> before you can start a quest!",
|
"needPartyToStartQuest": "Whoops! You need to <a href='http://habitica.wikia.com/wiki/Party' target='_blank'>create or join a Party</a> before you can start a quest!",
|
||||||
"createGroupPlan": "Create",
|
"createGroupPlan": "Create",
|
||||||
"create": "Create",
|
"create": "Create",
|
||||||
@@ -417,9 +418,8 @@
|
|||||||
"playInPartyDescription": "Take on amazing quests with friends or on your own. Battle monsters, create Challenges, and help yourself stay accountable through Parties.",
|
"playInPartyDescription": "Take on amazing quests with friends or on your own. Battle monsters, create Challenges, and help yourself stay accountable through Parties.",
|
||||||
"startYourOwnPartyTitle": "Start your own Party",
|
"startYourOwnPartyTitle": "Start your own Party",
|
||||||
"startYourOwnPartyDescription": "Battle monsters solo or invite as many of your friends as you'd like!",
|
"startYourOwnPartyDescription": "Battle monsters solo or invite as many of your friends as you'd like!",
|
||||||
"shartUserId": "Share User ID",
|
|
||||||
"wantToJoinPartyTitle": "Want to join a Party?",
|
"wantToJoinPartyTitle": "Want to join a Party?",
|
||||||
"wantToJoinPartyDescription": "Give your User ID to a friend who already has a Party, or head to the Party Wanted Guild to meet potential comrades!",
|
"wantToJoinPartyDescription": "Give your username to a friend who already has a Party, or head to the <a href='/groups/guild/f2db2a7f-13c5-454d-b3ee-ea1f5089e601'>Party Wanted Guild</a> to meet potential comrades!",
|
||||||
"copy": "Copy",
|
"copy": "Copy",
|
||||||
"inviteToPartyOrQuest": "Invite Party to Quest",
|
"inviteToPartyOrQuest": "Invite Party to Quest",
|
||||||
"inviteInformation": "Clicking \"Invite\" will send an invitation to your Party members. When all members have accepted or denied, the Quest begins.",
|
"inviteInformation": "Clicking \"Invite\" will send an invitation to your Party members. When all members have accepted or denied, the Quest begins.",
|
||||||
|
|||||||
Reference in New Issue
Block a user