Merge branch 'develop' into release

This commit is contained in:
SabreCat 2017-06-30 20:50:18 +00:00
commit 1853113aed
114 changed files with 4571 additions and 2057 deletions

View file

@ -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']);

View file

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

View file

@ -21,4 +21,4 @@ var processUsers = require('./groups/update-groups-with-group-plans');
processUsers()
.catch(function (err) {
console.log(err)
})
})

View file

@ -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();
};

701
npm-shrinkwrap.json generated
View file

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

View file

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

View file

@ -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();

View file

@ -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 () => {

View file

@ -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);
});
});
});

View file

@ -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}],

View file

@ -1,7 +1,7 @@
import axios from 'axios';
import generateStore from 'client/store';
describe('tasks actions', () => {
describe('user actions', () => {
let store;
beforeEach(() => {

View file

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

View file

@ -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});
};
}]);

View file

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

View file

@ -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/
- Were using `.vue` files that make it possible to have HTML, JS and CSS for each component together in a single location. Theyre 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 Ive only used the CSS part, it also has JS plugins but Ive yet to use them. It supports theming so if its not too difficult well 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`. Were using something very similar to Vuex (equivalent of Reacts Redux) for state management http://vuex.vuejs.org/en/index.html
The API is almost the same except that we dont 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 well be able to avoid splitting it into a different branch.
So far most of the work has been on the template, so theres no complex logic to understand. The only thing I would suggest you to read about is Vuex for data management: its basically a Flux implementation: theres 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/
- Were using `.vue` files that make it possible to have HTML, JS and CSS for each component together in a single location. Theyre 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 Ive only used the CSS part, it also has JS plugins but Ive yet to use them. It supports theming so if its not too difficult well 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`. Were using something very similar to Vuex (equivalent of Reacts Redux) for state management http://vuex.vuejs.org/en/index.html
The API is almost the same except that we dont 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 well be able to avoid splitting it into a different branch.
So far most of the work has been on the template, so theres no complex logic to understand. The only thing I would suggest you to read about is Vuex for data management: its basically a Flux implementation: theres 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.

View file

@ -1,16 +1,20 @@
<!-- Entry point component for the entire app -->
<template lang="pug">
#app
app-menu
.container-fluid
app-header
router-view
router-view(v-if="!isUserLoggedIn || isStaticPage")
template(v-else)
#loading-screen.h-100.w-100.d-flex.justify-content-center.align-items-center(v-if="!isUserLoaded")
p Loading...
template(v-else)
app-menu
.container-fluid
app-header
router-view
</template>
<script>
import AppMenu from './components/appMenu';
import AppHeader from './components/appHeader';
import { mapState } from 'client/libs/store';
export default {
name: 'app',
@ -18,9 +22,38 @@ export default {
AppMenu,
AppHeader,
},
data () {
return {
isUserLoaded: false,
};
},
computed: {
...mapState(['isUserLoggedIn']),
isStaticPage () {
return this.$route.meta.requiresLogin === false ? true : false;
},
},
created () {
// Setup listener for title
this.$store.watch(state => state.title, (title) => {
document.title = title;
});
if (this.isUserLoggedIn && !this.isStaticPage) {
// Load the user and the user tasks
Promise.all([
this.$store.dispatch('user:fetch'),
this.$store.dispatch('tasks:fetchUserTasks'),
]).then(() => {
this.isUserLoaded = true;
}).catch((err) => {
console.error('Impossible to fetch user. Clean up localStorage and refresh.', err); // eslint-disable-line no-console
});
}
},
};
</script>
<style src="bootstrap/scss/bootstrap.scss" lang="scss"></style>
<style src="assets/scss/index.scss" lang="scss"></style>
<style src="assets/css/index.css"></style>
<style src="assets/css/index.css"></style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 894 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 882 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

View file

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

View file

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

View file

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

View file

@ -20,4 +20,7 @@
@import './popover';
@import './item';
@import './stats';
@import './icon';
@import './icon';
@import './task';
@import './categories';
@import './dragdrop';

View file

@ -3,7 +3,7 @@ html {
}
html, body {
height: 100%;
height: calc(100% - 56px); // 56px is the menu
background: $gray-700;
}

View file

@ -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);
}
}
}

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14">
<path fill-rule="evenodd" d="M2 12h10V6H2v6zM12 2V0h-2v2H4V0H2v2a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2z"/>
</svg>

After

Width:  |  Height:  |  Size: 227 B

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="12" viewBox="0 0 14 12">
<path fill-rule="evenodd" d="M10 6.306L2.582 7.542A.5.5 0 0 1 2 7.05V2.591a.5.5 0 0 1 .582-.493L10 3.334v2.972zm2.329-4.612l-.024-.004c-.007-.002-.012-.007-.02-.009-.017-.005-.035.001-.052-.003L2.329.028A2 2 0 0 0 0 2v5.64a2 2 0 0 0 2.329 1.972l7.056-1.176-.525 2.1a1.175 1.175 0 0 0 2.28.57l.772-3.09.417-.07A2 2 0 0 0 14 5.971V3.667a2 2 0 0 0-1.671-1.973z"/>
</svg>

After

Width:  |  Height:  |  Size: 456 B

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="13" height="10" viewBox="0 0 13 10">
<path fill-rule="evenodd" d="M4.662 9.832c-.312 0-.61-.123-.831-.344L0 5.657l1.662-1.662 2.934 2.934L10.534 0l1.785 1.529-6.764 7.893a1.182 1.182 0 0 1-.848.409l-.045.001"/>
</svg>

After

Width:  |  Height:  |  Size: 268 B

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="20" viewBox="0 0 24 20">
<path fill="#C3C0C7" fill-rule="evenodd" d="M13 16h2v-2h-2v2zm-4 0h2v-2H9v2zm-4 0h2v-2H5v2zm12-4h2v-2h-2v2zm-4 0h2v-2h-2v2zm-4 0h2v-2H9v2zm13-4H2v8a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V8zm2-2v10a4 4 0 0 1-4 4H4a4 4 0 0 1-4-4V6a4 4 0 0 1 4-4h1V0h2v2h10V0h2v2h1a4 4 0 0 1 4 4zM5 12h2v-2H5v2z"/>
</svg>

After

Width:  |  Height:  |  Size: 382 B

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<path fill="#FFB445" fill-rule="evenodd" d="M10.667 10.667L16 8l-5.333-2.667L8 0 5.333 5.333 0 8l5.333 2.667L8 16z"/>
</svg>

After

Width:  |  Height:  |  Size: 213 B

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="13" viewBox="0 0 20 13">
<path fill="none" fill-rule="evenodd" stroke="#C3C0C7" stroke-width="4" d="M18 2l-8 8-8-8"/>
</svg>

After

Width:  |  Height:  |  Size: 188 B

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="8" height="16" viewBox="0 0 8 16">
<path fill="#878190" fill-rule="evenodd" d="M7.145 8.006H4.903V16H1.58V8.006H0V5.182h1.58V3.354C1.58 2.045 2.202 0 4.933 0l2.461.01v2.742H5.608c-.291 0-.705.145-.705.77v1.66h2.533l-.291 2.824z"/>
</svg>

After

Width:  |  Height:  |  Size: 289 B

View file

@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18">
<g fill="none" fill-rule="evenodd">
<path fill="#4285F4" d="M17.64 9.205c0-.639-.057-1.252-.164-1.841H9v3.481h4.844a4.14 4.14 0 0 1-1.796 2.716v2.259h2.908c1.702-1.567 2.684-3.875 2.684-6.615z"/>
<path fill="#34A853" d="M9 18c2.43 0 4.467-.806 5.956-2.18l-2.908-2.259c-.806.54-1.837.86-3.048.86-2.344 0-4.328-1.584-5.036-3.711H.957v2.332A8.997 8.997 0 0 0 9 18z"/>
<path fill="#FBBC05" d="M3.964 10.71A5.41 5.41 0 0 1 3.682 9c0-.593.102-1.17.282-1.71V4.958H.957A8.996 8.996 0 0 0 0 9c0 1.452.348 2.827.957 4.042l3.007-2.332z"/>
<path fill="#EA4335" d="M9 3.58c1.321 0 2.508.454 3.44 1.345l2.582-2.58C13.463.891 11.426 0 9 0A8.997 8.997 0 0 0 .957 4.958L3.964 7.29C4.672 5.163 6.656 3.58 9 3.58z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 834 B

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="65" height="70" viewBox="0 0 65 70">
<path fill="#FFF" fill-rule="evenodd" d="M62.749 65.754c1.728 2.297 1.853 3.623.136 3.623H51.783c-3.344 0-4.038.396-3.985-1.056.072-2.024.725-2.103 2.851-2.696 1.763-.493 1.13-3.63-.869-6.376-1.392-1.91-4.216-4.134-7.929-2.405-2.41 1.122-3.828 2.27-4.943 3.815-1.823 2.53-1.724 4.927 1.115 4.927 2.683 0 5.843-1.626 6.76.69 1.134 2.867.355 3.133-.2 3.133H26.79l.012.017c-9.532 0-19.054-.075-21.262-6.099-3.056-8.334 8.063-11.546 8.404-16.775.138-2.116-1.051-3.096-2.536-3.096H4.885V39.49H.921V27.6h3.964v3.964H8.85v3.964h3.965v5.506s3.447 1.71 3.33 5.478c-.186 5.982-10.304 9.296-8.45 15.567 1.695 5.738 14.502 4.364 14.053.804l-.222-2.028c-.063-2.84-.506-6.64.087-10.005.934-5.289 4.224-9.865 9.956-9.907 1.045-.008 1.929-.222 1.929-1.1 0-.927-1.68-.906-2.863-1.025-4.09-.417-9.18-1.787-13.627-5.445-2.634-2.167-6.736-8.999-4.054-8.498 1.656.31 3.116.437 4.4.433 1.346-.004 4.82-.382 4.82-1.203 0-1.138-2.01-.453-4.955-.741-3.521-.344-10.988-1.853-14.84-13.215C.932 5.89-.02-2.075 2.324.503 15.84 15.367 20.433 12.947 28.184 18.029c3.412 2.237 5.633 7.58 7.622 7.135 1.296-.292.502-1.483.696-4.262.173-2.484 1.354-3.022-1.683-3.773-3.538-.873-7.07-4.752-4.76-4.697 2.307.056 3.977.048 6-.828 2.024-.875 6.97-4.641 9.962-3.133 2.992 1.509 5.507-.656 7.531-.272 2.938.557 3.694 4.6 3.313 6.965-.24 1.5-.24 1.498-2.153 1.386-3.921-.23-4.92 2.893-2.162 4.615 4.197 2.621 5.98 4.588 7.121 10.395.555 2.817.105 3.474-.83 2.956-.933-.519-2.539-.778-.31 4.356 2.23 5.133-1.4 11.15-2.592 11.825-1.194.674-.986 1.452-.986 1.452-.023 12.387 0 13.315 2.078 13.538 3.15.34 4.49-1.565 5.718.067z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="30" height="20" viewBox="0 0 30 20">
<path fill="#C3C0C7" fill-rule="evenodd" d="M11 11H9v2H7v-2H5V9h2V7h2v2h2v2zm8 0h6V9h-6v2zm9 5c0 1.103-.897 2-2 2H16V2h10c1.103 0 2 .897 2 2v12zM4 18c-1.103 0-2-.897-2-2V4c0-1.103.897-2 2-2h10v16H4zM26 0H4a4 4 0 0 0-4 4v12a4 4 0 0 0 4 4h22a4 4 0 0 0 4-4V4a4 4 0 0 0-4-4z"/>
</svg>

After

Width:  |  Height:  |  Size: 369 B

View file

@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" width="144" height="31" viewBox="0 0 144 31">
<g fill="none" fill-rule="evenodd">
<path fill="#FFF" d="M120.876 24.007a2.27 2.27 0 0 0-3.183.41 4.595 4.595 0 0 1-3.663 1.804 4.62 4.62 0 0 1-4.613-4.335c-.005-.35-.009-2.864-.009-3.19a4.627 4.627 0 0 1 4.622-4.622c1.28 0 2.47.51 3.353 1.44a2.269 2.269 0 0 0 3.29-3.125 9.2 9.2 0 0 0-6.643-2.853c-5.05 0-9.16 4.109-9.16 9.16 0 .03.002 3.175.014 3.406a9.158 9.158 0 0 0 9.146 8.657 9.1 9.1 0 0 0 7.257-3.57 2.27 2.27 0 0 0-.411-3.182M134.373 26.221a4.62 4.62 0 0 1-4.613-4.333c-.005-.353-.008-2.877-.008-3.193a4.627 4.627 0 0 1 4.621-4.622 4.627 4.627 0 0 1 4.622 4.622c0 .328-.003 2.84-.009 3.189a4.618 4.618 0 0 1-4.613 4.337m6.891-17.078a2.264 2.264 0 0 0-2.19 1.706 9.095 9.095 0 0 0-4.7-1.313c-5.051 0-9.16 4.109-9.16 9.16 0 .031.001 3.173.013 3.406a9.158 9.158 0 0 0 9.146 8.657 9.118 9.118 0 0 0 4.81-1.37 2.268 2.268 0 0 0 4.35-.899V11.412a2.27 2.27 0 0 0-2.269-2.269M30.546 26.221a4.62 4.62 0 0 1-4.613-4.335c-.006-.35-.01-2.863-.01-3.19a4.627 4.627 0 0 1 4.623-4.623 4.627 4.627 0 0 1 4.622 4.622c0 .328-.004 2.84-.01 3.189a4.618 4.618 0 0 1-4.612 4.337m6.89-17.078a2.264 2.264 0 0 0-2.19 1.706 9.095 9.095 0 0 0-4.7-1.313c-5.052 0-9.16 4.109-9.16 9.16 0 .031 0 3.174.013 3.406a9.158 9.158 0 0 0 9.147 8.657 9.118 9.118 0 0 0 4.809-1.37 2.268 2.268 0 0 0 4.35-.899V11.412a2.27 2.27 0 0 0-2.269-2.269M70.84 9.143a2.27 2.27 0 0 0-2.27 2.27V28.49a2.27 2.27 0 0 0 4.539 0V11.412a2.27 2.27 0 0 0-2.27-2.269M97.563 9.143a2.27 2.27 0 0 0-2.27 2.27V28.49a2.27 2.27 0 0 0 4.538 0V11.412a2.27 2.27 0 0 0-2.268-2.269M59.066 21.888a4.62 4.62 0 0 1-4.613 4.333 4.62 4.62 0 0 1-4.613-4.338c-.006-.35-.009-2.86-.009-3.187a4.627 4.627 0 0 1 4.622-4.622 4.627 4.627 0 0 1 4.622 4.622c0 .315-.004 2.84-.009 3.192M54.453 9.536a9.089 9.089 0 0 0-4.622 1.265V2.33a2.27 2.27 0 0 0-4.537 0V28.49a2.269 2.269 0 0 0 4.35.9 9.117 9.117 0 0 0 4.81 1.37 9.16 9.16 0 0 0 9.146-8.666c.011-.224.013-3.367.013-3.398 0-5.052-4.11-9.16-9.16-9.16M8.92 9.536a9.143 9.143 0 0 0-4.382 1.11V2.33A2.27 2.27 0 0 0 0 2.33v26.16a2.269 2.269 0 1 0 4.538 0V16.763c.173-.147.333-.314.46-.516a4.601 4.601 0 0 1 3.921-2.173 4.627 4.627 0 0 1 4.622 4.622c0 .415-.004 9.233-.01 9.738a2.27 2.27 0 0 0 4.535.172c.01-.225.012-9.814.012-9.91 0-5.052-4.108-9.16-9.159-9.16M88.95 9.143h-2.648V2.33a2.27 2.27 0 0 0-4.538 0v6.813h-2.647a2.27 2.27 0 0 0 0 4.538h2.647V28.49a2.27 2.27 0 0 0 4.538 0V13.681h2.647a2.27 2.27 0 0 0 0-4.538"/>
<path fill="#FF6066" d="M73.025 2.33a2.27 2.27 0 1 1-4.538 0 2.27 2.27 0 0 1 4.538 0"/>
<path fill="#4FB5E8" d="M99.748 2.33a2.27 2.27 0 1 1-4.539 0 2.27 2.27 0 0 1 4.539 0"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="12" viewBox="0 0 16 12">
<path fill="#4F2A93" fill-rule="evenodd" d="M14 10H2V2l6 5 6-5v8zm0-10H2a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2z"/>
</svg>

After

Width:  |  Height:  |  Size: 235 B

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="10" height="2" viewBox="0 0 10 2">
<path fill-rule="evenodd" d="M0 0h10v2H0z"/>
</svg>

After

Width:  |  Height:  |  Size: 135 B

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="13" height="20" viewBox="0 0 13 20">
<path fill="none" fill-rule="evenodd" stroke="#A5A1AC" stroke-width="4" d="M2 2l8 8-8 8"/>
</svg>

After

Width:  |  Height:  |  Size: 186 B

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="0 0 10 10">
<path fill-rule="evenodd" d="M6 4V0H4v4H0v2h4v4h2V6h4V4H6z"/>
</svg>

After

Width:  |  Height:  |  Size: 157 B

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="13" height="20" viewBox="0 0 13 20">
<path fill="none" fill-rule="evenodd" stroke="#A5A1AC" stroke-width="4" d="M11 2l-8 8 8 8"/>
</svg>

After

Width:  |  Height:  |  Size: 188 B

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<path fill="#878190" fill-rule="evenodd" d="M7 8h1V7H7v1zm4 5h2v-2h-2v2zm-8 0h2v-2H3v2zm8-8h2V3h-2v2zM3 5h2V3H3v2zm7 1h4V2h-4v4zm4 3h-1V8h-1V7h1v1h1v1zm-4 5h4v-4h-4v4zm-3 0v-1h1v-1H7v-1h1v-1H7V9H6V7h1V5h1V4h1v1H8v1h1v2h1V7h1v2H9V8H8v2h1v3H8v1H7zm-5 0h4v-4H2v4zm0-6h1V7h2v1H4v1H2V8zm0-2h4V2H2v4zm7-4v1H7V2h2zm5-2H2a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2z"/>
</svg>

After

Width:  |  Height:  |  Size: 477 B

View file

@ -0,0 +1,36 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="424" height="80" viewBox="0 0 424 80">
<defs>
<path id="a" d="M0 0h424v80H0z"/>
<path id="b" d="M0 0h10v10H0z"/>
<path id="c" d="M414 0h10v10h-10z"/>
<path id="d" d="M0 70h10v10H0z"/>
<path id="e" d="M414 70h10v10h-10z"/>
</defs>
<g fill="#F9F9F9" fill-rule="evenodd">
<g>
<use fill="#F9F9F9" xlink:href="#a"/>
<path stroke="#E1E0E3" stroke-width="4" d="M2 2h420v76H2z"/>
<path stroke="#C3C0C7" stroke-width="2" d="M1 1h422v78H1z"/>
</g>
<g>
<use fill="#EDECEE" xlink:href="#b"/>
<path stroke="#E1E0E3" stroke-width="4" d="M2 2h6v6H2z"/>
<path stroke="#C3C0C7" stroke-width="2" d="M1 1h8v8H1z"/>
</g>
<g>
<use fill="#EDECEE" xlink:href="#c"/>
<path stroke="#E1E0E3" stroke-width="4" d="M416 2h6v6h-6z"/>
<path stroke="#C3C0C7" stroke-width="2" d="M415 1h8v8h-8z"/>
</g>
<g>
<use fill="#EDECEE" xlink:href="#d"/>
<path stroke="#E1E0E3" stroke-width="4" d="M2 72h6v6H2z"/>
<path stroke="#C3C0C7" stroke-width="2" d="M1 71h8v8H1z"/>
</g>
<g>
<use fill="#EDECEE" xlink:href="#e"/>
<path stroke="#E1E0E3" stroke-width="4" d="M416 72h6v6h-6z"/>
<path stroke="#C3C0C7" stroke-width="2" d="M415 71h8v8h-8z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="20" viewBox="0 0 18 20">
<path fill="#C3C0C7" fill-rule="evenodd" d="M3 16h4v-1H3v1zm0-4h9v-1H3v1zm0-2h9V9H3v1zm0-5h9V4H3v1zm0 3h9V7H3v1zm10 7v3H2V2h11v13zm2 3h1v-1h-1v1zm0-15h1V2h-1v1zm3 2V2a2 2 0 0 0-2-2H2a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-3h-3V5h3z"/>
</svg>

After

Width:  |  Height:  |  Size: 340 B

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<path fill="#878190" fill-rule="evenodd" d="M8 14a5.96 5.96 0 0 1-3.327-1.011l8.316-8.316A5.96 5.96 0 0 1 14 8c0 3.309-2.691 6-6 6M8 2a5.96 5.96 0 0 1 3.327 1.011l-8.316 8.316A5.96 5.96 0 0 1 2 8c0-3.309 2.691-6 6-6m0-2a8 8 0 1 0 0 16A8 8 0 0 0 8 0"/>
</svg>

After

Width:  |  Height:  |  Size: 347 B

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="26" height="20" viewBox="0 0 26 20">
<path fill="#C3C0C7" fill-rule="evenodd" d="M24 10h-8V8h4a2 2 0 0 0 2-2V2c1.103 0 2 .897 2 2v6zm0 6a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2v-4h8v1a2 2 0 0 0 2 2h2a2 2 0 0 0 2-2v-1h8v4zM2 4c0-1.103.897-2 2-2v4a2 2 0 0 0 2 2h4v2H2V4zm10 9h2V8h-2v5zm8-11v4H6V2h14zm2-2H4a4 4 0 0 0-4 4v12a4 4 0 0 0 4 4h18a4 4 0 0 0 4-4V4a4 4 0 0 0-4-4z"/>
</svg>

After

Width:  |  Height:  |  Size: 421 B

View file

@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<g fill="none" fill-rule="evenodd">
<path fill="#FFA623" d="M16 16l8-4-8-4-4-8-4 8-8 4 8 4 4 8z"/>
<path fill="#FFF" d="M4.5 12l5-2.5L12 12zM12 19.5l-2.5-5L12 12zM19.5 12l-5 2.5L12 12zM12 4.5l2.5 5L12 12z" opacity=".25"/>
<path fill="#BF7D1A" d="M19.5 12l-5-2.5L12 12z" opacity=".25"/>
<path fill="#BF7D1A" d="M12 19.5l2.5-5L12 12z" opacity=".5"/>
<path fill="#FFF" d="M4.5 12l5 2.5L12 12zM12 4.5l-2.5 5L12 12z" opacity=".5"/>
<path fill="#FFF" d="M10.8 13.2L8.5 12l2.3-1.2L12 8.5l1.2 2.3 2.3 1.2-2.3 1.2-1.2 2.3z" opacity=".5"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 682 B

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="8" viewBox="0 0 12 8">
<path fill-rule="evenodd" d="M11.376 3.15L6.777.086A.5.5 0 0 0 6 .5v6.132a.5.5 0 0 0 .777.416l4.599-3.066a.5.5 0 0 0 0-.832M.777.085L6 3.567.777 7.049A.5.5 0 0 1 0 6.633V.5A.5.5 0 0 1 .777.085"/>
</svg>

After

Width:  |  Height:  |  Size: 289 B

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14">
<path fill-rule="evenodd" d="M10 3a1 1 0 1 1 0 2 1 1 0 0 1 0-2zM2.004 6.994L7 2h5l-.004 5.006L7 12l.004-.004-5-5.002zM0 7c0 .55.22 1.05.59 1.41l5 5a1.996 1.996 0 0 0 2.83 0l4.99-4.99c.37-.37.59-.87.59-1.42V2c0-1.11-.89-2-2-2H7c-.55 0-1.05.22-1.41.58l-5 5C.23 5.94 0 6.44 0 7z"/>
</svg>

After

Width:  |  Height:  |  Size: 373 B

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20">
<path fill="#C3C0C7" fill-rule="evenodd" d="M8.343 14.916c-.312 0-.61-.123-.831-.344l-3.831-3.831 1.662-1.662 2.934 2.934 5.938-6.929L16 6.613l-6.764 7.893a1.182 1.182 0 0 1-.848.409l-.045.001zM18 16c0 1.103-.897 2-2 2H4c-1.102 0-2-.897-2-2V4c0-1.103.898-2 2-2h12c1.103 0 2 .897 2 2v12zM16 0H4a4 4 0 0 0-4 4v12a4 4 0 0 0 4 4h12a4 4 0 0 0 4-4V4a4 4 0 0 0-4-4z"/>
</svg>

After

Width:  |  Height:  |  Size: 457 B

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="13" viewBox="0 0 16 13">
<path fill="#878190" fill-rule="evenodd" d="M14.362 3.238c.007.141.01.281.01.424 0 4.338-3.302 9.34-9.34 9.34A9.284 9.284 0 0 1 0 11.527c.257.029.518.045.783.045a6.576 6.576 0 0 0 4.076-1.404 3.288 3.288 0 0 1-3.065-2.28 3.312 3.312 0 0 0 1.481-.056A3.288 3.288 0 0 1 .642 4.613v-.041c.444.246.949.393 1.488.41A3.28 3.28 0 0 1 .67 2.25c0-.602.162-1.166.444-1.651a9.315 9.315 0 0 0 6.766 3.43A3.28 3.28 0 0 1 11.078 0c.943 0 1.797.398 2.395 1.035a6.565 6.565 0 0 0 2.085-.797 3.289 3.289 0 0 1-1.443 1.816A6.543 6.543 0 0 0 16 1.539a6.665 6.665 0 0 1-1.638 1.699"/>
</svg>

After

Width:  |  Height:  |  Size: 660 B

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="13" viewBox="0 0 20 13">
<path fill="none" fill-rule="evenodd" stroke="#A5A1AC" stroke-width="4" d="M18 11l-8-8-8 8"/>
</svg>

After

Width:  |  Height:  |  Size: 189 B

View file

@ -1,7 +1,7 @@
<template lang="pug">
#app-header.row
member-details(:member="user", @click="$router.push({name: 'avatar'})")
.view-party
.view-party(v-if="user.party && user.party._id")
// TODO button should open the party members modal
router-link.btn.btn-primary(:active-class="''", :to="{name: 'party'}") {{ $t('viewParty') }}
.party-members.d-flex(v-if="partyMembers && partyMembers.length > 1")
@ -59,6 +59,7 @@
.no-party {
.small-text {
color: $header-color;
flex-wrap: nowrap;
}
h3 {
@ -66,7 +67,7 @@
margin-bottom: 4px;
}
button {
.btn {
margin-top: 16px;
}
}
@ -75,10 +76,12 @@
<script>
import { mapGetters, mapActions } from 'client/libs/store';
import MemberDetails from './memberDetails';
import createPartyModal from './guilds/createPartyModal';
export default {
components: {
MemberDetails,
createPartyModal,
},
data () {
return {
@ -104,7 +107,7 @@ export default {
},
},
created () {
this.getPartyMembers();
if (this.user.party && this.user.party._id) this.getPartyMembers();
},
};
</script>

View file

@ -14,7 +14,7 @@ nav.navbar.navbar-inverse.fixed-top.navbar-toggleable-sm
router-link.dropdown-item(:to="{name: 'stable'}") {{ $t('stable') }}
router-link.nav-item(tag="li", :to="{name: 'shops'}", exact)
a.nav-link(v-once) {{ $t('shops') }}
router-link.nav-item(tag="li", :to="{name: 'party'}", exact)
router-link.nav-item(tag="li", :to="{name: 'party'}")
a.nav-link(v-once) {{ $t('party') }}
router-link.nav-item.dropdown(tag="li", :to="{name: 'tavern'}", :class="{'active': $route.path.startsWith('/guilds')}")
a.nav-link(v-once) {{ $t('guilds') }}
@ -41,154 +41,154 @@ nav.navbar.navbar-inverse.fixed-top.navbar-toggleable-sm
router-link.dropdown.item-with-icon.item-user(:to="{name: 'avatar'}")
.svg-icon(v-html="icons.user")
.dropdown-menu.dropdown-menu-right.user-dropdown
router-link.dropdown-item.edit-avatar(:to="{name: 'avatar'}")
router-link.dropdown-item.edit-avatar(:to="{name: 'avatar'}")
h3 {{ user.profile.name }}
span.small-text {{ $t('editAvatar') }}
router-link.dropdown-item(:to="{name: 'inbox'}") {{ $t('inbox') }}
router-link.dropdown-item(:to="{name: 'stats'}") {{ $t('stats') }}
router-link.dropdown-item(:to="{name: 'achievements'}") {{ $t('achievements') }}
router-link.dropdown-item(:to="{name: 'settings'}") {{ $t('settings') }}
router-link.dropdown-item(to="/logout") {{ $t('logout') }}
a.nav-link.dropdown-item(to="/", @click.prevent='logout()') {{ $t('logout') }}
</template>
<style lang="scss" scoped>
@import '~client/assets/scss/colors.scss';
@import '~client/assets/scss/utils.scss';
@import '~client/assets/scss/colors.scss';
@import '~client/assets/scss/utils.scss';
nav.navbar {
background: $purple-100 url(~assets/svg/for-css/bits.svg) right no-repeat;
padding-left: 25px;
padding-right: 12.5px;
height: 56px;
box-shadow: 0 1px 2px 0 rgba($black, 0.24);
}
.navbar-header {
margin-right: 48px;
.logo {
width: 128px;
height: 28px;
}
}
.nav-item {
.nav-link {
font-size: 16px;
color: $white;
font-weight: bold;
line-height: 1.5;
padding: 16px 24px;
transition: none;
nav.navbar {
background: $purple-100 url(~assets/svg/for-css/bits.svg) right no-repeat;
padding-left: 25px;
padding-right: 12.5px;
height: 56px;
box-shadow: 0 1px 2px 0 rgba($black, 0.24);
}
&:hover {
.navbar-header {
margin-right: 48px;
.logo {
width: 128px;
height: 28px;
}
}
.nav-item {
.nav-link {
font-size: 16px;
color: $white;
background: $purple-200;
}
}
&.active:not(:hover) {
.nav-link {
box-shadow: 0px -4px 0px $purple-300 inset;
}
}
}
// Make the dropdown menu open on hover
.dropdown:hover .dropdown-menu {
display: block;
margin-top: 0; // remove the gap so it doesn't close
}
.dropdown + .dropdown {
margin-left: 0px;
}
.dropdown-menu:not(.user-dropdown) {
background: $purple-200;
border-radius: 0px;
border: none;
box-shadow: none;
padding: 0px;
border-bottom-right-radius: 5px;
border-bottom-left-radius: 5px;
.dropdown-item {
font-size: 16px;
box-shadow: none;
color: $white;
border: none;
line-height: 1.5;
&.active {
background: $purple-300;
font-weight: bold;
line-height: 1.5;
padding: 16px 24px;
transition: none;
}
&:hover {
background: $purple-300;
.nav-link {
color: $white;
background: $purple-200;
}
}
&:last-child {
border-bottom-right-radius: 5px;
border-bottom-left-radius: 5px;
&.active:not(:hover) {
.nav-link {
box-shadow: 0px -4px 0px $purple-300 inset;
}
}
}
}
.item-with-icon {
color: $white;
font-size: 16px;
font-weight: normal;
padding-top: 16px;
padding-left: 16px;
.svg-icon {
vertical-align: middle;
width: 24px;
height: 24px;
margin-right: 8px;
float: left;
}
}
.item-notifications, .item-user {
padding-right: 12.5px;
padding-left: 12.5px;
color: $header-color;
transition: none;
&:hover {
color: $white;
}
.svg-icon {
margin-right: 0px;
color: inherit;
}
}
.item-notifications {
margin-left: 33.5px;
}
.item-user .edit-avatar {
h3 {
color: $gray-10;
margin-bottom: 0px;
}
.small-text {
color: $gray-200;
font-style: normal;
// Make the dropdown menu open on hover
.dropdown:hover .dropdown-menu {
display: block;
margin-top: 0; // remove the gap so it doesn't close
}
padding-top: 16px;
padding-bottom: 16px;
}
.dropdown + .dropdown {
margin-left: 0px;
}
.dropdown-menu:not(.user-dropdown) {
background: $purple-200;
border-radius: 0px;
border: none;
box-shadow: none;
padding: 0px;
border-bottom-right-radius: 5px;
border-bottom-left-radius: 5px;
.dropdown-item {
font-size: 16px;
box-shadow: none;
color: $white;
border: none;
line-height: 1.5;
&.active {
background: $purple-300;
}
&:hover {
background: $purple-300;
&:last-child {
border-bottom-right-radius: 5px;
border-bottom-left-radius: 5px;
}
}
}
}
.item-with-icon {
color: $white;
font-size: 16px;
font-weight: normal;
padding-top: 16px;
padding-left: 16px;
.svg-icon {
vertical-align: middle;
width: 24px;
height: 24px;
margin-right: 8px;
float: left;
}
}
.item-notifications, .item-user {
padding-right: 12.5px;
padding-left: 12.5px;
color: $header-color;
transition: none;
&:hover {
color: $white;
}
.svg-icon {
margin-right: 0px;
color: inherit;
}
}
.item-notifications {
margin-left: 33.5px;
}
.item-user .edit-avatar {
h3 {
color: $gray-10;
margin-bottom: 0px;
}
.small-text {
color: $gray-200;
font-style: normal;
display: block;
}
padding-top: 16px;
padding-bottom: 16px;
}
</style>
<script>
@ -217,5 +217,11 @@ export default {
}),
...mapState({user: 'user.data'}),
},
methods: {
logout () {
localStorage.removeItem('habit-mobile-settings');
this.$router.go('/');
},
},
};
</script>

View file

@ -0,0 +1,230 @@
<template lang="pug">
.form-wrapper
#top-background
.seamless_stars_varied_opacity_repeat
#login-form
.text-center
div
.svg-icon.gryphon(v-html="icons.gryphon")
div
.svg-icon.habitica-logo(v-html="icons.habiticaIcon")
.form-group.row.text-center
.col-6
.btn.btn-secondary.social-button(@click='socialAuth("facebook")', v-once)
.svg-icon.social-icon(v-html="icons.facebookIcon")
| {{this.registering ? $t('signUpWithSocial', {social: 'Facebook'}) : $t('loginWithSocial', {social: 'Facebook'})}}
.col-6
.btn.btn-secondary.social-button(@click='socialAuth("google")', v-once)
.svg-icon.social-icon(v-html="icons.googleIcon")
| {{this.registering ? $t('signUpWithSocial', {social: 'Google'}) : $t('loginWithSocial', {social: 'Google'})}}
.form-group
label(for='usernameInput', v-once) {{$t('username')}}
input#usernameInput.form-control(type='text', :placeholder='$t("usernamePlaceholder")', v-model='username')
.form-group(v-if='registering')
label(for='emailInput', v-once) {{$t('email')}}
input#emailInput.form-control(type='email', :placeholder='$t("emailPlaceholder")', v-model='email')
.form-group
label(for='passwordInput', v-once) {{$t('password')}}
input#passwordInput.form-control(type='password', :placeholder='$t("passwordPlaceholder")', v-model='password')
.form-group(v-if='registering')
label(for='confirmPasswordInput', v-once) {{$t('confirmPassword')}}
input#confirmPasswordInput.form-control(type='password', :placeholder='$t("confirmPasswordPlaceholder")', v-model='passwordConfirm')
small.form-text(v-once) {{$t('termsAndAgreement')}}
.text-center
.btn.btn-info(@click='register()', v-if='registering', v-once) {{$t('joinHabitica')}}
.btn.btn-info(@click='login()', v-if='!registering', v-once) {{$t('login')}}
#bottom-background
.seamless_mountains_demo_repeat
.midground_foreground_extended2
</template>
<style lang="scss" scoped>
@import '~client/assets/scss/colors.scss';
.form-wrapper {
background-color: $purple-200;
}
#login-form {
margin: 0 auto;
width: 40em;
padding-top: 5em;
padding-bottom: 22.5em;
position: relative;
z-index: 1;
.gryphon {
width: 63.2px;
height: 69.4px;
}
.habitica-logo {
width: 144px;
height: 31px;
margin-top: 2em;
margin-bottom: 2em;
}
label {
color: $white;
font-weight: bold;
}
input {
margin-bottom: 2em;
border-radius: 2px;
background-color: #432874;
border-color: transparent;
height: 50px;
color: $purple-400;
::-webkit-input-placeholder { /* Chrome/Opera/Safari */
color: $purple-400;
}
::-moz-placeholder { /* Firefox 19+ */
color: $purple-400;
}
:-ms-input-placeholder { /* IE 10+ */
color: $purple-400;
}
:-moz-placeholder { /* Firefox 18- */
color: $purple-400;
}
}
.form-text {
font-size: 14px;
color: $white;
}
.social-button {
width: 100%;
text-align: left;
}
.social-icon {
margin-right: 1em;
width: 13px;
}
}
#top-background {
.seamless_stars_varied_opacity_repeat {
background-image: url('~assets/images/auth/seamless_stars_varied_opacity.png');
background-repeat: repeat-x;
position: absolute;
height: 500px;
width: 1600px;
}
}
#bottom-background {
position: relative;
.seamless_mountains_demo_repeat {
background-image: url('~assets/images/auth/seamless_mountains_demo.png');
background-repeat: repeat-x;
width: 1600px;
height: 500px;
position: absolute;
z-index: 0;
bottom: 0;
}
.midground_foreground_extended2 {
background-image: url('~assets/images/auth/midground_foreground_extended2.png');
position: relative;
width: 1500px;
height: 150px;
}
}
</style>
<script>
import hello from 'hellojs';
import gryphon from 'assets/svg/gryphon.svg';
import habiticaIcon from 'assets/svg/habitica-logo.svg';
import facebookIcon from 'assets/svg/facebook.svg';
import googleIcon from 'assets/svg/google.svg';
export default {
data () {
let data = {
username: '',
email: '',
password: '',
passwordConfirm: '',
registering: true,
};
data.icons = Object.freeze({
gryphon,
habiticaIcon,
facebookIcon,
googleIcon,
});
return data;
},
mounted () {
if (this.$route.path.startsWith('/login')) {
this.registering = false;
}
hello.init({
facebook: '',
// windows: WINDOWS_CLIENT_ID,
google: '',
});
},
methods: {
async register () {
if (this.password !== this.passwordConfirm) {
alert('Passwords must match');
return;
}
// @TODO: implement langauge and invite accepting
// var url = ApiUrl.get() + "/api/v3/user/auth/local/register";
// if (location.search && location.search.indexOf('Invite=') !== -1) { // matches groupInvite and partyInvite
// url += location.search;
// }
//
// if($rootScope.selectedLanguage) {
// var toAppend = url.indexOf('?') !== -1 ? '&' : '?';
// url = url + toAppend + 'lang=' + $rootScope.selectedLanguage.code;
// }
await this.$store.dispatch('auth:register', {
username: this.username,
email: this.email,
password: this.password,
passwordConfirm: this.passwordConfirm,
});
this.$router.go('/tasks');
},
async login () {
await this.$store.dispatch('auth:login', {
username: this.username,
// email: this.email,
password: this.password,
});
this.$router.go('/tasks');
},
async socialAuth (network) {
let auth = await hello(network).login({scope: 'email'});
await this.$store.dispatch('auth:socialAuth', {
auth,
});
this.$router.go('/tasks');
},
},
};
</script>

View file

@ -0,0 +1,121 @@
<template lang="pug">
b-modal#create-party-modal(title="Empty", size='lg', hide-footer=true)
.header-wrap(slot="modal-header")
h2 Image Here
.row
.col-12.text-center
h2(v-once) {{$t('playInPartyTitle')}}
p(v-once) {{$t('playInPartyDescription')}}
.row.grey-row
.col-6.text-center
img
h3(v-once) {{$t('startYourOwnPartyTitle')}}
p(v-once) {{$t('startYourOwnPartyDescription')}}
button.btn.btn-primary(v-once) {{$t('createParty')}}
.col-6
div.text-center
img
h3(v-once) {{$t('wantToJoinPartyTitle')}}
p(v-once) {{$t('wantToJoinPartyDescription')}}
button.btn.btn-primary(v-once, @click='shareUserIdShown = !shareUserIdShown') {{$t('shartUserId')}}
.share-userid-options(v-if="shareUserIdShown")
.option-item(v-once)
.svg-icon(v-html="icons.copy")
| {{$t('copy')}}
.option-item(v-once)
.svg-icon(v-html="icons.greyBadge")
| {{$t('lookingForGroup')}}
.option-item(v-once)
.svg-icon(v-html="icons.qrCode")
| {{$t('qrCode')}}
.option-item(v-once)
.svg-icon.facebook(v-html="icons.facebook")
| {{$t('facebook')}}
.option-item(v-once)
.svg-icon(v-html="icons.twitter")
| {{$t('twitter')}}
</template>
<style lang='scss'>
@import '~client/assets/scss/colors.scss';
.modal-body {
padding-bottom: 0;
padding-top: 0;
}
.grey-row {
background-color: $gray-700;
padding: 2em;
border-radius: 0px 0px 2px 2px;
}
.share-userid-options {
background-color: $white;
border-radius: 2px;
width: 180px;
position: absolute;
top: -8em;
left: 4.8em;
box-shadow: 0 2px 2px 0 rgba(26, 24, 29, 0.16), 0 1px 4px 0 rgba(26, 24, 29, 0.12);
.option-item {
padding: 1em;
.svg-icon {
margin-right: .5em;
}
.facebook svg {
width: 15px;
height: 15px;
}
}
.option-item:hover {
background-color: $header-color;
color: $purple-200;
cursor: pointer;
}
}
</style>
<script>
import bModal from 'bootstrap-vue/lib/components/modal';
import copyIcon from 'assets/svg/copy.svg';
import greyBadgeIcon from 'assets/svg/grey-badge.svg';
import qrCodeIcon from 'assets/svg/qrCode.svg';
import facebookIcon from 'assets/svg/facebook.svg';
import twitterIcon from 'assets/svg/twitter.svg';
export default {
components: {
bModal,
},
data () {
return {
icons: Object.freeze({
copy: copyIcon,
greyBadge: greyBadgeIcon,
qrCode: qrCodeIcon,
facebook: facebookIcon,
twitter: twitterIcon,
}),
shareUserIdShown: false,
};
},
methods: {
createParty () {
// group.loadingParty = true;
//
// if (!group.name) group.name = env.t('possessiveParty', {name: User.user.profile.name});
// Groups.Group.create(group)
// .then(function(response) {
// Analytics.updateUser({'partyID': $scope.group ._id, 'partySize': 1});
// $rootScope.hardRedirect('/#/options/groups/party');
// });
},
},
};
</script>

View file

@ -1,10 +1,10 @@
<template lang="pug">
b-modal#guild-form(:title="title", :hide-footer="true")
b-modal#guild-form(:title="title", :hide-footer="true", size='lg')
form(@submit.stop.prevent="submit")
.form-group
label
strong(v-once) {{$t('name')}}*
b-form-input(type="text", placeholder="$t('newGuildPlaceHolder')", v-model="newGuild.name")
b-form-input(type="text", :placeholder="$t('newGuildPlaceHolder')", v-model="newGuild.name")
.form-group(v-if='newGuild.id')
label
@ -29,7 +29,7 @@
span.custom-control-description(v-once) {{ $t('guildLeaderCantBeMessaged') }}
br
label.custom-control.custom-checkbox
label.custom-control.custom-checkbox(v-if='!creatingParty')
input.custom-control-input(type="checkbox", v-model="newGuild.privateGuild")
span.custom-control-indicator
span.custom-control-description(v-once) {{ $t('privateGuild') }}
@ -37,7 +37,7 @@
.svg-icon(v-html='icons.information')
br
label.custom-control.custom-checkbox
label.custom-control.custom-checkbox(v-if='!creatingParty')
input.custom-control-input(type="checkbox", v-model="newGuild.allowGuildInvationsFromNonMembers")
span.custom-control-indicator
span.custom-control-description(v-once) {{ $t('allowGuildInvationsFromNonMembers') }}
@ -46,14 +46,18 @@
label
strong(v-once) {{$t('description')}}*
div.description-count {{charactersRemaining}} {{ $t('charactersRemaining') }}
b-form-input(type="text", textarea :placeholder="$t('guildDescriptionPlaceHolder')", v-model="newGuild.description")
b-form-input(type="text", textarea :placeholder="creatingParty ? $t('partyDescriptionPlaceHolder') : $t('guildDescriptionPlaceHolder')", v-model="newGuild.description")
.form-group(v-if='newGuild.id')
.form-group(v-if='newGuild.id && !creatingParty')
label
strong(v-once) {{$t('guildInformation')}}*
b-form-input(type="text", textarea :placeholder="$t('guildInformationPlaceHolder')", v-model="newGuild.guildInformation")
b-form-input(type="text", textarea, :placeholder="$t('guildInformationPlaceHolder')", v-model="newGuild.guildInformation")
.form-group(style='position: relative;')
.form-group(v-if='creatingParty && !newGuild.id')
span
toggleSwitch(:label="$t('inviteMembersNow')", v-model='inviteMembers')
.form-group(style='position: relative;', v-if='!creatingParty')
label
strong(v-once) {{$t('categories')}}*
div.category-wrap(@click.prevent="toggleCategorySelect")
@ -70,13 +74,26 @@
span.custom-control-description(v-once) {{ $t(group.label) }}
button.btn.btn-primary(@click.prevent="toggleCategorySelect") {{$t('close')}}
.form-group(v-if='inviteMembers && !newGuild.id')
label
strong(v-once) Invite via Email or User ID
p Invite users via a valid email or 36-digit User ID. If an email isnt registered yet, well invite them to join.
div
div(v-for='(member, index) in membersToInvite')
input(type='text', v-model='member.value')
button(@click.prevent='removeMemberToInvite(index)') Remove
div
input(type='text', placeholder='Email address or User ID', v-model='newMemberToInvite.value')
button(@click.prevent='addMemberToInvite()') Add
.form-group.text-center
div.item-with-icon
div.item-with-icon(v-if='!creatingParty')
.svg-icon(v-html="icons.gem")
span.count 4
button.btn.btn-primary.btn-md(v-if='!newGuild.id', :disabled='!newGuild.name || !newGuild.description') {{ $t('createGuild') }}
button.btn.btn-primary.btn-md(v-if='newGuild.id', :disabled='!newGuild.name || !newGuild.description') {{ $t('updateGuild') }}
.gem-description(v-once) {{ $t('guildGemCostInfo') }}
button.btn.btn-primary.btn-md(v-if='!newGuild.id', :disabled='!newGuild.name || !newGuild.description') {{ creatingParty ? $t('createParty') : $t('createGuild') }}
button.btn.btn-primary.btn-md(v-if='newGuild.id', :disabled='!newGuild.name || !newGuild.description') {{ creatingParty ? $t('updateParty') : $t('updateGuild') }}
.gem-description(v-once, v-if='!creatingParty') {{ $t('guildGemCostInfo') }}
</template>
<style lang="scss" scoped>
@ -101,31 +118,6 @@
margin-top: 1em;
}
.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;
}
.item-with-icon {
display: inline-block;
@ -146,21 +138,6 @@
margin-top: 1em;
}
.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;
}
.icon {
margin-left: .5em;
display: inline-block;
@ -175,6 +152,7 @@ import bFormCheckbox from 'bootstrap-vue/lib/components/form-checkbox';
import bFormSelect from 'bootstrap-vue/lib/components/form-select';
import bTooltip from 'bootstrap-vue/lib/components/tooltip';
import toggleSwitch from 'client/components/ui/toggleSwitch';
import gemIcon from 'assets/svg/gem.svg';
import informationIcon from 'assets/svg/information.svg';
@ -186,6 +164,7 @@ export default {
bFormCheckbox,
bFormSelect,
bTooltip,
toggleSwitch,
},
data () {
let data = {
@ -251,6 +230,13 @@ export default {
],
showCategorySelect: false,
members: ['one', 'two'],
creatingParty: true,
inviteMembers: false,
newMemberToInvite: {
value: '',
type: '',
},
membersToInvite: [],
};
let hashedCategories = {};
@ -282,11 +268,23 @@ export default {
return 500 - this.newGuild.description.length;
},
title () {
if (this.creatingParty) return this.$t('createParty');
if (!this.newGuild.id) return this.$t('createGuild');
return this.$t('updateGuild');
},
},
methods: {
addMemberToInvite () {
// @TODO: determine type
this.membersToInvite.push(this.newMemberToInvite);
this.newMemberToInvite = {
value: '',
type: '',
};
},
removeMemberToInvite (index) {
this.membersToInvite.splice(index, 1);
},
toggleCategorySelect () {
this.showCategorySelect = !this.showCategorySelect;
},
@ -311,7 +309,7 @@ export default {
}
// @TODO: Add proper notifications
if (!confirm(this.$t('confirmGuild'))) return;
if (!this.newGuild.id && !confirm(this.$t('confirmGuild'))) return;
if (!this.newGuild.privateGuild) {
this.newGuild.privacy = 'public';
@ -342,6 +340,8 @@ export default {
privateGuild: true,
allowGuildInvationsFromNonMembers: true,
};
this.$root.$emit('hide::modal', 'guild-form');
},
},
};

View file

@ -1,25 +1,21 @@
<template lang="pug">
// TODO this is necessary until we have a way to wait for data to be loaded from the server
.row(v-if="guild")
.row(v-if="group")
group-form-modal
.clearfix.col-8
.row
.col-6.title-details
h1 {{group.name}}
strong.float-left(v-once) {{$t('groupLeader')}}
span.float-left(v-once, v-if='group.leader.profile') : {{group.leader.profile.name}}
.col-6
.float-left
h2 {{guild.name}}
strong.float-left(v-once) {{$t('groupLeader')}}
span.float-left : {{guild.leader.profile.name}}
.col-6
.float-right
.row.icon-row
.col-6
.svg-icon.shield(v-html="icons.goldGuildBadge")
span.number {{guild.memberCount}}
div(v-once) {{ $t('guildMembers') }}
.col-6
.item-with-icon
.svg-icon.gem(v-html="icons.gem")
span.number {{guild.memberCount}}
div(v-once) {{ $t('guildBank') }}
.row.icon-row
.col-4(v-bind:class="{ 'offset-8': isParty }")
members-modal(:group='group', v-if='isMember')
.col-6(v-if='!isParty')
.item-with-icon
.svg-icon.gem(v-html="icons.gem")
span.number {{group.memberCount}}
div(v-once) {{ $t('guildBank') }}
.row.chat-row
.col-12
h3(v-once) {{ $t('chat') }}
@ -34,7 +30,7 @@
.col-md-2
.svg-icon(v-html="icons.like")
.col-md-10
.card(v-for="msg in guild.chat", :key="msg.id")
.card(v-for="msg in group.chat", :key="msg.id")
.card-block
h3.leader Character name
span 2 hours ago
@ -62,146 +58,336 @@
.col-md-4.sidebar
.guild-background.row
.col-6
p Image here
p(v-if='!isParty') Image here
.col-6
members-modal(:group='guild')
br
button.btn.btn-primary(:class="[isMember ? 'btn-danger' : 'btn-success']") {{ isMember ? $t('leave') : $t('join') }}
br
button.btn.btn-primary(v-once) {{$t('inviteToGuild')}}
br
button.btn.btn-primary(v-once) {{$t('messageGuildLeader')}}
br
button.btn.btn-primary(v-once) {{$t('donateGems')}}
br
button.btn.btn-primary(b-btn, @click="updateGuild", v-once) {{ $t('updateGuild') }}
div
h3(v-once) {{ $t('description') }}
p(v-once) {{ guild.description }}
p Life hacks are tricks, shortcuts, or methods that help increase productivity, efficiency, health, and so on. Generally, they get you to a better state of life. Life hacking is the process of utilizing and implementing these secrets. And, in this guild, we want to help everyone discover these improved ways of doing things.
div
h3(v-once) {{$t('guildInformation')}}
h4 Welcome
p Below are some resources that some members might find useful. Consider checking them out before posting any questions, as they just might help answer some of them! Feel free to share your life hacks in the guild chat, or ask any questions that you might have. Please peruse at your leisure, and remember: this guild is meant to help guide you in the right direction. Only you will know what works best for you.
div
h3 Challenges
.card
h4 Challenge
.row
.col-8
p Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla scelerisque ultrices libero.
.col-4
.row
.col-md-12
span Tag
span 100
.button-container
button.btn.btn-success(class='btn-success', v-if='isLeader') {{ $t('upgrade') }}
.button-container
button.btn.btn-primary(b-btn, @click="updateGuild", v-once, v-if='isLeader') {{ $t('edit') }}
.button-container
button.btn.btn-success(class='btn-success', v-if='!isMember') {{ $t('join') }}
.button-container
button.btn.btn-primary(v-once) {{$t('invite')}}
.button-container
button.btn.btn-primary(v-once, v-if='!isLeader') {{$t('messageGuildLeader')}}
.button-container
button.btn.btn-primary(v-once, v-if='isMember && !isParty') {{$t('donateGems')}}
.section-header
.row
.col-10
h3(v-once) {{ $t('questDetailsTitle') }}
.col-2
.toggle-up(@click="sections.quest = !sections.quest", v-if="sections.quest")
.svg-icon(v-html="icons.upIcon")
.toggle-down(@click="sections.quest = !sections.quest", v-if="!sections.quest")
.svg-icon(v-html="icons.downIcon")
.section(v-if="sections.quest")
.row.no-quest-section(v-if='isParty && !onPendingQuest && !onActiveQuest')
.col-12.text-center
.svg-icon(v-html="icons.questIcon")
h4(v-once) {{ $t('yourNotOnQuest') }}
p(v-once) {{ $t('questDescription') }}
button.btn.btn-secondary(v-once, @click="openStartQuestModal()") {{ $t('startAQuest') }}
owned-quests-modal(:group='this.group')
.row.quest-active-section(v-if='isParty && onPendingQuest && !onActiveQuest')
h2 Pending quest
button.btn.btn-secondary(v-once, @click="questForceStart()") {{ $t('begin') }}
button.btn.btn-secondary(v-once, @click="questCancel()") {{ $t('cancel') }}
.row.quest-active-section(v-if='isParty && !onPendingQuest && onActiveQuest')
.col-12.text-center
.quest-boss(:class="'quest_' + questData.key")
h3(v-once) {{ questData.text() }}
.quest-box.svg-icon(v-html="icons.questBackground")
.collect-info(v-if='questData.collect')
.row(v-for='(value, key) in questData.collect')
.col-2
div(:class="'quest_' + questData.key + '_' + key")
.col-10
strong {{value.text()}}
.collect-progress-bar
strong {{group.quest.progress.collect[key]}} / {{value.count}}
.boss-info(v-if='questData.boss')
.row
.col-6
h4.float-left(v-once) {{ questData.boss.name() }}
.col-6
span.float-right(v-once) {{ $t('participants') }}
.row
.col-12
.boss-health-bar
.row.boss-details
.col-6
span.float-left
| {{group.quest.progress.hp}} / {{questData.boss.hp}}
.col-6
span.float-right 30 pending damage
button.btn.btn-secondary(v-once, @click="questAbort()") {{ $t('abort') }}
.section-header
.row
.col-10
h3(v-once) {{ $t('description') }}
.col-2
.toggle-up(@click="sections.description = !sections.description", v-if="sections.description")
.svg-icon(v-html="icons.upIcon")
.toggle-down(@click="sections.description = !sections.description", v-if="!sections.description")
.svg-icon(v-html="icons.downIcon")
.section(v-if="sections.description")
p(v-once) {{ group.description }}
p Life hacks are tricks, shortcuts, or methods that help increase productivity, efficiency, health, and so on. Generally, they get you to a better state of life. Life hacking is the process of utilizing and implementing these secrets. And, in this guild, we want to help everyone discover these improved ways of doing things.
.section-header
.row
.col-10
h3(v-once) {{ $t('guildInformation') }}
.col-2
.toggle-up(@click="sections.information = !sections.information", v-if="sections.information")
.svg-icon(v-html="icons.upIcon")
.toggle-down(@click="sections.information = !sections.information", v-if="!sections.information")
.svg-icon(v-html="icons.downIcon")
.section(v-if="sections.information")
h4 Welcome
p Below are some resources that some members might find useful. Consider checking them out before posting any questions, as they just might help answer some of them! Feel free to share your life hacks in the guild chat, or ask any questions that you might have. Please peruse at your leisure, and remember: this guild is meant to help guide you in the right direction. Only you will know what works best for you.
.section-header.challenge
.row
.col-10.information-header
h3(v-once)
| {{ $t('challenges') }}
b-tooltip.icon.tooltip-wrapper(:content="$t('privateDescription')")
.svg-icon(v-html='icons.information')
.col-2
.toggle-up(@click="sections.challenges = !sections.challenges", v-if="sections.challenges")
.svg-icon(v-html="icons.upIcon")
.toggle-down(@click="sections.challenges = !sections.challenges", v-if="!sections.challenges")
.svg-icon(v-html="icons.downIcon")
.section(v-if="sections.challenges")
.row.no-quest-section(v-if='!hasChallenges')
.col-12.text-center
.svg-icon(v-html="icons.challengeIcon")
h4(v-once) {{ $t('haveNoChallenges') }}
p(v-once) {{ $t('challengeDescription') }}
button.btn.btn-secondary(v-once) {{ $t('createChallenge') }}
div.text-center
button.btn.btn-primary(class='btn-danger', v-if='isMember') {{ $t('leave') }}
</template>
<style lang="scss" scoped>
@import '~client/assets/scss/colors.scss';
@import '~client/assets/scss/colors.scss';
.sidebar {
background-color: $gray-600;
}
.button-container {
margin-bottom: 1em;
.card {
margin: 2em 0;
padding: 1em;
h3.leader {
color: $purple-200;
button {
width: 100%;
}
}
.text {
.item-with-icon {
border-radius: 2px;
background-color: #ffffff;
box-shadow: 0 2px 2px 0 rgba(26, 24, 29, 0.16), 0 1px 4px 0 rgba(26, 24, 29, 0.12);
padding: 1em;
}
.sidebar {
background-color: $gray-600;
padding-top: 2em;
}
.card {
margin: 2em 0;
padding: 1em;
h3.leader {
color: $purple-200;
}
.text {
font-size: 16px;
line-height: 1.43;
color: $gray-50;
}
}
.guild-background {
background-image: linear-gradient(to bottom, rgba($gray-600, 0), $gray-600);
}
textarea {
height: 150px;
width: 100%;
background-color: $white;
border: solid 1px $gray-400;
font-size: 16px;
font-style: italic;
line-height: 1.43;
color: $gray-50;
color: $gray-300;
padding: .5em;
}
}
.guild-background {
background-image: linear-gradient(to bottom, rgba($gray-600, 0), $gray-600);
height: 300px;
}
textarea {
height: 150px;
width: 100%;
background-color: $white;
border: solid 1px $gray-400;
font-size: 16px;
font-style: italic;
line-height: 1.43;
color: $gray-300;
padding: .5em;
}
.svg-icon.shield, .svg-icon.gem {
width: 40px;
margin-right: 1em;
}
.icon-row {
width: 200px;
margin-top: 3em;
margin-right: 3em;
.number {
font-size: 22px;
font-weight: bold;
}
}
.chat-row {
margin-top: 2em;
.send-chat {
margin-top: -3.5em;
z-index: 10;
position: relative;
.svg-icon.shield, .svg-icon.gem {
width: 40px;
margin-right: 1em;
}
}
.hr {
width: 100%;
height: 20px;
border-bottom: 1px solid $gray-500;
text-align: center;
margin: 2em 0;
}
.title-details {
padding-top: 1em;
padding-left: 1em;
}
.hr-middle {
font-size: 16px;
font-weight: bold;
font-family: 'Roboto Condensed';
line-height: 1.5;
text-align: center;
color: $gray-200;
background-color: $gray-700;
padding: .2em;
margin-top: .2em;
display: inline-block;
width: 100px;
}
.icon-row {
margin-top: 1em;
span.action {
font-size: 14px;
line-height: 1.33;
color: $gray-200;
font-weight: 500;
margin-right: 1em;
}
.number {
font-size: 22px;
font-weight: bold;
}
}
span.action .icon {
margin-right: .3em;
}
.chat-row {
margin-top: 2em;
.send-chat {
margin-top: -3.5em;
z-index: 10;
position: relative;
margin-right: 1em;
}
}
.hr {
width: 100%;
height: 20px;
border-bottom: 1px solid $gray-500;
text-align: center;
margin: 2em 0;
}
.hr-middle {
font-size: 16px;
font-weight: bold;
font-family: 'Roboto Condensed';
line-height: 1.5;
text-align: center;
color: $gray-200;
background-color: $gray-700;
padding: .2em;
margin-top: .2em;
display: inline-block;
width: 100px;
}
span.action {
font-size: 14px;
line-height: 1.33;
color: $gray-200;
font-weight: 500;
margin-right: 1em;
}
span.action .icon {
margin-right: .3em;
}
.no-quest-section {
padding: 2em;
color: $gray-300;
h4 {
color: $gray-300;
}
p {
margin-bottom: 2em;
}
.svg-icon {
height: 30px;
width: 30px;
margin-bottom: 2em;
}
}
.information-header {
h3, .tooltip-wrapper {
display: inline-block;
}
.tooltip-wrapper {
margin-left: 2.2em;
}
}
.quest-active-section {
.quest-box {
height: 100px;
width: 100%;
svg: {
width: 100%;
height: 100%;
}
}
.boss-info, .collect-info {
position: relative;
top: -89px;
left: 15px;
width: 32em;
text-align: left;
}
}
.section-header {
border-top: 1px solid #e1e0e3;
margin-top: 1em;
padding-top: 1em;
}
.section-header.challenge {
border-bottom: 1px solid #e1e0e3;
margin-bottom: 1em;
padding-bottom: 1em;
}
.toggle-up, .toggle-down {
cursor: pointer;
}
.quest-boss {
margin: 0 auto;
}
.boss-health-bar {
width: 80%;
background-color: red;
height: 15px;
margin-bottom: .5em;
}
.collect-progress-bar {
background-color: #24cc8f;
height: 15px;
width: 80%;
}
</style>
<script>
import groupUtilities from 'client/mixins/groupsUtilities';
import { mapState } from 'client/libs/store';
import membersModal from './membersModal';
import ownedQuestsModal from './ownedQuestsModal';
import { TAVERN_ID } from 'common/script/constants';
import quests from 'common/script/content/quests';
import percent from 'common/script/libs/percent';
import groupFormModal from './groupFormModal';
import bCollapse from 'bootstrap-vue/lib/components/collapse';
import bCard from 'bootstrap-vue/lib/components/card';
import bToggle from 'bootstrap-vue/lib/directives/toggle';
import bTooltip from 'bootstrap-vue/lib/components/tooltip';
import deleteIcon from 'assets/svg/delete.svg';
import copyIcon from 'assets/svg/copy.svg';
@ -209,43 +395,85 @@ import likeIcon from 'assets/svg/like.svg';
import likedIcon from 'assets/svg/liked.svg';
import reportIcon from 'assets/svg/report.svg';
import gemIcon from 'assets/svg/gem.svg';
import goldGuildBadgeIcon from 'assets/svg/gold-guild-badge.svg';
import questIcon from 'assets/svg/quest.svg';
import challengeIcon from 'assets/svg/challenge.svg';
import informationIcon from 'assets/svg/information.svg';
import questBackground from 'assets/svg/quest-background-border.svg';
import upIcon from 'assets/svg/up.svg';
import downIcon from 'assets/svg/down.svg';
export default {
mixins: [groupUtilities],
props: ['guildId'],
components: {
membersModal,
ownedQuestsModal,
bCollapse,
bCard,
bTooltip,
groupFormModal,
},
directives: {
bToggle,
},
data () {
return {
guild: null,
group: null,
icons: Object.freeze({
like: likeIcon,
copy: copyIcon,
report: reportIcon,
delete: deleteIcon,
gem: gemIcon,
goldGuildBadge: goldGuildBadgeIcon,
liked: likedIcon,
questIcon,
challengeIcon,
information: informationIcon,
questBackground,
upIcon,
downIcon,
}),
questData: {},
selectedQuest: {},
sections: {
quest: true,
description: true,
information: true,
challenges: true,
},
};
},
computed: {
...mapState({user: 'user.data'}),
isParty () {
return this.$route.path.startsWith('/party');
},
onPendingQuest () {
return Boolean(this.group.quest.key) && !this.group.quest.active;
},
onActiveQuest () {
return this.group.quest.active;
},
isLeader () {
return this.user._id === this.group.leader._id;
},
isMember () {
return this.isMemberOfGroup(this.user, this.guild);
return this.isMemberOfGroup(this.user, this.group);
},
canEditQuest () {
let isQuestLeader = this.group.quest && this.group.quest.leader === this.user._id;
return isQuestLeader;
},
isMemberOfPendingQuest () {
let userid = this.user._id;
let group = this.guild;
let group = this.group;
if (!group.quest || !group.quest.members) return false;
if (group.quest.active) return false; // quest is started, not pending
return userid in group.quest.members && group.quest.members[userid] !== false;
},
isMemberOfRunningQuest () {
let userid = this.user._id;
let group = this.guild;
let group = this.group;
if (!group.quest || !group.quest.members) return false;
if (!group.quest.active) return false; // quest is pending, not started
return group.quest.members[userid];
@ -265,8 +493,28 @@ export default {
let userIsManager = Boolean(group.managers[userId]);
return leader || userIsManager;
},
hasChallenges () {
if (!this.group.challenges) return false;
return this.group.challenges.length === 0;
},
bossHpPercent () {
return percent(this.group.quest.progress.hp, this.questData.boss.hp);
},
},
created () {
if (this.isParty) {
this.groupId = 'party';
// @TODO: Set up from old client. Decide what we need and what we don't
// Check Desktop notifs
// Mark Chat seen
// Load members
// Load invites
// Load challenges
// Load group tasks for group plan
// Load approvals for group plan
} else if (this.$route.path.startsWith('/guilds/tavern')) {
this.groupId = TAVERN_ID;
}
this.fetchGuild();
},
watch: {
@ -275,23 +523,28 @@ export default {
},
methods: {
updateGuild () {
this.$store.state.editingGroup = this.guild;
this.$store.state.editingGroup = this.group;
this.$root.$emit('show::modal', 'guild-form');
},
async fetchGuild () {
this.guild = await this.$store.dispatch('guilds:getGroup', {groupId: this.guildId});
this.guild.chat = [
{
text: '@CharacterName Vestibulum ultricies, lorem non bibendum consequat, nisl lacus semper nulla, hendrerit dignissim ipsum erat eu odio. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla at aliquet urna. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Nulla non est ut nisl interdum tincidunt in eu dui. Proin condimentum a.',
},
];
let group = await this.$store.dispatch('guilds:getGroup', {groupId: this.groupId});
if (this.isParty) {
this.$store.party = group;
this.group = this.$store.party;
this.checkForAchievements();
this.questData = quests.quests[this.group.quest.key];
return;
}
this.group = group;
},
deleteAllMessages () {
if (confirm(this.$t('confirmDeleteAllMessages'))) {
// User.clearPMs();
}
},
openStartQuestModal () {
this.$root.$emit('show::modal', 'owned-quests-modal');
},
// inviteOrStartParty (group) {
// Analytics.track({'hitType':'event','eventCategory':'button','eventAction':'click','eventLabel':'Invite Friends'});
@ -314,6 +567,114 @@ export default {
// },
// });
// },
checkForAchievements () {
// Checks if user's party has reached 2 players for the first time.
if (!this.user.achievements.partyUp && this.group.memberCount >= 2) {
// @TODO
// User.set({'achievements.partyUp':true});
// Achievement.displayAchievement('partyUp');
}
// Checks if user's party has reached 4 players for the first time.
if (!this.user.achievements.partyOn && this.group.memberCount >= 4) {
// @TODO
// User.set({'achievements.partyOn':true});
// Achievement.displayAchievement('partyOn');
}
},
// @TODO: This should be moved to notifications component
async join () {
if (this.group.cancelledPlan && !confirm(this.$t('aboutToJoinCancelledGroupPlan'))) {
return;
}
await this.$store.dispatch('guilds:join', {groupId: this.group._id});
// @TODO: Implement
// User.sync();
// Analytics.updateUser({'partyID': party.id});
// $rootScope.hardRedirect('/#/options/groups/party');
},
clickLeave () {
// Analytics.track({'hitType':'event','eventCategory':'button','eventAction':'click','eventLabel':'Leave Party'});
// @TODO: Get challenges and ask to keep or remove
let keep = true;
this.leave(keep);
},
async leave (keepTasks) {
let keepChallenges = 'remain-in-challenges';
await this.$store.dispatch('guilds:leave', {
groupId: this.group._id,
keep: keepTasks,
keepChallenges,
});
// @TODO: Implement
// Analytics.updateUser({'partySize':null,'partyID':null});
// User.sync().then(function () {
// $rootScope.hardRedirect('/#/options/groups/party');
// });
},
// @TODO: Move to notificatin component
async leaveOldPartyAndJoinNewParty () {
let newPartyName = 'where does this come from';
if (!confirm(`Are you sure you want to delete your party and join${newPartyName}?`)) return;
let keepChallenges = 'remain-in-challenges';
await this.$store.dispatch('guilds:leave', {
groupId: this.group._id,
keep: false,
keepChallenges,
});
await this.$store.dispatch('guilds:join', {groupId: this.group._id});
},
// @TODO: Move to notificatin component
async reject () {
await this.$store.dispatch('guilds:rejectInvite', {groupId: this.group._id});
// User.sync();
},
clickStartQuest () {
// Analytics.track({'hitType':'event','eventCategory':'button','eventAction':'click','eventLabel':'Start a Quest'});
let hasQuests = find(this.user.items.quests, (quest) => {
return quest > 0;
});
if (hasQuests) {
this.$root.$emit('show::modal', 'owned-quests-modal');
return;
}
// $rootScope.$state.go('options.inventory.quests');
},
async questCancel () {
if (!confirm(this.$t('sureCancel'))) return;
let quest = await this.$store.dispatch('quests:sendAction', {groupId: this.group._id, action: 'quests/cancel'});
this.group.quest = quest;
},
async questAbort () {
if (!confirm(this.$t('sureAbort'))) return;
if (!confirm(this.$t('doubleSureAbort'))) return;
let quest = await this.$store.dispatch('quests:sendAction', {groupId: this.group._id, action: 'quests/abort'});
this.group.quest = quest;
},
async questLeave () {
if (!confirm(this.$t('sureLeave'))) return;
let quest = await this.$store.dispatch('quests:sendAction', {groupId: this.group._id, action: 'quests/leave'});
this.group.quest = quest;
},
async questAccept () {
let quest = await this.$store.dispatch('quests:sendAction', {groupId: this.group._id, action: 'quests/accept'});
this.group.quest = quest;
},
async questForceStart () {
let quest = await this.$store.dispatch('quests:sendAction', {groupId: this.group._id, action: 'quests/force-start'});
this.group.quest = quest;
},
// @TODO: Move to notificaitons component?
async questReject () {
let quest = await this.$store.dispatch('quests:sendAction', {groupId: this.group._id, action: 'quests/reject'});
this.group.quest = quest;
},
},
};
</script>

View file

@ -1,6 +1,9 @@
<template lang="pug">
div
button.btn.btn-primary(b-btn, @click="$root.$emit('show::modal','members-modal')") {{ $t('viewMembers') }}
.item-with-icon(@click="$root.$emit('show::modal','members-modal')")
.svg-icon.shield(v-html="icons.goldGuildBadgeIcon")
span.number {{group.memberCount}}
div(v-once) {{ $t('members') }}
b-modal#members-modal(:title="$t('createGuild')", size='lg')
.header-wrap(slot="modal-header")
@ -19,24 +22,25 @@ div
b-dropdown-item(v-for='sortOption in sortOptions', @click='sort(sortOption.value)', :key='sortOption.value') {{sortOption.text}}
.row(v-for='member in members', :key='member', )
.col-8.offset-1
member-details(:user='member')
member-details(:member='member')
.col-3.actions
b-dropdown(:text="$t('sort')", right=true)
b-dropdown-item(@click='sort(option.value)')
img.action-icon(src='~assets/members/remove.svg')
.svg-icon(v-html="icons.removeIcon")
| {{$t('removeMember')}}
b-dropdown-item(@click='sort(option.value)')
img.action-icon(src='~assets/members/message.svg')
.svg-icon(v-html="icons.messageIcon")
| {{$t('sendMessage')}}
b-dropdown-item(@click='sort(option.value)')
img.action-icon(src='~assets/members/star.svg')
.svg-icon(v-html="icons.starIcon")
| {{$t('promoteToLeader')}}
b-dropdown-item(@click='sort(option.value)')
img.action-icon(src='~assets/members/star.svg')
.svg-icon(v-html="icons.starIcon")
| {{$t('addManager')}}
b-dropdown-item(@click='sort(option.value)')
img.action-icon(src='~assets/members/remove.svg')
.svg-icon(v-html="icons.removeIcon")
| {{$t('removeManager2')}}
.row-fluid.gradient
b-modal#remove-member(:title="$t('confirmRemoveMember')")
button(@click='confirmRemoveMember(member)', v-once) {{$t('remove')}}
@ -45,7 +49,7 @@ div
button(@click='confirmRemoveMember(member)', v-once) {{$t('remove')}}
</template>
<style lang='scss'>
<style lang='scss' scoped>
header {
background-color: #edecee;
border-radius: 4px 4px 0 0;
@ -66,6 +70,49 @@ div
margin-right: 1em;
}
}
#members-modal_modal_body {
padding: 0;
.col-8 {
margin-left: 0;
}
.member-details {
margin: 0;
}
.member-stats {
width: 382px;
height: 147px;
}
.gradient {
background-image: linear-gradient(to bottom, rgba(255, 255, 255, 0), #ffffff);
height: 200px;
width: 100%;
position: absolute;
bottom: 0px;
}
}
.item-with-icon {
border-radius: 2px;
background-color: #ffffff;
box-shadow: 0 2px 2px 0 rgba(26, 24, 29, 0.16), 0 1px 4px 0 rgba(26, 24, 29, 0.12);
padding: 1em;
text-align: center;
}
.svg-icon.shield, .svg-icon.gem {
width: 40px;
margin-right: 1em;
}
.number {
font-size: 22px;
font-weight: bold;
}
</style>
<script>
@ -74,6 +121,10 @@ import bDropdown from 'bootstrap-vue/lib/components/dropdown';
import bDropdownItem from 'bootstrap-vue/lib/components/dropdown-item';
import MemberDetails from '../memberDetails';
import removeIcon from 'assets/members/remove.svg';
import messageIcon from 'assets/members/message.svg';
import starIcon from 'assets/members/star.svg';
import goldGuildBadgeIcon from 'assets/svg/gold-guild-badge.svg';
export default {
props: ['group'],
@ -109,6 +160,12 @@ export default {
},
],
searchTerm: '',
icons: Object.freeze({
removeIcon,
messageIcon,
starIcon,
goldGuildBadgeIcon,
}),
};
},
methods: {

View file

@ -0,0 +1,54 @@
<template lang="pug">
div
b-modal#owned-quests-modal(title="Which quest do you want to start?", size='md', hide-footer=true)
.row.content
.quest(v-for='(value, key, index) in user.items.quests', :class="'inventory_quest_scroll_' + key", @click='selectQuest(key)')
button.btn.btn-primary(@click='confirm()') Confirm
start-quest-modal(:group='group', :selectedQuest='selectedQuest')
</template>
<style lang='scss' scoped>
@import '~client/assets/scss/colors.scss';
.content {
padding: 2em;
.quest {
margin-right: 1em;
margin-bottom: 1em;
display: inline-block;
}
}
</style>
<script>
import { mapState } from 'client/libs/store';
import bModal from 'bootstrap-vue/lib/components/modal';
import startQuestModal from './startQuestModal';
export default {
props: ['group'],
components: {
bModal,
startQuestModal,
},
data () {
return {
selectedQuest: {},
};
},
computed: {
...mapState({user: 'user.data'}),
},
methods: {
selectQuest (quest) {
this.selectedQuest = quest;
},
confirm () {
this.$root.$emit('show::modal', 'start-quest-modal');
this.$root.$emit('hide::modal', 'owned-quests-modal');
},
},
};
</script>

View file

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

View file

@ -0,0 +1,156 @@
<template lang="pug">
b-modal#start-quest-modal(title="Empty", size='md', hide-footer=true, v-if='questData')
.quest-image(:class="'quest_' + questData.key")
h2 {{questData.text()}}
//- span by: Keith Holliday @TODO: Add author
p {{questData.notes()}}
div.quest-details
div(v-if=' questData.collect')
Strong {{$t('collect')}}: &nbsp;
span(v-for="(value, key, index) in questData.collect")
| {{$t('collectionItems', { number: questData.collect[key].count, items: questData.collect[key].text() })}}
div
Strong {{$t('collect')}}: &nbsp;
span
.svg-icon(v-html="icons.difficultyStarIcon")
div
button.btn.btn-primary(@click='questInit()') {{$t('inviteToPartyOrQuest')}}
div
p {{$t('inviteInformation')}}
.side-panel
h4.text-center {{$t('rewards')}}
.box
.svg-icon.rewards-icon(v-html="icons.starIcon")
strong {{questData.drop.exp}} {{$t('experience')}}
.box
.svg-icon.rewards-icon(v-html="icons.goldIcon")
strong {{questData.drop.gp}} {{$t('gold')}}
h4.text-center(v-if='questData.drop.items') {{$t('questOwnerRewards')}}
.box(v-for='item in questData.drop.items')
.rewards-icon(v-if='item.type === "quest"', :class="'quest_' + item.key")
.drop-rewards-icon(v-if='item.type === "gear"', :class="'shop_' + item.key")
strong.quest-reward-text {{item.text()}}
</template>
<style lang='scss' scoped>
@import '~client/assets/scss/colors.scss';
header {
background-color: $white !important;
border: none !important;
h5 {
text-indent: -99999px;
}
}
.quest-image {
margin: 0 auto;
margin-bottom: 1em;
}
.quest-details {
margin: 0 auto;
text-align: left;
width: 180px;
}
.btn-primary {
margin: 1em 0;
}
.side-panel {
background: #edecee;
position: absolute;
height: 460px;
width: 320px;
top: -1.8em;
left: 35em;
z-index: -1;
padding-top: 1em;
border-radius: 4px;
.drop-rewards-icon {
width: 35px;
height: 35px;
float: left;
}
.rewards-icon {
float: left;
width: 30px;
height: 30px;
svg {
width: 30px;
height: 30px;
}
}
.quest-reward-text {
font-size: 12px;
}
.box {
width: 220px;
height: 64px;
border-radius: 2px;
background-color: #ffffff;
margin: 0 auto;
margin-bottom: 1em;
padding: 1em;
}
}
</style>
<script>
import bModal from 'bootstrap-vue/lib/components/modal';
import quests from 'common/script/content/quests';
import copyIcon from 'assets/svg/copy.svg';
import greyBadgeIcon from 'assets/svg/grey-badge.svg';
import qrCodeIcon from 'assets/svg/qrCode.svg';
import facebookIcon from 'assets/svg/facebook.svg';
import twitterIcon from 'assets/svg/twitter.svg';
import starIcon from 'assets/svg/star.svg';
import goldIcon from 'assets/svg/gold.svg';
import difficultyStarIcon from 'assets/svg/difficulty-star.svg';
export default {
props: ['group', 'selectedQuest'],
components: {
bModal,
},
data () {
return {
icons: Object.freeze({
copy: copyIcon,
greyBadge: greyBadgeIcon,
qrCode: qrCodeIcon,
facebook: facebookIcon,
twitter: twitterIcon,
starIcon,
goldIcon,
difficultyStarIcon,
}),
shareUserIdShown: false,
};
},
computed: {
questData () {
return quests.quests[this.selectedQuest];
},
},
methods: {
async questInit () {
let key = this.selectedQuest;
// Analytics.updateUser({'partyID': party._id, 'partySize': party.memberCount});
let response = await this.$store.dispatch('guilds:inviteToQuest', {groupId: this.group._id, key});
let quest = response.data.data;
this.$store.party.quest = quest;
this.$root.$emit('hide::modal', 'start-quest-modal');
},
},
};
</script>

View file

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

View file

@ -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 }}
</template>
<script>
import bPopover from 'bootstrap-vue/lib/components/popover';
import { mapState } from 'client/libs/store';
export default {
components: {
@ -46,16 +49,22 @@ export default {
type: String,
default: 'bottom',
},
},
computed: {
...mapState({
ATTRIBUTES: 'constants.ATTRIBUTES',
}),
draggable: {
type: Boolean,
default: false,
},
},
methods: {
click () {
this.$emit('click', this.item);
},
onDrag (ev) {
if (this.draggable) {
this.$emit('onDrag', ev);
} else {
ev.preventDefault();
}
},
},
};
</script>

View file

@ -1,131 +0,0 @@
<template lang="pug">
.row
.col-2.standard-sidebar
.form-group
input.form-control(type="text", :placeholder="$t('search')")
.form
h3(v-once) {{ $t('filter') }}
.form-group
.form-check
label.form-check-label(v-once)
input.form-check-input(type="checkbox")
strong {{ $t('pets') }}
.form-check.nested-field
label.form-check-label(v-once)
input.form-check-input(type="checkbox")
span {{ $t('hatchingPotions') }}
.form-check.nested-field
label.form-check-label(v-once)
input.form-check-input(type="checkbox")
span {{ $t('quest') }}
.form-check.nested-field
label.form-check-label(v-once)
input.form-check-input(type="checkbox")
span {{ $t('special') }}
.form-group
.form-check
label.form-check-label(v-once)
input.form-check-input(type="checkbox")
strong {{ $t('mounts') }}
.form-check.nested-field
label.form-check-label(v-once)
input.form-check-input(type="checkbox")
span {{ $t('hatchingPotions') }}
.form-check.nested-field
label.form-check-label(v-once)
input.form-check-input(type="checkbox")
span {{ $t('quest') }}
.form-check.nested-field
label.form-check-label(v-once)
input.form-check-input(type="checkbox")
span {{ $t('special') }}
.col-10.standard-page
h4 Pets
.inventory-item-container(v-for="pet in dropPets")
.PixelPaw
.btn.btn-secondary.d-block(@click="open.dropPets = !open.dropPets") {{ open.dropPets ? 'Close' : 'Open' }}
h2 Magic Potions Pets
.inventory-item-container(v-for="pet in magicPets")
.PixelPaw
.btn.btn-secondary.d-block(@click="open.magicPets = !open.magicPets") {{ open.magicPets ? 'Close' : 'Open' }}
h2 Quest Pets
.inventory-item-container(v-for="pet in questPets")
.PixelPaw
.btn.btn-secondary.d-block(@click="open.questPets = !open.questPets") {{ open.questPets ? 'Close' : 'Open' }}
h2 Rare Pets
.inventory-item-container(v-for="pet in rarePets")
.PixelPaw
.btn.btn-secondary.d-block(@click="open.rarePets = !open.rarePets") {{ open.rarePets ? 'Close' : 'Open' }}
h2 Mounts
h2 Quest Mounts
h2 Rare Mounts
</template>
<style lang="scss">
.inventory-item-container {
padding: 20px;
border: 1px solid;
display: inline-block;
}
</style>
<script>
import { mapState } from 'client/libs/store';
import each from 'lodash/each';
// TODO Normalize special pets and mounts
// import Store from 'client/store';
// import deepFreeze from 'client/libs/deepFreeze';
// const specialMounts =
export default {
data () {
return {
open: {
dropPets: false,
magicPets: false,
questPets: false,
rarePets: false,
},
};
},
computed: {
...mapState(['content']),
dropPets () {
return this.listAnimals('pet', this.content.dropEggs, this.content.dropHatchingPotions, this.open.dropPets);
},
magicPets () {
return this.listAnimals('pet', this.content.dropEggs, this.content.premiumHatchingPotions, this.open.magicPets);
},
questPets () {
return this.listAnimals('pet', this.content.questEggs, this.content.dropHatchingPotions, this.open.questPets);
},
rarePets () {
return this.listAnimals('pet', this.content.dropEggs, this.content.dropHatchingPotions, this.open.rarePets);
},
},
methods: {
listAnimals (type, eggSource, potionSource, isOpen = false) {
let animals = [];
let iteration = 0;
each(eggSource, (egg) => {
if (iteration === 1 && !isOpen) return false;
iteration++;
each(potionSource, (potion) => {
let animalKey = `${egg.key}-${potion.key}`;
animals.push(this.content[`${type}Info`][animalKey].text());
});
});
return animals;
},
},
};
</script>

View file

@ -0,0 +1,33 @@
<template lang="pug">
span.badge.badge-pill.badge-item.badge-count(
v-if="show",
) {{ count }}
</template>
<style lang="scss">
@import '~client/assets/scss/colors.scss';
.badge-count {
right: -9px;
color: $white;
background: $orange-100;
padding: 4.5px 6px;
min-width: 24px;
height: 24px;
box-shadow: 0 1px 1px 0 rgba($black, 0.12);
}
</style>
<script>
export default {
props: {
show: {
type: Boolean,
},
count: {
type: Number,
},
},
};
</script>

View file

@ -0,0 +1,174 @@
<template lang="pug">
div.slider-root(
v-bind:class="{'scrollButtonsVisible': scrollButtonsVisible}",
)
div.slider-button-area.left-button(
v-if="scrollButtonsVisible",
@mousedown.left="lastPage"
)
a.slider-button
.svg-icon(v-html="icons.previous")
div.slider-button-area.right-button(
v-if="scrollButtonsVisible",
@mousedown.left="nextPage"
)
a.slider-button
.svg-icon(v-html="icons.next")
// 120 = width of the left/right buttons
div.sliding-content(v-resize="500", @resized="currentWidth = $event.width - 120")
.items.items-one-line
template(v-for="item in pages[currentPage]")
div.vertical-divider(v-if="item.ofNextPage")
slot(
name="item",
:item="item",
)
</template>
<style lang="scss">
@import '~client/assets/scss/colors.scss';
$buttonAreaWidth: 60;
.slider-root {
position: relative;
}
.slider-button {
width: 40px;
height: 40px;
border-radius: 50%;
background-color: #ffffff;
box-shadow: 0 2px 2px 0 rgba($black, 0.16), 0 1px 4px 0 rgba($black, 0.12);
position: absolute;
top: calc((100% - 40px) / 2);
.svg-icon {
color: #a5a1ac;
margin: auto 0;
position: absolute;
top: calc((100% - 12px) / 2);
width: 8px;
height: 16px;
right: 16px;
}
}
.scrollButtonsVisible {
.sliding-content {
overflow: hidden;
}
}
.slider-button-area {
width: $buttonAreaWidth+px;
height: 100%;
position: absolute;
z-index: 2;
&.left-button {
left: 0;
}
&.right-button {
right: 0;
}
}
.sliding-content .items {
padding-top: 10px;
margin-left: $buttonAreaWidth+ px;
& > div:last-of-type {
margin-right: $buttonAreaWidth + 20px;
}
}
.vertical-divider {
height: 92px;
width: 1px;
background: #34313a;
margin-bottom: 8px;
}
</style>
<script>
import previous from 'assets/svg/previous.svg';
import next from 'assets/svg/next.svg';
import ResizeDirective from 'client/directives/resize.directive';
import _chunk from 'lodash/chunk';
export default {
directives: {
resize: ResizeDirective,
},
data () {
return {
icons: Object.freeze({
previous,
next,
}),
currentWidth: 0,
currentPage: 0,
};
},
computed: {
pages () {
return _chunk(this.items, this.itemsPerPage() - 1).map((content, index, array) => {
let resultData = [...content];
if (array[index + 1]) {
resultData.push({
...array[index + 1][0],
ofNextPage: true,
});
}
return resultData;
});
},
},
methods: {
lastPage () {
if (this.currentPage > 0) {
this.currentPage--;
} else {
this.currentPage = this.pages.length - 1;
}
},
nextPage () {
if (this.currentPage < this.pages.length - 1) {
this.currentPage++;
} else {
this.currentPage = 0;
}
},
itemsPerPage () {
return Math.floor(this.currentWidth / (this.itemWidth + this.itemMargin));
},
},
props: {
scrollButtonsVisible: {
type: Boolean,
default: true,
},
items: {
type: Array,
},
itemWidth: {
type: Number,
},
itemMargin: {
type: Number,
},
},
};
</script>

View file

@ -0,0 +1,48 @@
<template lang="pug">
b-popover(
:triggers="['hover']",
:placement="'top'",
)
span(slot="content")
h4.popover-content-title {{ item.text() }}
div.popover-content-text(v-html="item.notes()")
.item-wrapper
.item
countBadge(
:show="true",
:count="itemCount"
)
span.item-content(
:class="'Pet_Food_'+item.key",
v-drag.food="item.key"
)
</template>
<script>
import bPopover from 'bootstrap-vue/lib/components/popover';
import DragDropDirective from 'client/directives/dragdrop.directive';
import CountBadge from './countBadge';
export default {
components: {
bPopover,
CountBadge,
},
directives: {
drag: DragDropDirective,
},
props: {
item: {
type: Object,
},
itemCount: {
type: Number,
},
itemContentClass: {
type: String,
},
},
};
</script>

View file

@ -0,0 +1,721 @@
<template lang="pug">
.row.stable
.standard-sidebar
div
b-popover(
:triggers="['hover']",
:placement="'right'"
)
span(slot="content")
h4.popover-content-title(v-once) {{ $t('mattBoch') }}
.popover-content-text(v-once) {{ $t('mattBochText1') }}
div.npc_matt
.form-group
input.form-control.input-search(type="text", v-model="searchText", :placeholder="$t('search')")
.form
h2(v-once) {{ $t('filter') }}
h3(v-once) {{ $t('pets') }}
.form-group
.form-check(
v-for="petGroup in petGroups",
v-if="viewOptions[petGroup.key].animalCount != 0",
:key="petGroup.key"
)
label.custom-control.custom-checkbox
input.custom-control-input(type="checkbox", v-model="viewOptions[petGroup.key].selected")
span.custom-control-indicator
span.custom-control-description(v-once) {{ petGroup.label }}
h3(v-once) {{ $t('mounts') }}
.form-group
.form-check(
v-for="mountGroup in mountGroups",
v-if="viewOptions[mountGroup.key].animalCount != 0",
:key="mountGroup.key"
)
label.custom-control.custom-checkbox
input.custom-control-input(type="checkbox", v-model="viewOptions[mountGroup.key].selected")
span.custom-control-indicator
span.custom-control-description(v-once) {{ mountGroup.label }}
div.form-group.clearfix
h3.float-left Hide Missing
toggle-switch.float-right.hideMissing(
:label="''",
:checked="hideMissing",
@change="updateHideMissing"
)
.standard-page(v-resize="500", @resized="availableContentWidth = $event.width - 48")
.clearfix
h1.float-left.mb-0.page-header(v-once) {{ $t('stable') }}
div.float-right
span.dropdown-label {{ $t('sortBy') }}
b-dropdown(:text="$t(selectedSortBy)", right=true)
b-dropdown-item(
v-for="sort in sortByItems",
@click="selectedSortBy = sort",
:active="selectedSortBy === sort",
:key="sort"
) {{ $t(sort) }}
h2
| {{ $t('pets') }}
|
span.badge.badge-pill.badge-default {{countOwnedAnimals(petGroups[0], 'pet')}}
div(
v-for="petGroup in petGroups",
v-if="viewOptions[petGroup.key].selected",
:key="petGroup.key"
)
h4(v-if="viewOptions[petGroup.key].animalCount != 0") {{ petGroup.label }}
div.items
div(
v-for="pet in pets(petGroup, viewOptions[petGroup.key].open, hideMissing, selectedSortBy, searchTextThrottled, availableContentWidth)",
:key="pet.key",
v-drag.drop.food="pet.key",
@dragover="onDragOver($event, pet)",
@dropped="onDrop($event, pet)",
)
petItem(
:item="pet",
:itemContentClass="getPetItemClass(pet)",
:popoverPosition="'top'",
:progress="pet.progress",
:emptyItem="!pet.isOwned()",
:showPopover="pet.isOwned() || pet.isHatchable()",
@hatchPet="hatchPet",
)
span(slot="popoverContent")
div(v-if="pet.isOwned()")
h4.popover-content-title {{ pet.name }}
div.hatchablePopover(v-else-if="pet.isHatchable()")
h4.popover-content-title {{ pet.name }}
div.popover-content-text(v-html="$t('haveHatchablePet', { potion: pet.potionName, egg: pet.eggName })")
div.potionEggGroup
div.potionEggBackground
div(:class="'Pet_HatchingPotion_'+pet.potionKey")
div.potionEggBackground
div(:class="'Pet_Egg_'+pet.eggKey")
template(slot="itemBadge", scope="ctx")
starBadge(
:selected="ctx.item.key === currentPet",
:show="ctx.item.isOwned()",
@click="selectPet(ctx.item)",
)
.btn.btn-show-more(
@click="viewOptions[petGroup.key].open = !viewOptions[petGroup.key].open",
v-if="viewOptions[petGroup.key].animalCount != 0"
) {{ $t(viewOptions[petGroup.key].open ? 'showLessAnimals' : 'showAllAnimals', { color: petGroup.label, type: $t('pets')}) }}
h2
| {{ $t('mounts') }}
|
span.badge.badge-pill.badge-default {{countOwnedAnimals(mountGroups[0], 'mount')}}
div(
v-for="mountGroup in mountGroups",
v-if="viewOptions[mountGroup.key].selected",
:key="mountGroup.key"
)
h4(v-if="viewOptions[mountGroup.key].animalCount != 0") {{ mountGroup.label }}
div.items
item(
v-for="mount in mounts(mountGroup, viewOptions[mountGroup.key].open, hideMissing, selectedSortBy, searchTextThrottled, availableContentWidth)",
:item="mount",
:itemContentClass="mount.isOwned() ? ('Mount_Icon_' + mount.key) : 'PixelPaw greyedOut'",
:key="mount.key",
:popoverPosition="'top'"
)
span(slot="popoverContent")
h4.popover-content-title {{ mount.name }}
template(slot="itemBadge", scope="ctx")
starBadge(
:selected="ctx.item.key === currentMount",
:show="mount.isOwned()",
@click="selectMount(ctx.item)",
)
.btn.btn-show-more(
@click="viewOptions[mountGroup.key].open = !viewOptions[mountGroup.key].open",
v-if="viewOptions[mountGroup.key].animalCount != 0"
) {{ $t(viewOptions[mountGroup.key].open ? 'showLessAnimals' : 'showAllAnimals', { color: mountGroup.label, type: $t('mounts')}) }}
drawer(
:title="$t('quickInventory')",
:errorMessage="(!hasDrawerTabItems(selectedDrawerTab)) ? $t('noFoodAvailable') : null"
)
div(slot="drawer-header")
.drawer-tab-container
.drawer-tab.text-right
a.drawer-tab-text(
@click="selectedDrawerTab = 0",
:class="{'drawer-tab-text-active': selectedDrawerTab === 0}",
) {{ drawerTabs[0].label }}
.clearfix
.drawer-tab.float-left
a.drawer-tab-text(
@click="selectedDrawerTab = 1",
:class="{ 'drawer-tab-text-active': selectedDrawerTab === 1 }",
) {{ drawerTabs[1].label }}
b-popover(
:triggers="['hover']",
:placement="'top'"
)
span(slot="content")
.popover-content-text Test Popover
div.float-right What does my pet like to eat?
drawer-slider(
:items="drawerTabs[selectedDrawerTab].items",
:scrollButtonsVisible="hasDrawerTabItems(selectedDrawerTab)",
slot="drawer-slider",
:itemWidth=94,
:itemMargin=24,
)
template(slot="item", scope="ctx")
foodItem(
:item="ctx.item",
:itemCount="userItems.food[ctx.item.key]",
)
b-modal#welcome-modal(
:ok-only="true",
:ok-title="$t('gotIt')",
:visible="!hideDialog",
:hide-header="true"
)
div.content
div.npc_matt
h1.page-header(v-once) {{ $t('welcomeStable') }}
div.content-text(v-once) {{ $t('welcomeStableText') }}
</template>
<style lang="scss">
@import '~client/assets/scss/colors.scss';
.inventory-item-container {
padding: 20px;
border: 1px solid;
display: inline-block;
}
.stable .item .item-content.Pet {
position: absolute;
top: -28px;
}
.toggle-switch-container.hideMissing {
margin-top: 0;
}
.hatchablePopover {
width: 180px
}
.potionEggGroup {
margin: 0 auto;
}
.potionEggBackground {
display: inline-flex;
align-items: center;
width: 64px;
height: 64px;
border-radius: 2px;
background-color: #4e4a57;
&:first-child {
margin-right: 24px;
}
& div {
margin: 0 auto;
}
}
.GreyedOut {
opacity: 0.3;
}
.item.item-empty {
width: 94px;
height: 92px;
border-radius: 2px;
background-color: #edecee;
}
.npc_matt {
margin-bottom: 17px;
}
.stable {
.standard-page {
flex: 1;
}
.drawer-container {
// 3% padding + 252px sidebar width
left: calc(3% + 252px) !important;
}
}
.drawer-slider .items {
height: 114px;
}
div#welcome-modal {
display: flex;
justify-content: center;
flex-direction: column;
header, footer {
border: 0;
}
.npc_matt {
margin: 0 auto 21px auto;
}
.content {
text-align: center;
// the modal already has 15px padding
margin-left: 33px;
margin-right: 33px;
margin-top: 25px;
}
.content-text {
font-family: 'Roboto', sans-serif;
font-size: 14px;
font-weight: normal;
line-height: 1.43;
width: 400px;
}
.modal-footer {
justify-content: center;
}
}
.modal-backdrop.fade.show {
background-color: $purple-50;
opacity: 0.9;
}
</style>
<script>
import {mapState} from 'client/libs/store';
import bDropdown from 'bootstrap-vue/lib/components/dropdown';
import bDropdownItem from 'bootstrap-vue/lib/components/dropdown-item';
import bPopover from 'bootstrap-vue/lib/components/popover';
import bModal from 'bootstrap-vue/lib/components/modal';
import _each from 'lodash/each';
import _sortBy from 'lodash/sortBy';
import _take from 'lodash/take';
import _filter from 'lodash/filter';
import _drop from 'lodash/drop';
import _flatMap from 'lodash/flatMap';
import _throttle from 'lodash/throttle';
import Item from '../item';
import PetItem from './petItem';
import FoodItem from './foodItem';
import Drawer from 'client/components/inventory/drawer';
import toggleSwitch from 'client/components/ui/toggleSwitch';
import StarBadge from 'client/components/inventory/starBadge';
import CountBadge from './countBadge';
import DrawerSlider from './drawerSlider';
import ResizeDirective from 'client/directives/resize.directive';
import DragDropDirective from 'client/directives/dragdrop.directive';
// TODO Normalize special pets and mounts
// import Store from 'client/store';
// import deepFreeze from 'client/libs/deepFreeze';
// const specialMounts =
export default {
components: {
PetItem,
Item,
FoodItem,
Drawer,
bDropdown,
bDropdownItem,
bPopover,
bModal,
toggleSwitch,
StarBadge,
CountBadge,
DrawerSlider,
},
directives: {
resize: ResizeDirective,
drag: DragDropDirective,
},
data () {
return {
viewOptions: {},
hideMissing: false,
searchText: null,
searchTextThrottled: '',
// sort has the translation-keys as values
selectedSortBy: 'standard',
sortByItems: [
'standard',
'AZ',
'sortByColor',
'sortByHatchable',
],
selectedDrawerTab: 0,
availableContentWidth: 0,
};
},
watch: {
searchText: _throttle(function throttleSearch () {
let search = this.searchText.toLowerCase();
this.searchTextThrottled = search;
}, 250),
},
computed: {
...mapState({
content: 'content',
currentPet: 'user.data.items.currentPet',
currentMount: 'user.data.items.currentMount',
userItems: 'user.data.items',
hideDialog: 'user.data.flags.tutorial.common.mounts',
}),
petGroups () {
let petGroups = [
{
label: this.$t('filterByStandard'),
key: 'standardPets',
petSource: {
eggs: this.content.dropEggs,
potions: this.content.dropHatchingPotions,
},
},
{
label: this.$t('filterByMagicPotion'),
key: 'magicPets',
petSource: {
eggs: this.content.dropEggs,
potions: this.content.premiumHatchingPotions,
},
},
{
label: this.$t('filterByQuest'),
key: 'questPets',
petSource: {
eggs: this.content.questEggs,
potions: this.content.dropHatchingPotions,
},
},
{
label: this.$t('special'),
key: 'specialPets',
petSource: {
special: this.content.specialPets,
},
alwaysHideMissing: true,
},
];
petGroups.map((petGroup) => {
this.$set(this.viewOptions, petGroup.key, {
selected: true,
open: false,
animalCount: 0,
});
});
return petGroups;
},
mountGroups () {
let mountGroups = [
{
label: this.$t('filterByStandard'),
key: 'standardMounts',
petSource: {
eggs: this.content.dropEggs,
potions: this.content.dropHatchingPotions,
},
},
{
label: this.$t('filterByMagicPotion'),
key: 'magicMounts',
petSource: {
eggs: this.content.dropEggs,
potions: this.content.premiumHatchingPotions,
},
},
{
label: this.$t('filterByQuest'),
key: 'questMounts',
petSource: {
eggs: this.content.questEggs,
potions: this.content.dropHatchingPotions,
},
},
{
label: this.$t('special'),
key: 'specialMounts',
petSource: {
special: this.content.specialMounts,
},
alwaysHideMissing: true,
},
];
mountGroups.map((mountGroup) => {
this.$set(this.viewOptions, mountGroup.key, {
selected: true,
open: false,
animalCount: 0,
});
});
return mountGroups;
},
drawerTabs () {
return [
{
label: this.$t('food'),
items: _filter(this.content.food, f => {
return f.key !== 'Saddle' && this.userItems.food[f.key];
}),
},
{
label: this.$t('special'),
items: _filter(this.content.food, f => {
return f.key === 'Saddle' && this.userItems.food[f.key];
}),
},
];
},
},
methods: {
getAnimalList (animalGroup, type) {
let key = animalGroup.key;
this.cachedAnimalList = this.cachedAnimalList || {};
if (this.cachedAnimalList[key]) {
return this.cachedAnimalList[key];
}
let animals = [];
let userItems = this.userItems;
switch (key) {
case 'specialPets':
case 'specialMounts': {
_each(animalGroup.petSource.special, (value, specialKey) => {
let eggKey = specialKey.split('-')[0];
let potionKey = specialKey.split('-')[1];
animals.push({
key: specialKey,
eggKey,
potionKey,
pet: this.content[`${type}Info`][specialKey].text(),
isOwned () {
return [`${type}s`][this.key] > 0;
},
isHatchable () {
return false;
},
});
});
break;
}
default: {
_each(animalGroup.petSource.eggs, (egg) => {
_each(animalGroup.petSource.potions, (potion) => {
let animalKey = `${egg.key}-${potion.key}`;
animals.push({
key: animalKey,
eggKey: egg.key,
eggName: egg.text(),
potionKey: potion.key,
potionName: potion.text(),
name: this.content[`${type}Info`][animalKey].text(),
isOwned () {
return userItems[`${type}s`][animalKey] > 0;
},
isHatchable () {
return userItems.eggs[egg.key] > 0 && userItems.hatchingPotions[potion.key] > 0;
},
});
});
});
}
}
this.cachedAnimalList[key] = animals;
return animals;
},
listAnimals (animalGroup, type, isOpen, hideMissing, sort, searchText, availableSpace) {
let animals = this.getAnimalList(animalGroup, type);
let isPetList = type === 'pet';
let withProgress = isPetList && animalGroup.key !== 'specialPets';
// 1. Filter
if (hideMissing || animalGroup.alwaysHideMissing) {
animals = _filter(animals, (a) => {
return a.isOwned();
});
}
if (searchText && searchText !== '') {
animals = _filter(animals, (a) => {
return a.name.toLowerCase().indexOf(searchText) !== -1;
});
}
// 2. Sort
switch (sort) {
case 'AZ':
animals = _sortBy(animals, ['pet']);
break;
case 'sortByColor':
animals = _sortBy(animals, ['potionName']);
break;
case 'sortByHatchable': {
if (isPetList) {
let sortFunc = (i) => {
return i.isHatchable() ? 0 : 1;
};
animals = _sortBy(animals, [sortFunc]);
}
break;
}
}
let animalRows = [];
let itemsPerRow = Math.floor(availableSpace / (94 + 24));
let rowsToShow = isOpen ? Math.ceil(animals.length / itemsPerRow) : 1;
for (let i = 0; i < rowsToShow; i++) {
let skipped = _drop(animals, i * itemsPerRow);
let row = _take(skipped, itemsPerRow);
let rowWithProgressData = withProgress ? _flatMap(row, (a) => {
let progress = this.userItems[`${type}s`][a.key];
return {
...a,
progress,
};
}) : row;
animalRows.push(...rowWithProgressData);
}
this.viewOptions[animalGroup.key].animalCount = animals.length;
return animalRows;
},
countOwnedAnimals (animalGroup, type) {
let animals = this.getAnimalList(animalGroup, type);
let countAll = animals.length;
let countOwned = _filter(animals, (a) => {
return a.isOwned();
});
return `${countOwned.length}/${countAll}`;
},
pets (animalGroup, showAll, hideMissing, sortBy, searchText, availableSpace) {
return this.listAnimals(animalGroup, 'pet', showAll, hideMissing, sortBy, searchText, availableSpace);
},
mounts (animalGroup, showAll, hideMissing, sortBy, searchText, availableSpace) {
return this.listAnimals(animalGroup, 'mount', showAll, hideMissing, sortBy, searchText, availableSpace);
},
getPetItemClass (pet) {
if (pet.isOwned()) {
return `Pet Pet-${pet.key}`;
}
if (pet.isHatchable()) {
return 'PixelPaw';
}
return 'GreyedOut PixelPaw';
},
hasDrawerTabItems (index) {
return this.drawerTabs && this.drawerTabs[index].items.length !== 0;
},
// Actions
updateHideMissing (newVal) {
this.hideMissing = newVal;
},
selectPet (item) {
this.$store.dispatch('common:equip', {key: item.key, type: 'pet'});
},
selectMount (item) {
this.$store.dispatch('common:equip', {key: item.key, type: 'mount'});
},
hatchPet (pet) {
this.$store.dispatch('common:hatch', {egg: pet.eggKey, hatchingPotion: pet.potionKey});
},
onDragOver (ev, pet) {
if (this.userItems.mounts[pet.key]) {
ev.dropable = false;
}
},
onDrop (ev, pet) {
this.$store.dispatch('common:feed', {pet: pet.key, food: ev.draggingKey});
},
},
};
</script>

View file

@ -0,0 +1,117 @@
<template lang="pug">
b-popover(
:triggers="[showPopover?'hover':'']",
:placement="popoverPosition",
)
span(slot="content")
slot(name="popoverContent", :item="item")
.item-wrapper
.item(
:class="{'item-empty': emptyItem}",
@mouseup="holdStop",
@mouseleave="holdStop",
@mousedown.left="holdStart"
)
slot(name="itemBadge", :item="item")
span.item-content(:class="itemContentClass")
span.pet-progress-background(v-if="progress > 0")
div.pet-progress-bar(v-bind:style="{width: 100 * progress/50 + '%' }")
span.pet-progress-background(v-if="holdProgress > 0")
div.pet-progress-bar.hold(v-bind:style="{width: 100 * holdProgress/5 + '%' }")
span.item-label(v-if="label") {{ label }}
</template>
<style lang="scss">
.pet-progress-background {
width: 62px;
height: 4px;
background-color: #e1e0e3;
position: absolute;
bottom: 4px;
left: calc((100% - 62px) / 2);
}
.pet-progress-bar {
height: 4px;
background-color: #24cc8f;
}
.pet-progress-bar.hold {
background-color: #54c3cc;
}
</style>
<script>
import bPopover from 'bootstrap-vue/lib/components/popover';
import {mapState} from 'client/libs/store';
export default {
components: {
bPopover,
},
props: {
item: {
type: Object,
},
itemContentClass: {
type: String,
},
label: {
type: String,
},
progress: {
type: Number,
default: -1,
},
emptyItem: {
type: Boolean,
default: false,
},
popoverPosition: {
type: String,
default: 'bottom',
},
showPopover: {
type: Boolean,
default: true,
},
},
data () {
return {
holdProgress: -1,
};
},
computed: {
...mapState({
ATTRIBUTES: 'constants.ATTRIBUTES',
}),
},
methods: {
holdStart () {
let pet = this.item;
if (pet.isOwned() || !pet.isHatchable()) {
return;
}
this.holdProgress = 1;
this.currentHoldingTimer = setInterval(() => {
if (this.holdProgress === 5) {
this.holdStop();
this.$emit('hatchPet', pet);
}
this.holdProgress += 1;
}, 1000);
},
holdStop () {
if (this.currentHoldingTimer) {
clearInterval(this.currentHoldingTimer);
this.holdProgress = -1;
}
},
},
};
</script>

View file

@ -0,0 +1,30 @@
<template lang="pug">
nav
router-link(:to="{name: 'login'}") Login
router-link(:to="{name: 'register'}") Register
</template>
<script>
export default {
methods: {
play () {
// @TODO:
// Auth play click
},
// passwordReset (email) {
// if(email == null || email.length == 0) {
// alert(window.env.t('invalidEmail'));
// } else {
// $http.post(ApiUrl.get() + '/api/v3/user/reset-password', {email:email})
// .success(function(){
// alert(window.env.t('newPassSent'));
// })
// .error(function(data){
// alert(data.err);
// });
// }
// },
},
};
</script>

View file

@ -1,33 +0,0 @@
<template lang="pug">
li
ul
li
strong {{task.text}}
li(v-if="task.type === 'habit'") up: {{task.up}}, down: {{task.down}}
li value: {{task.value}}
template(v-if="task.type === 'daily' || task.type === 'todo'")
li completed: {{task.completed}}
li
span checklist
ul
li(v-for="checklist in task.checklist") {{checklist.text}}
template(v-if="task.type === 'daily'")
li streak: {{task.streak}}
li repeat: {{task.repeat}}
li(v-if="task.type === 'todo'") due date: {{task.date}}
li attribute {{task.attribute}}
li difficulty {{task.priority}}
li tags {{getTagsFor(task)}}
</template>
<script>
import { mapState, mapGetters } from 'client/libs/store';
export default {
props: ['task'],
computed: {
...mapState({user: 'user.data'}),
...mapGetters({getTagsFor: 'tasks:getTagsFor'}),
},
};
</script>

View file

@ -0,0 +1,194 @@
<template lang="pug">
.tasks-column
.d-flex
h2.tasks-column-title(v-once) {{ $t(types[type].label) }}
.filters.d-flex.justify-content-end
.filter.small-text(
v-for="filter in types[type].filters",
:class="{active: activeFilter.label === filter.label}",
@click="activeFilter = filter",
) {{ $t(filter.label) }}
.tasks-list
task(v-for="task in tasks[`${type}s`]", :key="task.id", :task="task", v-if="activeFilter.filter(task)")
.bottom-gradient
.column-background(v-if="isUser === true", :class="{'initial-description': tasks[`${type}s`].length === 0}")
.svg-icon(v-html="icons[type]", :class="`icon-${type}`", v-once)
h3(v-once) {{$t('theseAreYourTasks', {taskType: `${type}s`})}}
.small-text {{$t(`${type}sDesc`)}}
</template>
<style lang="scss" scoped>
@import '~client/assets/scss/colors.scss';
.tasks-column {
flex-grow: 1;
}
.tasks-list {
border-radius: 4px;
background: $gray-600;
padding: 8px;
// not sure why but this is necessary or the last task will overflow the container
padding-bottom: 0.1px;
position: relative;
height: calc(100% - 64px);
}
.bottom-gradient {
position: absolute;
bottom: 0px;
left: -0px;
height: 42px;
background-image: linear-gradient(to bottom, rgba($gray-10, 0), rgba($gray-10, 0.24));
width: 100%;
}
.tasks-column-title {
margin-bottom: 8px;
}
.filters {
flex-grow: 1;
}
.filter {
font-weight: bold;
color: $gray-100;
font-style: normal;
padding: 8px;
cursor: pointer;
&:hover {
color: $purple-200;
}
&.active {
color: $purple-200;
border-bottom: 2px solid $purple-200;
padding-bottom: 6px;
}
}
.column-background {
position: absolute;
bottom: 32px;
z-index: 7;
&.initial-description {
top: 30%;
}
.svg-icon {
margin: 0 auto;
margin-bottom: 12px;
}
h3, .small-text {
color: $gray-300;
text-align: center;
}
h3 {
font-weight: normal;
margin-bottom: 4px;
}
.small-text {
font-style: normal;
padding-left: 24px;
padding-right: 24px;
}
}
.icon-habit {
width: 30px;
height: 20px;
}
.icon-daily {
width: 30px;
height: 20px;
}
.icon-todo {
width: 20px;
height: 20px;
}
.icon-reward {
width: 26px;
height: 20px;
}
</style>
<script>
import Task from './task';
import { mapState } from 'client/libs/store';
import { shouldDo } from 'common/script/cron';
import habitIcon from 'assets/svg/habit.svg';
import dailyIcon from 'assets/svg/daily.svg';
import todoIcon from 'assets/svg/todo.svg';
import rewardIcon from 'assets/svg/reward.svg';
export default {
components: {
Task,
},
props: ['type', 'isUser'],
data () {
const types = Object.freeze({
habit: {
label: 'habits',
filters: [
{label: 'all', filter: () => true, default: true},
{label: 'yellowred', filter: t => t.value < 1}, // weak
{label: 'greenblue', filter: t => t.value >= 1}, // strong
],
},
daily: {
label: 'dailies',
filters: [
{label: 'all', filter: () => true, default: true},
{label: 'due', filter: t => !t.completed && shouldDo(new Date(), t, this.userPreferences)},
{label: 'notDue', filter: t => t.completed || !shouldDo(new Date(), t, this.userPreferences)},
],
},
todo: {
label: 'todos',
filters: [
{label: 'remaining', filter: t => !t.completed, default: true}, // active
{label: 'scheduled', filter: t => !t.completed && t.date},
{label: 'complete2', filter: t => t.completed},
],
},
reward: {
label: 'rewards',
filters: [
{label: 'all', filter: () => true, default: true},
{label: 'custom', filter: () => true}, // all rewards made by the user
{label: 'wishlist', filter: () => false}, // not user tasks
],
},
});
const icons = Object.freeze({
habit: habitIcon,
daily: dailyIcon,
todo: todoIcon,
reward: rewardIcon,
});
return {
types,
activeFilter: types[this.type].filters.find(f => f.default === true),
icons,
};
},
computed: {
...mapState({
tasks: 'tasks.data',
userPreferences: 'user.data.preferences',
}),
},
};
</script>

View file

@ -0,0 +1,293 @@
<template lang="pug">
.task.d-flex
// Habits left side control
.left-control.d-flex.align-items-center.justify-content-center(v-if="task.type === 'habit'", :class="controlClass.up")
.task-control.habit-control(:class="controlClass.up + '-control-habit'")
.svg-icon.positive(v-html="icons.positive")
// Dailies and todos left side control
.left-control.d-flex.align-items-center.justify-content-center(v-if="task.type === 'daily' || task.type === 'todo'", :class="controlClass")
.task-control.daily-todo-control(:class="controlClass + '-control-daily-todo'")
.svg-icon.check(v-html="icons.check", v-if="task.completed")
// Task title, description and icons
.task-content(:class="contentClass")
h3.task-title(:class="{ 'has-notes': task.notes }") {{task.text}}
.task-notes.small-text {{task.notes}}
.icons.small-text.d-flex.align-items-center
.d-flex.align-items-center(v-if="task.type === 'todo' && task.date", :class="{'due-overdue': isDueOverdue}")
.svg-icon.calendar(v-html="icons.calendar")
span {{dueIn}}
.icons-right.d-flex.justify-content-end
.d-flex.align-items-center(v-if="showStreak")
.svg-icon.streak(v-html="icons.streak")
span(v-if="task.type === 'daily'") {{task.streak}}
span(v-if="task.type === 'habit'")
span.m-0(v-if="task.up") +{{task.counterUp}}
span.m-0(v-if="task.up && task.down") &nbsp;|&nbsp;
span.m-0(v-if="task.down") -{{task.counterDown}}
.d-flex.align-items-center(v-if="task.challenge && task.challenge.id")
.svg-icon.challenge(v-html="icons.challenge")
b-popover.tags-popover.no-span-margin(
:triggers="['hover']",
:placement="'bottom'",
:popover-style="{'max-width': '1000px'}",
)
.d-flex.align-items-center(slot="content")
.tags-popover-title(v-once) {{ `${$t('tags')}:` }}
.tag-label(v-for="tag in getTagsFor(task)") {{tag}}
.d-flex.align-items-center(v-if="task.tags && task.tags.length > 0")
.svg-icon.tags(v-html="icons.tags")
// Habits right side control
.right-control.d-flex.align-items-center.justify-content-center(v-if="task.type === 'habit'", :class="controlClass.down")
.task-control.habit-control(:class="controlClass.down + '-control-habit'")
.svg-icon.negative(v-html="icons.negative")
// Rewards right side control
.right-control.d-flex.align-items-center.justify-content-center.reward-control(v-if="task.type === 'reward'", :class="controlClass")
.svg-icon(v-html="icons.gold")
.small-text {{task.value}}
</template>
<style lang="scss">
@import '~client/assets/scss/colors.scss';
.task {
margin-bottom: 8px;
box-shadow: 0 2px 2px 0 rgba($black, 0.16), 0 1px 4px 0 rgba($black, 0.12);
background: $white;
border-radius: 2px;
z-index: 9;
position: relative;
}
.task-title {
margin-bottom: 8px;
color: $gray-10;
font-weight: normal;
&.has-notes {
margin-bottom: 0px;
}
}
.task-notes {
color: $gray-100;
font-style: normal;
margin-bottom: 4px;
}
.task-content {
padding: 8px;
flex-grow: 1;
}
.icons {
color: $gray-300;
font-style: normal;
&-right {
flex-grow: 1;
}
}
.icons-right .svg-icon {
margin-left: 8px;
}
.icons span {
margin-left: 4px;
}
.no-span-margin span {
margin-left: 0px !important;
}
.svg-icon.streak {
width: 11.6px;
height: 7.1px;
}
.tags.svg-icon, .calendar.svg-icon {
width: 14px;
height: 14px;
}
.tags:hover {
color: $purple-500;
}
.due-overdue {
color: $red-50;
}
.calendar.svg-icon {
margin-right: 2px;
margin-top: -2px;
}
.challenge.svg-icon {
width: 14px;
height: 12px;
}
.check.svg-icon {
width: 12.3px;
height: 9.8px;
margin: 8px;
}
.left-control, .right-control {
width: 40px;
flex-shrink: 0;
}
.left-control {
border-top-left-radius: 2px;
border-bottom-left-radius: 2px;
}
.right-control {
border-top-right-radius: 2px;
border-bottom-right-radius: 2px;
}
.task-control {
width: 28px;
height: 28px;
}
.habit-control {
border-radius: 100px;
color: $white;
.svg-icon {
width: 10px;
margin: 0 auto;
}
.positive {
margin-top: 9px;
}
.negative {
margin-top: 13px;
}
}
.daily-todo-control {
border-radius: 2px;
}
.reward-control {
flex-direction: column;
padding-top: 16px;
padding-bottom: 12px;
.svg-icon {
width: 24px;
height: 24px;
}
.small-text {
margin-top: 4px;
color: $yellow-10;
}
}
</style>
<style lang="scss"> // not working as scoped css
@import '~client/assets/scss/colors.scss';
.tags-popover {
// TODO fix padding, see https://github.com/bootstrap-vue/bootstrap-vue/issues/559#issuecomment-311150335
white-space: nowrap;
}
.tags-popover-title {
margin-right: 4px;
display: block;
float: left;
}
.tag-label {
display: block;
float: left;
margin-left: 4px;
border-radius: 100px;
background-color: $gray-50;
padding: 4px 10px;
color: $gray-300;
white-space: nowrap;
}
</style>
<script>
import { mapState, mapGetters } from 'client/libs/store';
import moment from 'moment';
import positiveIcon from 'assets/svg/positive.svg';
import negativeIcon from 'assets/svg/negative.svg';
import goldIcon from 'assets/svg/gold.svg';
import streakIcon from 'assets/svg/streak.svg';
import calendarIcon from 'assets/svg/calendar.svg';
import challengeIcon from 'assets/svg/challenge.svg';
import tagsIcon from 'assets/svg/tags.svg';
import checkIcon from 'assets/svg/check.svg';
import bPopover from 'bootstrap-vue/lib/components/popover';
export default {
components: {
bPopover,
},
props: ['task'],
data () {
return {
icons: Object.freeze({
positive: positiveIcon,
negative: negativeIcon,
gold: goldIcon,
streak: streakIcon,
calendar: calendarIcon,
challenge: challengeIcon,
tags: tagsIcon,
check: checkIcon,
}),
};
},
computed: {
...mapState({user: 'user.data'}),
...mapGetters({
getTagsFor: 'tasks:getTagsFor',
getTaskClasses: 'tasks:getTaskClasses',
}),
leftControl () {
const task = this.task;
if (task.type === 'reward') return false;
return true;
},
rightControl () {
const task = this.task;
if (task.type === 'reward') return true;
if (task.type === 'habit') return true;
return false;
},
controlClass () {
return this.getTaskClasses(this.task, 'control');
},
contentClass () {
return this.getTaskClasses(this.task, 'content');
},
showStreak () {
if (this.task.streak !== undefined) return true;
if (this.task.type === 'habit' && (this.task.up || this.task.down)) return true;
return false;
},
isDueOverdue () {
return moment().diff(this.task.date, 'days') >= 0;
},
dueIn () {
const dueIn = moment().to(this.task.date);
return this.$t('dueIn', {dueIn});
},
},
};
</script>

View file

@ -0,0 +1,56 @@
<template lang="pug">
.row.user-tasks-page
.col-12
.row.tasks-navigation
.col-4.offset-4
input.form-control.input-search(type="text", :placeholder="$t('search')")
.col-1.offset-3
button.btn.btn-success(v-once)
.svg-icon.positive(v-html="icons.positive")
| {{ $t('create') }}
.row.tasks-columns
task-column.col-3(v-for="column in columns", :type="column", :key="column", :isUser="true")
</template>
<style lang="scss" scoped>
@import '~client/assets/scss/colors.scss';
.user-tasks-page {
padding-top: 31px;
}
.tasks-navigation {
margin-bottom: 40px;
}
.positive {
display: inline-block;
width: 10px;
color: $green-500;
margin-right: 8px;
padding-top: 6px;
}
.tasks-columns {
height: 100%;
}
</style>
<script>
import Column from './column';
import positiveIcon from 'assets/svg/positive.svg';
export default {
components: {
TaskColumn: Column,
},
data () {
return {
columns: ['habit', 'daily', 'todo', 'reward'],
icons: Object.freeze({
positive: positiveIcon,
}),
};
},
};
</script>

View file

@ -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 @@
</template>
<style lang="scss" scoped>
@import '~client/assets/scss/colors.scss';
@import '~client/assets/scss/colors.scss';
.toggle-switch-container {
margin-top: 6px;
}
.toggle-switch-container {
margin-top: 6px;
}
.toggle-switch {
position: relative;
width: 40px;
user-select: none;
margin-left: 9px;
}
.toggle-switch {
position: relative;
width: 40px;
user-select: none;
margin-left: 9px;
}
.toggle-switch-description {
height: 20px;
border-bottom: 1px dashed $gray-200;
}
.toggle-switch-description {
height: 20px;
border-bottom: 1px dashed $gray-200;
}
.toggle-switch-checkbox {
display: none;
}
.toggle-switch-checkbox {
display: none;
}
.toggle-switch-label {
display: block;
overflow: hidden;
cursor: pointer;
border-radius: 100px;
margin-bottom: 0px;
margin-top: 3px;
}
.toggle-switch-label {
display: block;
overflow: hidden;
cursor: pointer;
border-radius: 100px;
margin-bottom: 0px;
margin-top: 3px;
}
.toggle-switch-inner {
display: block;
width: 200%;
margin-left: -100%;
transition: margin 0.3s ease-in 0s;
}
.toggle-switch-inner {
display: block;
width: 200%;
margin-left: -100%;
transition: margin 0.3s ease-in 0s;
}
.toggle-switch-inner:before, .toggle-switch-inner:after {
display: block;
float: left;
width: 50%;
height: 16px;
padding: 0;
}
.toggle-switch-inner:before, .toggle-switch-inner:after {
display: block;
float: left;
width: 50%;
height: 16px;
padding: 0;
}
.toggle-switch-inner:before {
content: "";
padding-left: 10px;
background-color: $purple-400;
}
.toggle-switch-inner:before {
content: "";
padding-left: 10px;
background-color: $purple-400;
}
.toggle-switch-inner:after {
content: "";
padding-right: 10px;
background-color: $gray-200;
text-align: right;
}
.toggle-switch-inner:after {
content: "";
padding-right: 10px;
background-color: $gray-200;
text-align: right;
}
.toggle-switch-switch {
box-shadow: 0 1px 2px 0 rgba($black, 0.32);
display: block;
width: 20px;
margin: -2px;
margin-top: 1px;
height: 20px;
background: $white;
position: absolute;
top: 0;
bottom: 0;
right: 22px;
border-radius: 100px;
transition: all 0.3s ease-in 0s;
}
.toggle-switch-switch {
box-shadow: 0 1px 2px 0 rgba($black, 0.32);
display: block;
width: 20px;
margin: -2px;
margin-top: 1px;
height: 20px;
background: $white;
position: absolute;
top: 0;
bottom: 0;
right: 22px;
border-radius: 100px;
transition: all 0.3s ease-in 0s;
}
.toggle-switch-checkbox:checked + .toggle-switch-label .toggle-switch-inner {
margin-left: 0;
}
.toggle-switch-checkbox:checked + .toggle-switch-label .toggle-switch-inner {
margin-left: 0;
}
.toggle-switch-checkbox:checked + .toggle-switch-label .toggle-switch-switch {
right: 0px;
}
.toggle-switch-checkbox:checked + .toggle-switch-label .toggle-switch-switch {
right: 0px;
}
</style>
<script>
@ -105,7 +106,14 @@ export default {
id: Math.random(),
};
},
model: {
prop: 'checked',
event: 'change',
},
props: {
value: {
default: true,
},
label: {
type: String,
required: true,
@ -115,5 +123,15 @@ export default {
default: false,
},
},
computed: {
isChecked () {
return this.checked === this.value;
},
},
methods: {
handleChange ({ target: { checked } }) {
this.$emit('change', checked ? this.value : this.uncheckedValue);
},
},
};
</script>

View file

@ -1,163 +0,0 @@
<template lang="pug">
.row
.col-12
.row
.col-3.p-4
h3 Input
input.form-control(type="text", placeholder="Placeholder")
.col-3.p-4
h3 Input Disabled
input.form-control(type="text", placeholder="Placeholder", disabled)
.col-3.p-4
h3 Input With Icon
input.form-control.input-search(type="text", placeholder="Placeholder")
.col-3.p-4
h3 Input With Icon Disabled
input.form-control.input-search(type="text", placeholder="Placeholder", disabled)
.col-3.p-4
h3 Input Valid
input.form-control.input-valid(type="text", placeholder="Placeholder")
.col-3.p-4
h3 Input Invalid
input.form-control.input-invalid(type="text", placeholder="Placeholder")
.row
.col-6.p-4
h3 Textarea
textarea.form-control(rows="5", cols="50")
.col-6.p-4
h3 Textarea Disabled
textarea.form-control(disabled, rows="10", cols="50")
.row
.col-2.p-4
toggleSwitch(label="Toggle Switch")
.row
.col-3.p-4
h3 Checkbox
label.custom-control.custom-checkbox
input.custom-control-input(type='checkbox')
span.custom-control-indicator
span.custom-control-description Check this custom checkbox
.col-3.p-4
h3 Checkbox Disabled Checked
label.custom-control.custom-checkbox
input.custom-control-input(type='checkbox', disabled, checked)
span.custom-control-indicator
span.custom-control-description Check this custom checkbox
.col-3.p-4
h3 Checkbox Disabled Not Checked
label.custom-control.custom-checkbox
input.custom-control-input(type='checkbox', disabled)
span.custom-control-indicator
span.custom-control-description Check this custom checkbox
.col-6.p-4
h3 Radio Button
form
label.custom-control.custom-radio
input#radio1.custom-control-input(name='radio', type='radio')
span.custom-control-indicator
span.custom-control-description Toggle this custom radio
label.custom-control.custom-radio
input#radio2.custom-control-input(name='radio', type='radio')
span.custom-control-indicator
span.custom-control-description Toggle this custom radio
.col-3.p-4
h3 Radio Button Disabled Checked
form
label.custom-control.custom-radio
input#radio3.custom-control-input(name='radio', type='radio', disabled, checked)
span.custom-control-indicator
span.custom-control-description Toggle this custom radio
.col-3.p-4
h3 Radio Button Disabled Not Checked
form
label.custom-control.custom-radio
input#radio3.custom-control-input(name='radio', type='radio', disabled)
span.custom-control-indicator
span.custom-control-description Toggle this custom radio
.row
.col-3.p-4
h3 Main Button
button.btn.btn-primary Button
.col-3.p-4
h3 Secondary Button
button.btn.btn-secondary Button
.col-3.p-4
h3 Green Button
button.btn.btn-success Button
.col-3.p-4
h3 Blue Button
button.btn.btn-info Button
.col-3.p-4
h3 Red Button
button.btn.btn-danger Button
.row
.col-3.p-4
h3 Main Button Disabled
button.btn.btn-primary(disabled=true) Button
.col-3.p-4
h3 Secondary Button Disabled
button.btn.btn-secondary(disabled=true) Button
.col-3.p-4
h3 Green Button Disabled
button.btn.btn-success(disabled=true) Button
.col-3.p-4
h3 Blue Button Disabled
button.btn.btn-info(disabled=true) Button
.col-3.p-4
h3 Red Button Disabled
button.btn.btn-danger(disabled=true) Button
.row
.col-6.p-4
h3 Dropdown Menu
b-dropdown(text="Menu", right=false)
b-dropdown-item(href="#") Menu item 1
b-dropdown-item(href="#") Menu item 2
b-dropdown-item(href="#") Menu item 3
b-dropdown-item(href="#") Menu item 4
.col-6.p-4
h3 Dropdown Menu Disabled
b-dropdown(text="Menu", disabled)
b-dropdown-item(href="#") Menu item 1
b-dropdown-item(href="#") Menu item 2
b-dropdown-item(href="#") Menu item 3
b-dropdown-item(href="#") Menu item 4
.row
.col-6.p-4
h1 Heading 1
h2 Heading 2
h3 Heading 3
h4 Heading 4
.col-6.p-4
p Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam vehicula, purus sit amet sodales pharetra, ipsum ipsum mollis orci, id pharetra velit diam et dui. Sed placerat ipsum eget pharetra rutrum. Ut vitae rutrum lacus, eu imperdiet velit. Pellentesque eu velit cursus, scelerisque dui quis, dapibus magna. Vestibulum molestie sed sapien et ultricies. Nam porta ipsum leo, non congue magna vestibulum a. Etiam dictum felis sit amet augue varius tincidunt. Sed eget urna auctor, convallis felis in, pretium justo. Curabitur aliquet, ligula id tincidunt ullamcorper, orci lorem pharetra neque, in ornare arcu magna accumsan arcu. Maecenas dignissim lorem sed eros accumsan scelerisque.
p.small-text Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam vehicula, purus sit amet sodales pharetra, ipsum ipsum mollis orci, id pharetra velit diam et dui.
.row
.col(v-for="taskType in tasksTypes")
h3 {{taskType}}s
ul
task(v-for="task in tasks", v-if="task.type === taskType", :key="task.id", :task="task")
</template>
<script>
import Task from './task';
import { mapState } from 'client/libs/store';
import bDropdown from 'bootstrap-vue/lib/components/dropdown';
import bDropdownItem from 'bootstrap-vue/lib/components/dropdown-item';
import toggleSwitch from 'client/components/ui/toggleSwitch';
export default {
components: {
Task,
bDropdown,
bDropdownItem,
toggleSwitch,
},
data () {
return {
tasksTypes: ['habit', 'daily', 'todo', 'reward'],
};
},
computed: {
...mapState({tasks: 'tasks.data'}),
},
};
</script>

View file

@ -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);
}
};

View file

@ -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);
}
},
};

View file

@ -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);
},
};

View file

@ -4,14 +4,9 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Habitica</title>
<!-- TODO load google fonts separately as @import is slow, find alternative -->
<link href="https://fonts.googleapis.com/css?family=Roboto+Condensed:700|Roboto:400,400i,700,700i" rel="stylesheet">
<link href="https://fonts.googleapis.com/css?family=Roboto+Condensed:400,400i,700,700i|Roboto:400,400i,700,700i" rel="stylesheet">
</head>
<body>
<!-- #loading-screen needs to be rendered before vue, will be deleted once app is loaded -->
<div id="loading-screen" class="h-100 w-100 d-flex justify-content-center align-items-center">
<p>Loading...</p>
</div>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>

View file

@ -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}".`));

View file

@ -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);
},
});
});

View file

@ -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/
- Were using `.vue` files that make it possible to have HTML, JS and CSS for each component together in a single location. Theyre 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 Ive only used the CSS part, it also has JS plugins but Ive yet to use them. It supports theming so if its not too difficult well 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`. Were using something very similar to Vuex (equivalent of Reacts Redux) for state management http://vuex.vuejs.org/en/index.html
The API is almost the same except that we dont 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 well be able to avoid splitting it into a different branch.
So far most of the work has been on the template, so theres no complex logic to understand. The only thing I would suggest you to read about is Vuex for data management: its basically a Flux implementation: theres 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/
- Were using `.vue` files that make it possible to have HTML, JS and CSS for each component together in a single location. Theyre 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 Ive only used the CSS part, it also has JS plugins but Ive yet to use them. It supports theming so if its not too difficult well 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`. Were using something very similar to Vuex (equivalent of Reacts Redux) for state management http://vuex.vuejs.org/en/index.html
The API is almost the same except that we dont 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 well be able to avoid splitting it into a different branch.
So far most of the work has been on the template, so theres no complex logic to understand. The only thing I would suggest you to read about is Vuex for data management: its basically a Flux implementation: theres 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.

View file

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

View file

@ -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);
}

View file

@ -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));
}
}
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));
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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! <b>Click and hold</b> 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 <br/> Quest Scroll to battle the Basi-List together!"
"inviteFriendsParty": "Inviting friends to your party will grant you an exclusive <br/> 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 dont 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 partys 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 partys 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 its the same password!",
"termsAndAgreement": "By clicking the button below, you are indicating that you have read and agree to the <a href=''>Terms of Service</a> and <a href=''>Privacy Policy</a>.",
"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."
}

View file

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

View file

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

View file

@ -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 <a href=\"https://support.apple.com/en-us/HT202039\">Apples official instructions</a> 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"
}

Some files were not shown because too many files have changed in this diff Show more