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:
Sabe Jones
2018-12-11 14:47:50 -06:00
parent 900676bf0a
commit d096695559
15 changed files with 211 additions and 156 deletions

View 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

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

View File

@@ -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>

View File

@@ -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.",