diff --git a/gulpfile.js b/gulpfile.js index 628e7507fb..39bb5da687 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -13,6 +13,7 @@ if (process.env.NODE_ENV === 'production') { require('./gulp/gulp-newstuff'); require('./gulp/gulp-build'); require('./gulp/gulp-babelify'); + require('./gulp/gulp-bootstrap'); } else { require('glob').sync('./gulp/gulp-*').forEach(require); require('gulp').task('default', ['test']); diff --git a/migrations/challenges/sync-all-challenges.js b/migrations/challenges/sync-all-challenges.js index 5200bab806..4cb9d00088 100644 --- a/migrations/challenges/sync-all-challenges.js +++ b/migrations/challenges/sync-all-challenges.js @@ -1,47 +1,47 @@ -import Bluebird from 'Bluebird'; - -import { model as Challenges } from '../../website/server/models/challenge'; -import { model as User } from '../../website/server/models/user'; - -async function syncChallengeToMembers (challenges) { - let challengSyncPromises = challenges.map(async function (challenge) { - let users = await User.find({ - // _id: '', - challenges: challenge._id, - }).exec(); - - let promises = []; - users.forEach(function (user) { - promises.push(challenge.syncToUser(user)); - promises.push(challenge.save()); - promises.push(user.save()); - }); - - return Bluebird.all(promises); - }); - - return await Bluebird.all(challengSyncPromises); -} - -async function syncChallenges (lastChallengeDate) { - let query = { - // _id: '', - }; - - if (lastChallengeDate) { - query.createdOn = { $lte: lastChallengeDate }; - } - - let challengesFound = await Challenges.find(query) - .limit(10) - .sort('-createdAt') - .exec(); - - let syncedChallenges = await syncChallengeToMembers(challengesFound) - .catch(reason => console.error(reason)); - let lastChallenge = challengesFound[challengesFound.length - 1]; - if (lastChallenge) syncChallenges(lastChallenge.createdAt); - return syncedChallenges; -}; - -module.exports = syncChallenges; +import Bluebird from 'Bluebird'; + +import { model as Challenges } from '../../website/server/models/challenge'; +import { model as User } from '../../website/server/models/user'; + +async function syncChallengeToMembers (challenges) { + let challengSyncPromises = challenges.map(async function (challenge) { + let users = await User.find({ + // _id: '', + challenges: challenge._id, + }).exec(); + + let promises = []; + users.forEach(function (user) { + promises.push(challenge.syncToUser(user)); + promises.push(challenge.save()); + promises.push(user.save()); + }); + + return Bluebird.all(promises); + }); + + return await Bluebird.all(challengSyncPromises); +} + +async function syncChallenges (lastChallengeDate) { + let query = { + // _id: '', + }; + + if (lastChallengeDate) { + query.createdOn = { $lte: lastChallengeDate }; + } + + let challengesFound = await Challenges.find(query) + .limit(10) + .sort('-createdAt') + .exec(); + + let syncedChallenges = await syncChallengeToMembers(challengesFound) + .catch(reason => console.error(reason)); + let lastChallenge = challengesFound[challengesFound.length - 1]; + if (lastChallenge) syncChallenges(lastChallenge.createdAt); + return syncedChallenges; +}; + +module.exports = syncChallenges; diff --git a/migrations/migration-runner.js b/migrations/migration-runner.js index 4f04540268..33b6d35279 100644 --- a/migrations/migration-runner.js +++ b/migrations/migration-runner.js @@ -21,4 +21,4 @@ var processUsers = require('./groups/update-groups-with-group-plans'); processUsers() .catch(function (err) { console.log(err) - }) \ No newline at end of file + }) diff --git a/migrations/users/users-to-test.js b/migrations/users/users-to-test.js new file mode 100644 index 0000000000..64b6c38e07 --- /dev/null +++ b/migrations/users/users-to-test.js @@ -0,0 +1,109 @@ +var migrationName = 'UserFromProdToTest'; +var authorName = 'TheHollidayInn'; // in case script author needs to know when their ... +var authorUuid = ''; //... own data is done + +/* + * This migraition will copy user data from prod to test + */ + +var monk = require('monk'); +var testConnectionSting = ''; // FOR TEST DATABASE +var usersTest = monk(testConnectionSting).get('users', { castIds: false }); +var groupsTest = monk(testConnectionSting).get('groups', { castIds: false }); +var challengesTest = monk(testConnectionSting).get('challenges', { castIds: false }); +var tasksTest = monk(testConnectionSting).get('tasks', { castIds: false }); + +var monk2 = require('monk'); +var liveConnectString = ''; // FOR TEST DATABASE +var userLive = monk2(liveConnectString).get('users', { castIds: false }); +var groupsLive = monk2(liveConnectString).get('groups', { castIds: false }); +var challengesLive = monk2(liveConnectString).get('challenges', { castIds: false }); +var tasksLive = monk2(liveConnectString).get('tasks', { castIds: false }); + +import uniq from 'lodash/uniq'; +import Bluebird from 'bluebird'; + +// Variabls for updating +let userIds = [ + '206039c6-24e4-4b9f-8a31-61cbb9aa3f66', +]; + +let groupIds = []; +let challengeIds = []; +let tasksIds = []; + +async function processUsers () { + let userPromises = []; + //{_id: {$in: userIds}} + + return userLive.find({guilds: 'b0764d64-8276-45a1-afa5-5ca9a5c64ca0'}) + .each((user, {close, pause, resume}) => { + if (user.guilds.length > 0) groupIds = groupIds.concat(user.guilds); + if (user.party._id) groupIds.push(user.party._id); + if (user.challenges.length > 0) challengeIds = challengeIds.concat(user.challenges); + if (user.tasksOrder.rewards.length > 0) tasksIds = tasksIds.concat(user.tasksOrder.rewards); + if (user.tasksOrder.todos.length > 0) tasksIds = tasksIds.concat(user.tasksOrder.todos); + if (user.tasksOrder.dailys.length > 0) tasksIds = tasksIds.concat(user.tasksOrder.dailys); + if (user.tasksOrder.habits.length > 0) tasksIds = tasksIds.concat(user.tasksOrder.habits); + + let userPromise = usersTest.update({'_id': user._id}, user, {upsert:true}); + userPromises.push(userPromise); + }).then(() => { + return Bluebird.all(userPromises); + }) + .then(() => { + console.log("Done User"); + }); +} + +function processGroups () { + let promises = []; + let groupsToQuery = uniq(groupIds); + return groupsLive.find({_id: {$in: groupsToQuery}}) + .each((group, {close, pause, resume}) => { + let promise = groupsTest.update({_id: group._id}, group, {upsert:true}); + promises.push(promise); + }).then(() => { + return Bluebird.all(promises); + }) + .then(() => { + console.log("Done Group"); + }); +} + +function processChallenges () { + let promises = []; + let challengesToQuery = uniq(challengeIds); + return challengesLive.find({_id: {$in: challengesToQuery}}) + .each((challenge, {close, pause, resume}) => { + let promise = challengesTest.update({_id: challenge._id}, challenge, {upsert:true}); + promises.push(promise); + }).then(() => { + return Bluebird.all(promises); + }) + .then(() => { + console.log("Done Challenge"); + }); +} + +function processTasks () { + let promises = []; + let tasksToQuery = uniq(tasksIds); + return tasksLive.find({_id: {$in: tasksToQuery}}) + .each((task, {close, pause, resume}) => { + let promise = tasksTest.update({_id: task._id}, task, {upsert:true}); + promises.push(promise); + }).then(() => { + return Bluebird.all(promises); + }) + .then(() => { + console.log("Done Tasks"); + }); +} + +module.exports = async function prodToTest () { + await processUsers(); + await processGroups(); + await processChallenges(); + await processTasks(); +}; diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index a9635b172c..0496a3b975 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -4208,700 +4208,6 @@ "from": "fs.realpath@>=1.0.0 <2.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz" }, - "fsevents": { - "version": "1.1.1", - "from": "fsevents@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.1.1.tgz", - "optional": true, - "dependencies": { - "abbrev": { - "version": "1.1.0", - "from": "abbrev@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.0.tgz", - "optional": true - }, - "ansi-regex": { - "version": "2.1.1", - "from": "ansi-regex@>=2.0.0 <3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz" - }, - "ansi-styles": { - "version": "2.2.1", - "from": "ansi-styles@>=2.2.1 <3.0.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "optional": true - }, - "aproba": { - "version": "1.1.1", - "from": "aproba@>=1.0.3 <2.0.0", - "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.1.1.tgz", - "optional": true - }, - "are-we-there-yet": { - "version": "1.1.2", - "from": "are-we-there-yet@>=1.1.2 <1.2.0", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.2.tgz", - "optional": true - }, - "asn1": { - "version": "0.2.3", - "from": "asn1@>=0.2.3 <0.3.0", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", - "optional": true - }, - "assert-plus": { - "version": "0.2.0", - "from": "assert-plus@>=0.2.0 <0.3.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.2.0.tgz", - "optional": true - }, - "asynckit": { - "version": "0.4.0", - "from": "asynckit@>=0.4.0 <0.5.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "optional": true - }, - "aws-sign2": { - "version": "0.6.0", - "from": "aws-sign2@>=0.6.0 <0.7.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.6.0.tgz", - "optional": true - }, - "aws4": { - "version": "1.6.0", - "from": "aws4@>=1.2.1 <2.0.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.6.0.tgz", - "optional": true - }, - "balanced-match": { - "version": "0.4.2", - "from": "balanced-match@>=0.4.1 <0.5.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.2.tgz" - }, - "bcrypt-pbkdf": { - "version": "1.0.1", - "from": "bcrypt-pbkdf@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz", - "optional": true - }, - "block-stream": { - "version": "0.0.9", - "from": "block-stream@*", - "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz" - }, - "boom": { - "version": "2.10.1", - "from": "boom@>=2.0.0 <3.0.0", - "resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz" - }, - "brace-expansion": { - "version": "1.1.6", - "from": "brace-expansion@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.6.tgz" - }, - "buffer-shims": { - "version": "1.0.0", - "from": "buffer-shims@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/buffer-shims/-/buffer-shims-1.0.0.tgz" - }, - "caseless": { - "version": "0.11.0", - "from": "caseless@>=0.11.0 <0.12.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.11.0.tgz", - "optional": true - }, - "chalk": { - "version": "1.1.3", - "from": "chalk@>=1.1.1 <2.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "optional": true - }, - "code-point-at": { - "version": "1.1.0", - "from": "code-point-at@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz" - }, - "combined-stream": { - "version": "1.0.5", - "from": "combined-stream@>=1.0.5 <1.1.0", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.5.tgz" - }, - "commander": { - "version": "2.9.0", - "from": "commander@>=2.9.0 <3.0.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.9.0.tgz", - "optional": true - }, - "concat-map": { - "version": "0.0.1", - "from": "concat-map@0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" - }, - "console-control-strings": { - "version": "1.1.0", - "from": "console-control-strings@>=1.1.0 <1.2.0", - "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz" - }, - "core-util-is": { - "version": "1.0.2", - "from": "core-util-is@>=1.0.0 <1.1.0", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz" - }, - "cryptiles": { - "version": "2.0.5", - "from": "cryptiles@>=2.0.0 <3.0.0", - "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz", - "optional": true - }, - "dashdash": { - "version": "1.14.1", - "from": "dashdash@>=1.12.0 <2.0.0", - "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "optional": true, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "from": "assert-plus@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "optional": true - } - } - }, - "debug": { - "version": "2.2.0", - "from": "debug@>=2.2.0 <2.3.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz", - "optional": true - }, - "deep-extend": { - "version": "0.4.1", - "from": "deep-extend@>=0.4.0 <0.5.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.4.1.tgz", - "optional": true - }, - "delayed-stream": { - "version": "1.0.0", - "from": "delayed-stream@>=1.0.0 <1.1.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz" - }, - "delegates": { - "version": "1.0.0", - "from": "delegates@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "optional": true - }, - "ecc-jsbn": { - "version": "0.1.1", - "from": "ecc-jsbn@>=0.1.1 <0.2.0", - "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", - "optional": true - }, - "escape-string-regexp": { - "version": "1.0.5", - "from": "escape-string-regexp@>=1.0.2 <2.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "optional": true - }, - "extend": { - "version": "3.0.0", - "from": "extend@>=3.0.0 <3.1.0", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.0.tgz", - "optional": true - }, - "extsprintf": { - "version": "1.0.2", - "from": "extsprintf@1.0.2", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.0.2.tgz" - }, - "forever-agent": { - "version": "0.6.1", - "from": "forever-agent@>=0.6.1 <0.7.0", - "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "optional": true - }, - "form-data": { - "version": "2.1.2", - "from": "form-data@>=2.1.1 <2.2.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.1.2.tgz", - "optional": true - }, - "fs.realpath": { - "version": "1.0.0", - "from": "fs.realpath@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz" - }, - "fstream": { - "version": "1.0.10", - "from": "fstream@>=1.0.2 <2.0.0", - "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.10.tgz" - }, - "fstream-ignore": { - "version": "1.0.5", - "from": "fstream-ignore@>=1.0.5 <1.1.0", - "resolved": "https://registry.npmjs.org/fstream-ignore/-/fstream-ignore-1.0.5.tgz", - "optional": true - }, - "gauge": { - "version": "2.7.3", - "from": "gauge@>=2.7.1 <2.8.0", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.3.tgz", - "optional": true - }, - "generate-function": { - "version": "2.0.0", - "from": "generate-function@>=2.0.0 <3.0.0", - "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.0.0.tgz", - "optional": true - }, - "generate-object-property": { - "version": "1.2.0", - "from": "generate-object-property@>=1.1.0 <2.0.0", - "resolved": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-1.2.0.tgz", - "optional": true - }, - "getpass": { - "version": "0.1.6", - "from": "getpass@>=0.1.1 <0.2.0", - "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.6.tgz", - "optional": true, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "from": "assert-plus@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "optional": true - } - } - }, - "glob": { - "version": "7.1.1", - "from": "glob@>=7.0.5 <8.0.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.1.tgz" - }, - "graceful-fs": { - "version": "4.1.11", - "from": "graceful-fs@>=4.1.2 <5.0.0", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz" - }, - "graceful-readlink": { - "version": "1.0.1", - "from": "graceful-readlink@>=1.0.0", - "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz", - "optional": true - }, - "har-validator": { - "version": "2.0.6", - "from": "har-validator@>=2.0.6 <2.1.0", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-2.0.6.tgz", - "optional": true - }, - "has-ansi": { - "version": "2.0.0", - "from": "has-ansi@>=2.0.0 <3.0.0", - "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", - "optional": true - }, - "has-unicode": { - "version": "2.0.1", - "from": "has-unicode@>=2.0.0 <3.0.0", - "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "optional": true - }, - "hawk": { - "version": "3.1.3", - "from": "hawk@>=3.1.3 <3.2.0", - "resolved": "https://registry.npmjs.org/hawk/-/hawk-3.1.3.tgz", - "optional": true - }, - "hoek": { - "version": "2.16.3", - "from": "hoek@>=2.0.0 <3.0.0", - "resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz" - }, - "http-signature": { - "version": "1.1.1", - "from": "http-signature@>=1.1.0 <1.2.0", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.1.tgz", - "optional": true - }, - "inflight": { - "version": "1.0.6", - "from": "inflight@>=1.0.4 <2.0.0", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz" - }, - "inherits": { - "version": "2.0.3", - "from": "inherits@>=2.0.1 <2.1.0", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz" - }, - "ini": { - "version": "1.3.4", - "from": "ini@>=1.3.0 <1.4.0", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.4.tgz", - "optional": true - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "from": "is-fullwidth-code-point@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz" - }, - "is-my-json-valid": { - "version": "2.15.0", - "from": "is-my-json-valid@>=2.12.4 <3.0.0", - "resolved": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.15.0.tgz", - "optional": true - }, - "is-property": { - "version": "1.0.2", - "from": "is-property@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", - "optional": true - }, - "is-typedarray": { - "version": "1.0.0", - "from": "is-typedarray@>=1.0.0 <1.1.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "optional": true - }, - "isarray": { - "version": "1.0.0", - "from": "isarray@>=1.0.0 <1.1.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz" - }, - "isstream": { - "version": "0.1.2", - "from": "isstream@>=0.1.2 <0.2.0", - "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "optional": true - }, - "jodid25519": { - "version": "1.0.2", - "from": "jodid25519@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/jodid25519/-/jodid25519-1.0.2.tgz", - "optional": true - }, - "jsbn": { - "version": "0.1.1", - "from": "jsbn@>=0.1.0 <0.2.0", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "optional": true - }, - "json-schema": { - "version": "0.2.3", - "from": "json-schema@0.2.3", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", - "optional": true - }, - "json-stringify-safe": { - "version": "5.0.1", - "from": "json-stringify-safe@>=5.0.1 <5.1.0", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "optional": true - }, - "jsonpointer": { - "version": "4.0.1", - "from": "jsonpointer@>=4.0.0 <5.0.0", - "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-4.0.1.tgz", - "optional": true - }, - "jsprim": { - "version": "1.3.1", - "from": "jsprim@>=1.2.2 <2.0.0", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.3.1.tgz", - "optional": true - }, - "mime-db": { - "version": "1.26.0", - "from": "mime-db@>=1.26.0 <1.27.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.26.0.tgz" - }, - "mime-types": { - "version": "2.1.14", - "from": "mime-types@>=2.1.7 <2.2.0", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.14.tgz" - }, - "minimatch": { - "version": "3.0.3", - "from": "minimatch@>=3.0.2 <4.0.0", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.3.tgz" - }, - "minimist": { - "version": "0.0.8", - "from": "minimist@0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz" - }, - "mkdirp": { - "version": "0.5.1", - "from": "mkdirp@>=0.5.1 <0.6.0", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz" - }, - "ms": { - "version": "0.7.1", - "from": "ms@0.7.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz", - "optional": true - }, - "node-pre-gyp": { - "version": "0.6.33", - "from": "node-pre-gyp@>=0.6.29 <0.7.0", - "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.6.33.tgz", - "optional": true - }, - "nopt": { - "version": "3.0.6", - "from": "nopt@>=3.0.6 <3.1.0", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", - "optional": true - }, - "npmlog": { - "version": "4.0.2", - "from": "npmlog@>=4.0.1 <5.0.0", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.0.2.tgz", - "optional": true - }, - "number-is-nan": { - "version": "1.0.1", - "from": "number-is-nan@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz" - }, - "oauth-sign": { - "version": "0.8.2", - "from": "oauth-sign@>=0.8.1 <0.9.0", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", - "optional": true - }, - "object-assign": { - "version": "4.1.1", - "from": "object-assign@>=4.1.0 <5.0.0", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "optional": true - }, - "once": { - "version": "1.4.0", - "from": "once@>=1.3.0 <2.0.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz" - }, - "path-is-absolute": { - "version": "1.0.1", - "from": "path-is-absolute@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz" - }, - "pinkie": { - "version": "2.0.4", - "from": "pinkie@>=2.0.0 <3.0.0", - "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", - "optional": true - }, - "pinkie-promise": { - "version": "2.0.1", - "from": "pinkie-promise@>=2.0.0 <3.0.0", - "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", - "optional": true - }, - "process-nextick-args": { - "version": "1.0.7", - "from": "process-nextick-args@>=1.0.6 <1.1.0", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz" - }, - "punycode": { - "version": "1.4.1", - "from": "punycode@>=1.4.1 <2.0.0", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "optional": true - }, - "qs": { - "version": "6.3.1", - "from": "qs@>=6.3.0 <6.4.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.3.1.tgz", - "optional": true - }, - "rc": { - "version": "1.1.7", - "from": "rc@>=1.1.6 <1.2.0", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.1.7.tgz", - "optional": true, - "dependencies": { - "minimist": { - "version": "1.2.0", - "from": "minimist@>=1.2.0 <2.0.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "optional": true - } - } - }, - "readable-stream": { - "version": "2.2.2", - "from": "readable-stream@>=2.0.0 <3.0.0||>=1.1.13 <2.0.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.2.2.tgz", - "optional": true - }, - "request": { - "version": "2.79.0", - "from": "request@>=2.79.0 <3.0.0", - "resolved": "https://registry.npmjs.org/request/-/request-2.79.0.tgz", - "optional": true - }, - "rimraf": { - "version": "2.5.4", - "from": "rimraf@>=2.5.4 <2.6.0", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.5.4.tgz" - }, - "semver": { - "version": "5.3.0", - "from": "semver@>=5.3.0 <5.4.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz", - "optional": true - }, - "set-blocking": { - "version": "2.0.0", - "from": "set-blocking@>=2.0.0 <2.1.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "optional": true - }, - "signal-exit": { - "version": "3.0.2", - "from": "signal-exit@>=3.0.0 <4.0.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", - "optional": true - }, - "sntp": { - "version": "1.0.9", - "from": "sntp@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz", - "optional": true - }, - "sshpk": { - "version": "1.10.2", - "from": "sshpk@>=1.7.0 <2.0.0", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.10.2.tgz", - "optional": true, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "from": "assert-plus@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "optional": true - } - } - }, - "string_decoder": { - "version": "0.10.31", - "from": "string_decoder@>=0.10.0 <0.11.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz" - }, - "string-width": { - "version": "1.0.2", - "from": "string-width@>=1.0.1 <2.0.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz" - }, - "stringstream": { - "version": "0.0.5", - "from": "stringstream@>=0.0.4 <0.1.0", - "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz", - "optional": true - }, - "strip-ansi": { - "version": "3.0.1", - "from": "strip-ansi@>=3.0.1 <4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz" - }, - "strip-json-comments": { - "version": "2.0.1", - "from": "strip-json-comments@>=2.0.1 <2.1.0", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "optional": true - }, - "supports-color": { - "version": "2.0.0", - "from": "supports-color@>=2.0.0 <3.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "optional": true - }, - "tar": { - "version": "2.2.1", - "from": "tar@>=2.2.1 <2.3.0", - "resolved": "https://registry.npmjs.org/tar/-/tar-2.2.1.tgz" - }, - "tar-pack": { - "version": "3.3.0", - "from": "tar-pack@>=3.3.0 <3.4.0", - "resolved": "https://registry.npmjs.org/tar-pack/-/tar-pack-3.3.0.tgz", - "optional": true, - "dependencies": { - "once": { - "version": "1.3.3", - "from": "once@>=1.3.3 <1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.3.3.tgz", - "optional": true - }, - "readable-stream": { - "version": "2.1.5", - "from": "readable-stream@>=2.1.4 <2.2.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.1.5.tgz", - "optional": true - } - } - }, - "tough-cookie": { - "version": "2.3.2", - "from": "tough-cookie@>=2.3.0 <2.4.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.2.tgz", - "optional": true - }, - "tunnel-agent": { - "version": "0.4.3", - "from": "tunnel-agent@>=0.4.1 <0.5.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.4.3.tgz", - "optional": true - }, - "tweetnacl": { - "version": "0.14.5", - "from": "tweetnacl@>=0.14.0 <0.15.0", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "optional": true - }, - "uid-number": { - "version": "0.0.6", - "from": "uid-number@>=0.0.6 <0.1.0", - "resolved": "https://registry.npmjs.org/uid-number/-/uid-number-0.0.6.tgz", - "optional": true - }, - "util-deprecate": { - "version": "1.0.2", - "from": "util-deprecate@>=1.0.1 <1.1.0", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz" - }, - "uuid": { - "version": "3.0.1", - "from": "uuid@>=3.0.0 <4.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.0.1.tgz", - "optional": true - }, - "verror": { - "version": "1.3.6", - "from": "verror@1.3.6", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.3.6.tgz", - "optional": true - }, - "wide-align": { - "version": "1.1.0", - "from": "wide-align@>=1.1.0 <2.0.0", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.0.tgz", - "optional": true - }, - "wrappy": { - "version": "1.0.2", - "from": "wrappy@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" - }, - "xtend": { - "version": "4.0.1", - "from": "xtend@>=4.0.0 <5.0.0", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", - "optional": true - } - } - }, "fstream": { "version": "1.0.11", "from": "fstream@>=1.0.2 <2.0.0", @@ -6128,6 +5434,11 @@ "from": "he@>=1.1.0 <1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz" }, + "hellojs": { + "version": "1.15.1", + "from": "hellojs@>=1.15.1 <2.0.0", + "resolved": "http://registry.npmjs.org/hellojs/-/hellojs-1.15.1.tgz" + }, "hmac-drbg": { "version": "1.0.1", "from": "hmac-drbg@>=1.0.0 <2.0.0", @@ -7717,7 +7028,7 @@ "lazy-debug-legacy": { "version": "0.0.1", "from": "lazy-debug-legacy@>=0.0.0 <0.1.0", - "resolved": "http://registry.npmjs.org/lazy-debug-legacy/-/lazy-debug-legacy-0.0.1.tgz" + "resolved": "https://registry.npmjs.org/lazy-debug-legacy/-/lazy-debug-legacy-0.0.1.tgz" }, "lazy-req": { "version": "1.1.0", diff --git a/package.json b/package.json index 2abec3f7a7..aa4203d0b5 100644 --- a/package.json +++ b/package.json @@ -69,6 +69,7 @@ "gulp-uglify": "^1.4.2", "gulp.spritesmith": "^4.1.0", "habitica-markdown": "^1.3.0", + "hellojs": "^1.15.1", "html-webpack-plugin": "^2.8.1", "image-size": "~0.3.2", "in-app-purchase": "^1.1.6", diff --git a/test/api/v3/unit/libs/cron.test.js b/test/api/v3/unit/libs/cron.test.js index 6382b85a3d..bf7017baa4 100644 --- a/test/api/v3/unit/libs/cron.test.js +++ b/test/api/v3/unit/libs/cron.test.js @@ -79,6 +79,13 @@ describe('cron', () => { expect(user.purchased.plan.gemsBought).to.equal(0); }); + it('resets plan.gemsBought on a new month if user does not have purchased.plan.dateUpdated', () => { + user.purchased.plan.gemsBought = 10; + user.purchased.plan.dateUpdated = undefined; + cron({user, tasksByType, daysMissed, analytics}); + expect(user.purchased.plan.gemsBought).to.equal(0); + }); + it('does not reset plan.gemsBought within the month', () => { let clock = sinon.useFakeTimers(moment().startOf('month').add(2, 'days').unix()); user.purchased.plan.dateUpdated = moment().startOf('month').toDate(); diff --git a/test/api/v3/unit/libs/payments/group-plans/group-payments-create.test.js b/test/api/v3/unit/libs/payments/group-plans/group-payments-create.test.js index de79e2b525..b53a619bf2 100644 --- a/test/api/v3/unit/libs/payments/group-plans/group-payments-create.test.js +++ b/test/api/v3/unit/libs/payments/group-plans/group-payments-create.test.js @@ -596,7 +596,7 @@ describe('Purchasing a group plan for group', () => { let updatedUser = await User.findById(recipient._id).exec(); - expect(updatedUser.purchased.plan.extraMonths).to.within(7, 8); + expect(updatedUser.purchased.plan.extraMonths).to.within(7, 9); }); it('adds months to members with existing recurring subscription and ignores existing negative extraMonths', async () => { diff --git a/test/client-old/spec/controllers/groupTaskActionsCtrlSpec.js b/test/client-old/spec/controllers/groupTaskActionsCtrlSpec.js index 1e2b5cc7e1..f058731b13 100644 --- a/test/client-old/spec/controllers/groupTaskActionsCtrlSpec.js +++ b/test/client-old/spec/controllers/groupTaskActionsCtrlSpec.js @@ -1,63 +1,63 @@ -describe('Group Tasks Meta Actions Controller', () => { - let rootScope, scope, user, userSerivce; - - beforeEach(() => { - module(function($provide) { - $provide.value('User', {}); - }); - - inject(($rootScope, $controller) => { - rootScope = $rootScope; - - user = specHelper.newUser(); - user._id = "unique-user-id"; - userSerivce = {user: user}; - - scope = $rootScope.$new(); - - scope.task = { - group: { - assignedUsers: [], - approval: { - required: false, - } - }, - }; - scope.task._edit = angular.copy(scope.task); - - $controller('GroupTaskActionsCtrl', {$scope: scope, User: userSerivce}); - }); - }); - - describe('toggleTaskRequiresApproval', function () { - it('toggles task approval required field from false to true', function () { - scope.toggleTaskRequiresApproval(); - expect(scope.task._edit.group.approval.required).to.be.true; - }); - - it('toggles task approval required field from true to false', function () { - scope.task._edit.group.approval.required = true; - scope.toggleTaskRequiresApproval(); - expect(scope.task._edit.group.approval.required).to.be.false; - }); - }); - - - describe('assign events', function () { - it('adds a group member to assigned users on "addedGroupMember" event ', () => { - var testId = 'test-id'; - rootScope.$broadcast('addedGroupMember', testId); - expect(scope.task.group.assignedUsers).to.contain(testId); - expect(scope.task._edit.group.assignedUsers).to.contain(testId); - }); - - it('removes a group member to assigned users on "addedGroupMember" event ', () => { - var testId = 'test-id'; - scope.task.group.assignedUsers.push(testId); - scope.task._edit.group.assignedUsers.push(testId); - rootScope.$broadcast('removedGroupMember', testId); - expect(scope.task.group.assignedUsers).to.not.contain(testId); - expect(scope.task._edit.group.assignedUsers).to.not.contain(testId); - }); - }); -}); +describe('Group Tasks Meta Actions Controller', () => { + let rootScope, scope, user, userSerivce; + + beforeEach(() => { + module(function($provide) { + $provide.value('User', {}); + }); + + inject(($rootScope, $controller) => { + rootScope = $rootScope; + + user = specHelper.newUser(); + user._id = "unique-user-id"; + userSerivce = {user: user}; + + scope = $rootScope.$new(); + + scope.task = { + group: { + assignedUsers: [], + approval: { + required: false, + } + }, + }; + scope.task._edit = angular.copy(scope.task); + + $controller('GroupTaskActionsCtrl', {$scope: scope, User: userSerivce}); + }); + }); + + describe('toggleTaskRequiresApproval', function () { + it('toggles task approval required field from false to true', function () { + scope.toggleTaskRequiresApproval(); + expect(scope.task._edit.group.approval.required).to.be.true; + }); + + it('toggles task approval required field from true to false', function () { + scope.task._edit.group.approval.required = true; + scope.toggleTaskRequiresApproval(); + expect(scope.task._edit.group.approval.required).to.be.false; + }); + }); + + + describe('assign events', function () { + it('adds a group member to assigned users on "addedGroupMember" event ', () => { + var testId = 'test-id'; + rootScope.$broadcast('addedGroupMember', testId); + expect(scope.task.group.assignedUsers).to.contain(testId); + expect(scope.task._edit.group.assignedUsers).to.contain(testId); + }); + + it('removes a group member to assigned users on "addedGroupMember" event ', () => { + var testId = 'test-id'; + scope.task.group.assignedUsers.push(testId); + scope.task._edit.group.assignedUsers.push(testId); + rootScope.$broadcast('removedGroupMember', testId); + expect(scope.task.group.assignedUsers).to.not.contain(testId); + expect(scope.task._edit.group.assignedUsers).to.not.contain(testId); + }); + }); +}); diff --git a/test/client/unit/specs/store/actions/tasks.js b/test/client/unit/specs/store/actions/tasks.js index 79683848c6..bdf9a2dc3f 100644 --- a/test/client/unit/specs/store/actions/tasks.js +++ b/test/client/unit/specs/store/actions/tasks.js @@ -9,7 +9,7 @@ describe('tasks actions', () => { }); describe('fetchUserTasks', () => { - it('fetches user tasks', async () => { + xit('fetches user tasks', async () => { expect(store.state.tasks.loadingStatus).to.equal('NOT_LOADED'); const tasks = [{_id: 1}]; sandbox.stub(axios, 'get').withArgs('/api/v3/tasks/user').returns(Promise.resolve({data: {data: tasks}})); @@ -36,7 +36,7 @@ describe('tasks actions', () => { expect(store.state.tasks.loadingStatus).to.equal('LOADED'); }); - it('can reload tasks if forceLoad is true', async () => { + xit('can reload tasks if forceLoad is true', async () => { store.state.tasks = { loadingStatus: 'LOADED', data: [{_id: 1}], diff --git a/test/client/unit/specs/store/actions/user.js b/test/client/unit/specs/store/actions/user.js index 5069e04087..38739d8c45 100644 --- a/test/client/unit/specs/store/actions/user.js +++ b/test/client/unit/specs/store/actions/user.js @@ -1,7 +1,7 @@ import axios from 'axios'; import generateStore from 'client/store'; -describe('tasks actions', () => { +describe('user actions', () => { let store; beforeEach(() => { diff --git a/website/client-old/css/header.styl b/website/client-old/css/header.styl index 475a633fac..1593de5d2c 100644 --- a/website/client-old/css/header.styl +++ b/website/client-old/css/header.styl @@ -91,8 +91,8 @@ height: 10.5em width: 100% - // Covers avatars, health bar at 1005-768. Fix: - @media (max-width: 1005px) and (min-width: 768px) + // Covers avatars, health bar at 1005-768. Fix: + @media (max-width: 1005px) and (min-width: 768px) margin-top: 2.8em; // this is a wrapper for avatars in the header diff --git a/website/client-old/js/controllers/groupPlansCtrl.js b/website/client-old/js/controllers/groupPlansCtrl.js index 6fd7d96317..487037ce87 100644 --- a/website/client-old/js/controllers/groupPlansCtrl.js +++ b/website/client-old/js/controllers/groupPlansCtrl.js @@ -1,43 +1,43 @@ -"use strict"; - -/* - A controller to manage the Group Plans page - */ - -angular.module('habitrpg') - .controller("GroupPlansCtrl", ['$scope', '$window', 'Groups', 'Payments', - function($scope, $window, Groups, Payments) { - $scope.PAGES = { - BENEFITS: 'benefits', - CREATE_GROUP: 'create-group', - UPGRADE_GROUP: 'upgrade-group', - }; - $scope.activePage = $scope.PAGES.BENEFITS; - $scope.newGroup = { - type: 'guild', - privacy: 'private', - }; - $scope.PAYMENTS = { - AMAZON: 'amazon', - STRIPE: 'stripe', - }; - - $scope.changePage = function (page) { - $scope.activePage = page; - $window.scrollTo(0, 0); - }; - - $scope.newGroupIsReady = function () { - return !!$scope.newGroup.name; - }; - - $scope.createGroup = function () { - $scope.changePage($scope.PAGES.UPGRADE_GROUP); - }; - - $scope.upgradeGroup = function (paymentType) { - var subscriptionKey = 'group_monthly'; // @TODO: Get from content API? - if (paymentType === $scope.PAYMENTS.STRIPE) Payments.showStripe({subscription: subscriptionKey, coupon: null, groupToCreate: $scope.newGroup}); - if (paymentType === $scope.PAYMENTS.AMAZON) Payments.amazonPayments.init({type: 'subscription', subscription: subscriptionKey, coupon: null, groupToCreate: $scope.newGroup}); - }; - }]); +"use strict"; + +/* + A controller to manage the Group Plans page + */ + +angular.module('habitrpg') + .controller("GroupPlansCtrl", ['$scope', '$window', 'Groups', 'Payments', + function($scope, $window, Groups, Payments) { + $scope.PAGES = { + BENEFITS: 'benefits', + CREATE_GROUP: 'create-group', + UPGRADE_GROUP: 'upgrade-group', + }; + $scope.activePage = $scope.PAGES.BENEFITS; + $scope.newGroup = { + type: 'guild', + privacy: 'private', + }; + $scope.PAYMENTS = { + AMAZON: 'amazon', + STRIPE: 'stripe', + }; + + $scope.changePage = function (page) { + $scope.activePage = page; + $window.scrollTo(0, 0); + }; + + $scope.newGroupIsReady = function () { + return !!$scope.newGroup.name; + }; + + $scope.createGroup = function () { + $scope.changePage($scope.PAGES.UPGRADE_GROUP); + }; + + $scope.upgradeGroup = function (paymentType) { + var subscriptionKey = 'group_monthly'; // @TODO: Get from content API? + if (paymentType === $scope.PAYMENTS.STRIPE) Payments.showStripe({subscription: subscriptionKey, coupon: null, groupToCreate: $scope.newGroup}); + if (paymentType === $scope.PAYMENTS.AMAZON) Payments.amazonPayments.init({type: 'subscription', subscription: subscriptionKey, coupon: null, groupToCreate: $scope.newGroup}); + }; + }]); diff --git a/website/client-old/js/services/paymentServices.js b/website/client-old/js/services/paymentServices.js index 1b657b6c63..d45d82250f 100644 --- a/website/client-old/js/services/paymentServices.js +++ b/website/client-old/js/services/paymentServices.js @@ -326,11 +326,23 @@ function($rootScope, User, $http, Content) { paymentMethod = paymentMethod.toLowerCase(); } - var cancelUrl = '/' + paymentMethod + '/subscribe/cancel?_id=' + User.user._id + '&apiToken=' + User.settings.auth.apiToken; + var queryParams = { + _id: User.user._id, + apiToken: User.settings.auth.apiToken, + noRedirect: true, + }; + if (group) { - cancelUrl += '&groupId=' + group._id; + queryParams.groupId = group._id; } - window.location.href = cancelUrl; + + var cancelUrl = '/' + paymentMethod + '/subscribe/cancel?' + $.param(queryParams); + + $http.get(cancelUrl) + .then(function (success) { + alert(window.evn.t('paypalCanceled')); + window.location.href = '/'; + }); } Payments.encodeGift = function(uuid, gift) { diff --git a/website/client/README.md b/website/client/README.md index f553bbd3a4..cfc8206c1b 100644 --- a/website/client/README.md +++ b/website/client/README.md @@ -1,20 +1,20 @@ -#Running - - Open a terminal and type `npm run client:dev` - - Open a second terminal and type `npm start` - -#Preparation Reading -- Vue 2 (https://vuejs.org) - -- Webpack (https://webpack.github.io/) is the build system and it includes plugins for code transformation, right now we have: BabelJS for ES6 transpilation, eslint for code style, less and postcss for css compilation. The code comes from https://github.com/vuejs-templates/webpack which is a Webpack template for Vue, with some small modifications to adapt it to our use case. Docs http://vuejs-templates.github.io/webpack/ - -- We’re using `.vue` files that make it possible to have HTML, JS and CSS for each component together in a single location. They’re implemented as a webpack plugin and the docs can be found here http://vue-loader.vuejs.org/en/ - -- SemanticUI is the UI framework http://semantic-ui.com/. So far I’ve only used the CSS part, it also has JS plugins but I’ve yet to use them. It supports theming so if it’s not too difficult we’ll want to customize the base theme with our own styles instead of writing CSS rules to override the original styling. - -The code is in `/website/client`. We’re using something very similar to Vuex (equivalent of React’s Redux) for state management http://vuex.vuejs.org/en/index.html - -The API is almost the same except that we don’t use mutations but only actions because it would make it difficult to work with common code - -The project is developed directly in the `develop` branch as long as we’ll be able to avoid splitting it into a different branch. - -So far most of the work has been on the template, so there’s no complex logic to understand. The only thing I would suggest you to read about is Vuex for data management: it’s basically a Flux implementation: there’s a central store that hold the data for the entire app, and every change to the data must happen through an action, the data cannot be mutated directly. +#Running + - Open a terminal and type `npm run client:dev` + - Open a second terminal and type `npm start` + +#Preparation Reading +- Vue 2 (https://vuejs.org) + +- Webpack (https://webpack.github.io/) is the build system and it includes plugins for code transformation, right now we have: BabelJS for ES6 transpilation, eslint for code style, less and postcss for css compilation. The code comes from https://github.com/vuejs-templates/webpack which is a Webpack template for Vue, with some small modifications to adapt it to our use case. Docs http://vuejs-templates.github.io/webpack/ + +- We’re using `.vue` files that make it possible to have HTML, JS and CSS for each component together in a single location. They’re implemented as a webpack plugin and the docs can be found here http://vue-loader.vuejs.org/en/ + +- SemanticUI is the UI framework http://semantic-ui.com/. So far I’ve only used the CSS part, it also has JS plugins but I’ve yet to use them. It supports theming so if it’s not too difficult we’ll want to customize the base theme with our own styles instead of writing CSS rules to override the original styling. + +The code is in `/website/client`. We’re using something very similar to Vuex (equivalent of React’s Redux) for state management http://vuex.vuejs.org/en/index.html + +The API is almost the same except that we don’t use mutations but only actions because it would make it difficult to work with common code + +The project is developed directly in the `develop` branch as long as we’ll be able to avoid splitting it into a different branch. + +So far most of the work has been on the template, so there’s no complex logic to understand. The only thing I would suggest you to read about is Vuex for data management: it’s basically a Flux implementation: there’s a central store that hold the data for the entire app, and every change to the data must happen through an action, the data cannot be mutated directly. diff --git a/website/client/app.vue b/website/client/app.vue index 7697892f88..2d0feaaf89 100644 --- a/website/client/app.vue +++ b/website/client/app.vue @@ -1,16 +1,20 @@ - - - \ No newline at end of file + diff --git a/website/client/assets/images/auth/midground_foreground_extended.png b/website/client/assets/images/auth/midground_foreground_extended.png new file mode 100644 index 0000000000..e030b8a61b Binary files /dev/null and b/website/client/assets/images/auth/midground_foreground_extended.png differ diff --git a/website/client/assets/images/auth/midground_foreground_extended2.png b/website/client/assets/images/auth/midground_foreground_extended2.png new file mode 100644 index 0000000000..5976cd9681 Binary files /dev/null and b/website/client/assets/images/auth/midground_foreground_extended2.png differ diff --git a/website/client/assets/images/auth/repeatinghill_leftslope.png b/website/client/assets/images/auth/repeatinghill_leftslope.png new file mode 100644 index 0000000000..952123e055 Binary files /dev/null and b/website/client/assets/images/auth/repeatinghill_leftslope.png differ diff --git a/website/client/assets/images/auth/repeatinghill_rightslope.png b/website/client/assets/images/auth/repeatinghill_rightslope.png new file mode 100644 index 0000000000..cf7bbecc12 Binary files /dev/null and b/website/client/assets/images/auth/repeatinghill_rightslope.png differ diff --git a/website/client/assets/images/auth/seamless_mountains_demo.png b/website/client/assets/images/auth/seamless_mountains_demo.png new file mode 100644 index 0000000000..c9c2f4e9b7 Binary files /dev/null and b/website/client/assets/images/auth/seamless_mountains_demo.png differ diff --git a/website/client/assets/images/auth/seamless_stars_varied_opacity.png b/website/client/assets/images/auth/seamless_stars_varied_opacity.png new file mode 100644 index 0000000000..0abababa1b Binary files /dev/null and b/website/client/assets/images/auth/seamless_stars_varied_opacity.png differ diff --git a/website/client/assets/scss/categories.scss b/website/client/assets/scss/categories.scss new file mode 100644 index 0000000000..7317e9ee5e --- /dev/null +++ b/website/client/assets/scss/categories.scss @@ -0,0 +1,39 @@ +.category-box { + padding: 1em; + max-width: 400px; + position: absolute; + top: -480px; + padding: 2em; + border-radius: 2px; + background-color: $white; + box-shadow: 0 2px 2px 0 rgba($black, 0.15), 0 1px 4px 0 rgba($black, 0.1); +} + +.category-label { + min-width: 100px; + border-radius: 100px; + background-color: $gray-600; + padding: .5em; + display: inline-block; + margin-right: .5em; + font-size: 12px; + font-weight: 500; + line-height: 1.33; + text-align: center; + color: $gray-300; +} + +.category-select { + border-radius: 2px; + background-color: $white; + box-shadow: 0 2px 2px 0 rgba($black, 0.16), 0 1px 4px 0 rgba($black, 0.12); + padding: 1em; +} + +.category-select:hover { + cursor: pointer; +} + +.category-wrap { + margin-top: .5em; +} diff --git a/website/client/assets/scss/dragdrop.scss b/website/client/assets/scss/dragdrop.scss new file mode 100644 index 0000000000..056e605186 --- /dev/null +++ b/website/client/assets/scss/dragdrop.scss @@ -0,0 +1,9 @@ +[draggable] { + cursor: move; /* fallback if grab cursor is unsupported */ + cursor: grab; +} + +/* (Optional) Apply a "closed-hand" cursor during drag operation. */ +[draggable]:active { + cursor: grabbing; +} diff --git a/website/client/assets/scss/icon.scss b/website/client/assets/scss/icon.scss index 0686d9e2e5..21a7e7686d 100644 --- a/website/client/assets/scss/icon.scss +++ b/website/client/assets/scss/icon.scss @@ -1,13 +1,15 @@ .svg-icon { - display: inline-block; - width: 1em; - height: 1em; + display: block; stroke-width: 0; stroke: currentColor; fill: currentColor; transition: none !important; -} -.svg-icon * { - transition: none !important; + svg { + display: block; + } + + * { + transition: none !important; + } } \ No newline at end of file diff --git a/website/client/assets/scss/index.scss b/website/client/assets/scss/index.scss index b01742f67b..acd5b2a8ec 100644 --- a/website/client/assets/scss/index.scss +++ b/website/client/assets/scss/index.scss @@ -20,4 +20,7 @@ @import './popover'; @import './item'; @import './stats'; -@import './icon'; \ No newline at end of file +@import './icon'; +@import './task'; +@import './categories'; +@import './dragdrop'; diff --git a/website/client/assets/scss/page.scss b/website/client/assets/scss/page.scss index 65ca00f521..a888e6c537 100644 --- a/website/client/assets/scss/page.scss +++ b/website/client/assets/scss/page.scss @@ -3,7 +3,7 @@ html { } html, body { - height: 100%; + height: calc(100% - 56px); // 56px is the menu background: $gray-700; } diff --git a/website/client/assets/scss/task.scss b/website/client/assets/scss/task.scss new file mode 100644 index 0000000000..805cdec48d --- /dev/null +++ b/website/client/assets/scss/task.scss @@ -0,0 +1,114 @@ + .task { + // for editing rewards or when a task is created + &-purple { + background: $purple-300; + } + + &-worst { + background: $maroon-100; + &-control-habit { + background: darken($maroon-100, 12%); + } + + &-control-daily-todo { + background: $maroon-500; + } + } + + &-worse { + background: $red-100; + &-control-habit { + background: darken($red-100, 12%); + } + + &-control-daily-todo { + background: $red-500; + } + } + + &-bad { + background: $orange-100; + &-control-habit { + background: darken($orange-100, 12%); + } + + &-control-daily-todo { + background: $orange-500; + } + } + + &-neutral { + background: $yellow-50; + &-control-habit { + background: darken($yellow-50, 12%); + } + + &-control-daily-todo { + background: $yellow-500; + } + } + + &-good { + background: $green-10; + &-control-habit { + background: darken($green-10, 12%); + } + + &-control-daily-todo { + background: $green-500; + } + } + + &-better { + background: $blue-50; + &-control-habit { + background: darken($blue-50, 12%); + } + + &-control-daily-todo { + background: $blue-500; + } + } + + &-best { + background: $teal-50; + &-control-habit { + background: darken($teal-50, 12%); + } + + &-control-daily-todo { + background: $teal-500; + } + } + + &-reward { + background: rgba($yellow-500, 0.26); + } + + &-daily-todo-disabled { + background: $gray-500; + + &-control { + background: $gray-400; + color: $gray-200; + } + } + + &-daily-todo-content-disabled { + background: $gray-600; + + * { + color: $gray-300 !important; + } + } + + &-habit-disabled { + background: $gray-700; + color: rgba(0, 0, 0, 0.12); + + &-control { + color: rgba(0, 0, 0, 0.12) !important; + border: 1px solid rgba(0, 0, 0, 0.12); + } + } +} \ No newline at end of file diff --git a/website/client/assets/svg/calendar.svg b/website/client/assets/svg/calendar.svg new file mode 100644 index 0000000000..ba08c072e8 --- /dev/null +++ b/website/client/assets/svg/calendar.svg @@ -0,0 +1,3 @@ + + + diff --git a/website/client/assets/svg/challenge.svg b/website/client/assets/svg/challenge.svg new file mode 100644 index 0000000000..1610d4bb2e --- /dev/null +++ b/website/client/assets/svg/challenge.svg @@ -0,0 +1,3 @@ + + + diff --git a/website/client/assets/svg/check.svg b/website/client/assets/svg/check.svg new file mode 100644 index 0000000000..2ae3d23949 --- /dev/null +++ b/website/client/assets/svg/check.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/website/client/assets/svg/daily.svg b/website/client/assets/svg/daily.svg new file mode 100644 index 0000000000..f1e28a1858 --- /dev/null +++ b/website/client/assets/svg/daily.svg @@ -0,0 +1,3 @@ + + + diff --git a/website/client/assets/svg/difficulty-star.svg b/website/client/assets/svg/difficulty-star.svg new file mode 100644 index 0000000000..9409886974 --- /dev/null +++ b/website/client/assets/svg/difficulty-star.svg @@ -0,0 +1,3 @@ + + + diff --git a/website/client/assets/svg/down.svg b/website/client/assets/svg/down.svg new file mode 100644 index 0000000000..4d99b85f69 --- /dev/null +++ b/website/client/assets/svg/down.svg @@ -0,0 +1,3 @@ + + + diff --git a/website/client/assets/svg/facebook.svg b/website/client/assets/svg/facebook.svg new file mode 100644 index 0000000000..314d9515ad --- /dev/null +++ b/website/client/assets/svg/facebook.svg @@ -0,0 +1,3 @@ + + + diff --git a/website/client/assets/svg/google.svg b/website/client/assets/svg/google.svg new file mode 100644 index 0000000000..6fdd374c1b --- /dev/null +++ b/website/client/assets/svg/google.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/website/client/assets/svg/gryphon.svg b/website/client/assets/svg/gryphon.svg new file mode 100644 index 0000000000..fdff439489 --- /dev/null +++ b/website/client/assets/svg/gryphon.svg @@ -0,0 +1,3 @@ + + + diff --git a/website/client/assets/svg/habit.svg b/website/client/assets/svg/habit.svg new file mode 100644 index 0000000000..3a433ff31d --- /dev/null +++ b/website/client/assets/svg/habit.svg @@ -0,0 +1,3 @@ + + + diff --git a/website/client/assets/svg/habitica-logo.svg b/website/client/assets/svg/habitica-logo.svg new file mode 100644 index 0000000000..e23bdf1981 --- /dev/null +++ b/website/client/assets/svg/habitica-logo.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/website/client/assets/svg/message.svg b/website/client/assets/svg/message.svg new file mode 100644 index 0000000000..dfd7c12a9c --- /dev/null +++ b/website/client/assets/svg/message.svg @@ -0,0 +1,3 @@ + + + diff --git a/website/client/assets/svg/negative.svg b/website/client/assets/svg/negative.svg new file mode 100644 index 0000000000..ca3e1d2e65 --- /dev/null +++ b/website/client/assets/svg/negative.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/website/client/assets/svg/next.svg b/website/client/assets/svg/next.svg new file mode 100644 index 0000000000..ce9bc639e9 --- /dev/null +++ b/website/client/assets/svg/next.svg @@ -0,0 +1,3 @@ + + + diff --git a/website/client/assets/svg/positive.svg b/website/client/assets/svg/positive.svg new file mode 100644 index 0000000000..2488512400 --- /dev/null +++ b/website/client/assets/svg/positive.svg @@ -0,0 +1,3 @@ + + + diff --git a/website/client/assets/svg/previous.svg b/website/client/assets/svg/previous.svg new file mode 100644 index 0000000000..08dc8cefa6 --- /dev/null +++ b/website/client/assets/svg/previous.svg @@ -0,0 +1,3 @@ + + + diff --git a/website/client/assets/svg/qrCode.svg b/website/client/assets/svg/qrCode.svg new file mode 100644 index 0000000000..7d1ea3e9f5 --- /dev/null +++ b/website/client/assets/svg/qrCode.svg @@ -0,0 +1,3 @@ + + + diff --git a/website/client/assets/svg/quest-background-border.svg b/website/client/assets/svg/quest-background-border.svg new file mode 100644 index 0000000000..81ae0b5bb3 --- /dev/null +++ b/website/client/assets/svg/quest-background-border.svg @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/website/client/assets/svg/quest.svg b/website/client/assets/svg/quest.svg new file mode 100644 index 0000000000..90ccf6ff77 --- /dev/null +++ b/website/client/assets/svg/quest.svg @@ -0,0 +1,3 @@ + + + diff --git a/website/client/assets/svg/remove.svg b/website/client/assets/svg/remove.svg new file mode 100644 index 0000000000..a717d49915 --- /dev/null +++ b/website/client/assets/svg/remove.svg @@ -0,0 +1,3 @@ + + + diff --git a/website/client/assets/svg/reward.svg b/website/client/assets/svg/reward.svg new file mode 100644 index 0000000000..52232a2259 --- /dev/null +++ b/website/client/assets/svg/reward.svg @@ -0,0 +1,3 @@ + + + diff --git a/website/client/assets/svg/star.svg b/website/client/assets/svg/star.svg new file mode 100644 index 0000000000..8728bfb3ca --- /dev/null +++ b/website/client/assets/svg/star.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/website/client/assets/svg/streak.svg b/website/client/assets/svg/streak.svg new file mode 100644 index 0000000000..e1314778b2 --- /dev/null +++ b/website/client/assets/svg/streak.svg @@ -0,0 +1,3 @@ + + + diff --git a/website/client/assets/svg/tags.svg b/website/client/assets/svg/tags.svg new file mode 100644 index 0000000000..c4becad0b6 --- /dev/null +++ b/website/client/assets/svg/tags.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/website/client/assets/svg/todo.svg b/website/client/assets/svg/todo.svg new file mode 100644 index 0000000000..f7a89b4be6 --- /dev/null +++ b/website/client/assets/svg/todo.svg @@ -0,0 +1,3 @@ + + + diff --git a/website/client/assets/svg/twitter.svg b/website/client/assets/svg/twitter.svg new file mode 100644 index 0000000000..3606bb754b --- /dev/null +++ b/website/client/assets/svg/twitter.svg @@ -0,0 +1,3 @@ + + + diff --git a/website/client/assets/svg/up.svg b/website/client/assets/svg/up.svg new file mode 100644 index 0000000000..047b91b7f7 --- /dev/null +++ b/website/client/assets/svg/up.svg @@ -0,0 +1,3 @@ + + + diff --git a/website/client/components/appHeader.vue b/website/client/components/appHeader.vue index cc66416dd2..352ad1086d 100644 --- a/website/client/components/appHeader.vue +++ b/website/client/components/appHeader.vue @@ -1,7 +1,7 @@ diff --git a/website/client/components/auth/registerLogin.vue b/website/client/components/auth/registerLogin.vue new file mode 100644 index 0000000000..7be5b3e007 --- /dev/null +++ b/website/client/components/auth/registerLogin.vue @@ -0,0 +1,230 @@ + + + + + diff --git a/website/client/components/guilds/createPartyModal.vue b/website/client/components/guilds/createPartyModal.vue new file mode 100644 index 0000000000..0e06046833 --- /dev/null +++ b/website/client/components/guilds/createPartyModal.vue @@ -0,0 +1,121 @@ + + + + + diff --git a/website/client/components/guilds/groupFormModal.vue b/website/client/components/guilds/groupFormModal.vue index 48f4871f36..d023aaa917 100644 --- a/website/client/components/guilds/groupFormModal.vue +++ b/website/client/components/guilds/groupFormModal.vue @@ -1,10 +1,10 @@ diff --git a/website/client/components/guilds/membersModal.vue b/website/client/components/guilds/membersModal.vue index 106a5698f6..274d84cdf7 100644 --- a/website/client/components/guilds/membersModal.vue +++ b/website/client/components/guilds/membersModal.vue @@ -1,6 +1,9 @@ - diff --git a/website/client/components/guilds/publicGuildItem.vue b/website/client/components/guilds/publicGuildItem.vue index 815fe2948f..40cf650d99 100644 --- a/website/client/components/guilds/publicGuildItem.vue +++ b/website/client/components/guilds/publicGuildItem.vue @@ -35,20 +35,6 @@ box-shadow: 0 2px 2px 0 rgba($black, 0.15), 0 1px 4px 0 rgba($black, 0.1); margin-bottom: 1rem; - .category-label { - min-width: 100px; - border-radius: 100px; - background-color: $gray-600; - padding: .5em; - display: inline-block; - margin-right: .5em; - font-size: 12px; - font-weight: 500; - line-height: 1.33; - text-align: center; - color: $gray-300; - } - .recommend-text { font-size: 12px; font-style: italic; diff --git a/website/client/components/guilds/startQuestModal.vue b/website/client/components/guilds/startQuestModal.vue new file mode 100644 index 0000000000..14834a3b32 --- /dev/null +++ b/website/client/components/guilds/startQuestModal.vue @@ -0,0 +1,156 @@ + + + + + diff --git a/website/client/components/inventory/drawer.vue b/website/client/components/inventory/drawer.vue index 7625ecd2e6..d82b1be2dc 100644 --- a/website/client/components/inventory/drawer.vue +++ b/website/client/components/inventory/drawer.vue @@ -91,8 +91,7 @@ } .drawer-slider { - padding: 12px 0 0 24px; - margin-left: -24px; + padding: 12px 0 0 0; overflow-x: auto; overflow-y: hidden; white-space: nowrap; diff --git a/website/client/components/inventory/item.vue b/website/client/components/inventory/item.vue index c20d0def70..971e382e8f 100644 --- a/website/client/components/inventory/item.vue +++ b/website/client/components/inventory/item.vue @@ -16,13 +16,16 @@ b-popover( .item-wrapper .item slot(name="itemBadge", :item="item") - span.item-content(:class="itemContentClass") + span.item-content( + :class="itemContentClass", + :draggable="draggable", + @dragstart="onDrag" + ) span.item-label(v-if="label") {{ label }} diff --git a/website/client/components/inventory/stable.vue b/website/client/components/inventory/stable.vue deleted file mode 100644 index a7e6fd673f..0000000000 --- a/website/client/components/inventory/stable.vue +++ /dev/null @@ -1,131 +0,0 @@ - - - - - diff --git a/website/client/components/inventory/stable/countBadge.vue b/website/client/components/inventory/stable/countBadge.vue new file mode 100644 index 0000000000..4f42bf7097 --- /dev/null +++ b/website/client/components/inventory/stable/countBadge.vue @@ -0,0 +1,33 @@ + + + + + diff --git a/website/client/components/inventory/stable/drawerSlider.vue b/website/client/components/inventory/stable/drawerSlider.vue new file mode 100644 index 0000000000..d54a27014d --- /dev/null +++ b/website/client/components/inventory/stable/drawerSlider.vue @@ -0,0 +1,174 @@ + + + + + diff --git a/website/client/components/inventory/stable/foodItem.vue b/website/client/components/inventory/stable/foodItem.vue new file mode 100644 index 0000000000..7861844f2a --- /dev/null +++ b/website/client/components/inventory/stable/foodItem.vue @@ -0,0 +1,48 @@ + + + diff --git a/website/client/components/inventory/stable/index.vue b/website/client/components/inventory/stable/index.vue new file mode 100644 index 0000000000..fc9743d064 --- /dev/null +++ b/website/client/components/inventory/stable/index.vue @@ -0,0 +1,721 @@ + + + + + diff --git a/website/client/components/inventory/stable/petItem.vue b/website/client/components/inventory/stable/petItem.vue new file mode 100644 index 0000000000..9d071bf959 --- /dev/null +++ b/website/client/components/inventory/stable/petItem.vue @@ -0,0 +1,117 @@ + + + + + diff --git a/website/client/components/static/home.vue b/website/client/components/static/home.vue new file mode 100644 index 0000000000..c6e85f8eb9 --- /dev/null +++ b/website/client/components/static/home.vue @@ -0,0 +1,30 @@ + + + + diff --git a/website/client/components/task.vue b/website/client/components/task.vue deleted file mode 100644 index 7256b4ba03..0000000000 --- a/website/client/components/task.vue +++ /dev/null @@ -1,33 +0,0 @@ - - - \ No newline at end of file diff --git a/website/client/components/tasks/column.vue b/website/client/components/tasks/column.vue new file mode 100644 index 0000000000..e26edd8455 --- /dev/null +++ b/website/client/components/tasks/column.vue @@ -0,0 +1,194 @@ + + + + + diff --git a/website/client/components/tasks/task.vue b/website/client/components/tasks/task.vue new file mode 100644 index 0000000000..f0348269c7 --- /dev/null +++ b/website/client/components/tasks/task.vue @@ -0,0 +1,293 @@ + + + + + + + \ No newline at end of file diff --git a/website/client/components/tasks/user.vue b/website/client/components/tasks/user.vue new file mode 100644 index 0000000000..41535f70a2 --- /dev/null +++ b/website/client/components/tasks/user.vue @@ -0,0 +1,56 @@ + + + + + diff --git a/website/client/components/ui/toggleSwitch.vue b/website/client/components/ui/toggleSwitch.vue index d1eda4074b..779f296aa3 100644 --- a/website/client/components/ui/toggleSwitch.vue +++ b/website/client/components/ui/toggleSwitch.vue @@ -4,8 +4,9 @@ .toggle-switch.float-left input.toggle-switch-checkbox( type='checkbox', :id="id", - @change="$emit('change', $event.target.checked)", - :checked="checked", + @change="handleChange", + :checked="isChecked", + :value="value", ) label.toggle-switch-label(:for="id") span.toggle-switch-inner @@ -13,88 +14,88 @@ diff --git a/website/client/components/userTasks.vue b/website/client/components/userTasks.vue deleted file mode 100644 index 3a46db218e..0000000000 --- a/website/client/components/userTasks.vue +++ /dev/null @@ -1,163 +0,0 @@ - - - diff --git a/website/client/directives/directive.common.js b/website/client/directives/directive.common.js new file mode 100644 index 0000000000..151bc76870 --- /dev/null +++ b/website/client/directives/directive.common.js @@ -0,0 +1,8 @@ +// https://stackoverflow.com/a/40720172/1298154 +export const emit = (vnode, emitName, data) => { + let handlers = vnode.data && vnode.data.on || vnode.componentOptions && vnode.componentOptions.listeners; + + if (handlers && handlers[emitName]) { + handlers[emitName].fns(data); + } +}; diff --git a/website/client/directives/dragdrop.directive.js b/website/client/directives/dragdrop.directive.js new file mode 100644 index 0000000000..8d5ed6029d --- /dev/null +++ b/website/client/directives/dragdrop.directive.js @@ -0,0 +1,63 @@ +import {emit} from './directive.common'; + +import _keys from 'lodash/keys'; +import _without from 'lodash/without'; + +/** + * DRAG_GROUP is a static custom value + * KEY_OF_ITEM + * + * v-drag.DRAG_GROUP="KEY_OF_ITEM" + * v-drag.drop.DRAG_GROUP="KEY_OF_ITEM" @dropped="callback" @dragover="optional" + */ + +const DROPPED_EVENT_NAME = 'dropped'; +const DRAGOVER_EVENT_NAME = 'dragover'; + +export default { + bind (el, binding, vnode) { + el.isDropHandler = binding.modifiers.drop === true; + el.dragGroup = _without(_keys(binding.modifiers), 'drop')[0]; + el.key = binding.value; + + if (!el.isDropHandler) { + el.draggable = true; + el.handleDrag = (ev) => { + ev.dataTransfer.setData('KEY', binding.value); + }; + el.addEventListener('dragstart', el.handleDrag); + } else { + el.handleDragOver = (ev) => { + let dragOverEventData = { + dropable: true, + draggingKey: ev.dataTransfer.getData('KEY'), + }; + + emit(vnode, DRAGOVER_EVENT_NAME, dragOverEventData); + + if (dragOverEventData.dropable) { + ev.preventDefault(); + } + }; + el.handleDrop = (ev) => { + let dropEventData = { + draggingKey: ev.dataTransfer.getData('KEY'), + }; + + emit(vnode, DROPPED_EVENT_NAME, dropEventData); + }; + + el.addEventListener('dragover', el.handleDragOver); + el.addEventListener('drop', el.handleDrop); + } + }, + + unbind (el) { + if (!el.isDropHandler) { + el.removeEventListener('drag', el.handleDrag); + } else { + el.removeEventListener('dragover', el.handleDragOver); + el.removeEventListener('drop', el.handleDrop); + } + }, +}; diff --git a/website/client/directives/resize.directive.js b/website/client/directives/resize.directive.js new file mode 100644 index 0000000000..2cd14deb27 --- /dev/null +++ b/website/client/directives/resize.directive.js @@ -0,0 +1,31 @@ +import Vue from 'vue'; + +import _throttle from 'lodash/throttle'; + +import { emit } from './directive.common'; + +/** + * v-resize="throttleTimeout", @resized="callback()" + */ + +const EVENT_NAME = 'resized'; + +export default { + bind (el, binding, vnode) { + el.handleWindowResize = _throttle(() => { + emit(vnode, EVENT_NAME, { + width: el.clientWidth, + height: el.clientHeight, + }); + }, binding.value); + + window.addEventListener('resize', el.handleWindowResize); + + // send the first width + Vue.nextTick(el.handleWindowResize); + }, + + unbind (el) { + window.removeEventListener('resize', el.handleWindowResize); + }, +}; diff --git a/website/client/index.html b/website/client/index.html index 6eac2d4fda..204f884234 100644 --- a/website/client/index.html +++ b/website/client/index.html @@ -4,14 +4,9 @@ Habitica - - + - -
-

Loading...

-
diff --git a/website/client/libs/asyncResource.js b/website/client/libs/asyncResource.js index 7d2ba8e394..852cac06e5 100644 --- a/website/client/libs/asyncResource.js +++ b/website/client/libs/asyncResource.js @@ -36,8 +36,11 @@ export function loadAsyncResource ({store, path, url, deserialize, forceLoad = f } else if (loadingStatus === 'NOT_LOADED' || loadingStatus === 'LOADED' && forceLoad) { return axios.get(url).then(response => { // TODO support more params resource.loadingStatus = 'LOADED'; - resource.data = deserialize(response); - return resource; + // deserialize can be a promise + return Promise.resolve(deserialize(response)).then(deserializedData => { + resource.data = deserializedData; + return resource; + }); }); } else { return Promise.reject(new Error(`Invalid loading status "${loadingStatus} for resource at "${path}".`)); diff --git a/website/client/main.js b/website/client/main.js index 77a2f4f8f1..4ec81e1669 100644 --- a/website/client/main.js +++ b/website/client/main.js @@ -3,10 +3,9 @@ require('babel-polyfill'); import Vue from 'vue'; -import axios from 'axios'; import AppComponent from './app'; import router from './router'; -import generateStore from './store'; +import getStore from './store'; import StoreModule from './libs/store'; import './filters/registerGlobals'; import i18n from './libs/i18n'; @@ -26,43 +25,9 @@ Vue.config.productionTip = IS_PRODUCTION; Vue.use(i18n); Vue.use(StoreModule); -// TODO just until we have proper authentication -let authSettings = localStorage.getItem('habit-mobile-settings'); - -if (authSettings) { - authSettings = JSON.parse(authSettings); - axios.defaults.headers.common['x-api-user'] = authSettings.auth.apiId; - axios.defaults.headers.common['x-api-key'] = authSettings.auth.apiToken; -} - export default new Vue({ + el: '#app', router, - store: generateStore(), + store: getStore(), render: h => h(AppComponent), - beforeCreate () { - // Setup listener for title - this.$store.watch(state => state.title, (title) => { - document.title = title; - }); - - // Mount the app when user and tasks are loaded - const userDataWatcher = this.$store.watch(state => [state.user.data, state.tasks.data], ([user, tasks]) => { - if (user && user._id && Array.isArray(tasks)) { - userDataWatcher(); // remove the watcher - this.$mount('#app'); - } - }); - - // Load the user and the user tasks - Promise.all([ - this.$store.dispatch('user:fetch'), - this.$store.dispatch('tasks:fetchUserTasks'), - ]).catch((err) => { - console.error('Impossible to fetch user. Copy into localStorage a valid habit-mobile-settings object.', err); // eslint-disable-line no-console - }); - }, - mounted () { // Remove the loading screen when the app is mounted - let loadingScreen = document.getElementById('loading-screen'); - if (loadingScreen) document.body.removeChild(loadingScreen); - }, -}); \ No newline at end of file +}); diff --git a/website/client/readme.md b/website/client/readme.md index f553bbd3a4..cfc8206c1b 100644 --- a/website/client/readme.md +++ b/website/client/readme.md @@ -1,20 +1,20 @@ -#Running - - Open a terminal and type `npm run client:dev` - - Open a second terminal and type `npm start` - -#Preparation Reading -- Vue 2 (https://vuejs.org) - -- Webpack (https://webpack.github.io/) is the build system and it includes plugins for code transformation, right now we have: BabelJS for ES6 transpilation, eslint for code style, less and postcss for css compilation. The code comes from https://github.com/vuejs-templates/webpack which is a Webpack template for Vue, with some small modifications to adapt it to our use case. Docs http://vuejs-templates.github.io/webpack/ - -- We’re using `.vue` files that make it possible to have HTML, JS and CSS for each component together in a single location. They’re implemented as a webpack plugin and the docs can be found here http://vue-loader.vuejs.org/en/ - -- SemanticUI is the UI framework http://semantic-ui.com/. So far I’ve only used the CSS part, it also has JS plugins but I’ve yet to use them. It supports theming so if it’s not too difficult we’ll want to customize the base theme with our own styles instead of writing CSS rules to override the original styling. - -The code is in `/website/client`. We’re using something very similar to Vuex (equivalent of React’s Redux) for state management http://vuex.vuejs.org/en/index.html - -The API is almost the same except that we don’t use mutations but only actions because it would make it difficult to work with common code - -The project is developed directly in the `develop` branch as long as we’ll be able to avoid splitting it into a different branch. - -So far most of the work has been on the template, so there’s no complex logic to understand. The only thing I would suggest you to read about is Vuex for data management: it’s basically a Flux implementation: there’s a central store that hold the data for the entire app, and every change to the data must happen through an action, the data cannot be mutated directly. +#Running + - Open a terminal and type `npm run client:dev` + - Open a second terminal and type `npm start` + +#Preparation Reading +- Vue 2 (https://vuejs.org) + +- Webpack (https://webpack.github.io/) is the build system and it includes plugins for code transformation, right now we have: BabelJS for ES6 transpilation, eslint for code style, less and postcss for css compilation. The code comes from https://github.com/vuejs-templates/webpack which is a Webpack template for Vue, with some small modifications to adapt it to our use case. Docs http://vuejs-templates.github.io/webpack/ + +- We’re using `.vue` files that make it possible to have HTML, JS and CSS for each component together in a single location. They’re implemented as a webpack plugin and the docs can be found here http://vue-loader.vuejs.org/en/ + +- SemanticUI is the UI framework http://semantic-ui.com/. So far I’ve only used the CSS part, it also has JS plugins but I’ve yet to use them. It supports theming so if it’s not too difficult we’ll want to customize the base theme with our own styles instead of writing CSS rules to override the original styling. + +The code is in `/website/client`. We’re using something very similar to Vuex (equivalent of React’s Redux) for state management http://vuex.vuejs.org/en/index.html + +The API is almost the same except that we don’t use mutations but only actions because it would make it difficult to work with common code + +The project is developed directly in the `develop` branch as long as we’ll be able to avoid splitting it into a different branch. + +So far most of the work has been on the template, so there’s no complex logic to understand. The only thing I would suggest you to read about is Vuex for data management: it’s basically a Flux implementation: there’s a central store that hold the data for the entire app, and every change to the data must happen through an action, the data cannot be mutated directly. diff --git a/website/client/router.js b/website/client/router.js index a835643494..aa97d8b968 100644 --- a/website/client/router.js +++ b/website/client/router.js @@ -1,5 +1,6 @@ import Vue from 'vue'; import VueRouter from 'vue-router'; +import getStore from 'client/store'; import EmptyView from './components/emptyView'; @@ -7,19 +8,23 @@ import EmptyView from './components/emptyView'; import ParentPage from './components/parentPage'; import Page from './components/page'; -// Tasks -import UserTasks from './components/userTasks'; +// Static Pages +const Home = () => import(/* webpackChunkName: "static" */'./components/static/home'); +const RegisterLogin = () => import(/* webpackChunkName: "auth" */'./components/auth/registerLogin'); -// Except for tasks that are always loaded all the other main level +// All the main level // components are loaded in separate webpack chunks. // See https://webpack.js.org/guides/code-splitting-async/ // for docs +// Tasks +const UserTasks = () => import(/* webpackChunkName: "userTasks" */'./components/tasks/user'); + // Inventory const InventoryContainer = () => import(/* webpackChunkName: "inventory" */'./components/inventory/index'); const ItemsPage = () => import(/* webpackChunkName: "inventory" */'./components/inventory/items/index'); const EquipmentPage = () => import(/* webpackChunkName: "inventory" */'./components/inventory/equipment/index'); -const StablePage = () => import(/* webpackChunkName: "inventory" */'./components/inventory/stable'); +const StablePage = () => import(/* webpackChunkName: "inventory" */'./components/inventory/stable/index'); // Social const InboxPage = () => import(/* webpackChunkName: "inbox" */ './components/social/inbox/index'); @@ -27,14 +32,14 @@ const InboxConversationPage = () => import(/* webpackChunkName: "inbox" */ './co // Guilds const GuildIndex = () => import(/* webpackChunkName: "guilds" */ './components/guilds/index'); -const TavernPage = () => import(/* webpackChunkName: "guilds" */ './components/guilds/tavern'); +// const TavernPage = () => import(/* webpackChunkName: "guilds" */ './components/guilds/tavern'); const MyGuilds = () => import(/* webpackChunkName: "guilds" */ './components/guilds/myGuilds'); const GuildsDiscoveryPage = () => import(/* webpackChunkName: "guilds" */ './components/guilds/discovery'); const GuildPage = () => import(/* webpackChunkName: "guilds" */ './components/guilds/guild'); Vue.use(VueRouter); -export default new VueRouter({ +const router = new VueRouter({ mode: 'history', base: process.env.NODE_ENV === 'production' ? '/new-app' : __dirname, // eslint-disable-line no-process-env linkActiveClass: 'active', @@ -43,7 +48,11 @@ export default new VueRouter({ scrollBehavior () { return { x: 0, y: 0 }; }, + // requiresLogin is true by default, isStatic false routes: [ + { name: 'home', path: '/home', component: Home, meta: {requiresLogin: false} }, + { name: 'register', path: '/register', component: RegisterLogin, meta: {requiresLogin: false} }, + { name: 'login', path: '/login', component: RegisterLogin, meta: {requiresLogin: false} }, { name: 'tasks', path: '/', component: UserTasks }, { path: '/inventory', @@ -55,11 +64,12 @@ export default new VueRouter({ ], }, { name: 'shops', path: '/shops', component: Page }, + { name: 'party', path: '/party', component: GuildPage }, { path: '/guilds', component: GuildIndex, children: [ - { name: 'tavern', path: 'tavern', component: TavernPage }, + { name: 'tavern', path: 'tavern', component: GuildPage }, { name: 'myGuilds', path: 'myGuilds', @@ -79,7 +89,6 @@ export default new VueRouter({ ], }, { name: 'challenges', path: 'challenges', component: Page }, - { name: 'party', path: 'party', component: Page }, { path: '/user', component: ParentPage, @@ -108,3 +117,22 @@ export default new VueRouter({ }, ], }); + +const store = getStore(); + +router.beforeEach(function routerGuard (to, from, next) { + const isUserLoggedIn = store.state.isUserLoggedIn; + const routeRequiresLogin = to.meta.requiresLogin !== false; + + if (!isUserLoggedIn && routeRequiresLogin) { + // Redirect to the login page unless the user is trying to reach the + // root of the website, in which case show the home page. + // TODO when redirecting to login if user login then redirect back to initial page + // so if you tried to go to /party you'll be redirected to /party after login/signup + return next({name: to.path === '/' ? 'home' : 'login'}); + } + + next(); +}); + +export default router; diff --git a/website/client/store/actions/auth.js b/website/client/store/actions/auth.js new file mode 100644 index 0000000000..31a05020de --- /dev/null +++ b/website/client/store/actions/auth.js @@ -0,0 +1,83 @@ +import axios from 'axios'; + +export async function register (store, params) { + let url = '/api/v3/user/auth/local/register'; + let result = await axios.post(url, { + username: params.username, + email: params.email, + password: params.password, + confirmPassword: params.passwordConfirm, + }); + + let user = result.data.data; + + let userLocalData = JSON.stringify({ + auth: { + apiId: user._id, + apiToken: user.apiToken, + }, + }); + localStorage.setItem('habit-mobile-settings', userLocalData); + + // @TODO: I think we just need analytics here + // Auth.runAuth(res.data._id, res.data.apiToken); + // Analytics.register(); + // $scope.registrationInProgress = false; + // Alert.authErrorAlert(data, status, headers, config) + // Analytics.login(); + // Analytics.updateUser(); + + store.state.user.data = user; +} + +export async function login (store, params) { + let url = '/api/v3/user/auth/local/login'; + let result = await axios.post(url, { + username: params.username, + // email: params.email, + password: params.password, + }); + + let user = result.data.data; + + let userLocalData = JSON.stringify({ + auth: { + apiId: user.id, + apiToken: user.apiToken, + }, + }); + + localStorage.setItem('habit-mobile-settings', userLocalData); + + // @TODO: I think we just need analytics here + // Auth.runAuth(res.data._id, res.data.apiToken); + // Analytics.register(); + // $scope.registrationInProgress = false; + // Alert.authErrorAlert(data, status, headers, config) + // Analytics.login(); + // Analytics.updateUser(); + + // @TODO: Update the api to return the user? + // store.state.user.data = user; +} + +export async function socialAuth (store, params) { + let url = '/api/v3/user/auth/social'; + let result = await axios.post(url, { + network: params.auth.network, + authResponse: params.auth.authResponse, + }); + + // @TODO: Analytics + + let user = result.data.data; + + let userLocalData = JSON.stringify({ + auth: { + apiId: user.id, + apiToken: user.apiToken, + }, + }); + + localStorage.setItem('habit-mobile-settings', userLocalData); +} diff --git a/website/client/store/actions/common.js b/website/client/store/actions/common.js index 41cc3271d0..f9adb779bd 100644 --- a/website/client/store/actions/common.js +++ b/website/client/store/actions/common.js @@ -1,5 +1,7 @@ import axios from 'axios'; import equipOp from 'common/script/ops/equip'; +import hatchOp from 'common/script/ops/hatch'; +import feedOp from 'common/script/ops/feed'; export function equip (store, params) { const user = store.state.user.data; @@ -9,4 +11,24 @@ export function equip (store, params) { // TODO // .then((res) => console.log('equip', res)) // .catch((err) => console.error('equip', err)); -} \ No newline at end of file +} + +export function hatch (store, params) { + const user = store.state.user.data; + hatchOp(user, {params}); + axios + .post(`/api/v3/user/hatch/${params.egg}/${params.hatchingPotion}`); + // TODO + // .then((res) => console.log('equip', res)) + // .catch((err) => console.error('equip', err)); +} + +export function feed (store, params) { + const user = store.state.user.data; + feedOp(user, {params}); + axios + .post(`/api/v3/user/feed/${params.pet}/${params.food}`); + // TODO + // .then((res) => console.log('equip', res)) + // .catch((err) => console.error('equip', err)); +} diff --git a/website/client/store/actions/index.js b/website/client/store/actions/index.js index 965e9041a3..b47a7540e6 100644 --- a/website/client/store/actions/index.js +++ b/website/client/store/actions/index.js @@ -6,6 +6,8 @@ import * as tasks from './tasks'; import * as guilds from './guilds'; import * as party from './party'; import * as members from './members'; +import * as auth from './auth'; +import * as quests from './quests'; // Actions should be named as 'actionName' and can be accessed as 'namespace:actionName' // Example: fetch in user.js -> 'user:fetch' @@ -17,6 +19,8 @@ const actions = flattenAndNamespace({ guilds, party, members, + auth, + quests, }); export default actions; diff --git a/website/client/store/actions/quests.js b/website/client/store/actions/quests.js new file mode 100644 index 0000000000..5ad1281e09 --- /dev/null +++ b/website/client/store/actions/quests.js @@ -0,0 +1,16 @@ +import axios from 'axios'; +// export async function initQuest (store) { +// } + +export async function sendAction (store, payload) { + // Analytics.updateUser({ + // partyID: party._id, + // partySize: party.memberCount + // }); + let response = await axios.post(`/api/v3/groups/${payload.groupId}/${payload.action}`); + + // @TODO: Update user? + // User.sync(); + + return response.data.quest || response.data.data; +} diff --git a/website/client/store/actions/tasks.js b/website/client/store/actions/tasks.js index d939810e3e..71ceca0221 100644 --- a/website/client/store/actions/tasks.js +++ b/website/client/store/actions/tasks.js @@ -1,13 +1,54 @@ import { loadAsyncResource } from 'client/libs/asyncResource'; +import compact from 'lodash/compact'; + export function fetchUserTasks (store, forceLoad = false) { return loadAsyncResource({ store, path: 'tasks', url: '/api/v3/tasks/user', deserialize (response) { - return response.data.data; + // Wait for the user to be loaded before deserializing + // because user.tasksOrder is necessary + return store.dispatch('user:fetch').then((userResource) => { + return store.dispatch('tasks:order', [response.data.data, userResource.data.tasksOrder]); + }); }, forceLoad, }); +} + +export function order (store, [rawTasks, tasksOrder]) { + const tasks = { + habits: [], + dailys: [], + todos: [], + rewards: [], + }; + + rawTasks.forEach(task => { + tasks[`${task.type}s`].push(task); + }); + + Object.keys(tasks).forEach((type) => { + let tasksOfType = tasks[type]; + + const orderOfType = tasksOrder[type]; + const orderedTasks = new Array(tasksOfType.length); + const unorderedTasks = []; // what we want to add later + + tasksOfType.forEach((task, index) => { + const taskId = task._id; + const i = orderOfType[index] === taskId ? index : orderOfType.indexOf(taskId); + if (i === -1) { + unorderedTasks.push(task); + } else { + orderedTasks[i] = task; + } + }); + + tasks[type] = compact(orderedTasks).concat(unorderedTasks); + }); + + return tasks; } \ No newline at end of file diff --git a/website/client/store/getters/tasks.js b/website/client/store/getters/tasks.js index 6e641c0708..72c9e7c419 100644 --- a/website/client/store/getters/tasks.js +++ b/website/client/store/getters/tasks.js @@ -1,6 +1,64 @@ +import { shouldDo } from 'common/script/cron'; + // Return all the tags belonging to an user task export function getTagsFor (store) { return (task) => store.state.user.data.tags .filter(tag => task.tags.indexOf(tag.id) !== -1) .map(tag => tag.name); +} + +function getTaskColorByValue (value) { + if (value < -20) { + return 'task-worst'; + } else if (value < -10) { + return 'task-worse'; + } else if (value < -1) { + return 'task-bad'; + } else if (value < 1) { + return 'task-neutral'; + } else if (value < 5) { + return 'task-good'; + } else if (value < 10) { + return 'task-better'; + } else { + return 'task-best'; + } +} + +export function getTaskClasses (store) { + const userPreferences = store.state.user.data.preferences; + + // Purpose is one of 'controls', 'editModal', 'createModal', 'content' + return (task, purpose) => { + const type = task.type; + + switch (purpose) { + case 'createModal': + return 'task-purple'; + case 'editModal': + return type === 'reward' ? 'task-purple' : getTaskColorByValue(task.value); + case 'control': + switch (type) { + case 'daily': + if (task.completed || !shouldDo(new Date(), task, userPreferences)) return 'task-daily-todo-disabled'; + return getTaskColorByValue(task.value); + case 'todo': + if (task.completed) return 'task-daily-todo-disabled'; + return getTaskColorByValue(task.value); + case 'habit': + return { + up: task.up ? getTaskColorByValue(task.value) : 'task-habit-disabled', + down: task.down ? getTaskColorByValue(task.value) : 'task-habit-disabled', + }; + case 'reward': + return 'task-reward'; + } + break; + case 'content': + if (type === 'daily' && (task.completed || !task.isDue) || type === 'todo' && task.completed) { + return 'task-daily-todo-content-disabled'; + } + break; + } + }; } \ No newline at end of file diff --git a/website/client/store/index.js b/website/client/store/index.js index bb5597514f..4643f22600 100644 --- a/website/client/store/index.js +++ b/website/client/store/index.js @@ -3,21 +3,44 @@ import deepFreeze from 'client/libs/deepFreeze'; import content from 'common/script/content/index'; import * as constants from 'common/script/constants'; import { asyncResourceFactory } from 'client/libs/asyncResource'; +import axios from 'axios'; import actions from './actions'; import getters from './getters'; +const IS_TEST = process.env.NODE_ENV === 'test'; // eslint-disable-line no-process-env + +// Load user auth parameters and determine if it's logged in +// before trying to load data +let isUserLoggedIn = false; + +let AUTH_SETTINGS = localStorage.getItem('habit-mobile-settings'); + +if (AUTH_SETTINGS) { + AUTH_SETTINGS = JSON.parse(AUTH_SETTINGS); + axios.defaults.headers.common['x-api-user'] = AUTH_SETTINGS.auth.apiId; + axios.defaults.headers.common['x-api-key'] = AUTH_SETTINGS.auth.apiToken; + isUserLoggedIn = true; +} + // Export a function that generates the store and not the store directly -// so that we can regenerate it multiple times for testing +// so that we can regenerate it multiple times for testing, when not testing +// always export the same route + +let existingStore; export default function () { - return new Store({ + if (!IS_TEST && existingStore) return existingStore; + + existingStore = new Store({ actions, getters, state: { title: 'Habitica', + isUserLoggedIn, user: asyncResourceFactory(), tasks: asyncResourceFactory(), // user tasks party: { + quest: {}, members: asyncResourceFactory(), }, myGuilds: [], @@ -29,4 +52,6 @@ export default function () { constants: deepFreeze(constants), }, }); + + return existingStore; } diff --git a/website/common/locales/en/groups.json b/website/common/locales/en/groups.json index 012bc259dc..c4ae284d37 100644 --- a/website/common/locales/en/groups.json +++ b/website/common/locales/en/groups.json @@ -114,7 +114,7 @@ "messageRequired": "A message is required.", "toUserIDRequired": "A User ID is required", "gemAmountRequired": "A number of gems is required", - "notAuthorizedToSendMessageToThisUser": "Can't send message to this user.", + "notAuthorizedToSendMessageToThisUser": "You can't send a message to this player because they have chosen to block messages.", "privateMessageGiftGemsMessage": "Hello <%= receiverName %>, <%= senderName %> has sent you <%= gemAmount %> gems!", "privateMessageGiftSubscriptionMessage": "<%= numberOfMonths %> months of subscription! ", "cannotSendGemsToYourself": "Cannot send gems to yourself. Try a subscription instead.", diff --git a/website/common/locales/en/newClient.json b/website/common/locales/en/newClient.json index 6db683c726..35fe7fbb17 100644 --- a/website/common/locales/en/newClient.json +++ b/website/common/locales/en/newClient.json @@ -10,6 +10,16 @@ "guildBank": "Guild Bank", "chatPlaceHolder": "Type your message to Guild members here", "today": "Today", + "theseAreYourTasks": "These are your <%= taskType %>", + "habitsDesc": "Habits don't have a rigid schedule. You can check them off multiple times per day.", + "dailysDesc": "Dailies repeat on a regular basis. Choose the schedule that works best for you!", + "todosDesc": "To-Dos need to be completed once. Add checklists to your To-Dos to increase their value.", + "rewardsDesc": "Rewards are a great way to use Habitica and complete your tasks. Try adding a few today!", + "dueIn": "Due <%= dueIn %>", + "complete2": "Complete", + "custom": "Custom", + "wishlist": "Wishlist", + "scheduled": "Scheduled", "like": "Like", "copyAsTodo": "Copy as To-Do", "report": "Report", @@ -25,6 +35,14 @@ "groupBy2": "Group By", "quantity": "Quantity", "AZ": "A-Z", + "costumeDisabled": "You have disabled your costume.", + "filterByStandard": "Standard", + "filterByMagicPotion": "Magin Potion", + "filterByQuest": "Quest", + "standard": "Standard", + "sortByColor": "Color", + "sortByHatchable": "Hatchable", + "haveHatchablePet": "You have a <%= potion %> and <%= egg %> to hatch this pet! Click and hold the paw print to hatch.", "editAvatar": "Edit Avatar", "sort": "Sort", "memberCount": "Member Count", @@ -53,8 +71,8 @@ "silverTier": "Silver Tier", "bronzeTier": "Bronze Tier", "privacySettings": "Privacy Settings", - "onlyLeaderCreatesChallenges": "Only the Guild Leader can create Guild Challenges", - "guildLeaderCantBeMessaged": "Guild Leader can not be messaged directly", + "onlyLeaderCreatesChallenges": "Only the Leader can create Challenges", + "guildLeaderCantBeMessaged": "Leader can not be messaged directly", "privateGuild": "Private Guild", "allowGuildInvationsFromNonMembers": "Allow Guild invitations from non-members", "charactersRemaining": "characters remaining", @@ -69,5 +87,45 @@ "sendMessage": "Send Message", "removeManager2": "Remove Manager", "promoteToLeader": "Promote to Leader", - "inviteFriendsParty": "Inviting friends to your party will grant you an exclusive
Quest Scroll to battle the Basi-List together!" + "inviteFriendsParty": "Inviting friends to your party will grant you an exclusive
Quest Scroll to battle the Basi-List together!", + "upgradeParty": "Upgrade Party", + "questDetailsTitle": "Quest Details", + "yourNotOnQuest": "You're not on a quest", + "questDescription": "Quests allow players to focus on long-term, in-game goals with the members of their party.", + "haveNoChallenges": "You don’t have any Challenges", + "challengeDescription": "Challenges are community events in which players compete and earn prizes by completing a group of related tasks.", + "createParty": "Create a Party", + "partyDescriptionPlaceHolder": "This is our party’s description. It describes what we do in this party. If you want to learn more about what we do in this party, read the description. Party on.", + "inviteMembersNow": "Would you like to invite users now?", + "playInPartyTitle": "Play Habitica in a Party!", + "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", + "startYourOwnPartyDescription": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam accumsan sagittis tellus tempor euismod. Sed imperdiet facilisis tortor in malesuada.", + "shartUserId": "Share User ID", + "wantToJoinPartyTitle": "Want to join a Party?", + "wantToJoinPartyDescription": "Aenean non mattis eros, quis semper ipsum. Phasellus vulputate in nibh et suscipit. In hac habitasse platea dictumst.", + "copy": "Copy", + "lookingForGroup": "Looking for Group", + "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.", + "questOwnerRewards": "Quest Owner Rewards", + "guildInformationPlaceHolder": "This is our party’s description. It describes what we do in this party. If you want to learn more about what we do in this party, read the description. Party on.", + "updateParty": "Update Party", + "upgrade": "Upgrade", + "signUpWithSocial": "Sign up with <%= social %>", + "loginWithSocial": "Login with <%= social %>", + "confirmPassword": "Confirm Password", + "usernamePlaceholder": "e.g., HabitRabbit", + "emailPlaceholder": "e.g., rabbit@habitica.com", + "passwordPlaceholder": "e.g., •••••••••••• ", + "confirmPasswordPlaceholder": "Make sure it’s the same password!", + "termsAndAgreement": "By clicking the button below, you are indicating that you have read and agree to the Terms of Service and Privacy Policy.", + "joinHabitica": "Join Habitica", + "showAllAnimals": "Show All <%= color %> <%= type %>", + "showLessAnimals": "Show Less <%= color %> <%= type %>", + "quickInventory": "Quick Inventory", + "noFoodAvailable": "You don't have any food.", + "gotIt": "Got it!", + "welcomeStable": "Welcome to the Stable!", + "welcomeStableText": "I'm Matt, the Beast Master. Starting at level 3, you can hatch Pets from Eggs by using Potions you find! When you hatch a Pet from your Inventory, it will appear here! Click a Pet's to add it to your avatar. Feed them with the Food you find in your Inventory after level 3, and they'll grow into hardy Mounts." } diff --git a/website/common/locales/en/questsContent.json b/website/common/locales/en/questsContent.json index c41d73d95b..601efa0f42 100644 --- a/website/common/locales/en/questsContent.json +++ b/website/common/locales/en/questsContent.json @@ -109,7 +109,7 @@ "questGoldenknight1DropGoldenknight2Quest": "The Golden Knight Part 2: Gold Knight (Scroll)", "questGoldenknight2Text": "The Golden Knight, Part 2: Gold Knight", - "questGoldenknight2Notes": "Armed with hundreds of Habitican's testimonies, you finally confront the Golden Knight. You begin to recite the Habitcan's complaints to her, one by one. \"And @Pfeffernusse says that your constant bragging-\" The knight raises her hand to silence you and scoffs, \"Please, these people are merely jealous of my success. Instead of complaining, they should simply work as hard as I! Perhaps I shall show you the power you can attain through diligence such as mine!\" She raises her morningstar and prepares to attack you!", + "questGoldenknight2Notes": "Armed with dozens of Habiticans' testimonies, you finally confront the Golden Knight. You begin to recite the Habitcans' complaints to her, one by one. \"And @Pfeffernusse says that your constant bragging-\" The knight raises her hand to silence you and scoffs, \"Please, these people are merely jealous of my success. Instead of complaining, they should simply work as hard as I! Perhaps I shall show you the power you can attain through diligence such as mine!\" She raises her morningstar and prepares to attack you!", "questGoldenknight2Boss": "Gold Knight", "questGoldenknight2DropGoldenknight3Quest": "The Golden Knight Part 3: The Iron Knight (Scroll)", diff --git a/website/common/locales/en/rebirth.json b/website/common/locales/en/rebirth.json index 4e49970b89..dc3e25eebf 100644 --- a/website/common/locales/en/rebirth.json +++ b/website/common/locales/en/rebirth.json @@ -11,7 +11,7 @@ "rebirthInList1": "Tasks, history, equipment, and settings remain.", "rebirthInList2": "Challenge, Guild, and Party memberships remain.", "rebirthInList3": "Gems, backer tiers, and contributor levels remain.", - "rebirthInList4": "Items obtained from Gems or drops (such as pets and mounts) remain, though you cannot access them until you unlock them again.", + "rebirthInList4": "Items obtained from Gems or drops (such as pets and mounts) remain.", "rebirthEarnAchievement": "You also earn an Achievement for beginning a new adventure!", "beReborn": "Be Reborn", "rebirthAchievement": "You've begun a new adventure! This is Rebirth <%= number %> for you, and the highest Level you've attained is <%= level %>. To stack this Achievement, begin your next new adventure when you've reached an even higher Level!", diff --git a/website/common/locales/en/subscriber.json b/website/common/locales/en/subscriber.json index f1866c206a..4cd3e59bd9 100644 --- a/website/common/locales/en/subscriber.json +++ b/website/common/locales/en/subscriber.json @@ -37,7 +37,7 @@ "subscribed": "Subscribed", "manageSub": "Click to manage subscription", "cancelSub": "Cancel Subscription", - "cancelSubInfoGoogle": "Please go to the \"My apps & games\" > \"Subscriptions\" section of the Google Play Store app to cancel your subscription or to see your subscription's termination date if you have already cancelled it. This screen is not able to show you whether your subscription has been cancelled.", + "cancelSubInfoGoogle": "Please go to the \"Account\" > \"Subscriptions\" section of the Google Play Store app to cancel your subscription or to see your subscription's termination date if you have already cancelled it. This screen is not able to show you whether your subscription has been cancelled.", "cancelSubInfoApple": "Please follow Apple’s official instructions to cancel your subscription or to see your subscription's termination date if you have already cancelled it. This screen is not able to show you whether your subscription has been cancelled.", "canceledSubscription": "Canceled Subscription", "cancelingSubscription": "Canceling the subscription", @@ -171,5 +171,6 @@ "missingPaymentId": "Missing req.query.paymentId", "missingCustomerId": "Missing req.query.customerId", "missingPaypalBlock": "Missing req.session.paypalBlock", - "missingSubKey": "Missing req.query.sub" + "missingSubKey": "Missing req.query.sub", + "paypalCanceled": "Your subscription has been canceled" } diff --git a/website/common/locales/en/tasks.json b/website/common/locales/en/tasks.json index e157f4f200..3ceb4c227f 100644 --- a/website/common/locales/en/tasks.json +++ b/website/common/locales/en/tasks.json @@ -1,7 +1,7 @@ { "clearCompleted": "Delete Completed", "lotOfToDos": "Your most recent 30 completed To-Dos are shown here. You can see older completed To-Dos from Data > Data Display Tool or Data > Export Data > User Data.", - "deleteToDosExplanation": "If you click the button below, all of your completed To-Dos and archived To-Dos will be permanently deleted. Export them first if you want to keep a record of them.", + "deleteToDosExplanation": "If you click the button below, all of your completed To-Dos and archived To-Dos will be permanently deleted, except for To-Dos from active challenges and Group Plans. Export them first if you want to keep a record of them.", "addmultiple": "Add Multiple", "addsingle": "Add Single", "habit": "Habit", diff --git a/website/server/controllers/api-v3/user.js b/website/server/controllers/api-v3/user.js index 25cd50f8ca..d57e32b354 100644 --- a/website/server/controllers/api-v3/user.js +++ b/website/server/controllers/api-v3/user.js @@ -329,7 +329,7 @@ api.deleteUser = { await user.remove(); if (feedback) { - txnEmail(TECH_ASSISTANCE_EMAIL, 'admin-feedback', [ + txnEmail({email: TECH_ASSISTANCE_EMAIL}, 'admin-feedback', [ {name: 'PROFILE_NAME', content: user.profile.name}, {name: 'UUID', content: user._id}, {name: 'EMAIL', content: getUserInfo(user, ['email']).email}, diff --git a/website/server/libs/cron.js b/website/server/libs/cron.js index 0c51587c54..0bc1cd94cc 100644 --- a/website/server/libs/cron.js +++ b/website/server/libs/cron.js @@ -206,8 +206,11 @@ export function cron (options = {}) { let perfect = true; // Reset Gold-to-Gems cap if it's the start of the month - if (user.purchased && user.purchased.plan && !moment(user.purchased.plan.dateUpdated).startOf('month').isSame(moment().startOf('month'))) { + let dateUpdatedFalse = !moment(user.purchased.plan.dateUpdated).startOf('month').isSame(moment().startOf('month')) || !user.purchased.plan.dateUpdated; + + if (user.purchased && user.purchased.plan && dateUpdatedFalse) { user.purchased.plan.gemsBought = 0; + if (!user.purchased.plan.dateUpdated) user.purchased.plan.dateUpdated = moment(); } if (user.isSubscribed()) { diff --git a/website/server/libs/payments.js b/website/server/libs/payments.js index c87d8f225f..2b664c649e 100644 --- a/website/server/libs/payments.js +++ b/website/server/libs/payments.js @@ -134,7 +134,7 @@ api.addSubToGroupUser = async function addSubToGroupUser (member, group) { let ignoreCustomerId = customerIdsToIgnore.indexOf(memberPlan.customerId) !== -1; if (ignorePaymentPlan) { - txnEmail(TECH_ASSISTANCE_EMAIL, 'admin-user-subscription-details', [ + txnEmail({email: TECH_ASSISTANCE_EMAIL}, 'admin-user-subscription-details', [ {name: 'PROFILE_NAME', content: member.profile.name}, {name: 'UUID', content: member._id}, {name: 'EMAIL', content: getUserInfo(member, ['email']).email}, diff --git a/website/views/options/profile/achievements.jade b/website/views/options/profile/achievements.jade index 0392405013..1abb279148 100644 --- a/website/views/options/profile/achievements.jade +++ b/website/views/options/profile/achievements.jade @@ -1,5 +1,5 @@ -script(id='partials/options.profile.achievements.html', type='text/ng-template') - .container-fluid - div(class='row') - div(ng-class='user.flags.classSelected && !user.preferences.disableClasses ? "col-md-4" : "col-md-6"') - include ../../shared/profiles/achievements +script(id='partials/options.profile.achievements.html', type='text/ng-template') + .container-fluid + div(class='row') + div(ng-class='user.flags.classSelected && !user.preferences.disableClasses ? "col-md-4" : "col-md-6"') + include ../../shared/profiles/achievements diff --git a/website/views/options/profile/avatar.jade b/website/views/options/profile/avatar.jade index e0213ef5b1..d97088da3f 100644 --- a/website/views/options/profile/avatar.jade +++ b/website/views/options/profile/avatar.jade @@ -1,2 +1,2 @@ -script(id='partials/options.profile.avatar.html', type='text/ng-template') - +customizeProfile() +script(id='partials/options.profile.avatar.html', type='text/ng-template') + +customizeProfile() diff --git a/website/views/options/settings/export.jade b/website/views/options/settings/export.jade index 6b80129f22..dbcea5a094 100644 --- a/website/views/options/settings/export.jade +++ b/website/views/options/settings/export.jade @@ -1,13 +1,13 @@ -script(id='partials/options.settings.export.html', type="text/ng-template") - .container-fluid - .row - .col-md-6 - h2=env.t('dataExport') - small=env.t('saveData') - h4=env.t('habitHistory') - =env.t('exportHistory') - a(href="/export/history.csv")= ' ' + env.t('csv') - h4=env.t('userData') - =env.t('exportUserData') - a(href="/export/userdata.xml")= ' ' + env.t('xml') + ' ' - a(href="/export/userdata.json")= env.t('json') +script(id='partials/options.settings.export.html', type="text/ng-template") + .container-fluid + .row + .col-md-6 + h2=env.t('dataExport') + small=env.t('saveData') + h4=env.t('habitHistory') + =env.t('exportHistory') + a(href="/export/history.csv")= ' ' + env.t('csv') + h4=env.t('userData') + =env.t('exportUserData') + a(href="/export/userdata.xml")= ' ' + env.t('xml') + ' ' + a(href="/export/userdata.json")= env.t('json') diff --git a/website/views/options/settings/notification.jade b/website/views/options/settings/notification.jade index 487df82af7..a1f8cbfb25 100644 --- a/website/views/options/settings/notification.jade +++ b/website/views/options/settings/notification.jade @@ -1,49 +1,49 @@ -script(id='partials/options.settings.notifications.html', type="text/ng-template") - .container-fluid - .row - .personal-options.col-md-6 - .panel.panel-default - .panel-heading - =env.t('notifications') - .panel-body - table.table - tr - td - th - span=env.t("email") - th - span=env.t("push") - -var unsubscribeFromAllEmails = 'user.preferences.emailNotifications.unsubscribeFromAll' - -var unsubscribeFromAllPush = 'user.preferences.pushNotifications.unsubscribeFromAll' - each notification in ['newPM', 'wonChallenge', 'giftedGems', 'giftedSubscription', 'invitedParty', 'invitedGuild', 'kickedGroup', 'questStarted', 'invitedQuest', 'importantAnnouncements', 'weeklyRecaps', 'onboarding'] - tr - td - span=env.t(notification) - td - -var preference = 'user.preferences.emailNotifications.' + notification - input(type='checkbox', ng-model='#{preference}', - ng-disabled='#{unsubscribeFromAllEmails} === true || #{preference} === undefined', - ng-checked='#{unsubscribeFromAllEmails} === false && #{preference} === true', - ng-change='set({"preferences.emailNotifications.#{notification}": #{preference} ? true: false})') - td - -var preference = 'user.preferences.pushNotifications.' + notification - input(type='checkbox', ng-model='#{preference}', - ng-disabled='#{unsubscribeFromAllPush} === true || #{preference} === undefined', - ng-checked='#{unsubscribeFromAllPush} === false && #{preference} === true', - ng-change='set({"preferences.pushNotifications.#{notification}": #{preference} ? true: false})') - - hr - - .checkbox - label - input(type='checkbox', ng-model='user.preferences.pushNotifications.unsubscribeFromAll', - ng-change='set({"preferences.pushNotifications.unsubscribeFromAll": user.preferences.pushNotifications.unsubscribeFromAll ? true: false})') - span=env.t('unsubscribeAllPush') - - .checkbox - label - input(type='checkbox', ng-model='user.preferences.emailNotifications.unsubscribeFromAll', - ng-change='set({"preferences.emailNotifications.unsubscribeFromAll": user.preferences.emailNotifications.unsubscribeFromAll ? true: false})') - span=env.t('unsubscribeAllEmails') - - small=env.t('unsubscribeAllEmailsText') +script(id='partials/options.settings.notifications.html', type="text/ng-template") + .container-fluid + .row + .personal-options.col-md-6 + .panel.panel-default + .panel-heading + =env.t('notifications') + .panel-body + table.table + tr + td + th + span=env.t("email") + th + span=env.t("push") + -var unsubscribeFromAllEmails = 'user.preferences.emailNotifications.unsubscribeFromAll' + -var unsubscribeFromAllPush = 'user.preferences.pushNotifications.unsubscribeFromAll' + each notification in ['newPM', 'wonChallenge', 'giftedGems', 'giftedSubscription', 'invitedParty', 'invitedGuild', 'kickedGroup', 'questStarted', 'invitedQuest', 'importantAnnouncements', 'weeklyRecaps', 'onboarding'] + tr + td + span=env.t(notification) + td + -var preference = 'user.preferences.emailNotifications.' + notification + input(type='checkbox', ng-model='#{preference}', + ng-disabled='#{unsubscribeFromAllEmails} === true || #{preference} === undefined', + ng-checked='#{unsubscribeFromAllEmails} === false && #{preference} === true', + ng-change='set({"preferences.emailNotifications.#{notification}": #{preference} ? true: false})') + td + -var preference = 'user.preferences.pushNotifications.' + notification + input(type='checkbox', ng-model='#{preference}', + ng-disabled='#{unsubscribeFromAllPush} === true || #{preference} === undefined', + ng-checked='#{unsubscribeFromAllPush} === false && #{preference} === true', + ng-change='set({"preferences.pushNotifications.#{notification}": #{preference} ? true: false})') + + hr + + .checkbox + label + input(type='checkbox', ng-model='user.preferences.pushNotifications.unsubscribeFromAll', + ng-change='set({"preferences.pushNotifications.unsubscribeFromAll": user.preferences.pushNotifications.unsubscribeFromAll ? true: false})') + span=env.t('unsubscribeAllPush') + + .checkbox + label + input(type='checkbox', ng-model='user.preferences.emailNotifications.unsubscribeFromAll', + ng-change='set({"preferences.emailNotifications.unsubscribeFromAll": user.preferences.emailNotifications.unsubscribeFromAll ? true: false})') + span=env.t('unsubscribeAllEmails') + + small=env.t('unsubscribeAllEmailsText') diff --git a/website/views/options/settings/promo.jade b/website/views/options/settings/promo.jade index 509f31b713..956d762b4e 100644 --- a/website/views/options/settings/promo.jade +++ b/website/views/options/settings/promo.jade @@ -1,21 +1,21 @@ -script(type='text/ng-template', id='partials/options.settings.promo.html') - .container-fluid - .row - .col-md-6 - h2=env.t('promoCode') - form.form-inline(role='form',ng-submit='enterCoupon(_couponCode)') - input.form-control(type='text', ng-model='_couponCode', placeholder=env.t('promoPlaceholder')) - button.btn.btn-primary(type='submit')= env.t('submit') - div - small= env.t('couponText') - div(ng-if='user.contributor.sudo') - hr - h4=env.t('generateCodes') - form.form(role='form',ng-submit='generateCodes(_codes)',ng-init='_codes={}') - .form-group - input.form-control(type='text',ng-model='_codes.event',placeholder="Event code (eg, 'wondercon')") - .form-group - input.form-control(type='number',ng-model='_codes.count',placeholder="Number of codes to generate (eg, 250)") - .form-group - button.btn.btn-primary(type='submit')=env.t('generate') - a.btn.btn-default(href='/api/v3/coupons?_id={{user._id}}&apiToken={{User.settings.auth.apiToken}}')=env.t('getCodes') +script(type='text/ng-template', id='partials/options.settings.promo.html') + .container-fluid + .row + .col-md-6 + h2=env.t('promoCode') + form.form-inline(role='form',ng-submit='enterCoupon(_couponCode)') + input.form-control(type='text', ng-model='_couponCode', placeholder=env.t('promoPlaceholder')) + button.btn.btn-primary(type='submit')= env.t('submit') + div + small= env.t('couponText') + div(ng-if='user.contributor.sudo') + hr + h4=env.t('generateCodes') + form.form(role='form',ng-submit='generateCodes(_codes)',ng-init='_codes={}') + .form-group + input.form-control(type='text',ng-model='_codes.event',placeholder="Event code (eg, 'wondercon')") + .form-group + input.form-control(type='number',ng-model='_codes.count',placeholder="Number of codes to generate (eg, 250)") + .form-group + button.btn.btn-primary(type='submit')=env.t('generate') + a.btn.btn-default(href='/api/v3/coupons?_id={{user._id}}&apiToken={{User.settings.auth.apiToken}}')=env.t('getCodes') diff --git a/website/views/options/settings/settings.jade b/website/views/options/settings/settings.jade index 903869d4fc..1f5a1b7e81 100644 --- a/website/views/options/settings/settings.jade +++ b/website/views/options/settings/settings.jade @@ -1,186 +1,186 @@ -script(type='text/ng-template', id='partials/options.settings.settings.html') - .container-fluid - .row - .personal-options.col-md-6 - .panel.panel-default - .panel-heading - =env.t('settings') - .panel-body - - .form-horizontal - h5=env.t('language') - select.form-control(ng-model='language.code', ng-options='lang.code as lang.name for lang in availableLanguages', ng-change='changeLanguage()') - small - !=env.t('americanEnglishGovern') - br - strong - !=env.t('helpWithTranslation') - - hr - - .form-horizontal - h5=env.t('dateFormat') - select.form-control(ng-model='user.preferences.dateFormat', ng-options='DF for DF in availableFormats', ng-change='set({"preferences.dateFormat": user.preferences.dateFormat})') - - hr - - .checkbox - label - input(type='checkbox', ng-click='hideHeader() ', ng-checked='user.preferences.hideHeader!==true') - span.hint(popover-trigger='mouseenter', popover-placement='right', popover=env.t('showHeaderPop'))=env.t('showHeader') - .checkbox - label - input(type='checkbox', ng-click='toggleStickyHeader()', ng-checked='user.preferences.stickyHeader!==false', ng-disabled="user.preferences.hideHeader!==false") - span.hint(popover-trigger='mouseenter', popover-placement='right', popover=env.t('stickyHeaderPop'))=env.t('stickyHeader') - .checkbox - label - input(type='checkbox', ng-model='user.preferences.newTaskEdit', ng-change='set({"preferences.newTaskEdit": user.preferences.newTaskEdit?true: false})') - span.hint(popover-trigger='mouseenter', popover-placement='right', popover=env.t('newTaskEditPop'))=env.t('newTaskEdit') - .checkbox - label - input(type='checkbox', ng-model='user.preferences.tagsCollapsed', ng-change='set({"preferences.tagsCollapsed": user.preferences.tagsCollapsed?true: false})') - span.hint(popover-trigger='mouseenter', popover-placement='right', popover=env.t('startCollapsedPop'))=env.t('startCollapsed') - .checkbox - label - input(type='checkbox', ng-model='user.preferences.advancedCollapsed', ng-change='set({"preferences.advancedCollapsed": user.preferences.advancedCollapsed?true: false})') - span.hint(popover-trigger='mouseenter', popover-placement='right', popover=env.t('startAdvCollapsedPop'))=env.t('startAdvCollapsed') - .checkbox - label - input(type='checkbox', ng-model='user.preferences.dailyDueDefaultView', ng-change='set({"preferences.dailyDueDefaultView": user.preferences.dailyDueDefaultView?true: false})') - span.hint(popover-trigger='mouseenter', popover-placement='right', popover=env.t('dailyDueDefaultViewPop'))=env.t('dailyDueDefaultView') - .checkbox(ng-if='party.memberCount === 1') - label - input(type='checkbox', ng-model='user.preferences.displayInviteToPartyWhenPartyIs1', ng-change='set({"preferences.displayInviteToPartyWhenPartyIs1": user.preferences.displayInviteToPartyWhenPartyIs1 ? true : false})') - span.hint(popover-trigger='mouseenter', popover-placement='right', popover=env.t('displayInviteToPartyWhenPartyIs1'))=env.t('displayInviteToPartyWhenPartyIs1') - .checkbox - label=env.t('suppressLevelUpModal') - input(type='checkbox', ng-model='user.preferences.suppressModals.levelUp', ng-change='set({"preferences.suppressModals.levelUp": user.preferences.suppressModals.levelUp?true: false})') - .checkbox - label=env.t('suppressHatchPetModal') - input(type='checkbox', ng-model='user.preferences.suppressModals.hatchPet', ng-change='set({"preferences.suppressModals.hatchPet": user.preferences.suppressModals.hatchPet?true: false})') - .checkbox - label=env.t('suppressRaisePetModal') - input(type='checkbox', ng-model='user.preferences.suppressModals.raisePet', ng-change='set({"preferences.suppressModals.raisePet": user.preferences.suppressModals.raisePet?true: false})') - .checkbox - label=env.t('suppressStreakModal') - input(type='checkbox', ng-model='user.preferences.suppressModals.streak', ng-change='set({"preferences.suppressModals.streak": user.preferences.suppressModals.streak?true: false})') - //- .checkbox - //- label=env.t('confirmScoreNotes') - //- input(type='checkbox', ng-model='user.preferences.tasks.confirmScoreNotes', ng-change='set({"preferences.tasks.confirmScoreNotes": user.preferences.tasks.confirmScoreNotes ? true: false})') - - //- .checkbox - //- label=env.t('groupTasksByChallenge') - //- input(type='checkbox', ng-model='user.preferences.tasks.groupByChallenge', ng-change='set({"preferences.tasks.groupByChallenge": user.preferences.tasks.groupByChallenge ? true: false})') - - hr - - button.btn.btn-default(ng-click='showBailey()', popover-trigger='mouseenter', popover-placement='right', popover=env.t('showBaileyPop'))= env.t('showBailey') - button.btn.btn-default(ng-click='openRestoreModal()', popover-trigger='mouseenter', popover-placement='right', popover=env.t('fixValPop'))= env.t('fixVal') - button.btn.btn-default(ng-if='user.preferences.disableClasses==true', ng-click='User.changeClass({})', popover-trigger='mouseenter', popover-placement='right', popover=env.t('enableClassPop'))= env.t('enableClass') - - hr - - h5=env.t('customDayStart') - alert.alert-warning=env.t('customDayStartInfo1') - - .form-horizontal - .form-group - .col-sm-7 - select.form-control(ng-model='dayStart') - - var number = 0 - while number < 24 - - var value = number - - var meridian = number < 12 ? 'AM' : 'PM' - - var hour = number++ % 12 - option(value=value) #{hour ? hour : 12}:00 #{meridian} - - .col-sm-5 - br.visible-xs - button.btn.btn-block.btn-primary(ng-click='openDayStartModal(dayStart)', - ng-disabled='dayStart == user.preferences.dayStart') - =env.t('saveCustomDayStart') - - hr - - h5=env.t('timezone') - .form-horizontal - .form-group - .col-sm-12 - p!=env.t('timezoneUTC', {utc: "{{ user.preferences.timezoneOffset | timezoneOffsetToUtc }}"}) - br - p!=env.t('timezoneInfo') - - .personal-options.col-md-6 - .panel.panel-default - .panel-heading - span=env.t('registration') - .panel-body - div - ul.list-inline - li(ng-repeat='network in SOCIAL_AUTH_NETWORKS') - button.btn.btn-primary(ng-if='!user.auth[network.key].id', ng-click='socialLogin(network.key, user)')=env.t('registerWithSocial', {network: '{{network.name}}'}) - button.btn.btn-primary(disabled='disabled', ng-if='!hasBackupAuthOption(user, network.key) && user.auth[network.key].id')=env.t('registeredWithSocial', {network: '{{network.name}}'}) - button.btn.btn-danger(ng-click='deleteSocialAuth(network.key)', ng-if='hasBackupAuthOption(user, network.key) && user.auth[network.key].id')=env.t('detachSocial', {network: '{{network.name}}'}) - hr - div(ng-if='!user.auth.local.username') - p=env.t('addLocalAuth') - form(ng-submit='http("post", "/api/v3/user/auth/local/register", localAuth, "addedLocalAuth")', ng-init='localAuth={}', name='localAuth', novalidate) - //-.alert.alert-danger(ng-messages='changeUsername.$error && changeUsername.submitted')=env.t('fillAll') - .form-group - input.form-control(type='text', placeholder=env.t('username'), ng-model='localAuth.username', required) - .form-group - input.form-control(type='text', placeholder=env.t('email'), ng-model='localAuth.email', required) - .form-group - input.form-control(type='password', placeholder=env.t('password'), ng-model='localAuth.password', required) - .form-group - input.form-control(type='password', placeholder=env.t('confirmPass'), ng-model='localAuth.confirmPassword', required) - input.btn.btn-default(type='submit', ng-disabled='localAuth.$invalid', value=env.t('submit')) - - div(ng-if='user.auth.local.username') - p=env.t('username') - |: {{user.auth.local.username}} - p - small.muted - =env.t('loginNameDescription1') - |  - a(href='/#/options/profile/profile')=env.t('loginNameDescription2') - |  - =env.t('loginNameDescription3') - p=env.t('email') - |: {{user.auth.local.email}} - hr - - h5=env.t('changeUsername') - form(ng-submit='changeUser("username", usernameUpdates)', ng-init='usernameUpdates={}', ng-show='user.auth.local', name='changeUsername', novalidate) - //-.alert.alert-danger(ng-messages='changeUsername.$error && changeUsername.submitted')=env.t('fillAll') - .form-group - input.form-control(type='text', placeholder=env.t('newUsername'), ng-model='usernameUpdates.username', required) - .form-group - input.form-control(type='password', placeholder=env.t('password'), ng-model='usernameUpdates.password', required) - input.btn.btn-default(type='submit', ng-disabled='changeUsername.$invalid', value=env.t('submit')) - - h5=env.t('changeEmail') - form(ng-submit='changeUser("email", emailUpdates)', ng-show='user.auth.local', name='changeEmail', novalidate) - .form-group - input.form-control(type='text', placeholder=env.t('newEmail'), ng-model='emailUpdates.newEmail', required) - .form-group - input.form-control(type='password', placeholder=env.t('password'), ng-model='emailUpdates.password', required) - input.btn.btn-default(type='submit', ng-disabled='changeEmail.$invalid', value=env.t('submit')) - - h5=env.t('changePass') - form(ng-submit='changeUser("password", passwordUpdates)', ng-show='user.auth.local', name='changePassword', novalidate) - .form-group - input.form-control(type='password', placeholder=env.t('oldPass'), ng-model='passwordUpdates.password', required) - .form-group - input.form-control(type='password', placeholder=env.t('newPass'), ng-model='passwordUpdates.newPassword', required) - .form-group - input.form-control(type='password', placeholder=env.t('confirmPass'), ng-model='passwordUpdates.confirmPassword', required) - input.btn.btn-default(type='submit', ng-disabled='changePassword.$invalid', value=env.t('submit')) - - - .panel.panel-default - .panel-heading - span=env.t('dangerZone') - .panel-body - a.btn.btn-danger(ng-click='openModal("reset", {controller:"SettingsCtrl"})', popover-trigger='mouseenter', popover-placement='right', popover=env.t('resetAccPop'))= env.t('resetAccount') - a.btn.btn-danger(ng-click='openModal("delete", {controller:"SettingsCtrl"})', popover-trigger='mouseenter', popover=env.t('deleteAccPop'))= env.t('deleteAccount') +script(type='text/ng-template', id='partials/options.settings.settings.html') + .container-fluid + .row + .personal-options.col-md-6 + .panel.panel-default + .panel-heading + =env.t('settings') + .panel-body + + .form-horizontal + h5=env.t('language') + select.form-control(ng-model='language.code', ng-options='lang.code as lang.name for lang in availableLanguages', ng-change='changeLanguage()') + small + !=env.t('americanEnglishGovern') + br + strong + !=env.t('helpWithTranslation') + + hr + + .form-horizontal + h5=env.t('dateFormat') + select.form-control(ng-model='user.preferences.dateFormat', ng-options='DF for DF in availableFormats', ng-change='set({"preferences.dateFormat": user.preferences.dateFormat})') + + hr + + .checkbox + label + input(type='checkbox', ng-click='hideHeader() ', ng-checked='user.preferences.hideHeader!==true') + span.hint(popover-trigger='mouseenter', popover-placement='right', popover=env.t('showHeaderPop'))=env.t('showHeader') + .checkbox + label + input(type='checkbox', ng-click='toggleStickyHeader()', ng-checked='user.preferences.stickyHeader!==false', ng-disabled="user.preferences.hideHeader!==false") + span.hint(popover-trigger='mouseenter', popover-placement='right', popover=env.t('stickyHeaderPop'))=env.t('stickyHeader') + .checkbox + label + input(type='checkbox', ng-model='user.preferences.newTaskEdit', ng-change='set({"preferences.newTaskEdit": user.preferences.newTaskEdit?true: false})') + span.hint(popover-trigger='mouseenter', popover-placement='right', popover=env.t('newTaskEditPop'))=env.t('newTaskEdit') + .checkbox + label + input(type='checkbox', ng-model='user.preferences.tagsCollapsed', ng-change='set({"preferences.tagsCollapsed": user.preferences.tagsCollapsed?true: false})') + span.hint(popover-trigger='mouseenter', popover-placement='right', popover=env.t('startCollapsedPop'))=env.t('startCollapsed') + .checkbox + label + input(type='checkbox', ng-model='user.preferences.advancedCollapsed', ng-change='set({"preferences.advancedCollapsed": user.preferences.advancedCollapsed?true: false})') + span.hint(popover-trigger='mouseenter', popover-placement='right', popover=env.t('startAdvCollapsedPop'))=env.t('startAdvCollapsed') + .checkbox + label + input(type='checkbox', ng-model='user.preferences.dailyDueDefaultView', ng-change='set({"preferences.dailyDueDefaultView": user.preferences.dailyDueDefaultView?true: false})') + span.hint(popover-trigger='mouseenter', popover-placement='right', popover=env.t('dailyDueDefaultViewPop'))=env.t('dailyDueDefaultView') + .checkbox(ng-if='party.memberCount === 1') + label + input(type='checkbox', ng-model='user.preferences.displayInviteToPartyWhenPartyIs1', ng-change='set({"preferences.displayInviteToPartyWhenPartyIs1": user.preferences.displayInviteToPartyWhenPartyIs1 ? true : false})') + span.hint(popover-trigger='mouseenter', popover-placement='right', popover=env.t('displayInviteToPartyWhenPartyIs1'))=env.t('displayInviteToPartyWhenPartyIs1') + .checkbox + label=env.t('suppressLevelUpModal') + input(type='checkbox', ng-model='user.preferences.suppressModals.levelUp', ng-change='set({"preferences.suppressModals.levelUp": user.preferences.suppressModals.levelUp?true: false})') + .checkbox + label=env.t('suppressHatchPetModal') + input(type='checkbox', ng-model='user.preferences.suppressModals.hatchPet', ng-change='set({"preferences.suppressModals.hatchPet": user.preferences.suppressModals.hatchPet?true: false})') + .checkbox + label=env.t('suppressRaisePetModal') + input(type='checkbox', ng-model='user.preferences.suppressModals.raisePet', ng-change='set({"preferences.suppressModals.raisePet": user.preferences.suppressModals.raisePet?true: false})') + .checkbox + label=env.t('suppressStreakModal') + input(type='checkbox', ng-model='user.preferences.suppressModals.streak', ng-change='set({"preferences.suppressModals.streak": user.preferences.suppressModals.streak?true: false})') + //- .checkbox + //- label=env.t('confirmScoreNotes') + //- input(type='checkbox', ng-model='user.preferences.tasks.confirmScoreNotes', ng-change='set({"preferences.tasks.confirmScoreNotes": user.preferences.tasks.confirmScoreNotes ? true: false})') + + //- .checkbox + //- label=env.t('groupTasksByChallenge') + //- input(type='checkbox', ng-model='user.preferences.tasks.groupByChallenge', ng-change='set({"preferences.tasks.groupByChallenge": user.preferences.tasks.groupByChallenge ? true: false})') + + hr + + button.btn.btn-default(ng-click='showBailey()', popover-trigger='mouseenter', popover-placement='right', popover=env.t('showBaileyPop'))= env.t('showBailey') + button.btn.btn-default(ng-click='openRestoreModal()', popover-trigger='mouseenter', popover-placement='right', popover=env.t('fixValPop'))= env.t('fixVal') + button.btn.btn-default(ng-if='user.preferences.disableClasses==true', ng-click='User.changeClass({})', popover-trigger='mouseenter', popover-placement='right', popover=env.t('enableClassPop'))= env.t('enableClass') + + hr + + h5=env.t('customDayStart') + alert.alert-warning=env.t('customDayStartInfo1') + + .form-horizontal + .form-group + .col-sm-7 + select.form-control(ng-model='dayStart') + - var number = 0 + while number < 24 + - var value = number + - var meridian = number < 12 ? 'AM' : 'PM' + - var hour = number++ % 12 + option(value=value) #{hour ? hour : 12}:00 #{meridian} + + .col-sm-5 + br.visible-xs + button.btn.btn-block.btn-primary(ng-click='openDayStartModal(dayStart)', + ng-disabled='dayStart == user.preferences.dayStart') + =env.t('saveCustomDayStart') + + hr + + h5=env.t('timezone') + .form-horizontal + .form-group + .col-sm-12 + p!=env.t('timezoneUTC', {utc: "{{ user.preferences.timezoneOffset | timezoneOffsetToUtc }}"}) + br + p!=env.t('timezoneInfo') + + .personal-options.col-md-6 + .panel.panel-default + .panel-heading + span=env.t('registration') + .panel-body + div + ul.list-inline + li(ng-repeat='network in SOCIAL_AUTH_NETWORKS') + button.btn.btn-primary(ng-if='!user.auth[network.key].id', ng-click='socialLogin(network.key, user)')=env.t('registerWithSocial', {network: '{{network.name}}'}) + button.btn.btn-primary(disabled='disabled', ng-if='!hasBackupAuthOption(user, network.key) && user.auth[network.key].id')=env.t('registeredWithSocial', {network: '{{network.name}}'}) + button.btn.btn-danger(ng-click='deleteSocialAuth(network.key)', ng-if='hasBackupAuthOption(user, network.key) && user.auth[network.key].id')=env.t('detachSocial', {network: '{{network.name}}'}) + hr + div(ng-if='!user.auth.local.username') + p=env.t('addLocalAuth') + form(ng-submit='http("post", "/api/v3/user/auth/local/register", localAuth, "addedLocalAuth")', ng-init='localAuth={}', name='localAuth', novalidate) + //-.alert.alert-danger(ng-messages='changeUsername.$error && changeUsername.submitted')=env.t('fillAll') + .form-group + input.form-control(type='text', placeholder=env.t('username'), ng-model='localAuth.username', required) + .form-group + input.form-control(type='text', placeholder=env.t('email'), ng-model='localAuth.email', required) + .form-group + input.form-control(type='password', placeholder=env.t('password'), ng-model='localAuth.password', required) + .form-group + input.form-control(type='password', placeholder=env.t('confirmPass'), ng-model='localAuth.confirmPassword', required) + input.btn.btn-default(type='submit', ng-disabled='localAuth.$invalid', value=env.t('submit')) + + div(ng-if='user.auth.local.username') + p=env.t('username') + |: {{user.auth.local.username}} + p + small.muted + =env.t('loginNameDescription1') + |  + a(href='/#/options/profile/profile')=env.t('loginNameDescription2') + |  + =env.t('loginNameDescription3') + p=env.t('email') + |: {{user.auth.local.email}} + hr + + h5=env.t('changeUsername') + form(ng-submit='changeUser("username", usernameUpdates)', ng-init='usernameUpdates={}', ng-show='user.auth.local', name='changeUsername', novalidate) + //-.alert.alert-danger(ng-messages='changeUsername.$error && changeUsername.submitted')=env.t('fillAll') + .form-group + input.form-control(type='text', placeholder=env.t('newUsername'), ng-model='usernameUpdates.username', required) + .form-group + input.form-control(type='password', placeholder=env.t('password'), ng-model='usernameUpdates.password', required) + input.btn.btn-default(type='submit', ng-disabled='changeUsername.$invalid', value=env.t('submit')) + + h5=env.t('changeEmail') + form(ng-submit='changeUser("email", emailUpdates)', ng-show='user.auth.local', name='changeEmail', novalidate) + .form-group + input.form-control(type='text', placeholder=env.t('newEmail'), ng-model='emailUpdates.newEmail', required) + .form-group + input.form-control(type='password', placeholder=env.t('password'), ng-model='emailUpdates.password', required) + input.btn.btn-default(type='submit', ng-disabled='changeEmail.$invalid', value=env.t('submit')) + + h5=env.t('changePass') + form(ng-submit='changeUser("password", passwordUpdates)', ng-show='user.auth.local', name='changePassword', novalidate) + .form-group + input.form-control(type='password', placeholder=env.t('oldPass'), ng-model='passwordUpdates.password', required) + .form-group + input.form-control(type='password', placeholder=env.t('newPass'), ng-model='passwordUpdates.newPassword', required) + .form-group + input.form-control(type='password', placeholder=env.t('confirmPass'), ng-model='passwordUpdates.confirmPassword', required) + input.btn.btn-default(type='submit', ng-disabled='changePassword.$invalid', value=env.t('submit')) + + + .panel.panel-default + .panel-heading + span=env.t('dangerZone') + .panel-body + a.btn.btn-danger(ng-click='openModal("reset", {controller:"SettingsCtrl"})', popover-trigger='mouseenter', popover-placement='right', popover=env.t('resetAccPop'))= env.t('resetAccount') + a.btn.btn-danger(ng-click='openModal("delete", {controller:"SettingsCtrl"})', popover-trigger='mouseenter', popover=env.t('deleteAccPop'))= env.t('deleteAccount') diff --git a/website/views/options/social/group.jade b/website/views/options/social/group.jade index 7cac400daa..51fc476bb1 100644 --- a/website/views/options/social/group.jade +++ b/website/views/options/social/group.jade @@ -107,7 +107,7 @@ a.pull-right.gem-wallet(ng-if='group.type!="party"', popover-trigger='mouseenter table.table.table-striped(ng-show='::group.memberCount > 1 || group.type !== "party"' bindonce='group') tr(ng-repeat='member in group.members track by member._id') td.media - // allow leaders and administrators to ban members + // allow leaders and administrators to remove members .pull-left(ng-show='(isAbleToEditGroup(group) && member._id != user._id)') a.media-object(ng-click='removeMember(group, member, true)') span.glyphicon.glyphicon-ban-circle(tooltip=env.t('banTip')) @@ -130,7 +130,7 @@ a.pull-right.gem-wallet(ng-if='group.type!="party"', popover-trigger='mouseenter table.table.table-striped tr(ng-repeat='invite in group.invites') td.media - // allow leaders and administrators to ban members + // allow leaders and administrators to remove members .pull-left(ng-show='isAbleToEditGroup(group)') a.media-object(ng-click='removeMember(group, invite, false)') span.glyphicon.glyphicon-ban-circle(tooltip=env.t('banTip')) diff --git a/website/views/shared/modals/task-notes.jade b/website/views/shared/modals/task-notes.jade index fcad3da5c5..c68bb6b9fd 100644 --- a/website/views/shared/modals/task-notes.jade +++ b/website/views/shared/modals/task-notes.jade @@ -1,10 +1,10 @@ -script(type='text/ng-template', id='modals/task-notes.html') - .modal-header - h4 Task Notes - - .modal-body - textarea.form-control(ng-model="task.scoreNotes", row="10") - - .modal-footer - .btn.btn-default(ng-click='$close()')=env.t('cancel') - .btn.btn-primary(ng-click="$close(task)")=env.t('save') +script(type='text/ng-template', id='modals/task-notes.html') + .modal-header + h4 Task Notes + + .modal-body + textarea.form-control(ng-model="task.scoreNotes", row="10") + + .modal-footer + .btn.btn-default(ng-click='$close()')=env.t('cancel') + .btn.btn-primary(ng-click="$close(task)")=env.t('save') diff --git a/website/views/shared/modals/tasks-edit.jade b/website/views/shared/modals/tasks-edit.jade index 0ed820bb76..8459ee3c16 100644 --- a/website/views/shared/modals/tasks-edit.jade +++ b/website/views/shared/modals/tasks-edit.jade @@ -1,6 +1,6 @@ -script(type='text/ng-template', id='modals/task-edit.html') - .modal-content.task-modal(style='min-width:22em', class='{{taskStatus}}', id="task-{{task._id}}") - .modal-body.text-center(style='padding-bottom:0') - include ../tasks/edit/index - .modal-footer(style='margin-top:0') - .container-fluid +script(type='text/ng-template', id='modals/task-edit.html') + .modal-content.task-modal(style='min-width:22em', class='{{taskStatus}}', id="task-{{task._id}}") + .modal-body.text-center(style='padding-bottom:0') + include ../tasks/edit/index + .modal-footer(style='margin-top:0') + .container-fluid diff --git a/website/views/shared/tasks/task-list.jade b/website/views/shared/tasks/task-list.jade index c1a65a15dd..18a81f6bc0 100644 --- a/website/views/shared/tasks/task-list.jade +++ b/website/views/shared/tasks/task-list.jade @@ -1,20 +1,20 @@ -script(id='templates/task-list.html', type="text/ng-template") - ul(ng-init='setObj(obj)', class='{{::list.type}}s main-list', - ng-show='obj[list.type+"s"].length > 0', - ng-if='showNormalList(obj)') - task(hrpg-sort-tasks) - - div(ng-init='setObj(obj);') - div( - ng-repeat="(key, taskList) in groupedList[list.type]", - ng-if='showGroupedList(obj)') - h3 {{key}} - ul(class='{{::list.type}}s main-list', - ng-show='taskList.length > 0') - task(hrpg-sort-tasks) - - //Loads the non-sortable lists for challenges - ul(ng-init='setObj(obj)', class='{{::list.type}}s main-list', - ng-show='obj[list.type + "s"].length > 0', - ng-if='showChallengeList()') - task +script(id='templates/task-list.html', type="text/ng-template") + ul(ng-init='setObj(obj)', class='{{::list.type}}s main-list', + ng-show='obj[list.type+"s"].length > 0', + ng-if='showNormalList(obj)') + task(hrpg-sort-tasks) + + div(ng-init='setObj(obj);') + div( + ng-repeat="(key, taskList) in groupedList[list.type]", + ng-if='showGroupedList(obj)') + h3 {{key}} + ul(class='{{::list.type}}s main-list', + ng-show='taskList.length > 0') + task(hrpg-sort-tasks) + + //Loads the non-sortable lists for challenges + ul(ng-init='setObj(obj)', class='{{::list.type}}s main-list', + ng-show='obj[list.type + "s"].length > 0', + ng-if='showChallengeList()') + task