mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-13 04:37:36 +01:00
Merge branch 'develop' into release
This commit is contained in:
33
package-lock.json
generated
33
package-lock.json
generated
@@ -1542,18 +1542,18 @@
|
||||
}
|
||||
},
|
||||
"apidoc": {
|
||||
"version": "0.23.0",
|
||||
"resolved": "https://registry.npmjs.org/apidoc/-/apidoc-0.23.0.tgz",
|
||||
"integrity": "sha512-bn2QNaqyyL5ihRUlBBqwffElJg+hAgxAWPDdjJiLxDJ66Jiw+jf8uucFLYC9XHFqjdM7YTqLluM2PPshrqzObg==",
|
||||
"version": "0.24.0",
|
||||
"resolved": "https://registry.npmjs.org/apidoc/-/apidoc-0.24.0.tgz",
|
||||
"integrity": "sha512-1aKEMOJaX9coPTwbVcDtCcxtv7VIhR6YEWxWMtoKouYaaMqmScTBikq8umIH8pjr77F2AjrNWFiLhfwsHLsvkA==",
|
||||
"requires": {
|
||||
"apidoc-core": "^0.11.1",
|
||||
"commander": "^2.20.0",
|
||||
"fs-extra": "^8.1.0",
|
||||
"handlebars": "^4.7.6",
|
||||
"lodash": "^4.17.15",
|
||||
"lodash": "^4.17.19",
|
||||
"markdown-it": "^10.0.0",
|
||||
"nodemon": "^2.0.3",
|
||||
"winston": "^3.2.1"
|
||||
"nodemon": "^2.0.4",
|
||||
"winston": "^3.3.3"
|
||||
}
|
||||
},
|
||||
"apidoc-core": {
|
||||
@@ -3008,6 +3008,15 @@
|
||||
"check-error": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"chai-moment": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/chai-moment/-/chai-moment-0.1.0.tgz",
|
||||
"integrity": "sha1-SpFoDPo6dc/aGEULK6tltIyQJAE=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"moment": "^2.10.6"
|
||||
}
|
||||
},
|
||||
"chalk": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz",
|
||||
@@ -9227,9 +9236,9 @@
|
||||
}
|
||||
},
|
||||
"mongoose": {
|
||||
"version": "5.9.24",
|
||||
"resolved": "https://registry.npmjs.org/mongoose/-/mongoose-5.9.24.tgz",
|
||||
"integrity": "sha512-uxTLy/ExYmOfKvvbjn1PHbjSJg0SQzff+dW6jbnywtbBcfPRC/3etnG9hPv6KJe/5TFZQGxCyiSezkqa0+iJAQ==",
|
||||
"version": "5.9.25",
|
||||
"resolved": "https://registry.npmjs.org/mongoose/-/mongoose-5.9.25.tgz",
|
||||
"integrity": "sha512-vz/DqJ3mrHqEIlfRbKmDZ9TzQ1a0hCtSQpjHScIxr4rEtLs0tjsXDeEWcJ/vEEc3oLfP6vRx9V+uYSprXDUvFQ==",
|
||||
"requires": {
|
||||
"bson": "^1.1.4",
|
||||
"kareem": "2.3.1",
|
||||
@@ -11106,9 +11115,9 @@
|
||||
}
|
||||
},
|
||||
"regenerator-runtime": {
|
||||
"version": "0.13.5",
|
||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.5.tgz",
|
||||
"integrity": "sha512-ZS5w8CpKFinUzOwW3c83oPeVXoNsrLsaCoLtJvAClH135j/R77RuymhiSErhm2lKcwSCIpmvIWSbDkIfAqKQlA=="
|
||||
"version": "0.13.7",
|
||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz",
|
||||
"integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew=="
|
||||
},
|
||||
"regenerator-transform": {
|
||||
"version": "0.14.5",
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"accepts": "^1.3.5",
|
||||
"amazon-payments": "^0.2.8",
|
||||
"amplitude": "^3.5.0",
|
||||
"apidoc": "^0.23.0",
|
||||
"apidoc": "^0.24.0",
|
||||
"apn": "^2.2.0",
|
||||
"apple-auth": "^1.0.6",
|
||||
"bcrypt": "^5.0.0",
|
||||
@@ -48,7 +48,7 @@
|
||||
"method-override": "^3.0.0",
|
||||
"moment": "^2.27.0",
|
||||
"moment-recur": "^1.0.7",
|
||||
"mongoose": "^5.9.23",
|
||||
"mongoose": "^5.9.25",
|
||||
"morgan": "^1.10.0",
|
||||
"nconf": "^0.10.0",
|
||||
"node-gcm": "^1.0.3",
|
||||
@@ -62,7 +62,7 @@
|
||||
"ps-tree": "^1.0.0",
|
||||
"rate-limiter-flexible": "^2.1.7",
|
||||
"redis": "^3.0.2",
|
||||
"regenerator-runtime": "^0.13.5",
|
||||
"regenerator-runtime": "^0.13.7",
|
||||
"remove-markdown": "^0.3.0",
|
||||
"rimraf": "^3.0.2",
|
||||
"short-uuid": "^3.0.0",
|
||||
@@ -112,6 +112,7 @@
|
||||
"axios": "^0.19.2",
|
||||
"chai": "^4.1.2",
|
||||
"chai-as-promised": "^7.1.1",
|
||||
"chai-moment": "^0.1.0",
|
||||
"chalk": "^4.1.0",
|
||||
"cross-spawn": "^7.0.3",
|
||||
"expect.js": "^0.3.1",
|
||||
|
||||
@@ -42,13 +42,13 @@ describe('cron', () => {
|
||||
});
|
||||
|
||||
it('updates user.preferences.timezoneOffsetAtLastCron', () => {
|
||||
const timezoneOffsetFromUserPrefs = 1;
|
||||
const timezoneUtcOffsetFromUserPrefs = -1;
|
||||
|
||||
cron({
|
||||
user, tasksByType, daysMissed, analytics, timezoneOffsetFromUserPrefs,
|
||||
user, tasksByType, daysMissed, analytics, timezoneUtcOffsetFromUserPrefs,
|
||||
});
|
||||
|
||||
expect(user.preferences.timezoneOffsetAtLastCron).to.equal(timezoneOffsetFromUserPrefs);
|
||||
expect(user.preferences.timezoneOffsetAtLastCron).to.equal(1);
|
||||
});
|
||||
|
||||
it('resets user.items.lastDrop.count', () => {
|
||||
@@ -240,7 +240,7 @@ describe('cron', () => {
|
||||
user1.purchased.plan.consecutive.gemCapExtra = 0;
|
||||
|
||||
it('does not increment consecutive benefits after the first month', () => {
|
||||
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(1, 'months')
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(1, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
// Add 1 month to simulate what happens a month after the subscription was created.
|
||||
@@ -256,7 +256,7 @@ describe('cron', () => {
|
||||
});
|
||||
|
||||
it('does not increment consecutive benefits after the second month', () => {
|
||||
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(2, 'months')
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(2, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
// Add 1 month to simulate what happens a month after the subscription was created.
|
||||
@@ -272,7 +272,7 @@ describe('cron', () => {
|
||||
});
|
||||
|
||||
it('increments consecutive benefits after the third month', () => {
|
||||
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(3, 'months')
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(3, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
// Add 1 month to simulate what happens a month after the subscription was created.
|
||||
@@ -288,7 +288,7 @@ describe('cron', () => {
|
||||
});
|
||||
|
||||
it('does not increment consecutive benefits after the fourth month', () => {
|
||||
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(4, 'months')
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(4, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
// Add 1 month to simulate what happens a month after the subscription was created.
|
||||
@@ -304,7 +304,7 @@ describe('cron', () => {
|
||||
});
|
||||
|
||||
it('increments consecutive benefits correctly if user has been absent with continuous subscription', () => {
|
||||
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(10, 'months')
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(10, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
cron({
|
||||
@@ -339,7 +339,7 @@ describe('cron', () => {
|
||||
user3.purchased.plan.consecutive.gemCapExtra = 5;
|
||||
|
||||
it('does not increment consecutive benefits in the first month of the first paid period that they already have benefits for', () => {
|
||||
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(1, 'months')
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(1, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
cron({
|
||||
@@ -352,7 +352,7 @@ describe('cron', () => {
|
||||
});
|
||||
|
||||
it('does not increment consecutive benefits in the middle of the period that they already have benefits for', () => {
|
||||
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(2, 'months')
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(2, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
cron({
|
||||
@@ -365,7 +365,7 @@ describe('cron', () => {
|
||||
});
|
||||
|
||||
it('does not increment consecutive benefits in the final month of the period that they already have benefits for', () => {
|
||||
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(3, 'months')
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(3, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
cron({
|
||||
@@ -378,7 +378,7 @@ describe('cron', () => {
|
||||
});
|
||||
|
||||
it('increments consecutive benefits the month after the second paid period has started', () => {
|
||||
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(4, 'months')
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(4, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
cron({
|
||||
@@ -391,7 +391,7 @@ describe('cron', () => {
|
||||
});
|
||||
|
||||
it('does not increment consecutive benefits in the second month of the second period that they already have benefits for', () => {
|
||||
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(5, 'months')
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(5, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
cron({
|
||||
@@ -404,7 +404,7 @@ describe('cron', () => {
|
||||
});
|
||||
|
||||
it('does not increment consecutive benefits in the final month of the second period that they already have benefits for', () => {
|
||||
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(6, 'months')
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(6, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
cron({
|
||||
@@ -417,7 +417,7 @@ describe('cron', () => {
|
||||
});
|
||||
|
||||
it('increments consecutive benefits the month after the third paid period has started', () => {
|
||||
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(7, 'months')
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(7, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
cron({
|
||||
@@ -430,7 +430,7 @@ describe('cron', () => {
|
||||
});
|
||||
|
||||
it('increments consecutive benefits correctly if user has been absent with continuous subscription', () => {
|
||||
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(10, 'months')
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(10, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
cron({
|
||||
@@ -465,7 +465,7 @@ describe('cron', () => {
|
||||
user6.purchased.plan.consecutive.gemCapExtra = 10;
|
||||
|
||||
it('does not increment consecutive benefits in the first month of the first paid period that they already have benefits for', () => {
|
||||
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(1, 'months')
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(1, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
cron({
|
||||
@@ -478,7 +478,7 @@ describe('cron', () => {
|
||||
});
|
||||
|
||||
it('does not increment consecutive benefits in the final month of the period that they already have benefits for', () => {
|
||||
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(6, 'months')
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(6, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
cron({
|
||||
@@ -491,7 +491,7 @@ describe('cron', () => {
|
||||
});
|
||||
|
||||
it('increments consecutive benefits the month after the second paid period has started', () => {
|
||||
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(7, 'months')
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(7, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
cron({
|
||||
@@ -504,7 +504,7 @@ describe('cron', () => {
|
||||
});
|
||||
|
||||
it('increments consecutive benefits the month after the third paid period has started', () => {
|
||||
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(13, 'months')
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(13, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
cron({
|
||||
@@ -517,7 +517,7 @@ describe('cron', () => {
|
||||
});
|
||||
|
||||
it('increments consecutive benefits correctly if user has been absent with continuous subscription', () => {
|
||||
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(19, 'months')
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(19, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
cron({
|
||||
@@ -552,7 +552,7 @@ describe('cron', () => {
|
||||
user12.purchased.plan.consecutive.gemCapExtra = 20;
|
||||
|
||||
it('does not increment consecutive benefits in the first month of the first paid period that they already have benefits for', () => {
|
||||
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(1, 'months')
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(1, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
cron({
|
||||
@@ -565,7 +565,7 @@ describe('cron', () => {
|
||||
});
|
||||
|
||||
it('does not increment consecutive benefits in the final month of the period that they already have benefits for', () => {
|
||||
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(12, 'months')
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(12, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
cron({
|
||||
@@ -578,7 +578,7 @@ describe('cron', () => {
|
||||
});
|
||||
|
||||
it('increments consecutive benefits the month after the second paid period has started', () => {
|
||||
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(13, 'months')
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(13, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
cron({
|
||||
@@ -591,7 +591,7 @@ describe('cron', () => {
|
||||
});
|
||||
|
||||
it('increments consecutive benefits the month after the third paid period has started', () => {
|
||||
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(25, 'months')
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(25, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
cron({
|
||||
@@ -604,7 +604,7 @@ describe('cron', () => {
|
||||
});
|
||||
|
||||
it('increments consecutive benefits correctly if user has been absent with continuous subscription', () => {
|
||||
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(37, 'months')
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(37, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
cron({
|
||||
@@ -641,7 +641,7 @@ describe('cron', () => {
|
||||
user3g.purchased.plan.consecutive.gemCapExtra = 5;
|
||||
|
||||
it('does not increment consecutive benefits in the first month of the gift subscription', () => {
|
||||
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(1, 'months')
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(1, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
cron({
|
||||
@@ -654,7 +654,7 @@ describe('cron', () => {
|
||||
});
|
||||
|
||||
it('does not increment consecutive benefits in the second month of the gift subscription', () => {
|
||||
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(2, 'months')
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(2, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
cron({
|
||||
@@ -667,7 +667,7 @@ describe('cron', () => {
|
||||
});
|
||||
|
||||
it('does not increment consecutive benefits in the third month of the gift subscription', () => {
|
||||
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(3, 'months')
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(3, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
cron({
|
||||
@@ -680,7 +680,7 @@ describe('cron', () => {
|
||||
});
|
||||
|
||||
it('does not increment consecutive benefits in the month after the gift subscription has ended', () => {
|
||||
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(4, 'months')
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(4, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
cron({
|
||||
@@ -717,7 +717,7 @@ describe('cron', () => {
|
||||
user6x.purchased.plan.consecutive.gemCapExtra = 15;
|
||||
|
||||
it('increments consecutive benefits in the first month since the fix for #4819 goes live', () => {
|
||||
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(1, 'months')
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(1, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
cron({
|
||||
@@ -730,7 +730,7 @@ describe('cron', () => {
|
||||
});
|
||||
|
||||
it('does not increment consecutive benefits in the second month after the fix goes live', () => {
|
||||
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(2, 'months')
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(2, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
cron({
|
||||
@@ -743,7 +743,7 @@ describe('cron', () => {
|
||||
});
|
||||
|
||||
it('does not increment consecutive benefits in the third month after the fix goes live', () => {
|
||||
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(3, 'months')
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(3, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
cron({
|
||||
@@ -756,7 +756,7 @@ describe('cron', () => {
|
||||
});
|
||||
|
||||
it('increments consecutive benefits in the seventh month after the fix goes live', () => {
|
||||
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(7, 'months')
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(7, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
cron({
|
||||
|
||||
@@ -9,7 +9,7 @@ describe('preenHistory', () => {
|
||||
beforeEach(() => {
|
||||
// Replace system clocks so we can get predictable results
|
||||
clock = sinon.useFakeTimers({
|
||||
now: Number(moment('2013-10-20').zone(0).startOf('day').toDate()),
|
||||
now: Number(moment('2013-10-20').utcOffset(0).startOf('day').toDate()),
|
||||
toFake: ['Date'],
|
||||
});
|
||||
});
|
||||
|
||||
@@ -235,15 +235,16 @@ describe('Group Task Methods', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('removes an assigned task and unlinks assignees', async () => {
|
||||
it('removes assigned tasks when master task is deleted', async () => {
|
||||
await guild.syncTask(task, leader);
|
||||
await guild.removeTask(task);
|
||||
|
||||
const updatedLeader = await User.findOne({ _id: leader._id });
|
||||
const updatedLeadersTasks = await Tasks.Task.find({ _id: { $in: updatedLeader.tasksOrder[`${taskType}s`] } });
|
||||
const updatedLeadersTasks = await Tasks.Task.find({ userId: leader._id, type: taskType });
|
||||
const syncedTask = find(updatedLeadersTasks, findLinkedTask);
|
||||
|
||||
expect(syncedTask.group.broken).to.equal('TASK_DELETED');
|
||||
expect(updatedLeader.tasksOrder[`${taskType}s`]).to.not.include(task._id);
|
||||
expect(syncedTask).to.not.exist;
|
||||
});
|
||||
|
||||
it('unlinks and deletes group tasks for a user when remove-all is specified', async () => {
|
||||
|
||||
@@ -761,7 +761,7 @@ describe('User Model', () => {
|
||||
});
|
||||
});
|
||||
|
||||
context('days missed', () => {
|
||||
describe('daysUserHasMissed', () => {
|
||||
// http://forbrains.co.uk/international_tools/earth_timezones
|
||||
let user;
|
||||
|
||||
@@ -769,24 +769,51 @@ describe('User Model', () => {
|
||||
user = new User();
|
||||
});
|
||||
|
||||
it('should not cron early when going back a timezone', () => {
|
||||
const yesterday = moment('2017-12-05T00:00:00.000-06:00'); // 11 pm on 4 Texas
|
||||
const timezoneOffset = moment().zone('-06:00').zone();
|
||||
user.lastCron = yesterday;
|
||||
user.preferences.timezoneOffset = timezoneOffset;
|
||||
it('correctly calculates days missed since lastCron', () => {
|
||||
const now = moment();
|
||||
user.lastCron = moment(now).subtract(5, 'days');
|
||||
|
||||
const today = moment('2017-12-06T00:00:00.000-06:00'); // 11 pm on 4 Texas
|
||||
const req = {};
|
||||
req.header = () => timezoneOffset + 60;
|
||||
const { daysMissed } = user.daysUserHasMissed(now);
|
||||
|
||||
const { daysMissed } = user.daysUserHasMissed(today, req);
|
||||
expect(daysMissed).to.eql(5);
|
||||
});
|
||||
|
||||
it('uses timezone from preferences to calculate days missed', () => {
|
||||
const now = moment('2017-07-08 01:00:00Z');
|
||||
user.lastCron = moment('2017-07-04 13:00:00Z');
|
||||
user.preferences.timezoneOffset = 120;
|
||||
|
||||
const { daysMissed } = user.daysUserHasMissed(now);
|
||||
|
||||
expect(daysMissed).to.eql(3);
|
||||
});
|
||||
|
||||
it('uses timezone at last cron to calculate days missed', () => {
|
||||
const now = moment('2017-09-08 13:00:00Z');
|
||||
user.lastCron = moment('2017-09-06 01:00:00+02:00');
|
||||
user.preferences.timezoneOffset = 0;
|
||||
user.preferences.timezoneOffsetAtLastCron = -120;
|
||||
|
||||
const { daysMissed } = user.daysUserHasMissed(now);
|
||||
|
||||
expect(daysMissed).to.eql(2);
|
||||
});
|
||||
|
||||
it('respects new timezone that drags time into same day', () => {
|
||||
user.lastCron = moment('2017-12-05T00:00:00.000-06:00');
|
||||
user.preferences.timezoneOffset = 360;
|
||||
const today = moment('2017-12-06T00:00:00.000-06:00');
|
||||
const requestWithMinus7Timezone = { header: () => 420 };
|
||||
|
||||
const { daysMissed } = user.daysUserHasMissed(today, requestWithMinus7Timezone);
|
||||
|
||||
expect(user.preferences.timezoneOffset).to.eql(420);
|
||||
expect(daysMissed).to.eql(0);
|
||||
});
|
||||
|
||||
it('should not cron early when going back a timezone with a custom day start', () => {
|
||||
const yesterday = moment('2017-12-05T02:00:00.000-08:00');
|
||||
const timezoneOffset = moment().zone('-08:00').zone();
|
||||
const timezoneOffset = 480;
|
||||
user.lastCron = yesterday;
|
||||
user.preferences.timezoneOffset = timezoneOffset;
|
||||
user.preferences.dayStart = 2;
|
||||
|
||||
@@ -75,12 +75,7 @@ describe('POST /group/:groupId/remove-manager', () => {
|
||||
await nonLeader.post(`/tasks/${task._id}/assign/${nonManager._id}`);
|
||||
const memberTasks = await nonManager.get('/tasks/user');
|
||||
const syncedTask = find(memberTasks, findAssignedTask);
|
||||
await expect(nonManager.post(`/tasks/${syncedTask._id}/score/up`))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('taskApprovalHasBeenRequested'),
|
||||
});
|
||||
await nonManager.post(`/tasks/${syncedTask._id}/score/up`);
|
||||
|
||||
const updatedGroup = await leader.post(`/groups/${groupToUpdate._id}/remove-manager`, {
|
||||
managerId: nonLeader._id,
|
||||
|
||||
@@ -153,12 +153,12 @@ describe('GET /tasks/user', () => {
|
||||
});
|
||||
|
||||
xit('returns dailies with isDue for the date specified and will add CDS offset if time is not supplied and assumes timezones', async () => {
|
||||
const timezone = 420;
|
||||
const timezoneOffset = 420;
|
||||
await user.update({
|
||||
'preferences.dayStart': 0,
|
||||
'preferences.timezoneOffset': timezone,
|
||||
'preferences.timezoneOffset': timezoneOffset,
|
||||
});
|
||||
const startDate = moment().zone(timezone).subtract('4', 'days').startOf('day')
|
||||
const startDate = moment().utcOffset(-timezoneOffset).subtract('4', 'days').startOf('day')
|
||||
.toISOString();
|
||||
await user.post('/tasks/user', [
|
||||
{
|
||||
@@ -180,12 +180,12 @@ describe('GET /tasks/user', () => {
|
||||
});
|
||||
|
||||
xit('returns dailies with isDue for the date specified and will add CDS offset if time is not supplied and assumes timezones', async () => {
|
||||
const timezone = 240;
|
||||
const timezoneOffset = 240;
|
||||
await user.update({
|
||||
'preferences.dayStart': 0,
|
||||
'preferences.timezoneOffset': timezone,
|
||||
'preferences.timezoneOffset': timezoneOffset,
|
||||
});
|
||||
const startDate = moment().zone(timezone).subtract('4', 'days').startOf('day')
|
||||
const startDate = moment().utcOffset(-timezoneOffset).subtract('4', 'days').startOf('day')
|
||||
.toISOString();
|
||||
await user.post('/tasks/user', [
|
||||
{
|
||||
@@ -207,12 +207,12 @@ describe('GET /tasks/user', () => {
|
||||
});
|
||||
|
||||
xit('returns dailies with isDue for the date specified and will add CDS offset if time is not supplied and assumes timezones', async () => {
|
||||
const timezone = 540;
|
||||
const timezoneOffset = 540;
|
||||
await user.update({
|
||||
'preferences.dayStart': 0,
|
||||
'preferences.timezoneOffset': timezone,
|
||||
'preferences.timezoneOffset': timezoneOffset,
|
||||
});
|
||||
const startDate = moment().zone(timezone).subtract('4', 'days').startOf('day')
|
||||
const startDate = moment().utcOffset(-timezoneOffset).subtract('4', 'days').startOf('day')
|
||||
.toISOString();
|
||||
await user.post('/tasks/user', [
|
||||
{
|
||||
|
||||
@@ -73,12 +73,7 @@ describe('Groups DELETE /tasks/:id', () => {
|
||||
});
|
||||
const memberTasks = await member.get('/tasks/user');
|
||||
const syncedTask = find(memberTasks, findAssignedTask);
|
||||
await expect(member.post(`/tasks/${syncedTask._id}/score/up`))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('taskApprovalHasBeenRequested'),
|
||||
});
|
||||
await member.post(`/tasks/${syncedTask._id}/score/up`);
|
||||
|
||||
await user.sync();
|
||||
await member2.sync();
|
||||
@@ -96,16 +91,16 @@ describe('Groups DELETE /tasks/:id', () => {
|
||||
expect(member2.notifications.length).to.equal(1);
|
||||
});
|
||||
|
||||
it('unlinks assigned user', async () => {
|
||||
it('deletes task from assigned user', async () => {
|
||||
await user.del(`/tasks/${task._id}`);
|
||||
|
||||
const memberTasks = await member.get('/tasks/user');
|
||||
const syncedTask = find(memberTasks, findAssignedTask);
|
||||
|
||||
expect(syncedTask.group.broken).to.equal('TASK_DELETED');
|
||||
expect(syncedTask).to.not.exist;
|
||||
});
|
||||
|
||||
it('unlinks all assigned users', async () => {
|
||||
it('deletes task from all assigned users', async () => {
|
||||
await user.del(`/tasks/${task._id}`);
|
||||
|
||||
const memberTasks = await member.get('/tasks/user');
|
||||
@@ -114,8 +109,8 @@ describe('Groups DELETE /tasks/:id', () => {
|
||||
const member2Tasks = await member2.get('/tasks/user');
|
||||
const member2SyncedTask = find(member2Tasks, findAssignedTask);
|
||||
|
||||
expect(syncedTask.group.broken).to.equal('TASK_DELETED');
|
||||
expect(member2SyncedTask.group.broken).to.equal('TASK_DELETED');
|
||||
expect(syncedTask).to.not.exist;
|
||||
expect(member2SyncedTask).to.not.exist;
|
||||
});
|
||||
|
||||
it('prevents a user from deleting a task they are assigned to', async () => {
|
||||
@@ -130,22 +125,6 @@ describe('Groups DELETE /tasks/:id', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('allows a user to delete a broken task', async () => {
|
||||
const memberTasks = await member.get('/tasks/user');
|
||||
const syncedTask = find(memberTasks, findAssignedTask);
|
||||
|
||||
await user.del(`/tasks/${task._id}`);
|
||||
|
||||
await member.del(`/tasks/${syncedTask._id}`);
|
||||
|
||||
await expect(member.get(`/tasks/${syncedTask._id}`))
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 404,
|
||||
error: 'NotFound',
|
||||
message: 'Task not found.',
|
||||
});
|
||||
});
|
||||
|
||||
it('allows a user to delete a task after leaving a group', async () => {
|
||||
const memberTasks = await member.get('/tasks/user');
|
||||
const syncedTask = find(memberTasks, findAssignedTask);
|
||||
|
||||
@@ -58,22 +58,14 @@ describe('POST /tasks/:id/approve/:userId', () => {
|
||||
let memberTasks = await member.get('/tasks/user');
|
||||
let syncedTask = find(memberTasks, findAssignedTask);
|
||||
|
||||
await expect(member.post(`/tasks/${syncedTask._id}/score/up`))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('taskApprovalHasBeenRequested'),
|
||||
});
|
||||
|
||||
await member.post(`/tasks/${syncedTask._id}/score/up`);
|
||||
await user.post(`/tasks/${task._id}/approve/${member._id}`);
|
||||
|
||||
await member.sync();
|
||||
|
||||
expect(member.notifications.length).to.equal(3);
|
||||
expect(member.notifications.length).to.equal(2);
|
||||
expect(member.notifications[1].type).to.equal('GROUP_TASK_APPROVED');
|
||||
expect(member.notifications[1].data.message).to.equal(t('yourTaskHasBeenApproved', { taskText: task.text }));
|
||||
expect(member.notifications[2].type).to.equal('SCORED_TASK');
|
||||
expect(member.notifications[2].data.message).to.equal(t('yourTaskHasBeenApproved', { taskText: task.text }));
|
||||
|
||||
memberTasks = await member.get('/tasks/user');
|
||||
syncedTask = find(memberTasks, findAssignedTask);
|
||||
@@ -93,21 +85,13 @@ describe('POST /tasks/:id/approve/:userId', () => {
|
||||
let memberTasks = await member.get('/tasks/user');
|
||||
let syncedTask = find(memberTasks, findAssignedTask);
|
||||
|
||||
await expect(member.post(`/tasks/${syncedTask._id}/score/up`))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('taskApprovalHasBeenRequested'),
|
||||
});
|
||||
|
||||
await member.post(`/tasks/${syncedTask._id}/score/up`);
|
||||
await member2.post(`/tasks/${task._id}/approve/${member._id}`);
|
||||
await member.sync();
|
||||
|
||||
expect(member.notifications.length).to.equal(3);
|
||||
expect(member.notifications.length).to.equal(2);
|
||||
expect(member.notifications[1].type).to.equal('GROUP_TASK_APPROVED');
|
||||
expect(member.notifications[1].data.message).to.equal(t('yourTaskHasBeenApproved', { taskText: task.text }));
|
||||
expect(member.notifications[2].type).to.equal('SCORED_TASK');
|
||||
expect(member.notifications[2].data.message).to.equal(t('yourTaskHasBeenApproved', { taskText: task.text }));
|
||||
|
||||
memberTasks = await member.get('/tasks/user');
|
||||
syncedTask = find(memberTasks, findAssignedTask);
|
||||
@@ -125,12 +109,7 @@ describe('POST /tasks/:id/approve/:userId', () => {
|
||||
await member2.post(`/tasks/${task._id}/assign/${member._id}`);
|
||||
const memberTasks = await member.get('/tasks/user');
|
||||
const syncedTask = find(memberTasks, findAssignedTask);
|
||||
await expect(member.post(`/tasks/${syncedTask._id}/score/up`))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('taskApprovalHasBeenRequested'),
|
||||
});
|
||||
await member.post(`/tasks/${syncedTask._id}/score/up`);
|
||||
|
||||
await user.sync();
|
||||
await member2.sync();
|
||||
@@ -157,14 +136,9 @@ describe('POST /tasks/:id/approve/:userId', () => {
|
||||
|
||||
const memberTasks = await member.get('/tasks/user');
|
||||
const syncedTask = find(memberTasks, findAssignedTask);
|
||||
await expect(member.post(`/tasks/${syncedTask._id}/score/up`))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('taskApprovalHasBeenRequested'),
|
||||
});
|
||||
|
||||
await member.post(`/tasks/${syncedTask._id}/score/up`);
|
||||
await member2.post(`/tasks/${task._id}/approve/${member._id}`);
|
||||
|
||||
await expect(user.post(`/tasks/${task._id}/approve/${member._id}`))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
code: 401,
|
||||
@@ -197,13 +171,7 @@ describe('POST /tasks/:id/approve/:userId', () => {
|
||||
|
||||
const memberTasks = await member.get('/tasks/user');
|
||||
const syncedTask = find(memberTasks, findAssignedTask);
|
||||
await expect(member.post(`/tasks/${syncedTask._id}/score/up`))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('taskApprovalHasBeenRequested'),
|
||||
});
|
||||
|
||||
await member.post(`/tasks/${syncedTask._id}/score/up`);
|
||||
await user.post(`/tasks/${sharedCompletionTask._id}/approve/${member._id}`);
|
||||
|
||||
const groupTasks = await user.get(`/tasks/group/${guild._id}?type=completedTodos`);
|
||||
@@ -226,13 +194,7 @@ describe('POST /tasks/:id/approve/:userId', () => {
|
||||
|
||||
const memberTasks = await member.get('/tasks/user');
|
||||
const syncedTask = find(memberTasks, findAssignedTask);
|
||||
await expect(member.post(`/tasks/${syncedTask._id}/score/up`))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('taskApprovalHasBeenRequested'),
|
||||
});
|
||||
|
||||
await member.post(`/tasks/${syncedTask._id}/score/up`);
|
||||
await user.post(`/tasks/${sharedCompletionTask._id}/approve/${member._id}`);
|
||||
|
||||
const member2Tasks = await member2.get('/tasks/user');
|
||||
@@ -258,13 +220,7 @@ describe('POST /tasks/:id/approve/:userId', () => {
|
||||
|
||||
const memberTasks = await member.get('/tasks/user');
|
||||
const syncedTask = find(memberTasks, findAssignedTask);
|
||||
await expect(member.post(`/tasks/${syncedTask._id}/score/up`))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('taskApprovalHasBeenRequested'),
|
||||
});
|
||||
|
||||
await member.post(`/tasks/${syncedTask._id}/score/up`);
|
||||
await user.post(`/tasks/${sharedCompletionTask._id}/approve/${member._id}`);
|
||||
|
||||
const groupTasks = await user.get(`/tasks/group/${guild._id}`);
|
||||
@@ -287,21 +243,10 @@ describe('POST /tasks/:id/approve/:userId', () => {
|
||||
|
||||
const memberTasks = await member.get('/tasks/user');
|
||||
const syncedTask = find(memberTasks, findAssignedTask);
|
||||
await expect(member.post(`/tasks/${syncedTask._id}/score/up`))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('taskApprovalHasBeenRequested'),
|
||||
});
|
||||
|
||||
await member.post(`/tasks/${syncedTask._id}/score/up`);
|
||||
const member2Tasks = await member2.get('/tasks/user');
|
||||
const member2SyncedTask = find(member2Tasks, findAssignedTask);
|
||||
await expect(member2.post(`/tasks/${member2SyncedTask._id}/score/up`))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('taskApprovalHasBeenRequested'),
|
||||
});
|
||||
await member2.post(`/tasks/${member2SyncedTask._id}/score/up`);
|
||||
|
||||
await user.post(`/tasks/${sharedCompletionTask._id}/approve/${member._id}`);
|
||||
await user.post(`/tasks/${sharedCompletionTask._id}/approve/${member2._id}`);
|
||||
|
||||
@@ -61,13 +61,7 @@ describe('POST /tasks/:id/needs-work/:userId', () => {
|
||||
let syncedTask = find(memberTasks, findAssignedTask);
|
||||
|
||||
// score task to require approval
|
||||
await expect(member.post(`/tasks/${syncedTask._id}/score/up`))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('taskApprovalHasBeenRequested'),
|
||||
});
|
||||
|
||||
await member.post(`/tasks/${syncedTask._id}/score/up`);
|
||||
await user.post(`/tasks/${task._id}/needs-work/${member._id}`);
|
||||
|
||||
[memberTasks] = await Promise.all([member.get('/tasks/user'), member.sync()]);
|
||||
@@ -114,12 +108,7 @@ describe('POST /tasks/:id/needs-work/:userId', () => {
|
||||
let syncedTask = find(memberTasks, findAssignedTask);
|
||||
|
||||
// score task to require approval
|
||||
await expect(member.post(`/tasks/${syncedTask._id}/score/up`))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('taskApprovalHasBeenRequested'),
|
||||
});
|
||||
await member.post(`/tasks/${syncedTask._id}/score/up`);
|
||||
|
||||
const initialNotifications = member.notifications.length;
|
||||
|
||||
@@ -172,13 +161,7 @@ describe('POST /tasks/:id/needs-work/:userId', () => {
|
||||
const memberTasks = await member.get('/tasks/user');
|
||||
const syncedTask = find(memberTasks, findAssignedTask);
|
||||
|
||||
await expect(member.post(`/tasks/${syncedTask._id}/score/up`))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('taskApprovalHasBeenRequested'),
|
||||
});
|
||||
|
||||
await member.post(`/tasks/${syncedTask._id}/score/up`);
|
||||
await member2.post(`/tasks/${task._id}/approve/${member._id}`);
|
||||
await expect(user.post(`/tasks/${task._id}/needs-work/${member._id}`))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
|
||||
@@ -44,12 +44,11 @@ describe('POST /tasks/:id/score/:direction', () => {
|
||||
const syncedTask = find(memberTasks, findAssignedTask);
|
||||
const direction = 'up';
|
||||
|
||||
await expect(member.post(`/tasks/${syncedTask._id}/score/${direction}`))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('taskApprovalHasBeenRequested'),
|
||||
});
|
||||
const response = await member.post(`/tasks/${syncedTask._id}/score/${direction}`);
|
||||
|
||||
expect(response.data.approvalRequested).to.equal(true);
|
||||
expect(response.message).to.equal(t('taskApprovalHasBeenRequested'));
|
||||
|
||||
const updatedTask = await member.get(`/tasks/${syncedTask._id}`);
|
||||
|
||||
await user.sync();
|
||||
@@ -76,12 +75,7 @@ describe('POST /tasks/:id/score/:direction', () => {
|
||||
const syncedTask = find(memberTasks, findAssignedTask);
|
||||
const direction = 'up';
|
||||
|
||||
await expect(member.post(`/tasks/${syncedTask._id}/score/${direction}`))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('taskApprovalHasBeenRequested'),
|
||||
});
|
||||
await member.post(`/tasks/${syncedTask._id}/score/${direction}`);
|
||||
const updatedTask = await member.get(`/tasks/${syncedTask._id}`);
|
||||
await user.sync();
|
||||
await member2.sync();
|
||||
@@ -111,12 +105,7 @@ describe('POST /tasks/:id/score/:direction', () => {
|
||||
const memberTasks = await member.get('/tasks/user');
|
||||
const syncedTask = find(memberTasks, findAssignedTask);
|
||||
|
||||
await expect(member.post(`/tasks/${syncedTask._id}/score/up`))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('taskApprovalHasBeenRequested'),
|
||||
});
|
||||
await member.post(`/tasks/${syncedTask._id}/score/up`);
|
||||
|
||||
await expect(member.post(`/tasks/${syncedTask._id}/score/up`))
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
@@ -130,12 +119,7 @@ describe('POST /tasks/:id/score/:direction', () => {
|
||||
const memberTasks = await member.get('/tasks/user');
|
||||
const syncedTask = find(memberTasks, findAssignedTask);
|
||||
|
||||
await expect(member.post(`/tasks/${syncedTask._id}/score/up`))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('taskApprovalHasBeenRequested'),
|
||||
});
|
||||
await member.post(`/tasks/${syncedTask._id}/score/up`);
|
||||
|
||||
await user.post(`/tasks/${task._id}/approve/${member._id}`);
|
||||
|
||||
|
||||
@@ -71,12 +71,10 @@ describe('PUT /tasks/:id', () => {
|
||||
const syncedTask = find(memberTasks, memberTask => memberTask.group.taskId === habit._id);
|
||||
|
||||
// score up to trigger approval
|
||||
await expect(member2.post(`/tasks/${syncedTask._id}/score/up`))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('taskApprovalHasBeenRequested'),
|
||||
});
|
||||
const response = await member2.post(`/tasks/${syncedTask._id}/score/up`);
|
||||
|
||||
expect(response.data.approvalRequested).to.equal(true);
|
||||
expect(response.message).to.equal(t('taskApprovalHasBeenRequested'));
|
||||
});
|
||||
|
||||
it('member updates a group task value - not allowed', async () => {
|
||||
|
||||
25
test/common/fns/getUtcOffset.test.js
Normal file
25
test/common/fns/getUtcOffset.test.js
Normal file
@@ -0,0 +1,25 @@
|
||||
import getUtcOffset from '../../../website/common/script/fns/getUtcOffset';
|
||||
|
||||
describe('getUtcOffset', () => {
|
||||
let user;
|
||||
|
||||
beforeEach(() => {
|
||||
user = { preferences: {} };
|
||||
});
|
||||
|
||||
it('returns 0 when user.timezoneOffset is not set', () => {
|
||||
expect(getUtcOffset(user)).to.equal(0);
|
||||
});
|
||||
|
||||
it('returns 0 when user.timezoneOffset is zero', () => {
|
||||
user.preferences.timezoneOffset = 0;
|
||||
|
||||
expect(getUtcOffset(user)).to.equal(0);
|
||||
});
|
||||
|
||||
it('returns the opposite of user.timezoneOffset', () => {
|
||||
user.preferences.timezoneOffset = -10;
|
||||
|
||||
expect(getUtcOffset(user)).to.eql(10);
|
||||
});
|
||||
});
|
||||
184
test/common/libs/cron.test.js
Normal file
184
test/common/libs/cron.test.js
Normal file
@@ -0,0 +1,184 @@
|
||||
import moment from 'moment';
|
||||
|
||||
import { startOfDay, daysSince } from '../../../website/common/script/cron';
|
||||
|
||||
function localMoment (timeString, utcOffset) {
|
||||
return moment(timeString).utcOffset(utcOffset, true);
|
||||
}
|
||||
|
||||
describe('cron utility functions', () => {
|
||||
describe('startOfDay', () => {
|
||||
it('is zero when no daystart configured', () => {
|
||||
const options = { now: moment('2020-02-02 09:30:00Z'), timezoneOffset: 0 };
|
||||
|
||||
const result = startOfDay(options);
|
||||
|
||||
expect(result).to.be.sameMoment('2020-02-02 00:00:00Z');
|
||||
});
|
||||
|
||||
it('is zero when negative daystart configured', () => {
|
||||
const options = {
|
||||
now: moment('2020-02-02 09:30:00Z'),
|
||||
timezoneOffset: 0,
|
||||
daystart: -5,
|
||||
};
|
||||
|
||||
const result = startOfDay(options);
|
||||
|
||||
expect(result).to.be.sameMoment('2020-02-02 00:00:00Z');
|
||||
});
|
||||
|
||||
it('is zero when daystart over 24 is configured', () => {
|
||||
const options = {
|
||||
now: moment('2020-02-02 09:30:00Z'),
|
||||
timezoneOffset: 0,
|
||||
daystart: 25,
|
||||
};
|
||||
|
||||
const result = startOfDay(options);
|
||||
|
||||
expect(result).to.be.sameMoment('2020-02-02 00:00:00Z');
|
||||
});
|
||||
|
||||
it('is equal to daystart o\'clock when daystart configured', () => {
|
||||
const options = {
|
||||
now: moment('2020-02-02 09:30:00Z'),
|
||||
timezoneOffset: 0,
|
||||
dayStart: 5,
|
||||
};
|
||||
|
||||
const result = startOfDay(options);
|
||||
|
||||
expect(result).to.be.sameMoment('2020-02-02 05:00:00Z');
|
||||
});
|
||||
|
||||
it('is previous day daystart o\'clock when daystart is after current time', () => {
|
||||
const options = {
|
||||
now: moment('2020-02-02 04:30:00Z'),
|
||||
timezoneOffset: 0,
|
||||
dayStart: 5,
|
||||
};
|
||||
|
||||
const result = startOfDay(options);
|
||||
|
||||
expect(result).to.be.sameMoment('2020-02-01 05:00:00Z');
|
||||
});
|
||||
|
||||
it('is daystart o\'clock when daystart is after current time due to timezone', () => {
|
||||
const options = {
|
||||
now: moment('2020-02-02 04:30:00Z'),
|
||||
timezoneOffset: -120,
|
||||
dayStart: 5,
|
||||
};
|
||||
|
||||
const result = startOfDay(options);
|
||||
|
||||
expect(result).to.be.sameMoment('2020-02-02 05:00:00+02:00');
|
||||
});
|
||||
|
||||
it('returns in default timezone if no timezone defined', () => {
|
||||
const utcOffset = moment().utcOffset();
|
||||
const now = localMoment('2020-02-02 04:30:00', utcOffset).utc();
|
||||
|
||||
const result = startOfDay({ now });
|
||||
|
||||
expect(result).to.be.sameMoment(localMoment('2020-02-02', utcOffset));
|
||||
});
|
||||
|
||||
it('returns in default timezone if timezone lower than -12:00', () => {
|
||||
const utcOffset = moment().utcOffset();
|
||||
const options = {
|
||||
now: localMoment('2020-02-02 17:30:00', utcOffset).utc(),
|
||||
timezoneOffset: 721,
|
||||
};
|
||||
|
||||
const result = startOfDay(options);
|
||||
|
||||
expect(result).to.be.sameMoment(localMoment('2020-02-02', utcOffset));
|
||||
});
|
||||
|
||||
it('returns in default timezone if timezone higher than +14:00', () => {
|
||||
const utcOffset = moment().utcOffset();
|
||||
const options = {
|
||||
now: localMoment('2020-02-02 07:32:25.376', utcOffset).utc(),
|
||||
timezoneOffset: -841,
|
||||
};
|
||||
|
||||
const result = startOfDay(options);
|
||||
|
||||
expect(result).to.be.sameMoment(localMoment('2020-02-02', utcOffset));
|
||||
});
|
||||
|
||||
it('returns in overridden timezone if override present', () => {
|
||||
const options = {
|
||||
now: moment('2020-02-02 13:30:27Z'),
|
||||
timezoneOffset: 0,
|
||||
timezoneUtcOffsetOverride: -240,
|
||||
};
|
||||
|
||||
const result = startOfDay(options);
|
||||
|
||||
expect(result).to.be.sameMoment('2020-02-02 00:00:00-04:00');
|
||||
});
|
||||
|
||||
it('returns start of yesterday if timezone difference carries it over datelines', () => {
|
||||
const offset = 300;
|
||||
const options = {
|
||||
now: moment('2020-02-02 04:30:00Z'),
|
||||
timezoneOffset: offset,
|
||||
};
|
||||
|
||||
const result = startOfDay(options);
|
||||
|
||||
expect(result).to.be.sameMoment(localMoment('2020-02-01', -offset));
|
||||
});
|
||||
});
|
||||
|
||||
describe('daysSince', () => {
|
||||
it('correctly calculates days between two dates', () => {
|
||||
const now = moment();
|
||||
const dayBeforeYesterday = moment(now).subtract({ days: 2 });
|
||||
|
||||
expect(daysSince(dayBeforeYesterday, { now })).to.equal(2);
|
||||
});
|
||||
|
||||
it('is one lower if current time is before dayStart', () => {
|
||||
const oneWeekAgoAtOnePm = moment().hour(13).subtract({ days: 7 });
|
||||
const thisMorningThreeAm = moment().hour(3);
|
||||
const options = {
|
||||
now: thisMorningThreeAm,
|
||||
dayStart: 6,
|
||||
};
|
||||
|
||||
const result = daysSince(oneWeekAgoAtOnePm, options);
|
||||
|
||||
expect(result).to.equal(6);
|
||||
});
|
||||
|
||||
it('is one higher if reference time is before dayStart and current time after dayStart', () => {
|
||||
const oneWeekAgoAtEightAm = moment().hour(8).subtract({ days: 7 });
|
||||
const todayAtFivePm = moment().hour(17);
|
||||
const options = {
|
||||
now: todayAtFivePm,
|
||||
dayStart: 11,
|
||||
};
|
||||
|
||||
const result = daysSince(oneWeekAgoAtEightAm, options);
|
||||
|
||||
expect(result).to.equal(8);
|
||||
});
|
||||
|
||||
// Variations in timezone configuration options are already covered by startOfDay tests.
|
||||
it('uses now in user timezone as configured in options', () => {
|
||||
const timezoneOffset = 120;
|
||||
const options = {
|
||||
now: moment('1989-11-09 02:53:00+01:00'),
|
||||
timezoneOffset,
|
||||
};
|
||||
|
||||
const result = daysSince(localMoment('1989-11-08', -timezoneOffset), options);
|
||||
|
||||
expect(result).to.equal(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,7 @@
|
||||
import moment from 'moment';
|
||||
|
||||
import taskDefaults from '../../../website/common/script/libs/taskDefaults';
|
||||
import getUtcOffset from '../../../website/common/script/fns/getUtcOffset';
|
||||
import { generateUser } from '../../helpers/common.helper';
|
||||
|
||||
describe('taskDefaults', () => {
|
||||
@@ -72,7 +73,7 @@ describe('taskDefaults', () => {
|
||||
|
||||
expect(task.startDate).to.eql(
|
||||
moment()
|
||||
.zone(user.preferences.timezoneOffset, 'hour')
|
||||
.utcOffset(getUtcOffset(user))
|
||||
.startOf('day')
|
||||
.subtract(1, 'day')
|
||||
.toDate(),
|
||||
|
||||
@@ -5,6 +5,8 @@ import 'moment-recur';
|
||||
describe('shouldDo', () => {
|
||||
let day; let
|
||||
dailyTask;
|
||||
// Options is a mapping of user.preferences, therefor `timezoneOffset` still holds old zone
|
||||
// values instead of utcOffset values.
|
||||
let options = {};
|
||||
let nextDue = [];
|
||||
|
||||
@@ -80,17 +82,17 @@ describe('shouldDo', () => {
|
||||
|
||||
it('returns true if the user\'s current time is after start date and Custom Day Start', () => {
|
||||
options.dayStart = 4;
|
||||
day = moment().zone(options.timezoneOffset).startOf('day').add(6, 'hours')
|
||||
day = moment().utcOffset(-options.timezoneOffset).startOf('day').add(6, 'hours')
|
||||
.toDate();
|
||||
dailyTask.startDate = moment().zone(options.timezoneOffset).startOf('day').toDate();
|
||||
dailyTask.startDate = moment().utcOffset(-options.timezoneOffset).startOf('day').toDate();
|
||||
expect(shouldDo(day, dailyTask, options)).to.equal(true);
|
||||
});
|
||||
|
||||
it('returns false if the user\'s current time is before Custom Day Start', () => {
|
||||
options.dayStart = 8;
|
||||
day = moment().zone(options.timezoneOffset).startOf('day').add(2, 'hours')
|
||||
day = moment().utcOffset(-options.timezoneOffset).startOf('day').add(2, 'hours')
|
||||
.toDate();
|
||||
dailyTask.startDate = moment().zone(options.timezoneOffset).startOf('day').toDate();
|
||||
dailyTask.startDate = moment().utcOffset(-options.timezoneOffset).startOf('day').toDate();
|
||||
expect(shouldDo(day, dailyTask, options)).to.equal(false);
|
||||
});
|
||||
});
|
||||
@@ -112,14 +114,14 @@ describe('shouldDo', () => {
|
||||
|
||||
it('returns true if the user\'s current time is after Custom Day Start', () => {
|
||||
options.dayStart = 4;
|
||||
day = moment().zone(options.timezoneOffset).startOf('day').add(6, 'hours')
|
||||
day = moment().utcOffset(-options.timezoneOffset).startOf('day').add(6, 'hours')
|
||||
.toDate();
|
||||
expect(shouldDo(day, dailyTask, options)).to.equal(true);
|
||||
});
|
||||
|
||||
it('returns false if the user\'s current time is before Custom Day Start', () => {
|
||||
options.dayStart = 8;
|
||||
day = moment().zone(options.timezoneOffset).startOf('day').add(2, 'hours')
|
||||
day = moment().utcOffset(-options.timezoneOffset).startOf('day').add(2, 'hours')
|
||||
.toDate();
|
||||
expect(shouldDo(day, dailyTask, options)).to.equal(false);
|
||||
});
|
||||
|
||||
@@ -8,8 +8,9 @@
|
||||
//------------------------------
|
||||
global._ = require('lodash');
|
||||
global.chai = require('chai');
|
||||
chai.use(require('sinon-chai'));
|
||||
chai.use(require('chai-as-promised'));
|
||||
chai.use(require('chai-moment'));
|
||||
chai.use(require('sinon-chai'));
|
||||
|
||||
global.expect = chai.expect;
|
||||
global.sinon = require('sinon');
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
/* eslint-disable import/no-extraneous-dependencies */
|
||||
import { configure } from '@storybook/vue';
|
||||
import './margin.css';
|
||||
import '../../src/assets/scss/index.scss';
|
||||
import '../../src/assets/scss/spacing.scss';
|
||||
import '../../src/assets/css/sprites.css';
|
||||
|
||||
import '../../src/assets/css/sprites/spritesmith-main-0.css';
|
||||
|
||||
13
website/client/config/storybook/margin.css
Normal file
13
website/client/config/storybook/margin.css
Normal file
@@ -0,0 +1,13 @@
|
||||
.background {
|
||||
background: teal;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.content {
|
||||
color: white;
|
||||
background: grey;
|
||||
}
|
||||
|
||||
.inline-block {
|
||||
display: inline-block;
|
||||
}
|
||||
292
website/client/package-lock.json
generated
292
website/client/package-lock.json
generated
@@ -6336,6 +6336,13 @@
|
||||
"bn.js": "^4.0.0",
|
||||
"inherits": "^2.0.1",
|
||||
"minimalistic-assert": "^1.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"bn.js": {
|
||||
"version": "4.11.9",
|
||||
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz",
|
||||
"integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"assert": {
|
||||
@@ -7074,9 +7081,9 @@
|
||||
"integrity": "sha512-j4nzWIqEFpLSbdhUApHRGDwfXbV8ALhqOn+FY5L6XBdKPAXU9BpGgFSbDsgqogfqPPR9R2WooseWCsfhfEC6uQ=="
|
||||
},
|
||||
"bn.js": {
|
||||
"version": "4.11.8",
|
||||
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz",
|
||||
"integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA=="
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.1.2.tgz",
|
||||
"integrity": "sha512-40rZaf3bUNKTVYu9sIeeEGOg7g14Yvnj9kH7b50EiwX0Q7A6umbvfI5tvHaOERH0XigqKkfLkFQxzb4e6CIXnA=="
|
||||
},
|
||||
"body-parser": {
|
||||
"version": "1.19.0",
|
||||
@@ -7355,20 +7362,46 @@
|
||||
"requires": {
|
||||
"bn.js": "^4.1.0",
|
||||
"randombytes": "^2.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"bn.js": {
|
||||
"version": "4.11.9",
|
||||
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz",
|
||||
"integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"browserify-sign": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.0.4.tgz",
|
||||
"integrity": "sha1-qk62jl17ZYuqa/alfmMMvXqT0pg=",
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.0.tgz",
|
||||
"integrity": "sha512-hEZC1KEeYuoHRqhGhTy6gWrpJA3ZDjFWv0DE61643ZnOXAKJb3u7yWcrU0mMc9SwAqK1n7myPGndkp0dFG7NFA==",
|
||||
"requires": {
|
||||
"bn.js": "^4.1.1",
|
||||
"browserify-rsa": "^4.0.0",
|
||||
"create-hash": "^1.1.0",
|
||||
"create-hmac": "^1.1.2",
|
||||
"elliptic": "^6.0.0",
|
||||
"inherits": "^2.0.1",
|
||||
"parse-asn1": "^5.0.0"
|
||||
"bn.js": "^5.1.1",
|
||||
"browserify-rsa": "^4.0.1",
|
||||
"create-hash": "^1.2.0",
|
||||
"create-hmac": "^1.1.7",
|
||||
"elliptic": "^6.5.2",
|
||||
"inherits": "^2.0.4",
|
||||
"parse-asn1": "^5.1.5",
|
||||
"readable-stream": "^3.6.0",
|
||||
"safe-buffer": "^5.2.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"readable-stream": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
|
||||
"integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
|
||||
"requires": {
|
||||
"inherits": "^2.0.3",
|
||||
"string_decoder": "^1.1.1",
|
||||
"util-deprecate": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"safe-buffer": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"browserify-zlib": {
|
||||
@@ -8490,6 +8523,13 @@
|
||||
"requires": {
|
||||
"bn.js": "^4.1.0",
|
||||
"elliptic": "^6.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"bn.js": {
|
||||
"version": "4.11.9",
|
||||
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz",
|
||||
"integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"create-hash": {
|
||||
@@ -9110,6 +9150,13 @@
|
||||
"bn.js": "^4.1.0",
|
||||
"miller-rabin": "^4.0.0",
|
||||
"randombytes": "^2.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"bn.js": {
|
||||
"version": "4.11.9",
|
||||
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz",
|
||||
"integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"dir-glob": {
|
||||
@@ -9371,9 +9418,9 @@
|
||||
}
|
||||
},
|
||||
"elliptic": {
|
||||
"version": "6.5.2",
|
||||
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.2.tgz",
|
||||
"integrity": "sha512-f4x70okzZbIQl/NSRLkI/+tteV/9WqL98zx+SQ69KbXxmVrmjwsNUPn/gYJJ0sHvEak24cZgHIPegRePAtA/xw==",
|
||||
"version": "6.5.3",
|
||||
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.3.tgz",
|
||||
"integrity": "sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw==",
|
||||
"requires": {
|
||||
"bn.js": "^4.4.0",
|
||||
"brorand": "^1.0.1",
|
||||
@@ -9382,6 +9429,13 @@
|
||||
"inherits": "^2.0.1",
|
||||
"minimalistic-assert": "^1.0.0",
|
||||
"minimalistic-crypto-utils": "^1.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"bn.js": {
|
||||
"version": "4.11.9",
|
||||
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz",
|
||||
"integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"emoji-regex": {
|
||||
@@ -9426,9 +9480,9 @@
|
||||
}
|
||||
},
|
||||
"enhanced-resolve": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.1.1.tgz",
|
||||
"integrity": "sha512-98p2zE+rL7/g/DzMHMTF4zZlCgeVdJ7yr6xzEpJRYwFYrGi9ANdn5DnJURg6RpBkyk60XYDnWIv51VfIhfNGuA==",
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.3.0.tgz",
|
||||
"integrity": "sha512-3e87LvavsdxyoCfGusJnrZ5G8SLPOFeHSNpZI/ATL9a5leXo2k0w6MKnbqhdBad9qTobSfB20Ld7UmgoNbAZkQ==",
|
||||
"requires": {
|
||||
"graceful-fs": "^4.1.2",
|
||||
"memory-fs": "^0.5.0",
|
||||
@@ -10021,9 +10075,9 @@
|
||||
"integrity": "sha512-qerSRB0p+UDEssxTtm6EDKcE7W4OaoisfIMl4CngyEhjpYglocpNg6UEqCvemdGhosAsg4sO2dXJOdyBifPGCg=="
|
||||
},
|
||||
"events": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/events/-/events-3.1.0.tgz",
|
||||
"integrity": "sha512-Rv+u8MLHNOdMjTAFeT3nCjHn2aGlx435FP/sDHNaRhDEMwyI/aB22Kj2qIN8R0cw3z28psEQLYwxVKLsKrMgWg=="
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/events/-/events-3.2.0.tgz",
|
||||
"integrity": "sha512-/46HWwbfCX2xTawVfkKLGxMifJYQBWMwY1mjywRtb4c9x8l5NP3KoJtnIOiL1hfdRkIuYhETxQlo62IF8tcnlg=="
|
||||
},
|
||||
"eventsource": {
|
||||
"version": "1.0.7",
|
||||
@@ -11647,12 +11701,30 @@
|
||||
}
|
||||
},
|
||||
"hash-base": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.4.tgz",
|
||||
"integrity": "sha1-X8hoaEfs1zSZQDMZprCj8/auSRg=",
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.0.tgz",
|
||||
"integrity": "sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==",
|
||||
"requires": {
|
||||
"inherits": "^2.0.1",
|
||||
"safe-buffer": "^5.0.1"
|
||||
"inherits": "^2.0.4",
|
||||
"readable-stream": "^3.6.0",
|
||||
"safe-buffer": "^5.2.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"readable-stream": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
|
||||
"integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
|
||||
"requires": {
|
||||
"inherits": "^2.0.3",
|
||||
"string_decoder": "^1.1.1",
|
||||
"util-deprecate": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"safe-buffer": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"hash-sum": {
|
||||
@@ -13556,6 +13628,13 @@
|
||||
"requires": {
|
||||
"bn.js": "^4.0.0",
|
||||
"brorand": "^1.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"bn.js": {
|
||||
"version": "4.11.9",
|
||||
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz",
|
||||
"integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"mime": {
|
||||
@@ -14998,9 +15077,9 @@
|
||||
"integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA="
|
||||
},
|
||||
"pbkdf2": {
|
||||
"version": "3.0.17",
|
||||
"resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.17.tgz",
|
||||
"integrity": "sha512-U/il5MsrZp7mGg3mSQfn742na2T+1/vHDCG5/iTI3X9MKUuYUZVLQhyRsg06mCgDBTd57TxzgZt7P+fYfjRLtA==",
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.1.tgz",
|
||||
"integrity": "sha512-4Ejy1OPxi9f2tt1rRV7Go7zmfDQ+ZectEQz3VGUQhgq62HtIRPDyG/JtnwIxs6x3uNMwo2V7q1fMvKjb+Tnpqg==",
|
||||
"requires": {
|
||||
"create-hash": "^1.1.2",
|
||||
"create-hmac": "^1.1.4",
|
||||
@@ -16060,6 +16139,13 @@
|
||||
"parse-asn1": "^5.0.0",
|
||||
"randombytes": "^2.0.1",
|
||||
"safe-buffer": "^5.1.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"bn.js": {
|
||||
"version": "4.11.9",
|
||||
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz",
|
||||
"integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"pump": {
|
||||
@@ -19829,13 +19915,123 @@
|
||||
}
|
||||
},
|
||||
"watchpack": {
|
||||
"version": "1.6.1",
|
||||
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.6.1.tgz",
|
||||
"integrity": "sha512-+IF9hfUFOrYOOaKyfaI7h7dquUIOgyEMoQMLA7OP5FxegKA2+XdXThAZ9TU2kucfhDH7rfMHs1oPYziVGWRnZA==",
|
||||
"version": "1.7.4",
|
||||
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.7.4.tgz",
|
||||
"integrity": "sha512-aWAgTW4MoSJzZPAicljkO1hsi1oKj/RRq/OJQh2PKI2UKL04c2Bs+MBOB+BBABHTXJpf9mCwHN7ANCvYsvY2sg==",
|
||||
"requires": {
|
||||
"chokidar": "^2.1.8",
|
||||
"chokidar": "^3.4.1",
|
||||
"graceful-fs": "^4.1.2",
|
||||
"neo-async": "^2.5.0"
|
||||
"neo-async": "^2.5.0",
|
||||
"watchpack-chokidar2": "^2.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"anymatch": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz",
|
||||
"integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"normalize-path": "^3.0.0",
|
||||
"picomatch": "^2.0.4"
|
||||
}
|
||||
},
|
||||
"binary-extensions": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.1.0.tgz",
|
||||
"integrity": "sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ==",
|
||||
"optional": true
|
||||
},
|
||||
"braces": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
|
||||
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"fill-range": "^7.0.1"
|
||||
}
|
||||
},
|
||||
"chokidar": {
|
||||
"version": "3.4.1",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.4.1.tgz",
|
||||
"integrity": "sha512-TQTJyr2stihpC4Sya9hs2Xh+O2wf+igjL36Y75xx2WdHuiICcn/XJza46Jwt0eT5hVpQOzo3FpY3cj3RVYLX0g==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"anymatch": "~3.1.1",
|
||||
"braces": "~3.0.2",
|
||||
"fsevents": "~2.1.2",
|
||||
"glob-parent": "~5.1.0",
|
||||
"is-binary-path": "~2.1.0",
|
||||
"is-glob": "~4.0.1",
|
||||
"normalize-path": "~3.0.0",
|
||||
"readdirp": "~3.4.0"
|
||||
}
|
||||
},
|
||||
"fill-range": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
|
||||
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"to-regex-range": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"fsevents": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz",
|
||||
"integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==",
|
||||
"optional": true
|
||||
},
|
||||
"glob-parent": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz",
|
||||
"integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"is-glob": "^4.0.1"
|
||||
}
|
||||
},
|
||||
"is-binary-path": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
|
||||
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"binary-extensions": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"is-number": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
||||
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
|
||||
"optional": true
|
||||
},
|
||||
"readdirp": {
|
||||
"version": "3.4.0",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.4.0.tgz",
|
||||
"integrity": "sha512-0xe001vZBnJEK+uKcj8qOhyAKPzIT+gStxWr3LCB0DwcXR5NZJ3IaC+yGnHCYzB/S7ov3m3EEbZI2zeNvX+hGQ==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"picomatch": "^2.2.1"
|
||||
}
|
||||
},
|
||||
"to-regex-range": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"is-number": "^7.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"watchpack-chokidar2": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/watchpack-chokidar2/-/watchpack-chokidar2-2.0.0.tgz",
|
||||
"integrity": "sha512-9TyfOyN/zLUbA288wZ8IsMZ+6cbzvsNyEzSBp6e/zkifi6xxbl8SmQ/CxQq32k8NNqrdVEVUVSEf56L4rQ/ZxA==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"chokidar": "^2.1.8"
|
||||
}
|
||||
},
|
||||
"wbuf": {
|
||||
@@ -19860,9 +20056,9 @@
|
||||
"integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg=="
|
||||
},
|
||||
"webpack": {
|
||||
"version": "4.43.0",
|
||||
"resolved": "https://registry.npmjs.org/webpack/-/webpack-4.43.0.tgz",
|
||||
"integrity": "sha512-GW1LjnPipFW2Y78OOab8NJlCflB7EFskMih2AHdvjbpKMeDJqEgSx24cXXXiPS65+WSwVyxtDsJH6jGX2czy+g==",
|
||||
"version": "4.44.0",
|
||||
"resolved": "https://registry.npmjs.org/webpack/-/webpack-4.44.0.tgz",
|
||||
"integrity": "sha512-wAuJxK123sqAw31SpkPiPW3iKHgFUiKvO7E7UZjtdExcsRe3fgav4mvoMM7vvpjLHVoJ6a0Mtp2fzkoA13e0Zw==",
|
||||
"requires": {
|
||||
"@webassemblyjs/ast": "1.9.0",
|
||||
"@webassemblyjs/helper-module-context": "1.9.0",
|
||||
@@ -19872,7 +20068,7 @@
|
||||
"ajv": "^6.10.2",
|
||||
"ajv-keywords": "^3.4.1",
|
||||
"chrome-trace-event": "^1.0.2",
|
||||
"enhanced-resolve": "^4.1.0",
|
||||
"enhanced-resolve": "^4.3.0",
|
||||
"eslint-scope": "^4.0.3",
|
||||
"json-parse-better-errors": "^1.0.2",
|
||||
"loader-runner": "^2.4.0",
|
||||
@@ -19885,7 +20081,7 @@
|
||||
"schema-utils": "^1.0.0",
|
||||
"tapable": "^1.1.3",
|
||||
"terser-webpack-plugin": "^1.4.3",
|
||||
"watchpack": "^1.6.1",
|
||||
"watchpack": "^1.7.4",
|
||||
"webpack-sources": "^1.4.1"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -19902,21 +20098,29 @@
|
||||
"minimist": "^1.2.5"
|
||||
}
|
||||
},
|
||||
"serialize-javascript": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-3.1.0.tgz",
|
||||
"integrity": "sha512-JIJT1DGiWmIKhzRsG91aS6Ze4sFUrYbltlkg2onR5OrnNM02Kl/hnY/T4FN2omvyeBbQmMJv+K4cPOpGzOTFBg==",
|
||||
"requires": {
|
||||
"randombytes": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"source-map": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
|
||||
},
|
||||
"terser-webpack-plugin": {
|
||||
"version": "1.4.3",
|
||||
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.4.3.tgz",
|
||||
"integrity": "sha512-QMxecFz/gHQwteWwSo5nTc6UaICqN1bMedC5sMtUc7y3Ha3Q8y6ZO0iCR8pq4RJC8Hjf0FEPEHZqcMB/+DFCrA==",
|
||||
"version": "1.4.4",
|
||||
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.4.4.tgz",
|
||||
"integrity": "sha512-U4mACBHIegmfoEe5fdongHESNJWqsGU+W0S/9+BmYGVQDw1+c2Ow05TpMhxjPK1sRb7cuYq1BPl1e5YHJMTCqA==",
|
||||
"requires": {
|
||||
"cacache": "^12.0.2",
|
||||
"find-cache-dir": "^2.1.0",
|
||||
"is-wsl": "^1.1.0",
|
||||
"schema-utils": "^1.0.0",
|
||||
"serialize-javascript": "^2.1.2",
|
||||
"serialize-javascript": "^3.1.0",
|
||||
"source-map": "^0.6.1",
|
||||
"terser": "^4.1.2",
|
||||
"webpack-sources": "^1.4.0",
|
||||
|
||||
@@ -60,6 +60,6 @@
|
||||
"vue-template-compiler": "^2.6.11",
|
||||
"vuedraggable": "^2.24.0",
|
||||
"vuejs-datepicker": "git://github.com/habitrpg/vuejs-datepicker.git#5d237615463a84a23dd6f3f77c6ab577d68593ec",
|
||||
"webpack": "^4.43.0"
|
||||
"webpack": "^4.44.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -297,7 +297,7 @@ export default {
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState(['isUserLoggedIn', 'browserTimezoneOffset', 'isUserLoaded', 'notificationsRemoved']),
|
||||
...mapState(['isUserLoggedIn', 'browserTimezoneUtcOffset', 'isUserLoaded', 'notificationsRemoved']),
|
||||
...mapState({ user: 'user.data' }),
|
||||
isStaticPage () {
|
||||
return this.$route.meta.requiresLogin === false;
|
||||
@@ -493,9 +493,10 @@ export default {
|
||||
this.hideLoadingScreen();
|
||||
|
||||
// Adjust the timezone offset
|
||||
if (this.user.preferences.timezoneOffset !== this.browserTimezoneOffset) {
|
||||
const browserTimezoneOffset = -this.browserTimezoneUtcOffset;
|
||||
if (this.user.preferences.timezoneOffset !== browserTimezoneOffset) {
|
||||
this.$store.dispatch('user:set', {
|
||||
'preferences.timezoneOffset': this.browserTimezoneOffset,
|
||||
'preferences.timezoneOffset': browserTimezoneOffset,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -52,9 +52,11 @@
|
||||
border-color: $purple-400;
|
||||
}
|
||||
|
||||
&:not(:disabled):not(.disabled):active:focus, &:not(:disabled):not(.disabled).active:focus {
|
||||
box-shadow: none;
|
||||
border-color: $purple-400;
|
||||
&:not(:disabled):not(.disabled) {
|
||||
&:active:focus, &.active:focus {
|
||||
box-shadow: none;
|
||||
border-color: $purple-400;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(:disabled):not(.disabled):active, &:not(:disabled):not(.disabled).active {
|
||||
|
||||
@@ -1,20 +1,27 @@
|
||||
.dropdown > .btn {
|
||||
padding: 9px 15.5px;
|
||||
padding: 0.25rem 0.75rem;
|
||||
font-family: 'Roboto', sans-serif;
|
||||
font-size: 14px;
|
||||
font-weight: normal;
|
||||
line-height: 1.43;
|
||||
}
|
||||
|
||||
.dropdown-toggle:hover {
|
||||
--caret-color: #{$purple-200};
|
||||
}
|
||||
|
||||
.dropdown.show > .dropdown-toggle:not(.btn-success) {
|
||||
color: $purple-200;
|
||||
border-color: $purple-500 !important;
|
||||
border-color: $purple-400 !important;
|
||||
box-shadow: none;
|
||||
|
||||
&::after {
|
||||
--caret-color: #{$purple-200};
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-toggle::after {
|
||||
margin-left: 16px;
|
||||
border-top: 6px solid;
|
||||
border-top-color: var(--caret-color);
|
||||
border-right: 5px solid transparent;
|
||||
border-left: 5px solid transparent;
|
||||
vertical-align: 0;
|
||||
@@ -23,14 +30,18 @@
|
||||
.dropdown-menu {
|
||||
padding: 0px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 2px 0 rgba($black, 0.15), 0 1px 4px 0 rgba($white, 0.1);
|
||||
border-radius: 2px;
|
||||
box-shadow: 0 3px 6px 0 rgba(26, 24, 29, 0.16), 0 3px 6px 0 rgba(26, 24, 29, 0.24);
|
||||
|
||||
}
|
||||
|
||||
// shared dropdown-item styles
|
||||
.dropdown-item {
|
||||
// header items & not selectList-items
|
||||
padding-left: 24px;
|
||||
padding-top: 8px;
|
||||
padding-bottom: 8px;
|
||||
|
||||
font-size: 14px;
|
||||
line-height: 1.71;
|
||||
color: $gray-50;
|
||||
@@ -42,8 +53,8 @@
|
||||
}
|
||||
|
||||
|
||||
&:active, &:hover, &.active {
|
||||
background-color: rgba(#d5c8ff, 0.32);
|
||||
&:active, &:hover, &:focus, &.active {
|
||||
background-color: rgba($purple-600, 0.32);
|
||||
color: $purple-200;
|
||||
}
|
||||
|
||||
@@ -86,16 +97,28 @@
|
||||
|
||||
.dropdown-toggle {
|
||||
width: 100% !important;
|
||||
height: 32px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.dropdown-toggle::after {
|
||||
position: absolute;
|
||||
right: 16px;
|
||||
top: 17px;
|
||||
right: 12px;
|
||||
top: 14px;
|
||||
}
|
||||
|
||||
.dropdown-menu.show {
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
|
||||
// selectList.vue items sizing
|
||||
.selectListItem .dropdown-item {
|
||||
padding: 0.25rem 0.75rem;
|
||||
height: 32px;
|
||||
|
||||
&:active, &:hover, &:focus, &.active {
|
||||
background-color: rgba($purple-600, 0.25);
|
||||
color: $purple-300;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,10 +16,10 @@ label small {
|
||||
}
|
||||
}
|
||||
|
||||
// Inputs and texteares
|
||||
// Inputs and textareas
|
||||
|
||||
input, textarea, input.form-control, textarea.form-control {
|
||||
padding: 10px 16px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 2px;
|
||||
font-size: 14px;
|
||||
line-height: 1.43;
|
||||
@@ -31,14 +31,14 @@ input, textarea, input.form-control, textarea.form-control {
|
||||
}
|
||||
|
||||
&:active:not(:disabled), &:focus:not(:disabled) {
|
||||
border-color: $purple-500;
|
||||
border-color: $purple-400;
|
||||
outline: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.64;
|
||||
background: $gray-500;
|
||||
background: $gray-700;
|
||||
}
|
||||
|
||||
&.input-search {
|
||||
@@ -68,11 +68,48 @@ input, textarea, input.form-control, textarea.form-control {
|
||||
}
|
||||
}
|
||||
|
||||
.input-group {
|
||||
.input-group-prepend , .input-group-append {
|
||||
.input-group-outer {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
.input-group {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/** Colored Input-Groups, ignoring checklist */
|
||||
.input-group:not(.checklist-group) {
|
||||
border-radius: 2px;
|
||||
border: solid 1px $gray-400;
|
||||
|
||||
&:hover {
|
||||
border-color: $gray-300;
|
||||
}
|
||||
|
||||
&:focus, &:active, &:focus-within {
|
||||
border: solid 1px $purple-400;
|
||||
}
|
||||
|
||||
.input-group-prepend , .input-group-append {
|
||||
background: $gray-600;
|
||||
color: $gray-300;
|
||||
border-radius: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
/** Generic Input Group Styles */
|
||||
.input-group {
|
||||
height: 2rem;
|
||||
|
||||
.input-group-prepend , .input-group-append {
|
||||
color: $gray-200;
|
||||
border: 0;
|
||||
height: 30px;
|
||||
width: 2rem;
|
||||
margin: 0;
|
||||
|
||||
&.grow {
|
||||
width: initial;
|
||||
min-width: 2rem;
|
||||
}
|
||||
|
||||
&.input-group-text {
|
||||
font-size: 14px;
|
||||
@@ -83,28 +120,30 @@ input, textarea, input.form-control, textarea.form-control {
|
||||
}
|
||||
|
||||
&.input-group-icon {
|
||||
border: solid 1px $gray-400;
|
||||
border-right: none;
|
||||
border-top-right-radius: 0px;
|
||||
border-bottom-right-radius: 0px;
|
||||
display: flex;
|
||||
align-self: center;
|
||||
align-items: center;
|
||||
justify-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.svg-icon {
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
&.streak-addon .svg-icon {
|
||||
width: 11.6px;
|
||||
height: 7.1px;
|
||||
margin: 15px 13.4px 15.9px 13px;
|
||||
}
|
||||
|
||||
&.positive-addon .svg-icon {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
margin: 14px 14px;
|
||||
}
|
||||
|
||||
&.negative-addon .svg-icon {
|
||||
width: 10px;
|
||||
height: 2px;
|
||||
margin: 18px 14px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,6 +154,19 @@ input, textarea, input.form-control, textarea.form-control {
|
||||
input:first-child {
|
||||
border-right: none !important;
|
||||
}
|
||||
|
||||
input {
|
||||
height: 30px;
|
||||
border: 0;
|
||||
background: $white !important;
|
||||
}
|
||||
}
|
||||
|
||||
.input-group-spaced {
|
||||
margin-left: 12px;
|
||||
height: 2rem;
|
||||
border-radius: 2px;
|
||||
background-color: $gray-600;
|
||||
}
|
||||
|
||||
.form-check {
|
||||
@@ -200,9 +252,13 @@ $bg-disabled-control: #34303a;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.destroy-icon {
|
||||
width: 14px;
|
||||
height: 16px;
|
||||
.destroy-icon.svg-icon {
|
||||
margin-top: 1px !important;
|
||||
|
||||
svg {
|
||||
width: 14px;
|
||||
height: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
.svg-icon {
|
||||
display: block;
|
||||
transition: none !important;
|
||||
fill: currentColor;
|
||||
|
||||
svg {
|
||||
display: block;
|
||||
}
|
||||
transition: none;
|
||||
|
||||
* {
|
||||
transition: none !important;
|
||||
path {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
|
||||
&.color {
|
||||
@@ -64,4 +64,4 @@
|
||||
&:hover svg path {
|
||||
stroke: $gray-100;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,3 +37,4 @@
|
||||
@import './tiers';
|
||||
@import './payments';
|
||||
@import './datepicker.scss';
|
||||
@import './spacing';
|
||||
|
||||
@@ -14,4 +14,16 @@
|
||||
border-right: 4px solid transparent;
|
||||
border-left: 4px solid transparent;
|
||||
border-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
* {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.transition {
|
||||
transition-duration: 0.15s;
|
||||
transition-property: border-color, color;
|
||||
transition-property: border-color, box-shadow, color;
|
||||
transition-property: border-color, box-shadow, color;
|
||||
transition-timing-function: ease-in;
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
|
||||
.modal {
|
||||
z-index: 1350;
|
||||
padding-left: 0px !important;
|
||||
}
|
||||
|
||||
.modal-dialog {
|
||||
|
||||
59
website/client/src/assets/scss/spacing.scss
Normal file
59
website/client/src/assets/scss/spacing.scss
Normal file
@@ -0,0 +1,59 @@
|
||||
.m-75 {
|
||||
margin: 0.75rem; // 12px
|
||||
}
|
||||
|
||||
.mx-75 {
|
||||
margin-left: 0.75rem;
|
||||
margin-right: 0.75rem;
|
||||
}
|
||||
|
||||
.my-75 {
|
||||
margin-bottom: 0.75rem;
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
|
||||
.mb-75 {
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.ml-75 {
|
||||
margin-left: 0.75rem;
|
||||
}
|
||||
|
||||
.mr-75 {
|
||||
margin-right: 0.75rem;
|
||||
}
|
||||
|
||||
.mt-75 {
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
|
||||
.p-75 {
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.px-75 {
|
||||
padding-left: 0.75rem;
|
||||
padding-right: 0.75rem;
|
||||
}
|
||||
|
||||
.py-75 {
|
||||
padding-bottom: 0.75rem;
|
||||
padding-top: 0.75rem;
|
||||
}
|
||||
|
||||
.pb-75 {
|
||||
padding-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.pl-75 {
|
||||
padding-left: 0.75rem;
|
||||
}
|
||||
|
||||
.pr-75 {
|
||||
padding-right: 0.75rem;
|
||||
}
|
||||
|
||||
.pt-75 {
|
||||
padding-top: 0.75rem;
|
||||
}
|
||||
@@ -3,7 +3,8 @@
|
||||
.habit-option-button {
|
||||
border: 2px solid $disabled-color;
|
||||
}
|
||||
&:hover {
|
||||
// TODO refactor to use more css-vars and less duplicate generated css code
|
||||
&:hover, &:focus, &:active {
|
||||
.habit-option-button {
|
||||
border: 2px solid $active-color;
|
||||
}
|
||||
@@ -28,7 +29,7 @@
|
||||
&-control {
|
||||
&-bg {
|
||||
background: $maroon-100 !important;
|
||||
.habit-control:hover { background: rgba($black, 0.5) !important; }
|
||||
.habit-control:not(.task-not-scoreable):hover { background: rgba($black, 0.5) !important; }
|
||||
.daily-todo-control:hover { background: rgba($white, 0.75) !important; }
|
||||
}
|
||||
&-bg-noninteractive { background: $maroon-100 !important; }
|
||||
@@ -40,6 +41,7 @@
|
||||
|
||||
&-modal {
|
||||
&-bg { background: $maroon-100 !important; }
|
||||
&-headings { color: $white; }
|
||||
&-icon { color: $maroon-100 !important; }
|
||||
&-text { color: $red-1 !important; }
|
||||
&-content {
|
||||
@@ -58,7 +60,7 @@
|
||||
&-control {
|
||||
&-bg {
|
||||
background: $red-100 !important;
|
||||
.habit-control:hover { background: rgba($black, 0.5) !important; }
|
||||
.habit-control:not(.task-not-scoreable):hover { background: rgba($black, 0.5) !important; }
|
||||
.daily-todo-control:hover { background: rgba($white, 0.75) !important; }
|
||||
}
|
||||
&-bg-noninteractive { background: $red-100 !important; }
|
||||
@@ -70,8 +72,8 @@
|
||||
|
||||
&-modal {
|
||||
&-bg { background: $red-100 !important; }
|
||||
&-headings, &-text { color: $red-1 !important; }
|
||||
&-icon { color: $red-100 !important; }
|
||||
&-text { color: $red-1 !important; }
|
||||
&-content {
|
||||
--svg-color: #{$red-100};
|
||||
}
|
||||
@@ -89,7 +91,7 @@
|
||||
&-control {
|
||||
&-bg {
|
||||
background: $orange-100 !important;
|
||||
.habit-control:hover { background: rgba($orange-1, 0.5) !important; }
|
||||
.habit-control:not(.task-not-scoreable):hover { background: rgba($orange-1, 0.5) !important; }
|
||||
.daily-todo-control:hover { background: rgba($white, 0.75) !important; }
|
||||
}
|
||||
&-bg-noninteractive { background: $orange-100 !important; }
|
||||
@@ -101,8 +103,8 @@
|
||||
|
||||
&-modal {
|
||||
&-bg { background: $orange-100 !important; }
|
||||
&-headings, &-text { color: $orange-1 !important; }
|
||||
&-icon { color: $orange-100 !important; }
|
||||
&-text { color: $orange-1 !important; }
|
||||
&-content {
|
||||
--svg-color: #{$orange-100};
|
||||
}
|
||||
@@ -120,7 +122,7 @@
|
||||
&-control {
|
||||
&-bg {
|
||||
background: $yellow-100 !important;
|
||||
.habit-control:hover { background: rgba($yellow-1, 0.5) !important; }
|
||||
.habit-control:not(.task-not-scoreable):hover { background: rgba($yellow-1, 0.5) !important; }
|
||||
.daily-todo-control:hover { background: rgba($white, 0.75) !important; }
|
||||
}
|
||||
&-bg-noninteractive { background: $yellow-100 !important; }
|
||||
@@ -132,8 +134,8 @@
|
||||
|
||||
&-modal {
|
||||
&-bg { background: $yellow-100 !important; }
|
||||
&-headings, &-text { color: $yellow-1 !important; }
|
||||
&-icon { color: $yellow-100 !important; }
|
||||
&-text { color: $yellow-1 !important; }
|
||||
@include modal-text-input($yellow-1);
|
||||
&-option-disabled:hover {
|
||||
.svg-icon { color: $yellow-100 !important; }
|
||||
@@ -151,7 +153,7 @@
|
||||
&-control {
|
||||
&-bg {
|
||||
background: $green-100 !important;
|
||||
.habit-control:hover { background: rgba($black, 0.5) !important; }
|
||||
.habit-control:not(.task-not-scoreable):hover { background: rgba($black, 0.5) !important; }
|
||||
.daily-todo-control:hover { background: rgba($white, 0.75) !important; }
|
||||
}
|
||||
&-bg-noninteractive { background: $green-100 !important; }
|
||||
@@ -163,8 +165,8 @@
|
||||
|
||||
&-modal {
|
||||
&-bg { background: $green-100 !important; }
|
||||
&-headings, &-text { color: $green-1 !important; }
|
||||
&-icon { color: $green-10 !important; }
|
||||
&-text { color: $green-1 !important; }
|
||||
&-content {
|
||||
--svg-color: #{$green-100};
|
||||
}
|
||||
@@ -183,7 +185,7 @@
|
||||
&-control {
|
||||
&-bg {
|
||||
background: $teal-100 !important;
|
||||
.habit-control:hover { background: rgba($black, 0.5) !important; }
|
||||
.habit-control:not(.task-not-scoreable):hover { background: rgba($black, 0.5) !important; }
|
||||
.daily-todo-control:hover { background: rgba($white, 0.75) !important; }
|
||||
}
|
||||
&-bg-noninteractive { background: $teal-100 !important; }
|
||||
@@ -195,8 +197,8 @@
|
||||
|
||||
&-modal {
|
||||
&-bg { background: $teal-100 !important; }
|
||||
&-headings, &-text { color: $teal-1 !important; }
|
||||
&-icon { color: $teal-100 !important; }
|
||||
&-text { color: $teal-1 !important; }
|
||||
&-content {
|
||||
--svg-color: #{$teal-100};
|
||||
}
|
||||
@@ -214,7 +216,7 @@
|
||||
&-control {
|
||||
&-bg {
|
||||
background: $blue-100 !important;
|
||||
.habit-control:hover { background: rgba($black, 0.5) !important; }
|
||||
.habit-control:not(.task-not-scoreable):hover { background: rgba($black, 0.5) !important; }
|
||||
.daily-todo-control:hover { background: rgba($white, 0.75) !important; }
|
||||
}
|
||||
&-bg-noninteractive { background: $blue-100 !important; }
|
||||
@@ -226,8 +228,8 @@
|
||||
|
||||
&-modal {
|
||||
&-bg { background: $blue-100 !important; }
|
||||
&-headings, &-text { color: $blue-1 !important; }
|
||||
&-icon { color: $blue-100 !important; }
|
||||
&-text { color: $blue-1 !important; }
|
||||
&-content {
|
||||
--svg-color: #{$blue-100};
|
||||
}
|
||||
@@ -246,7 +248,7 @@
|
||||
&-control {
|
||||
&-bg {
|
||||
background: $purple-task !important;
|
||||
.habit-control:hover { background: rgba($black, 0.5) !important; }
|
||||
.habit-control:not(.task-not-scoreable):hover { background: rgba($black, 0.5) !important; }
|
||||
.daily-todo-control:hover { background: rgba($white, 0.75) !important; }
|
||||
}
|
||||
&-inner-habit { background: rgba($black, 0.25) !important; }
|
||||
@@ -256,6 +258,7 @@
|
||||
|
||||
&-modal {
|
||||
&-bg { background: $purple-300 !important; }
|
||||
&-headings { color: $white; }
|
||||
&-icon { color: $purple-300 !important; }
|
||||
&-text { color: $black !important; }
|
||||
&-content {
|
||||
|
||||
@@ -53,7 +53,7 @@ h1 {
|
||||
|
||||
h2 {
|
||||
font-size: 20px;
|
||||
line-height: 1.2;
|
||||
line-height: 1.4;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
@@ -73,3 +73,7 @@ h4 {
|
||||
font-family: 'Roboto Condensed', sans-serif;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.opacity-75 {
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||
<path fill="#A5A1AC" fill-rule="evenodd" d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zM7 7h2v5H7V7zm0-3h2v2H7V4z"/>
|
||||
<path fill="#878190" fill-rule="evenodd" d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zM7 7h2v5H7V7zm0-3h2v2H7V4z"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 238 B After Width: | Height: | Size: 238 B |
@@ -13,20 +13,20 @@
|
||||
v-html="html"
|
||||
></div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<div class="modal-footer d-flex align-items-center pb-0">
|
||||
<a
|
||||
class="btn btn-info"
|
||||
href="http://habitica.fandom.com/wiki/Whats_New"
|
||||
target="_blank"
|
||||
class="mr-auto"
|
||||
>{{ this.$t('newsArchive') }}</a>
|
||||
<button
|
||||
class="btn btn-secondary"
|
||||
class="btn btn-secondary ml-auto"
|
||||
@click="tellMeLater()"
|
||||
>
|
||||
{{ this.$t('tellMeLater') }}
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-warning"
|
||||
class="btn btn-primary"
|
||||
@click="dismissAlert();"
|
||||
>
|
||||
{{ this.$t('dismissAlert') }}
|
||||
|
||||
@@ -179,7 +179,7 @@
|
||||
<div class="col-12 col-md-4">
|
||||
<button
|
||||
v-if="user"
|
||||
class="btn btn-contribute btn-flat"
|
||||
class="btn btn-contribute btn-front btn-flat"
|
||||
@click="donate()"
|
||||
>
|
||||
<div
|
||||
@@ -192,7 +192,7 @@
|
||||
</button>
|
||||
<div
|
||||
v-else
|
||||
class="btn btn-contribute btn-flat"
|
||||
class="btn btn-contribute btn-front btn-flat"
|
||||
>
|
||||
<a
|
||||
href="http://habitica.fandom.com/wiki/Contributing_to_Habitica"
|
||||
@@ -417,6 +417,7 @@
|
||||
background: #c3c0c7;
|
||||
box-shadow: none;
|
||||
border-radius: 4px;
|
||||
font-family: Roboto Condensed,sans-serif;
|
||||
|
||||
&:hover {
|
||||
background: #a5a1ac;
|
||||
|
||||
@@ -214,16 +214,13 @@ export default {
|
||||
if (this.$route.query.showGroupOverview) {
|
||||
this.$root.$emit('bv::show::modal', 'group-plan-overview');
|
||||
}
|
||||
|
||||
this.$root.$on('habitica:team-sync', () => {
|
||||
this.loadTasks();
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
async load () {
|
||||
this.tasksByType = {
|
||||
habit: [],
|
||||
daily: [],
|
||||
todo: [],
|
||||
reward: [],
|
||||
};
|
||||
|
||||
this.group = await this.$store.dispatch('guilds:getGroup', {
|
||||
groupId: this.searchId,
|
||||
});
|
||||
@@ -231,6 +228,16 @@ export default {
|
||||
const members = await this.$store.dispatch('members:getGroupMembers', { groupId: this.searchId });
|
||||
this.group.members = members;
|
||||
|
||||
this.loadTasks();
|
||||
},
|
||||
async loadTasks () {
|
||||
this.tasksByType = {
|
||||
habit: [],
|
||||
daily: [],
|
||||
todo: [],
|
||||
reward: [],
|
||||
};
|
||||
|
||||
const tasks = await this.$store.dispatch('tasks:getGroupTasks', {
|
||||
groupId: this.searchId,
|
||||
});
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<div class="col-md-auto col-md-12 col-xl-4">
|
||||
<button
|
||||
v-once
|
||||
class="btn btn-info btn-follow-guidelines"
|
||||
class="btn btn-secondary btn-follow-guidelines"
|
||||
@click="acceptCommunityGuidelines()"
|
||||
>
|
||||
{{ $t('acceptCommunityGuidelines') }}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<base-notification
|
||||
:can-remove="canRemove"
|
||||
:can-remove="false"
|
||||
:has-icon="false"
|
||||
:notification="notification"
|
||||
@click="action"
|
||||
@@ -9,13 +9,13 @@
|
||||
<div v-html="notification.data.message"></div>
|
||||
<div class="notifications-buttons">
|
||||
<div
|
||||
class="btn btn-small btn-success"
|
||||
class="btn btn-small btn-success mr-2"
|
||||
@click.stop="approve()"
|
||||
>
|
||||
{{ $t('approve') }}
|
||||
</div>
|
||||
<div
|
||||
class="btn btn-small btn-warning"
|
||||
class="btn btn-small btn-secondary"
|
||||
@click.stop="needsWork()"
|
||||
>
|
||||
{{ $t('needsWork') }}
|
||||
@@ -45,8 +45,7 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
action () {
|
||||
const groupId = this.notification.data.group.id;
|
||||
this.$router.push({ name: 'groupPlanDetailTaskInformation', params: { groupId } });
|
||||
this.$router.push({ name: 'groupPlanDetailTaskInformation', params: { groupId: this.notification.data.groupId } });
|
||||
},
|
||||
async approve () {
|
||||
// Redirect users to the group tasks page if the notification doesn't have data
|
||||
|
||||
@@ -1,31 +1,44 @@
|
||||
<template>
|
||||
<base-notification
|
||||
:can-remove="canRemove"
|
||||
:can-remove="false"
|
||||
:has-icon="false"
|
||||
:notification="notification"
|
||||
:read-after-click="true"
|
||||
@click="action"
|
||||
>
|
||||
<div
|
||||
slot="content"
|
||||
class="notification-green"
|
||||
v-html="notification.data.message"
|
||||
></div>
|
||||
<div slot="content">
|
||||
<div
|
||||
class="notification-green"
|
||||
v-html="notification.data.message"
|
||||
></div>
|
||||
<div class="notifications-buttons">
|
||||
<div
|
||||
class="btn btn-small btn-primary"
|
||||
@click.stop="action()"
|
||||
>
|
||||
{{ $t('claimRewards') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</base-notification>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import BaseNotification from './base';
|
||||
import scoreTask from '@/mixins/scoreTask';
|
||||
import sync from '@/mixins/sync';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
BaseNotification,
|
||||
},
|
||||
mixins: [scoreTask, sync],
|
||||
props: ['notification', 'canRemove'],
|
||||
methods: {
|
||||
action () {
|
||||
const { groupId } = this.notification.data;
|
||||
this.$router.push({ name: 'groupPlanDetailTaskInformation', params: { groupId } });
|
||||
async action () {
|
||||
const { task, direction } = this.notification.data;
|
||||
await this.taskScore(task, direction);
|
||||
this.sync();
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -191,7 +191,7 @@ export default {
|
||||
openStatus: undefined,
|
||||
actionableNotifications: [
|
||||
'GUILD_INVITATION', 'PARTY_INVITATION', 'CHALLENGE_INVITATION',
|
||||
'QUEST_INVITATION', 'GROUP_TASK_NEEDS_WORK',
|
||||
'QUEST_INVITATION', 'GROUP_TASK_APPROVAL', 'GROUP_TASK_APPROVED',
|
||||
],
|
||||
// A list of notifications handled by this component,
|
||||
// listed in the order they should appear in the notifications panel.
|
||||
|
||||
@@ -24,14 +24,14 @@
|
||||
class="user-dropdown"
|
||||
>
|
||||
<a
|
||||
class="dropdown-item edit-avatar dropdown-separated"
|
||||
class="topbar-dropdown-item dropdown-item edit-avatar dropdown-separated"
|
||||
@click="showAvatar('body', 'size')"
|
||||
>
|
||||
<h3>{{ user.profile.name }}</h3>
|
||||
<span class="small-text">{{ $t('editAvatar') }}</span>
|
||||
</a>
|
||||
<a
|
||||
class="nav-link dropdown-item
|
||||
class="topbar-dropdown-item nav-link dropdown-item
|
||||
dropdown-separated d-flex justify-content-between align-items-center"
|
||||
@click.prevent="showPrivateMessages()"
|
||||
>
|
||||
@@ -42,42 +42,42 @@
|
||||
/>
|
||||
</a>
|
||||
<a
|
||||
class="dropdown-item"
|
||||
class="topbar-dropdown-item dropdown-item"
|
||||
@click="showAvatar('backgrounds', '2020')"
|
||||
>{{ $t('backgrounds') }}</a>
|
||||
<a
|
||||
class="dropdown-item"
|
||||
class="topbar-dropdown-item dropdown-item"
|
||||
@click="showProfile('stats')"
|
||||
>{{ $t('stats') }}</a>
|
||||
<a
|
||||
class="dropdown-item"
|
||||
class="topbar-dropdown-item dropdown-item"
|
||||
@click="showProfile('achievements')"
|
||||
>{{ $t('achievements') }}</a>
|
||||
<a
|
||||
class="dropdown-item dropdown-separated"
|
||||
class="topbar-dropdown-item dropdown-item dropdown-separated"
|
||||
@click="showProfile('profile')"
|
||||
>{{ $t('profile') }}</a>
|
||||
<router-link
|
||||
class="dropdown-item"
|
||||
class="topbar-dropdown-item dropdown-item"
|
||||
:to="{name: 'site'}"
|
||||
>
|
||||
{{ $t('settings') }}
|
||||
</router-link>
|
||||
<router-link
|
||||
class="dropdown-item dropdown-separated"
|
||||
class="topbar-dropdown-item dropdown-item dropdown-separated"
|
||||
:to="{name: 'subscription'}"
|
||||
>
|
||||
{{ $t('subscription') }}
|
||||
</router-link>
|
||||
<a
|
||||
class="nav-link dropdown-item dropdown-separated"
|
||||
class="topbar-dropdown-item nav-link dropdown-item dropdown-separated"
|
||||
@click.prevent="logout()"
|
||||
>{{ $t('logout') }}</a>
|
||||
<li
|
||||
v-if="!user.purchased.plan.customerId"
|
||||
@click="showBuyGemsModal()"
|
||||
>
|
||||
<div class="dropdown-item text-center">
|
||||
<div class="topbar-dropdown-item dropdown-item text-center">
|
||||
<h3 class="purple">
|
||||
{{ $t('needMoreGems') }}
|
||||
</h3>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div v-if="emptyItem">
|
||||
<div class="item-wrapper">
|
||||
<div class="item item-empty">
|
||||
<div class="item transition item-empty">
|
||||
<div class="item-content"></div>
|
||||
</div><span
|
||||
v-if="label"
|
||||
@@ -15,7 +15,7 @@
|
||||
@click="click"
|
||||
>
|
||||
<div
|
||||
class="item"
|
||||
class="item transition"
|
||||
:class="{'item-active': active, 'highlight-border':highlightBorder }"
|
||||
>
|
||||
<slot
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
@click="click($event)"
|
||||
>
|
||||
<div
|
||||
class="item"
|
||||
class="item transition"
|
||||
:class="{'item-active': active }"
|
||||
>
|
||||
<countBadge
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
@click="click()"
|
||||
>
|
||||
<div
|
||||
class="item pet-slot"
|
||||
class="item pet-slot transition"
|
||||
:class="{'item-empty': !isOwned()}"
|
||||
>
|
||||
<slot
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
@click="click()"
|
||||
>
|
||||
<div
|
||||
class="item pet-slot"
|
||||
class="item pet-slot transition"
|
||||
:class="{'item-empty': !isOwned(), 'highlight': highlightBorder}"
|
||||
>
|
||||
<slot
|
||||
|
||||
@@ -388,7 +388,7 @@ export default {
|
||||
'GUILD_PROMPT', 'REBIRTH_ENABLED', 'WON_CHALLENGE', 'STREAK_ACHIEVEMENT',
|
||||
'ULTIMATE_GEAR_ACHIEVEMENT', 'REBIRTH_ACHIEVEMENT', 'GUILD_JOINED_ACHIEVEMENT',
|
||||
'CHALLENGE_JOINED_ACHIEVEMENT', 'INVITED_FRIEND_ACHIEVEMENT', 'NEW_CONTRIBUTOR_LEVEL',
|
||||
'CRON', 'SCORED_TASK', 'LOGIN_INCENTIVE', 'ACHIEVEMENT_ALL_YOUR_BASE', 'ACHIEVEMENT_BACK_TO_BASICS',
|
||||
'CRON', 'LOGIN_INCENTIVE', 'ACHIEVEMENT_ALL_YOUR_BASE', 'ACHIEVEMENT_BACK_TO_BASICS',
|
||||
'GENERIC_ACHIEVEMENT', 'ACHIEVEMENT_PARTY_UP', 'ACHIEVEMENT_PARTY_ON', 'ACHIEVEMENT_BEAST_MASTER',
|
||||
'ACHIEVEMENT_MOUNT_MASTER', 'ACHIEVEMENT_TRIAD_BINGO', 'ACHIEVEMENT_DUST_DEVIL', 'ACHIEVEMENT_ARID_AUTHORITY',
|
||||
'ACHIEVEMENT_MONSTER_MAGUS', 'ACHIEVEMENT_UNDEAD_UNDERTAKER', 'ACHIEVEMENT_PRIMED_FOR_PAINTING',
|
||||
@@ -738,7 +738,6 @@ export default {
|
||||
if (!after || after.length === 0 || !Array.isArray(after)) return;
|
||||
|
||||
const notificationsToRead = [];
|
||||
const scoreTaskNotification = [];
|
||||
|
||||
after.forEach(notification => {
|
||||
// This notification type isn't implemented here
|
||||
@@ -830,23 +829,6 @@ export default {
|
||||
// Not needed because it's shown already by the userHp and userMp watchers
|
||||
// Keeping an empty block so that it gets read
|
||||
break;
|
||||
case 'SCORED_TASK':
|
||||
// Search if it is a read notification
|
||||
for (let i = 0; i < this.alreadyReadNotification.length; i += 1) {
|
||||
if (this.alreadyReadNotification[i] === notification.id) {
|
||||
markAsRead = false; // Do not let it be read again
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Only process the notification if it is an unread notification
|
||||
if (markAsRead) {
|
||||
scoreTaskNotification.push(notification);
|
||||
|
||||
// Add to array of read notifications
|
||||
this.alreadyReadNotification.push(notification.id);
|
||||
}
|
||||
break;
|
||||
case 'LOGIN_INCENTIVE':
|
||||
if (this.user.flags.tour.intro === this.TOUR_END && this.user.flags.welcomed) {
|
||||
this.notificationData = notification.data;
|
||||
@@ -872,43 +854,12 @@ export default {
|
||||
if (markAsRead) notificationsToRead.push(notification.id);
|
||||
});
|
||||
|
||||
const userReadNotifsPromise = false;
|
||||
|
||||
if (notificationsToRead.length > 0) {
|
||||
await axios.post('/api/v4/notifications/read', {
|
||||
notificationIds: notificationsToRead,
|
||||
});
|
||||
}
|
||||
|
||||
// @TODO this code is never run because userReadNotifsPromise is never true
|
||||
if (userReadNotifsPromise) {
|
||||
userReadNotifsPromise.then(() => {
|
||||
// Only run this code for scoring approved tasks
|
||||
if (scoreTaskNotification.length > 0) {
|
||||
const approvedTasks = [];
|
||||
for (let i = 0; i < scoreTaskNotification.length; i += 1) {
|
||||
// Array with all approved tasks
|
||||
const scoreData = scoreTaskNotification[i].data;
|
||||
let direction = 'up';
|
||||
if (scoreData.direction) direction = scoreData.direction;
|
||||
|
||||
approvedTasks.push({
|
||||
params: {
|
||||
task: scoreData.scoreTask,
|
||||
direction,
|
||||
},
|
||||
});
|
||||
|
||||
// Show notification of task approved
|
||||
this.markdown(scoreTaskNotification[i].data.message);
|
||||
}
|
||||
|
||||
// Score approved tasks
|
||||
// TODO: User.bulkScore(approvedTasks);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.debounceCheckUserAchievements();
|
||||
},
|
||||
},
|
||||
|
||||
@@ -24,36 +24,38 @@
|
||||
{{ $t('gemsPopoverTitle') }}
|
||||
</h3>
|
||||
<div class="panel-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<input
|
||||
v-model="gift.gems.amount"
|
||||
class="form-control"
|
||||
type="number"
|
||||
placeholder="Number of Gems"
|
||||
min="0"
|
||||
:max="gift.gems.fromBalance ? userLoggedIn.balance * 4 : 9999"
|
||||
>
|
||||
</div>
|
||||
<div class="d-flex mb-3">
|
||||
<div class="form-group mb-0">
|
||||
<input
|
||||
v-model="gift.gems.amount"
|
||||
class="form-control"
|
||||
type="number"
|
||||
placeholder="Number of Gems"
|
||||
min="0"
|
||||
:max="gift.gems.fromBalance ? userLoggedIn.balance * 4 : 9999"
|
||||
>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="btn-group">
|
||||
<button
|
||||
class="btn btn-secondary"
|
||||
:class="{active: gift.gems.fromBalance}"
|
||||
@click="gift.gems.fromBalance = true"
|
||||
>
|
||||
{{ $t('sendGiftFromBalance') }}
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-secondary"
|
||||
:class="{active: !gift.gems.fromBalance}"
|
||||
@click="gift.gems.fromBalance = false"
|
||||
>
|
||||
{{ $t('sendGiftPurchase') }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="btn-group ml-auto">
|
||||
<button
|
||||
class="btn"
|
||||
:class="{
|
||||
'btn-primary': gift.gems.fromBalance,
|
||||
'btn-secondary': !gift.gems.fromBalance,
|
||||
}"
|
||||
@click="gift.gems.fromBalance = true"
|
||||
>
|
||||
{{ $t('sendGiftFromBalance') }}
|
||||
</button>
|
||||
<button
|
||||
class="btn"
|
||||
:class="{
|
||||
'btn-primary': !gift.gems.fromBalance,
|
||||
'btn-secondary': gift.gems.fromBalance,
|
||||
}"
|
||||
@click="gift.gems.fromBalance = false"
|
||||
>
|
||||
{{ $t('sendGiftPurchase') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
@@ -77,7 +79,7 @@
|
||||
<div class="panel-body">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="form-group">
|
||||
<div class="form-group mb-0">
|
||||
<!-- eslint-disable vue/no-use-v-if-with-v-for -->
|
||||
<div
|
||||
v-for="block in subscriptionBlocks"
|
||||
|
||||
@@ -79,19 +79,21 @@
|
||||
>
|
||||
</td>
|
||||
<td>
|
||||
<a
|
||||
class="btn btn-warning checklist-icons"
|
||||
<div
|
||||
class="btn btn-danger checklist-icons mr-2"
|
||||
@click="deleteWebhook(webhook, index)"
|
||||
>
|
||||
<span
|
||||
class="glyphicon glyphicon-trash"
|
||||
:tooltip="$t('delete')"
|
||||
>Delete</span>
|
||||
</a>
|
||||
<a
|
||||
class="btn btn-success checklist-icons"
|
||||
> {{ $t('delete') }} </span>
|
||||
</div>
|
||||
<div
|
||||
class="btn btn-primary checklist-icons"
|
||||
@click="saveWebhook(webhook, index)"
|
||||
>Update</a>
|
||||
>
|
||||
{{ $t('subUpdateTitle') }}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
||||
@@ -559,6 +559,7 @@ import resetModal from './resetModal';
|
||||
import deleteModal from './deleteModal';
|
||||
import { SUPPORTED_SOCIAL_NETWORKS } from '@/../../common/script/constants';
|
||||
import changeClass from '@/../../common/script/ops/changeClass';
|
||||
import getUtcOffset from '@/../../common/script/fns/getUtcOffset';
|
||||
import notificationsMixin from '../../mixins/notifications';
|
||||
import sounds from '../../libs/sounds';
|
||||
import { buildAppleAuthUrl } from '../../libs/auth';
|
||||
@@ -616,17 +617,8 @@ export default {
|
||||
return ['off', ...this.content.audioThemes];
|
||||
},
|
||||
timezoneOffsetToUtc () {
|
||||
let offset = this.user.preferences.timezoneOffset;
|
||||
const sign = offset > 0 ? '-' : '+';
|
||||
|
||||
offset = Math.abs(offset) / 60;
|
||||
|
||||
const hour = Math.floor(offset);
|
||||
|
||||
const minutesInt = (offset - hour) * 60;
|
||||
const minutes = minutesInt < 10 ? `0${minutesInt}` : minutesInt;
|
||||
|
||||
return `UTC${sign}${hour}:${minutes}`;
|
||||
const offsetString = moment().utcOffset(getUtcOffset(this.user)).format('Z');
|
||||
return `UTC${offsetString}`;
|
||||
},
|
||||
dayStart () {
|
||||
return this.user.preferences.dayStart;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<transition name="fade">
|
||||
<div
|
||||
v-if="show"
|
||||
class="notification callout animated"
|
||||
class="notification callout animated pt-0"
|
||||
:class="classes"
|
||||
@click="handleOnClick()"
|
||||
>
|
||||
|
||||
@@ -128,7 +128,7 @@
|
||||
</div>
|
||||
<router-link
|
||||
v-if="$route.name === 'home'"
|
||||
class="btn btn-primary login-button pull-right"
|
||||
class="btn btn-primary btn-front login-button pull-right"
|
||||
to="/login"
|
||||
>
|
||||
{{ $t('login') }}
|
||||
|
||||
@@ -304,7 +304,7 @@
|
||||
<div class="row">
|
||||
<div class="col-12 text-center">
|
||||
<button
|
||||
class="btn btn-primary join-button"
|
||||
class="btn btn-primary btn-front join-button"
|
||||
@click="playButtonClick()"
|
||||
>
|
||||
{{ $t('joinToday') }}
|
||||
@@ -680,10 +680,13 @@
|
||||
padding-bottom: 5em;
|
||||
}
|
||||
|
||||
.join-button:hover {
|
||||
.join-button {
|
||||
cursor: pointer;
|
||||
background-color: #b288ff;
|
||||
box-shadow: 0 4px 4px 0 rgba(26, 24, 29, 0.16), 0 1px 8px 0 rgba(26, 24, 29, 0.12);
|
||||
|
||||
&:hover {
|
||||
background-color: #5d3b9c;
|
||||
box-shadow: 0 4px 4px 0 rgba(26, 24, 29, 0.16), 0 1px 8px 0 rgba(26, 24, 29, 0.12);
|
||||
}
|
||||
}
|
||||
|
||||
.featured .row {
|
||||
|
||||
@@ -67,7 +67,7 @@
|
||||
}
|
||||
|
||||
.login-button:hover {
|
||||
background-color: #b288ff;
|
||||
background-color: #5d3b9c;
|
||||
box-shadow: 0 4px 4px 0 rgba(26, 24, 29, 0.16), 0 1px 8px 0 rgba(26, 24, 29, 0.12) !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
<div>
|
||||
<approval-modal :task="task" />
|
||||
<div
|
||||
v-if="!approvalRequested && !multipleApprovalsRequested"
|
||||
class="claim-bottom-message d-flex align-items-center"
|
||||
>
|
||||
<div
|
||||
@@ -10,7 +9,7 @@
|
||||
v-html="message"
|
||||
></div>
|
||||
<div
|
||||
v-if="!userIsAssigned"
|
||||
v-if="!userIsAssigned && !task.completed"
|
||||
class="ml-auto mr-2"
|
||||
>
|
||||
<a
|
||||
@@ -19,7 +18,7 @@
|
||||
>{{ $t('claim') }}</a>
|
||||
</div>
|
||||
<div
|
||||
v-if="userIsAssigned"
|
||||
v-if="userIsAssigned && !approvalRequested && !task.completed"
|
||||
class="ml-auto mr-2"
|
||||
>
|
||||
<a
|
||||
@@ -44,6 +43,16 @@
|
||||
>
|
||||
<a @click="showRequests()">{{ $t('viewRequests') }}</a>
|
||||
</div>
|
||||
<div
|
||||
v-if="userIsAssigned && task.group.approval.approved
|
||||
&& !task.completed && task.type !== 'habit'"
|
||||
class="claim-bottom-message d-flex align-items-center justify-content-around"
|
||||
>
|
||||
<a
|
||||
class="approve-color"
|
||||
@click="$emit('claimRewards')"
|
||||
>{{ $t('claimRewards') }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -176,6 +185,8 @@ export default {
|
||||
});
|
||||
this.task.group.assignedUsers.splice(0, 1);
|
||||
this.task.approvals.splice(0, 1);
|
||||
|
||||
this.sync();
|
||||
},
|
||||
needsWork () {
|
||||
if (!window.confirm(this.$t('confirmNeedsWork'))) return;
|
||||
@@ -185,6 +196,8 @@ export default {
|
||||
userId: userIdNeedsMoreWork,
|
||||
});
|
||||
this.task.approvals.splice(0, 1);
|
||||
|
||||
this.sync();
|
||||
},
|
||||
showRequests () {
|
||||
this.$root.$emit('bv::show::modal', 'approval-modal');
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
<div
|
||||
v-if="message"
|
||||
class="claim-top-message d-flex align-content-center"
|
||||
:class="{'approval-action': userIsAdmin, 'approval-pending': !userIsAdmin}"
|
||||
:class="{ 'approval-action': userIsAdmin || task.group.approval.approved,
|
||||
'approval-pending': !userIsAdmin && !task.group.approval.approved }"
|
||||
>
|
||||
<div
|
||||
class="m-auto"
|
||||
@@ -47,14 +48,19 @@ export default {
|
||||
|
||||
if (approvalsLength === 1 && !userIsRequesting) {
|
||||
return this.$t('userRequestsApproval', { userName: approvals[0].userId.profile.name });
|
||||
} if (approvalsLength > 1 && !userIsRequesting) {
|
||||
}
|
||||
if (approvalsLength > 1 && !userIsRequesting) {
|
||||
return this.$t('userCountRequestsApproval', { userCount: approvalsLength });
|
||||
} if (
|
||||
(approvalsLength === 1 && userIsRequesting)
|
||||
}
|
||||
if ((approvalsLength === 1 && userIsRequesting)
|
||||
|| (this.task.group.approval
|
||||
&& this.task.group.approval.requested && !this.task.group.approval.approved)) {
|
||||
&& this.task.group.approval.requested
|
||||
&& !this.task.group.approval.approved)) {
|
||||
return this.$t('youAreRequestingApproval');
|
||||
}
|
||||
if (this.task.group.approval.approved && !this.task.completed) {
|
||||
return this.$t('thisTaskApproved');
|
||||
}
|
||||
return null;
|
||||
},
|
||||
userIsAdmin () {
|
||||
|
||||
@@ -201,6 +201,7 @@
|
||||
border-color: transparent;
|
||||
transition: background 0.15s ease-in;
|
||||
resize: none;
|
||||
overflow: hidden;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba($black, 0.1);
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
<template>
|
||||
<div class="checklist-component">
|
||||
<label
|
||||
v-once
|
||||
class="mb-1"
|
||||
>{{ $t('checklist') }}</label>
|
||||
<br>
|
||||
<lockable-label
|
||||
:locked="disabled || disableItems"
|
||||
:text="$t('checklist')"
|
||||
/>
|
||||
<draggable
|
||||
v-model="checklist"
|
||||
:options="{
|
||||
@@ -20,7 +19,7 @@
|
||||
class="inline-edit-input-group checklist-group input-group"
|
||||
>
|
||||
<span
|
||||
v-if="!disabled"
|
||||
v-if="!disabled && !disableDrag"
|
||||
class="grippy"
|
||||
v-html="icons.grip"
|
||||
>
|
||||
@@ -29,19 +28,19 @@
|
||||
<checkbox
|
||||
:id="`checklist-${item.id}`"
|
||||
:checked.sync="item.completed"
|
||||
:disabled="disabled"
|
||||
:disabled="disabled || disableItems"
|
||||
class="input-group-prepend"
|
||||
:class="{'cursor-auto': disabled}"
|
||||
:class="{'cursor-auto': disabled || disableItems}"
|
||||
/>
|
||||
|
||||
<input
|
||||
v-model="item.text"
|
||||
class="inline-edit-input checklist-item form-control"
|
||||
type="text"
|
||||
:disabled="disabled"
|
||||
:disabled="disabled || disableItems"
|
||||
>
|
||||
<span
|
||||
v-if="!disabled"
|
||||
v-if="!disabled && !disableItems"
|
||||
class="input-group-append"
|
||||
@click="removeChecklistItem($index)"
|
||||
>
|
||||
@@ -55,8 +54,9 @@
|
||||
</div>
|
||||
</draggable>
|
||||
<div
|
||||
v-if="!disabled"
|
||||
v-if="!disabled && !disableItems"
|
||||
class="inline-edit-input-group checklist-group input-group new-checklist"
|
||||
:class="{'top-border': items.length === 0}"
|
||||
>
|
||||
<span
|
||||
v-once
|
||||
@@ -88,17 +88,25 @@ import deleteIcon from '@/assets/svg/delete.svg';
|
||||
import chevronIcon from '@/assets/svg/chevron.svg';
|
||||
import gripIcon from '@/assets/svg/grip.svg';
|
||||
import checkbox from '@/components/ui/checkbox';
|
||||
import lockableLabel from './lockableLabel';
|
||||
|
||||
export default {
|
||||
name: 'Checklist',
|
||||
components: {
|
||||
draggable,
|
||||
checkbox,
|
||||
draggable,
|
||||
lockableLabel,
|
||||
},
|
||||
props: {
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
},
|
||||
disableDrag: {
|
||||
type: Boolean,
|
||||
},
|
||||
disableItems: {
|
||||
type: Boolean,
|
||||
},
|
||||
items: {
|
||||
type: Array,
|
||||
},
|
||||
@@ -158,12 +166,20 @@ export default {
|
||||
|
||||
.checklist-component {
|
||||
|
||||
.top-border {
|
||||
border-top: 1px solid $gray-500;
|
||||
}
|
||||
|
||||
.lock-icon {
|
||||
color: $gray-200;
|
||||
}
|
||||
|
||||
.checklist-group {
|
||||
height: 2rem;
|
||||
border-top: 1px solid $gray-500;
|
||||
border-bottom: 1px solid $gray-500;
|
||||
|
||||
&.new-checklist {
|
||||
border-bottom: 1px solid $gray-500;
|
||||
&:first-of-type {
|
||||
border-top: 1px solid $gray-500;
|
||||
}
|
||||
|
||||
.inline-edit-input {
|
||||
@@ -172,7 +188,7 @@ export default {
|
||||
|
||||
.input-group-prepend {
|
||||
margin-left: 0.375rem;
|
||||
margin-top: 0.475rem;
|
||||
margin-top: 0.375rem;
|
||||
margin-right: 0;
|
||||
padding: 0;
|
||||
&:not(.new-icon) {
|
||||
@@ -183,6 +199,8 @@ export default {
|
||||
margin-left: 0.688rem;
|
||||
margin-top: 0.625rem;
|
||||
margin-bottom: 0.625rem;
|
||||
height: 10px;
|
||||
width: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -267,14 +285,5 @@ export default {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
label {
|
||||
height: 1.5rem;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
line-height: 1.71;
|
||||
letter-spacing: normal;
|
||||
color: $gray-50;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
<template>
|
||||
<div
|
||||
class="d-flex"
|
||||
:class="[{ 'opacity-75': locked }, classOverride]"
|
||||
>
|
||||
<span
|
||||
v-if="locked"
|
||||
v-once
|
||||
class="svg-icon lock-icon icon-10 mr-1"
|
||||
:class="classOverride ? classOverride : 'gray-200'"
|
||||
v-html="icons.lock"
|
||||
>
|
||||
</span>
|
||||
<label
|
||||
v-once
|
||||
class="mb-1"
|
||||
v-html="text"
|
||||
></label>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
|
||||
label {
|
||||
height: 1.5rem;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
line-height: 1.71;
|
||||
letter-spacing: normal;
|
||||
}
|
||||
|
||||
.gray-200 {
|
||||
color: $gray-200;
|
||||
}
|
||||
|
||||
.lock-icon {
|
||||
margin-top: 5px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import lockIcon from '@/assets/svg/lock.svg';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
classOverride: {
|
||||
type: String,
|
||||
},
|
||||
locked: {
|
||||
type: Boolean,
|
||||
},
|
||||
text: {
|
||||
type: String,
|
||||
},
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
icons: Object.freeze({
|
||||
lock: lockIcon,
|
||||
}),
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
189
website/client/src/components/tasks/modal-controls/multiList.vue
Normal file
189
website/client/src/components/tasks/modal-controls/multiList.vue
Normal file
@@ -0,0 +1,189 @@
|
||||
<template>
|
||||
<div
|
||||
class="multi-list"
|
||||
:class="{ 'break': maxItems === 0 }"
|
||||
>
|
||||
<template v-if="items.length === 0">
|
||||
<div class="items-none">
|
||||
{{ emptyMessage }}
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div
|
||||
v-for="item in truncatedSelectedItems"
|
||||
:key="item.id"
|
||||
:title="item.name"
|
||||
class="multi-item mr-1 d-inline-flex align-items-center"
|
||||
:class="{'margin-adjust': maxItems !== 0, 'pill-invert': pillInvert}"
|
||||
|
||||
@click.stop="removeItem($event, item)"
|
||||
>
|
||||
<div
|
||||
v-markdown="item.name"
|
||||
class="multi-label my-auto ml-75 mr-2"
|
||||
></div>
|
||||
<div
|
||||
class="remove ml-auto mr-75"
|
||||
v-html="icons.remove"
|
||||
></div>
|
||||
</div>
|
||||
<div
|
||||
v-if="remainingSelectedItems.length > 0"
|
||||
class="items-more ml-75"
|
||||
>
|
||||
+{{ remainingSelectedItems.length }}
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<style lang="scss">
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
|
||||
.multi-list {
|
||||
p {
|
||||
display: inline;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.multi-item {
|
||||
&:hover {
|
||||
.remove svg path {
|
||||
stroke: $maroon-50;
|
||||
}
|
||||
}
|
||||
|
||||
.remove {
|
||||
svg {
|
||||
width: 0.5rem;
|
||||
height: 0.5rem;
|
||||
|
||||
path {
|
||||
stroke: $gray-200;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
|
||||
.margin-adjust {
|
||||
margin-top: -1px;
|
||||
}
|
||||
|
||||
.multi-list {
|
||||
width: 100%;
|
||||
|
||||
&.break {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.multi-item {
|
||||
margin-bottom: 0.375rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.multi-item {
|
||||
display: inline-block;
|
||||
height: 1.5rem;
|
||||
border-radius: 100px;
|
||||
background-color: $gray-600;
|
||||
|
||||
cursor: pointer;
|
||||
|
||||
&.pill-invert {
|
||||
background-color: $white;
|
||||
border: solid 1px $gray-400;
|
||||
}
|
||||
|
||||
.multi-label {
|
||||
height: 1rem;
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
letter-spacing: normal;
|
||||
color: $gray-100;
|
||||
}
|
||||
|
||||
.remove {
|
||||
display: inline-block;
|
||||
object-fit: contain;
|
||||
margin-top: -0.125rem;
|
||||
}
|
||||
}
|
||||
|
||||
.items-more {
|
||||
color: $gray-100;
|
||||
font-size: 12px;
|
||||
display: inline-block;
|
||||
height: 1rem;
|
||||
font-weight: bold;
|
||||
line-height: 1.33;
|
||||
letter-spacing: normal;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import removeIcon from '@/assets/svg/remove.svg';
|
||||
import markdownDirective from '@/directives/markdown';
|
||||
|
||||
export default {
|
||||
directives: {
|
||||
markdown: markdownDirective,
|
||||
},
|
||||
components: {},
|
||||
props: {
|
||||
addNew: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
emptyMessage: {
|
||||
type: String,
|
||||
},
|
||||
maxItems: {
|
||||
type: Number,
|
||||
default: 3,
|
||||
},
|
||||
pillInvert: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
items: {
|
||||
type: Array,
|
||||
},
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
icons: Object.freeze({
|
||||
remove: removeIcon,
|
||||
}),
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
truncatedSelectedItems () {
|
||||
if (this.maxItems <= 0) {
|
||||
return this.items;
|
||||
}
|
||||
|
||||
return this.items.slice(0, this.maxItems);
|
||||
},
|
||||
remainingSelectedItems () {
|
||||
if (this.maxItems <= 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return this.items.slice(this.maxItems);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
removeItem ($event, item) {
|
||||
this.$emit('remove-item', item.id);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,190 @@
|
||||
<template>
|
||||
<div>
|
||||
<select-list
|
||||
:items="items"
|
||||
:key-prop="'icon'"
|
||||
class="difficulty-select"
|
||||
:class="{disabled: disabled}"
|
||||
:disabled="disabled"
|
||||
:value="selected"
|
||||
@select="$emit('select', $event.value)"
|
||||
>
|
||||
<template v-slot:item="{ item, button }">
|
||||
<div
|
||||
v-if="item"
|
||||
class="difficulty-item"
|
||||
:class="{ 'isButton': button }"
|
||||
>
|
||||
<span class="label">{{ item.label }}</span>
|
||||
|
||||
<div class="svg-icon">
|
||||
<span
|
||||
v-for="n in item.stars"
|
||||
:key="n"
|
||||
v-html="icons.difficultyTrivial"
|
||||
>
|
||||
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</select-list>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
|
||||
.difficulty-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
div.svg-icon {
|
||||
::v-deep svg {
|
||||
fill: $gray-200;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover, &.isButton {
|
||||
div.svg-icon {
|
||||
::v-deep svg {
|
||||
fill: var(--svg-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.label {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
div.svg-icon {
|
||||
flex-grow: 0;
|
||||
width: 80px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-left: 0.375rem;
|
||||
|
||||
span {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
::v-deep svg {
|
||||
margin-left: 0.125rem;
|
||||
width: 0.625rem;
|
||||
height: 0.625rem;
|
||||
object-fit: contain;
|
||||
|
||||
fill: $gray-200;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
|
||||
.difficulty-select {
|
||||
|
||||
&.disabled {
|
||||
.btn-secondary:disabled, .btn-secondary.disabled, .dropdown >
|
||||
.btn-secondary.dropdown-toggle:not(.btn-success):disabled, .dropdown >
|
||||
.btn-secondary.dropdown-toggle:not(.btn-success).disabled, .show >
|
||||
.btn-secondary.dropdown-toggle:not(.btn-success):disabled, .show >
|
||||
.btn-secondary.dropdown-toggle:not(.btn-success).disabled {
|
||||
background: $gray-700;
|
||||
}
|
||||
}
|
||||
// restyle the selected item
|
||||
.dropdown-toggle {
|
||||
|
||||
&.disabled {
|
||||
box-shadow: 0 1px 3px 0 rgba(26, 24, 29, 0.12), 0 1px 2px 0 rgba(26, 24, 29, 0.24);
|
||||
|
||||
&::after {
|
||||
color: $gray-300;
|
||||
border-top-color: $gray-300;
|
||||
}
|
||||
|
||||
.label {
|
||||
color: $gray-200;
|
||||
}
|
||||
}
|
||||
|
||||
.label {
|
||||
flex: 0;
|
||||
}
|
||||
|
||||
div.svg-icon {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
// highlight the svg icons,
|
||||
// when focusing with keyboard (it is outside of the template item
|
||||
.dropdown-item:focus .difficulty-item {
|
||||
div.svg-icon {
|
||||
svg {
|
||||
fill: var(--svg-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import difficultyNormalIcon from '@/assets/svg/difficulty-normal.svg';
|
||||
import difficultyTrivialIcon from '@/assets/svg/difficulty-trivial.svg';
|
||||
import difficultyMediumIcon from '@/assets/svg/difficulty-medium.svg';
|
||||
import difficultyHardIcon from '@/assets/svg/difficulty-hard.svg';
|
||||
import selectList from '@/components/ui/selectList';
|
||||
|
||||
|
||||
export default {
|
||||
components: {
|
||||
selectList,
|
||||
},
|
||||
props: {
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
},
|
||||
value: {
|
||||
type: Number,
|
||||
},
|
||||
},
|
||||
data () {
|
||||
const items = [
|
||||
{
|
||||
value: 0.1,
|
||||
label: this.$t('trivial'),
|
||||
stars: 1,
|
||||
},
|
||||
{
|
||||
value: 1,
|
||||
label: this.$t('easy'),
|
||||
stars: 2,
|
||||
},
|
||||
{
|
||||
value: 1.5,
|
||||
label: this.$t('medium'),
|
||||
stars: 3,
|
||||
},
|
||||
{
|
||||
value: 2,
|
||||
label: this.$t('hard'),
|
||||
stars: 4,
|
||||
},
|
||||
];
|
||||
|
||||
return {
|
||||
items,
|
||||
icons: Object.freeze({
|
||||
difficultyNormal: difficultyNormalIcon,
|
||||
difficultyTrivial: difficultyTrivialIcon,
|
||||
difficultyMedium: difficultyMediumIcon,
|
||||
difficultyHard: difficultyHardIcon,
|
||||
}),
|
||||
selected: items.find(i => i.value === this.value),
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,133 @@
|
||||
/* eslint-disable import/no-extraneous-dependencies */
|
||||
import { storiesOf } from '@storybook/vue';
|
||||
import { withKnobs, number } from '@storybook/addon-knobs';
|
||||
|
||||
import MultiList from './multiList';
|
||||
import SelectMulti from './selectMulti';
|
||||
import getStore from '@/store';
|
||||
|
||||
const stories = storiesOf('Multiple Select List', module);
|
||||
|
||||
stories.addDecorator(withKnobs);
|
||||
|
||||
// Needed for SelectTag
|
||||
const store = getStore();
|
||||
store.state.user.data = {
|
||||
tags: [],
|
||||
};
|
||||
|
||||
const exampleTagList = [
|
||||
1, 2, 3,
|
||||
];
|
||||
|
||||
const allTags = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Small Tag',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'This is a long tag',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'This is a long tag',
|
||||
},
|
||||
{
|
||||
id: 12,
|
||||
name: 'This is a different tag',
|
||||
},
|
||||
{
|
||||
id: 9001,
|
||||
name: 'OVER 9000',
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: 'Four',
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: 'Five :tada:',
|
||||
challenge: true,
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: 'Six',
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
name: 'Seven **Markdown**',
|
||||
},
|
||||
];
|
||||
|
||||
stories
|
||||
.add('tag-list', () => ({
|
||||
components: { MultiList },
|
||||
template: `
|
||||
<div style="position: absolute; margin: 20px">
|
||||
<MultiList :max-items="maxTags" :items="tagList"></MultiList>
|
||||
</div>
|
||||
`,
|
||||
props: {
|
||||
tagList: {
|
||||
default: allTags,
|
||||
},
|
||||
maxTags: {
|
||||
default: number('Max-Tags', 3),
|
||||
},
|
||||
},
|
||||
}))
|
||||
.add('select-tag', () => ({
|
||||
components: { SelectMulti },
|
||||
template: `
|
||||
<div style="position: absolute; margin: 20px">
|
||||
<SelectMulti :selectedItems="tagList"
|
||||
:add-new="true"
|
||||
:all-items="allTags"
|
||||
style="width: 400px"
|
||||
@changed="tagList = $event"
|
||||
@addNew="added = $event">
|
||||
|
||||
</SelectMulti>
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
Added event: {{ added }}
|
||||
</div>
|
||||
`,
|
||||
store,
|
||||
data () {
|
||||
return {
|
||||
tagList: exampleTagList,
|
||||
added: '',
|
||||
};
|
||||
},
|
||||
props: {
|
||||
allTags: {
|
||||
default: allTags,
|
||||
},
|
||||
},
|
||||
}))
|
||||
.add('longer select-tag', () => ({
|
||||
components: { SelectMulti },
|
||||
template: `
|
||||
<div style="position: absolute; margin: 20px">
|
||||
<SelectMulti :selectedItems="tagList"
|
||||
:all-items="allTags"
|
||||
style="width: 400px"
|
||||
@changed="tagList = $event"></SelectMulti>
|
||||
</div>
|
||||
`,
|
||||
store,
|
||||
data () {
|
||||
return {
|
||||
tagList: [],
|
||||
};
|
||||
},
|
||||
props: {
|
||||
allTags: {
|
||||
default: allTags,
|
||||
},
|
||||
},
|
||||
}));
|
||||
@@ -0,0 +1,308 @@
|
||||
multi<template>
|
||||
<div>
|
||||
<b-dropdown
|
||||
ref="dropdown"
|
||||
class="inline-dropdown select-multi"
|
||||
:toggle-class="isOpened ? 'active' : null"
|
||||
@show="wasOpened()"
|
||||
@hide="hideCallback($event)"
|
||||
@toggle="openOrClose($event)"
|
||||
>
|
||||
<b-dropdown-header>
|
||||
<div class="mb-2">
|
||||
<b-form-input
|
||||
v-model="search"
|
||||
type="text"
|
||||
:placeholder="searchPlaceholder"
|
||||
@keyup.enter="handleSubmit"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<multi-list
|
||||
v-if="selectedItems.length > 0"
|
||||
:add-new="addNew"
|
||||
:pill-invert="pillInvert"
|
||||
:items="selectedItemsAsObjects"
|
||||
:max-items="0"
|
||||
@remove-item="removeItem($event)"
|
||||
/>
|
||||
</b-dropdown-header>
|
||||
<template v-slot:button-content>
|
||||
<multi-list
|
||||
:items="selectedItemsAsObjects"
|
||||
:add-new="addNew"
|
||||
:pill-invert="pillInvert"
|
||||
:empty-message="emptyMessage"
|
||||
@remove-item="removeItem($event)"
|
||||
/>
|
||||
</template>
|
||||
<div
|
||||
v-if="addNew || availableToSelect.length > 0"
|
||||
:class="{
|
||||
'item-group': true,
|
||||
'add-new': availableToSelect.length === 0 && search !== '',
|
||||
'scroll': availableToSelect.length > 5
|
||||
}"
|
||||
>
|
||||
<b-dropdown-item-button
|
||||
v-for="item in availableToSelect"
|
||||
:key="item.id"
|
||||
class="ignore-hide multi-item"
|
||||
:class="{ 'none': item.id === 'none', selectListItem: true }"
|
||||
@click.prevent.stop="selectItem(item)"
|
||||
>
|
||||
<div
|
||||
v-markdown="item.name"
|
||||
class="label"
|
||||
></div>
|
||||
<div
|
||||
v-if="item.challenge"
|
||||
class="challenge"
|
||||
>
|
||||
{{ $t('challenge') }}
|
||||
</div>
|
||||
</b-dropdown-item-button>
|
||||
|
||||
<div
|
||||
v-if="addNew"
|
||||
class="hint"
|
||||
>
|
||||
{{ $t('pressEnterToAddTag', { tagName: search }) }}
|
||||
</div>
|
||||
</div>
|
||||
</b-dropdown>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
|
||||
$itemHeight: 2rem;
|
||||
|
||||
.select-multi {
|
||||
.dropdown-toggle {
|
||||
padding-left: 0.75rem;
|
||||
}
|
||||
|
||||
.dropdown-header {
|
||||
background-color: $gray-700;
|
||||
padding-bottom: 0;
|
||||
min-height: 3rem;
|
||||
}
|
||||
|
||||
.dropdown-item, .dropdown-header {
|
||||
padding-left: 0.75rem;
|
||||
padding-right: 0.75rem;
|
||||
}
|
||||
|
||||
.none {
|
||||
cursor: default;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.multi-item button {
|
||||
height: $itemHeight;
|
||||
display: flex;
|
||||
|
||||
.label {
|
||||
height: 1.5rem;
|
||||
font-size: 14px;
|
||||
line-height: 1.71;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.challenge {
|
||||
height: 1rem;
|
||||
font-size: 12px;
|
||||
font-weight: normal;
|
||||
font-stretch: normal;
|
||||
font-style: normal;
|
||||
line-height: 1.33;
|
||||
letter-spacing: normal;
|
||||
text-align: right;
|
||||
color: $gray-100;
|
||||
align-self: center;
|
||||
margin-top: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.challenge {
|
||||
color: $purple-300;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.item-group {
|
||||
max-height: #{5*$itemHeight};
|
||||
|
||||
&.add-new {
|
||||
height: 30px;
|
||||
|
||||
.hint {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
&.scroll {
|
||||
overflow-y: scroll;
|
||||
}
|
||||
}
|
||||
|
||||
.hint {
|
||||
display: none;
|
||||
height: 2rem;
|
||||
font-size: 12px;
|
||||
font-weight: normal;
|
||||
font-stretch: normal;
|
||||
font-style: normal;
|
||||
line-height: 1.33;
|
||||
letter-spacing: normal;
|
||||
color: $gray-100;
|
||||
|
||||
margin-left: 0.75rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import Vue from 'vue';
|
||||
import MultiList from '@/components/tasks/modal-controls/multiList';
|
||||
import markdownDirective from '@/directives/markdown';
|
||||
|
||||
export default {
|
||||
directives: {
|
||||
markdown: markdownDirective,
|
||||
},
|
||||
components: {
|
||||
MultiList,
|
||||
},
|
||||
props: {
|
||||
addNew: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
allItems: {
|
||||
type: Array,
|
||||
},
|
||||
emptyMessage: {
|
||||
type: String,
|
||||
},
|
||||
pillInvert: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
searchPlaceholder: {
|
||||
type: String,
|
||||
},
|
||||
selectedItems: {
|
||||
type: Array,
|
||||
},
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
preventHide: true,
|
||||
isOpened: false,
|
||||
selected: this.selectedItems,
|
||||
search: '',
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
selectedItemsIdList () {
|
||||
return this.selectedItems
|
||||
? this.selectedItems.map(t => t)
|
||||
: [];
|
||||
},
|
||||
allItemsMap () {
|
||||
const obj = {};
|
||||
this.allItems.forEach(t => {
|
||||
obj[t.id] = t;
|
||||
});
|
||||
return obj;
|
||||
},
|
||||
selectedItemsAsObjects () {
|
||||
return this.selectedItems.map(t => this.allItemsMap[t]);
|
||||
},
|
||||
availableToSelect () {
|
||||
const availableItems = this.allItems.filter(t => !this.selectedItemsIdList.includes(t.id));
|
||||
|
||||
const searchString = this.search.toLowerCase();
|
||||
|
||||
const filteredItems = availableItems.filter(i => i.name.toLowerCase().includes(searchString));
|
||||
|
||||
return filteredItems;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
selected () {
|
||||
this.$emit('changed', this.selected);
|
||||
},
|
||||
},
|
||||
created () {
|
||||
document.addEventListener('keyup', this.handleEsc);
|
||||
},
|
||||
beforeDestroy () {
|
||||
document.removeEventListener('keyup', this.handleEsc);
|
||||
},
|
||||
mounted () {
|
||||
this.$refs.dropdown.clickOutHandler = () => {
|
||||
this.closeSelectPopup();
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
closeSelectPopup () {
|
||||
this.preventHide = false;
|
||||
this.isOpened = false;
|
||||
Vue.nextTick(() => {
|
||||
this.$refs.dropdown.hide();
|
||||
});
|
||||
},
|
||||
openOrClose ($event) {
|
||||
if (this.isOpened) {
|
||||
this.closeSelectPopup();
|
||||
$event.preventDefault();
|
||||
}
|
||||
},
|
||||
closeIfOpen () {
|
||||
this.closeSelectPopup();
|
||||
},
|
||||
selectItem (item) {
|
||||
this.selectedItems.push(item.id);
|
||||
this.$emit('toggle', item.id);
|
||||
},
|
||||
removeItem ($event) {
|
||||
const foundIndex = this.selectedItems.findIndex(t => t === $event);
|
||||
|
||||
this.selectedItems.splice(foundIndex, 1);
|
||||
|
||||
this.$emit('toggle', $event);
|
||||
},
|
||||
hideCallback ($event) {
|
||||
if (this.preventHide) {
|
||||
$event.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
this.isOpened = false;
|
||||
},
|
||||
wasOpened () {
|
||||
this.isOpened = true;
|
||||
this.preventHide = true;
|
||||
},
|
||||
handleEsc (e) {
|
||||
if (e.keyCode === 27) {
|
||||
this.closeSelectPopup();
|
||||
}
|
||||
},
|
||||
handleSubmit () {
|
||||
if (!this.addNew) return;
|
||||
const { search } = this;
|
||||
this.$emit('addNew', search);
|
||||
|
||||
this.search = '';
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,76 @@
|
||||
<template>
|
||||
<div>
|
||||
<select-list
|
||||
:items="items"
|
||||
:value="selected"
|
||||
class="array-select"
|
||||
:class="{disabled: disabled}"
|
||||
:disabled="disabled"
|
||||
@select="selectItem($event)"
|
||||
>
|
||||
<template v-slot:item="{ item }">
|
||||
<span class="label">{{ $t(item) }}</span>
|
||||
</template>
|
||||
</select-list>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
|
||||
.array-select.disabled {
|
||||
.btn-secondary:disabled, .btn-secondary.disabled, .dropdown >
|
||||
.btn-secondary.dropdown-toggle:not(.btn-success):disabled, .dropdown >
|
||||
.btn-secondary.dropdown-toggle:not(.btn-success).disabled, .show >
|
||||
.btn-secondary.dropdown-toggle:not(.btn-success):disabled, .show >
|
||||
.btn-secondary.dropdown-toggle:not(.btn-success).disabled {
|
||||
background: $gray-700;
|
||||
}
|
||||
|
||||
.dropdown-toggle::after {
|
||||
color: $gray-300;
|
||||
border-top-color: $gray-300;
|
||||
}
|
||||
|
||||
.label {
|
||||
color: $gray-200;
|
||||
}
|
||||
}
|
||||
|
||||
.array-select .disabled, .array-select .disabled:hover {
|
||||
box-shadow: 0 1px 3px 0 rgba(26, 24, 29, 0.12), 0 1px 2px 0 rgba(26, 24, 29, 0.24);
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import selectList from '@/components/ui/selectList';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
selectList,
|
||||
},
|
||||
props: {
|
||||
items: {
|
||||
type: Array,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
},
|
||||
value: [String, Number, Object],
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
selected: this.items.find(i => i === this.value),
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
selectItem (item) {
|
||||
this.selected = item;
|
||||
this.$emit('select', item);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -1,187 +0,0 @@
|
||||
<template>
|
||||
<div class="tags-popup">
|
||||
<!-- eslint-disable vue/no-use-v-if-with-v-for -->
|
||||
<div
|
||||
v-for="tagsType in tagsByType"
|
||||
v-if="tagsType.tags.length > 0 || tagsType.key === 'tags'"
|
||||
:key="tagsType.key"
|
||||
class="tags-category d-flex"
|
||||
>
|
||||
<!-- eslint-enable vue/no-use-v-if-with-v-for -->
|
||||
<div class="tags-header">
|
||||
<strong v-once>{{ $t(tagsType.key) }}</strong>
|
||||
</div>
|
||||
<div class="tags-list container">
|
||||
<div class="row">
|
||||
<div
|
||||
v-for="(tag) in tagsType.tags"
|
||||
:key="tag.id"
|
||||
class="col-4"
|
||||
>
|
||||
<div class="custom-control custom-checkbox">
|
||||
<input
|
||||
:id="`tag-${tag.id}`"
|
||||
v-model="selectedTags"
|
||||
class="custom-control-input"
|
||||
type="checkbox"
|
||||
:value="tag.id"
|
||||
>
|
||||
<label
|
||||
v-markdown="tag.name"
|
||||
class="custom-control-label"
|
||||
:title="tag.name"
|
||||
:for="`tag-${tag.id}`"
|
||||
></label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tags-footer">
|
||||
<span
|
||||
class="clear-tags"
|
||||
@click="clearTags()"
|
||||
>{{ $t("clearTags") }}</span>
|
||||
<span
|
||||
class="close-tags"
|
||||
@click="close()"
|
||||
>{{ $t("close") }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
|
||||
.tags-popup {
|
||||
padding-left: 24px;
|
||||
padding-right: 24px;
|
||||
width: 593px;
|
||||
z-index: 9999;
|
||||
background: $white;
|
||||
border-radius: 2px;
|
||||
box-shadow: 0 2px 2px 0 rgba($black, 0.16), 0 1px 4px 0 rgba($black, 0.12);
|
||||
font-size: 14px;
|
||||
line-height: 1.43;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
.tags-category {
|
||||
border-bottom: 1px solid $gray-600;
|
||||
padding-bottom: 24px;
|
||||
padding-top: 24px;
|
||||
}
|
||||
|
||||
.tags-header {
|
||||
flex-basis: 96px;
|
||||
flex-shrink: 0;
|
||||
|
||||
a {
|
||||
font-size: 12px;
|
||||
line-height: 1.33;
|
||||
color: $blue-10;
|
||||
margin-top: 4px;
|
||||
|
||||
&:focus, &:hover, &:active {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tags-list {
|
||||
.custom-control-label {
|
||||
color: $gray-50 !important;
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
|
||||
.tags-footer {
|
||||
border-top: 1px solid $gray-600;
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
.close-tags {
|
||||
color: $blue-10;
|
||||
margin: 1.1em 0;
|
||||
margin-left: 2em;
|
||||
font-size: 14px;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.clear-tags {
|
||||
cursor: pointer;
|
||||
margin: 1.1em 0;
|
||||
color: $red-50;
|
||||
font-size: 14px;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import markdownDirective from '@/directives/markdown';
|
||||
|
||||
export default {
|
||||
directives: {
|
||||
markdown: markdownDirective,
|
||||
},
|
||||
props: ['tags', 'value'],
|
||||
data () {
|
||||
return {
|
||||
selectedTags: [],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
tagsByType () {
|
||||
const tagsByType = {
|
||||
challenges: {
|
||||
key: 'challenges',
|
||||
tags: [],
|
||||
},
|
||||
groups: {
|
||||
key: 'groups',
|
||||
tags: [],
|
||||
},
|
||||
user: {
|
||||
key: 'tags',
|
||||
tags: [],
|
||||
},
|
||||
};
|
||||
this.$props.tags.forEach(t => {
|
||||
if (t.group) {
|
||||
tagsByType.groups.tags.push(t);
|
||||
} else if (t.challenge) {
|
||||
tagsByType.challenges.tags.push(t);
|
||||
} else {
|
||||
tagsByType.user.tags.push(t);
|
||||
}
|
||||
});
|
||||
return tagsByType;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
selectedTags () {
|
||||
this.$emit('input', this.selectedTags);
|
||||
},
|
||||
},
|
||||
mounted () {
|
||||
this.selectedTags = this.value;
|
||||
},
|
||||
methods: {
|
||||
clearTags () {
|
||||
this.selectedTags = [];
|
||||
},
|
||||
close () {
|
||||
this.$emit('close');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -1,8 +1,12 @@
|
||||
<template>
|
||||
<div class="task-wrapper">
|
||||
<div
|
||||
class="task"
|
||||
:class="[{'groupTask': task.group.id}, `type_${task.type}`]"
|
||||
class="task transition"
|
||||
:class="[{
|
||||
'groupTask': task.group.id,
|
||||
'task-not-editable': !teamManagerAccess},
|
||||
`type_${task.type}`
|
||||
]"
|
||||
@click="castEnd($event, task)"
|
||||
>
|
||||
<approval-header
|
||||
@@ -12,12 +16,13 @@
|
||||
/>
|
||||
<div
|
||||
class="d-flex"
|
||||
:class="{'task-not-scoreable': isUser !== true}"
|
||||
:class="{'task-not-scoreable': isUser !== true || task.group.approval.requested
|
||||
&& !(task.group.approval.approved && task.type === 'habit')}"
|
||||
>
|
||||
<!-- Habits left side control-->
|
||||
<div
|
||||
v-if="task.type === 'habit'"
|
||||
class="left-control d-flex align-items-center justify-content-center"
|
||||
class="left-control d-flex justify-content-center pt-3"
|
||||
:class="[{
|
||||
'control-bottom-box': task.group.id,
|
||||
'control-top-box': approvalsClass
|
||||
@@ -28,8 +33,11 @@
|
||||
:class="[{
|
||||
'habit-control-positive-enabled': task.up && isUser,
|
||||
'habit-control-positive-disabled': !task.up && isUser,
|
||||
'task-not-scoreable': isUser !== true
|
||||
|| (task.group.approval.requested && !task.group.approval.approved),
|
||||
}, controlClass.up.inner]"
|
||||
@click="(isUser && task.up) ? score('up') : null"
|
||||
@click="(isUser && task.up && (!task.group.approval.requested
|
||||
|| task.group.approval.approved)) ? score('up') : null"
|
||||
>
|
||||
<div
|
||||
v-if="!isUser"
|
||||
@@ -55,7 +63,8 @@
|
||||
<div
|
||||
class="task-control daily-todo-control"
|
||||
:class="controlClass.inner"
|
||||
@click="isUser ? score(task.completed ? 'down' : 'up') : null"
|
||||
@click="isUser && !task.group.approval.requested
|
||||
? score(task.completed ? 'down' : 'up' ) : null"
|
||||
>
|
||||
<div
|
||||
v-if="!isUser"
|
||||
@@ -66,7 +75,10 @@
|
||||
<div
|
||||
v-else
|
||||
class="svg-icon check"
|
||||
:class="{'display-check-icon': task.completed, [controlClass.checkbox]: true}"
|
||||
:class="{
|
||||
'display-check-icon': task.completed || task.group.approval.requested,
|
||||
[controlClass.checkbox]: true,
|
||||
}"
|
||||
v-html="icons.check"
|
||||
></div>
|
||||
</div>
|
||||
@@ -74,7 +86,7 @@
|
||||
<!-- Task title, description and icons-->
|
||||
<div
|
||||
class="task-content"
|
||||
:class="contentClass"
|
||||
:class="[{'cursor-auto': !teamManagerAccess}, contentClass]"
|
||||
>
|
||||
<div
|
||||
class="task-clickable-area"
|
||||
@@ -85,7 +97,7 @@
|
||||
<h3
|
||||
v-markdown="task.text"
|
||||
class="task-title"
|
||||
:class="{ 'has-notes': task.notes }"
|
||||
:class="{ 'has-notes': task.notes || (!isUser && task.group.managerNotes)}"
|
||||
></h3>
|
||||
<menu-dropdown
|
||||
v-if="!isRunningYesterdailies && showOptions"
|
||||
@@ -157,7 +169,7 @@
|
||||
</menu-dropdown>
|
||||
</div>
|
||||
<div
|
||||
v-markdown="task.notes"
|
||||
v-markdown="displayNotes"
|
||||
class="task-notes small-text"
|
||||
:class="{'has-checklist': task.notes && hasChecklist}"
|
||||
></div>
|
||||
@@ -171,11 +183,12 @@
|
||||
<div
|
||||
v-b-tooltip.hover.right="$t(`${task.collapseChecklist
|
||||
? 'expand': 'collapse'}Checklist`)"
|
||||
class="collapse-checklist d-flex align-items-center expand-toggle"
|
||||
class="collapse-checklist mb-2 d-flex align-items-center expand-toggle"
|
||||
:class="{open: !task.collapseChecklist}"
|
||||
@click="collapseChecklist(task)"
|
||||
>
|
||||
<div
|
||||
v-once
|
||||
class="svg-icon"
|
||||
v-html="icons.checklist"
|
||||
></div>
|
||||
@@ -302,7 +315,7 @@
|
||||
<!-- Habits right side control-->
|
||||
<div
|
||||
v-if="task.type === 'habit'"
|
||||
class="right-control d-flex align-items-center justify-content-center"
|
||||
class="right-control d-flex justify-content-center pt-3"
|
||||
:class="[{
|
||||
'control-bottom-box': task.group.id,
|
||||
'control-top-box': approvalsClass}, controlClass.down.bg]"
|
||||
@@ -312,8 +325,11 @@
|
||||
:class="[{
|
||||
'habit-control-negative-enabled': task.down && isUser,
|
||||
'habit-control-negative-disabled': !task.down && isUser,
|
||||
'task-not-scoreable': isUser !== true
|
||||
|| (task.group.approval.requested && !task.group.approval.approved),
|
||||
}, controlClass.down.inner]"
|
||||
@click="(isUser && task.down) ? score('down') : null"
|
||||
@click="(isUser && task.down && (!task.group.approval.requested
|
||||
|| task.group.approval.approved)) ? score('down') : null"
|
||||
>
|
||||
<div
|
||||
v-if="!isUser"
|
||||
@@ -348,6 +364,7 @@
|
||||
v-if="task.group.id"
|
||||
:task="task"
|
||||
:group="group"
|
||||
@claimRewards="score('up')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -368,7 +385,7 @@
|
||||
}
|
||||
|
||||
.cursor-auto {
|
||||
cursor: auto;
|
||||
cursor: auto !important;
|
||||
}
|
||||
|
||||
.task {
|
||||
@@ -378,7 +395,7 @@
|
||||
border-radius: 2px;
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
&:hover:not(.task-not-editable) {
|
||||
box-shadow: 0 1px 8px 0 rgba($black, 0.12), 0 4px 4px 0 rgba($black, 0.16);
|
||||
z-index: 11;
|
||||
}
|
||||
@@ -393,8 +410,7 @@
|
||||
}
|
||||
|
||||
.task.groupTask {
|
||||
|
||||
&:hover {
|
||||
&:hover:not(.task-not-editable) {
|
||||
border: $purple-400 solid 1px;
|
||||
border-radius: 3px;
|
||||
margin: -1px; // to counter the border width
|
||||
@@ -551,7 +567,6 @@
|
||||
line-height: 1.2;
|
||||
text-align: center;
|
||||
color: $gray-200;
|
||||
margin-bottom: 9px;
|
||||
|
||||
&.open {
|
||||
}
|
||||
@@ -795,13 +810,9 @@
|
||||
|
||||
<script>
|
||||
import moment from 'moment';
|
||||
import axios from 'axios';
|
||||
import Vue from 'vue';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
import { mapState, mapGetters, mapActions } from '@/libs/store';
|
||||
import scoreTask from '@/../../common/script/ops/scoreTask';
|
||||
import * as Analytics from '@/libs/analytics';
|
||||
|
||||
import positiveIcon from '@/assets/svg/positive.svg';
|
||||
import negativeIcon from '@/assets/svg/negative.svg';
|
||||
@@ -820,7 +831,7 @@ import checklistIcon from '@/assets/svg/checklist.svg';
|
||||
import lockIcon from '@/assets/svg/lock.svg';
|
||||
import menuIcon from '@/assets/svg/menu.svg';
|
||||
import markdownDirective from '@/directives/markdown';
|
||||
import notifications from '@/mixins/notifications';
|
||||
import scoreTask from '@/mixins/scoreTask';
|
||||
import approvalHeader from './approvalHeader';
|
||||
import approvalFooter from './approvalFooter';
|
||||
import MenuDropdown from '../ui/customMenuDropdown';
|
||||
@@ -834,7 +845,7 @@ export default {
|
||||
directives: {
|
||||
markdown: markdownDirective,
|
||||
},
|
||||
mixins: [notifications],
|
||||
mixins: [scoreTask],
|
||||
props: ['task', 'isUser', 'group', 'challenge', 'dueDate'], // @TODO: maybe we should store the group on state?
|
||||
data () {
|
||||
return {
|
||||
@@ -918,6 +929,7 @@ export default {
|
||||
return classes;
|
||||
},
|
||||
showStreak () {
|
||||
if (!this.task.userId) return false;
|
||||
if (this.task.streak !== undefined) return true;
|
||||
if (this.task.type === 'habit' && (this.task.up || this.task.down)) return true;
|
||||
return false;
|
||||
@@ -948,7 +960,7 @@ export default {
|
||||
|
||||
return this.task.challenge.shortName ? this.task.challenge.shortName.toString() : '';
|
||||
},
|
||||
isChallangeTask () {
|
||||
isChallengeTask () {
|
||||
return !isEmpty(this.task.challenge);
|
||||
},
|
||||
isGroupTask () {
|
||||
@@ -957,7 +969,7 @@ export default {
|
||||
taskCategory () {
|
||||
let taskCategory = 'default';
|
||||
if (this.isGroupTask) taskCategory = 'group';
|
||||
else if (this.isChallangeTask) taskCategory = 'challenge';
|
||||
else if (this.isChallengeTask) taskCategory = 'challenge';
|
||||
return taskCategory;
|
||||
},
|
||||
showDelete () {
|
||||
@@ -969,6 +981,14 @@ export default {
|
||||
showOptions () {
|
||||
return this.showEdit || this.showDelete || this.isUser;
|
||||
},
|
||||
teamManagerAccess () {
|
||||
if (!this.isGroupTask || !this.group) return true;
|
||||
return (this.group.leader._id === this.user._id || this.group.managers[this.user._id]);
|
||||
},
|
||||
displayNotes () {
|
||||
if (this.isGroupTask && !this.isUser) return this.task.group.managerNotes;
|
||||
return this.task.notes;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions({
|
||||
@@ -1021,125 +1041,8 @@ export default {
|
||||
castEnd (e, task) {
|
||||
setTimeout(() => this.$root.$emit('castEnd', task, 'task', e), 0);
|
||||
},
|
||||
async score (direction) {
|
||||
if (this.castingSpell) return;
|
||||
|
||||
// TODO move to an action
|
||||
const Content = this.$store.state.content;
|
||||
const { user } = this;
|
||||
const { task } = this;
|
||||
|
||||
if (task.group.approval.required) {
|
||||
task.group.approval.requested = true;
|
||||
const groupResponse = await axios.get(`/api/v4/groups/${task.group.id}`);
|
||||
const managers = Object.keys(groupResponse.data.data.managers);
|
||||
managers.push(groupResponse.data.data.leader._id);
|
||||
if (managers.indexOf(user._id) !== -1) {
|
||||
task.group.approval.approved = true;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
scoreTask({ task, user, direction });
|
||||
} catch (err) {
|
||||
this.text(err.message);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (this.task.type) { // eslint-disable-line default-case
|
||||
case 'habit':
|
||||
this.$root.$emit('playSound', direction === 'up' ? 'Plus_Habit' : 'Minus_Habit');
|
||||
break;
|
||||
case 'todo':
|
||||
this.$root.$emit('playSound', 'Todo');
|
||||
break;
|
||||
case 'daily':
|
||||
this.$root.$emit('playSound', 'Daily');
|
||||
break;
|
||||
case 'reward':
|
||||
this.$root.$emit('playSound', 'Reward');
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
Analytics.updateUser();
|
||||
const response = await axios.post(`/api/v4/tasks/${task._id}/score/${direction}`);
|
||||
// used to notify drops, critical hits and other bonuses
|
||||
const tmp = response.data.data._tmp || {};
|
||||
const { crit } = tmp;
|
||||
const { drop } = tmp;
|
||||
const { firstDrops } = tmp;
|
||||
const { quest } = tmp;
|
||||
|
||||
if (crit) {
|
||||
const critBonus = crit * 100 - 100;
|
||||
this.crit(critBonus);
|
||||
}
|
||||
|
||||
if (quest && user.party.quest && user.party.quest.key) {
|
||||
const userQuest = Content.quests[user.party.quest.key];
|
||||
if (quest.progressDelta && userQuest.boss) {
|
||||
this.damage(quest.progressDelta.toFixed(1));
|
||||
} else if (quest.collection && userQuest.collect) {
|
||||
user.party.quest.progress.collectedItems += 1;
|
||||
this.quest('questCollection', quest.collection);
|
||||
}
|
||||
}
|
||||
|
||||
if (firstDrops) {
|
||||
if (!user.items.eggs[firstDrops.egg]) Vue.set(user.items.eggs, firstDrops.egg, 0);
|
||||
if (!user.items.hatchingPotions[firstDrops.hatchingPotion]) {
|
||||
Vue.set(user.items.hatchingPotions, firstDrops.hatchingPotion, 0);
|
||||
}
|
||||
user.items.eggs[firstDrops.egg] += 1;
|
||||
user.items.hatchingPotions[firstDrops.hatchingPotion] += 1;
|
||||
}
|
||||
|
||||
if (drop) {
|
||||
let dropText;
|
||||
let dropNotes;
|
||||
let type;
|
||||
|
||||
this.$root.$emit('playSound', 'Item_Drop');
|
||||
|
||||
// Note: For Mystery Item gear, drop.type will be 'head', 'armor', etc
|
||||
// so we use drop.notificationType below.
|
||||
|
||||
if (drop.type !== 'gear' && drop.type !== 'Quest' && drop.notificationType !== 'Mystery') {
|
||||
if (drop.type === 'Food') {
|
||||
type = 'food';
|
||||
} else if (drop.type === 'HatchingPotion') {
|
||||
type = 'hatchingPotions';
|
||||
} else {
|
||||
type = `${drop.type.toLowerCase()}s`;
|
||||
}
|
||||
|
||||
if (!user.items[type][drop.key]) {
|
||||
Vue.set(user.items[type], drop.key, 0);
|
||||
}
|
||||
user.items[type][drop.key] += 1;
|
||||
}
|
||||
|
||||
if (drop.type === 'HatchingPotion') {
|
||||
dropText = Content.hatchingPotions[drop.key].text();
|
||||
dropNotes = Content.hatchingPotions[drop.key].notes();
|
||||
this.drop(this.$t('messageDropPotion', { dropText, dropNotes }), drop);
|
||||
} else if (drop.type === 'Egg') {
|
||||
dropText = Content.eggs[drop.key].text();
|
||||
dropNotes = Content.eggs[drop.key].notes();
|
||||
this.drop(this.$t('messageDropEgg', { dropText, dropNotes }), drop);
|
||||
} else if (drop.type === 'Food') {
|
||||
dropText = Content.food[drop.key].textA();
|
||||
dropNotes = Content.food[drop.key].notes();
|
||||
this.drop(this.$t('messageDropFood', { dropText, dropNotes }), drop);
|
||||
} else if (drop.type === 'Quest') {
|
||||
// TODO $rootScope.selectedQuest = Content.quests[drop.key];
|
||||
// $rootScope.openModal('questDrop', {controller:'PartyCtrl', size:'sm'});
|
||||
} else {
|
||||
// Keep support for another type of drops that might be added
|
||||
this.drop(drop.dialog);
|
||||
}
|
||||
}
|
||||
score (direction) {
|
||||
this.taskScore(this.task, direction);
|
||||
},
|
||||
handleBrokenTask (task) {
|
||||
if (this.$store.state.isRunningYesterdailies) return;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
70
website/client/src/components/ui/checkbox.stories.js
Normal file
70
website/client/src/components/ui/checkbox.stories.js
Normal file
@@ -0,0 +1,70 @@
|
||||
/* eslint-disable import/no-extraneous-dependencies */
|
||||
import { storiesOf } from '@storybook/vue';
|
||||
import { withKnobs } from '@storybook/addon-knobs';
|
||||
|
||||
import Checkbox from './checkbox';
|
||||
import ToggleCheckbox from './toggleCheckbox';
|
||||
|
||||
const stories = storiesOf('Checkbox', module);
|
||||
|
||||
stories.addDecorator(withKnobs);
|
||||
|
||||
stories
|
||||
.add('checkbox', () => ({
|
||||
components: {
|
||||
Checkbox,
|
||||
},
|
||||
template: `
|
||||
<div style="position: absolute; margin: 20px">
|
||||
<Checkbox text="My Checkbox" id="someId"></Checkbox> <br/>
|
||||
<Checkbox text="My Checked Checkbox" id="someOtherId" :checked.sync="checked"></Checkbox>
|
||||
</div>
|
||||
`,
|
||||
data () {
|
||||
return {
|
||||
checked: true,
|
||||
};
|
||||
},
|
||||
}))
|
||||
.add('Toggle Checkbox Group', () => ({
|
||||
components: {
|
||||
ToggleCheckbox,
|
||||
},
|
||||
template: `
|
||||
<div style="position: absolute; margin: 20px">
|
||||
{{ checked }}
|
||||
<div class="toggle-group" style="width: 300px">
|
||||
<ToggleCheckbox text="Su"></ToggleCheckbox>
|
||||
<ToggleCheckbox text="Mo"
|
||||
:checked.sync="checked"></ToggleCheckbox>
|
||||
<ToggleCheckbox text="Tu"></ToggleCheckbox>
|
||||
<ToggleCheckbox text="We"
|
||||
:checked.sync="checked"></ToggleCheckbox>
|
||||
<ToggleCheckbox text="Th"></ToggleCheckbox>
|
||||
<ToggleCheckbox text="Fr"
|
||||
:checked.sync="checked"></ToggleCheckbox>
|
||||
<ToggleCheckbox text="Sa"
|
||||
:checked.sync="checked"
|
||||
:disabled="true"></ToggleCheckbox>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
Disabled:
|
||||
<div class="toggle-group" style="width: 300px">
|
||||
<ToggleCheckbox text="Su" :disabled="true"></ToggleCheckbox>
|
||||
<ToggleCheckbox text="Mo" :disabled="true"></ToggleCheckbox>
|
||||
<ToggleCheckbox text="Tu" :disabled="true"></ToggleCheckbox>
|
||||
<ToggleCheckbox text="We" :disabled="true"></ToggleCheckbox>
|
||||
<ToggleCheckbox text="Th" :disabled="true"></ToggleCheckbox>
|
||||
<ToggleCheckbox text="Fr" :disabled="true"></ToggleCheckbox>
|
||||
<ToggleCheckbox text="Sa" :disabled="true"></ToggleCheckbox>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
data () {
|
||||
return {
|
||||
checked: true,
|
||||
};
|
||||
},
|
||||
}));
|
||||
@@ -6,18 +6,24 @@
|
||||
v-model="isChecked"
|
||||
class="custom-control-input"
|
||||
type="checkbox"
|
||||
:disabled="disabled"
|
||||
>
|
||||
<label
|
||||
v-once
|
||||
class="custom-control-label"
|
||||
:class="{disabled: disabled}"
|
||||
:for="id"
|
||||
:disabled="disabled"
|
||||
>{{ text }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
label {
|
||||
.custom-control.custom-checkbox {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
label:not(:disabled):not(.disabled) {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
@@ -26,6 +32,7 @@
|
||||
export default {
|
||||
props: {
|
||||
checked: Boolean,
|
||||
disabled: Boolean,
|
||||
id: String,
|
||||
text: String,
|
||||
},
|
||||
@@ -35,6 +42,9 @@ export default {
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
checked (after) {
|
||||
this.isChecked = after;
|
||||
},
|
||||
isChecked (after) {
|
||||
this.$emit('update:checked', after);
|
||||
},
|
||||
|
||||
71
website/client/src/components/ui/input-group.stories.js
Normal file
71
website/client/src/components/ui/input-group.stories.js
Normal file
@@ -0,0 +1,71 @@
|
||||
/* eslint-disable import/no-extraneous-dependencies */
|
||||
import { storiesOf } from '@storybook/vue';
|
||||
import { number, text, withKnobs } from '@storybook/addon-knobs';
|
||||
|
||||
import positiveIcon from '@/assets/svg/positive.svg';
|
||||
|
||||
const stories = storiesOf('Input-Group', module);
|
||||
|
||||
stories.addDecorator(withKnobs);
|
||||
|
||||
stories
|
||||
.add('states', () => ({
|
||||
components: { },
|
||||
template: `
|
||||
<div style="position: absolute; margin: 20px">
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend positive-addon input-group-icon">
|
||||
<div
|
||||
class="svg-icon"
|
||||
v-html="icon"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
v-model="number"
|
||||
class="form-control"
|
||||
type="number"
|
||||
min="0"
|
||||
required="required"
|
||||
ref="input"
|
||||
>
|
||||
</div>
|
||||
<br />
|
||||
<button class="btn btn-dark" @click="$refs.input.focus()">Focus ^</button>
|
||||
|
||||
<br />
|
||||
<br />
|
||||
<div class="input-group">
|
||||
|
||||
<input
|
||||
v-model="number"
|
||||
class="form-control"
|
||||
type="number"
|
||||
min="0"
|
||||
required="required"
|
||||
>
|
||||
<div class="input-group-append positive-addon input-group-icon">
|
||||
<div
|
||||
class="svg-icon"
|
||||
v-html="icon"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
`,
|
||||
data () {
|
||||
return {
|
||||
icon: positiveIcon,
|
||||
};
|
||||
},
|
||||
props: {
|
||||
text: {
|
||||
default: text('Input Text', 'example text'),
|
||||
},
|
||||
number: {
|
||||
default: number('Input Number', 0),
|
||||
},
|
||||
},
|
||||
}));
|
||||
45
website/client/src/components/ui/margin.stories.js
Normal file
45
website/client/src/components/ui/margin.stories.js
Normal file
@@ -0,0 +1,45 @@
|
||||
/* eslint-disable import/no-extraneous-dependencies */
|
||||
import { storiesOf } from '@storybook/vue';
|
||||
|
||||
const stories = storiesOf('Margins', module);
|
||||
|
||||
const margins = [
|
||||
'mr-1 ml-1 my-1',
|
||||
'mx-2 ml-3 my-2',
|
||||
'mx-2 ml-1 my-1',
|
||||
'ml-1 mr-4',
|
||||
'ml-2 mr-2 my-1',
|
||||
'ml-75 my-3 mr-2',
|
||||
];
|
||||
|
||||
stories
|
||||
.add('overview', () => ({
|
||||
components: { },
|
||||
template: `
|
||||
<div style="position: absolute; margin: 20px">
|
||||
<span class="background inline-block">
|
||||
<span class="content mx-1 my-1 inline-block">
|
||||
<span class="text mx-1 my-1 inline-block">
|
||||
The margin between gray and teal is the margin content.
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<br />
|
||||
<br />
|
||||
|
||||
<span v-for="m in margins"
|
||||
class="background mx-1 my-1 inline-block">
|
||||
<span class="content inline-block" :class="m">
|
||||
<span class="mx-1 my-1 inline-block">{{m}}</span>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
`,
|
||||
|
||||
data () {
|
||||
return {
|
||||
margins,
|
||||
};
|
||||
},
|
||||
}));
|
||||
110
website/client/src/components/ui/selectList.stories.js
Normal file
110
website/client/src/components/ui/selectList.stories.js
Normal file
@@ -0,0 +1,110 @@
|
||||
/* eslint-disable import/no-extraneous-dependencies */
|
||||
import { storiesOf } from '@storybook/vue';
|
||||
import { withKnobs } from '@storybook/addon-knobs';
|
||||
|
||||
import SelectList from './selectList.vue';
|
||||
import SelectDifficulty from '../tasks/modal-controls/selectDifficulty';
|
||||
import SelectTranslatedArray from '../tasks/modal-controls/selectTranslatedArray';
|
||||
|
||||
const stories = storiesOf('Select List', module);
|
||||
|
||||
stories.addDecorator(withKnobs);
|
||||
|
||||
stories
|
||||
.add('states', () => ({
|
||||
components: { SelectList },
|
||||
template: `
|
||||
<div class="m-xl">
|
||||
Hover / Click on:
|
||||
<select-list class="mb-4"
|
||||
:items="items"
|
||||
:key-prop="'key'"
|
||||
:value="selected"
|
||||
@select="selected = $event">
|
||||
<template v-slot:item="{ item }">
|
||||
<div v-if="item">
|
||||
Template: {{ item?.key }} - {{ item?.value.text }}
|
||||
</div>
|
||||
<div v-else>
|
||||
Nothing selected
|
||||
</div>
|
||||
</template>
|
||||
</select-list>
|
||||
|
||||
Disabled:
|
||||
<select-list :disabled="true"
|
||||
:value="selected"
|
||||
:items="items"
|
||||
:key-prop="'key'"
|
||||
class="mb-4">
|
||||
<template v-slot:item="{ item }">
|
||||
Template: {{ item?.key }} - {{ item?.value.text }}
|
||||
</template>
|
||||
</select-list>
|
||||
|
||||
<br/>
|
||||
Selected: {{ selected }} <br/>
|
||||
|
||||
</div>
|
||||
`,
|
||||
data () {
|
||||
return {
|
||||
selected: null,
|
||||
items: [
|
||||
{
|
||||
key: 1,
|
||||
value: {
|
||||
text: 'First',
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 2,
|
||||
value: {
|
||||
text: 'Second',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
}))
|
||||
.add('difficulty', () => ({
|
||||
components: { SelectDifficulty },
|
||||
template: `
|
||||
<div class="m-xl">
|
||||
<select-difficulty
|
||||
:value="selected"
|
||||
@select="selected = $event"
|
||||
>
|
||||
|
||||
</select-difficulty>
|
||||
|
||||
Selected: {{ selected }}
|
||||
</div>
|
||||
`,
|
||||
data () {
|
||||
return {
|
||||
selected: 2,
|
||||
};
|
||||
},
|
||||
}))
|
||||
.add('translated array', () => ({
|
||||
components: { SelectTranslatedArray },
|
||||
template: `
|
||||
<div class="m-xl">
|
||||
<select-translated-array
|
||||
:items="['daily', 'weekly', 'monthly']"
|
||||
:value="selected"
|
||||
@select="selected = $event"
|
||||
>
|
||||
|
||||
</select-translated-array>
|
||||
|
||||
Selected: {{ selected }}
|
||||
</div>
|
||||
`,
|
||||
data () {
|
||||
return {
|
||||
selected: 'weekly',
|
||||
};
|
||||
},
|
||||
}));
|
||||
75
website/client/src/components/ui/selectList.vue
Normal file
75
website/client/src/components/ui/selectList.vue
Normal file
@@ -0,0 +1,75 @@
|
||||
<template>
|
||||
<div>
|
||||
<b-dropdown
|
||||
class="inline-dropdown"
|
||||
:toggle-class="isOpened ? 'active' : null"
|
||||
:disabled="disabled"
|
||||
@show="isOpened = true"
|
||||
@hide="isOpened = false"
|
||||
>
|
||||
<template v-slot:button-content>
|
||||
<slot
|
||||
name="item"
|
||||
:item="selected"
|
||||
:button="true"
|
||||
>
|
||||
<!-- Fallback content -->
|
||||
{{ value }}
|
||||
</slot>
|
||||
</template>
|
||||
<b-dropdown-item
|
||||
v-for="item in items"
|
||||
:key="keyProp ? item[keyProp] : item"
|
||||
:disabled="typeof item[disabledProp] === 'undefined' ? false : item[disabledProp]"
|
||||
:class="{active: item === selected, selectListItem: true}"
|
||||
@click="selectItem(item)"
|
||||
>
|
||||
<slot
|
||||
name="item"
|
||||
:item="item"
|
||||
:button="false"
|
||||
>
|
||||
<!-- Fallback content -->
|
||||
{{ item }}
|
||||
</slot>
|
||||
</b-dropdown-item>
|
||||
</b-dropdown>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
|
||||
</style>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
items: {
|
||||
type: Array,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
},
|
||||
value: [String, Number, Object],
|
||||
keyProp: {
|
||||
type: String,
|
||||
},
|
||||
disabledProp: {
|
||||
type: String,
|
||||
},
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
isOpened: false,
|
||||
selected: this.value,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
selectItem (item) {
|
||||
this.selected = item;
|
||||
this.$emit('select', item);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
118
website/client/src/components/ui/toggleCheckbox.vue
Normal file
118
website/client/src/components/ui/toggleCheckbox.vue
Normal file
@@ -0,0 +1,118 @@
|
||||
<template>
|
||||
<button
|
||||
class="toggle-checkbox"
|
||||
:class="{checked: isChecked}"
|
||||
type="button"
|
||||
:disabled="disabled"
|
||||
@click="isChecked = !isChecked"
|
||||
>
|
||||
{{ text }}
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
checked: Boolean,
|
||||
disabled: Boolean,
|
||||
text: String,
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
isChecked: this.checked,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
checked (after) {
|
||||
this.isChecked = after;
|
||||
},
|
||||
isChecked (after) {
|
||||
this.$emit('update:checked', after);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
|
||||
.toggle-checkbox {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
height: 2rem;
|
||||
border: solid 1px $gray-400;
|
||||
background-color: $white;
|
||||
|
||||
cursor: pointer;
|
||||
|
||||
> * {
|
||||
line-height: 1.71;
|
||||
margin-top: 0.25rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
&:disabled, &.disabled {
|
||||
cursor: default;
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
&.checked {
|
||||
border-color: $purple-100;
|
||||
background-color: $purple-300;
|
||||
color: $white;
|
||||
|
||||
&:active {
|
||||
outline: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.disabled):not(:disabled) {
|
||||
&:not(.checked) {
|
||||
&:hover {
|
||||
border-color: $gray-300;
|
||||
background-color: $white;
|
||||
color: $purple-300;
|
||||
}
|
||||
|
||||
&:focus, &:active {
|
||||
border-color: $purple-400;
|
||||
background-color: $white;
|
||||
color: $gray-50;
|
||||
|
||||
outline: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&:focus, &:active {
|
||||
outline: 1px solid $purple-400;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(:first-of-type) {
|
||||
border-left: none !important;
|
||||
}
|
||||
|
||||
&:first-of-type {
|
||||
border-bottom-left-radius: 2px;
|
||||
border-top-left-radius: 2px;
|
||||
}
|
||||
&:last-of-type {
|
||||
border-bottom-right-radius: 2px;
|
||||
border-top-right-radius: 2px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
.toggle-group {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
width: 100%;
|
||||
|
||||
.toggle-checkbox {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -25,7 +25,13 @@
|
||||
:for="toggleId"
|
||||
>
|
||||
<span class="toggle-switch-inner"></span>
|
||||
<span class="toggle-switch-switch"></span>
|
||||
<span
|
||||
class="toggle-switch-switch"
|
||||
tabindex="0"
|
||||
@focus="handleFocus"
|
||||
@blur="handleBlur"
|
||||
@keyup.space="handleSpace"
|
||||
></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
@@ -89,18 +95,18 @@
|
||||
.toggle-switch-inner:before {
|
||||
content: "";
|
||||
padding-left: 10px;
|
||||
background-color: $purple-400;
|
||||
background-color: $green-50;
|
||||
}
|
||||
|
||||
.toggle-switch-inner:after {
|
||||
content: "";
|
||||
padding-right: 10px;
|
||||
background-color: $gray-200;
|
||||
background-color: $gray-300;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.toggle-switch-switch {
|
||||
box-shadow: 0 1px 2px 0 rgba($black, 0.32);
|
||||
box-shadow: 0 1px 3px 0 rgba($black, 0.12), 0 1px 2px 0 rgba($black, 0.24);
|
||||
display: block;
|
||||
width: 20px;
|
||||
margin: -2px;
|
||||
@@ -113,6 +119,11 @@
|
||||
right: 22px;
|
||||
border-radius: 100px;
|
||||
transition: all 0.3s ease-in 0s;
|
||||
|
||||
&:focus {
|
||||
border: 1px solid $purple-400;
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
.toggle-switch-checkbox:checked + .toggle-switch-label .toggle-switch-inner {
|
||||
@@ -151,6 +162,7 @@ export default {
|
||||
toggleId: this.generateId(),
|
||||
// The container requires a unique id to link it to the pop-over
|
||||
containerId: this.generateId(),
|
||||
focused: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -159,9 +171,20 @@ export default {
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
handleBlur () {
|
||||
this.focused = false;
|
||||
},
|
||||
handleChange ({ target: { checked } }) {
|
||||
this.$emit('change', checked);
|
||||
},
|
||||
handleFocus () {
|
||||
this.focused = true;
|
||||
},
|
||||
handleSpace () {
|
||||
if (this.focused) {
|
||||
document.getElementById(this.toggleId).click();
|
||||
}
|
||||
},
|
||||
generateId () {
|
||||
return `id-${Math.random().toString(36).substr(2, 16)}`;
|
||||
},
|
||||
|
||||
@@ -9,12 +9,7 @@
|
||||
class="profile"
|
||||
>
|
||||
<div class="header">
|
||||
<span
|
||||
class="close-icon svg-icon inline icon-10"
|
||||
@click="close()"
|
||||
v-html="icons.close"
|
||||
></span>
|
||||
<div class="profile-actions">
|
||||
<div class="profile-actions d-flex">
|
||||
<router-link
|
||||
:to="{ path: '/private-messages', query: { uuid: user._id } }"
|
||||
replace
|
||||
@@ -24,6 +19,7 @@
|
||||
class="btn btn-secondary message-icon"
|
||||
>
|
||||
<div
|
||||
v-once
|
||||
class="svg-icon message-icon"
|
||||
v-html="icons.message"
|
||||
></div>
|
||||
@@ -42,7 +38,7 @@
|
||||
<button
|
||||
v-if="user._id !== userLoggedIn._id && userLoggedIn.inbox.blocks.indexOf(user._id) === -1"
|
||||
v-b-tooltip.hover.right="$t('blockWarning')"
|
||||
class="btn btn-secondary block-icon"
|
||||
class="btn btn-secondary block-icon d-flex justify-content-center align-items-center"
|
||||
@click="blockUser()"
|
||||
>
|
||||
<div
|
||||
@@ -65,7 +61,7 @@
|
||||
<button
|
||||
v-if="userLoggedIn.contributor.admin"
|
||||
v-b-tooltip.hover.right="'Admin - Toggle Tools'"
|
||||
class="btn btn-secondary positive-icon"
|
||||
class="btn btn-secondary positive-icon d-flex justify-content-center align-items-center"
|
||||
@click="toggleAdminTools()"
|
||||
>
|
||||
<div
|
||||
@@ -292,13 +288,13 @@
|
||||
</div>
|
||||
<div class="col-12 text-center">
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
class="btn btn-primary mr-2"
|
||||
@click="save()"
|
||||
>
|
||||
{{ $t("save") }}
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-warning"
|
||||
class="btn btn-secondary"
|
||||
@click="editing = false"
|
||||
>
|
||||
{{ $t("cancel") }}
|
||||
@@ -329,7 +325,7 @@
|
||||
>
|
||||
<div
|
||||
:id="achievKey + '-achievement'"
|
||||
class="box achievement-container d-flex align-items-center justify-content-center"
|
||||
class="box achievement-container"
|
||||
:class="{'achievement-unearned': !achievement.earned}"
|
||||
>
|
||||
<b-popover
|
||||
@@ -437,6 +433,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
.standard-page {
|
||||
padding-bottom: 0rem;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: #f9f9f9;
|
||||
}
|
||||
@@ -570,8 +570,8 @@
|
||||
|
||||
.achievement-wrapper {
|
||||
width: 94px;
|
||||
min-width: 94px !important;
|
||||
max-width: 94px;
|
||||
min-width: 94px;
|
||||
margin-right: 12px;
|
||||
margin-left: 12px;
|
||||
padding: 0px;
|
||||
@@ -580,6 +580,7 @@
|
||||
.box {
|
||||
margin: 0 auto;
|
||||
margin-bottom: 1em;
|
||||
padding-top: 1.2em;
|
||||
background: $white;
|
||||
}
|
||||
|
||||
@@ -618,6 +619,7 @@
|
||||
margin-right: 8px;
|
||||
background: $gray-600;
|
||||
color: $gray-300;
|
||||
height: fit-content;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -729,7 +731,6 @@ import lock from '@/assets/svg/lock.svg';
|
||||
import challenge from '@/assets/svg/challenge.svg';
|
||||
import member from '@/assets/svg/member-icon.svg';
|
||||
import staff from '@/assets/svg/tier-staff.svg';
|
||||
import svgClose from '@/assets/svg/close.svg';
|
||||
import error404 from '../404';
|
||||
// @TODO: EMAILS.COMMUNITY_MANAGER_EMAIL
|
||||
const COMMUNITY_MANAGER_EMAIL = 'admin@habitica.com';
|
||||
@@ -757,7 +758,6 @@ export default {
|
||||
lock,
|
||||
member,
|
||||
staff,
|
||||
close: svgClose,
|
||||
}),
|
||||
adminToolsLoaded: false,
|
||||
userIdToMessage: '',
|
||||
@@ -1021,9 +1021,6 @@ export default {
|
||||
const status = this.achievementsCategories[categoryKey].open;
|
||||
this.achievementsCategories[categoryKey].open = !status;
|
||||
},
|
||||
close () {
|
||||
this.$root.$emit('bv::hide::modal', 'profile');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -8,13 +8,14 @@ export function setUpAxios (AUTH_SETTINGS) { // eslint-disable-line import/prefe
|
||||
AUTH_SETTINGS = JSON.parse(AUTH_SETTINGS); // eslint-disable-line no-param-reassign
|
||||
}
|
||||
|
||||
const browserTimezoneOffset = moment().zone();
|
||||
const browserTimezoneUtcOffset = moment().utcOffset();
|
||||
|
||||
if (AUTH_SETTINGS.auth && AUTH_SETTINGS.auth.apiId && AUTH_SETTINGS.auth.apiToken) {
|
||||
axios.defaults.headers.common['x-api-user'] = AUTH_SETTINGS.auth.apiId;
|
||||
axios.defaults.headers.common['x-api-key'] = AUTH_SETTINGS.auth.apiToken;
|
||||
|
||||
axios.defaults.headers.common['x-user-timezoneOffset'] = browserTimezoneOffset;
|
||||
// Communicate in "old" timezone variant for backwards compatibility
|
||||
axios.defaults.headers.common['x-user-timezoneOffset'] = -browserTimezoneUtcOffset;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
136
website/client/src/mixins/scoreTask.js
Normal file
136
website/client/src/mixins/scoreTask.js
Normal file
@@ -0,0 +1,136 @@
|
||||
import axios from 'axios';
|
||||
import Vue from 'vue';
|
||||
|
||||
import * as Analytics from '@/libs/analytics';
|
||||
import notifications from './notifications';
|
||||
import scoreTask from '@/../../common/script/ops/scoreTask';
|
||||
import { mapState } from '@/libs/store';
|
||||
|
||||
export default {
|
||||
mixins: [notifications],
|
||||
computed: {
|
||||
...mapState({
|
||||
castingSpell: 'spellOptions.castingSpell',
|
||||
user: 'user.data',
|
||||
}),
|
||||
},
|
||||
methods: {
|
||||
async taskScore (task, direction) {
|
||||
if (this.castingSpell) return;
|
||||
const { user } = this;
|
||||
|
||||
const Content = this.$store.state.content;
|
||||
|
||||
if (task.group.approval.required && !task.group.approval.approved) {
|
||||
task.group.approval.requested = true;
|
||||
const groupResponse = await axios.get(`/api/v4/groups/${task.group.id}`);
|
||||
const managers = Object.keys(groupResponse.data.data.managers);
|
||||
managers.push(groupResponse.data.data.leader._id);
|
||||
if (managers.indexOf(user._id) !== -1) {
|
||||
task.group.approval.approved = true;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
scoreTask({ task, user, direction });
|
||||
} catch (err) {
|
||||
this.text(err.message);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (task.type) { // eslint-disable-line default-case
|
||||
case 'habit':
|
||||
this.$root.$emit('playSound', direction === 'up' ? 'Plus_Habit' : 'Minus_Habit');
|
||||
break;
|
||||
case 'todo':
|
||||
this.$root.$emit('playSound', 'Todo');
|
||||
break;
|
||||
case 'daily':
|
||||
this.$root.$emit('playSound', 'Daily');
|
||||
break;
|
||||
case 'reward':
|
||||
this.$root.$emit('playSound', 'Reward');
|
||||
break;
|
||||
}
|
||||
|
||||
Analytics.updateUser();
|
||||
const response = await axios.post(`/api/v4/tasks/${task._id}/score/${direction}`);
|
||||
// used to notify drops, critical hits and other bonuses
|
||||
const tmp = response.data.data._tmp || {};
|
||||
const { crit } = tmp;
|
||||
const { drop } = tmp;
|
||||
const { firstDrops } = tmp;
|
||||
const { quest } = tmp;
|
||||
|
||||
if (crit) {
|
||||
const critBonus = crit * 100 - 100;
|
||||
this.crit(critBonus);
|
||||
}
|
||||
|
||||
if (quest && user.party.quest && user.party.quest.key) {
|
||||
const userQuest = Content.quests[user.party.quest.key];
|
||||
if (quest.progressDelta && userQuest.boss) {
|
||||
this.damage(quest.progressDelta.toFixed(1));
|
||||
} else if (quest.collection && userQuest.collect) {
|
||||
user.party.quest.progress.collectedItems += 1;
|
||||
this.quest('questCollection', quest.collection);
|
||||
}
|
||||
}
|
||||
|
||||
if (firstDrops) {
|
||||
if (!user.items.eggs[firstDrops.egg]) Vue.set(user.items.eggs, firstDrops.egg, 0);
|
||||
if (!user.items.hatchingPotions[firstDrops.hatchingPotion]) {
|
||||
Vue.set(user.items.hatchingPotions, firstDrops.hatchingPotion, 0);
|
||||
}
|
||||
user.items.eggs[firstDrops.egg] += 1;
|
||||
user.items.hatchingPotions[firstDrops.hatchingPotion] += 1;
|
||||
}
|
||||
|
||||
if (drop) {
|
||||
let dropText;
|
||||
let dropNotes;
|
||||
let type;
|
||||
|
||||
this.$root.$emit('playSound', 'Item_Drop');
|
||||
|
||||
// Note: For Mystery Item gear, drop.type will be 'head', 'armor', etc
|
||||
// so we use drop.notificationType below.
|
||||
|
||||
if (drop.type !== 'gear' && drop.type !== 'Quest' && drop.notificationType !== 'Mystery') {
|
||||
if (drop.type === 'Food') {
|
||||
type = 'food';
|
||||
} else if (drop.type === 'HatchingPotion') {
|
||||
type = 'hatchingPotions';
|
||||
} else {
|
||||
type = `${drop.type.toLowerCase()}s`;
|
||||
}
|
||||
|
||||
if (!user.items[type][drop.key]) {
|
||||
Vue.set(user.items[type], drop.key, 0);
|
||||
}
|
||||
user.items[type][drop.key] += 1;
|
||||
}
|
||||
|
||||
if (drop.type === 'HatchingPotion') {
|
||||
dropText = Content.hatchingPotions[drop.key].text();
|
||||
dropNotes = Content.hatchingPotions[drop.key].notes();
|
||||
this.drop(this.$t('messageDropPotion', { dropText, dropNotes }), drop);
|
||||
} else if (drop.type === 'Egg') {
|
||||
dropText = Content.eggs[drop.key].text();
|
||||
dropNotes = Content.eggs[drop.key].notes();
|
||||
this.drop(this.$t('messageDropEgg', { dropText, dropNotes }), drop);
|
||||
} else if (drop.type === 'Food') {
|
||||
dropText = Content.food[drop.key].textA();
|
||||
dropNotes = Content.food[drop.key].notes();
|
||||
this.drop(this.$t('messageDropFood', { dropText, dropNotes }), drop);
|
||||
} else if (drop.type === 'Quest') {
|
||||
// TODO $rootScope.selectedQuest = Content.quests[drop.key];
|
||||
// $rootScope.openModal('questDrop', {controller:'PartyCtrl', size:'sm'});
|
||||
} else {
|
||||
// Keep support for another type of drops that might be added
|
||||
this.drop(drop.dialog);
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -4,6 +4,9 @@ export default {
|
||||
methods: {
|
||||
async sync () {
|
||||
this.$root.$emit(EVENTS.RESYNC_REQUESTED);
|
||||
if (this.$route.fullPath.indexOf('task-information') !== -1) {
|
||||
this.$root.$emit('habitica:team-sync');
|
||||
}
|
||||
await Promise.all([
|
||||
this.$store.dispatch('user:fetch', { forceLoad: true }),
|
||||
this.$store.dispatch('tasks:fetchUserTasks', { forceLoad: true }),
|
||||
|
||||
@@ -9,9 +9,13 @@ export async function getTags () {
|
||||
export async function createTag (store, payload) {
|
||||
const url = 'api/v4/tags';
|
||||
const response = await axios.post(url, {
|
||||
tagDetails: payload.tagDetails,
|
||||
name: payload.name,
|
||||
});
|
||||
return response.data.data;
|
||||
|
||||
const tag = response.data.data;
|
||||
|
||||
store.state.user.data.tags.push(tag);
|
||||
return tag;
|
||||
}
|
||||
|
||||
export async function getTag (store, payload) {
|
||||
|
||||
@@ -13,6 +13,15 @@ export function getTagsFor (store) {
|
||||
.map(tag => tag.name);
|
||||
}
|
||||
|
||||
export function getTagsByIdList (store) {
|
||||
return function tagsByIdListFunc (taskIdArray) {
|
||||
return (taskIdArray || []).length > 0
|
||||
? store.state.user.data.tags
|
||||
.filter(tag => taskIdArray.indexOf(tag.id) !== -1)
|
||||
: [];
|
||||
};
|
||||
}
|
||||
|
||||
function getTaskColor (task) {
|
||||
if (task.type === 'reward' || task.byHabitica) return 'purple';
|
||||
|
||||
@@ -110,7 +119,9 @@ export function canEdit (store) {
|
||||
|
||||
function _nonInteractive (task) {
|
||||
return (task.group && task.group.id && !task.userId)
|
||||
|| (task.challenge && task.challenge.id && !task.userId);
|
||||
|| (task.challenge && task.challenge.id && !task.userId)
|
||||
|| (task.group && task.group.approval && task.group.approval.requested
|
||||
&& task.type !== 'habit');
|
||||
}
|
||||
|
||||
export function getTaskClasses (store) {
|
||||
@@ -132,6 +143,8 @@ export function getTaskClasses (store) {
|
||||
return `task-${color}-modal-content`;
|
||||
case 'create-modal-content':
|
||||
return 'task-purple-modal-content';
|
||||
case 'edit-modal-headings':
|
||||
return `task-${color}-modal-headings`;
|
||||
case 'edit-modal-text':
|
||||
return `task-${color}-modal-text`;
|
||||
case 'edit-modal-icon':
|
||||
@@ -144,6 +157,8 @@ export function getTaskClasses (store) {
|
||||
return `task-${color}-modal-habit-control-disabled`;
|
||||
case 'create-modal-bg':
|
||||
return 'task-purple-modal-bg';
|
||||
case 'create-modal-headings':
|
||||
return 'task-purple-modal-headings';
|
||||
case 'create-modal-text':
|
||||
return 'task-purple-modal-text';
|
||||
case 'create-modal-input':
|
||||
|
||||
@@ -17,8 +17,8 @@ const IS_TEST = process.env.NODE_ENV === 'test'; // eslint-disable-line no-proce
|
||||
// before trying to load data
|
||||
let isUserLoggedIn = false;
|
||||
|
||||
// eg, 240 - this will be converted on server as -(offset/60)
|
||||
const browserTimezoneOffset = moment().zone();
|
||||
// eg, -240 - this will be converted on server as (offset/60)
|
||||
const browserTimezoneUtcOffset = moment().utcOffset();
|
||||
|
||||
axios.defaults.headers.common['x-client'] = 'habitica-web';
|
||||
|
||||
@@ -71,7 +71,7 @@ export default function () {
|
||||
// store the timezone offset in case it's different than the one in
|
||||
// user.preferences.timezoneOffset and change it after the user is synced
|
||||
// in app.vue
|
||||
browserTimezoneOffset,
|
||||
browserTimezoneUtcOffset,
|
||||
tasks: asyncResourceFactory(), // user tasks
|
||||
// @TODO use asyncresource?
|
||||
completedTodosStatus: 'NOT_LOADED',
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"achievementMindOverMatterText": "Dokončil/a kamennou, slizovou a vlněnou mazlíčkovou výpravu.",
|
||||
"achievementMindOverMatterModalText": "Dokončil jsi kamennou, slizovou a vlněnou mazlíčkovou výpravu!",
|
||||
"achievementJustAddWater": "Jenom přidej vodu",
|
||||
"achievementJustAddWaterText": "Dokončil mazlíčkové výpravy za chobotnicí, mořským koníkem, sépií, velrybou, želvou, nahožábrým, mořským hadem a delfínem.",
|
||||
"achievementJustAddWaterText": "Dokončil výpravy za chobotnicí, mořským koníkem, sépií, velrybou, želvou, nahožábrým, mořským hadem a delfínem.",
|
||||
"achievementJustAddWaterModalText": "Dokončil/a jsi mazlíčkovou výpravu za chobotnicí, mořským koníkem, sépií, velrybou, želvou, nahožábrým, mořským hadem a delfínem!",
|
||||
"achievementBackToBasics": "Zpět k základům",
|
||||
"achievementBackToBasicsText": "Posbíral/a všechny základní mazlíčky.",
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
"commGuideList02I": "<strong>Zavádějící/diskutabilní konverzace by měla být nahlášena moderátorům</strong> označením zapojených zpráv nebo použitím <a href='http://contact.habitica.com/' target='_blank'>Formuláře pro kontaktování moderátora</a>. Pokud cítíš, že se konverzace začíná vyostřovat, je příliš emoční nebo urážlivá, přestaň se zapojovat. Místo toho příspěvky nahlaš, abychom o tom věděli. Moderátoři Ti odpoví co nejdřív to půjde. Je naší prací, aby ses cítil bezpečně. Pokud si myslíš, že je potřeba větší zásah, můžeš problém nahlásit použitím <a href='http://contact.habitica.com/' target='_blank'>Formuláře pro kontaktování moderátora</a>.",
|
||||
"commGuideList02J": "<strong>Do not spam</strong>. Spamming may include, but is not limited to: posting the same comment or query in multiple places, posting links without explanation or context, posting nonsensical messages, posting multiple promotional messages about a Guild, Party or Challenge, or posting many messages in a row. Asking for gems or a subscription in any of the chat spaces or via Private Message is also considered spamming. If people clicking on a link will result in any benefit to you, you need to disclose that in the text of your message or that will also be considered spam.<br/><br/>It is up to the mods to decide if something constitutes spam or might lead to spam, even if you don’t feel that you have been spamming. For example, advertising a Guild is acceptable once or twice, but multiple posts in one day would probably constitute spam, no matter how useful the Guild is!",
|
||||
"commGuideList02K": "<strong>Avoid posting large header text in the public chat spaces, particularly the Tavern</strong>. Much like ALL CAPS, it reads as if you were yelling, and interferes with the comfortable atmosphere.",
|
||||
"commGuideList02L": "<strong>We highly discourage the exchange of personal information -- particularly information that can be used to identify you -- in public chat spaces</strong>. Identifying information can include but is not limited to: your address, your email address, and your API token/password. This is for your safety! Staff or moderators may remove such posts at their discretion. If you are asked for personal information in a private Guild, Party, or PM, we highly recommend that you politely refuse and alert the staff and moderators by either 1) flagging the message if it is in a Party or private Guild, or 2) filling out the <a href='http://contact.habitica.com/' target='_blank'>Moderator Contact Form</a> and including screenshots.",
|
||||
"commGuideList02L": "<strong>Rozhodně by ses měl/a vyhnout výměně osobních údajů - především informací, které mohou být zneužity ke tvé identifikaci - ve veřejných chatech. </strong>. Mezi informace, které mohou vést ke tvé identifikaci patří mimo jiné: tvoje adresa, tvůj e-mail a tvůj APO token/heslo. Tohle zajistí tvé bezpečí! Zaměstnanci nebo moderátoři mohou takové příspěvky odstranit. Pokud se tě někdo zeptá na soukromé informace v neveřejné gildě, druzině nebo soukromé zprávě, doporučujeme, abys slušně odmítnul a informoval zaměstnance nebo moderátory skrze 1 ) označení zprávy pokud je to ve družině nebo v soukromé gildě, nebo skrze 2) vyplnění <a href='https://contact.habitica.com/' target='_blank'>kontaktního formuláře pro moderátora</a> a přiložením screenshotů.",
|
||||
"commGuidePara019": "<strong>In private spaces</strong>, users have more freedom to discuss whatever topics they would like, but they still may not violate the Terms and Conditions, including posting slurs or any discriminatory, violent, or threatening content. Note that, because Challenge names appear in the winner's public profile, ALL Challenge names must obey the public space guidelines, even if they appear in a private space.",
|
||||
"commGuidePara020": "<strong>Soukromé zprávy (SZ)</strong> mají pár dalších zásad. Pokud tě někdo zablokoval, nekontaktuj je jiným způsobem, aby tě odblokoval. Dále, neměl bys posílat soukromé zprávy někomu, kdo žádá o pomoc (veřejné odpovědi na žádosti o pomoc mohou pomoc i jiným v komunitě). Nakonec, neposílej nikomu soukromé zprávy, ve kterých prosíš o drahokamy nebo předplatné, jelikož by to mohlo být považováno za spam.",
|
||||
"commGuidePara020A": "<strong>If you see a post that you believe is in violation of the public space guidelines outlined above, or if you see a post that concerns you or makes you uncomfortable, you can bring it to the attention of Moderators and Staff by clicking the flag icon to report it</strong>. A Staff member or Moderator will respond to the situation as soon as possible. Please note that intentionally reporting innocent posts is an infraction of these Guidelines (see below in “Infractions”). PMs cannot be flagged at this time, so if you need to report a PM, please contact the Mods via the form on the “Contact Us” page, which you can also access via the help menu by clicking “<a href='http://contact.habitica.com/' target='_blank'>Contact the Moderation Team</a>.” You may want to do this if there are multiple problematic posts by the same person in different Guilds, or if the situation requires some explanation. You may contact us in your native language if that is easier for you: we may have to use Google Translate, but we want you to feel comfortable about contacting us if you have a problem.",
|
||||
|
||||
@@ -1812,5 +1812,8 @@
|
||||
"weaponSpecialSummer2019HealerText": "Bublinová hůlka",
|
||||
"armorSpecialSpring2019MageText": "Jantarová róba",
|
||||
"armorSpecialSpring2019RogueText": "Mrakové brnění",
|
||||
"weaponSpecialWinter2020RogueText": "Tyč lucerny"
|
||||
"weaponSpecialWinter2020RogueText": "Tyč lucerny",
|
||||
"weaponSpecialFall2019RogueNotes": "Ať už dirigujete orchestr nebo zpíváte árii, toto nápomocné zařízení uvolňuje vaše ruce pro dramatická gesta! Zvyšuje sílu o <%= str %>. Limitovaná edice podzimní výzbroj 2019.",
|
||||
"weaponSpecialSummer2019HealerNotes": "Bubliny z hůlky zachycují léčivou energii a prastarou magii oceánů. Zvyšuje inteligenci o <%= int %>. Limitovaná Edice Letní výzbroj 2019.",
|
||||
"weaponSpecialSummer2019MageNotes": "ovoce tvých snah, poprvé sklizen, tento malý poklad posiluje a inspiruje. Zvyšuje inteligenci o <%= int %>. Limitovaná edice letní výzbroj 2019."
|
||||
}
|
||||
|
||||
@@ -137,10 +137,10 @@
|
||||
"PMPlaceholderTitleRevoked": "Vaše oprávnění k chatu byla zrušena",
|
||||
"PMPlaceholderDescriptionRevoked": "You are not able to send private messages because your chat privileges have been revoked. If you have questions or concerns about this, please email <a href=\"mailto:admin@habitica.com\">admin@habitica.com</a> to discuss it with the staff.",
|
||||
"PMReceive": "Přijmout soukromé zprávy",
|
||||
"PMEnabledOptPopoverText": "Private Messages are enabled. Users can contact you via your profile.",
|
||||
"PMDisabledOptPopoverText": "Private Messages are disabled. Enable this option to allow users to contact you via your profile.",
|
||||
"PMDisabledCaptionTitle": "Private Messages are disabled",
|
||||
"PMDisabledCaptionText": "You can still send messages, but no one can send them to you.",
|
||||
"PMEnabledOptPopoverText": "Soukromé zprávy jsou aktivovány. Uživatelé tě mohou kontaktovat přes tvůj profil.",
|
||||
"PMDisabledOptPopoverText": "Soukromé zprávy jsou deaktivovány. Aktivuj tuto možnost, aby tě mohli ostatní uživatelé zkontaktovat přes tvůj profil.",
|
||||
"PMDisabledCaptionTitle": "Soukromé zprávy jsou deaktivovány.",
|
||||
"PMDisabledCaptionText": "Ty můžeš zprávy stále odesílat, ale nikdo Ti nemůže zprávy posílat.",
|
||||
"block": "Blokovat",
|
||||
"unblock": "Odblokovat",
|
||||
"blockWarning": "Block - This will have no effect if the player is a moderator now or becomes a moderator in future.",
|
||||
@@ -149,32 +149,32 @@
|
||||
"messageRequired": "Je požadována zpráva.",
|
||||
"toUserIDRequired": "Je požadováno ID uživatele",
|
||||
"gemAmountRequired": "Je požadován určitý počet drahokamů",
|
||||
"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 %> měsíců předplatného! ",
|
||||
"notAuthorizedToSendMessageToThisUser": "Nemůžeš poslat zprávu tomuto uživateli, protože se rozhodli blokovat zprávy.",
|
||||
"privateMessageGiftGemsMessage": "Ahoj <%= receiverName %>, <%= senderName %> ti poslal/a <%= gemAmount %> drahokamy!",
|
||||
"privateMessageGiftSubscriptionMessage": "<%= numberOfMonths %> měsíců předplatného!",
|
||||
"cannotSendGemsToYourself": "Nemůžete sám sobě poslat drahokamy. Raději zkuste předplatné.",
|
||||
"badAmountOfGemsToSend": "Částka musí být mezi 1 a vaším současným počtem drahokamů.",
|
||||
"report": "Report",
|
||||
"abuseFlag": "Nahlaš porušení Zásad komunity",
|
||||
"abuseFlagModalHeading": "Report a Violation",
|
||||
"abuseFlagModalHeading": "Nahlaš porušení podmínek",
|
||||
"abuseFlagModalBody": "Are you sure you want to report this post? You should <strong>only</strong> report a post that violates the <%= firstLinkStart %>Community Guidelines<%= linkEnd %> and/or <%= secondLinkStart %>Terms of Service<%= linkEnd %>. Inappropriately reporting a post is a violation of the Community Guidelines and may give you an infraction.",
|
||||
"abuseFlagModalButton": "Nahlásit porušení pravidel",
|
||||
"abuseReported": "Děkujeme za nahlášení tohoto příspěvku. Moderátoři byli upozorněni.",
|
||||
"abuseAlreadyReported": "Již jsi tento příspěvek nahlásil.",
|
||||
"whyReportingPost": "Why are you reporting this post?",
|
||||
"whyReportingPostPlaceholder": "Please help our moderators by letting us know why you are reporting this post for a violation, e.g., spam, swearing, religious oaths, bigotry, slurs, adult topics, violence.",
|
||||
"optional": "Optional",
|
||||
"whyReportingPost": "Proč nahlašuješ tento příspěvek?",
|
||||
"whyReportingPostPlaceholder": "Prosím pomož našim moderatorům a vysvětli, proč ohlašuješ tento příspěvek kvůli porušení pravidel, tedy zda je to spam, sprostá slova, náboženské přísahy, netolerance, urážky, témata nevhodná pro mladistvé, násilí.",
|
||||
"optional": "Možný",
|
||||
"needsText": "Prosím, napiš zprávu.",
|
||||
"needsTextPlaceholder": "Napiš svou zprávu sem.",
|
||||
"copyMessageAsToDo": "Zkopírovat zprávu jako Úkol",
|
||||
"copyAsTodo": "Copy as To-Do",
|
||||
"messageAddedAsToDo": "Zpráva zkopírována jako Úkol.",
|
||||
"copyMessageAsToDo": "Zkopírovat zprávu jako úkol",
|
||||
"copyAsTodo": "Zkopírovat jako úkol",
|
||||
"messageAddedAsToDo": "Zpráva zkopírována jako úkol.",
|
||||
"messageWroteIn": "<%= user %> napsal v <%= group %>",
|
||||
"msgPreviewHeading": "Náhled zprávy",
|
||||
"leaderOnlyChallenges": "Pouze velitel družiny může vytvářet Výzvy",
|
||||
"sendGift": "Poslat dárek",
|
||||
"inviteFriends": "Pozvat přátele",
|
||||
"partyMembersInfo": "Your Party currently has <%= memberCount %> members and <%= invitationCount %> pending invitations. The limit of members in a Party is <%= limitMembers %>. Invitations above this limit cannot be sent.",
|
||||
"partyMembersInfo": "Tvoje družina má momentálně <%= memberCount %> členů a <%= invitationCount %> nezodpovězených pozvánek. Limit počtu členů družiny je <%= limitMembers %>. Pozvánky nad tento limit nemohou být poslány.",
|
||||
"inviteByEmail": "Pozvi přátele přes email",
|
||||
"inviteByEmailExplanation": "If a friend joins Habitica via your email, they'll automatically be invited to your Party!",
|
||||
"inviteMembersHowTo": "Invite people via a valid email or 36-digit User ID. If an email isn't registered yet, we'll invite them to join Habitica.",
|
||||
@@ -237,7 +237,7 @@
|
||||
"cannotInviteSelfToGroup": "You cannot invite yourself to a group.",
|
||||
"userAlreadyInvitedToGroup": "UserID: <%= userId %>, User \"<%= username %>\" already invited to that group.",
|
||||
"userAlreadyPendingInvitation": "UserID: <%= userId %>, User \"<%= username %>\" already pending invitation.",
|
||||
"userAlreadyInAParty": "UserID: <%= userId %>, User \"<%= username %>\" already in a party. ",
|
||||
"userAlreadyInAParty": "UserID: <%= userId %>, uživatel \"<%= username %>\" už je ve družině.",
|
||||
"userWithIDNotFound": "Uživatel s id „<%= userId %>\" nenalezen.",
|
||||
"userWithUsernameNotFound": "User with username \"<%= username %>\" not found.",
|
||||
"userHasNoLocalRegistration": "User does not have a local registration (username, email, password).",
|
||||
@@ -339,7 +339,7 @@
|
||||
"aboutToJoinCancelledGroupPlan": "You are about to join a group with a canceled plan. You will NOT receive a free subscription.",
|
||||
"cannotChangeLeaderWithActiveGroupPlan": "You can not change the leader while the group has an active plan.",
|
||||
"leaderCannotLeaveGroupWithActiveGroup": "A leader can not leave a group while the group has an active plan",
|
||||
"youHaveGroupPlan": "You have a free subscription because you are a member of a group that has a Group Plan. This will end when you are no longer in the group that has a Group Plan. Any months of extra subscription credit you have will be applied at the end of the Group Plan.",
|
||||
"youHaveGroupPlan": "Máš předplatné zadarmo, protože jsi součástí skupinového předplatného. Tvé předplatné skončí, když už nebudeš členem skupiny.",
|
||||
"cancelGroupSub": "Cancel Group Plan",
|
||||
"confirmCancelGroupPlan": "Opravdu chcete zrušit Plán Družiny? Všichni členové Družiny ztratí předplatné a výhody.",
|
||||
"canceledGroupPlan": "Plán Družiny byl zrušen",
|
||||
@@ -484,5 +484,15 @@
|
||||
"suggestedGroup": "Navrženo, protože jste v Habitica nový/á.",
|
||||
"taskClaimed": "<%= userName %> nárokoval úkol <span class=\"notification-bold\"><%= taskText %></span>.",
|
||||
"youHaveBeenAssignedTask": "<%= managerName %> vám přidělil úkol <span class=\"notification-bold\"><%= taskText %></span>.",
|
||||
"pmReported": "Děkujeme za nahlášení této zprávy."
|
||||
"pmReported": "Děkujeme za nahlášení této zprávy.",
|
||||
"newPartyPlaceholder": "Zadej jméno tvé družiny.",
|
||||
"userWithUsernameOrUserIdNotFound": "Uživatelské jméno nebo uživatelské ID nebylo nalezeno.",
|
||||
"usernameOrUserId": "Uživatelské jméno nebo uživatelské ID",
|
||||
"sendGiftToWhom": "Komu chceš poslat dárek?",
|
||||
"selectGift": "Vybrat dárek",
|
||||
"blockedToSendToThisUser": "Této osobě nemůžeš psát, protože jsi ji zablokoval.",
|
||||
"PMUnblockUserToSendMessages": "Odblokuj tuto osobu, abys jí mohl/a psát a přijímat zprávy.",
|
||||
"PMUserDoesNotReceiveMessages": "Tento uživatel už nepřijímá soukromé zprávy",
|
||||
"PMCanNotReply": "Na tuto zprávu nemůžeš odpovědět",
|
||||
"PMDisabled": "Zakaž soukromé zprávy"
|
||||
}
|
||||
|
||||
@@ -182,5 +182,7 @@
|
||||
"summer2020RainbowTroutWarriorSet": "Duhový pstruh (Válečník)",
|
||||
"spring2020LapisLazuliRogueSet": "Lapis Lazuli (Zloděj)",
|
||||
"spring2020IrisHealerSet": "Iris (Léčitel)",
|
||||
"spring2020PuddleMageSet": "Louže (Mág)"
|
||||
"spring2020PuddleMageSet": "Louže (Mág)",
|
||||
"summer2020SeaGlassHealerSet": "Mořské sklo (Léčitel)",
|
||||
"summer2020OarfishMageSet": "Hlístoun (Mág)"
|
||||
}
|
||||
|
||||
@@ -1,34 +1,33 @@
|
||||
{
|
||||
"habiticaBackSoon": "Nebojte se, Habitica bude brzy zpět!",
|
||||
"importantMaintenance": "Právě probíhá důležitá údržba. Odhadujeme, že bude trvat do 10PM Pacifického času (5:00AM UTC).",
|
||||
"maintenance": "Údržba",
|
||||
"maintenanceMoreInfo": "Chcete více informací ohledně údržby? <%= linkStart %>Podívejte se na naši stránku<%= linkEnd %>.",
|
||||
"noDamageKeepStreaks": "Neutrpíte ŽÁDNÉ poškození a neztratíte vaše série úspěšnosti!",
|
||||
"thanksForPatience": "Děkujeme za vaší trpělivost.",
|
||||
"twitterMaintenanceUpdates": "Pro nejnovější novinky sledujte náš <a href='https://twitter.com/habitica'>Twitter</a>, kde budeme přidávat více informací.",
|
||||
"veteranPetAward": "Po údržbě obdržíte Veteránského mazlíčka!",
|
||||
|
||||
"maintenanceInfoTitle": "Informace o nadcházející údržbě země Habitica",
|
||||
"maintenanceInfoWhat": "Co se děje?",
|
||||
"maintenanceInfoWhatText": "21. května bude Habitica většinu dne mimo provoz, z důvodu údržby. Během víkendu neutrpíte žádné poškození a to, ani když se včas nepřihlásíte a neodškrtnete si své denní úkoly. Budeme se snažit zprovoznit program Habitica co nejdříve, novinky ohledně údržby budeme přidávat na <a href='https://twitter.com/habitica' target='_blank'>náš Twitter účet</a>. Po údržbě obdržíte Veteránského mazlíčka!",
|
||||
"maintenanceInfoWhy": "Proč se to děje?",
|
||||
"maintenanceInfoWhyText": "Pár posledních měsíců jsme zemi Habitica v pozadí důkladně rekonstruovali. Specificky, jsme přepsali API. I když to na povrchu možná nevypadá o tolik jinak, pod povrchem je to naprosto nový svět. V budoucnosti nám toto povolí vytvořit mnohem snadněji mnoho funkcí a zlepšit celkovou hratelnost!",
|
||||
"maintenanceInfoTechDetails": "Chcete více detailů ohledně technické stránky procesu? Navštivte <a href='http://devs.habitica.com/' target='_blank'>Kovárnu, náš blog o vývoji</a>.",
|
||||
"maintenanceInfoMore": "Více informací",
|
||||
"maintenanceInfoAccountChanges": "Jaké změn, ve svém účtu, si budu moci všimnout, až bude přepis dokončen? ",
|
||||
"maintenanceInfoAccountChangesText": "Nejdříve nebudou žádné viditelné změny až na zlepšení výkonu pro funkce jako např. Výzvy. Pokuď si všimneš jakékoli změny, která tam nemá být, napiš nám email na <%= hrefTechAssistanceEmail %> a my se na to podíváme!",
|
||||
"maintenanceInfoAddFeatures": "Jaké vylepšení se přidají do země Habitica?",
|
||||
"maintenanceInfoAddFeaturesText": "Dokončení tohoto přepisu nám dovolí začít budovat vylepšený chat a Cechy, plány pro organizace a rodiny a další produktivní funkce, například Měsíčky a možnost si nahrát včerejší aktivitu! To jsou všechno funkce, které jsou jednotlivé, takže bude trvat déle je zavést,pokud bychom však nezačali s tímto přepisem, nebylo by možné je začít provádět.",
|
||||
"maintenanceInfoHowLong": "Jako dlouho bude údržba trvat?",
|
||||
"maintenanceInfoHowLongText": "Musíme převést úkoly a data pro 1.3 milióny uživatelů programu Habitica - nejednoduchý úkol! Očekáváme, že se údržba bude konat od (20:00 UTC) do (05:00 UTC). Buďte si jistí, že děláme co je v našich silách, abychom vše stihli co nejrychleji. Můžete sledovat <a href='https://twitter.com/habitica' target='_blank'>aktualizace na našem Twitteru</a>.",
|
||||
"maintenanceInfoStatsAffected": "Co se stane s mými Denními úkoly, Sériemi úspěšnosti, Bonusy a Výpravami?",
|
||||
"maintenanceInfoStatsAffectedText1": "Během víkendu neutrpíte ŽÁDNÉ poškození a neztratíte vaše série úspěšnosti, váš sen se však resetuje normálně! Denní úkoly, které jste odškrtli budou odškrtnuté, bonusy se resetují, atd. Jste-li na sběratelské výpravě, stále budete nalézat předměty, jste-li v bitvě s Bossem, stále budete působit poškození, ovšem Boss nebude působit poškození vám. (I příšery potřebují pauzu!)",
|
||||
"maintenanceInfoStatsAffectedText2": "Po dlouhém přemýšlení se náš tým rozhodl, že toto je nejférovější cesta jak si poradit se skutečností, že mnoho uživatelů nebude schopno si odškrtnout své Denní úkoly během údržby. Omlouváme se za veškeré nepříjemnosti!",
|
||||
"maintenanceInfoSeeTasks": "Co když potřebuji vidět svůj list úkolů?",
|
||||
"maintenanceInfoSeeTasksText": "Budete-li potřebovat vidět váš seznam úkolů v sobotu, abyste věděli co dělat, doporučujeme vám si ho před začátkem údržby vyfotit.",
|
||||
"maintenanceInfoRarePet": "Jakého vzácného mazlíčka dostanu?",
|
||||
"maintenanceInfoRarePetText": "Abychom vám poděkovali za vaší trpělivost během prostoje, každý dostane vzácného Veteránského mazlíčka. Pokud jste žádného nikdy předtím nedostali, obdržíte Veteránského vlka. Máte-li už Veteránského vlka, obdržíte Veteránského tygra. Máte-li už oba tyto mazlíčky, obdržíte Veteránského mazlíčka, kterého ještě nikdo nikdy neviděl! Může trvat i několik hodin, po dokončení migrace serveru, než se váš mazlíček objeví ale nebojte se, každý jednoho dostane.",
|
||||
"maintenanceInfoWho": "Kdo pracoval na tomto obrovském projektu?",
|
||||
"maintenanceInfoWhoText": "Děkujeme za optání! Bylo to vedeno naším úžasným přispěvatelem jménem paglias, s hodně pomocí od hráčů, kteří si říkají Blade, TheHollidayInn, SabreCat, Victor Pudeyev, TheUnknown, a Alys.",
|
||||
"maintenanceInfoTesting": "Tato nová verze byla také neúnavně testovaná skupinou úžasných dobrovolníků. Děkujeme vám - bez vás bychom to nezvládli."
|
||||
"habiticaBackSoon": "Nebojte se, Habitica bude brzy zpět!",
|
||||
"importantMaintenance": "Právě probíhá důležitá údržba. Odhadujeme, že bude trvat do 10PM Pacifického času (5:00AM UTC).",
|
||||
"maintenance": "Údržba",
|
||||
"maintenanceMoreInfo": "Chcete více informací ohledně údržby? <%= linkStart %>Podívejte se na naši stránku<%= linkEnd %>.",
|
||||
"noDamageKeepStreaks": "Neutrpíte ŽÁDNÉ poškození a neztratíte vaše série úspěšnosti!",
|
||||
"thanksForPatience": "Děkujeme za vaší trpělivost!",
|
||||
"twitterMaintenanceUpdates": "Pro nejnovější novinky sledujte náš <a href='https://twitter.com/habitica'>Twitter</a>, kde budeme přidávat více informací.",
|
||||
"veteranPetAward": "Po údržbě obdržíte Veteránského mazlíčka!",
|
||||
"maintenanceInfoTitle": "Informace o nadcházející údržbě země Habitica",
|
||||
"maintenanceInfoWhat": "Co se děje?",
|
||||
"maintenanceInfoWhatText": "21. května bude Habitica většinu dne mimo provoz, z důvodu údržby. Během víkendu neutrpíte žádné poškození a to, ani když se včas nepřihlásíte a neodškrtnete si své denní úkoly. Budeme se snažit zprovoznit program Habitica co nejdříve, novinky ohledně údržby budeme přidávat na <a href='https://twitter.com/habitica' target='_blank'>náš Twitter účet</a>. Po údržbě obdržíte Veteránského mazlíčka!",
|
||||
"maintenanceInfoWhy": "Proč se to děje?",
|
||||
"maintenanceInfoWhyText": "Pár posledních měsíců jsme zemi Habitica v pozadí důkladně rekonstruovali. Specificky, jsme přepsali API. I když to na povrchu možná nevypadá o tolik jinak, pod povrchem je to naprosto nový svět. V budoucnosti nám toto povolí vytvořit mnohem snadněji mnoho funkcí a zlepšit celkovou hratelnost!",
|
||||
"maintenanceInfoTechDetails": "Chcete více detailů ohledně technické stránky procesu? Navštivte <a href='http://devs.habitica.com/' target='_blank'>Kovárnu, náš blog o vývoji</a>.",
|
||||
"maintenanceInfoMore": "Více informací",
|
||||
"maintenanceInfoAccountChanges": "Jakých změn ve svém účtu si budu moci všimnout, až bude přepis dokončen?",
|
||||
"maintenanceInfoAccountChangesText": "Nejdříve nebudou žádné viditelné změny až na zlepšení výkonu pro funkce jako např. Výzvy. Pokuď si všimneš jakékoli změny, která tam nemá být, napiš nám email na <%= hrefTechAssistanceEmail %> a my se na to podíváme!",
|
||||
"maintenanceInfoAddFeatures": "Jaké vylepšení se přidají do země Habitica?",
|
||||
"maintenanceInfoAddFeaturesText": "Dokončení tohoto přepisu nám dovolí začít budovat vylepšený chat a Cechy, plány pro organizace a rodiny a další produktivní funkce, například Měsíčky a možnost si nahrát včerejší aktivitu! To jsou všechno funkce, které jsou jednotlivé, takže bude trvat déle je zavést,pokud bychom však nezačali s tímto přepisem, nebylo by možné je začít provádět.",
|
||||
"maintenanceInfoHowLong": "Jako dlouho bude údržba trvat?",
|
||||
"maintenanceInfoHowLongText": "Musíme převést úkoly a data pro 1.3 milióny uživatelů programu Habitica - nejednoduchý úkol! Očekáváme, že se údržba bude konat od (20:00 UTC) do (05:00 UTC). Buďte si jistí, že děláme co je v našich silách, abychom vše stihli co nejrychleji. Můžete sledovat <a href='https://twitter.com/habitica' target='_blank'>aktualizace na našem Twitteru</a>.",
|
||||
"maintenanceInfoStatsAffected": "Co se stane s mými Denními úkoly, Sériemi úspěšnosti, Bonusy a Výpravami?",
|
||||
"maintenanceInfoStatsAffectedText1": "Během víkendu neutrpíte ŽÁDNÉ poškození a neztratíte vaše série úspěšnosti, váš sen se však resetuje normálně! Denní úkoly, které jste odškrtli budou odškrtnuté, bonusy se resetují, atd. Jste-li na sběratelské výpravě, stále budete nalézat předměty, jste-li v bitvě s Bossem, stále budete působit poškození, ovšem Boss nebude působit poškození vám. (I příšery potřebují pauzu!)",
|
||||
"maintenanceInfoStatsAffectedText2": "Po dlouhém přemýšlení se náš tým rozhodl, že toto je nejférovější cesta jak si poradit se skutečností, že mnoho uživatelů nebude schopno si odškrtnout své Denní úkoly během údržby. Omlouváme se za veškeré nepříjemnosti!",
|
||||
"maintenanceInfoSeeTasks": "Co když potřebuji vidět svůj list úkolů?",
|
||||
"maintenanceInfoSeeTasksText": "Budete-li potřebovat vidět váš seznam úkolů v sobotu, abyste věděli co dělat, doporučujeme vám si ho před začátkem údržby vyfotit.",
|
||||
"maintenanceInfoRarePet": "Jakého vzácného mazlíčka dostanu?",
|
||||
"maintenanceInfoRarePetText": "Abychom vám poděkovali za vaší trpělivost během prostoje, každý dostane vzácného Veteránského mazlíčka. Pokud jste žádného nikdy předtím nedostali, obdržíte Veteránského vlka. Máte-li už Veteránského vlka, obdržíte Veteránského tygra. Máte-li už oba tyto mazlíčky, obdržíte Veteránského mazlíčka, kterého ještě nikdo nikdy neviděl! Může trvat i několik hodin, po dokončení migrace serveru, než se váš mazlíček objeví ale nebojte se, každý jednoho dostane.",
|
||||
"maintenanceInfoWho": "Kdo pracoval na tomto obrovském projektu?",
|
||||
"maintenanceInfoWhoText": "Děkujeme za optání! Bylo to vedeno naším úžasným přispěvatelem jménem paglias, s hodně pomocí od hráčů, kteří si říkají Blade, TheHollidayInn, SabreCat, Victor Pudeyev, TheUnknown, a Alys.",
|
||||
"maintenanceInfoTesting": "Tato nová verze byla také neúnavně testovaná skupinou úžasných dobrovolníků. Děkujeme vám - bez vás bychom to nezvládli."
|
||||
}
|
||||
|
||||
@@ -5,18 +5,18 @@
|
||||
"welcomeTo": "Vítej v",
|
||||
"welcomeBack": "Vítej zpět!",
|
||||
"justin": "Justin",
|
||||
"justinIntroMessage1": "Hello there! You must be new here. My name is <strong>Justin</strong>, and I'll be your guide in Habitica.",
|
||||
"justinIntroMessage1": "Ahoj, ty musíš být nový/á. Jmenuji se <strong>Justin</strong>, a budu tě provázet po světě Habitica.",
|
||||
"justinIntroMessage2": "Pro začátek budeš potřebovat vytvořit tvojí postavu.",
|
||||
"justinIntroMessage3": "Skvěle! Teď - na čem by jsi rád pracoval na tvé výpravě?",
|
||||
"justinIntroMessageUsername": "Before we begin, let’s figure out what to call you. Below you’ll find a display name and username I’ve generated for you. After you’ve picked a display name and username, we’ll get started by creating an avatar!",
|
||||
"justinIntroMessageAppearance": "So how would you like to look? Don’t worry, you can change this later.",
|
||||
"justinIntroMessageUsername": "Než začneme, musíme vymyslet, jak Ti budeme říkat. Dole uvidíš veřejné a uživatelské jméno, které jsem pro tebe vygeneroval. Poté, co si vybereš své veřejné a uživatelské jméno, začneme s tvorbou avatara!",
|
||||
"justinIntroMessageAppearance": "Jak bys chtěl/a vypadat? Neboj se, můžeš svůj vzhled později změnit.",
|
||||
"introTour": "A jsme tu! Vyplnil jsem ti pár úkolů na základě tvých zájmů, takže můžeš ihned začít. Klikni na úkol pro jeho úpravu. nebo přidej nový úkol, který by odpovídal tvé rutině!",
|
||||
"prev": "Předch",
|
||||
"next": "Další",
|
||||
"randomize": "Znáhodnit",
|
||||
"mattBoch": "Matt Boch",
|
||||
"mattShall": "<%= name %>, cítíš se na projížďku? Jakmile dostatečně nakrmíš mazlíčka, objeví se tady a budeš se na něm moci projet. Klikni na zvíře, které si chceš osedlat!",
|
||||
"mattBochText1": "Vítej ve stáji! Jsem Matt, pán zvířat. Od levelu 3 můžeš nalézt vejce a lektvary, kterými z nich můžeš vylíhnout mazlíčky. Když si na trhu vylíhneš mazlíčka, objeví se tady! Klikni na obrázek mazlíčka, abys ho přidal ke svému avataru. Krm je jídlem, které můžeš od levelu 3 naleznout a vyrostou ti v otužilá zvířata.",
|
||||
"mattBochText1": "Vítej ve stáji! Jsem Matt, pán zvířat. Pokaždé, když dokončíš úkol, můžeš nalézt vejce a lektvary, kterými z nich můžeš vylíhnout mazlíčky. Když se vylíhne mazlíček, objeví se tady! Klikni na obrázek mazlíčka, abys ho přidal ke svému avataru. Krm je jídlem, které najdeš a vyrostou ti v otužilá zvířata.",
|
||||
"welcomeToTavern": "Vítej v Krčmě!",
|
||||
"sleepDescription": "Potřebuješ pauzu? Ubytuj se v Danielově krčmě pro pauznutí některých z těžších herních mechanismů země Habitica:",
|
||||
"sleepBullet1": "Promeškané denní úkoly tě nezraní",
|
||||
@@ -43,7 +43,7 @@
|
||||
"displayPotionForGold": "Chceš prodat <strong><%= itemType %> lektvar</strong>?",
|
||||
"sellForGold": "Prodat za <%= gold %> zlaťáky",
|
||||
"howManyToSell": "Kolik by jsi chtěl prodat?",
|
||||
"yourBalance": "Tvá bilance",
|
||||
"yourBalance": "Tvá bilance:",
|
||||
"sell": "Prodat",
|
||||
"buyNow": "Koupit teď",
|
||||
"sortByNumber": "Číslo",
|
||||
@@ -90,7 +90,7 @@
|
||||
"pathRequired": "Je požadována cesta k vláknu",
|
||||
"unlocked": "Předměty byly odemčeny",
|
||||
"alreadyUnlocked": "Celý set je již odemčen.",
|
||||
"alreadyUnlockedPart": "Celý set je již částečně odemčen.",
|
||||
"alreadyUnlockedPart": "Celý set je již částečně odemčen. Je levnější koupit zbývající předměty samostatně.",
|
||||
"invalidQuantity": "Množství k nákupu musí být kladné celé číslo.",
|
||||
"USD": "(USD)",
|
||||
"newStuff": "Nové věci od Bailey",
|
||||
@@ -106,11 +106,11 @@
|
||||
"card": "Platební kartou",
|
||||
"amazonInstructions": "Klikni pro zaplacení přes Amazon platby",
|
||||
"paymentMethods": "Platební metody",
|
||||
"paymentSuccessful": "Your payment was successful!",
|
||||
"paymentSuccessful": "Tvá platba proběhla úspěšně!",
|
||||
"paymentYouReceived": "Obdržel jsi:",
|
||||
"paymentYouSentGems": "You sent <strong><%= name %></strong>:",
|
||||
"paymentYouSentSubscription": "You sent <strong><%= name %></strong> a <%= months %>-months Habitica subscription.",
|
||||
"paymentSubBilling": "Your subscription will be billed <strong>$<%= amount %></strong> every <strong><%= months %> months</strong>.",
|
||||
"paymentYouSentGems": "Poslal/a jsi <strong><%= name %></strong>:",
|
||||
"paymentYouSentSubscription": "Poslal/a jsi <strong><%= name %></strong> předplatné na <%= months %>-měsíce/ů v Habitica.",
|
||||
"paymentSubBilling": "Tvoje předplatné ve výši <strong>$<%= amount %></strong> bude účtovano každé/ých <strong><%= months %> měsíce/ů </strong>.",
|
||||
"success": "Úspěch!",
|
||||
"classGear": "Vybavení pro tvé povolání",
|
||||
"classGearText": "Gratuluji k vybrání povolání! Přidal jsem ti základní zbraň do tvého inventáře. Podívej se dolů a vybav se!",
|
||||
@@ -118,11 +118,11 @@
|
||||
"autoAllocate": "Připisovat automaticky",
|
||||
"autoAllocateText": "Pokud je vybrané 'Automatické přidělení', tvůj avatar dostává dovednostní body automaticky na základě nastavení tvých úkolů, které můžeš najít v: <strong>ÚKOL > Upravit > Pokročilé nastavení > Přidělení dovednostních bodů</strong>. Například, pokud často chodíš do posilovny a tvůj denní úkol 'Posilovna' je nastavený na 'Sílu', bod dovednosti se ti automaticky přidělí k Síle.",
|
||||
"spells": "Dovednosti",
|
||||
"spellsText": "Nyní můžeš odemknout nové schopnosti tvého povolání. První uvidíš na 11 úrovni. Tvá mana se obnovuje po 10 bodech každý den, plus 1 bod za splněný <a target='_blank' href='http://habitica.fandom.com/wiki/Todos'>Úkol</a>.",
|
||||
"spellsText": "Nyní můžeš odemknout nové schopnosti tvé třídy. První uvidíš na 11. úrovni. Tvá mana se obnovuje po 10 bodech každý den, plus 1 bod za splněný <a target='_blank' href='http://habitica.fandom.com/wiki/Todos'>úkol</a>.",
|
||||
"skillsTitle": "Dovednosti",
|
||||
"toDo": "úkolu",
|
||||
"toDo": "úkol",
|
||||
"moreClass": "Pro více informací o systému povolání, přejdi na <a href='http://habitica.fandom.com/wiki/Class_System' target='_blank'>Wikia</a>.",
|
||||
"tourWelcome": "Vítej v zemi Habitica! Tohle tvůj Úkolníček. Odškrtni si úkol abys mohl pokračovat!",
|
||||
"tourWelcome": "Vítej v zemi Habitica! Tohle tvůj úkolníček. Odškrtni si úkol, abys mohl pokračovat!",
|
||||
"tourExp": "Skvělá práce! Odškrtnutí úkolu ti přidává Zkušenost a Zlaťáky!",
|
||||
"tourDailies": "Tento sloupec je pro denní úkoly. Přidej sem úkol, který bys měl plnit každý den! <strong>Příklady denních úkolů</strong>: <strong>Ustlat postel</strong>, <strong>Použít dentální nit</strong>, <strong>Zkontrolovat pracovní e-mail</strong>",
|
||||
"tourCron": "Úžasné! Tvé Denní úkoly se budou resetovat každý den.",
|
||||
@@ -138,9 +138,9 @@
|
||||
"tourPartyPage": "Tvá družina ti pomůže dodržovat cíle. Pozvi své přátele a odemkni Svitek výpravy!",
|
||||
"tourGuildsPage": "Cechy jsou chatovací skupiny vytvořeny hráči pro hráče sdílející určité společné zájmy. Procházej list cechů a pokud se ti nějaký zalíbí, přidej se k němu. Nezapomeň se také případně podivát na populární cech Habitica Help: Ask a Question, kde se jakýkoliv hráč může zeptat na otázky ohledně Habitiky!",
|
||||
"tourChallengesPage": "Výzvy jsou seznamy tématických úkolů vytvořené uživateli! Přidání se k výzvě ti přidá úkoly do tvých listů. Soutěž proti ostatním uživatelům a vyhraj cenné drahokamy!",
|
||||
"tourMarketPage": "Když dosáhneš úrovně 3, začneš po splnění úkolů náhodně nacházet vejce a lektvary. Budou se objevovat tady - použij je k vylíhnutí mazlíčků! Můžeš si je také koupi na Trhu.",
|
||||
"tourMarketPage": "Po každém splnění úkolu máš šanci náhodně najít vejce, lektvar, nebo kus jídla pro tvé mazlíčky. Můžeš si je také koupit na trhu.",
|
||||
"tourHallPage": "Vítej v Síni hrdinů, kde jsou oslavování open-source přispěvatelé programu Habitica. Vysloužili si Drahokamy, exkluzivní vybavení a prestižní tituly ať už za kódování, obrázky, hudbu, psaní, nebo za pomoc. Také můžeš programu Habitica přispět!",
|
||||
"tourPetsPage": "Tohle je stáj. Po dosažení levelu 3 začneš při plnění úkolů získávat vejce a líhnoucí lektvary. Když si na trhu vylíhneš mazlíčka, objeví se tady! Klikni na obrázek mazlíčka, abys ho přidal ke svému avataru. Krm je jídlem, které budeš nacházet a oni ti vyrostou v silná zvířata.",
|
||||
"tourPetsPage": "Vítej ve stáji. Při splnění úkolu máš šanci získat vejce a líhnoucí lektvar k vylíhnutí mazlíčků. Když se vylíhne mazlíček, objeví se tady! Klikni na obrázek mazlíčka, abys ho přidal ke svému avataru. Krm je jídlem, které budeš nacházet a oni ti vyrostou v silná zvířata.",
|
||||
"tourMountsPage": "Jak nakrmíš dostatečně mazlíčka jídlem, aby se změnil na jezdecké zvíře, objeví se tady. Klikni na jezdecké zvíře k osedlání!",
|
||||
"tourEquipmentPage": "Tady se ti ukládá vybavení! Tvá Bojová zbroj ovlivňuje tvé statistiky. Pokud chceš, aby se ti zobrazovalo jiné vybavení na tvém avataru aniž by se ti statistiky nějak ovlivnily, klikni na \"Povolit kostým.\"",
|
||||
"equipmentAlreadyOwned": "Tuto část vybavení již vlastníte",
|
||||
@@ -149,7 +149,7 @@
|
||||
"tourSplendid": "Velkolepé!",
|
||||
"tourNifty": "Prima!",
|
||||
"tourAvatarProceed": "Ukaž mi moje úkoly!",
|
||||
"tourToDosBrief": "<strong>Úkolníček</strong><ul><li>Splň úkoly v Úkolníčku a získej za to Zlaťáky a Zkušenost!</li><li>Nesplněné úkoly v Úkolníčku ti nikdy neublíží.</li></ul>",
|
||||
"tourToDosBrief": "<strong>Úkolníček</strong><ul><li>Splň úkoly v úkolníčku a získej za to zlaťáky a zkušenosti!</li><li>Nesplněné úkoly v úkolníčku ti nikdy neublíží.</li></ul>",
|
||||
"tourDailiesBrief": "<strong>Denní úkoly</strong><ul><li>Denní úkoly se opakují každý den.</li><li>Pokud je nesplníš, uberou ti zdraví.</li></ul>",
|
||||
"tourDailiesProceed": "Budu opatrný!",
|
||||
"tourHabitsBrief": "<strong>Dobré zvyky a zlozvyky</strong><ul><li>Za splnění dobrých zvyků získáš Zlaťáky a Zkušenost.</li><li>Zlozvyky ti ublíží.</li></ul>",
|
||||
@@ -170,5 +170,7 @@
|
||||
"limitedOffer": "Dostupné do <%= date %>",
|
||||
"paymentCanceledDisputes": "Na váš e-mail jsme zaslali potvrzení o zrušení. Pokud e-mail nevidíte, kontaktujte nás, abychom předešli budoucím sporům o fakturaci.",
|
||||
"paymentAutoRenew": "Toto předplatné se automaticky obnoví, dokud nebude zrušeno. Pokud potřebujete předplatné zrušit, můžete tak učinit z nastavení.",
|
||||
"cannotUnpinItem": "Tuto položku nelze odepnout."
|
||||
"cannotUnpinItem": "Tuto položku nelze odepnout.",
|
||||
"paymentSubBillingWithMethod": "Tvé předplatné <strong>$<%= amount %> bude účtováno </strong> každé/CZ <strong><%= months %> měsíce/ů </strong> skrze <strong><%= paymentMethod %></strong>.",
|
||||
"invalidUnlockSet": "Tento set předmětů je prošlý a nemůže být odemčen."
|
||||
}
|
||||
|
||||
@@ -44,11 +44,11 @@
|
||||
"hatchingPotion": "líhnoucí lektvar",
|
||||
"noHatchingPotions": "Nemáš žádné líhnoucí lektvary.",
|
||||
"inventoryText": "Po kliknutí na vejce se zeleně zvýrazní použitelné lektvary. Poté klikni na jeden z nich pro vylíhnutí mazlíčka. Pokud nejsou žádné lektvary zvýrazněny, klikni na vejce znovu pro zrušení jeho výběru a místo toho klikni nejprve na lektvar, aby se označila použitelná vejce. Také můžeš nechtěné nalezené předměty prodat obchodníku Alexanderovi.",
|
||||
"haveHatchablePet": "Máš <%= potion %> líhnoucí lektvar a <%= egg %> vajíček k vylíhnutí tohoto mazlíčka! <b>Klikni</b> na packu pro vylíhnutí.",
|
||||
"haveHatchablePet": "Máš <%= potion %> líhnoucí lektvar a <%= egg %> vajíčko k vylíhnutí tohoto mazlíčka! <b>Klikni</b> pro vylíhnutí!",
|
||||
"quickInventory": "Rychlý Inventář",
|
||||
"foodText": "jídlo",
|
||||
"food": "Jídlo a sedla",
|
||||
"noFoodAvailable": "Nemáš žádné Jídlo.",
|
||||
"noFoodAvailable": "Nemáš žádné jídlo.",
|
||||
"noSaddlesAvailable": "Nemáš žádná Sedla.",
|
||||
"noFood": "Nemáš žádné jídlo ani žádná sedla.",
|
||||
"dropsExplanation": "Tyto předměty můžeš získat rychleji, když si je koupíš za Drahokamy, a nemusíš tak čekat, až je najdeš po splnění úkolu. <a href=\"http://habitica.fandom.com/wiki/Drops\">Zjisti více o nalézání předmětů.</a>",
|
||||
@@ -78,7 +78,7 @@
|
||||
"hatchAPot": "Chceš vylíhnout nového <%= potion %><%= egg %>?",
|
||||
"hatchedPet": "Vylíhnul jsi nového <%= potion %><%= egg %>!",
|
||||
"hatchedPetGeneric": "Vylíhl se ti nový mazlíček!",
|
||||
"hatchedPetHowToUse": "Visit the [Stable](<%= stableUrl %>) to feed and equip your newest pet!",
|
||||
"hatchedPetHowToUse": "Navštiv [stáje](<%= stableUrl %>), abys mohl svého nového mazlíčka nakrmit a vybavit!",
|
||||
"displayNow": "Zobrazit hned",
|
||||
"displayLater": "Zobrazit později",
|
||||
"petNotOwned": "Nevlastníte tohoto mazlíčka.",
|
||||
@@ -123,7 +123,7 @@
|
||||
"foodWikiText": "Co můj mazlíček rád jí?",
|
||||
"foodWikiUrl": "http://habitica.fandom.com/wiki/Food_Preferences",
|
||||
"welcomeStable": "Vítej ve Stájích!",
|
||||
"welcomeStableText": "Já jsem Matt, Mistr Zvířat. Počínaje 3 úrovní můžeš líhnout mazlíčky pomocí nalezených líhnoucích lektvarů! Když vylíhneš nějakého mazlíčka ve svém inventáři, objeví se zde! Kliknutím na jeho ikonu ho zobrazíš u své postavy. Krm ho přímo zde potravou, kterou začneš nacházet od 3 úrovně, a jednou na něj budeš moci nasednout.",
|
||||
"welcomeStableText": "Vítej ve stáji! Jsem Matt, pán zvířat. Pokaždé, když dokončíš úkol, můžeš nalézt vejce a lektvary, kterými z nich můžeš vylíhnout mazlíčky. Když se vylíhne mazlíček, objeví se tady! Klikni na obrázek mazlíčka, abys ho přidal ke svému avataru. Krm je jídlem, které najdeš a vyrostou ti v otužilá zvířata.",
|
||||
"petLikeToEat": "Co můj mazlíček rád jí?",
|
||||
"petLikeToEatText": "Mazlíčci rostou bez ohledu na to, čím je krmíš, ale porostou rychleji, když jim dáš jejich oblíbené krmení. Experimentuj a odhal ho sám, nebo se podívej na odpovědi tady: <br/> <a href=\"http://habitica.fandom.com/wiki/Food_Preferences\" target=\"_blank\">http://habitica.fandom.com/wiki/Food_Preferences</a>",
|
||||
"filterByStandard": "Běžný",
|
||||
@@ -145,5 +145,8 @@
|
||||
"notEnoughPetsMounts": "Nenasbíral jsi dostatek Mazlíčků a Osedlaných mazlíčků",
|
||||
"filterByWacky": "Šílené",
|
||||
"gryphatrice": "Gryfatrice",
|
||||
"wackyPets": "Šílená zvířátka"
|
||||
"wackyPets": "Šílená zvířátka",
|
||||
"invalidAmount": "Neplatný počet jídla,je vyžadováno pozitivní celé číslo",
|
||||
"tooMuchFood": "Snažíš se dát svému zvířeti moc jídla, akce byla zrušena",
|
||||
"notEnoughFood": "Nemáš dost jídla"
|
||||
}
|
||||
|
||||
@@ -213,5 +213,6 @@
|
||||
"mentioning": "Zmínka",
|
||||
"buyGemsGoldCapBase": "Drahokamy zastropovány na <%= amount %>",
|
||||
"chatExtensionDesc": "Rozšíření Chat pro Habitica přidává intuitivní chatovací okno do habitica.com. To umožňuje uživatelům povídat si v Taverně, s jejich družinou a každém cechu, ve které jsou členy.",
|
||||
"chatExtension": "<a target='blank' href='https://chrome.google.com/webstore/detail/habitrpg-chat-client/hidkdfgonpoaiannijofifhjidbnilbb'>Rozšíření Chrome Chat</a> a <a target='blank' href='https://addons.mozilla.org/en-US/firefox/addon/habitica-chat-client-2/'>Rozšíření Firefox Chat</a>"
|
||||
"chatExtension": "<a target='blank' href='https://chrome.google.com/webstore/detail/habitrpg-chat-client/hidkdfgonpoaiannijofifhjidbnilbb'>Rozšíření Chrome Chat</a> a <a target='blank' href='https://addons.mozilla.org/en-US/firefox/addon/habitica-chat-client-2/'>Rozšíření Firefox Chat</a>",
|
||||
"displaynameIssueNewline": "Zobrazovaná jména nesmí obsahovat zpětné lomítko následované písmenem N."
|
||||
}
|
||||
|
||||
@@ -81,5 +81,8 @@
|
||||
"yourProgress": "Dein Fortschritt",
|
||||
"achievementBareNecessitiesModalText": "Du hast die Affen-, Faultier- und Bäumlinghaustier-Quests erfüllt!",
|
||||
"achievementBareNecessitiesText": "Hat die Affen-, Faultier- und Bäumlinghaustier-Quests erfüllt.",
|
||||
"achievementBareNecessities": "Mit Bloßen Händen"
|
||||
"achievementBareNecessities": "Mit Bloßen Händen",
|
||||
"achievementFreshwaterFriendsModalText": "Du hast die Axolotl-, Frosch- und Nilpferdhaustier-Quests erfüllt!",
|
||||
"achievementFreshwaterFriendsText": "Hat die Axolotl-, Frosch- und Nilpferdhaustier-Quests erfüllt.",
|
||||
"achievementFreshwaterFriends": "Frischwasser-Freunde"
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
"commGuideList02L": "<strong>Wir raten Dir dringend davon ab, persönliche Informationen - besonders solche, mit denen Du identifiziert werden könntest - in öffentlichen Chats zu teilen.</strong> Zu den identifizierenden Informationen gehören unter anderem: Deine Adresse, Deine E-Mail-Adresse und Dein API-Token/Passwort. Dies dient nur Deiner Sicherheit! Mitarbeiter oder Moderatoren werden solche Beiträge nach eigenem Ermessen entfernen. Wenn Du nach persönlichen Informationen in einer privaten Gilde, Party oder per PN gefragt wirst, empfehlen wir dringend, dass Du höflich ablehnst und Mitarbeiter und Moderatoren informierst, indem Du entweder 1) den Beitrag über das Fähnchen meldest, wenn er in einer Party oder privaten Gilde ist, oder 2) das <a href='https://contact.habitica.com/' target='_blank'>Moderator-Kontaktformular</a> ausfüllst und einen Screenshot anhängst.",
|
||||
"commGuidePara019": "<strong>An privaten Orten</strong> haben Benutzer die Freiheit, alle möglichen Themen zu besprechen, solange diese nicht den AGB widersprechen. Dies umfasst das Posten von diskriminierenden, gewalttätigen oder einschüchternden Inhalten. Beachte, dass Herausforderungsnamen im öffentlichen Profil des Gewinners angezeigt werden, daher müssen ALLE Herausforderungsnamen den Community-Richtlinien für öffentliche Orte entsprechen, auch wenn sie an privaten Orten genutzt werden.",
|
||||
"commGuidePara020": "<strong>Für private Nachrichten (PNs)</strong> gibt es einige zusätzliche Richtlinien. Falls Dich jemand geblockt hat, kontaktiere ihn nicht über andere Wege, um ihn oder sie zu bitten Dich nicht mehr zu blocken. Außerdem solltest Du keine PNs schicken, wenn Du Hilfe mit der Seite, also \"Support\" brauchst (allgemein zugängliche Antworten auf diese Fragen in der Taverne oder im Forum kommen der Gemeinschaft zugute). Schicke auch bitte keine PNs, in denen Du um Edelsteine oder ein Abonnement bettelst, da dies als Spam gewertet werden kann.",
|
||||
"commGuidePara020A": "<strong>Siehst Du einen Beitrag, der Dich beunruhigt oder von dem Du glaubst, er verletze die oben zusammengefassten Community-Richtlinien für öffentliche Orte, kannst Du ihn bei Moderatoren und Mitarbeitern melden, indem Du auf das Fähnchen klickst</strong>. Ein Mitarbeiter oder Moderator wird sich dieser Meldung sobald wie möglich annehmen. Bitte beachte, dass das vorsätzliche Melden harmloser Beiträge eine Verletzung dieser Richtlinien darstellt (siehe unten unter “Regelverletzung”). PNs können derzeit nicht über das Fähnchen gemeldet werden. Um eine PN zu melden, benutze bitte das <a href='https://contact.habitica.com/' target='_blank'>Moderatoren-Kontaktformular</a> auf der “Kontakt”-Seite oder über “Kontaktiere das Moderatoren-Team” im Hilfe-Menü. Du kannst dies tun, wenn es mehrere problematische Beiträge derselben Person in verschiedenen Gilden gibt, oder wenn die Situation einer Erklärung bedarf. Du kannst uns in Deiner Muttersprache kontaktieren, wenn das für Dich einfacher ist: Wir müssen vielleicht Google Translate verwenden, aber wir möchten, dass Du Dich wohl fühlst, wenn Du uns ein Problem mitteilst.",
|
||||
"commGuidePara020A": "<strong>Siehst Du einen Beitrag (oder eine persönliche Nachricht), der Dich beunruhigt oder von dem Du glaubst, er verletze die oben zusammengefassten Community-Richtlinien für öffentliche Orte, kannst Du ihn bei Moderatoren und Mitarbeitern melden, indem Du auf das Fähnchen klickst</strong>. Ein Mitarbeiter oder Moderator wird sich dieser Meldung sobald wie möglich annehmen. Bitte beachte, dass das vorsätzliche Melden harmloser Beiträge eine Verletzung dieser Richtlinien darstellt (siehe unten unter “Regelverletzung”). Du kannst die Moderatoren auch kontaktieren über das <a href='https://contact.habitica.com/' target='_blank'>Moderatoren-Kontaktformular</a> auf der “Kontakt”-Seite oder über “Kontaktiere das Moderatoren-Team” im Hilfe-Menü. Du kannst dies tun, wenn es mehrere problematische Beiträge derselben Person in verschiedenen Gilden gibt, oder wenn die Situation einer Erklärung bedarf. Du kannst uns in Deiner Muttersprache kontaktieren, wenn das für Dich einfacher ist: Wir müssen vielleicht Google Translate verwenden, aber wir möchten, dass Du Dich wohl fühlst, wenn Du uns ein Problem mitteilst.",
|
||||
"commGuidePara021": "Manche öffentliche Orte in Habitica haben außerdem noch weitere Regeln.",
|
||||
"commGuideHeadingTavern": "Die Taverne",
|
||||
"commGuidePara022": "Die Taverne ist der Haupttreffpunkt der Habiticaner. Daniel der Gastwirt hält das Haus blitzblank und Lemoness zaubert Dir gerne eine Limonade herbei, während Du Dich setzt und mit den anderen unterhältst. Und denk dran…",
|
||||
|
||||
@@ -2028,7 +2028,7 @@
|
||||
"weaponArmoireHappyBannerText": "Herzliche Gösch",
|
||||
"weaponMystery202002Notes": "Ein Accessoire, das Dir einen Hauch Rätselhaftigkeit und Romantik verleiht. Sonnenschutz als Bonus! Gewährt keinen Attributbonus. Abonnentengegenstand, Februar 2020.",
|
||||
"weaponMystery202002Text": "\"Stilvolles Schätzchen\"-Sonnenschirm",
|
||||
"bodyMystery202002Notes": "Zu tragen, wenn Dein Herz warm ist, aber die Brisen des Februars böig sind. Gewährt keinen Attributbonus. Abonnentengegenstand Februar 2020.",
|
||||
"bodyMystery202002Notes": "Zu tragen, wenn Dein Herz warm ist, aber die Brisen des Februars böig sind. Gewährt keinen Attributbonus. Abonnentengegenstand, Februar 2020.",
|
||||
"bodyMystery202002Text": "\"Stilvolles Schätzchen\"-Schal",
|
||||
"armorSpecialBirthday2020Notes": "Herzlichen Glückwunsch zum Geburtstag, Habitica! Trage diese Frevelhafte Fetenrobe, um diesen wundervollen Tag zu zelebrieren. Gewährt keinen Attributbonus.",
|
||||
"armorSpecialBirthday2020Text": "Frevelhafte Fetenrobe",
|
||||
@@ -2090,8 +2090,8 @@
|
||||
"shieldArmoireHobbyHorseNotes": "Reite auf Deinem stattlichen Steckenpferd zu Deinen verdienten Belohnungen! Erhöht Wahrnehmung und Ausdauer um je <%= attrs %>. Verzauberter Schrank: Papierritter-Set (Gegenstand 2 von 3).",
|
||||
"shieldSpecialSpring2020HealerNotes": "Wehre die muffigen, alten To-Dos mit dem süßen Duft dieses Schilds ab. Erhöht Ausdauer um <%= con %>. Limitierte Ausgabe 2020 Frühlingsausrüstung.",
|
||||
"shieldSpecialSpring2020WarriorNotes": "Lass Dich nicht von den sanften Farben täuschen. Dieser Schild hält Dir den Rücken frei! Erhöht Ausdauer um <%= con %>. Limitierte Ausgabe 2020 Frühlingsausrüstung.",
|
||||
"backMystery202005Notes": "Obschon sie ganz leicht zerfetzt sind, tragen Dich diese Flügel immer noch, wo auch immer Du hin musst. Gewährt keinen Attributbonus. Abonnentengegenstand Mai 2020.",
|
||||
"headAccessoryMystery202005Notes": "Mit so mächtigen Hörnern wird es kaum eine Kreatur wagen, Dich herauszufordern. Gewährt keinen Attributbonus. Abonnentengegenstand Mai 2020.",
|
||||
"backMystery202005Notes": "Obschon sie ganz leicht zerfetzt sind, tragen Dich diese Flügel immer noch, wo auch immer Du hin musst. Gewährt keinen Attributbonus. Abonnentengegenstand, Mai 2020.",
|
||||
"headAccessoryMystery202005Notes": "Mit so mächtigen Hörnern wird es kaum eine Kreatur wagen, Dich herauszufordern. Gewährt keinen Attributbonus. Abonnentengegenstand, Mai 2020.",
|
||||
"headAccessoryMystery202005Text": "Wunderliche Wyvernhörner",
|
||||
"backMystery202005Text": "Wunderliche Wyvernflügel",
|
||||
"weaponArmoireFiddlersBowText": "Fiedelbogen",
|
||||
@@ -2139,15 +2139,21 @@
|
||||
"shieldArmoireLifeBuoyNotes": "Oh (See)mann! Dies wird Dir noch nützlich sein wenn Du jemanden erspähst, der in Aufgaben und Verantwortlichkeiten ertrinkt. Erhöht Ausdauer um <%= con %>. Verzauberter Schrank: Rettungsschwimmer-Set (Gegenstand 2 von 3).",
|
||||
"shieldSpecialSummer2020HealerNotes": "Wie in der steten Bewegung von Sand und Wasser Abfälle zu Schätzen werden, so wird Magie die Wunden in Stärke verwandeln. Erhöht Ausdauer um <%= con %>. Limitierte Ausgabe 2020 Sommerausrüstung.",
|
||||
"shieldSpecialSummer2020HealerText": "Gescheuerte Glas-Aegis",
|
||||
"headMystery202007Notes": "Dieser Helm schwingt Dich ein auf die komplexen, wunderschönen Gesänge Deiner Walgefährten. Gewährt keinen Attributbonus. Juli 2020 Abonnentengegenstand.",
|
||||
"headMystery202007Notes": "Dieser Helm schwingt Dich ein auf die komplexen, wunderschönen Gesänge Deiner Walgefährten. Gewährt keinen Attributbonus. Abonnentengegenstand, Juli 2020.",
|
||||
"headSpecialSummer2020HealerNotes": "Steh aufrecht, damit Strandguträuber die Hände von Deinen Haaren lassen. Erhöht Intelligenz um <%= int %>. Limitierte Ausgabe 2020 Sommerausrüstung.",
|
||||
"headSpecialSummer2020WarriorNotes": "Demonstriere Deine Stärke und Gewandtheit mit dieser höchst sichtbaren Kopfbedeckung. Erhöht Stärke um <%= str %>. Limitierte Ausgabe 2020 Sommerausrüstung.",
|
||||
"armorMystery202007Notes": "Schwimm, spring, tauche und brause mit diesem ansehnlichen, mächtigen Schwanz! Gewährt keinen Attributbonus. Juli 2020 Abonnentengegenstand.",
|
||||
"armorMystery202007Notes": "Schwimm, spring, tauche und brause mit diesem ansehnlichen, mächtigen Schwanz! Gewährt keinen Attributbonus. Abonnentengegenstand, Juli 2020.",
|
||||
"armorSpecialSummer2020HealerNotes": "Du bist so geduldig wie der Ozean, so stark wie seine Strömungen, so zuverlässig wie die Gezeiten. Erhöht Ausdauer um <%= con %>. Limitierte Ausgabe 2020 Sommerausrüstung.",
|
||||
"armorSpecialSummer2020HealerText": "Ornat der tosenden Wellen",
|
||||
"armorSpecialSummer2020MageNotes": "Nutze die Macht der Meerestiefen mit dieser berauschenden Rüstung. Erhöht Intelligenz um <%= int %>. Limitierte Ausgabe 2020 Sommerausrüstung.",
|
||||
"armorSpecialSummer2020WarriorNotes": "Mit diesen schillernden Schuppen wirst Du der glänzendste Fisch im öden Strom sein! Erhöht Ausdauer um <%= con %>. Limitierte Ausgabe 2020 Sommerausrüstung.",
|
||||
"weaponArmoireHandyHookNotes": "Wer braucht schon opponierbare Daumen? Dieser Haken ist \"handlich\" genug für jeden. Erhöht Stärke um <%= str %>. Verzauberter Schrank: Piratenset (Gegenstand 1 von 3).",
|
||||
"weaponSpecialSummer2020HealerNotes": "Wie die Strömung scharfe Kanten abträgt, so soll Deine Magie die Schmerzen Deiner Freunde lindern. Erhöht Intelligenz um <%= int %>. Limitierte Ausgabe 2020 Sommerausrüstung.",
|
||||
"weaponSpecialSummer2020HealerText": "Milchglas-Stab"
|
||||
"weaponSpecialSummer2020HealerText": "Milchglas-Stab",
|
||||
"headMystery202008Text": "\"Oiliges Orakel\"-Kopfschmuck",
|
||||
"bodyMystery202008Notes": "Momentan sind Deine Flügel eingeschlagen. Aber bist Du mal fertig damit, Deine Weisheit unter die Leute zu bringen, oder erspähst Du Deine Beute im Gras, passt auf! Gewährt keinen Attributbonus. Abonnentengegenstand, August 2020.",
|
||||
"bodyMystery202008Text": "\"Oiliges Orakel\"-Mantel",
|
||||
"backSpecialNamingDay2020Notes": "Alles Liebe zum Namenstag! Schwinge diesen feurigen, pixligen Schwanz umher, um Habitica zu feiern. Gewährt keinen Attributbonus.",
|
||||
"backSpecialNamingDay2020Text": "Königlicher purpurfarbener Greifenschweif",
|
||||
"headMystery202008Notes": "Huuh Huuh, wer bist denn Duuh? Wer sucht unseren Rat? Gewährt keinen Attributbonus. Abonnentengegenstand, August 2020."
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
"questHarpyUnlockText": "Schaltet den Kauf von Papageieneiern auf dem Marktplatz frei",
|
||||
"questRoosterText": "Hahnenkampf",
|
||||
"questRoosterNotes": "Jahrelang nutzte der Farmer @extrajordanary Hähne als Wecker. Doch nun ist ein riesiger Hahn aufgetaucht, der lauter kräht als je einer davor - und alle Einwohner Habiticas weckt! Die unausgeschlafenen Habiticaner mühen sich durch ihre Tagesaufgaben. @Pandoro beschließt, dass nun die Zeit gekommen sei, dem ein Ende zu bereiten. \"Bitte, gibt es jemanden, der diesem Hahn beibringen kann, leise zu krähen?\" Du meldest Dich freiwillig und näherst Dich dem Hahn eines frühen Morgens - aber er dreht sich um, schlägt mit seinen gigantischen Flügeln, zeigt seine scharfen Krallen und kräht einen Schlachtruf.",
|
||||
"questRoosterCompletion": "Mit Raffinesse und Stärke ist es Dir gelungen, die wilde Bestie zu zähmen. Die Ohren des Hahnes, die bisher mit Federn und halbvergessenen Aufgaben verstopft waren, sind nun offen wie ein Scheunentor. Er kräht Dich leise an und kuschelt seinen Schnabel an Deine Schulter. Am nächsten Tag willst Du wieder aufbrechen, aber @EmeraldOx rennt auf Dich zu, in der Hand einen bedeckten Korb. \"Warte! Als ich diesen Morgen ins Bauernhaus kam, hatte der Hahn dies hier an die Tür geschoben, hinter der Du geschlafen hast. Ich glaube, er will, dass Du sie bekommst.\"\nDu öffnest den Korb und siehst drei zierliche Eier.",
|
||||
"questRoosterCompletion": "Mit Raffinesse und Stärke ist es Dir gelungen, die wilde Bestie zu zähmen. Die Ohren des Hahnes, die bisher mit Federn und halbvergessenen Aufgaben verstopft waren, sind nun offen wie ein Scheunentor. Er kräht Dich leise an und kuschelt seinen Schnabel an Deine Schulter. Am nächsten Tag willst Du wieder aufbrechen, aber @EmeraldOx rennt auf Dich zu, in der Hand einen bedeckten Korb. \"Warte! Als ich diesen Morgen ins Bauernhaus kam, hatte der Hahn dies hier an die Tür geschoben, hinter der Du geschlafen hast. Ich glaube, er will, dass Du sie bekommst.\" Du öffnest den Korb und siehst drei zierliche Eier.",
|
||||
"questRoosterBoss": "Hahn",
|
||||
"questRoosterDropRoosterEgg": "Hahn (Ei)",
|
||||
"questRoosterUnlockText": "Schaltet den Kauf von Hahneneiern auf dem Marktplatz frei",
|
||||
|
||||
@@ -213,5 +213,6 @@
|
||||
"mentioning": "Erwähnung",
|
||||
"chatExtensionDesc": "Die Chat Erweiterung für Habitica fügt eine intuitive Chatbox für habitica.com hinzu. Sie erlaubt Benutzern, in der Taverne, ihrer Party und all ihren Gilden zu chatten.",
|
||||
"chatExtension": "<a target='blank' href='https://chrome.google.com/webstore/detail/habitrpg-chat-client/hidkdfgonpoaiannijofifhjidbnilbb'>Chrome Chat Erweiterung</a> und <a target='blank' href='https://addons.mozilla.org/de/firefox/addon/habitica-chat-client-2/'>Firefox Chat Erweiterung</a>",
|
||||
"buyGemsGoldCapBase": "Edelsteinobergrenze bei <%= amount %>"
|
||||
"buyGemsGoldCapBase": "Edelsteinobergrenze bei <%= amount %>",
|
||||
"displaynameIssueNewline": "Anzeigenamen dürfen keinen Backslash gefolgt von einem Buchstaben N enthalten."
|
||||
}
|
||||
|
||||
@@ -213,5 +213,8 @@
|
||||
"addNotes": "Notizen hinzufügen",
|
||||
"addATitle": "Überschrift hinzufügen",
|
||||
"sureDeleteType": "Möchtest Du diese <%= type %> wirklich löschen?",
|
||||
"deleteTaskType": "Diese <%= type %> löschen"
|
||||
"deleteTaskType": "Diese <%= type %> löschen",
|
||||
"pressEnterToAddTag": "Drücke Enter um das Tag '<%= tagName %>' hinzuzufügen",
|
||||
"enterTag": "Gib ein Tag ein",
|
||||
"addTags": "Tags hinzufügen…"
|
||||
}
|
||||
|
||||
@@ -261,12 +261,14 @@
|
||||
"onlyCreatorOrAdminCanDeleteChat": "Not authorized to delete this message!",
|
||||
"onlyGroupLeaderCanEditTasks": "Not authorized to manage tasks!",
|
||||
"onlyGroupTasksCanBeAssigned": "Only group tasks can be assigned",
|
||||
"assignedTo": "Assigned To",
|
||||
"assignedTo": "Assign To",
|
||||
"assignedToUser": "Assigned to <strong><%= userName %></strong>",
|
||||
"assignedToMembers": "Assigned to <strong><%= userCount %> members</strong>",
|
||||
"assignedToYouAndMembers": "Assigned to you and <strong><%= userCount %> members</strong>",
|
||||
"youAreAssigned": "You are assigned to this task",
|
||||
"youAreAssigned": "Assigned to you",
|
||||
"taskIsUnassigned": "This task is unassigned",
|
||||
"unassigned": "Unassigned",
|
||||
"chooseTeamMember": "Choose a team member",
|
||||
"confirmClaim": "Are you sure you want to claim this task?",
|
||||
"confirmUnClaim": "Are you sure you want to unclaim this task?",
|
||||
"confirmApproval": "Are you sure you want to approve this task?",
|
||||
@@ -293,6 +295,7 @@
|
||||
"onlyGroupLeaderCanManageSubscription": "Only the group leader can manage the group's subscription",
|
||||
"youHaveBeenAssignedTask": "<%= managerName %> has assigned you the task <span class=\"notification-bold\"><%= taskText %></span>.",
|
||||
"yourTaskHasBeenApproved": "Your task <span class=\"notification-green notification-bold\"><%= taskText %></span> has been approved.",
|
||||
"thisTaskApproved": "This task was approved",
|
||||
"taskClaimed": "<%= userName %> has claimed the task <span class=\"notification-bold\"><%= taskText %></span>.",
|
||||
"taskNeedsWork": "<span class=\"notification-bold\"><%= managerName %></span> marked <span class=\"notification-bold\"><%= taskText %></span> as needing additional work.",
|
||||
"userHasRequestedTaskApproval": "<span class=\"notification-bold\"><%= user %></span> requests approval for <span class=\"notification-bold\"><%= taskName %></span>",
|
||||
@@ -498,5 +501,9 @@
|
||||
"singleCompletion": "Single - Completes when any assigned user finishes",
|
||||
"allAssignedCompletion": "All - Completes when all assigned users finish",
|
||||
"suggestedGroup": "Suggested because you’re new to Habitica.",
|
||||
"groupActivityNotificationTitle": "<%= user %> posted in <%= group %>"
|
||||
"groupActivityNotificationTitle": "<%= user %> posted in <%= group %>",
|
||||
"managerNotes": "Manager's Notes",
|
||||
"assignedDateOnly": "Assigned on <strong><%= date %></strong>",
|
||||
"assignedDateAndUser": "Assigned by <strong>@<%= username %></strong> on <strong><%= date %></strong>",
|
||||
"claimRewards": "Claim Rewards"
|
||||
}
|
||||
|
||||
@@ -211,5 +211,8 @@
|
||||
"repeatDayError": "Please ensure that you have at least one day of the week selected.",
|
||||
"searchTasks": "Search titles and descriptions...",
|
||||
"sessionOutdated": "Your session is outdated. Please refresh or sync.",
|
||||
"errorTemporaryItem": "This item is temporary and cannot be pinned."
|
||||
"errorTemporaryItem": "This item is temporary and cannot be pinned.",
|
||||
"addTags": "Add tags...",
|
||||
"enterTag": "Enter a tag",
|
||||
"pressEnterToAddTag": "Press Enter to add tag: '<%= tagName %>'"
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
"commGuideList02L": "<strong>We highly discourage the exchange of personal information -- particularly information that can be used to identify you -- in public chat spaces</strong>. Identifying information can include but is not limited to: your address, your email address, and your API token/password. This is for your safety! Staff or moderators may remove such posts at their discretion. If you are asked for personal information in a private Guild, Party, or PM, we highly recommend that you politely refuse and alert the staff and moderators by either 1) flagging the message if it is in a Party or private Guild, or 2) filling out the <a href='https://contact.habitica.com/' target='_blank'>Moderator Contact Form</a> and including screenshots.",
|
||||
"commGuidePara019": "<strong>In private spaces</strong>, users have more freedom to discuss whatever topics they would like, but they still may not violate the Terms and Conditions, including posting slurs or any discriminatory, violent, or threatening content. Note that, because Challenge names appear in the winner's public profile, ALL Challenge names must obey the public space guidelines, even if they appear in a private space.",
|
||||
"commGuidePara020": "<strong>Private Messages (PMs)</strong> have some additional guidelines. If someone has blocked you, do not contact them elsewhere to ask them to unblock you. Additionally, you should not send PMs to someone asking for support (since public answers to support questions are helpful to the community). Finally, do not send anyone PMs begging for a gift of gems or a subscription, as this can be considered spamming.",
|
||||
"commGuidePara020A": "<strong>If you see a post that you believe is in violation of the public space guidelines outlined above, or if you see a post that concerns you or makes you uncomfortable, you can bring it to the attention of Moderators and Staff by clicking the flag icon to report it</strong>. A Staff member or Moderator will respond to the situation as soon as possible. Please note that intentionally reporting innocent posts is an infraction of these Guidelines (see below in “Infractions”). PMs cannot be flagged at this time, so if you need to report a PM, please contact the Mods via the form on the “Contact Us” page, which you can also access via the help menu by clicking “<a href='https://contact.habitica.com/' target='_blank'>Contact the Moderation Team</a>.” You may want to do this if there are multiple problematic posts by the same person in different Guilds, or if the situation requires some explanation. You may contact us in your native language if that is easier for you: we may have to use Google Translate, but we want you to feel comfortable about contacting us if you have a problem.",
|
||||
"commGuidePara020A": "<strong>If you see a post or private message that you believe is in violation of the public space guidelines outlined above, or if you see a post or private message that concerns you or makes you uncomfortable, you can bring it to the attention of Moderators and Staff by clicking the flag icon to report it</strong>. A Staff member or Moderator will respond to the situation as soon as possible. Please note that intentionally reporting innocent posts is an infraction of these Guidelines (see below in “Infractions”). You can also contact the Mods via the form on the “Contact Us” page, which you can also access via the help menu by clicking “<a href='https://contact.habitica.com/' target='_blank'>Contact the Moderation Team</a>.” You may want to do this if there are multiple problematic posts by the same person in different Guilds, or if the situation requires some explanation. You may contact us in your native language if that is easier for you: we may have to use Google Translate, but we want you to feel comfortable about contacting us if you have a problem.",
|
||||
"commGuidePara021": "Furthermore, some public spaces in Habitica have additional guidelines.",
|
||||
"commGuideHeadingTavern": "The Tavern",
|
||||
"commGuidePara022": "The Tavern is the main spot for Habiticans to mingle. Daniel the Innkeeper keeps the place spic-and-span, and Lemoness will happily conjure up some lemonade while you sit and chat. Just keep in mind…",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user