mirror of
https://github.com/HabitRPG/habitica.git
synced 2026-05-20 03:14:01 -05:00
Compare commits
53 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a09c6b84d5 | |||
| 3ba161abbe | |||
| 9038572ff9 | |||
| 80825f3478 | |||
| a254c50a8e | |||
| 918a769441 | |||
| 1fab19acf4 | |||
| 5743fb86b0 | |||
| 5443bf2459 | |||
| c0d5566417 | |||
| ded71b46c5 | |||
| 9693ad321c | |||
| dd3679f329 | |||
| f3029953dc | |||
| 01881b2fd8 | |||
| 11a22d0f5d | |||
| 5f9bf07045 | |||
| 719c03e2f5 | |||
| 379afa9554 | |||
| dbc23e89b8 | |||
| 0c6e254742 | |||
| 8327e69bdd | |||
| 2d953f4f59 | |||
| 7118d63949 | |||
| 20af8d038e | |||
| 3d9dfbb5e1 | |||
| ae0b966f45 | |||
| cef8a34c06 | |||
| 6432823eec | |||
| 563b780d85 | |||
| aa9b1b2cac | |||
| 401e541b86 | |||
| c13bed3bad | |||
| b3c4817fb4 | |||
| 7c9c45ac5f | |||
| 95142e3684 | |||
| dc1cce6ddb | |||
| 43cf77f33c | |||
| 93780d7056 | |||
| 2ad17d408e | |||
| b0f7567367 | |||
| 3f2b1d3f79 | |||
| 29eb8ca10b | |||
| 8c71ca12b8 | |||
| 72a753626f | |||
| 35ebb12bf2 | |||
| 1ff418f62d | |||
| e1aa437ea5 | |||
| 2a4239bf3c | |||
| 399563435b | |||
| 59f7e25c85 | |||
| ad845dff43 | |||
| fd1eb2d900 |
@@ -7,5 +7,14 @@ module.exports = {
|
||||
rules: {
|
||||
'prefer-regex-literals': 'warn',
|
||||
'import/no-extraneous-dependencies': 'off',
|
||||
'require-await': 'error',
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: ['migrations/**', 'gulp/**'], // Or *.test.js
|
||||
rules: {
|
||||
'require-await': 'off',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
name: Test
|
||||
|
||||
on: [push, pull_request]
|
||||
on:
|
||||
push:
|
||||
branches-ignore:
|
||||
- 'phillip/**'
|
||||
- 'sabrecat/**'
|
||||
- 'kalista/**'
|
||||
- 'natalie/**'
|
||||
pull_request:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
+1
-1
@@ -47,5 +47,5 @@ webpack.webstorm.config
|
||||
|
||||
# mongodb replica set for local dev
|
||||
mongodb-*.tgz
|
||||
/mongodb-data
|
||||
/mongodb-data*
|
||||
/.nyc_output
|
||||
|
||||
+2
-1
@@ -93,5 +93,6 @@
|
||||
"TRUSTED_DOMAINS": "localhost,https://habitica.com",
|
||||
"TIME_TRAVEL_ENABLED": "false",
|
||||
"DEBUG_ENABLED": "false",
|
||||
"CONTENT_SWITCHOVER_TIME_OFFSET": 8
|
||||
"CONTENT_SWITCHOVER_TIME_OFFSET": 8,
|
||||
"SLOW_REQUEST_THRESHOLD": 1000
|
||||
}
|
||||
|
||||
@@ -64,6 +64,15 @@ function filterFile (file) {
|
||||
if (file.relative.indexOf('icon_background') === 0) {
|
||||
return false;
|
||||
}
|
||||
if (file.relative.indexOf('notif_') === 0) {
|
||||
return false;
|
||||
}
|
||||
if (file.relative.indexOf('quest_') === 0) {
|
||||
return false;
|
||||
}
|
||||
if (file.relative.indexOf('inventory_quest_') === 0) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
+1
-1
Submodule habitica-images updated: 1db5adcca4...aa72332019
Generated
+203
-128
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "habitica",
|
||||
"version": "5.33.0",
|
||||
"version": "5.35.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "habitica",
|
||||
"version": "5.33.0",
|
||||
"version": "5.35.1",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.22.10",
|
||||
@@ -56,7 +56,7 @@
|
||||
"method-override": "^3.0.0",
|
||||
"moment": "^2.29.4",
|
||||
"moment-recur": "^1.0.7",
|
||||
"mongoose": "^7.8.3",
|
||||
"mongoose": "^8.9.5",
|
||||
"morgan": "^1.10.0",
|
||||
"nconf": "^0.12.1",
|
||||
"node-gcm": "^1.0.5",
|
||||
@@ -3047,7 +3047,7 @@
|
||||
"version": "1.1.9",
|
||||
"resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.1.9.tgz",
|
||||
"integrity": "sha512-tVkljjeEaAhCqTzajSdgbQ6gE6f3oneVwa3iXR6csiEwXXOFsiC6Uh9iAjAhXPtqa/XMDHWjjeNH/77m/Yq2dw==",
|
||||
"optional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"sparse-bitfield": "^3.0.3"
|
||||
}
|
||||
@@ -3677,14 +3677,15 @@
|
||||
"node_modules/@types/webidl-conversions": {
|
||||
"version": "7.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz",
|
||||
"integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA=="
|
||||
"integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/whatwg-url": {
|
||||
"version": "8.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-8.2.2.tgz",
|
||||
"integrity": "sha512-FtQu10RWgn3D9U4aazdwIE2yzphmTJREDqNdODHrbrZmmMqI0vMheC/6NE/J1Yveaj8H+ela+YwWTjq5PGmuhA==",
|
||||
"version": "11.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.5.tgz",
|
||||
"integrity": "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*",
|
||||
"@types/webidl-conversions": "*"
|
||||
}
|
||||
},
|
||||
@@ -6401,10 +6402,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/bson": {
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/bson/-/bson-6.3.0.tgz",
|
||||
"integrity": "sha512-balJfqwwTBddxfnidJZagCBPP/f48zj9Sdp3OJswREOgsJzHiQSaOIAtApSgDQFYgHqAvFkp53AFSqjMDZoTFw==",
|
||||
"dev": true,
|
||||
"version": "6.10.2",
|
||||
"resolved": "https://registry.npmjs.org/bson/-/bson-6.10.2.tgz",
|
||||
"integrity": "sha512-5afhLTjqDSA3akH56E+/2J6kTDuSIlBxyXPdQslj9hcIgOUE378xdOfZvC/9q3LifJNI6KR/juZ+d0NRNYBwXg==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=16.20.1"
|
||||
}
|
||||
@@ -13360,28 +13361,6 @@
|
||||
"resolved": "https://registry.npmjs.org/iota-array/-/iota-array-1.0.0.tgz",
|
||||
"integrity": "sha512-pZ2xT+LOHckCatGQ3DcG/a+QuEqvoxqkiL7tvE8nn3uuu+f6i1TtpB5/FtWFbxUuVr5PZCx8KskuGatbJDXOWA=="
|
||||
},
|
||||
"node_modules/ip-address": {
|
||||
"version": "9.0.5",
|
||||
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz",
|
||||
"integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==",
|
||||
"dependencies": {
|
||||
"jsbn": "1.1.0",
|
||||
"sprintf-js": "^1.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
}
|
||||
},
|
||||
"node_modules/ip-address/node_modules/jsbn": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz",
|
||||
"integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A=="
|
||||
},
|
||||
"node_modules/ip-address/node_modules/sprintf-js": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz",
|
||||
"integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA=="
|
||||
},
|
||||
"node_modules/ipaddr.js": {
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
||||
@@ -14296,9 +14275,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/kareem": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/kareem/-/kareem-2.5.1.tgz",
|
||||
"integrity": "sha512-7jFxRVm+jD+rkq3kY0iZDJfsO2/t4BBPeEb2qKn2lR/9KhuksYk5hxzfRYWMPV8P/x2d0kHD306YyWLzjjH+uA==",
|
||||
"version": "2.6.3",
|
||||
"resolved": "https://registry.npmjs.org/kareem/-/kareem-2.6.3.tgz",
|
||||
"integrity": "sha512-C3iHfuGUXK2u8/ipq9LfjFfXFxAZMQJJq7vLS45r3D9Y2xQ/m4S8zaR4zMLFWh9AsNPXmcFfUDhTEO8UIC/V6Q==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
@@ -14307,7 +14287,7 @@
|
||||
"version": "1.1.7",
|
||||
"resolved": "https://registry.npmjs.org/kerberos/-/kerberos-1.1.7.tgz",
|
||||
"integrity": "sha512-1zXg4rARjsh/VMz2jjZeTfRHbJTVNR6f2DYHbLvtUSOW1satj33Fvc7vOJ0YVWB9+/9ITJWd1QKp4w217SsiFA==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"bindings": "^1.5.0",
|
||||
@@ -14965,8 +14945,7 @@
|
||||
"node_modules/memory-pager": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz",
|
||||
"integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==",
|
||||
"optional": true
|
||||
"integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg=="
|
||||
},
|
||||
"node_modules/meow": {
|
||||
"version": "3.7.0",
|
||||
@@ -15475,43 +15454,47 @@
|
||||
}
|
||||
},
|
||||
"node_modules/mongodb-connection-string-url": {
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-2.6.0.tgz",
|
||||
"integrity": "sha512-WvTZlI9ab0QYtTYnuMLgobULWhokRjtC7db9LtcVfJ+Hsnyr5eo6ZtNAt3Ly24XZScGMelOcGtm7lSn0332tPQ==",
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.2.tgz",
|
||||
"integrity": "sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@types/whatwg-url": "^8.2.1",
|
||||
"whatwg-url": "^11.0.0"
|
||||
"@types/whatwg-url": "^11.0.2",
|
||||
"whatwg-url": "^14.1.0 || ^13.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/mongodb-connection-string-url/node_modules/tr46": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz",
|
||||
"integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==",
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/tr46/-/tr46-5.0.0.tgz",
|
||||
"integrity": "sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"punycode": "^2.1.1"
|
||||
"punycode": "^2.3.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/mongodb-connection-string-url/node_modules/webidl-conversions": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
|
||||
"integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/mongodb-connection-string-url/node_modules/whatwg-url": {
|
||||
"version": "11.0.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz",
|
||||
"integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==",
|
||||
"version": "14.1.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.1.0.tgz",
|
||||
"integrity": "sha512-jlf/foYIKywAt3x/XWKZ/3rz8OSJPiWktjmk891alJUEjiVxKX9LEO92qH3hv4aJ0mN3MWPvGMCy8jQi95xK4w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tr46": "^3.0.0",
|
||||
"tr46": "^5.0.0",
|
||||
"webidl-conversions": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/mongodb-core": {
|
||||
@@ -15596,55 +15579,64 @@
|
||||
}
|
||||
},
|
||||
"node_modules/mongoose": {
|
||||
"version": "7.8.3",
|
||||
"resolved": "https://registry.npmjs.org/mongoose/-/mongoose-7.8.3.tgz",
|
||||
"integrity": "sha512-eFnbkKgyVrICoHB6tVJ4uLanS7d5AIo/xHkEbQeOv6g2sD7gh/1biRwvFifsmbtkIddQVNr3ROqHik6gkknN3g==",
|
||||
"version": "8.9.7",
|
||||
"resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.9.7.tgz",
|
||||
"integrity": "sha512-mvNXmU0V8qZzMR/qoK2mjT4Ti2ALdtfS0teK+twxhlGkwzOD76V02/zWajTu2MJ7QyEmZe9OWvnJsIY0iAuX3Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bson": "^5.5.0",
|
||||
"kareem": "2.5.1",
|
||||
"mongodb": "5.9.2",
|
||||
"bson": "^6.10.1",
|
||||
"kareem": "2.6.3",
|
||||
"mongodb": "~6.12.0",
|
||||
"mpath": "0.9.0",
|
||||
"mquery": "5.0.0",
|
||||
"ms": "2.1.3",
|
||||
"sift": "16.0.1"
|
||||
"sift": "17.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.20.1"
|
||||
"node": ">=16.20.1"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/mongoose"
|
||||
}
|
||||
},
|
||||
"node_modules/mongoose/node_modules/bson": {
|
||||
"version": "5.5.1",
|
||||
"resolved": "https://registry.npmjs.org/bson/-/bson-5.5.1.tgz",
|
||||
"integrity": "sha512-ix0EwukN2EpC0SRWIj/7B5+A6uQMQy6KMREI9qQqvgpkV2frH63T0UDVd1SYedL6dNCmDBYB3QtXi4ISk9YT+g==",
|
||||
"node_modules/mongoose/node_modules/kerberos": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/kerberos/-/kerberos-2.2.1.tgz",
|
||||
"integrity": "sha512-Vlyv1tjAPb0y2VIJ03dKkUjsneGIBuTkH24uGRx6/DrKpFlVuGPmct3m5aEotljVUlw7PAGWABwR5aNeW7y8Zw==",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"node-addon-api": "^6.1.0",
|
||||
"prebuild-install": "^7.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.20.1"
|
||||
"node": ">=12.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/mongoose/node_modules/mongodb": {
|
||||
"version": "5.9.2",
|
||||
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-5.9.2.tgz",
|
||||
"integrity": "sha512-H60HecKO4Bc+7dhOv4sJlgvenK4fQNqqUIlXxZYQNbfEWSALGAwGoyJd/0Qwk4TttFXUOHJ2ZJQe/52ScaUwtQ==",
|
||||
"version": "6.12.0",
|
||||
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.12.0.tgz",
|
||||
"integrity": "sha512-RM7AHlvYfS7jv7+BXund/kR64DryVI+cHbVAy9P61fnb1RcWZqOW1/Wj2YhqMCx+MuYhqTRGv7AwHBzmsCKBfA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"bson": "^5.5.0",
|
||||
"mongodb-connection-string-url": "^2.6.0",
|
||||
"socks": "^2.7.1"
|
||||
"@mongodb-js/saslprep": "^1.1.9",
|
||||
"bson": "^6.10.1",
|
||||
"mongodb-connection-string-url": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.20.1"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@mongodb-js/saslprep": "^1.1.0"
|
||||
"node": ">=16.20.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@aws-sdk/credential-providers": "^3.188.0",
|
||||
"@mongodb-js/zstd": "^1.0.0",
|
||||
"kerberos": "^1.0.0 || ^2.0.0",
|
||||
"mongodb-client-encryption": ">=2.3.0 <3",
|
||||
"snappy": "^7.2.2"
|
||||
"@mongodb-js/zstd": "^1.1.0 || ^2.0.0",
|
||||
"gcp-metadata": "^5.2.0",
|
||||
"kerberos": "^2.0.1",
|
||||
"mongodb-client-encryption": ">=6.0.0 <7",
|
||||
"snappy": "^7.2.2",
|
||||
"socks": "^2.7.1"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@aws-sdk/credential-providers": {
|
||||
@@ -15653,6 +15645,9 @@
|
||||
"@mongodb-js/zstd": {
|
||||
"optional": true
|
||||
},
|
||||
"gcp-metadata": {
|
||||
"optional": true
|
||||
},
|
||||
"kerberos": {
|
||||
"optional": true
|
||||
},
|
||||
@@ -15661,6 +15656,9 @@
|
||||
},
|
||||
"snappy": {
|
||||
"optional": true
|
||||
},
|
||||
"socks": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -15669,6 +15667,105 @@
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
|
||||
},
|
||||
"node_modules/mongoose/node_modules/napi-build-utils": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz",
|
||||
"integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/mongoose/node_modules/node-abi": {
|
||||
"version": "3.74.0",
|
||||
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.74.0.tgz",
|
||||
"integrity": "sha512-c5XK0MjkGBrQPGYG24GBADZud0NCbznxNx0ZkS+ebUTrmV1qTDxPxSL8zEAPURXSbLRWVexxmP4986BziahL5w==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"semver": "^7.3.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/mongoose/node_modules/node-addon-api": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz",
|
||||
"integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/mongoose/node_modules/prebuild-install": {
|
||||
"version": "7.1.3",
|
||||
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz",
|
||||
"integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"detect-libc": "^2.0.0",
|
||||
"expand-template": "^2.0.3",
|
||||
"github-from-package": "0.0.0",
|
||||
"minimist": "^1.2.3",
|
||||
"mkdirp-classic": "^0.5.3",
|
||||
"napi-build-utils": "^2.0.0",
|
||||
"node-abi": "^3.3.0",
|
||||
"pump": "^3.0.0",
|
||||
"rc": "^1.2.7",
|
||||
"simple-get": "^4.0.0",
|
||||
"tar-fs": "^2.0.0",
|
||||
"tunnel-agent": "^0.6.0"
|
||||
},
|
||||
"bin": {
|
||||
"prebuild-install": "bin.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/mongoose/node_modules/semver": {
|
||||
"version": "7.7.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz",
|
||||
"integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==",
|
||||
"license": "ISC",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/mongoose/node_modules/simple-get": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz",
|
||||
"integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"decompress-response": "^6.0.0",
|
||||
"once": "^1.3.1",
|
||||
"simple-concat": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/monk": {
|
||||
"version": "7.3.4",
|
||||
"resolved": "https://registry.npmjs.org/monk/-/monk-7.3.4.tgz",
|
||||
@@ -16082,7 +16179,7 @@
|
||||
"version": "2.30.1",
|
||||
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-2.30.1.tgz",
|
||||
"integrity": "sha512-/2D0wOQPgaUWzVSVgRMx+trKJRC2UG4SUc4oCJoXx9Uxjtp0Vy3/kt7zcbxHF8+Z/pK3UloLWzBISg72brfy1w==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"semver": "^5.4.1"
|
||||
}
|
||||
@@ -16091,7 +16188,7 @@
|
||||
"version": "5.7.2",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
|
||||
"integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"semver": "bin/semver"
|
||||
}
|
||||
@@ -16281,7 +16378,7 @@
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/noop-logger/-/noop-logger-0.1.1.tgz",
|
||||
"integrity": "sha512-6kM8CLXvuW5crTxsAtva2YLrRrDaiTIkIePWs9moLHqbFWT94WpNFjwS/5dfLfECg5i/lkmw3aoqVidxt23TEQ==",
|
||||
"devOptional": true
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/nopt": {
|
||||
"version": "1.0.10",
|
||||
@@ -17896,7 +17993,7 @@
|
||||
"version": "6.1.2",
|
||||
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-6.1.2.tgz",
|
||||
"integrity": "sha512-PzYWIKZeP+967WuKYXlTOhYBgGOvTRSfaKI89XnfJ0ansRAH7hDU45X+K+FZeI1Wb/7p/NnuctPH3g0IqKUuSQ==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"detect-libc": "^1.0.3",
|
||||
"expand-template": "^2.0.3",
|
||||
@@ -17924,7 +18021,7 @@
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
|
||||
"integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -17933,13 +18030,13 @@
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz",
|
||||
"integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==",
|
||||
"devOptional": true
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/prebuild-install/node_modules/are-we-there-yet": {
|
||||
"version": "1.1.7",
|
||||
"resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.7.tgz",
|
||||
"integrity": "sha512-nxwy40TuMiUGqMyRHgCSWZ9FM4VAoRP4xUYSTv5ImRog+h9yISPbVH7H8fASCIzYn9wlEv4zvFL7uKDMCFQm3g==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"delegates": "^1.0.0",
|
||||
"readable-stream": "^2.0.6"
|
||||
@@ -17949,7 +18046,7 @@
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
|
||||
"integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"detect-libc": "bin/detect-libc.js"
|
||||
},
|
||||
@@ -17961,7 +18058,7 @@
|
||||
"version": "2.7.4",
|
||||
"resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz",
|
||||
"integrity": "sha512-14x4kjc6lkD3ltw589k0NrPD6cCNTD6CWoVUNpB85+DrtONoZn+Rug6xZU5RvSC4+TZPxA5AnBibQYAvZn41Hg==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"aproba": "^1.0.3",
|
||||
"console-control-strings": "^1.0.0",
|
||||
@@ -17977,7 +18074,7 @@
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz",
|
||||
"integrity": "sha512-1pqUqRjkhPJ9miNq9SwMfdvi6lBJcd6eFxvfaivQhaH3SgisfiuudvFntdKOmxuee/77l+FPjKrQjWvmPjWrRw==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"number-is-nan": "^1.0.0"
|
||||
},
|
||||
@@ -17989,13 +18086,13 @@
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
||||
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
|
||||
"devOptional": true
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/prebuild-install/node_modules/npmlog": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz",
|
||||
"integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"are-we-there-yet": "~1.1.2",
|
||||
"console-control-strings": "~1.1.0",
|
||||
@@ -18007,7 +18104,7 @@
|
||||
"version": "2.3.8",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
|
||||
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"core-util-is": "~1.0.0",
|
||||
"inherits": "~2.0.3",
|
||||
@@ -18022,7 +18119,7 @@
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
||||
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"safe-buffer": "~5.1.0"
|
||||
}
|
||||
@@ -18031,7 +18128,7 @@
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
|
||||
"integrity": "sha512-0XsVpQLnVCXHJfyEs8tC0zpTVIr5PKKsQtkT29IwupnPTjtPmQ3xT/4yCREF9hYkV/3M3kzcUTSAZT6a6h81tw==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"code-point-at": "^1.0.0",
|
||||
"is-fullwidth-code-point": "^1.0.0",
|
||||
@@ -18045,7 +18142,7 @@
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
|
||||
"integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"ansi-regex": "^2.0.0"
|
||||
},
|
||||
@@ -19576,9 +19673,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/sift": {
|
||||
"version": "16.0.1",
|
||||
"resolved": "https://registry.npmjs.org/sift/-/sift-16.0.1.tgz",
|
||||
"integrity": "sha512-Wv6BjQ5zbhW7VFefWusVP33T/EM0vYikCaQ2qR8yULbsilAT8/wQaXvuQ3ptGLpoKx+lihJE3y2UTgKDyyNHZQ=="
|
||||
"version": "17.1.3",
|
||||
"resolved": "https://registry.npmjs.org/sift/-/sift-17.1.3.tgz",
|
||||
"integrity": "sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/signal-exit": {
|
||||
"version": "3.0.7",
|
||||
@@ -19608,7 +19706,7 @@
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.1.1.tgz",
|
||||
"integrity": "sha512-CQ5LTKGfCpvE1K0n2us+kuMPbk/q0EKl82s4aheV9oXjFEz6W/Y7oQFVJuU6QG77hRT4Ghb5RURteF5vnWjupA==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"decompress-response": "^4.2.0",
|
||||
"once": "^1.3.1",
|
||||
@@ -19619,7 +19717,7 @@
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz",
|
||||
"integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"mimic-response": "^2.0.0"
|
||||
},
|
||||
@@ -19631,7 +19729,7 @@
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz",
|
||||
"integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
@@ -19772,15 +19870,6 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/smart-buffer": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
|
||||
"integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==",
|
||||
"engines": {
|
||||
"node": ">= 6.0.0",
|
||||
"npm": ">= 3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/snapdragon": {
|
||||
"version": "0.8.2",
|
||||
"resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz",
|
||||
@@ -19900,19 +19989,6 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/socks": {
|
||||
"version": "2.8.3",
|
||||
"resolved": "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz",
|
||||
"integrity": "sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==",
|
||||
"dependencies": {
|
||||
"ip-address": "^9.0.5",
|
||||
"smart-buffer": "^4.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10.0.0",
|
||||
"npm": ">= 3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/sort-keys": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz",
|
||||
@@ -19990,7 +20066,6 @@
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz",
|
||||
"integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"memory-pager": "^1.0.2"
|
||||
}
|
||||
|
||||
+3
-2
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "habitica",
|
||||
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
|
||||
"version": "5.33.0",
|
||||
"version": "5.35.1",
|
||||
"main": "./website/server/index.js",
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.22.10",
|
||||
@@ -51,7 +51,7 @@
|
||||
"method-override": "^3.0.0",
|
||||
"moment": "^2.29.4",
|
||||
"moment-recur": "^1.0.7",
|
||||
"mongoose": "^7.8.3",
|
||||
"mongoose": "^8.9.5",
|
||||
"morgan": "^1.10.0",
|
||||
"nconf": "^0.12.1",
|
||||
"node-gcm": "^1.0.5",
|
||||
@@ -110,6 +110,7 @@
|
||||
"start:simple": "node ./website/server/index.js",
|
||||
"debug": "gulp nodemon --inspect",
|
||||
"mongo:dev": "run-rs -v 5.0.23 -l ubuntu1804 --keep --dbpath mongodb-data --number 1 --quiet",
|
||||
"mongo:test": "run-rs -v 5.0.23 -l ubuntu1804 --keep --dbpath mongodb-data-testing --number 1 --quiet",
|
||||
"postinstall": "git config --global url.\"https://\".insteadOf git:// && gulp build && cd website/client && npm install",
|
||||
"apidoc": "gulp apidoc",
|
||||
"heroku-postbuild": ".heroku/report_deploy.sh"
|
||||
|
||||
+253
-122
@@ -2,13 +2,22 @@
|
||||
import moment from 'moment';
|
||||
import nconf from 'nconf';
|
||||
import requireAgain from 'require-again';
|
||||
import { recoverCron, cron } from '../../../../website/server/libs/cron';
|
||||
import { v4 as generateUUID } from 'uuid';
|
||||
import {
|
||||
generateRes,
|
||||
generateReq,
|
||||
generateTodo,
|
||||
generateDaily,
|
||||
} from '../../../helpers/api-unit.helper';
|
||||
import { cron, cronWrapper } from '../../../../website/server/libs/cron';
|
||||
import { model as User } from '../../../../website/server/models/user';
|
||||
import * as Tasks from '../../../../website/server/models/task';
|
||||
import common from '../../../../website/common';
|
||||
import * as analytics from '../../../../website/server/libs/analyticsService';
|
||||
import { model as Group } from '../../../../website/server/models/group';
|
||||
|
||||
// const scoreTask = common.ops.scoreTask;
|
||||
const CRON_TIMEOUT_WAIT = new Date(5 * 60 * 1000).getTime();
|
||||
const CRON_TIMEOUT_UNIT = new Date(60 * 1000).getTime();
|
||||
|
||||
const pathToCronLib = '../../../../website/server/libs/cron';
|
||||
|
||||
@@ -1200,7 +1209,7 @@ describe('cron', async () => {
|
||||
it('increments perfect day achievement if all (at least 1) due dailies were completed', async () => {
|
||||
daysMissed = 1;
|
||||
tasksByType.dailys[0].completed = true;
|
||||
tasksByType.dailys[0].startDate = moment(new Date()).subtract({ days: 1 });
|
||||
tasksByType.dailys[0].isDue = true;
|
||||
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
@@ -1212,7 +1221,7 @@ describe('cron', async () => {
|
||||
it('does not increment perfect day achievement if no due dailies', async () => {
|
||||
daysMissed = 1;
|
||||
tasksByType.dailys[0].completed = true;
|
||||
tasksByType.dailys[0].startDate = moment(new Date()).add({ days: 1 });
|
||||
tasksByType.dailys[0].isDue = false;
|
||||
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
@@ -1224,7 +1233,7 @@ describe('cron', async () => {
|
||||
it('gives perfect day buff if all (at least 1) due dailies were completed', async () => {
|
||||
daysMissed = 1;
|
||||
tasksByType.dailys[0].completed = true;
|
||||
tasksByType.dailys[0].startDate = moment(new Date()).subtract({ days: 1 });
|
||||
tasksByType.dailys[0].isDue = true;
|
||||
|
||||
const previousBuffs = user.stats.buffs.toObject();
|
||||
|
||||
@@ -1242,7 +1251,7 @@ describe('cron', async () => {
|
||||
user.preferences.sleep = true;
|
||||
daysMissed = 1;
|
||||
tasksByType.dailys[0].completed = true;
|
||||
tasksByType.dailys[0].startDate = moment(new Date()).subtract({ days: 1 });
|
||||
tasksByType.dailys[0].isDue = true;
|
||||
|
||||
const previousBuffs = user.stats.buffs.toObject();
|
||||
|
||||
@@ -1259,7 +1268,7 @@ describe('cron', async () => {
|
||||
it('clears buffs if user does not have a perfect day (no due dailys)', async () => {
|
||||
daysMissed = 1;
|
||||
tasksByType.dailys[0].completed = true;
|
||||
tasksByType.dailys[0].startDate = moment(new Date()).add({ days: 1 });
|
||||
tasksByType.dailys[0].isDue = false;
|
||||
|
||||
user.stats.buffs = {
|
||||
str: 1,
|
||||
@@ -1488,78 +1497,6 @@ describe('cron', async () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('notifications', async () => {
|
||||
it('adds a user notification', async () => {
|
||||
const mpBefore = user.stats.mp;
|
||||
tasksByType.dailys[0].completed = true;
|
||||
|
||||
const statsComputedRes = common.statsComputed(user);
|
||||
const stubbedStatsComputed = sinon.stub(common, 'statsComputed');
|
||||
stubbedStatsComputed.returns(Object.assign(statsComputedRes, { maxMP: 100 }));
|
||||
|
||||
daysMissed = 1;
|
||||
const hpBefore = user.stats.hp;
|
||||
tasksByType.dailys[0].startDate = moment(new Date()).subtract({ days: 1 });
|
||||
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
|
||||
expect(user.notifications.length).to.be.greaterThan(0);
|
||||
expect(user.notifications[1].type).to.equal('CRON');
|
||||
expect(user.notifications[1].data).to.eql({
|
||||
hp: user.stats.hp - hpBefore,
|
||||
mp: user.stats.mp - mpBefore,
|
||||
});
|
||||
|
||||
common.statsComputed.restore();
|
||||
});
|
||||
|
||||
it('condenses multiple notifications into one', async () => {
|
||||
const mpBefore1 = user.stats.mp;
|
||||
tasksByType.dailys[0].completed = true;
|
||||
|
||||
const statsComputedRes = common.statsComputed(user);
|
||||
const stubbedStatsComputed = sinon.stub(common, 'statsComputed');
|
||||
stubbedStatsComputed.returns(Object.assign(statsComputedRes, { maxMP: 100 }));
|
||||
|
||||
daysMissed = 1;
|
||||
const hpBefore1 = user.stats.hp;
|
||||
tasksByType.dailys[0].startDate = moment(new Date()).subtract({ days: 1 });
|
||||
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
|
||||
expect(user.notifications.length).to.be.greaterThan(0);
|
||||
expect(user.notifications[1].type).to.equal('CRON');
|
||||
expect(user.notifications[1].data).to.eql({
|
||||
hp: user.stats.hp - hpBefore1,
|
||||
mp: user.stats.mp - mpBefore1,
|
||||
});
|
||||
|
||||
const notifsBefore2 = user.notifications.length;
|
||||
const hpBefore2 = user.stats.hp;
|
||||
const mpBefore2 = user.stats.mp;
|
||||
|
||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
|
||||
expect(user.notifications.length - notifsBefore2).to.equal(0);
|
||||
expect(user.notifications[0].type).to.not.equal('CRON');
|
||||
expect(user.notifications[1].type).to.equal('CRON');
|
||||
expect(user.notifications[1].data).to.eql({
|
||||
hp: user.stats.hp - hpBefore2 - (hpBefore2 - hpBefore1),
|
||||
mp: user.stats.mp - mpBefore2 - (mpBefore2 - mpBefore1),
|
||||
});
|
||||
expect(user.notifications[0].type).to.not.equal('CRON');
|
||||
common.statsComputed.restore();
|
||||
});
|
||||
});
|
||||
|
||||
describe('private messages', async () => {
|
||||
let lastMessageId;
|
||||
|
||||
@@ -1606,7 +1543,7 @@ describe('cron', async () => {
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user.notifications.length).to.be.greaterThan(1);
|
||||
expect(user.notifications.length).to.eql(1);
|
||||
expect(user.notifications[0].type).to.eql('LOGIN_INCENTIVE');
|
||||
});
|
||||
|
||||
@@ -1820,64 +1757,258 @@ describe('cron', async () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('recoverCron', async () => {
|
||||
let locals; let status; let
|
||||
execStub;
|
||||
describe('cron wrapper', () => {
|
||||
let res; let
|
||||
req;
|
||||
let user;
|
||||
|
||||
beforeEach(async () => {
|
||||
execStub = sandbox.stub();
|
||||
sandbox.stub(User, 'findOne').returns({ exec: execStub });
|
||||
|
||||
status = { times: 0 };
|
||||
locals = {
|
||||
user: new User({
|
||||
auth: {
|
||||
local: {
|
||||
username: 'username',
|
||||
lowerCaseUsername: 'username',
|
||||
email: 'email@example.com',
|
||||
salt: 'salt',
|
||||
hashed_password: 'hashed_password', // eslint-disable-line camelcase
|
||||
},
|
||||
},
|
||||
}),
|
||||
};
|
||||
res = generateRes();
|
||||
req = generateReq();
|
||||
user = await res.locals.user.save();
|
||||
res.analytics = analytics;
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
afterEach(() => {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
it('throws an error if user cannot be found', async () => {
|
||||
execStub.returns(Promise.resolve(null));
|
||||
it('calls next when user is not attached', async () => {
|
||||
res.locals.user = null;
|
||||
await cronWrapper(req, res);
|
||||
});
|
||||
|
||||
it('calls next when days have not been missed', async () => {
|
||||
await cronWrapper(req, res);
|
||||
});
|
||||
|
||||
it('should clear todos older than 30 days for free users', async () => {
|
||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||
const task = generateTodo(user);
|
||||
task.dateCompleted = moment(new Date()).subtract({ days: 31 });
|
||||
task.completed = true;
|
||||
await task.save();
|
||||
await user.save();
|
||||
|
||||
await cronWrapper(req, res);
|
||||
const taskRes = await Tasks.Task.findOne({ _id: task._id });
|
||||
expect(taskRes).to.not.exist;
|
||||
});
|
||||
|
||||
it('should not clear todos older than 30 days for subscribed users', async () => {
|
||||
user.purchased.plan.customerId = 'subscribedId';
|
||||
user.purchased.plan.dateUpdated = moment('012013', 'MMYYYY');
|
||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||
const task = generateTodo(user);
|
||||
task.dateCompleted = moment(new Date()).subtract({ days: 31 });
|
||||
task.completed = true;
|
||||
await Promise.all([task.save(), user.save()]);
|
||||
|
||||
await cronWrapper(req, res);
|
||||
const taskRes = await Tasks.Task.findOne({ _id: task._id });
|
||||
expect(taskRes).to.exist;
|
||||
});
|
||||
|
||||
it('should clear todos older than 90 days for subscribed users', async () => {
|
||||
user.purchased.plan.customerId = 'subscribedId';
|
||||
user.purchased.plan.dateUpdated = moment('012013', 'MMYYYY');
|
||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||
|
||||
const task = generateTodo(user);
|
||||
task.dateCompleted = moment(new Date()).subtract({ days: 91 });
|
||||
task.completed = true;
|
||||
await task.save();
|
||||
await user.save();
|
||||
|
||||
await cronWrapper(req, res);
|
||||
const taskRes = await Tasks.Task.findOne({ _id: task._id });
|
||||
expect(taskRes).to.not.exist;
|
||||
});
|
||||
|
||||
it('should call next if user was not modified after cron', async () => {
|
||||
const hpBefore = user.stats.hp;
|
||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||
await user.save();
|
||||
|
||||
await cronWrapper(req, res);
|
||||
expect(hpBefore).to.equal(user.stats.hp);
|
||||
});
|
||||
|
||||
it('runs cron if previous cron was incomplete', async () => {
|
||||
user.lastCron = moment(new Date()).subtract({ days: 1 });
|
||||
user.auth.timestamps.loggedin = moment(new Date()).subtract({ days: 4 });
|
||||
const now = new Date();
|
||||
await user.save();
|
||||
|
||||
await cronWrapper(req, res);
|
||||
expect(moment(now).isSame(user.lastCron, 'day'));
|
||||
expect(moment(now).isSame(user.auth.timestamps.loggedin, 'day'));
|
||||
});
|
||||
|
||||
it('updates user.auth.timestamps.loggedin and lastCron', async () => {
|
||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||
const now = new Date();
|
||||
await user.save();
|
||||
|
||||
await cronWrapper(req, res);
|
||||
expect(moment(now).isSame(user.lastCron, 'day'));
|
||||
expect(moment(now).isSame(user.auth.timestamps.loggedin, 'day'));
|
||||
});
|
||||
|
||||
it('does damage for missing dailies', async () => {
|
||||
const hpBefore = user.stats.hp;
|
||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||
const daily = generateDaily(user);
|
||||
daily.startDate = moment(new Date()).subtract({ days: 2 });
|
||||
await daily.save();
|
||||
await user.save();
|
||||
|
||||
await cronWrapper(req, res);
|
||||
const updatedUser = await User.findOne({ _id: user._id });
|
||||
expect(updatedUser.stats.hp).to.be.lessThan(hpBefore);
|
||||
});
|
||||
|
||||
it('updates tasks', async () => {
|
||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||
const todo = generateTodo(user);
|
||||
const todoValueBefore = todo.value;
|
||||
await Promise.all([todo.save(), user.save()]);
|
||||
|
||||
await cronWrapper(req, res);
|
||||
const todoFound = await Tasks.Task.findOne({ _id: todo._id });
|
||||
expect(todoFound.value).to.be.lessThan(todoValueBefore);
|
||||
});
|
||||
|
||||
it('updates large number of tasks', async () => {
|
||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||
const todo = generateTodo(user);
|
||||
const todoValueBefore = todo.value;
|
||||
const start = new Date();
|
||||
const saves = [todo.save(), user.save()];
|
||||
for (let i = 0; i < 200; i += 1) {
|
||||
const newTodo = generateTodo(user);
|
||||
newTodo.value = i;
|
||||
saves.push(newTodo.save());
|
||||
}
|
||||
await Promise.all(saves);
|
||||
|
||||
await cronWrapper(req, res);
|
||||
const duration = new Date() - start;
|
||||
expect(duration).to.be.lessThan(1000);
|
||||
const todoFound = await Tasks.Task.findOne({ _id: todo._id });
|
||||
expect(moment(start).isSame(user.lastCron, 'day'));
|
||||
expect(moment(start).isSame(user.auth.timestamps.loggedin, 'day'));
|
||||
expect(todoFound.value).to.be.lessThan(todoValueBefore);
|
||||
});
|
||||
|
||||
it('fails entire cron if one task is failing', async () => {
|
||||
const lastCron = moment(new Date()).subtract({ days: 2 });
|
||||
user.lastCron = lastCron;
|
||||
const todo = generateTodo(user);
|
||||
const todoValueBefore = todo.value;
|
||||
const badTodo = generateTodo(user);
|
||||
badTodo.text = 'bad todo';
|
||||
badTodo.attribute = 'bad';
|
||||
await Promise.all([badTodo.save({ validateBeforeSave: false }), todo.save(), user.save()]);
|
||||
|
||||
try {
|
||||
await recoverCron(status, locals);
|
||||
throw new Error('no exception when user cannot be found');
|
||||
await cronWrapper(req, res);
|
||||
} catch (err) {
|
||||
expect(err.message).to.eql(`User ${locals.user._id} not found while recovering.`);
|
||||
expect(err).to.exist;
|
||||
}
|
||||
const todoFound = await Tasks.Task.findOne({ _id: todo._id });
|
||||
expect(moment(lastCron).isSame(user.lastCron, 'day'));
|
||||
expect(todoFound.value).to.be.equal(todoValueBefore);
|
||||
});
|
||||
|
||||
it('applies quest progress', async () => {
|
||||
const hpBefore = user.stats.hp;
|
||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||
const daily = generateDaily(user);
|
||||
daily.startDate = moment(new Date()).subtract({ days: 2 });
|
||||
await daily.save();
|
||||
|
||||
const questKey = 'dilatory';
|
||||
user.party.quest.key = questKey;
|
||||
|
||||
const party = new Group({
|
||||
type: 'party',
|
||||
name: generateUUID(),
|
||||
leader: user._id,
|
||||
});
|
||||
party.quest.members[user._id] = true;
|
||||
party.quest.key = questKey;
|
||||
await party.save();
|
||||
|
||||
user.party._id = party._id;
|
||||
await user.save();
|
||||
|
||||
party.startQuest(user);
|
||||
|
||||
await cronWrapper(req, res);
|
||||
const updatedUser = await User.findOne({ _id: user._id });
|
||||
expect(updatedUser.stats.hp).to.be.lessThan(hpBefore);
|
||||
});
|
||||
|
||||
it('cronSignature less than 5 minutes ago should error', async () => {
|
||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||
const now = new Date();
|
||||
await User.updateOne({
|
||||
_id: user._id,
|
||||
}, {
|
||||
$set: {
|
||||
_cronSignature: now.getTime() - CRON_TIMEOUT_WAIT + CRON_TIMEOUT_UNIT,
|
||||
},
|
||||
}).exec();
|
||||
await user.save();
|
||||
try {
|
||||
await cronWrapper(req, res);
|
||||
} catch (err) {
|
||||
expect(err).to.exist;
|
||||
}
|
||||
});
|
||||
|
||||
it('increases status.times count and reruns up to 4 times', async () => {
|
||||
execStub.returns(Promise.resolve({ _cronSignature: 'RUNNING_CRON' }));
|
||||
execStub.onCall(4).returns(Promise.resolve({ _cronSignature: 'NOT_RUNNING' }));
|
||||
it('cronSignature longer than an hour ago should allow cron', async () => {
|
||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||
const now = new Date();
|
||||
await User.updateOne({
|
||||
_id: user._id,
|
||||
}, {
|
||||
$set: {
|
||||
_cronSignature: now.getTime() - CRON_TIMEOUT_WAIT - CRON_TIMEOUT_UNIT,
|
||||
},
|
||||
}).exec();
|
||||
await user.save();
|
||||
|
||||
await recoverCron(status, locals);
|
||||
|
||||
expect(status.times).to.eql(4);
|
||||
expect(locals.user).to.eql({ _cronSignature: 'NOT_RUNNING' });
|
||||
await cronWrapper(req, res);
|
||||
expect(moment(now).isSame(user.auth.timestamps.loggedin, 'day'));
|
||||
expect(user._cronSignature).to.be.equal('NOT_RUNNING');
|
||||
});
|
||||
|
||||
it('throws an error if recoverCron runs 5 times', async () => {
|
||||
execStub.returns(Promise.resolve({ _cronSignature: 'RUNNING_CRON' }));
|
||||
it('cron should not run more than once', async () => {
|
||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||
await user.save();
|
||||
|
||||
try {
|
||||
await recoverCron(status, locals);
|
||||
throw new Error('no exception when recoverCron runs 5 times');
|
||||
} catch (err) {
|
||||
expect(status.times).to.eql(5);
|
||||
expect(err.message).to.eql(`Impossible to recover from cron for user ${locals.user._id}.`);
|
||||
}
|
||||
const result = await Promise.allSettled([
|
||||
cronWrapper(req, res),
|
||||
cronWrapper(req, res),
|
||||
new Promise((resolve, reject) => {
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
const runResult = await cronWrapper(req, res);
|
||||
if (runResult !== null) {
|
||||
reject(new Error('cron ran more than once'));
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
}, 200);
|
||||
}),
|
||||
]);
|
||||
|
||||
expect(result.filter(r => r.status === 'fulfilled')).to.have.lengthOf(2);
|
||||
expect(result.filter(r => r.status === 'rejected')).to.have.lengthOf(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -171,23 +171,23 @@ describe('emails', () => {
|
||||
expect(got.post).not.to.be.called;
|
||||
});
|
||||
|
||||
it('throws error when mail target is only a string', () => {
|
||||
it('throws error when mail target is only a string', async () => {
|
||||
const emailType = 'an email type';
|
||||
const mailingInfo = 'my email';
|
||||
|
||||
expect(sendTxn(mailingInfo, emailType)).to.throw;
|
||||
await expect(sendTxn(mailingInfo, emailType)).to.be.rejectedWith('Argument Error mailingInfoArray: does not contain email or _id');
|
||||
});
|
||||
|
||||
it('throws error when mail target has no _id or email', () => {
|
||||
it('throws error when mail target has no _id or email', async () => {
|
||||
const emailType = 'an email type';
|
||||
const mailingInfo = {
|
||||
|
||||
};
|
||||
|
||||
expect(sendTxn(mailingInfo, emailType)).to.throw;
|
||||
await expect(sendTxn(mailingInfo, emailType)).to.be.rejectedWith('Argument Error mailingInfoArray: does not contain email or _id');
|
||||
});
|
||||
|
||||
it('throws error when variables not an array', () => {
|
||||
it('throws error when variables not an array', async () => {
|
||||
const emailType = 'an email type';
|
||||
const mailingInfo = {
|
||||
name: 'my name',
|
||||
@@ -195,9 +195,10 @@ describe('emails', () => {
|
||||
};
|
||||
const variables = {};
|
||||
|
||||
expect(sendTxn(mailingInfo, emailType, variables)).to.throw;
|
||||
await expect(sendTxn(mailingInfo, emailType, variables)).to.be.rejectedWith('Argument Error variables: is not an array');
|
||||
});
|
||||
it('throws error when variables array not contain name/content', () => {
|
||||
|
||||
it('throws error when variables array not contain name/content', async () => {
|
||||
const emailType = 'an email type';
|
||||
const mailingInfo = {
|
||||
name: 'my name',
|
||||
@@ -209,8 +210,9 @@ describe('emails', () => {
|
||||
},
|
||||
];
|
||||
|
||||
expect(sendTxn(mailingInfo, emailType, variables)).to.throw;
|
||||
await expect(sendTxn(mailingInfo, emailType, variables)).to.be.rejectedWith('Argument Error variables: does not contain name or content');
|
||||
});
|
||||
|
||||
it('throws no error when variables array contain name but no content', () => {
|
||||
const emailType = 'an email type';
|
||||
const mailingInfo = {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import os from 'os';
|
||||
import nconf from 'nconf';
|
||||
import requireAgain from 'require-again';
|
||||
|
||||
const pathToMongoLib = '../../../../website/server/libs/mongodb';
|
||||
@@ -29,22 +28,4 @@ describe('mongodb', () => {
|
||||
expect(string).to.equal('mongodb://hostname:3030');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDefaultConnectionOptions', () => {
|
||||
it('returns development config when IS_PROD is false', () => {
|
||||
sandbox.stub(nconf, 'get').withArgs('IS_PROD').returns(false);
|
||||
const mongoLibOverride = requireAgain(pathToMongoLib);
|
||||
|
||||
const options = mongoLibOverride.getDefaultConnectionOptions();
|
||||
expect(options).to.have.all.keys(['useNewUrlParser', 'useUnifiedTopology']);
|
||||
});
|
||||
|
||||
it('returns production config when IS_PROD is true', () => {
|
||||
sandbox.stub(nconf, 'get').withArgs('IS_PROD').returns(true);
|
||||
const mongoLibOverride = requireAgain(pathToMongoLib);
|
||||
|
||||
const options = mongoLibOverride.getDefaultConnectionOptions();
|
||||
expect(options).to.have.all.keys(['useNewUrlParser', 'useUnifiedTopology']);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,332 +0,0 @@
|
||||
import moment from 'moment';
|
||||
import { v4 as generateUUID } from 'uuid';
|
||||
import {
|
||||
generateRes,
|
||||
generateReq,
|
||||
generateTodo,
|
||||
generateDaily,
|
||||
} from '../../../helpers/api-unit.helper';
|
||||
import cronMiddleware from '../../../../website/server/middlewares/cron';
|
||||
import { model as User } from '../../../../website/server/models/user';
|
||||
import { model as Group } from '../../../../website/server/models/group';
|
||||
import * as Tasks from '../../../../website/server/models/task';
|
||||
import * as analyticsService from '../../../../website/server/libs/analyticsService';
|
||||
import * as cronLib from '../../../../website/server/libs/cron';
|
||||
|
||||
const CRON_TIMEOUT_WAIT = new Date(60 * 60 * 1000).getTime();
|
||||
const CRON_TIMEOUT_UNIT = new Date(60 * 1000).getTime();
|
||||
|
||||
describe('cron middleware', () => {
|
||||
let res; let
|
||||
req;
|
||||
let user;
|
||||
|
||||
beforeEach(async () => {
|
||||
res = generateRes();
|
||||
req = generateReq();
|
||||
user = await res.locals.user.save();
|
||||
res.analytics = analyticsService;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
it('calls next when user is not attached', done => {
|
||||
res.locals.user = null;
|
||||
cronMiddleware(req, res, done);
|
||||
});
|
||||
|
||||
it('calls next when days have not been missed', done => {
|
||||
cronMiddleware(req, res, done);
|
||||
});
|
||||
|
||||
it('should clear todos older than 30 days for free users', async () => {
|
||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||
const task = generateTodo(user);
|
||||
task.dateCompleted = moment(new Date()).subtract({ days: 31 });
|
||||
task.completed = true;
|
||||
await task.save();
|
||||
await user.save();
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
cronMiddleware(req, res, err => {
|
||||
if (err) return reject(err);
|
||||
|
||||
Tasks.Task.findOne({ _id: task }).then(foundTask => {
|
||||
expect(foundTask).to.not.exist;
|
||||
resolve();
|
||||
});
|
||||
|
||||
return null;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should not clear todos older than 30 days for subscribed users', async () => {
|
||||
user.purchased.plan.customerId = 'subscribedId';
|
||||
user.purchased.plan.dateUpdated = moment('012013', 'MMYYYY');
|
||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||
const task = generateTodo(user);
|
||||
task.dateCompleted = moment(new Date()).subtract({ days: 31 });
|
||||
task.completed = true;
|
||||
await task.save();
|
||||
await user.save();
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
cronMiddleware(req, res, err => {
|
||||
if (err) return reject(err);
|
||||
Tasks.Task.findOne({ _id: task }).then(foundTask => {
|
||||
expect(foundTask).to.exist;
|
||||
return resolve();
|
||||
});
|
||||
return null;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should clear todos older than 90 days for subscribed users', async () => {
|
||||
user.purchased.plan.customerId = 'subscribedId';
|
||||
user.purchased.plan.dateUpdated = moment('012013', 'MMYYYY');
|
||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||
|
||||
const task = generateTodo(user);
|
||||
task.dateCompleted = moment(new Date()).subtract({ days: 91 });
|
||||
task.completed = true;
|
||||
await task.save();
|
||||
await user.save();
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
cronMiddleware(req, res, err => {
|
||||
if (err) return reject(err);
|
||||
Tasks.Task.findOne({ _id: task }).then(foundTask => {
|
||||
expect(foundTask).to.not.exist;
|
||||
return resolve();
|
||||
});
|
||||
return null;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should call next if user was not modified after cron', async () => {
|
||||
const hpBefore = user.stats.hp;
|
||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||
await user.save();
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
cronMiddleware(req, res, err => {
|
||||
if (err) return reject(err);
|
||||
expect(hpBefore).to.equal(user.stats.hp);
|
||||
return resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('runs cron if previous cron was incomplete', async () => {
|
||||
user.lastCron = moment(new Date()).subtract({ days: 1 });
|
||||
user.auth.timestamps.loggedin = moment(new Date()).subtract({ days: 4 });
|
||||
const now = new Date();
|
||||
await user.save();
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
cronMiddleware(req, res, err => {
|
||||
if (err) return reject(err);
|
||||
expect(moment(now).isSame(user.lastCron, 'day'));
|
||||
expect(moment(now).isSame(user.auth.timestamps.loggedin, 'day'));
|
||||
return resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('updates user.auth.timestamps.loggedin and lastCron', async () => {
|
||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||
const now = new Date();
|
||||
await user.save();
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
cronMiddleware(req, res, err => {
|
||||
if (err) return reject(err);
|
||||
expect(moment(now).isSame(user.lastCron, 'day'));
|
||||
expect(moment(now).isSame(user.auth.timestamps.loggedin, 'day'));
|
||||
return resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('does damage for missing dailies', async () => {
|
||||
const hpBefore = user.stats.hp;
|
||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||
const daily = generateDaily(user);
|
||||
daily.startDate = moment(new Date()).subtract({ days: 2 });
|
||||
await daily.save();
|
||||
await user.save();
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
cronMiddleware(req, res, err => {
|
||||
if (err) return reject(err);
|
||||
return User.findOne({ _id: user._id }).then(updatedUser => {
|
||||
expect(updatedUser.stats.hp).to.be.lessThan(hpBefore);
|
||||
return resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('updates tasks', async () => {
|
||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||
const todo = generateTodo(user);
|
||||
const todoValueBefore = todo.value;
|
||||
await Promise.all([todo.save(), user.save()]);
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
cronMiddleware(req, res, err => {
|
||||
if (err) return reject(err);
|
||||
return Tasks.Task.findOne({ _id: todo._id }).then(todoFound => {
|
||||
expect(todoFound.value).to.be.lessThan(todoValueBefore);
|
||||
return resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('applies quest progress', async () => {
|
||||
const hpBefore = user.stats.hp;
|
||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||
const daily = generateDaily(user);
|
||||
daily.startDate = moment(new Date()).subtract({ days: 2 });
|
||||
await daily.save();
|
||||
|
||||
const questKey = 'dilatory';
|
||||
user.party.quest.key = questKey;
|
||||
|
||||
const party = new Group({
|
||||
type: 'party',
|
||||
name: generateUUID(),
|
||||
leader: user._id,
|
||||
});
|
||||
party.quest.members[user._id] = true;
|
||||
party.quest.key = questKey;
|
||||
await party.save();
|
||||
|
||||
user.party._id = party._id;
|
||||
await user.save();
|
||||
|
||||
party.startQuest(user);
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
cronMiddleware(req, res, err => {
|
||||
if (err) return reject(err);
|
||||
return User.findOne({ _id: user._id }).then(updatedUser => {
|
||||
expect(updatedUser.stats.hp).to.be.lessThan(hpBefore);
|
||||
return resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('recovers from failed cron and does not error when user is already cronning', async () => {
|
||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||
await user.save();
|
||||
|
||||
const updatedUser = user.toObject();
|
||||
updatedUser.matchedCount = 0;
|
||||
|
||||
sandbox.spy(cronLib, 'recoverCron');
|
||||
|
||||
sandbox.stub(User, 'updateOne')
|
||||
.withArgs({
|
||||
_id: user._id,
|
||||
$or: [
|
||||
{ _cronSignature: 'NOT_RUNNING' },
|
||||
{ _cronSignature: { $lt: sinon.match.number } },
|
||||
],
|
||||
})
|
||||
.returns({
|
||||
exec () {
|
||||
return Promise.resolve(updatedUser);
|
||||
},
|
||||
});
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
cronMiddleware(req, res, err => {
|
||||
if (err) return reject(err);
|
||||
expect(cronLib.recoverCron).to.be.calledOnce;
|
||||
|
||||
return resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('cronSignature less than an hour ago should error', async () => {
|
||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||
const now = new Date();
|
||||
await User.updateOne({
|
||||
_id: user._id,
|
||||
}, {
|
||||
$set: {
|
||||
_cronSignature: now.getTime() - CRON_TIMEOUT_WAIT + CRON_TIMEOUT_UNIT,
|
||||
},
|
||||
}).exec();
|
||||
await user.save();
|
||||
const expectedErrMessage = `Impossible to recover from cron for user ${user._id}.`;
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
cronMiddleware(req, res, err => {
|
||||
if (!err) return reject(new Error('Cron should have failed.'));
|
||||
expect(err.message).to.be.equal(expectedErrMessage);
|
||||
return resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('cronSignature longer than an hour ago should allow cron', async () => {
|
||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||
const now = new Date();
|
||||
await User.updateOne({
|
||||
_id: user._id,
|
||||
}, {
|
||||
$set: {
|
||||
_cronSignature: now.getTime() - CRON_TIMEOUT_WAIT - CRON_TIMEOUT_UNIT,
|
||||
},
|
||||
}).exec();
|
||||
await user.save();
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
cronMiddleware(req, res, err => {
|
||||
if (err) return reject(err);
|
||||
expect(moment(now).isSame(user.auth.timestamps.loggedin, 'day'));
|
||||
expect(user._cronSignature).to.be.equal('NOT_RUNNING');
|
||||
return resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('cron should not run more than once', async () => {
|
||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||
await user.save();
|
||||
|
||||
sandbox.spy(cronLib, 'cron');
|
||||
|
||||
await Promise.all([new Promise((resolve, reject) => {
|
||||
cronMiddleware(req, res, err => {
|
||||
if (err) return reject(err);
|
||||
return resolve();
|
||||
});
|
||||
}), new Promise((resolve, reject) => {
|
||||
cronMiddleware(req, res, err => {
|
||||
if (err) return reject(err);
|
||||
return resolve();
|
||||
});
|
||||
}), new Promise((resolve, reject) => {
|
||||
setTimeout(() => {
|
||||
cronMiddleware(req, res, err => {
|
||||
if (err) return reject(err);
|
||||
return resolve();
|
||||
});
|
||||
}, 400);
|
||||
}),
|
||||
]);
|
||||
|
||||
expect(cronLib.cron).to.be.calledOnce;
|
||||
});
|
||||
});
|
||||
@@ -10,6 +10,7 @@ describe('GET /heroes/:heroId', () => {
|
||||
const heroFields = [
|
||||
'_id', 'id', 'auth', 'balance', 'contributor', 'flags', 'items',
|
||||
'lastCron', 'party', 'preferences', 'profile', 'purchased', 'secret', 'achievements',
|
||||
'stats',
|
||||
];
|
||||
|
||||
before(async () => {
|
||||
|
||||
@@ -11,6 +11,7 @@ describe('PUT /heroes/:heroId', () => {
|
||||
const heroFields = [
|
||||
'_id', 'auth', 'balance', 'contributor', 'flags', 'items', 'lastCron',
|
||||
'party', 'preferences', 'profile', 'purchased', 'secret', 'permissions', 'achievements',
|
||||
'stats',
|
||||
];
|
||||
|
||||
before(async () => {
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import {
|
||||
generateUser,
|
||||
translate as t,
|
||||
} from '../../../../helpers/api-integration/v3';
|
||||
import common from '../../../../../website/common';
|
||||
|
||||
describe('GET /members/username/:username', () => {
|
||||
let user;
|
||||
|
||||
before(async () => {
|
||||
user = await generateUser();
|
||||
});
|
||||
|
||||
it('validates req.params.username', async () => {
|
||||
await expect(user.get('/members/username/')).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('invalidReqParams'),
|
||||
});
|
||||
});
|
||||
|
||||
it('returns a member\'s public data only', async () => {
|
||||
// make sure user has all the fields that can be returned by the getMember call
|
||||
const member = await generateUser({
|
||||
contributor: { level: 1 },
|
||||
backer: { tier: 3 },
|
||||
preferences: {
|
||||
costume: false,
|
||||
background: 'volcano',
|
||||
},
|
||||
secret: {
|
||||
text: 'Clark Kent',
|
||||
},
|
||||
});
|
||||
const memberRes = await user.get(`/members/username/${member.auth.local.username}`);
|
||||
expect(memberRes).to.have.all.keys([ // works as: object has all and only these keys
|
||||
'_id', 'id', 'preferences', 'profile', 'stats', 'achievements', 'party',
|
||||
'backer', 'contributor', 'auth', 'items', 'inbox', 'loginIncentives', 'flags',
|
||||
]);
|
||||
expect(Object.keys(memberRes.auth)).to.eql(['local', 'timestamps']);
|
||||
expect(Object.keys(memberRes.preferences).sort()).to.eql([
|
||||
'size', 'hair', 'skin', 'shirt',
|
||||
'chair', 'costume', 'sleep', 'background', 'tasks', 'disableClasses',
|
||||
].sort());
|
||||
|
||||
expect(memberRes.stats.maxMP).to.exist;
|
||||
expect(memberRes.stats.maxHealth).to.equal(common.maxHealth);
|
||||
expect(memberRes.stats.toNextLevel).to.equal(common.tnl(memberRes.stats.lvl));
|
||||
expect(memberRes.inbox.optOut).to.exist;
|
||||
expect(memberRes.inbox.canReceive).to.exist;
|
||||
expect(memberRes.inbox.messages).to.not.exist;
|
||||
expect(memberRes.secret).to.not.exist;
|
||||
|
||||
expect(memberRes.blocks).to.not.exist;
|
||||
});
|
||||
});
|
||||
@@ -64,6 +64,18 @@ describe('POST /user/auth/social', () => {
|
||||
await expect(getProperty('users', response.id, 'profile.name')).to.eventually.equal('a google user');
|
||||
});
|
||||
|
||||
it('fails if allowRegister is false and user does not exist', async () => {
|
||||
await expect(api.post(endpoint, {
|
||||
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
|
||||
network,
|
||||
allowRegister: false,
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 404,
|
||||
error: 'NotFound',
|
||||
message: t('userNotFound'),
|
||||
});
|
||||
});
|
||||
|
||||
it('logs an existing user in', async () => {
|
||||
const registerResponse = await api.post(endpoint, {
|
||||
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
|
||||
@@ -131,6 +143,36 @@ describe('POST /user/auth/social', () => {
|
||||
expect(response.newUser).to.be.false;
|
||||
});
|
||||
|
||||
it('logs an existing user into their social account if allowRegister is false', async () => {
|
||||
const registerResponse = await api.post(endpoint, {
|
||||
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
|
||||
network,
|
||||
});
|
||||
expect(registerResponse.newUser).to.be.true;
|
||||
// This is important for existing accounts before the new social handling
|
||||
passport._strategies.google.userProfile.restore();
|
||||
const expectedResult = {
|
||||
id: randomGoogleId,
|
||||
displayName: 'a google user',
|
||||
emails: [
|
||||
{ value: user.auth.local.email },
|
||||
],
|
||||
};
|
||||
sandbox.stub(passport._strategies.google, 'userProfile').yields(null, expectedResult);
|
||||
|
||||
const response = await api.post(endpoint, {
|
||||
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
|
||||
network,
|
||||
allowRegister: false,
|
||||
});
|
||||
|
||||
expect(response.apiToken).to.eql(registerResponse.apiToken);
|
||||
expect(response.id).to.eql(registerResponse.id);
|
||||
expect(response.apiToken).not.to.eql(user.apiToken);
|
||||
expect(response.id).not.to.eql(user._id);
|
||||
expect(response.newUser).to.be.false;
|
||||
});
|
||||
|
||||
it('add social auth to an existing user', async () => {
|
||||
const response = await user.post(endpoint, {
|
||||
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
import find from 'lodash/find';
|
||||
import {
|
||||
generateUser,
|
||||
translate as t,
|
||||
} from '../../../helpers/api-integration/v4';
|
||||
|
||||
/**
|
||||
* Checks the messages array if the uniqueMessageId has the like flag
|
||||
* @param {InboxMessage[]} messages
|
||||
* @param {String} uniqueMessageId
|
||||
* @param {String} userId
|
||||
* @param {Boolean} likeStatus
|
||||
*/
|
||||
function expectMessagesLikeStatus (messages, uniqueMessageId, userId, likeStatus) {
|
||||
const messageToCheck = find(messages, { uniqueMessageId });
|
||||
|
||||
expect(messageToCheck.likes[userId]).to.equal(likeStatus);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line mocha/no-exclusive-tests
|
||||
describe('POST /inbox/like-private-message/:messageId', () => {
|
||||
let userToSendMessage;
|
||||
const getLikeUrl = messageId => `/inbox/like-private-message/${messageId}`;
|
||||
|
||||
before(async () => {
|
||||
userToSendMessage = await generateUser();
|
||||
});
|
||||
|
||||
it('returns an error when private message is not found', async () => {
|
||||
await expect(userToSendMessage.post(getLikeUrl('some-unknown-id')))
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 404,
|
||||
error: 'NotFound',
|
||||
message: t('messageGroupChatNotFound'),
|
||||
});
|
||||
});
|
||||
|
||||
it('likes a message', async () => {
|
||||
const receiver = await generateUser();
|
||||
|
||||
const sentMessageResult = await userToSendMessage.post('/members/send-private-message', {
|
||||
message: 'some message :)',
|
||||
toUserId: receiver._id,
|
||||
});
|
||||
|
||||
const { uniqueMessageId } = sentMessageResult.message;
|
||||
|
||||
const likeResult = await receiver.post(getLikeUrl(uniqueMessageId));
|
||||
expect(likeResult.likes[receiver._id]).to.equal(true);
|
||||
|
||||
const senderMessages = await userToSendMessage.get('/inbox/messages');
|
||||
|
||||
expectMessagesLikeStatus(senderMessages, uniqueMessageId, receiver._id, true);
|
||||
|
||||
const receiversMessages = await receiver.get('/inbox/messages');
|
||||
|
||||
expectMessagesLikeStatus(receiversMessages, uniqueMessageId, receiver._id, true);
|
||||
});
|
||||
|
||||
it('allows a user to like their own private message', async () => {
|
||||
const receiver = await generateUser();
|
||||
|
||||
const sentMessageResult = await userToSendMessage.post('/members/send-private-message', {
|
||||
message: 'some message :)',
|
||||
toUserId: receiver._id,
|
||||
});
|
||||
|
||||
const { uniqueMessageId } = sentMessageResult.message;
|
||||
|
||||
const likeResult = await userToSendMessage.post(getLikeUrl(uniqueMessageId));
|
||||
expect(likeResult.likes[userToSendMessage._id]).to.equal(true);
|
||||
|
||||
const messages = await userToSendMessage.get('/inbox/messages');
|
||||
expectMessagesLikeStatus(messages, uniqueMessageId, userToSendMessage._id, true);
|
||||
|
||||
const receiversMessages = await receiver.get('/inbox/messages');
|
||||
|
||||
expectMessagesLikeStatus(receiversMessages, uniqueMessageId, userToSendMessage._id, true);
|
||||
});
|
||||
|
||||
it('unlikes a message', async () => {
|
||||
const receiver = await generateUser();
|
||||
|
||||
const sentMessageResult = await userToSendMessage.post('/members/send-private-message', {
|
||||
message: 'some message :)',
|
||||
toUserId: receiver._id,
|
||||
});
|
||||
|
||||
const { uniqueMessageId } = sentMessageResult.message;
|
||||
|
||||
const likeResult = await receiver.post(getLikeUrl(uniqueMessageId));
|
||||
|
||||
expect(likeResult.likes[receiver._id]).to.equal(true);
|
||||
|
||||
const unlikeResult = await receiver.post(getLikeUrl(uniqueMessageId));
|
||||
|
||||
expect(unlikeResult.likes[receiver._id]).to.equal(false);
|
||||
|
||||
const messages = await userToSendMessage.get('/inbox/messages');
|
||||
|
||||
const messageToCheck = find(messages, { id: sentMessageResult.message.id });
|
||||
expect(messageToCheck.likes[receiver._id]).to.equal(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,56 @@
|
||||
import {
|
||||
translate as t,
|
||||
requester,
|
||||
generateUser,
|
||||
} from '../../../../helpers/api-integration/v4';
|
||||
|
||||
const ENDPOINT = '/user/auth/check-email';
|
||||
|
||||
describe('POST /user/auth/check-email', () => {
|
||||
const email = 'SOmE-nEw-emAIl_2@example.net';
|
||||
let api;
|
||||
|
||||
beforeEach(async () => {
|
||||
api = requester();
|
||||
});
|
||||
|
||||
it('returns email if it is not used yet', async () => {
|
||||
const response = await api.post(ENDPOINT, {
|
||||
email,
|
||||
});
|
||||
expect(response.email).to.eql(email);
|
||||
});
|
||||
|
||||
it('rejects if email is not provided', async () => {
|
||||
await expect(api.post(ENDPOINT, {
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: 'Invalid request parameters.',
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects if email is already taken', async () => {
|
||||
const user = await generateUser();
|
||||
|
||||
await expect(api.post(ENDPOINT, {
|
||||
email: user.auth.local.email,
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('emailTaken'),
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects if casing is different', async () => {
|
||||
const user = await generateUser();
|
||||
|
||||
await expect(api.post(ENDPOINT, {
|
||||
email: user.auth.local.email.toUpperCase(),
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('emailTaken'),
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -10,7 +10,7 @@ describe('events', () => {
|
||||
});
|
||||
|
||||
it('returns empty array when no events are active', () => {
|
||||
clock = sinon.useFakeTimers(new Date('2024-01-08'));
|
||||
clock = sinon.useFakeTimers(new Date('2024-01-11'));
|
||||
const events = getRepeatingEvents();
|
||||
expect(events).to.be.empty;
|
||||
});
|
||||
|
||||
@@ -190,7 +190,7 @@ describe('Content Schedule', () => {
|
||||
const date = new Date('2024-04-15');
|
||||
const matchers = getAllScheduleMatchingGroups(date);
|
||||
expect(matchers.premiumHatchingPotions).to.exist;
|
||||
expect(matchers.premiumHatchingPotions.items.length).to.equal(5);
|
||||
expect(matchers.premiumHatchingPotions.items.length).to.equal(6);
|
||||
expect(matchers.premiumHatchingPotions.items.indexOf('Veggie')).to.not.equal(-1);
|
||||
expect(matchers.premiumHatchingPotions.items.indexOf('Porcelain')).to.not.equal(-1);
|
||||
});
|
||||
|
||||
@@ -74,15 +74,10 @@ export async function getDocument (collectionName, doc) {
|
||||
}
|
||||
|
||||
before(done => {
|
||||
mongoose.connection.on('open', err => {
|
||||
if (err) return done(err);
|
||||
return resetHabiticaDB()
|
||||
.then(() => {
|
||||
done();
|
||||
})
|
||||
.catch(error => {
|
||||
throw error;
|
||||
});
|
||||
mongoose.connection.once('open', async err => {
|
||||
if (err) throw err;
|
||||
await resetHabiticaDB();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ module.exports = {
|
||||
extends: [
|
||||
'habitrpg/lib/vue',
|
||||
],
|
||||
ignorePatterns: ['dist/', 'node_modules/'],
|
||||
ignorePatterns: ['dist/', 'node_modules/', '*.d.ts'],
|
||||
rules: {
|
||||
'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',
|
||||
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
|
||||
|
||||
@@ -22,7 +22,8 @@
|
||||
height: 219px;
|
||||
}
|
||||
|
||||
.Pet_HatchingPotion_Dessert, .Pet_HatchingPotion_Veggie, .Pet_HatchingPotion_Windup, .Pet_HatchingPotion_VirtualPet, .Pet_HatchingPotion_Fungi {
|
||||
.Pet_HatchingPotion_Dessert, .Pet_HatchingPotion_Veggie, .Pet_HatchingPotion_Windup,
|
||||
.Pet_HatchingPotion_VirtualPet, .Pet_HatchingPotion_Fungi, .Pet_HatchingPotion_Cryptid {
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
@@ -47,6 +48,10 @@
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet_HatchingPotion_Fungi.gif") no-repeat;
|
||||
}
|
||||
|
||||
.Pet_HatchingPotion_Cryptid {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet_HatchingPotion_Cryptid.gif") no-repeat;
|
||||
}
|
||||
|
||||
.Gems {
|
||||
display:inline-block;
|
||||
margin-right:5px;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -19,7 +19,7 @@
|
||||
top: -16px !important;
|
||||
}
|
||||
|
||||
$foolPets: Veggie, Dessert, VirtualPet, TeaShop, Fungi;
|
||||
$foolPets: Veggie, Dessert, VirtualPet, TeaShop, Fungi, Cryptid;
|
||||
|
||||
@each $foolPet in $foolPets {
|
||||
.Pet.Pet-FlyingPig-#{$foolPet} {
|
||||
|
||||
@@ -101,8 +101,7 @@
|
||||
|
||||
.btn-secondary,
|
||||
.dropdown > .btn-secondary.dropdown-toggle:not(.btn-success),
|
||||
.show > .btn-secondary.dropdown-toggle:not(.btn-success)
|
||||
{
|
||||
.show > .btn-secondary.dropdown-toggle:not(.btn-success) {
|
||||
background: $white;
|
||||
border: 2px solid transparent;
|
||||
color: $gray-50;
|
||||
@@ -298,6 +297,16 @@
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.btn-flat,
|
||||
.dropdown > .btn-flat.dropdown-toggle:not(.btn-success),
|
||||
.show > .btn-flat.dropdown-toggle:not(.btn-success) {
|
||||
&.with-icon {
|
||||
.svg-icon.color {
|
||||
color: var(--icon-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.btn-cancel {
|
||||
color: $blue-10;
|
||||
}
|
||||
|
||||
@@ -38,7 +38,12 @@
|
||||
border-radius: 2px;
|
||||
box-shadow: 0 3px 6px 0 rgba($black, 0.16), 0 3px 6px 0 rgba($black, 0.24);
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.no-min-width {
|
||||
.dropdown-menu {
|
||||
min-width: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
// shared dropdown-item styles
|
||||
@@ -54,6 +59,8 @@
|
||||
color: $gray-50 !important;
|
||||
cursor: pointer;
|
||||
|
||||
--dropdown-item-hover-icon-color: #{$gray-200};
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
background-color: inherit;
|
||||
@@ -88,7 +95,7 @@
|
||||
|
||||
&:not(:hover) {
|
||||
.with-icon .svg-icon {
|
||||
color: $gray-200;
|
||||
color: var(dropdown-item-hover-icon-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -151,7 +158,7 @@
|
||||
|
||||
// selectList.vue items sizing
|
||||
.selectListItem .dropdown-item {
|
||||
padding: 0.25rem 0.75rem;
|
||||
padding: 0.25rem 1rem 0.25rem 0.75rem;
|
||||
height: 32px;
|
||||
|
||||
&:active, &:hover, &:focus, &.active {
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<div class="modal-body">
|
||||
<div class="row">
|
||||
<div class="col-6 offset-3">
|
||||
<div class="shop_armoire"></div>
|
||||
<Sprite image-name="shop_armoire" />
|
||||
<p>{{ $t('armoireLastItem') }}</p>
|
||||
<p>{{ $t('armoireNotesEmpty') }}</p>
|
||||
</div>
|
||||
@@ -34,7 +34,12 @@
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import Sprite from '@/components/ui/sprite';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Sprite,
|
||||
},
|
||||
methods: {
|
||||
close () {
|
||||
this.$root.$emit('bv::hide::modal', 'armoire-empty');
|
||||
|
||||
@@ -95,7 +95,11 @@
|
||||
@click="clickDisableClasses(); close();"
|
||||
>{{ $t('optOutOfClasses') }}</span>
|
||||
</div>
|
||||
<span class="opt-out-description">{{ $t('optOutOfClassesText') }}</span>
|
||||
<div
|
||||
v-once
|
||||
class="opt-out-description"
|
||||
v-html="$t('optOutOfClassesText')"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
></span>
|
||||
</div>
|
||||
|
||||
<div :class="questClass"></div>
|
||||
<Sprite :image-name="questClass" />
|
||||
</section>
|
||||
<!-- @TODO: Keep this? .checkboxinput(type='checkbox', v-model=
|
||||
'user.preferences.suppressModals.levelUp', @change='changeLevelupSuppress()')
|
||||
@@ -150,15 +150,12 @@ label(style='display:inline-block') {{ $t('dontShowAgain') }}
|
||||
section.greyed {
|
||||
padding-bottom: 17px
|
||||
}
|
||||
|
||||
.scroll {
|
||||
margin: -11px auto 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import Avatar from '../avatar';
|
||||
import Sprite from '@/components/ui/sprite';
|
||||
import { mapState } from '@/libs/store';
|
||||
import starGroup from '@/assets/svg/star-group.svg';
|
||||
import sparkles from '@/assets/svg/sparkles-left.svg';
|
||||
@@ -173,6 +170,7 @@ const levelQuests = {
|
||||
export default {
|
||||
components: {
|
||||
Avatar,
|
||||
Sprite,
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
@@ -191,7 +189,9 @@ export default {
|
||||
return this.user.stats.lvl in levelQuests;
|
||||
},
|
||||
questClass () {
|
||||
return `scroll inventory_quest_scroll_${levelQuests[this.user.stats.lvl]}`;
|
||||
const questKey = levelQuests[this.user.stats.lvl];
|
||||
if (questKey) return `inventory_quest_scroll_${questKey}`;
|
||||
return '';
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
<p v-html="$t('moreGearAchievements')"></p>
|
||||
<br>
|
||||
</div>
|
||||
<div class="shop_armoire"></div>
|
||||
<Sprite image-name="shop_armoire" />
|
||||
<p v-html="$t('armoireUnlocked')"></p>
|
||||
<br>
|
||||
<button
|
||||
@@ -87,11 +87,13 @@
|
||||
import achievementFooter from './achievementFooter';
|
||||
import achievementAvatar from './achievementAvatar';
|
||||
import { mapState } from '@/libs/store';
|
||||
import Sprite from '@/components/ui/sprite.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
achievementFooter,
|
||||
achievementAvatar,
|
||||
Sprite,
|
||||
},
|
||||
computed: {
|
||||
...mapState({ user: 'user.data' }),
|
||||
|
||||
@@ -92,8 +92,6 @@ export default {
|
||||
params: { userIdentifier },
|
||||
}).catch(failure => {
|
||||
if (isNavigationFailure(failure, NavigationFailureType.duplicated)) {
|
||||
// the admin has requested that the same user be displayed again so reload the page
|
||||
// (e.g., if they changed their mind about changes they were making)
|
||||
this.$router.go();
|
||||
}
|
||||
});
|
||||
@@ -101,14 +99,16 @@ export default {
|
||||
|
||||
async loadUser (userIdentifier) {
|
||||
const id = userIdentifier || this.user._id;
|
||||
|
||||
this.$router.push({
|
||||
if (this.$router.currentRoute.name === 'adminPanelUser') {
|
||||
await this.$router.push({
|
||||
name: 'adminPanel',
|
||||
});
|
||||
}
|
||||
await this.$router.push({
|
||||
name: 'adminPanelUser',
|
||||
params: { userIdentifier: id },
|
||||
}).catch(failure => {
|
||||
if (isNavigationFailure(failure, NavigationFailureType.duplicated)) {
|
||||
// the admin has requested that the same user be displayed again so reload the page
|
||||
// (e.g., if they changed their mind about changes they were making)
|
||||
this.$router.go();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
import VueRouter from 'vue-router';
|
||||
|
||||
const { isNavigationFailure, NavigationFailureType } = VueRouter;
|
||||
|
||||
export default {
|
||||
methods: {
|
||||
async saveHero ({ hero, msg = 'User', clearData }) {
|
||||
async saveHero ({
|
||||
hero,
|
||||
msg = 'User',
|
||||
clearData,
|
||||
reloadData,
|
||||
}) {
|
||||
await this.$store.dispatch('hall:updateHero', { heroDetails: hero });
|
||||
await this.$store.dispatch('snackbars:add', {
|
||||
title: '',
|
||||
@@ -14,6 +23,20 @@ export default {
|
||||
// The admin should re-fetch the data if they need to keep working on that user.
|
||||
this.$emit('clear-data');
|
||||
this.$router.push({ name: 'adminPanel' });
|
||||
} else if (reloadData) {
|
||||
if (this.$router.currentRoute.name === 'adminPanelUser') {
|
||||
await this.$router.push({
|
||||
name: 'adminPanel',
|
||||
});
|
||||
}
|
||||
await this.$router.push({
|
||||
name: 'adminPanelUser',
|
||||
params: { userIdentifier: hero._id },
|
||||
}).catch(failure => {
|
||||
if (isNavigationFailure(failure, NavigationFailureType.duplicated)) {
|
||||
this.$router.go();
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@@ -7,7 +7,11 @@
|
||||
>
|
||||
Could not find any matching users.
|
||||
</div>
|
||||
<loading-spinner class="mx-auto mb-2" dark-color="true" v-if="isSearching" />
|
||||
<loading-spinner
|
||||
v-if="isSearching"
|
||||
class="mx-auto mb-2"
|
||||
dark-color="true"
|
||||
/>
|
||||
<div
|
||||
v-if="users.length > 0"
|
||||
class="list-group"
|
||||
@@ -59,6 +63,10 @@ export default {
|
||||
components: {
|
||||
LoadingSpinner,
|
||||
},
|
||||
beforeRouteUpdate (to, from, next) {
|
||||
this.userIdentifier = to.params.userIdentifier;
|
||||
next();
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
userIdentifier: '',
|
||||
@@ -70,10 +78,6 @@ export default {
|
||||
computed: {
|
||||
...mapState({ user: 'user.data' }),
|
||||
},
|
||||
beforeRouteUpdate (to, from, next) {
|
||||
this.userIdentifier = to.params.userIdentifier;
|
||||
next();
|
||||
},
|
||||
watch: {
|
||||
userIdentifier () {
|
||||
this.isSearching = true;
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
<li
|
||||
v-for="item in achievements"
|
||||
:key="item.path"
|
||||
v-b-tooltip.hover="item.notes"
|
||||
>
|
||||
<form @submit.prevent="saveItem(item)">
|
||||
<span
|
||||
@@ -27,7 +28,7 @@
|
||||
{{ item.value }}
|
||||
</span>
|
||||
:
|
||||
{{ item.text || item.key }}
|
||||
{{ item.text || item.key }} - <i> {{ item.key }} </i>
|
||||
</span>
|
||||
|
||||
<div
|
||||
@@ -68,6 +69,7 @@
|
||||
<li
|
||||
v-for="item in nestedAchievements[achievementType]"
|
||||
:key="item.path"
|
||||
v-b-tooltip.hover="item.notes"
|
||||
>
|
||||
<form @submit.prevent="saveItem(item)">
|
||||
<span
|
||||
@@ -78,7 +80,7 @@
|
||||
{{ item.value }}
|
||||
</span>
|
||||
:
|
||||
{{ item.text || item.key }}
|
||||
{{ item.text || item.key }} - <i> {{ item.key }} </i>
|
||||
</span>
|
||||
|
||||
<div
|
||||
@@ -143,79 +145,28 @@ function getText (achievementItem) {
|
||||
}
|
||||
const { titleKey } = achievementItem;
|
||||
if (titleKey !== undefined) {
|
||||
return i18n.t(titleKey, 'en');
|
||||
return i18n.t(titleKey);
|
||||
}
|
||||
const { singularTitleKey } = achievementItem;
|
||||
if (singularTitleKey !== undefined) {
|
||||
return i18n.t(singularTitleKey, 'en');
|
||||
return i18n.t(singularTitleKey);
|
||||
}
|
||||
return achievementItem.key;
|
||||
}
|
||||
|
||||
function collateItemData (self) {
|
||||
const achievements = [];
|
||||
const nestedAchievements = {};
|
||||
const basePath = 'achievements';
|
||||
const ownedAchievements = self.hero.achievements;
|
||||
const allAchievements = content.achievements;
|
||||
|
||||
for (const key of Object.keys(ownedAchievements)) {
|
||||
const value = ownedAchievements[key];
|
||||
if (typeof value === 'object') {
|
||||
nestedAchievements[key] = [];
|
||||
for (const nestedKey of Object.keys(value)) {
|
||||
const valueIsInteger = self.integerTypes.includes(key);
|
||||
let text = nestedKey;
|
||||
if (allAchievements[key] && allAchievements[key][nestedKey]) {
|
||||
text = getText(allAchievements[key][nestedKey]);
|
||||
}
|
||||
nestedAchievements[key].push({
|
||||
key: nestedKey,
|
||||
text,
|
||||
achievementType: key,
|
||||
modified: false,
|
||||
path: `${basePath}.${key}.${nestedKey}`,
|
||||
value: value[nestedKey],
|
||||
valueIsInteger,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const valueIsInteger = self.integerTypes.includes(key);
|
||||
achievements.push({
|
||||
key,
|
||||
text: getText(allAchievements[key]),
|
||||
modified: false,
|
||||
path: `${basePath}.${key}`,
|
||||
value: ownedAchievements[key],
|
||||
valueIsInteger,
|
||||
});
|
||||
}
|
||||
function getNotes (achievementItem, count) {
|
||||
if (achievementItem === undefined) {
|
||||
return '';
|
||||
}
|
||||
|
||||
for (const key of Object.keys(allAchievements)) {
|
||||
if (key !== '' && !key.endsWith('UltimateGear') && !key.endsWith('Quest')) {
|
||||
if (ownedAchievements[key] === undefined) {
|
||||
const valueIsInteger = self.integerTypes.includes(key);
|
||||
achievements.push({
|
||||
key,
|
||||
text: getText(allAchievements[key]),
|
||||
modified: false,
|
||||
path: `${basePath}.${key}`,
|
||||
value: valueIsInteger ? 0 : false,
|
||||
valueIsInteger,
|
||||
neverOwned: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
const { textKey } = achievementItem;
|
||||
if (textKey !== undefined) {
|
||||
return i18n.t(textKey, { count });
|
||||
}
|
||||
|
||||
self.achievements = achievements;
|
||||
self.nestedAchievements = nestedAchievements;
|
||||
}
|
||||
|
||||
function resetData (self) {
|
||||
collateItemData(self);
|
||||
self.nestedAchievementKeys.forEach(itemType => { self.expandItemType[itemType] = false; });
|
||||
const { singularTextKey } = achievementItem;
|
||||
if (singularTextKey !== undefined) {
|
||||
return i18n.t(singularTextKey, { count });
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
export default {
|
||||
@@ -241,26 +192,34 @@ export default {
|
||||
},
|
||||
nestedAchievementKeys: ['quests', 'ultimateGearSets'],
|
||||
integerTypes: ['streak', 'perfect', 'birthday', 'habiticaDays', 'habitSurveys', 'habitBirthdays',
|
||||
'valentine', 'congrats', 'shinySeed', 'goodluck', 'thankyou', 'seafoam', 'snowball', 'quests'],
|
||||
'valentine', 'congrats', 'shinySeed', 'goodluck', 'thankyou', 'seafoam', 'snowball', 'quests',
|
||||
'rebirths', 'rebirthLevel', 'greeting', 'spookySparkles', 'nye', 'costumeContests', 'congrats',
|
||||
'getwell', 'beastMasterCount', 'mountMasterCount', 'triadBingoCount',
|
||||
],
|
||||
cardTypes: ['greeting', 'birthday', 'valentine', 'goodluck', 'thankyou', 'greeting', 'nye',
|
||||
'congrats', 'getwell'],
|
||||
achievements: [],
|
||||
nestedAchievements: {},
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
resetCounter () {
|
||||
resetData(this);
|
||||
this.resetData();
|
||||
},
|
||||
},
|
||||
mounted () {
|
||||
resetData(this);
|
||||
this.resetData();
|
||||
},
|
||||
methods: {
|
||||
async saveItem (item) {
|
||||
// prepare the item's new value and path for being saved
|
||||
this.hero.achievementPath = item.path;
|
||||
this.hero.achievementVal = item.value;
|
||||
|
||||
await this.saveHero({ hero: this.hero, msg: item.path });
|
||||
await this.saveHero({
|
||||
hero: {
|
||||
_id: this.hero._id,
|
||||
achievementPath: item.path,
|
||||
achievementVal: item.value,
|
||||
},
|
||||
msg: item.path,
|
||||
});
|
||||
item.modified = false;
|
||||
},
|
||||
enableValueChange (item) {
|
||||
@@ -270,6 +229,85 @@ export default {
|
||||
item.value = !item.value;
|
||||
}
|
||||
},
|
||||
resetData () {
|
||||
this.collateItemData();
|
||||
this.nestedAchievementKeys.forEach(itemType => { this.expandItemType[itemType] = false; });
|
||||
},
|
||||
collateItemData () {
|
||||
const achievements = [];
|
||||
const nestedAchievements = {};
|
||||
const basePath = 'achievements';
|
||||
const ownedAchievements = this.hero.achievements;
|
||||
const allAchievements = content.achievements;
|
||||
|
||||
const ownedKeys = Object.keys(ownedAchievements).sort();
|
||||
for (const key of ownedKeys) {
|
||||
const value = ownedAchievements[key];
|
||||
let contentKey = key;
|
||||
if (this.cardTypes.indexOf(key) !== -1) {
|
||||
contentKey += 'Cards';
|
||||
}
|
||||
if (typeof value === 'object') {
|
||||
nestedAchievements[key] = [];
|
||||
for (const nestedKey of Object.keys(value)) {
|
||||
const valueIsInteger = this.integerTypes.includes(key);
|
||||
let text = nestedKey;
|
||||
if (allAchievements[key] && allAchievements[key][contentKey]) {
|
||||
text = getText(allAchievements[key][contentKey]);
|
||||
}
|
||||
let notes = '';
|
||||
if (allAchievements[key] && allAchievements[key][contentKey]) {
|
||||
notes = getNotes(allAchievements[key][contentKey], ownedAchievements[key]);
|
||||
}
|
||||
nestedAchievements[key].push({
|
||||
key: nestedKey,
|
||||
text,
|
||||
notes,
|
||||
achievementType: key,
|
||||
modified: false,
|
||||
path: `${basePath}.${key}.${nestedKey}`,
|
||||
value: value[nestedKey],
|
||||
valueIsInteger,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const valueIsInteger = this.integerTypes.includes(key);
|
||||
achievements.push({
|
||||
key,
|
||||
text: getText(allAchievements[contentKey]),
|
||||
notes: getNotes(allAchievements[contentKey], ownedAchievements[key]),
|
||||
modified: false,
|
||||
path: `${basePath}.${key}`,
|
||||
value: ownedAchievements[key],
|
||||
valueIsInteger,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const allKeys = Object.keys(allAchievements).sort();
|
||||
|
||||
for (const key of allKeys) {
|
||||
if (key !== '' && !key.endsWith('UltimateGear') && !key.endsWith('Quest')) {
|
||||
const ownedKey = key.replace('Cards', '');
|
||||
if (ownedAchievements[ownedKey] === undefined) {
|
||||
const valueIsInteger = this.integerTypes.includes(ownedKey);
|
||||
achievements.push({
|
||||
key: ownedKey,
|
||||
text: getText(allAchievements[key]),
|
||||
notes: getNotes(allAchievements[key], 0),
|
||||
modified: false,
|
||||
path: `${basePath}.${ownedKey}`,
|
||||
value: valueIsInteger ? 0 : false,
|
||||
valueIsInteger,
|
||||
neverOwned: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.achievements = achievements;
|
||||
this.nestedAchievements = nestedAchievements;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
<template>
|
||||
<form @submit.prevent="saveHero({ hero, msg: 'Contributor details', clearData: true })">
|
||||
<form
|
||||
@submit.prevent="saveHero({ hero: {
|
||||
_id: hero._id,
|
||||
contributor: hero.contributor,
|
||||
secret: hero.secret,
|
||||
permissions: hero.permissions,
|
||||
}, msg: 'Contributor details', clearData: true })"
|
||||
>
|
||||
<div class="card mt-2">
|
||||
<div class="card-header">
|
||||
<h3
|
||||
@@ -8,6 +15,12 @@
|
||||
@click="expand = !expand"
|
||||
>
|
||||
Contributor Details
|
||||
<b
|
||||
v-if="hasUnsavedChanges && !expand"
|
||||
class="text-warning float-right"
|
||||
>
|
||||
Unsaved changes
|
||||
</b>
|
||||
</h3>
|
||||
</div>
|
||||
<div
|
||||
@@ -104,13 +117,16 @@
|
||||
</div>
|
||||
<div
|
||||
v-if="expand"
|
||||
class="card-footer"
|
||||
class="card-footer d-flex align-items-center justify-content-between"
|
||||
>
|
||||
<input
|
||||
type="submit"
|
||||
value="Save"
|
||||
class="btn btn-primary mt-1"
|
||||
>
|
||||
<b v-if="hasUnsavedChanges" class="text-warning float-right">
|
||||
Unsaved changes
|
||||
</b>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@@ -190,6 +206,10 @@ export default {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
hasUnsavedChanges: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
<template>
|
||||
<form @submit.prevent="saveHero({ hero, msg: 'Authentication' })">
|
||||
<form
|
||||
@submit.prevent="saveHero({ hero: {
|
||||
_id: hero._id,
|
||||
auth: hero.auth,
|
||||
preferences: hero.preferences,
|
||||
}, msg: 'Authentication' })"
|
||||
>
|
||||
<div class="card mt-2">
|
||||
<div class="card-header">
|
||||
<h3
|
||||
@@ -38,7 +44,10 @@
|
||||
<strong v-else>No</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="cronError" class="form-group row">
|
||||
<div
|
||||
v-if="cronError"
|
||||
class="form-group row"
|
||||
>
|
||||
<label class="col-sm-3 col-form-label">lastCron value:</label>
|
||||
<strong>{{ hero.lastCron | formatDate }}</strong>
|
||||
<br>
|
||||
@@ -53,12 +62,12 @@
|
||||
<div class="col-sm-9 col-form-label">
|
||||
<strong>
|
||||
{{ hero.auth.timestamps.loggedin | formatDate }}</strong>
|
||||
<button
|
||||
<a
|
||||
class="btn btn-warning btn-sm ml-4"
|
||||
@click="resetCron()"
|
||||
>
|
||||
Reset Cron to Yesterday
|
||||
</button>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
@@ -110,13 +119,14 @@
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-3 col-form-label">API Token</label>
|
||||
<div class="col-sm-9">
|
||||
<button
|
||||
<a
|
||||
href="#"
|
||||
value="Change API Token"
|
||||
class="btn btn-danger"
|
||||
@click="changeApiToken()"
|
||||
>
|
||||
Change API Token
|
||||
</button>
|
||||
</a>
|
||||
<div
|
||||
v-if="tokenModified"
|
||||
>
|
||||
@@ -268,13 +278,24 @@ export default {
|
||||
return false;
|
||||
},
|
||||
async changeApiToken () {
|
||||
this.hero.changeApiToken = true;
|
||||
await this.saveHero({ hero: this.hero, msg: 'API Token' });
|
||||
await this.saveHero({
|
||||
hero: {
|
||||
_id: this.hero._id,
|
||||
changeApiToken: true,
|
||||
},
|
||||
msg: 'API Token',
|
||||
});
|
||||
this.tokenModified = true;
|
||||
},
|
||||
resetCron () {
|
||||
this.hero.resetCron = true;
|
||||
this.saveHero({ hero: this.hero, msg: 'Last Cron', clearData: true });
|
||||
this.saveHero({
|
||||
hero: {
|
||||
_id: this.hero._id,
|
||||
resetCron: true,
|
||||
},
|
||||
msg: 'Last Cron',
|
||||
clearData: true,
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
:
|
||||
<span :class="{ ownedItem: !item.neverOwned }">{{ item.text }}</span>
|
||||
</span>
|
||||
{{ item.set }}
|
||||
- {{ itemType }}.{{item.key}} - <i> {{ item.set }}</i>
|
||||
|
||||
<div
|
||||
v-if="item.modified"
|
||||
@@ -232,11 +232,14 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
async saveItem (item) {
|
||||
// prepare the item's new value and path for being saved
|
||||
this.hero.purchasedPath = item.path;
|
||||
this.hero.purchasedVal = item.value;
|
||||
|
||||
await this.saveHero({ hero: this.hero, msg: item.path });
|
||||
await this.saveHero({
|
||||
hero: {
|
||||
_id: this.hero._id,
|
||||
purchasedPath: item.path,
|
||||
purchasedVal: item.value,
|
||||
},
|
||||
msg: item.path,
|
||||
});
|
||||
item.modified = false;
|
||||
},
|
||||
enableValueChange (item) {
|
||||
|
||||
@@ -15,10 +15,17 @@
|
||||
<privileges-and-gems
|
||||
:hero="hero"
|
||||
:reset-counter="resetCounter"
|
||||
:has-unsaved-changes="hasUnsavedChanges([hero.flags, unModifiedHero.flags],
|
||||
[hero.auth, unModifiedHero.auth],
|
||||
[hero.balance, unModifiedHero.balance],
|
||||
[hero.secret, unModifiedHero.secret])"
|
||||
/>
|
||||
|
||||
<subscription-and-perks
|
||||
:hero="hero"
|
||||
:group-plans="groupPlans"
|
||||
:has-unsaved-changes="hasUnsavedChanges([hero.purchased.plan,
|
||||
unModifiedHero.purchased.plan])"
|
||||
/>
|
||||
|
||||
<cron-and-auth
|
||||
@@ -29,6 +36,7 @@
|
||||
<user-profile
|
||||
:hero="hero"
|
||||
:reset-counter="resetCounter"
|
||||
:has-unsaved-changes="hasUnsavedChanges([hero.profile, unModifiedHero.profile])"
|
||||
/>
|
||||
|
||||
<party-and-quest
|
||||
@@ -47,6 +55,12 @@
|
||||
:preferences="hero.preferences"
|
||||
/>
|
||||
|
||||
<stats
|
||||
:hero="hero"
|
||||
:has-unsaved-changes="hasUnsavedChanges([hero.stats, unModifiedHero.stats])"
|
||||
:reset-counter="resetCounter"
|
||||
/>
|
||||
|
||||
<items-owned
|
||||
:hero="hero"
|
||||
:reset-counter="resetCounter"
|
||||
@@ -67,8 +81,18 @@
|
||||
:reset-counter="resetCounter"
|
||||
/>
|
||||
|
||||
<user-history
|
||||
:hero="hero"
|
||||
:reset-counter="resetCounter"
|
||||
/>
|
||||
|
||||
<contributor-details
|
||||
:hero="hero"
|
||||
:hasUnsavedChanges="hasUnsavedChanges(
|
||||
[hero.contributor, unModifiedHero.contributor],
|
||||
[hero.permissions, unModifiedHero.permissions],
|
||||
[hero.secret, unModifiedHero.secret],
|
||||
)"
|
||||
:reset-counter="resetCounter"
|
||||
@clear-data="clearData"
|
||||
/>
|
||||
@@ -109,6 +133,7 @@
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import isEqualWith from 'lodash/isEqualWith';
|
||||
import BasicDetails from './basicDetails';
|
||||
import ItemsOwned from './itemsOwned';
|
||||
import CronAndAuth from './cronAndAuth';
|
||||
@@ -121,6 +146,8 @@ import Transactions from './transactions';
|
||||
import SubscriptionAndPerks from './subscriptionAndPerks';
|
||||
import CustomizationsOwned from './customizationsOwned.vue';
|
||||
import Achievements from './achievements.vue';
|
||||
import UserHistory from './userHistory.vue';
|
||||
import Stats from './stats.vue';
|
||||
|
||||
import { userStateMixin } from '../../../mixins/userState';
|
||||
|
||||
@@ -135,6 +162,8 @@ export default {
|
||||
PrivilegesAndGems,
|
||||
ContributorDetails,
|
||||
Transactions,
|
||||
UserHistory,
|
||||
Stats,
|
||||
SubscriptionAndPerks,
|
||||
UserProfile,
|
||||
Achievements,
|
||||
@@ -148,8 +177,10 @@ export default {
|
||||
return {
|
||||
userIdentifier: '',
|
||||
resetCounter: 0,
|
||||
unModifiedHero: {},
|
||||
hero: {},
|
||||
party: {},
|
||||
groupPlans: [],
|
||||
hasParty: false,
|
||||
partyNotExistError: false,
|
||||
adminHasPrivForParty: true,
|
||||
@@ -168,6 +199,7 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
clearData () {
|
||||
this.unModifiedHero = {};
|
||||
this.hero = {};
|
||||
},
|
||||
|
||||
@@ -176,6 +208,7 @@ export default {
|
||||
this.$emit('changeUserIdentifier', id); // change user identifier in Admin Panel's form
|
||||
|
||||
this.hero = await this.$store.dispatch('hall:getHero', { uuid: id });
|
||||
this.unModifiedHero = JSON.parse(JSON.stringify(this.hero));
|
||||
|
||||
if (!this.hero.flags) {
|
||||
this.hero.flags = {
|
||||
@@ -206,8 +239,38 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
if (this.hero.purchased.plan.planId === 'group_plan_auto') {
|
||||
try {
|
||||
this.groupPlans = await this.$store.dispatch('hall:getHeroGroupPlans', { heroId: this.hero._id });
|
||||
} catch (e) {
|
||||
this.groupPlans = [];
|
||||
}
|
||||
}
|
||||
|
||||
this.resetCounter += 1; // tell child components to reinstantiate from scratch
|
||||
},
|
||||
hasUnsavedChanges (...comparisons) {
|
||||
for (const index in comparisons) {
|
||||
if (index && comparisons[index]) {
|
||||
const objs = comparisons[index];
|
||||
const obj1 = objs[0];
|
||||
const obj2 = objs[1];
|
||||
if (!isEqualWith(obj1, obj2, (x, y) => {
|
||||
if (typeof x === 'object' && typeof y === 'object') {
|
||||
return undefined;
|
||||
}
|
||||
if (x === false && y === undefined) {
|
||||
// Special case for checkboxes
|
||||
return true;
|
||||
}
|
||||
return x == y; // eslint-disable-line eqeqeq
|
||||
})) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -269,16 +269,19 @@ export default {
|
||||
methods: {
|
||||
async saveItem (item) {
|
||||
// prepare the item's new value and path for being saved
|
||||
this.hero.itemPath = item.path;
|
||||
const toSave = {
|
||||
_id: this.hero._id,
|
||||
};
|
||||
toSave.itemPath = item.path;
|
||||
if (item.value === null) {
|
||||
this.hero.itemVal = 'null';
|
||||
toSave.itemVal = 'null';
|
||||
} else if (item.value === false) {
|
||||
this.hero.itemVal = 'false';
|
||||
toSave.itemVal = 'false';
|
||||
} else {
|
||||
this.hero.itemVal = item.value;
|
||||
toSave.itemVal = item.value;
|
||||
}
|
||||
|
||||
await this.saveHero({ hero: this.hero, msg: item.key });
|
||||
await this.saveHero({ hero: toSave, msg: item.key });
|
||||
item.neverOwned = false;
|
||||
item.modified = false;
|
||||
},
|
||||
|
||||
@@ -31,22 +31,41 @@
|
||||
v-html="questErrors"
|
||||
></p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
Party:
|
||||
<span v-if="userHasParty">
|
||||
yes: party ID {{ groupPartyData._id }},
|
||||
member count {{ groupPartyData.memberCount }} (may be wrong)
|
||||
<br>
|
||||
<div v-if="userHasParty">
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-3 col-form-label">
|
||||
Party ID
|
||||
</label>
|
||||
<strong class="col-sm-9 col-form-label">
|
||||
{{ groupPartyData._id }}
|
||||
</strong>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-3 col-form-label">
|
||||
Estimated Member Count
|
||||
</label>
|
||||
<strong class="col-sm-9 col-form-label">
|
||||
{{ groupPartyData.memberCount }}
|
||||
</strong>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-3 col-form-label">
|
||||
Leader
|
||||
</label>
|
||||
<strong class="col-sm-9 col-form-label">
|
||||
<span v-if="userIsPartyLeader">User is the party leader</span>
|
||||
<span v-else>Party leader is
|
||||
<router-link :to="{'name': 'userProfile', 'params': {'userId': groupPartyData.leader}}">
|
||||
{{ groupPartyData.leader }}
|
||||
</router-link>
|
||||
</span>
|
||||
</span>
|
||||
<span v-else>no</span>
|
||||
</strong>
|
||||
</div>
|
||||
<div
|
||||
class="btn btn-danger"
|
||||
@click="removeFromParty()">Remove from Party</div>
|
||||
</div>
|
||||
<strong v-else>User is not in a party.</strong>
|
||||
<div class="subsection-start">
|
||||
<p v-html="questStatus"></p>
|
||||
</div>
|
||||
@@ -56,6 +75,7 @@
|
||||
|
||||
<script>
|
||||
import * as quests from '@/../../common/script/content/quests';
|
||||
import saveHero from '../mixins/saveHero';
|
||||
|
||||
function determineQuestStatus (self) {
|
||||
// Quest data is in the user doc and party doc. They can be out of sync.
|
||||
@@ -271,6 +291,7 @@ function resetData (self) {
|
||||
}
|
||||
|
||||
export default {
|
||||
mixins: [saveHero],
|
||||
props: {
|
||||
resetCounter: {
|
||||
type: Number,
|
||||
@@ -318,5 +339,14 @@ export default {
|
||||
mounted () {
|
||||
resetData(this);
|
||||
},
|
||||
methods: {
|
||||
removeFromParty () {
|
||||
this.saveHero({
|
||||
hero: { _id: this.userId, removeFromParty: true },
|
||||
msg: 'Removed from party',
|
||||
reloadData: true,
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
<template>
|
||||
<form @submit.prevent="saveHero({hero, msg: 'Privileges or Gems or Moderation Notes'})">
|
||||
<form @submit.prevent="saveHero({hero: {
|
||||
_id: hero._id,
|
||||
flags: hero.flags,
|
||||
balance: hero.balance,
|
||||
auth: hero.auth,
|
||||
secret: hero.secret,
|
||||
}, msg: 'Privileges or Gems or Moderation Notes'})">
|
||||
<div class="card mt-2">
|
||||
<div class="card-header">
|
||||
<h3
|
||||
@@ -8,6 +14,9 @@
|
||||
@click="expand = !expand"
|
||||
>
|
||||
Privileges, Gem Balance
|
||||
<b v-if="hasUnsavedChanges && !expand" class="text-warning float-right">
|
||||
Unsaved changes
|
||||
</b>
|
||||
</h3>
|
||||
</div>
|
||||
<div
|
||||
@@ -117,13 +126,16 @@
|
||||
</div>
|
||||
<div
|
||||
v-if="expand"
|
||||
class="card-footer"
|
||||
class="card-footer d-flex align-items-center justify-content-between"
|
||||
>
|
||||
<input
|
||||
type="submit"
|
||||
value="Save"
|
||||
class="btn btn-primary mt-1"
|
||||
>
|
||||
<b v-if="hasUnsavedChanges" class="text-warning float-right">
|
||||
Unsaved changes
|
||||
</b>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@@ -169,6 +181,10 @@ export default {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
hasUnsavedChanges: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
<template>
|
||||
<div class="form-group row">
|
||||
<label
|
||||
class="col-sm-3 col-form-label"
|
||||
:class="color"
|
||||
>{{ label }}</label>
|
||||
<div class="col-sm-9">
|
||||
<input
|
||||
:value="value"
|
||||
class="form-control"
|
||||
type="number"
|
||||
:step="step"
|
||||
:max="max"
|
||||
:min="min"
|
||||
@input="$emit('input', parseInt($event.target.value, 10))"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
|
||||
.about-row {
|
||||
margin-left: 0px;
|
||||
margin-right: 0px;
|
||||
}
|
||||
|
||||
.red-label {
|
||||
color: $red_100;
|
||||
}
|
||||
.blue-label {
|
||||
color: $blue_100;
|
||||
}
|
||||
.purple-label {
|
||||
color: $purple_300;
|
||||
}
|
||||
.yellow-label {
|
||||
color: $yellow_50;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
model: {
|
||||
prop: 'value',
|
||||
event: 'input',
|
||||
},
|
||||
props: {
|
||||
label: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: 'text-label',
|
||||
},
|
||||
value: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
step: {
|
||||
type: String,
|
||||
default: 'any',
|
||||
},
|
||||
min: {
|
||||
},
|
||||
max: {
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,286 @@
|
||||
<template>
|
||||
<form @submit.prevent="submitClicked()">
|
||||
<div class="card mt-2">
|
||||
<div class="card-header">
|
||||
<h3
|
||||
class="mb-0 mt-0"
|
||||
:class="{'open': expand}"
|
||||
@click="expand = !expand"
|
||||
>
|
||||
Stats
|
||||
<b v-if="hasUnsavedChanges && !expand" class="text-warning float-right">
|
||||
Unsaved changes
|
||||
</b>
|
||||
</h3>
|
||||
</div>
|
||||
<div
|
||||
v-if="expand"
|
||||
class="card-body"
|
||||
>
|
||||
<stats-row
|
||||
label="Health"
|
||||
color="red-label"
|
||||
:max="maxHealth"
|
||||
v-model="hero.stats.hp" />
|
||||
<stats-row
|
||||
label="Experience"
|
||||
color="yellow-label"
|
||||
min="0"
|
||||
:max="maxFieldHardCap"
|
||||
v-model="hero.stats.exp" />
|
||||
<stats-row
|
||||
label="Mana"
|
||||
color="blue-label"
|
||||
min="0"
|
||||
:max="maxFieldHardCap"
|
||||
v-model="hero.stats.mp" />
|
||||
<stats-row
|
||||
label="Level"
|
||||
step="1"
|
||||
min="0"
|
||||
:max="maxLevelHardCap"
|
||||
v-model="hero.stats.lvl" />
|
||||
<stats-row
|
||||
label="Gold"
|
||||
min="0"
|
||||
:max="maxFieldHardCap"
|
||||
v-model="hero.stats.gp" />
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-3 col-form-label">Selected Class</label>
|
||||
<div class="col-sm-9">
|
||||
<select
|
||||
id="selectedClass"
|
||||
v-model="hero.stats.class"
|
||||
class="form-control"
|
||||
:disabled="hero.stats.lvl < 10"
|
||||
>
|
||||
<option value="warrior">Warrior</option>
|
||||
<option value="wizard">Mage</option>
|
||||
<option value="healer">Healer</option>
|
||||
<option value="rogue">Rogue</option>
|
||||
</select>
|
||||
<small>
|
||||
When changing class, players usually need stat points deallocated as well.
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3>Stat Points</h3>
|
||||
<stats-row
|
||||
label="Unallocated"
|
||||
min="0"
|
||||
step="1"
|
||||
:max="maxStatPoints"
|
||||
v-model="hero.stats.points" />
|
||||
<stats-row
|
||||
label="Strength"
|
||||
color="red-label"
|
||||
min="0"
|
||||
:max="maxStatPoints"
|
||||
step="1"
|
||||
v-model="hero.stats.str" />
|
||||
<stats-row
|
||||
label="Intelligence"
|
||||
color="blue-label"
|
||||
min="0"
|
||||
:max="maxStatPoints"
|
||||
step="1"
|
||||
v-model="hero.stats.int" />
|
||||
<stats-row
|
||||
label="Perception"
|
||||
color="purple-label"
|
||||
min="0"
|
||||
:max="maxStatPoints"
|
||||
step="1"
|
||||
v-model="hero.stats.per" />
|
||||
<stats-row
|
||||
label="Constitution"
|
||||
color="yellow-label"
|
||||
min="0"
|
||||
:max="maxStatPoints"
|
||||
step="1"
|
||||
v-model="hero.stats.con" />
|
||||
<div class="form-group row">
|
||||
<div class="offset-sm-3 col-sm-9">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-warning btn-sm"
|
||||
@click="deallocateStatPoints">
|
||||
Deallocate all stat points
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row" v-if="statPointsIncorrect">
|
||||
<div class="offset-sm-3 col-sm-9 text-danger">
|
||||
Error: Sum of stat points should equal the users level
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3>Buffs</h3>
|
||||
<stats-row
|
||||
label="Strength"
|
||||
color="red-label"
|
||||
min="0"
|
||||
step="1"
|
||||
v-model="hero.stats.buffs.str" />
|
||||
<stats-row
|
||||
label="Intelligence"
|
||||
color="blue-label"
|
||||
min="0"
|
||||
step="1"
|
||||
v-model="hero.stats.buffs.int" />
|
||||
<stats-row
|
||||
label="Perception"
|
||||
color="purple-label"
|
||||
min="0"
|
||||
step="1"
|
||||
v-model="hero.stats.buffs.per" />
|
||||
<stats-row
|
||||
label="Constitution"
|
||||
color="yellow-label"
|
||||
min="0"
|
||||
step="1"
|
||||
v-model="hero.stats.buffs.con" />
|
||||
<div class="form-group row">
|
||||
<div class="offset-sm-3 col-sm-9">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-warning btn-sm"
|
||||
@click="resetBuffs">
|
||||
Reset Buffs
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="expand"
|
||||
class="card-footer d-flex align-items-center justify-content-between"
|
||||
>
|
||||
<input
|
||||
type="submit"
|
||||
value="Save"
|
||||
class="btn btn-primary mt-1"
|
||||
>
|
||||
<b v-if="hasUnsavedChanges" class="text-warning float-right">
|
||||
Unsaved changes
|
||||
</b>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
|
||||
.about-row {
|
||||
margin-left: 0px;
|
||||
margin-right: 0px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import {
|
||||
MAX_HEALTH,
|
||||
MAX_STAT_POINTS,
|
||||
MAX_LEVEL_HARD_CAP,
|
||||
MAX_FIELD_HARD_CAP,
|
||||
} from '@/../../common/script/constants';
|
||||
import markdownDirective from '@/directives/markdown';
|
||||
import saveHero from '../mixins/saveHero';
|
||||
|
||||
import { mapState } from '@/libs/store';
|
||||
import { userStateMixin } from '../../../mixins/userState';
|
||||
|
||||
import StatsRow from './stats-row';
|
||||
|
||||
function resetData (self) {
|
||||
self.expand = false;
|
||||
}
|
||||
|
||||
export default {
|
||||
directives: {
|
||||
markdown: markdownDirective,
|
||||
},
|
||||
components: {
|
||||
StatsRow,
|
||||
},
|
||||
mixins: [
|
||||
userStateMixin,
|
||||
saveHero,
|
||||
],
|
||||
computed: {
|
||||
...mapState({ user: 'user.data' }),
|
||||
statPointsIncorrect () {
|
||||
if (this.hero.stats.lvl >= 10) {
|
||||
return (parseInt(this.hero.stats.points, 10)
|
||||
+ parseInt(this.hero.stats.str, 10)
|
||||
+ parseInt(this.hero.stats.int, 10)
|
||||
+ parseInt(this.hero.stats.per, 10)
|
||||
+ parseInt(this.hero.stats.con, 10)
|
||||
) !== this.hero.stats.lvl;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
},
|
||||
props: {
|
||||
resetCounter: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
hero: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
hasUnsavedChanges: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
expand: false,
|
||||
maxHealth: MAX_HEALTH,
|
||||
maxStatPoints: MAX_STAT_POINTS,
|
||||
maxLevelHardCap: MAX_LEVEL_HARD_CAP,
|
||||
maxFieldHardCap: MAX_FIELD_HARD_CAP,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
resetCounter () {
|
||||
resetData(this);
|
||||
},
|
||||
},
|
||||
mounted () {
|
||||
resetData(this);
|
||||
},
|
||||
methods: {
|
||||
submitClicked () {
|
||||
if (this.statPointsIncorrect) {
|
||||
return;
|
||||
}
|
||||
this.saveHero({
|
||||
hero: {
|
||||
_id: this.hero._id,
|
||||
stats: this.hero.stats,
|
||||
},
|
||||
msg: 'Stats',
|
||||
});
|
||||
},
|
||||
resetBuffs () {
|
||||
this.hero.stats.buffs = {
|
||||
str: 0,
|
||||
int: 0,
|
||||
per: 0,
|
||||
con: 0,
|
||||
};
|
||||
},
|
||||
deallocateStatPoints () {
|
||||
this.hero.stats.points = this.hero.stats.lvl;
|
||||
this.hero.stats.str = 0;
|
||||
this.hero.stats.int = 0;
|
||||
this.hero.stats.per = 0;
|
||||
this.hero.stats.con = 0;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -1,30 +1,135 @@
|
||||
<template>
|
||||
<form @submit.prevent="saveHero({ hero, msg: 'Subscription Perks' })">
|
||||
<form
|
||||
@submit.prevent="saveHero({ hero: {
|
||||
_id: hero._id,
|
||||
purchased: hero.purchased
|
||||
}, msg: 'Subscription Perks' })"
|
||||
>
|
||||
<div class="card mt-2">
|
||||
<div class="card-header">
|
||||
<div class="card-header"
|
||||
@click="expand = !expand">
|
||||
<h3
|
||||
class="mb-0 mt-0"
|
||||
:class="{ 'open': expand }"
|
||||
@click="expand = !expand"
|
||||
>
|
||||
Subscription, Monthly Perks
|
||||
<b v-if="hasUnsavedChanges && !expand" class="text-warning float-right">
|
||||
Unsaved changes
|
||||
</b>
|
||||
</h3>
|
||||
</div>
|
||||
<div
|
||||
v-if="expand"
|
||||
class="card-body"
|
||||
>
|
||||
<div v-if="hero.purchased.plan.paymentMethod">
|
||||
Payment method:
|
||||
<strong>{{ hero.purchased.plan.paymentMethod }}</strong>
|
||||
<div
|
||||
class="form-group row"
|
||||
>
|
||||
<label class="col-sm-3 col-form-label">
|
||||
Payment method:
|
||||
</label>
|
||||
<div class="col-sm-9">
|
||||
<input v-model="hero.purchased.plan.paymentMethod"
|
||||
class="form-control"
|
||||
type="text"
|
||||
v-if="!isRegularPaymentMethod"
|
||||
>
|
||||
<select
|
||||
v-else
|
||||
v-model="hero.purchased.plan.paymentMethod"
|
||||
class="form-control"
|
||||
type="text"
|
||||
>
|
||||
<option value="groupPlan">Group Plan</option>
|
||||
<option value="Stripe">Stripe</option>
|
||||
<option value="Apple">Apple</option>
|
||||
<option value="Google">Google</option>
|
||||
<option value="Amazon Payments">Amazon</option>
|
||||
<option value="PayPal">PayPal</option>
|
||||
<option value="Gift">Gift</option>
|
||||
<option value="">Clear out</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="hero.purchased.plan.planId">
|
||||
Payment schedule ("basic-earned" is monthly):
|
||||
<strong>{{ hero.purchased.plan.planId }}</strong>
|
||||
<div
|
||||
class="form-group row"
|
||||
>
|
||||
<label class="col-sm-3 col-form-label">
|
||||
Payment schedule:
|
||||
</label>
|
||||
<div class="col-sm-9">
|
||||
<input v-model="hero.purchased.plan.planId"
|
||||
class="form-control"
|
||||
type="text"
|
||||
v-if="!isRegularPlanId"
|
||||
>
|
||||
<select
|
||||
v-else
|
||||
v-model="hero.purchased.plan.planId"
|
||||
class="form-control"
|
||||
type="text"
|
||||
>
|
||||
<option value="basic_earned">Monthly recurring</option>
|
||||
<option value="basic_3mo">3 Months recurring</option>
|
||||
<option value="basic_6mo">6 Months recurring</option>
|
||||
<option value="basic_12mo">12 Months recurring</option>
|
||||
<option value="group_monthly">Group Plan (legacy)</option>
|
||||
<option value="group_plan_auto">Group Plan (auto)</option>
|
||||
<option value="">Clear out</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="hero.purchased.plan.planId == 'group_plan_auto'">
|
||||
Group plan ID:
|
||||
<strong>{{ hero.purchased.plan.owner }}</strong>
|
||||
<div
|
||||
class="form-group row"
|
||||
>
|
||||
<label class="col-sm-3 col-form-label">
|
||||
Customer ID:
|
||||
</label>
|
||||
<div class="col-sm-9">
|
||||
<input
|
||||
v-model="hero.purchased.plan.customerId"
|
||||
class="form-control"
|
||||
type="text"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row"
|
||||
v-if="hero.purchased.plan.planId === 'group_plan_auto'">
|
||||
<label class="col-sm-3 col-form-label">
|
||||
Group Plan Memberships:
|
||||
</label>
|
||||
<div class="col-sm-9 col-form-label">
|
||||
<loading-spinner
|
||||
v-if="!groupPlans"
|
||||
dark-color=true
|
||||
/>
|
||||
<b
|
||||
v-else-if="groupPlans.length === 0"
|
||||
class="text-danger col-form-label"
|
||||
>User is not part of an active group plan!</b>
|
||||
<div
|
||||
v-else
|
||||
v-for="group in groupPlans"
|
||||
:key="group._id"
|
||||
class="card mb-2">
|
||||
<div class="card-body">
|
||||
<h6 class="card-title">{{ group.name }}
|
||||
<small class="float-right">{{ group._id }}</small>
|
||||
</h6>
|
||||
<p class="card-text">
|
||||
<strong>Leader: </strong>
|
||||
<a
|
||||
v-if="group.leader !== hero._id"
|
||||
@click="switchUser(group.leader)"
|
||||
>{{ group.leader }}</a>
|
||||
<strong v-else class="text-success">This user</strong>
|
||||
</p>
|
||||
<p class="card-text">
|
||||
<strong>Members: </strong> {{ group.memberCount }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="hero.purchased.plan.dateCreated"
|
||||
@@ -85,8 +190,18 @@
|
||||
<strong class="input-group-text">
|
||||
{{ dateFormat(hero.purchased.plan.dateTerminated) }}
|
||||
</strong>
|
||||
<a class="btn btn-danger"
|
||||
href="#"
|
||||
v-b-modal.sub_termination_modal
|
||||
v-if="!hero.purchased.plan.dateTerminated && hero.purchased.plan.planId">
|
||||
Terminate
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<small v-if="!hero.purchased.plan.dateTerminated
|
||||
&& hero.purchased.plan.planId" class="text-success">
|
||||
The subscription does not have a termination date and is active.
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
@@ -101,6 +216,35 @@
|
||||
min="0"
|
||||
step="1"
|
||||
>
|
||||
<small class="text-secondary">
|
||||
Cumulative subscribed months across subscription periods.
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-3 col-form-label">
|
||||
Extra months:
|
||||
</label>
|
||||
<div class="col-sm-9">
|
||||
<div class="input-group">
|
||||
<input
|
||||
v-model="hero.purchased.plan.extraMonths"
|
||||
class="form-control"
|
||||
type="number"
|
||||
min="0"
|
||||
step="any"
|
||||
>
|
||||
<div class="input-group-append">
|
||||
<a class="btn btn-warning"
|
||||
@click="applyExtraMonths"
|
||||
v-if="hero.purchased.plan.dateTerminated && hero.purchased.plan.extraMonths > 0">
|
||||
Apply Credit
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<small class="text-secondary">
|
||||
Additional credit that is applied if a subscription is cancelled.
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
@@ -174,10 +318,6 @@
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="hero.purchased.plan.extraMonths > 0">
|
||||
Additional credit (applied upon cancellation):
|
||||
<strong>{{ hero.purchased.plan.extraMonths }}</strong>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-3 col-form-label">
|
||||
Mystery Items:
|
||||
@@ -199,18 +339,64 @@
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row"
|
||||
v-if="!isConvertingToGroupPlan && hero.purchased.plan.planId !== 'group_plan_auto'">
|
||||
<div class="offset-sm-3 col-sm-9">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-secondary btn-sm"
|
||||
@click="beginGroupPlanConvert">
|
||||
Begin converting to group plan subscription
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row"
|
||||
v-if="isConvertingToGroupPlan">
|
||||
<label class="col-sm-3 col-form-label">
|
||||
Group Plan group ID:
|
||||
</label>
|
||||
<div class="col-sm-9">
|
||||
<input
|
||||
v-model="groupPlanID"
|
||||
class="form-control"
|
||||
type="text"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="expand"
|
||||
class="card-footer"
|
||||
class="card-footer d-flex align-items-center justify-content-between"
|
||||
>
|
||||
<input
|
||||
type="submit"
|
||||
value="Save"
|
||||
class="btn btn-primary mt-1"
|
||||
@click="saveClicked"
|
||||
>
|
||||
<b v-if="hasUnsavedChanges" class="text-warning float-right">
|
||||
Unsaved changes
|
||||
</b>
|
||||
</div>
|
||||
</div>
|
||||
<b-modal id="sub_termination_modal" title="Set Termination Date">
|
||||
<p>
|
||||
You can set the sub benefit termination date to today or to the last
|
||||
day of the current billing cycle. Any extra subscription credit will
|
||||
then be processed and automatically added onto the selected date.
|
||||
</p>
|
||||
<template #modal-footer>
|
||||
<div class="mt-3 btn btn-secondary" @click="$bvModal.hide('sub_termination_modal')">
|
||||
Close
|
||||
</div>
|
||||
<div class="mt-3 btn btn-danger" @click="terminateSubscription()">
|
||||
Set to Today
|
||||
</div>
|
||||
<div class="mt-3 btn btn-danger" @click="terminateSubscription(todayWithRemainingCycle)">
|
||||
Set to {{ todayWithRemainingCycle.utc().format('MM/DD/YYYY') }}
|
||||
</div>
|
||||
</template>
|
||||
</b-modal>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
@@ -231,21 +417,38 @@
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import isUUID from 'validator/es/lib/isUUID';
|
||||
import moment from 'moment';
|
||||
import { getPlanContext } from '@/../../common/script/cron';
|
||||
import saveHero from '../mixins/saveHero';
|
||||
import subscriptionBlocks from '../../../../../common/script/content/subscriptionBlocks';
|
||||
import LoadingSpinner from '@/components/ui/loadingSpinner';
|
||||
|
||||
export default {
|
||||
mixins: [saveHero],
|
||||
components: {
|
||||
LoadingSpinner,
|
||||
},
|
||||
props: {
|
||||
hero: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
hasUnsavedChanges: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
groupPlans: {
|
||||
type: Array,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
expand: false,
|
||||
isConvertingToGroupPlan: false,
|
||||
groupPlanID: '',
|
||||
subscriptionBlocks,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -255,6 +458,30 @@ export default {
|
||||
if (!currentPlanContext.nextHourglassDate) return 'N/A';
|
||||
return currentPlanContext.nextHourglassDate.format('MMMM YYYY');
|
||||
},
|
||||
isRegularPlanId () {
|
||||
return this.subscriptionBlocks[this.hero.purchased.plan.planId] !== undefined;
|
||||
},
|
||||
isRegularPaymentMethod () {
|
||||
return [
|
||||
'groupPlan',
|
||||
'Group Plan',
|
||||
'Stripe',
|
||||
'Apple',
|
||||
'Google',
|
||||
'Amazon Payments',
|
||||
'PayPal',
|
||||
'Gift',
|
||||
].includes(this.hero.purchased.plan.paymentMethod);
|
||||
},
|
||||
todayWithRemainingCycle () {
|
||||
const now = moment();
|
||||
const monthCount = subscriptionBlocks[this.hero.purchased.plan.planId].months;
|
||||
const terminationDate = moment(this.hero.purchased.plan.dateCurrentTypeCreated || new Date());
|
||||
while (terminationDate.isBefore(now)) {
|
||||
terminationDate.add(monthCount, 'months');
|
||||
}
|
||||
return terminationDate;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
dateFormat (date) {
|
||||
@@ -263,6 +490,46 @@ export default {
|
||||
}
|
||||
return moment(date).format('YYYY/MM/DD');
|
||||
},
|
||||
terminateSubscription (terminationDate) {
|
||||
if (terminationDate) {
|
||||
this.hero.purchased.plan.dateTerminated = terminationDate.utc().format();
|
||||
} else {
|
||||
this.hero.purchased.plan.dateTerminated = moment(new Date()).utc().format();
|
||||
}
|
||||
this.applyExtraMonths();
|
||||
this.saveHero({ hero: this.hero, msg: 'Subscription Termination', reloadData: true });
|
||||
},
|
||||
applyExtraMonths () {
|
||||
if (this.hero.purchased.plan.extraMonths > 0 || this.hero.purchased.plan.extraMonths !== '0') {
|
||||
const date = moment(this.hero.purchased.plan.dateTerminated || new Date());
|
||||
const extraMonths = Math.max(this.hero.purchased.plan.extraMonths, 0);
|
||||
const extraDays = Math.ceil(30.5 * extraMonths);
|
||||
this.hero.purchased.plan.dateTerminated = date.add(extraDays, 'days').utc().format();
|
||||
this.hero.purchased.plan.extraMonths = 0;
|
||||
}
|
||||
},
|
||||
beginGroupPlanConvert () {
|
||||
this.isConvertingToGroupPlan = true;
|
||||
this.hero.purchased.plan.owner = '';
|
||||
},
|
||||
saveClicked (e) {
|
||||
e.preventDefault();
|
||||
if (this.isConvertingToGroupPlan) {
|
||||
if (!isUUID(this.groupPlanID)) {
|
||||
alert('Invalid group ID');
|
||||
return;
|
||||
}
|
||||
this.hero.purchased.plan.convertToGroupPlan = this.groupPlanID;
|
||||
this.saveHero({ hero: this.hero, msg: 'Group Plan Subscription', reloadData: true });
|
||||
} else {
|
||||
this.saveHero({ hero: this.hero, msg: 'Subscription Perks', reloadData: true });
|
||||
}
|
||||
},
|
||||
switchUser (id) {
|
||||
if (window.confirm('Switch to this user?')) {
|
||||
this.$emit('changeUserIdentifier', id);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,263 @@
|
||||
<template>
|
||||
<div class="card mt-2">
|
||||
<div class="card-header">
|
||||
<h3
|
||||
class="mb-0 mt-0"
|
||||
:class="{'open': expand}"
|
||||
@click="toggleHistoryOpen"
|
||||
>
|
||||
User History
|
||||
</h3>
|
||||
</div>
|
||||
<div
|
||||
v-if="expand"
|
||||
class="card-body"
|
||||
>
|
||||
<div>
|
||||
<div class="clearfix">
|
||||
<div class="mb-4 float-left">
|
||||
<button
|
||||
class="page-header btn-flat tab-button textCondensed"
|
||||
:class="{'active': selectedTab === 'armoire'}"
|
||||
@click="selectTab('armoire')"
|
||||
>
|
||||
Armoire
|
||||
</button>
|
||||
<button
|
||||
class="page-header btn-flat tab-button textCondensed"
|
||||
:class="{'active': selectedTab === 'questInvites'}"
|
||||
@click="selectTab('questInvites')"
|
||||
>
|
||||
Quest Invitations
|
||||
</button>
|
||||
<button
|
||||
class="page-header btn-flat tab-button textCondensed"
|
||||
:class="{'active': selectedTab === 'cron'}"
|
||||
@click="selectTab('cron')"
|
||||
>
|
||||
Cron
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div
|
||||
v-if="selectedTab === 'armoire'"
|
||||
class="col-12"
|
||||
>
|
||||
<table class="table">
|
||||
<tr>
|
||||
<th
|
||||
v-once
|
||||
>
|
||||
{{ $t('timestamp') }}
|
||||
</th>
|
||||
<th v-once>
|
||||
Client
|
||||
</th>
|
||||
<th
|
||||
v-once
|
||||
>
|
||||
Received
|
||||
</th>
|
||||
</tr>
|
||||
<tr
|
||||
v-for="entry in armoire"
|
||||
:key="entry.timestamp"
|
||||
>
|
||||
<td>
|
||||
<span
|
||||
v-b-tooltip.hover="entry.timestamp"
|
||||
>{{ entry.timestamp | timeAgo }}</span>
|
||||
</td>
|
||||
<td>{{ entry.client }}</td>
|
||||
<td>{{ entry.reward }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div
|
||||
v-if="selectedTab === 'questInvites'"
|
||||
class="col-12"
|
||||
>
|
||||
<table class="table">
|
||||
<tr>
|
||||
<th
|
||||
v-once
|
||||
>
|
||||
{{ $t('timestamp') }}
|
||||
</th>
|
||||
<th v-once>
|
||||
Client
|
||||
</th>
|
||||
<th v-once>
|
||||
Quest Key
|
||||
</th>
|
||||
<th v-once>
|
||||
Response
|
||||
</th>
|
||||
</tr>
|
||||
<tr
|
||||
v-for="entry in questInviteResponses"
|
||||
:key="entry.timestamp"
|
||||
>
|
||||
<td>
|
||||
<span
|
||||
v-b-tooltip.hover="entry.timestamp"
|
||||
>{{ entry.timestamp | timeAgo }}</span>
|
||||
</td>
|
||||
<td>{{ entry.client }}</td>
|
||||
<td>{{ entry.quest }}</td>
|
||||
<td>{{ questInviteResponseText(entry.response) }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div
|
||||
v-if="selectedTab === 'cron'"
|
||||
class="col-12"
|
||||
>
|
||||
<table class="table">
|
||||
<tr>
|
||||
<th
|
||||
v-once
|
||||
>
|
||||
{{ $t('timestamp') }}
|
||||
</th>
|
||||
<th v-once>
|
||||
Client
|
||||
</th>
|
||||
<th v-once>
|
||||
Checkin Count
|
||||
</th>
|
||||
</tr>
|
||||
<tr
|
||||
v-for="entry in cron"
|
||||
:key="entry.timestamp"
|
||||
>
|
||||
<td>
|
||||
<span
|
||||
v-b-tooltip.hover="entry.timestamp"
|
||||
>{{ entry.timestamp | timeAgo }}</span>
|
||||
</td>
|
||||
<td>{{ entry.client }}</td>
|
||||
<td>{{ entry.checkinCount }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
|
||||
.page-header.btn-flat {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.tab-button {
|
||||
height: 2rem;
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
font-stretch: condensed;
|
||||
line-height: 1.33;
|
||||
letter-spacing: normal;
|
||||
color: $gray-10;
|
||||
|
||||
margin-right: 1.125rem;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
padding-bottom: 2.5rem;
|
||||
|
||||
&.active, &:hover {
|
||||
color: $purple-300;
|
||||
box-shadow: 0px -0.25rem 0px $purple-300 inset;
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import moment from 'moment';
|
||||
import { userStateMixin } from '../../../mixins/userState';
|
||||
|
||||
export default {
|
||||
filters: {
|
||||
timeAgo (value) {
|
||||
return moment(value).fromNow();
|
||||
},
|
||||
},
|
||||
mixins: [userStateMixin],
|
||||
props: {
|
||||
hero: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
resetCounter: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
expand: false,
|
||||
selectedTab: 'armoire',
|
||||
armoire: [],
|
||||
questInviteResponses: [],
|
||||
cron: [],
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
resetCounter () {
|
||||
if (this.expand) {
|
||||
this.retrieveUserHistory();
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
selectTab (type) {
|
||||
this.selectedTab = type;
|
||||
},
|
||||
async toggleHistoryOpen () {
|
||||
this.expand = !this.expand;
|
||||
if (this.expand) {
|
||||
this.retrieveUserHistory();
|
||||
}
|
||||
},
|
||||
async retrieveUserHistory () {
|
||||
const history = await this.$store.dispatch('adminPanel:getUserHistory', { userIdentifier: this.hero._id });
|
||||
this.armoire = history.armoire;
|
||||
this.questInviteResponses = history.questInviteResponses;
|
||||
this.cron = history.cron;
|
||||
},
|
||||
questInviteResponseText (response) {
|
||||
if (response === 'accept') {
|
||||
return 'Accepted';
|
||||
}
|
||||
if (response === 'reject') {
|
||||
return 'Rejected';
|
||||
}
|
||||
if (response === 'leave') {
|
||||
return 'Left active quest';
|
||||
}
|
||||
if (response === 'invite') {
|
||||
return 'Accepted as owner';
|
||||
}
|
||||
if (response === 'abort') {
|
||||
return 'Aborted by owner';
|
||||
}
|
||||
if (response === 'abortByLeader') {
|
||||
return 'Aborted by party leader';
|
||||
}
|
||||
if (response === 'cancel') {
|
||||
return 'Cancelled before start';
|
||||
}
|
||||
if (response === 'cancelByLeader') {
|
||||
return 'Cancelled before start by party leader';
|
||||
}
|
||||
return response;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -1,5 +1,10 @@
|
||||
<template>
|
||||
<form @submit.prevent="saveHero({hero, msg: 'Users Profile'})">
|
||||
<form
|
||||
@submit.prevent="saveHero({hero: {
|
||||
_id: hero._id,
|
||||
profile: hero.profile
|
||||
}, msg: 'Users Profile'})"
|
||||
>
|
||||
<div class="card mt-2">
|
||||
<div class="card-header">
|
||||
<h3
|
||||
@@ -8,6 +13,9 @@
|
||||
@click="expand = !expand"
|
||||
>
|
||||
User Profile
|
||||
<b v-if="hasUnsavedChanges && !expand" class="text-warning float-right">
|
||||
Unsaved changes
|
||||
</b>
|
||||
</h3>
|
||||
</div>
|
||||
<div
|
||||
@@ -51,13 +59,16 @@
|
||||
</div>
|
||||
<div
|
||||
v-if="expand"
|
||||
class="card-footer"
|
||||
class="card-footer d-flex align-items-center justify-content-between"
|
||||
>
|
||||
<input
|
||||
type="submit"
|
||||
value="Save"
|
||||
class="btn btn-primary mt-1"
|
||||
>
|
||||
<b v-if="hasUnsavedChanges" class="text-warning float-right">
|
||||
Unsaved changes
|
||||
</b>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@@ -101,6 +112,10 @@ export default {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
hasUnsavedChanges: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
|
||||
@@ -55,9 +55,9 @@
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="https://habitica.fandom.com/wiki/Whats_New"
|
||||
target="_blank"
|
||||
>{{ $t('oldNews') }}
|
||||
@click="showBailey()"
|
||||
>
|
||||
{{ $t('oldNews') }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -80,7 +80,7 @@
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="https://habitica.fandom.com/wiki/Contributing_to_Habitica"
|
||||
href="https://github.com/HabitRPG/habitica/wiki/Contributing-to-Habitica"
|
||||
target="_blank"
|
||||
>{{ $t('companyContribute') }}
|
||||
</a>
|
||||
@@ -158,13 +158,6 @@
|
||||
>{{ $t('guidanceForBlacksmiths') }}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="https://habitica.fandom.com/wiki/Extensions,_Add-Ons,_and_Customizations"
|
||||
target="_blank"
|
||||
>{{ $t('communityExtensions') }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -518,7 +511,7 @@ footer {
|
||||
background-color: $gray-500;
|
||||
color: $gray-50;
|
||||
padding: 32px 142px 40px;
|
||||
a {
|
||||
a, a:not([href]) {
|
||||
color: $gray-50;
|
||||
}
|
||||
a:hover {
|
||||
@@ -951,24 +944,28 @@ export default {
|
||||
},
|
||||
async jumpTime (amount) {
|
||||
const response = await axios.post('/api/v4/debug/jump-time', { offsetDays: amount });
|
||||
if (amount > 0) {
|
||||
Vue.config.clock.jump(amount * 24 * 60 * 60 * 1000);
|
||||
} else {
|
||||
Vue.config.clock.setSystemTime(moment().add(amount, 'days').toDate());
|
||||
}
|
||||
this.lastTimeJump = response.data.data.time;
|
||||
this.triggerGetWorldState(true);
|
||||
setTimeout(() => {
|
||||
if (amount > 0) {
|
||||
Vue.config.clock.jump(amount * 24 * 60 * 60 * 1000);
|
||||
} else {
|
||||
Vue.config.clock.setSystemTime(moment().add(amount, 'days').toDate());
|
||||
}
|
||||
this.lastTimeJump = response.data.data.time;
|
||||
this.triggerGetWorldState(true);
|
||||
}, 1000);
|
||||
},
|
||||
async resetTime () {
|
||||
const response = await axios.post('/api/v4/debug/jump-time', { reset: true });
|
||||
const time = new Date(response.data.data.time);
|
||||
Vue.config.clock.restore();
|
||||
Vue.config.clock = sinon.useFakeTimers({
|
||||
now: time,
|
||||
shouldAdvanceTime: true,
|
||||
});
|
||||
this.lastTimeJump = response.data.data.time;
|
||||
this.triggerGetWorldState(true);
|
||||
setTimeout(() => {
|
||||
Vue.config.clock.restore();
|
||||
Vue.config.clock = sinon.useFakeTimers({
|
||||
now: time,
|
||||
shouldAdvanceTime: true,
|
||||
});
|
||||
this.lastTimeJump = response.data.data.time;
|
||||
this.triggerGetWorldState(true);
|
||||
}, 1000);
|
||||
},
|
||||
addExp () {
|
||||
// @TODO: Name these variables better
|
||||
@@ -996,7 +993,6 @@ export default {
|
||||
async bossRage () {
|
||||
await axios.post('/api/v4/debug/boss-rage');
|
||||
},
|
||||
|
||||
async makeAdmin () {
|
||||
await axios.post('/api/v4/debug/make-admin');
|
||||
// @TODO: Notification.text('You are now an admin!
|
||||
@@ -1006,6 +1002,9 @@ export default {
|
||||
donate () {
|
||||
this.$root.$emit('bv::show::modal', 'buy-gems', { alreadyTracked: true });
|
||||
},
|
||||
showBailey () {
|
||||
this.$root.$emit('bv::show::modal', 'new-stuff');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
v-if="member.preferences"
|
||||
class="avatar"
|
||||
:style="{width, height, paddingTop}"
|
||||
:class="backgroundClass"
|
||||
:class="topLevelClassList"
|
||||
@click.prevent="castEnd()"
|
||||
>
|
||||
<div
|
||||
@@ -55,7 +55,11 @@
|
||||
<span :class="[getGearClass('eyewear'), specialMountClass]"></span>
|
||||
<span :class="[getGearClass('head'), specialMountClass]"></span>
|
||||
<span :class="[getGearClass('headAccessory'), specialMountClass]"></span>
|
||||
<span :class="['hair_flower_' + member.preferences.hair.flower, specialMountClass]"></span>
|
||||
<span
|
||||
:class="[
|
||||
'hair_flower_' + member.preferences.hair.flower, specialMountClass
|
||||
]"
|
||||
></span>
|
||||
<span
|
||||
v-if="!hideGear('shield')"
|
||||
:class="[getGearClass('shield'), specialMountClass]"
|
||||
@@ -63,6 +67,7 @@
|
||||
<span
|
||||
v-if="!hideGear('weapon')"
|
||||
:class="[getGearClass('weapon'), specialMountClass]"
|
||||
class="weapon"
|
||||
></span>
|
||||
</template>
|
||||
<!-- Resting-->
|
||||
@@ -96,15 +101,23 @@
|
||||
|
||||
.avatar {
|
||||
width: 141px;
|
||||
height: 147px;
|
||||
image-rendering: pixelated;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
|
||||
&.centered-avatar {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
// resetting the additional padding
|
||||
margin-bottom: -0.5rem !important;
|
||||
}
|
||||
|
||||
.character-sprites {
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.character-sprites span {
|
||||
@@ -123,22 +136,49 @@
|
||||
.invert {
|
||||
filter: invert(100%);
|
||||
}
|
||||
|
||||
.debug {
|
||||
border: 1px solid red;
|
||||
|
||||
.character-sprites {
|
||||
border: 1px solid blue;
|
||||
}
|
||||
|
||||
.weapon {
|
||||
border: 1px solid green;
|
||||
}
|
||||
|
||||
span {
|
||||
border: 1px solid yellow;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import some from 'lodash/some';
|
||||
import moment from 'moment';
|
||||
import { mapState } from '@/libs/store';
|
||||
import foolPet from '../mixins/foolPet';
|
||||
|
||||
import ClassBadge from '@/components/members/classBadge';
|
||||
|
||||
/**
|
||||
* TODO replace avatarOnly with multiple options like
|
||||
* - showMount
|
||||
* - showPet
|
||||
* - showBackground
|
||||
* - showWeapons
|
||||
*/
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ClassBadge,
|
||||
},
|
||||
mixins: [foolPet],
|
||||
props: {
|
||||
debugMode: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
member: {
|
||||
type: Object,
|
||||
required: true,
|
||||
@@ -156,14 +196,21 @@ export default {
|
||||
},
|
||||
overrideAvatarGear: {
|
||||
type: Object,
|
||||
default (data) {
|
||||
return data;
|
||||
},
|
||||
},
|
||||
width: {
|
||||
type: Number,
|
||||
default: 140,
|
||||
type: String,
|
||||
default: '141px',
|
||||
},
|
||||
height: {
|
||||
type: Number,
|
||||
default: 147,
|
||||
type: String,
|
||||
default: '147px',
|
||||
},
|
||||
centerAvatar: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
spritesMargin: {
|
||||
type: String,
|
||||
@@ -171,11 +218,16 @@ export default {
|
||||
},
|
||||
overrideTopPadding: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
showVisualBuffs: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
showWeapon: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
@@ -204,6 +256,19 @@ export default {
|
||||
|
||||
return val;
|
||||
},
|
||||
topLevelClassList () {
|
||||
const classes = [this.backgroundClass];
|
||||
|
||||
if (this.debugMode) {
|
||||
classes.push('debug');
|
||||
}
|
||||
|
||||
if (this.centerAvatar) {
|
||||
classes.push('centered-avatar');
|
||||
}
|
||||
|
||||
return classes.join(' ');
|
||||
},
|
||||
backgroundClass () {
|
||||
if (this.member) {
|
||||
const { background } = this.member.preferences;
|
||||
@@ -256,11 +321,10 @@ export default {
|
||||
return null;
|
||||
},
|
||||
petClass () {
|
||||
if (some(
|
||||
this.currentEventList,
|
||||
event => moment().isBetween(event.start, event.end) && event.aprilFools && event.aprilFools === 'Fungi',
|
||||
)) {
|
||||
return this.foolPet(this.member.items.currentPet);
|
||||
const foolEvent = this.currentEventList?.find(event => moment()
|
||||
.isBetween(event.start, event.end) && event.aprilFools);
|
||||
if (foolEvent) {
|
||||
return this.foolPet(this.member.items.currentPet, foolEvent.aprilFools);
|
||||
}
|
||||
if (this.member?.items.currentPet) return `Pet-${this.member.items.currentPet}`;
|
||||
return '';
|
||||
@@ -290,6 +354,10 @@ export default {
|
||||
},
|
||||
hideGear (gearType) {
|
||||
if (!this.member) return true;
|
||||
if (!this.showWeapon) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (gearType === 'weapon') {
|
||||
const equippedWeapon = this.member.items.gear[this.costumeClass][gearType];
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
>
|
||||
<div
|
||||
v-for="option in items"
|
||||
:key="option.key"
|
||||
:id="option.imageName"
|
||||
:key="option.key"
|
||||
class="outer-option-background"
|
||||
:class="{
|
||||
premium: Boolean(option.gem),
|
||||
@@ -28,15 +28,14 @@
|
||||
v-if="!option.none"
|
||||
class="sprite"
|
||||
:prefix="option.isGear ? 'shop' : 'icon'"
|
||||
:imageName="option.imageName"
|
||||
:image-name="option.imageName"
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
class="redline-outer"
|
||||
>
|
||||
<div class="redline"></div>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="redline-outer"
|
||||
>
|
||||
<div class="redline"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,352 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<div
|
||||
v-if="isUserMentioned"
|
||||
class="mentioned-icon"
|
||||
></div>
|
||||
<div
|
||||
v-if="hasPermission(user, 'moderator') && msg.flagCount"
|
||||
class="message-hidden"
|
||||
>
|
||||
{{ flagCountDescription }}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<user-link
|
||||
:user-id="msg.uuid"
|
||||
:name="msg.user"
|
||||
:backer="msg.backer"
|
||||
:contributor="msg.contributor"
|
||||
/>
|
||||
<p class="time">
|
||||
<span
|
||||
v-if="msg.username"
|
||||
class="mr-1"
|
||||
>@{{ msg.username }}</span>
|
||||
<span
|
||||
v-if="msg.username"
|
||||
class="mr-1"
|
||||
>•</span>
|
||||
<span
|
||||
v-b-tooltip.hover="messageDate"
|
||||
>{{ msg.timestamp | timeAgo }} </span>
|
||||
<span v-if="msg.client && user.contributor.level >= 4">({{ msg.client }})</span>
|
||||
</p>
|
||||
<div
|
||||
ref="markdownContainer"
|
||||
class="text markdown"
|
||||
dir="auto"
|
||||
v-html="parseMarkdown(msg.text)"
|
||||
></div>
|
||||
<hr>
|
||||
<div
|
||||
v-if="msg.id"
|
||||
class="d-flex"
|
||||
>
|
||||
<div
|
||||
class="action d-flex align-items-center"
|
||||
@click="copyAsTodo(msg)"
|
||||
>
|
||||
<div
|
||||
class="svg-icon"
|
||||
v-html="icons.copy"
|
||||
></div>
|
||||
<div>{{ $t('copyAsTodo') }}</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="(user.flags.communityGuidelinesAccepted && msg.uuid !== 'system')
|
||||
&& (!isMessageReported || hasPermission(user, 'moderator'))"
|
||||
class="action d-flex align-items-center"
|
||||
@click="report(msg)"
|
||||
>
|
||||
<div
|
||||
v-once
|
||||
class="svg-icon"
|
||||
v-html="icons.report"
|
||||
></div>
|
||||
<div v-once>
|
||||
{{ $t('report') }}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="msg.uuid === user._id || hasPermission(user, 'moderator')"
|
||||
class="action d-flex align-items-center"
|
||||
@click="remove()"
|
||||
>
|
||||
<div
|
||||
v-once
|
||||
class="svg-icon"
|
||||
v-html="icons.delete"
|
||||
></div>
|
||||
<div v-once>
|
||||
{{ $t('delete') }}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-b-tooltip="{title: likeTooltip(msg.likes[user._id])}"
|
||||
class="ml-auto d-flex"
|
||||
>
|
||||
<div
|
||||
v-if="likeCount > 0"
|
||||
class="action d-flex align-items-center mr-0"
|
||||
:class="{activeLike: msg.likes[user._id]}"
|
||||
@click="like()"
|
||||
>
|
||||
<div
|
||||
class="svg-icon"
|
||||
:title="$t('liked')"
|
||||
v-html="icons.liked"
|
||||
></div>
|
||||
+{{ likeCount }}
|
||||
</div>
|
||||
<div
|
||||
v-if="likeCount === 0"
|
||||
class="action d-flex align-items-center mr-0"
|
||||
:class="{activeLike: msg.likes[user._id]}"
|
||||
@click="like()"
|
||||
>
|
||||
<div
|
||||
class="svg-icon"
|
||||
:title="$t('like')"
|
||||
v-html="icons.like"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<span v-if="!msg.likes[user._id]">{{ $t('like') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.at-highlight {
|
||||
background-color: rgba(213, 200, 255, 0.32);
|
||||
padding: 0.1rem;
|
||||
}
|
||||
|
||||
.at-text {
|
||||
color: #6133b4;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
|
||||
.mentioned-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
background-color: #bda8ff;
|
||||
box-shadow: 0 1px 1px 0 rgba(26, 24, 29, 0.12);
|
||||
position: absolute;
|
||||
right: -.5em;
|
||||
top: -.5em;
|
||||
}
|
||||
|
||||
.message-hidden {
|
||||
margin-left: 1.5em;
|
||||
margin-top: 1em;
|
||||
color: red;
|
||||
}
|
||||
|
||||
hr {
|
||||
margin-bottom: 0.5rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 0.75rem 1.25rem 0.75rem 1.25rem;
|
||||
|
||||
.time {
|
||||
font-size: 12px;
|
||||
color: #878190;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.text {
|
||||
font-size: 14px;
|
||||
color: #4e4a57;
|
||||
text-align: initial;
|
||||
min-height: 0rem;
|
||||
}
|
||||
}
|
||||
|
||||
.action {
|
||||
display: inline-block;
|
||||
color: #878190;
|
||||
margin-right: 1em;
|
||||
font-size: 12px;
|
||||
|
||||
:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.svg-icon {
|
||||
color: #A5A1AC;
|
||||
margin-right: .2em;
|
||||
width: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.activeLike {
|
||||
color: $purple-300;
|
||||
|
||||
.svg-icon {
|
||||
color: $purple-400;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import moment from 'moment';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import escapeRegExp from 'lodash/escapeRegExp';
|
||||
|
||||
import { CHAT_FLAG_LIMIT_FOR_HIDING, CHAT_FLAG_FROM_SHADOW_MUTE } from '@/../../common/script/constants';
|
||||
import renderWithMentions from '@/libs/renderWithMentions';
|
||||
import { userStateMixin } from '../../mixins/userState';
|
||||
import userLink from '../userLink';
|
||||
|
||||
import deleteIcon from '@/assets/svg/delete.svg';
|
||||
import copyIcon from '@/assets/svg/copy.svg';
|
||||
import likeIcon from '@/assets/svg/like.svg';
|
||||
import likedIcon from '@/assets/svg/liked.svg';
|
||||
import reportIcon from '@/assets/svg/report.svg';
|
||||
|
||||
export default {
|
||||
components: { userLink },
|
||||
filters: {
|
||||
timeAgo (value) {
|
||||
return moment(value).fromNow();
|
||||
},
|
||||
date (value) {
|
||||
// @TODO: Vue doesn't support this so we cant user preference
|
||||
return moment(value).toDate().toString();
|
||||
},
|
||||
},
|
||||
mixins: [userStateMixin],
|
||||
props: {
|
||||
msg: {},
|
||||
groupId: {},
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
icons: Object.freeze({
|
||||
like: likeIcon,
|
||||
copy: copyIcon,
|
||||
report: reportIcon,
|
||||
delete: deleteIcon,
|
||||
liked: likedIcon,
|
||||
}),
|
||||
reported: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
isUserMentioned () {
|
||||
const message = this.msg;
|
||||
|
||||
if (message.highlight) return true;
|
||||
|
||||
const { user } = this;
|
||||
const displayName = user.profile.name;
|
||||
const { username } = user.auth.local;
|
||||
const pattern = `@(${escapeRegExp(displayName)}|${escapeRegExp(username)})(\\b)`;
|
||||
message.highlight = new RegExp(pattern, 'i').test(message.text);
|
||||
|
||||
return message.highlight;
|
||||
},
|
||||
likeCount () {
|
||||
const message = this.msg;
|
||||
if (!message.likes) return 0;
|
||||
|
||||
let likeCount = 0;
|
||||
for (const key of Object.keys(message.likes)) {
|
||||
const like = message.likes[key];
|
||||
if (like) likeCount += 1;
|
||||
}
|
||||
return likeCount;
|
||||
},
|
||||
isMessageReported () {
|
||||
return (this.msg.flags && this.msg.flags[this.user.id]) || this.reported;
|
||||
},
|
||||
flagCountDescription () {
|
||||
if (!this.msg.flagCount) return '';
|
||||
if (this.msg.flagCount < CHAT_FLAG_LIMIT_FOR_HIDING) return 'Message flagged once, not hidden';
|
||||
if (this.msg.flagCount < CHAT_FLAG_FROM_SHADOW_MUTE) return 'Message hidden';
|
||||
return 'Message hidden (shadow-muted)';
|
||||
},
|
||||
messageDate () {
|
||||
const date = moment(this.msg.timestamp).toDate();
|
||||
return date.toString();
|
||||
},
|
||||
},
|
||||
mounted () {
|
||||
const links = this.$refs.markdownContainer.getElementsByTagName('a');
|
||||
for (let i = 0; i < links.length; i += 1) {
|
||||
let link = links[i].pathname;
|
||||
|
||||
// Internet Explorer does not provide the leading slash character in the pathname
|
||||
link = link.charAt(0) === '/' ? link : `/${link}`;
|
||||
|
||||
if (link.startsWith('/profile/')) {
|
||||
links[i].onclick = ev => {
|
||||
ev.preventDefault();
|
||||
this.$router.push({ path: link });
|
||||
};
|
||||
}
|
||||
}
|
||||
this.CHAT_FLAG_LIMIT_FOR_HIDING = CHAT_FLAG_LIMIT_FOR_HIDING;
|
||||
this.CHAT_FLAG_FROM_SHADOW_MUTE = CHAT_FLAG_FROM_SHADOW_MUTE;
|
||||
this.$emit('chat-card-mounted', this.msg.id);
|
||||
},
|
||||
methods: {
|
||||
async like () {
|
||||
const message = cloneDeep(this.msg);
|
||||
|
||||
await this.$store.dispatch('chat:like', {
|
||||
groupId: this.groupId,
|
||||
chatId: message.id,
|
||||
});
|
||||
|
||||
message.likes[this.user._id] = !message.likes[this.user._id];
|
||||
|
||||
this.$emit('message-liked', message);
|
||||
this.$root.$emit('bv::hide::tooltip');
|
||||
},
|
||||
likeTooltip (likedStatus) {
|
||||
if (!likedStatus) return this.$t('like');
|
||||
return null;
|
||||
},
|
||||
copyAsTodo (message) {
|
||||
this.$root.$emit('habitica::copy-as-todo', message);
|
||||
},
|
||||
report () {
|
||||
this.$root.$on('habitica:report-result', data => {
|
||||
if (data.ok) {
|
||||
this.reported = true;
|
||||
}
|
||||
|
||||
this.$root.$off('habitica:report-result');
|
||||
});
|
||||
|
||||
this.$root.$emit('habitica::report-chat', {
|
||||
message: this.msg,
|
||||
groupId: this.groupId || 'privateMessage',
|
||||
});
|
||||
},
|
||||
async remove () {
|
||||
if (!window.confirm(this.$t('areYouSureDeleteMessage'))) return; // eslint-disable-line no-alert
|
||||
|
||||
const message = this.msg;
|
||||
this.$emit('message-removed', message);
|
||||
|
||||
await this.$store.dispatch('chat:deleteChat', {
|
||||
groupId: this.groupId,
|
||||
chatId: message.id,
|
||||
});
|
||||
},
|
||||
parseMarkdown (text) {
|
||||
return renderWithMentions(text, this.user);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -3,15 +3,6 @@
|
||||
ref="container"
|
||||
class="container-fluid"
|
||||
>
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<copy-as-todo-modal
|
||||
:group-type="groupType"
|
||||
:group-name="groupName"
|
||||
:group-id="groupId"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row loadmore">
|
||||
<div v-if="canLoadMore">
|
||||
<div class="loadmore-divider"></div>
|
||||
@@ -33,11 +24,14 @@
|
||||
<div
|
||||
v-for="msg in messages.filter(m => chat && canViewFlag(m))"
|
||||
:key="msg.id"
|
||||
class="message-row"
|
||||
:class="{ 'margin-right': user._id !== msg.uuid}"
|
||||
>
|
||||
<div class="d-flex">
|
||||
<avatar
|
||||
v-if="user._id !== msg.uuid && msg.uuid !== 'system'"
|
||||
class="avatar-left"
|
||||
:height="null"
|
||||
:class="{ invisible: avatarUnavailable(msg) }"
|
||||
:member="msg.userStyles || cachedProfileData[msg.uuid] || {}"
|
||||
:avatar-only="true"
|
||||
@@ -45,20 +39,19 @@
|
||||
:override-top-padding="'14px'"
|
||||
@click.native="showMemberModal(msg.uuid)"
|
||||
/>
|
||||
<div class="card">
|
||||
<chat-card
|
||||
:msg="msg"
|
||||
:group-id="groupId"
|
||||
@message-liked="messageLiked"
|
||||
@message-removed="messageRemoved"
|
||||
@show-member-modal="showMemberModal"
|
||||
@chat-card-mounted="itemWasMounted"
|
||||
/>
|
||||
</div>
|
||||
<message-card
|
||||
:msg="msg"
|
||||
:group-id="groupId"
|
||||
:user-sent-message="user._id === msg.uuid"
|
||||
@message-liked="messageLiked"
|
||||
@message-removed="messageRemoved"
|
||||
@message-card-mounted="itemWasMounted"
|
||||
/>
|
||||
<avatar
|
||||
v-if="user._id === msg.uuid"
|
||||
:class="{ invisible: avatarUnavailable(msg) }"
|
||||
:member="msg.userStyles || cachedProfileData[msg.uuid] || {}"
|
||||
:height="null"
|
||||
:avatar-only="true"
|
||||
:hide-class-badge="true"
|
||||
:override-top-padding="'14px'"
|
||||
@@ -105,11 +98,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.avatar-left {
|
||||
margin-left: -1.5rem;
|
||||
margin-right: 2rem;
|
||||
}
|
||||
|
||||
.hr {
|
||||
width: 100%;
|
||||
height: 20px;
|
||||
@@ -137,11 +125,27 @@
|
||||
margin-bottom: .5em;
|
||||
padding: 0rem;
|
||||
width: 90%;
|
||||
|
||||
&.system-message {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.message-scroll .d-flex {
|
||||
min-width: 1px;
|
||||
}
|
||||
|
||||
.message-row {
|
||||
margin-left: 12px;
|
||||
margin-right: 0;
|
||||
margin-bottom: 1.2rem;
|
||||
|
||||
&:not(.margin-right) {
|
||||
.d-flex {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
@@ -152,13 +156,13 @@ import findIndex from 'lodash/findIndex';
|
||||
import { userStateMixin } from '../../mixins/userState';
|
||||
|
||||
import Avatar from '../avatar';
|
||||
import copyAsTodoModal from './copyAsTodoModal';
|
||||
import chatCard from './chatCard';
|
||||
import MessageCard from '@/components/messages/messageCard.vue';
|
||||
|
||||
// TODO merge chatMessages.vue (party message list) with messageList.vue (private message list)
|
||||
|
||||
export default {
|
||||
components: {
|
||||
copyAsTodoModal,
|
||||
chatCard,
|
||||
MessageCard,
|
||||
Avatar,
|
||||
},
|
||||
mixins: [userStateMixin],
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
<template>
|
||||
<b-modal
|
||||
id="copyAsTodo"
|
||||
:title="$t('copyMessageAsToDo')"
|
||||
:hide-footer="true"
|
||||
size="md"
|
||||
>
|
||||
<div class="form-group">
|
||||
<input
|
||||
v-model="task.text"
|
||||
class="form-control"
|
||||
type="text"
|
||||
>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<textarea
|
||||
v-model="task.notes"
|
||||
class="form-control"
|
||||
rows="5"
|
||||
focus-element="true"
|
||||
></textarea>
|
||||
</div>
|
||||
<hr>
|
||||
<task
|
||||
v-if="task._id"
|
||||
:is-user="isUser"
|
||||
:task="task"
|
||||
/>
|
||||
<div class="modal-footer">
|
||||
<button
|
||||
class="btn btn-secondary"
|
||||
@click="close()"
|
||||
>
|
||||
{{ $t('close') }}
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
@click="saveTodo()"
|
||||
>
|
||||
{{ $t('submit') }}
|
||||
</button>
|
||||
</div>
|
||||
</b-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import taskDefaults from '@/../../common/script/libs/taskDefaults';
|
||||
import { mapActions } from '@/libs/store';
|
||||
import markdownDirective from '@/directives/markdown';
|
||||
import notificationsMixin from '@/mixins/notifications';
|
||||
import Task from '@/components/tasks/task';
|
||||
|
||||
const baseUrl = 'https://habitica.com';
|
||||
|
||||
export default {
|
||||
directives: {
|
||||
markdown: markdownDirective,
|
||||
},
|
||||
components: {
|
||||
Task,
|
||||
},
|
||||
mixins: [notificationsMixin],
|
||||
props: ['copyingMessage', 'groupType', 'groupName', 'groupId'],
|
||||
data () {
|
||||
return {
|
||||
isUser: true,
|
||||
task: {},
|
||||
};
|
||||
},
|
||||
mounted () {
|
||||
this.$root.$on('habitica::copy-as-todo', message => {
|
||||
const notes = `${message.user || 'system message'}${message.user ? ' wrote' : ''} in [${this.groupName}](${this.groupPath()})`;
|
||||
const newTask = {
|
||||
text: message.text,
|
||||
type: 'todo',
|
||||
notes,
|
||||
};
|
||||
this.task = taskDefaults(newTask, this.$store.state.user.data);
|
||||
this.$root.$emit('bv::show::modal', 'copyAsTodo');
|
||||
});
|
||||
},
|
||||
beforeDestroy () {
|
||||
this.$root.$off('habitica::copy-as-todo');
|
||||
},
|
||||
methods: {
|
||||
...mapActions({
|
||||
createTask: 'tasks:create',
|
||||
}),
|
||||
groupPath () {
|
||||
if (this.groupType === 'party') {
|
||||
return `${baseUrl}/party`;
|
||||
}
|
||||
return `${baseUrl}/groups/guild/${this.groupId}`;
|
||||
},
|
||||
close () {
|
||||
this.$root.$emit('bv::hide::modal', 'copyAsTodo');
|
||||
},
|
||||
saveTodo () {
|
||||
this.createTask(this.task);
|
||||
this.text(this.$t('messageAddedAsToDo'));
|
||||
this.$root.$emit('bv::hide::modal', 'copyAsTodo');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -22,13 +22,13 @@
|
||||
:placeholder="placeholder"
|
||||
:class="{'user-entry': newMessage}"
|
||||
:maxlength="MAX_MESSAGE_LENGTH"
|
||||
@keydown="updateCarretPosition"
|
||||
@keydown="autoCompleteMixinUpdateCarretPosition"
|
||||
@keyup.ctrl.enter="sendMessageShortcut()"
|
||||
@keydown.tab="handleTab($event)"
|
||||
@keydown.up="selectPreviousAutocomplete($event)"
|
||||
@keydown.down="selectNextAutocomplete($event)"
|
||||
@keypress.enter="selectAutocomplete($event)"
|
||||
@keydown.esc="handleEscape($event)"
|
||||
@keydown.tab="autoCompleteMixinHandleTab($event)"
|
||||
@keydown.up="autoCompleteMixinSelectPreviousAutocomplete($event)"
|
||||
@keydown.down="autoCompleteMixinSelectNextAutocomplete($event)"
|
||||
@keypress.enter="autoCompleteMixinSelectAutocomplete($event)"
|
||||
@keydown.esc="autoCompleteMixinHandleEscape($event)"
|
||||
@paste="disableMessageSendShortcut()"
|
||||
></textarea>
|
||||
<span>{{ currentLength }} / {{ MAX_MESSAGE_LENGTH }}</span>
|
||||
@@ -36,8 +36,8 @@
|
||||
ref="autocomplete"
|
||||
:text="newMessage"
|
||||
:textbox="textbox"
|
||||
:coords="coords"
|
||||
:caret-position="caretPosition"
|
||||
:coords="mixinData.autoComplete.coords"
|
||||
:caret-position="mixinData.autoComplete.caretPosition"
|
||||
:chat="group.chat"
|
||||
@select="selectedAutocomplete"
|
||||
/>
|
||||
@@ -74,7 +74,7 @@
|
||||
<slot name="additionRow"></slot>
|
||||
<div class="row">
|
||||
<div class="hr col-12"></div>
|
||||
<chat-message
|
||||
<chat-messages
|
||||
:chat.sync="group.chat"
|
||||
:group-type="group.type"
|
||||
:group-id="group._id"
|
||||
@@ -86,16 +86,15 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import debounce from 'lodash/debounce';
|
||||
|
||||
import { MAX_MESSAGE_LENGTH } from '@/../../common/script/constants';
|
||||
import externalLinks from '../../mixins/externalLinks';
|
||||
|
||||
import autocomplete from '../chat/autoComplete';
|
||||
import communityGuidelines from './communityGuidelines';
|
||||
import chatMessage from '../chat/chatMessages';
|
||||
import chatMessages from '../chat/chatMessages';
|
||||
import { mapState } from '@/libs/store';
|
||||
import markdownDirective from '@/directives/markdown';
|
||||
import { autoCompleteHelperMixin } from '@/mixins/autoCompleteHelper';
|
||||
|
||||
export default {
|
||||
directives: {
|
||||
@@ -104,23 +103,18 @@ export default {
|
||||
components: {
|
||||
autocomplete,
|
||||
communityGuidelines,
|
||||
chatMessage,
|
||||
chatMessages,
|
||||
},
|
||||
mixins: [externalLinks],
|
||||
mixins: [externalLinks, autoCompleteHelperMixin],
|
||||
props: ['label', 'group', 'placeholder'],
|
||||
data () {
|
||||
return {
|
||||
newMessage: '',
|
||||
sending: false,
|
||||
caretPosition: 0,
|
||||
chat: {
|
||||
submitDisable: false,
|
||||
submitTimeout: null,
|
||||
},
|
||||
coords: {
|
||||
TOP: 0,
|
||||
LEFT: 0,
|
||||
},
|
||||
textbox: null,
|
||||
MAX_MESSAGE_LENGTH: MAX_MESSAGE_LENGTH.toString(),
|
||||
};
|
||||
@@ -142,35 +136,6 @@ export default {
|
||||
this.handleExternalLinks();
|
||||
},
|
||||
methods: {
|
||||
// https://medium.com/@_jh3y/how-to-where-s-the-caret-getting-the-xy-position-of-the-caret-a24ba372990a
|
||||
getCoord (e, text) {
|
||||
this.caretPosition = text.selectionEnd;
|
||||
const div = document.createElement('div');
|
||||
const span = document.createElement('span');
|
||||
const copyStyle = getComputedStyle(text);
|
||||
|
||||
[].forEach.call(copyStyle, prop => {
|
||||
div.style[prop] = copyStyle[prop];
|
||||
});
|
||||
|
||||
div.style.position = 'absolute';
|
||||
document.body.appendChild(div);
|
||||
div.textContent = text.value.substr(0, this.caretPosition);
|
||||
span.textContent = text.value.substr(this.caretPosition) || '.';
|
||||
div.appendChild(span);
|
||||
this.coords = {
|
||||
TOP: span.offsetTop,
|
||||
LEFT: span.offsetLeft,
|
||||
};
|
||||
document.body.removeChild(div);
|
||||
},
|
||||
updateCarretPosition: debounce(function updateCarretPosition (eventUpdate) {
|
||||
this._updateCarretPosition(eventUpdate);
|
||||
}, 250),
|
||||
_updateCarretPosition (eventUpdate) {
|
||||
const text = eventUpdate.target;
|
||||
this.getCoord(eventUpdate, text);
|
||||
},
|
||||
async sendMessageShortcut () {
|
||||
// If the user recently pasted in the text field, don't submit
|
||||
if (!this.chat.submitDisable) {
|
||||
@@ -221,50 +186,6 @@ export default {
|
||||
}, 500);
|
||||
},
|
||||
|
||||
handleTab (e) {
|
||||
if (this.$refs.autocomplete.searchActive) {
|
||||
e.preventDefault();
|
||||
if (e.shiftKey) {
|
||||
this.$refs.autocomplete.selectPrevious();
|
||||
} else {
|
||||
this.$refs.autocomplete.selectNext();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
handleEscape (e) {
|
||||
if (this.$refs.autocomplete.searchActive) {
|
||||
e.preventDefault();
|
||||
this.$refs.autocomplete.cancel();
|
||||
}
|
||||
},
|
||||
|
||||
selectNextAutocomplete (e) {
|
||||
if (this.$refs.autocomplete.searchActive) {
|
||||
e.preventDefault();
|
||||
this.$refs.autocomplete.selectNext();
|
||||
}
|
||||
},
|
||||
|
||||
selectPreviousAutocomplete (e) {
|
||||
if (this.$refs.autocomplete.searchActive) {
|
||||
e.preventDefault();
|
||||
this.$refs.autocomplete.selectPrevious();
|
||||
}
|
||||
},
|
||||
|
||||
selectAutocomplete (e) {
|
||||
if (this.$refs.autocomplete.searchActive) {
|
||||
if (this.$refs.autocomplete.selected !== null) {
|
||||
e.preventDefault();
|
||||
this.$refs.autocomplete.makeSelection();
|
||||
} else {
|
||||
// no autocomplete selected, newline instead
|
||||
this.$refs.autocomplete.cancel();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
selectedAutocomplete (newText, newCaret) {
|
||||
this.newMessage = newText;
|
||||
// Wait for v-modal to update
|
||||
@@ -273,7 +194,6 @@ export default {
|
||||
this.textbox.focus();
|
||||
});
|
||||
},
|
||||
|
||||
fetchRecentMessages () {
|
||||
this.$emit('fetchRecentMessages');
|
||||
},
|
||||
@@ -284,10 +204,7 @@ export default {
|
||||
beforeRouteUpdate (to, from, next) {
|
||||
// Reset chat
|
||||
this.newMessage = '';
|
||||
this.coords = {
|
||||
TOP: 0,
|
||||
LEFT: 0,
|
||||
};
|
||||
this.autoCompleteMixinResetCoordsPosition();
|
||||
|
||||
next();
|
||||
},
|
||||
|
||||
@@ -225,10 +225,9 @@
|
||||
</a>
|
||||
</div>
|
||||
<div class="quest-icon">
|
||||
<div
|
||||
<Sprite
|
||||
class="quest"
|
||||
:class="`inventory_quest_scroll_${questData.key}`"
|
||||
></div>
|
||||
:image-name="`inventory_quest_scroll_${questData.key}`" />
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
|
||||
@@ -297,7 +297,7 @@
|
||||
<div class="topbar-dropdown">
|
||||
<router-link
|
||||
v-if="user.permissions.fullAccess ||
|
||||
user.permissions.userSupport || user.permissions.newsPoster"
|
||||
user.permissions.userSupport"
|
||||
class="topbar-dropdown-item dropdown-item"
|
||||
:to="{name: 'adminPanel'}"
|
||||
>
|
||||
|
||||
@@ -12,20 +12,21 @@
|
||||
<strong> {{ notification.data.title }} </strong>
|
||||
<span> {{ notification.data.text }} </span>
|
||||
</div>
|
||||
<div
|
||||
<Sprite
|
||||
slot="icon"
|
||||
class="mt-3"
|
||||
:class="notification.data.icon"
|
||||
></div>
|
||||
:image-name="notification.data.icon" />
|
||||
</base-notification>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import BaseNotification from './base';
|
||||
import Sprite from '@/components/ui/sprite.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
BaseNotification,
|
||||
Sprite,
|
||||
},
|
||||
props: {
|
||||
notification: {
|
||||
|
||||
@@ -10,20 +10,21 @@
|
||||
slot="content"
|
||||
v-html="$t('newSubscriberItem')"
|
||||
></div>
|
||||
<div
|
||||
<Sprite
|
||||
slot="icon"
|
||||
:class="mysteryClass"
|
||||
></div>
|
||||
:image-name="mysteryClass" />
|
||||
</base-notification>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import moment from 'moment';
|
||||
import BaseNotification from './base';
|
||||
import Sprite from '@/components/ui/sprite.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
BaseNotification,
|
||||
Sprite,
|
||||
},
|
||||
props: ['notification', 'canRemove'],
|
||||
computed: {
|
||||
|
||||
@@ -114,7 +114,6 @@
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import some from 'lodash/some';
|
||||
import moment from 'moment';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { mapState } from '@/libs/store';
|
||||
@@ -183,13 +182,12 @@ export default {
|
||||
return 'GreyedOut';
|
||||
},
|
||||
imageName () {
|
||||
if (this.isOwned() && some(
|
||||
this.currentEventList,
|
||||
event => moment().isBetween(event.start, event.end) && event.aprilFools && event.aprilFools === 'Fungi',
|
||||
)) {
|
||||
if (this.isSpecial()) return `stable_${this.foolPet(this.item.key)}`;
|
||||
const foolEvent = this.currentEventList?.find(event => moment()
|
||||
.isBetween(event.start, event.end) && event.aprilFools);
|
||||
if (this.isOwned() && foolEvent) {
|
||||
if (this.isSpecial()) return `stable_${this.foolPet(this.item.key, foolEvent.aprilFools)}`;
|
||||
const petString = `${this.item.eggKey}-${this.item.key}`;
|
||||
return `stable_${this.foolPet(petString)}`;
|
||||
return `stable_${this.foolPet(petString, foolEvent.aprilFools)}`;
|
||||
}
|
||||
|
||||
if (this.isOwned() || (this.mountOwned() && this.isHatchable())) {
|
||||
|
||||
@@ -28,7 +28,6 @@
|
||||
:name="member.profile.name"
|
||||
:backer="member.backer"
|
||||
:contributor="member.contributor"
|
||||
:smaller-style="true"
|
||||
/>
|
||||
<inline-class-badge
|
||||
v-if="member.stats"
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
<template>
|
||||
<div
|
||||
class="d-inline-flex like-button"
|
||||
@click="like()"
|
||||
>
|
||||
<div
|
||||
v-b-tooltip="{title: likeTooltip(likeCount)}"
|
||||
class="d-flex"
|
||||
>
|
||||
<div
|
||||
v-if="likeCount > 0"
|
||||
class="action d-flex align-items-center mr-0"
|
||||
:class="{isLiked: true, currentUserLiked: likedByCurrentUser}"
|
||||
>
|
||||
<div
|
||||
class="svg-icon mr-1"
|
||||
:title="$t('liked')"
|
||||
v-html="likedIcon"
|
||||
></div>
|
||||
+{{ likeCount }}
|
||||
</div>
|
||||
<div
|
||||
v-if="likeCount === 0"
|
||||
class="action d-flex align-items-center mr-1"
|
||||
>
|
||||
<div
|
||||
class="svg-icon"
|
||||
:title="$t('like')"
|
||||
v-html="icons.like"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<span v-if="likeCount === 0">{{ $t('like') }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
@import '~@/assets/scss/tiers.scss';
|
||||
|
||||
.action {
|
||||
display: inline-block;
|
||||
margin-right: 1em;
|
||||
|
||||
.svg-icon {
|
||||
color: $gray-100;
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
&.isLiked.currentUserLiked {
|
||||
color: $purple-200;
|
||||
font-weight: bold;
|
||||
|
||||
.svg-icon {
|
||||
color: $purple-300;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.like-button {
|
||||
color: $gray-100;
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
color: $purple-200;
|
||||
|
||||
.svg-icon {
|
||||
color: $purple-300;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import likeIcon from '@/assets/svg/like.svg';
|
||||
import likedIcon from '@/assets/svg/liked.svg';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
likeCount: {
|
||||
type: Number,
|
||||
},
|
||||
likedByCurrentUser: {
|
||||
type: Boolean,
|
||||
},
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
icons: Object.freeze({
|
||||
like: likeIcon,
|
||||
liked: likedIcon,
|
||||
}),
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
likedIcon () {
|
||||
return this.likedByCurrentUser ? this.icons.liked : this.icons.like;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async like () {
|
||||
this.$emit('toggle-like');
|
||||
},
|
||||
likeTooltip (likedStatus) {
|
||||
if (!likedStatus) return this.$t('like');
|
||||
return null;
|
||||
},
|
||||
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -1,73 +1,157 @@
|
||||
<template>
|
||||
<div class="card-body">
|
||||
<user-link
|
||||
:user-id="msg.uuid"
|
||||
:name="msg.user"
|
||||
:backer="msg.backer"
|
||||
:contributor="msg.contributor"
|
||||
/>
|
||||
<p class="time">
|
||||
<span
|
||||
v-if="msg.username"
|
||||
class="mr-1"
|
||||
>@{{ msg.username }}</span><span
|
||||
v-if="msg.username"
|
||||
class="mr-1"
|
||||
>•</span>
|
||||
<span
|
||||
v-b-tooltip.hover="messageDate"
|
||||
>{{ msg.timestamp | timeAgo }} </span>
|
||||
<span v-if="msg.client && user.contributor.level >= 4"> ({{ msg.client }})</span>
|
||||
</p>
|
||||
<div
|
||||
class="card"
|
||||
:class="{
|
||||
'system-message': isSystemMessage
|
||||
}"
|
||||
>
|
||||
<div
|
||||
class="text markdown"
|
||||
dir="auto"
|
||||
v-html="parseMarkdown(msg.text)"
|
||||
></div>
|
||||
<div
|
||||
v-if="isMessageReported"
|
||||
class="reported"
|
||||
>
|
||||
<span v-once>{{ $t('reportedMessage') }}</span><br>
|
||||
<span v-once>{{ $t('canDeleteNow') }}</span>
|
||||
</div>
|
||||
<hr>
|
||||
<div
|
||||
v-if="msg.id"
|
||||
class="d-flex"
|
||||
v-b-tooltip.hover="messageDateForSystemMessage"
|
||||
class="message-card"
|
||||
|
||||
:class="{
|
||||
'user-sent-message': userSentMessage,
|
||||
'user-received-message': !userSentMessage && !isSystemMessage,
|
||||
'system-message': isSystemMessage
|
||||
}"
|
||||
>
|
||||
<div
|
||||
v-if="!isMessageReported"
|
||||
class="action d-flex align-items-center"
|
||||
@click="report(msg)"
|
||||
v-if="isUserMentioned"
|
||||
class="mentioned-icon"
|
||||
></div>
|
||||
<div
|
||||
v-if="userIsModerator && msg.flagCount"
|
||||
class="message-hidden"
|
||||
>
|
||||
<div
|
||||
v-once
|
||||
class="svg-icon"
|
||||
v-html="icons.report"
|
||||
></div>
|
||||
<div v-once>
|
||||
{{ $t('report') }}
|
||||
</div>
|
||||
{{ flagCountDescription }}
|
||||
</div>
|
||||
<div
|
||||
class="action d-flex align-items-center"
|
||||
@click="remove()"
|
||||
class="card-body"
|
||||
>
|
||||
<user-link
|
||||
v-if="!isSystemMessage"
|
||||
:user-id="msg.uuid"
|
||||
:name="msg.user"
|
||||
:backer="msg.backer"
|
||||
:contributor="msg.contributor"
|
||||
/>
|
||||
<p
|
||||
v-if="!isSystemMessage"
|
||||
class="time"
|
||||
>
|
||||
<span
|
||||
v-if="msg.username"
|
||||
class="mr-1"
|
||||
>@{{ msg.username }}</span><span
|
||||
v-if="msg.username"
|
||||
class="mr-1"
|
||||
>•</span>
|
||||
<span v-b-tooltip.hover="messageDate">{{ msg.timestamp | timeAgo }} </span>
|
||||
<span v-if="msg.client && user.contributor.level >= 4">
|
||||
({{ msg.client }})
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<b-dropdown
|
||||
v-if="!isSystemMessage"
|
||||
right="right"
|
||||
variant="flat"
|
||||
toggle-class="with-icon"
|
||||
class="card-menu no-min-width"
|
||||
:no-caret="true"
|
||||
>
|
||||
<template #button-content>
|
||||
<span
|
||||
v-once
|
||||
class="svg-icon inline menuIcon color"
|
||||
v-html="icons.menuIcon"
|
||||
>
|
||||
</span>
|
||||
</template>
|
||||
<b-dropdown-item
|
||||
class="selectListItem"
|
||||
@click="copy(msg)"
|
||||
>
|
||||
<span class="with-icon">
|
||||
<span
|
||||
v-once
|
||||
class="svg-icon icon-16 color"
|
||||
v-html="icons.copy"
|
||||
></span>
|
||||
<span v-once>
|
||||
{{ $t('copy') }}
|
||||
</span>
|
||||
</span>
|
||||
</b-dropdown-item>
|
||||
<b-dropdown-item
|
||||
v-if="canReportMessage"
|
||||
class="selectListItem custom-hover--red"
|
||||
@click="report(msg)"
|
||||
>
|
||||
<span class="with-icon">
|
||||
<span
|
||||
v-once
|
||||
class="svg-icon icon-16 color"
|
||||
v-html="icons.report"
|
||||
></span>
|
||||
<span v-once>
|
||||
{{ $t('report') }}
|
||||
</span>
|
||||
</span>
|
||||
</b-dropdown-item>
|
||||
<b-dropdown-item
|
||||
v-if="canDeleteMessage"
|
||||
class="selectListItem custom-hover--red"
|
||||
@click="remove()"
|
||||
>
|
||||
<span class="with-icon">
|
||||
<span
|
||||
v-once
|
||||
class="svg-icon icon-16 color"
|
||||
v-html="icons.delete"
|
||||
></span>
|
||||
<span v-once>
|
||||
{{ $t('delete') }}
|
||||
</span>
|
||||
</span>
|
||||
</b-dropdown-item>
|
||||
</b-dropdown>
|
||||
|
||||
<div
|
||||
v-once
|
||||
class="svg-icon"
|
||||
v-html="icons.delete"
|
||||
></div>
|
||||
<div v-once>
|
||||
{{ $t('delete') }}
|
||||
v-if="isSystemMessage"
|
||||
class="system-message-body"
|
||||
>
|
||||
{{ msg.unformattedText }}
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
ref="markdownContainer"
|
||||
class="text markdown"
|
||||
dir="auto"
|
||||
v-html="parseMarkdown(msg.text)"
|
||||
></div>
|
||||
<div
|
||||
v-if="isMessageReported"
|
||||
class="reported"
|
||||
>
|
||||
<span v-once>{{ $t('reportedMessage') }}</span><br>
|
||||
<span v-once>{{ $t('canDeleteNow') }}</span>
|
||||
</div>
|
||||
|
||||
<like-button
|
||||
v-if="canLikeMessage"
|
||||
class="mt-75"
|
||||
:liked-by-current-user="msg.likes[user._id]"
|
||||
:like-count="likeCount"
|
||||
@toggle-like="like()"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.message-card {
|
||||
.at-highlight {
|
||||
background-color: rgba(213, 200, 255, 0.32);
|
||||
padding: 0.1rem;
|
||||
@@ -76,43 +160,76 @@
|
||||
.at-text {
|
||||
color: #6133b4;
|
||||
}
|
||||
|
||||
.card-menu button {
|
||||
justify-content: center;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 1rem;
|
||||
width: 1rem;
|
||||
}
|
||||
|
||||
.markdown p:last-of-type {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
@import '~@/assets/scss/tiers.scss';
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
@import '~@/assets/scss/tiers.scss';
|
||||
|
||||
.action {
|
||||
display: inline-block;
|
||||
color: $gray-200;
|
||||
margin-right: 1em;
|
||||
font-size: 12px;
|
||||
.card {
|
||||
background: transparent !important;
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
.message-card:not(.system-message) {
|
||||
background: white;
|
||||
}
|
||||
|
||||
.svg-icon {
|
||||
color: $gray-300;
|
||||
margin-right: .2em;
|
||||
width: 16px;
|
||||
}
|
||||
.mentioned-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
background-color: $purple-500;
|
||||
box-shadow: 0 1px 1px 0 rgba(26, 24, 29, 0.12);
|
||||
position: absolute;
|
||||
right: -.5em;
|
||||
top: -.5em;
|
||||
}
|
||||
|
||||
.message-hidden {
|
||||
margin-left: 1.5em;
|
||||
margin-top: 1em;
|
||||
color: red;
|
||||
}
|
||||
|
||||
.active {
|
||||
color: $purple-300;
|
||||
|
||||
.svg-icon {
|
||||
color: $purple-400;
|
||||
}
|
||||
}
|
||||
|
||||
.active {
|
||||
color: $purple-300;
|
||||
.message-card {
|
||||
border-radius: 7px;
|
||||
margin: 0;
|
||||
padding: 1rem 0.75rem 0.5rem 1rem;
|
||||
|
||||
.svg-icon {
|
||||
color: $purple-400;
|
||||
}
|
||||
&.system-message {
|
||||
padding-top: 0.5rem;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 0.75rem 1.25rem 0.75rem 1.25rem;
|
||||
position: relative;
|
||||
padding: 0;
|
||||
|
||||
.time {
|
||||
font-size: 12px;
|
||||
color: $gray-200;
|
||||
color: $gray-100;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
@@ -123,49 +240,173 @@
|
||||
min-height: 0rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
hr {
|
||||
margin-bottom: 0.5rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
.card-menu {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
|
||||
.reported {
|
||||
margin-top: 18px;
|
||||
color: $red-50;
|
||||
&:not(.show) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.card-body:hover {
|
||||
.card-menu {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
hr {
|
||||
margin-bottom: 0.5rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.reported {
|
||||
margin-top: 18px;
|
||||
color: $red-50;
|
||||
}
|
||||
|
||||
.selectListItem:not(:hover) .svg-icon.icon-16.color {
|
||||
color: #{$gray-100}
|
||||
}
|
||||
|
||||
.custom-hover--red {
|
||||
--hover-color: #{$maroon-50};
|
||||
--hover-background: #{rgba($red-500, 0.25)};
|
||||
}
|
||||
|
||||
.user-sent-message {
|
||||
border: 1px solid $purple-400;
|
||||
}
|
||||
|
||||
.system-message {
|
||||
border: 1px solid $purple-400;
|
||||
}
|
||||
|
||||
.user-received-message {
|
||||
border: 1px solid $gray-500;
|
||||
}
|
||||
|
||||
.card-menu {
|
||||
// icon-color is the menu icon itself
|
||||
--icon-color: #{$gray-100};
|
||||
|
||||
--dropdown-item-hover-icon-color: #{$gray-100};
|
||||
|
||||
&:hover {
|
||||
--icon-color: #{$purple-300};
|
||||
}
|
||||
}
|
||||
|
||||
.menuIcon {
|
||||
width: 4px;
|
||||
height: 1rem;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.system-message-body {
|
||||
line-height: 1.71;
|
||||
text-align: center;
|
||||
color: $purple-300;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import axios from 'axios';
|
||||
import moment from 'moment';
|
||||
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import escapeRegExp from 'lodash/escapeRegExp';
|
||||
import { CHAT_FLAG_FROM_SHADOW_MUTE, CHAT_FLAG_LIMIT_FOR_HIDING } from '@/../../common/script/constants';
|
||||
import externalLinks from '../../mixins/externalLinks';
|
||||
|
||||
import { CopyToClipboardMixin } from '@/mixins/copyToClipboard';
|
||||
|
||||
import renderWithMentions from '@/libs/renderWithMentions';
|
||||
import { mapState } from '@/libs/store';
|
||||
import userLink from '../userLink';
|
||||
|
||||
import deleteIcon from '@/assets/svg/delete.svg';
|
||||
import reportIcon from '@/assets/svg/report.svg';
|
||||
import menuIcon from '@/assets/svg/menu.svg';
|
||||
import { userStateMixin } from '@/mixins/userState';
|
||||
import copyIcon from '@/assets/svg/copy.svg';
|
||||
import LikeButton from '@/components/messages/likeButton.vue';
|
||||
|
||||
const LikeLogicMixin = {
|
||||
computed: {
|
||||
likeCount () {
|
||||
const message = this.msg;
|
||||
if (!message.likes) return 0;
|
||||
|
||||
let likeCount = 0;
|
||||
for (const key of Object.keys(message.likes)) {
|
||||
const like = message.likes[key];
|
||||
if (like) likeCount += 1;
|
||||
}
|
||||
return likeCount;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
|
||||
async like () {
|
||||
const message = cloneDeep(this.msg);
|
||||
|
||||
await this.$store.dispatch('chat:like', {
|
||||
groupId: this.groupId,
|
||||
chatMessageId: this.privateMessageMode ? message.uniqueMessageId : message.id,
|
||||
});
|
||||
|
||||
message.likes[this.user._id] = !message.likes[this.user._id];
|
||||
|
||||
this.$emit('message-liked', message);
|
||||
this.$root.$emit('bv::hide::tooltip');
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default {
|
||||
components: {
|
||||
LikeButton,
|
||||
userLink,
|
||||
},
|
||||
filters: {
|
||||
timeAgo (value) {
|
||||
return moment(value).fromNow();
|
||||
},
|
||||
date (value) {
|
||||
// @TODO: Vue doesn't support this so we cant user preference
|
||||
return moment(value).toDate().toString();
|
||||
},
|
||||
},
|
||||
mixins: [externalLinks],
|
||||
mixins: [
|
||||
externalLinks, userStateMixin, LikeLogicMixin,
|
||||
CopyToClipboardMixin,
|
||||
],
|
||||
props: {
|
||||
msg: {},
|
||||
msg: {
|
||||
type: Object,
|
||||
},
|
||||
groupId: {
|
||||
type: String,
|
||||
},
|
||||
privateMessageMode: {
|
||||
type: Boolean,
|
||||
},
|
||||
userSentMessage: {
|
||||
type: Boolean,
|
||||
},
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
icons: Object.freeze({
|
||||
delete: deleteIcon,
|
||||
report: reportIcon,
|
||||
copy: copyIcon,
|
||||
menuIcon,
|
||||
}),
|
||||
reported: false,
|
||||
};
|
||||
@@ -175,19 +416,100 @@ export default {
|
||||
isMessageReported () {
|
||||
return (this.msg.flags && this.msg.flags[this.user.id]) || this.reported;
|
||||
},
|
||||
messageDateForSystemMessage () {
|
||||
return this.isSystemMessage ? this.messageDate : '';
|
||||
},
|
||||
messageDate () {
|
||||
const date = moment(this.msg.timestamp).toDate();
|
||||
return date.toString();
|
||||
},
|
||||
userIsModerator () {
|
||||
return this.hasPermission(this.user, 'moderator');
|
||||
},
|
||||
isSystemMessage () {
|
||||
return this.msg.uuid === 'system';
|
||||
},
|
||||
canLikeMessage () {
|
||||
if (this.isSystemMessage) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.privateMessageMode) {
|
||||
return Boolean(this.msg.uniqueMessageId);
|
||||
}
|
||||
|
||||
return this.msg.id;
|
||||
},
|
||||
canDeleteMessage () {
|
||||
return this.privateMessageMode
|
||||
|| this.msg.uuid === this.user._id
|
||||
|| this.userIsModerator;
|
||||
},
|
||||
canReportMessage () {
|
||||
if (this.privateMessageMode) {
|
||||
return !this.isMessageReported;
|
||||
}
|
||||
return (this.user.flags.communityGuidelinesAccepted && this.msg.uuid !== 'system')
|
||||
&& (!this.isMessageReported || this.userIsModerator);
|
||||
},
|
||||
isUserMentioned () {
|
||||
const message = this.msg;
|
||||
|
||||
if (message.highlight) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const { user } = this;
|
||||
const displayName = user.profile.name;
|
||||
const { username } = user.auth.local;
|
||||
const pattern = `@(${escapeRegExp(displayName)}|${escapeRegExp(username)})(\\b)`;
|
||||
message.highlight = new RegExp(pattern, 'i').test(message.text);
|
||||
|
||||
return message.highlight;
|
||||
},
|
||||
flagCountDescription () {
|
||||
if (!this.msg.flagCount) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (this.msg.flagCount < CHAT_FLAG_LIMIT_FOR_HIDING) {
|
||||
return 'Message flagged once, not hidden';
|
||||
}
|
||||
|
||||
if (this.msg.flagCount < CHAT_FLAG_FROM_SHADOW_MUTE) {
|
||||
return 'Message hidden';
|
||||
}
|
||||
|
||||
return 'Message hidden (shadow-muted)';
|
||||
},
|
||||
},
|
||||
mounted () {
|
||||
this.$emit('message-card-mounted');
|
||||
this.handleExternalLinks();
|
||||
this.mapProfileLinksToModal();
|
||||
},
|
||||
updated () {
|
||||
this.handleExternalLinks();
|
||||
this.mapProfileLinksToModal();
|
||||
},
|
||||
methods: {
|
||||
mapProfileLinksToModal () {
|
||||
const links = this.$refs.markdownContainer.getElementsByTagName('a');
|
||||
for (let i = 0; i < links.length; i += 1) {
|
||||
let link = links[i].pathname;
|
||||
|
||||
// Internet Explorer does not provide the leading slash character in the pathname
|
||||
link = link.charAt(0) === '/' ? link : `/${link}`;
|
||||
|
||||
if (link.startsWith('/profile/')) {
|
||||
links[i].onclick = ev => {
|
||||
ev.preventDefault();
|
||||
this.$router.push({ path: link });
|
||||
};
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
report () {
|
||||
this.$root.$on('habitica:report-result', data => {
|
||||
if (data.ok) {
|
||||
@@ -199,16 +521,29 @@ export default {
|
||||
|
||||
this.$root.$emit('habitica::report-chat', {
|
||||
message: this.msg,
|
||||
groupId: 'privateMessage',
|
||||
groupId: this.groupId,
|
||||
});
|
||||
},
|
||||
async remove () {
|
||||
if (!window.confirm(this.$t('areYouSureDeleteMessage'))) return; // eslint-disable-line no-alert
|
||||
// eslint-disable-next-line no-alert
|
||||
if (!window.confirm(this.$t('areYouSureDeleteMessage'))) {
|
||||
return;
|
||||
}
|
||||
|
||||
const message = this.msg;
|
||||
this.$emit('message-removed', message);
|
||||
|
||||
await axios.delete(`/api/v4/inbox/messages/${message.id}`);
|
||||
if (this.privateMessageMode) {
|
||||
await axios.delete(`/api/v4/inbox/messages/${message.id}`);
|
||||
} else {
|
||||
await this.$store.dispatch('chat:deleteChat', {
|
||||
groupId: this.groupId,
|
||||
chatId: message.id,
|
||||
});
|
||||
}
|
||||
},
|
||||
copy (message) {
|
||||
this.mixinCopyToClipboard(message.text, this.$t('messageCopiedToClipboard'));
|
||||
},
|
||||
parseMarkdown (text) {
|
||||
return renderWithMentions(text, this.user);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<template>
|
||||
<div
|
||||
ref="container"
|
||||
class="container-fluid"
|
||||
class="message-list"
|
||||
>
|
||||
<div class="row loadmore">
|
||||
<div class="loadmore">
|
||||
<div v-if="canLoadMore && !isLoading">
|
||||
<div class="loadmore-divider-holder">
|
||||
<div class="loadmore-divider"></div>
|
||||
@@ -28,7 +28,7 @@
|
||||
<div
|
||||
v-for="(msg) in messages"
|
||||
:key="msg.id"
|
||||
class="row message-row"
|
||||
class="message-row"
|
||||
:class="{ 'margin-right': user._id !== msg.uuid}"
|
||||
>
|
||||
<div
|
||||
@@ -39,28 +39,33 @@
|
||||
class="avatar-left"
|
||||
:member="conversationOpponentUser"
|
||||
:avatar-only="true"
|
||||
:override-top-padding="'14px'"
|
||||
:show-weapon="true"
|
||||
:debug-mode="false"
|
||||
:height="null"
|
||||
:override-top-padding="'0'"
|
||||
:hide-class-badge="true"
|
||||
@click.native="showMemberModal(msg.uuid)"
|
||||
/>
|
||||
<div
|
||||
class="card"
|
||||
:class="{'card-right': user._id !== msg.uuid, 'card-left': user._id === msg.uuid}"
|
||||
>
|
||||
<message-card
|
||||
:msg="msg"
|
||||
@message-removed="messageRemoved"
|
||||
@show-member-modal="showMemberModal"
|
||||
@message-card-mounted="itemWasMounted"
|
||||
/>
|
||||
</div>
|
||||
<message-card
|
||||
:msg="msg"
|
||||
:user-sent-message="user._id === msg.uuid"
|
||||
:group-id="'privateMessage'"
|
||||
:private-message-mode="true"
|
||||
@message-liked="messageLiked"
|
||||
@message-removed="messageRemoved"
|
||||
@show-member-modal="showMemberModal"
|
||||
@message-card-mounted="itemWasMounted"
|
||||
/>
|
||||
<avatar
|
||||
v-if="user && user._id === msg.uuid"
|
||||
class="avatar-right"
|
||||
:member="user"
|
||||
:height="null"
|
||||
:avatar-only="true"
|
||||
:show-weapon="true"
|
||||
:debug-mode="false"
|
||||
:hide-class-badge="true"
|
||||
:override-top-padding="'14px'"
|
||||
:override-top-padding="'0'"
|
||||
@click.native="showMemberModal(msg.uuid)"
|
||||
/>
|
||||
</div>
|
||||
@@ -69,121 +74,110 @@
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
|
||||
.avatar {
|
||||
width: 170px;
|
||||
min-width: 8rem;
|
||||
height: 120px;
|
||||
padding-top: 0 !important;
|
||||
.avatar-left, .avatar-right {
|
||||
align-self: center;
|
||||
|
||||
::v-deep .character-sprites {
|
||||
margin-bottom: -5px !important;
|
||||
padding-bottom: 0 !important;
|
||||
margin-top: -1px !important;
|
||||
}
|
||||
|
||||
.avatar-right {
|
||||
margin-left: -1rem;
|
||||
::v-deep .avatar {
|
||||
margin-left: -1.75rem;
|
||||
margin-right: -0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
::v-deep .character-sprites {
|
||||
margin-right: 1rem !important;
|
||||
.card {
|
||||
border: 0px;
|
||||
margin-bottom: 1rem;
|
||||
padding: 0rem;
|
||||
width: 684px;
|
||||
|
||||
}
|
||||
|
||||
.message-list {
|
||||
width: 100%;
|
||||
padding-right: 10px;
|
||||
margin-right: 0 !important;
|
||||
}
|
||||
|
||||
.message-row {
|
||||
margin-left: 12px;
|
||||
margin-right: 0;
|
||||
margin-bottom: 1.2rem;
|
||||
|
||||
&:not(.margin-right) {
|
||||
.d-flex {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
border: 0px;
|
||||
margin-bottom: 1rem;
|
||||
padding: 0rem;
|
||||
width: 684px;
|
||||
}
|
||||
.message-row {
|
||||
margin-left: 12px;
|
||||
margin-right: 12px;
|
||||
.hr {
|
||||
width: 100%;
|
||||
height: 20px;
|
||||
border-bottom: 1px solid $gray-500;
|
||||
text-align: center;
|
||||
margin: 2em 0;
|
||||
}
|
||||
|
||||
&:not(.margin-right) {
|
||||
.d-flex {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
}
|
||||
@media only screen and (max-width: 1200px) {
|
||||
.card {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
.hr-middle {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
font-family: 'Roboto Condensed';
|
||||
line-height: 1.5;
|
||||
text-align: center;
|
||||
color: $gray-200;
|
||||
background-color: $gray-700;
|
||||
padding: .2em;
|
||||
margin-top: .2em;
|
||||
display: inline-block;
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 1400px) {
|
||||
.message-row {
|
||||
margin-left: -15px;
|
||||
margin-right: -30px;
|
||||
}
|
||||
}
|
||||
.loadmore {
|
||||
justify-content: center;
|
||||
margin-right: 12px;
|
||||
margin-top: 12px;
|
||||
margin-bottom: 24px;
|
||||
|
||||
.card-left {
|
||||
border: 1px solid $purple-500;
|
||||
}
|
||||
|
||||
.card-right {
|
||||
border: 1px solid $gray-500;
|
||||
}
|
||||
|
||||
.hr {
|
||||
> div {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 20px;
|
||||
border-bottom: 1px solid $gray-500;
|
||||
text-align: center;
|
||||
margin: 2em 0;
|
||||
}
|
||||
align-items: center;
|
||||
|
||||
.hr-middle {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
font-family: 'Roboto Condensed';
|
||||
line-height: 1.5;
|
||||
text-align: center;
|
||||
color: $gray-200;
|
||||
background-color: $gray-700;
|
||||
padding: .2em;
|
||||
margin-top: .2em;
|
||||
display: inline-block;
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.loadmore {
|
||||
justify-content: center;
|
||||
margin-right: 12px;
|
||||
margin-top: 12px;
|
||||
margin-bottom: 24px;
|
||||
|
||||
> div {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
|
||||
button {
|
||||
text-align: center;
|
||||
color: $gray-50;
|
||||
}
|
||||
button {
|
||||
text-align: center;
|
||||
color: $gray-50;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.loadmore-divider-holder {
|
||||
flex: 1;
|
||||
margin-left: 24px;
|
||||
margin-right: 24px;
|
||||
.loadmore-divider-holder {
|
||||
flex: 1;
|
||||
margin-left: 24px;
|
||||
margin-right: 24px;
|
||||
|
||||
&:last-of-type {
|
||||
margin-right: 0;
|
||||
}
|
||||
&:last-of-type {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.loadmore-divider {
|
||||
height: 1px;
|
||||
border-top: 1px $gray-500 solid;
|
||||
width: 100%;
|
||||
.loadmore-divider {
|
||||
height: 1px;
|
||||
border-top: 1px $gray-500 solid;
|
||||
width: 100%;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.loading {
|
||||
padding-left: 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.loading {
|
||||
padding-left: 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@@ -280,6 +274,9 @@ export default {
|
||||
// container.style.overflowY = 'scroll';
|
||||
}
|
||||
}, 50),
|
||||
messageLiked (message) {
|
||||
this.$emit('message-liked', message);
|
||||
},
|
||||
messageRemoved (message) {
|
||||
this.$emit('message-removed', message);
|
||||
},
|
||||
|
||||
@@ -692,7 +692,7 @@
|
||||
<div class="form-inline clearfix">
|
||||
<Sprite
|
||||
class="pull-left"
|
||||
:class="'inventory_quest_scroll_' + item.key"
|
||||
:image-name="'inventory_quest_scroll_' + item.key"
|
||||
style="margin-right: 10px"
|
||||
/>
|
||||
<p>{{ item.text() }}</p>
|
||||
|
||||
@@ -107,7 +107,7 @@ export default {
|
||||
if (lastPublishedPost) this.posts.push(lastPublishedPost);
|
||||
|
||||
// If the user is authorized, show any draft
|
||||
if (this.user && this.user.contributor.newsPoster) {
|
||||
if (this.user && (this.user.permissions.news || this.user.permissions.fullAccess)) {
|
||||
this.posts.unshift(
|
||||
...postsFromServer
|
||||
.filter(p => !p.published || moment().isBefore(p.publishDate)),
|
||||
|
||||
@@ -843,7 +843,6 @@ export default {
|
||||
purchasedPlanIdInfo () {
|
||||
if (!this.subscriptionBlocks[this.user.purchased.plan.planId]) {
|
||||
// @TODO: find which subs are in the common
|
||||
// console.log(this.subscriptionBlocks
|
||||
// [this.user.purchased.plan.planId]); // eslint-disable-line
|
||||
return {
|
||||
price: 0,
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
<div
|
||||
v-for="currency of currencies"
|
||||
:key="currency.key"
|
||||
:needed-currency-only="neededCurrencyOnly"
|
||||
class="d-flex align-items-center"
|
||||
>
|
||||
<div
|
||||
@@ -54,6 +55,9 @@ export default {
|
||||
amountNeeded: {
|
||||
type: Number,
|
||||
},
|
||||
neededCurrencyOnly: {
|
||||
type: Boolean,
|
||||
},
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
@@ -66,34 +70,34 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
currencies () {
|
||||
const currencies = [];
|
||||
currencies.push({
|
||||
const currencies = [{
|
||||
type: 'hourglasses',
|
||||
icon: this.icons.hourglasses,
|
||||
value: this.userHourglasses,
|
||||
});
|
||||
},
|
||||
|
||||
currencies.push({
|
||||
{
|
||||
type: 'gems',
|
||||
icon: this.icons.gem,
|
||||
value: this.userGems,
|
||||
});
|
||||
},
|
||||
|
||||
currencies.push({
|
||||
{
|
||||
type: 'gold',
|
||||
icon: this.icons.gold,
|
||||
value: this.userGold,
|
||||
});
|
||||
}];
|
||||
|
||||
for (const currency of currencies) {
|
||||
if (
|
||||
currency.type === this.currencyNeeded
|
||||
&& !this.enoughCurrency(this.currencyNeeded, this.amountNeeded)
|
||||
if (currency.type === this.currencyNeeded
|
||||
&& !this.enoughCurrency(this.currencyNeeded, this.amountNeeded)
|
||||
) {
|
||||
currency.notEnough = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.neededCurrencyOnly) {
|
||||
return currencies.filter(curr => curr.type === this.currencyNeeded);
|
||||
}
|
||||
return currencies;
|
||||
},
|
||||
},
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
}"
|
||||
>
|
||||
<input
|
||||
ref="textInput"
|
||||
:value="value"
|
||||
class="form-control"
|
||||
:type="inputType"
|
||||
@@ -29,19 +30,23 @@
|
||||
}"
|
||||
:readonly="readonly"
|
||||
:aria-readonly="readonly"
|
||||
autocomplete="off"
|
||||
|
||||
:placeholder="placeholder"
|
||||
@keyup="handleChange"
|
||||
@keyup.enter="$emit('enter')"
|
||||
@blur="$emit('blur')"
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
v-for="issue in invalidIssues"
|
||||
:key="issue"
|
||||
class="input-error"
|
||||
>
|
||||
{{ issue }}
|
||||
</div>
|
||||
<template v-if="!hideErrorLine">
|
||||
<div
|
||||
v-for="issue in invalidIssues"
|
||||
:key="issue"
|
||||
class="input-error"
|
||||
>
|
||||
{{ issue }}
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -85,6 +90,10 @@ export default {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
hideErrorLine: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
@@ -107,6 +116,9 @@ export default {
|
||||
this.wasChanged = true;
|
||||
this.$emit('update:value', value);
|
||||
},
|
||||
focus () {
|
||||
this.$refs.textInput.focus();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -128,4 +140,12 @@ export default {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* this removes safari "save username" UI, we only search for one, we dont want to save it */
|
||||
input::-webkit-contacts-auto-fill-button,
|
||||
input::-webkit-credentials-auto-fill-button {
|
||||
visibility: hidden;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@@ -29,20 +29,12 @@
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
|
||||
.user-link { // this is the user name
|
||||
font-family: 'Roboto Condensed', sans-serif;
|
||||
font-weight: bold;
|
||||
margin-bottom: 0;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
font-size: 16px;
|
||||
|
||||
// currently used in the member-details-new.vue
|
||||
&.smaller {
|
||||
font-family: Roboto;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
line-height: 1.71;
|
||||
}
|
||||
font-size: 14px;
|
||||
line-height: 1.71;
|
||||
display: inline-flex !important;
|
||||
|
||||
&.no-tier {
|
||||
color: $gray-50;
|
||||
@@ -111,7 +103,6 @@ export default {
|
||||
'backer',
|
||||
'contributor',
|
||||
'hideTooltip',
|
||||
'smallerStyle',
|
||||
'showBuffed',
|
||||
'context',
|
||||
],
|
||||
@@ -173,7 +164,7 @@ export default {
|
||||
return this.hideTooltip ? '' : achievementsLib.getContribText(this.contributor, this.isNPC) || '';
|
||||
},
|
||||
levelStyle () {
|
||||
return `${this.userLevelStyleFromLevel(this.level, this.isNPC)} ${this.smallerStyle ? 'smaller' : ''}`;
|
||||
return `${this.userLevelStyleFromLevel(this.level, this.isNPC)}`;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
import debounce from 'lodash/debounce';
|
||||
|
||||
export const autoCompleteHelperMixin = {
|
||||
data () {
|
||||
return {
|
||||
mixinData: {
|
||||
autoComplete: {
|
||||
caretPosition: 0,
|
||||
coords: {
|
||||
TOP: 0,
|
||||
LEFT: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
autoCompleteMixinHandleTab (e) {
|
||||
if (this.$refs.autocomplete.searchActive) {
|
||||
e.preventDefault();
|
||||
if (e.shiftKey) {
|
||||
this.$refs.autocomplete.selectPrevious();
|
||||
} else {
|
||||
this.$refs.autocomplete.selectNext();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
autoCompleteMixinHandleEscape (e) {
|
||||
if (this.$refs.autocomplete.searchActive) {
|
||||
e.preventDefault();
|
||||
this.$refs.autocomplete.cancel();
|
||||
}
|
||||
},
|
||||
|
||||
autoCompleteMixinSelectNextAutocomplete (e) {
|
||||
if (this.$refs.autocomplete.searchActive) {
|
||||
e.preventDefault();
|
||||
this.$refs.autocomplete.selectNext();
|
||||
}
|
||||
},
|
||||
|
||||
autoCompleteMixinSelectPreviousAutocomplete (e) {
|
||||
if (this.$refs.autocomplete.searchActive) {
|
||||
e.preventDefault();
|
||||
this.$refs.autocomplete.selectPrevious();
|
||||
}
|
||||
},
|
||||
|
||||
autoCompleteMixinSelectAutocomplete (e) {
|
||||
if (this.$refs.autocomplete.searchActive) {
|
||||
if (this.$refs.autocomplete.selected !== null) {
|
||||
e.preventDefault();
|
||||
this.$refs.autocomplete.makeSelection();
|
||||
} else {
|
||||
// no autocomplete selected, newline instead
|
||||
this.$refs.autocomplete.cancel();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
autoCompleteMixinUpdateCarretPosition: debounce(function updateCarretPosition (eventUpdate) {
|
||||
this._updateCarretPosition(eventUpdate);
|
||||
}, 250),
|
||||
|
||||
autoCompleteMixinResetCoordsPosition () {
|
||||
this.mixinData.autoComplete.coords = {
|
||||
TOP: 0,
|
||||
LEFT: 0,
|
||||
};
|
||||
},
|
||||
|
||||
// https://medium.com/@_jh3y/how-to-where-s-the-caret-getting-the-xy-position-of-the-caret-a24ba372990a
|
||||
_getCoord (e, text) {
|
||||
const caretPosition = text.selectionEnd;
|
||||
this.mixinData.autoComplete.caretPosition = caretPosition;
|
||||
|
||||
const div = document.createElement('div');
|
||||
const span = document.createElement('span');
|
||||
const copyStyle = getComputedStyle(text);
|
||||
|
||||
[].forEach.call(copyStyle, prop => {
|
||||
div.style[prop] = copyStyle[prop];
|
||||
});
|
||||
|
||||
div.style.position = 'absolute';
|
||||
document.body.appendChild(div);
|
||||
div.textContent = text.value.substr(0, caretPosition);
|
||||
span.textContent = text.value.substr(caretPosition) || '.';
|
||||
div.appendChild(span);
|
||||
this.mixinData.autoComplete.coords = {
|
||||
TOP: span.offsetTop,
|
||||
LEFT: span.offsetLeft,
|
||||
};
|
||||
document.body.removeChild(div);
|
||||
},
|
||||
_updateCarretPosition (eventUpdate) {
|
||||
const text = eventUpdate.target;
|
||||
this._getCoord(eventUpdate, text);
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -1,7 +1,7 @@
|
||||
import notifications from './notifications';
|
||||
import { NotificationMixins } from './notifications';
|
||||
|
||||
export default {
|
||||
mixins: [notifications],
|
||||
export const CopyToClipboardMixin = {
|
||||
mixins: [NotificationMixins],
|
||||
methods: {
|
||||
async mixinCopyToClipboard (valueToCopy, notificationToShow = null) {
|
||||
if (navigator.clipboard) {
|
||||
@@ -21,3 +21,5 @@ export default {
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default CopyToClipboardMixin;
|
||||
|
||||
@@ -2,54 +2,55 @@ import includes from 'lodash/includes';
|
||||
|
||||
export default {
|
||||
methods: {
|
||||
foolPet (pet) {
|
||||
foolPet (pet, prank) {
|
||||
const SPECIAL_PETS = [
|
||||
'Wolf-Veteran',
|
||||
'Wolf-Cerberus',
|
||||
'Dragon-Hydra',
|
||||
'Turkey-Base',
|
||||
'BearCub-Polar',
|
||||
'MantisShrimp-Base',
|
||||
'JackOLantern-Base',
|
||||
'Mammoth-Base',
|
||||
'Tiger-Veteran',
|
||||
'Phoenix-Base',
|
||||
'Turkey-Gilded',
|
||||
'MagicalBee-Base',
|
||||
'Lion-Veteran',
|
||||
'Gryphon-RoyalPurple',
|
||||
'JackOLantern-Ghost',
|
||||
'Jackalope-RoyalPurple',
|
||||
'Orca-Base',
|
||||
'Bear-Veteran',
|
||||
'Hippogriff-Hopeful',
|
||||
'Fox-Veteran',
|
||||
'JackOLantern-Glow',
|
||||
'Gryphon-Gryphatrice',
|
||||
'Gryphatrice-Jubilant',
|
||||
'JackOLantern-RoyalPurple',
|
||||
'BearCub-Polar',
|
||||
'Cactus-Veteran',
|
||||
'Dragon-Hydra',
|
||||
'Dragon-Veteran',
|
||||
'Fox-Veteran',
|
||||
'Gryphatrice-Jubilant',
|
||||
'Gryphon-Gryphatrice',
|
||||
'Gryphon-RoyalPurple',
|
||||
'Hippogriff-Hopeful',
|
||||
'Jackalope-RoyalPurple',
|
||||
'JackOLantern-Base',
|
||||
'JackOLantern-Ghost',
|
||||
'JackOLantern-Glow',
|
||||
'JackOLantern-RoyalPurple',
|
||||
'Lion-Veteran',
|
||||
'MagicalBee-Base',
|
||||
'Mammoth-Base',
|
||||
'MantisShrimp-Base',
|
||||
'Orca-Base',
|
||||
'Phoenix-Base',
|
||||
'Tiger-Veteran',
|
||||
'Turkey-Base',
|
||||
'Turkey-Gilded',
|
||||
'Wolf-Cerberus',
|
||||
'Wolf-Veteran',
|
||||
];
|
||||
const BASE_PETS = [
|
||||
'Wolf',
|
||||
'TigerCub',
|
||||
'PandaCub',
|
||||
'LionCub',
|
||||
'Fox',
|
||||
'FlyingPig',
|
||||
'BearCub',
|
||||
'Dragon',
|
||||
'Cactus',
|
||||
'Dragon',
|
||||
'FlyingPig',
|
||||
'Fox',
|
||||
'LionCub',
|
||||
'PandaCub',
|
||||
'TigerCub',
|
||||
'Wolf',
|
||||
];
|
||||
if (!pet) return 'Pet-TigerCub-Fungi';
|
||||
if (!pet) return `Pet-TigerCub-${prank}`;
|
||||
if (SPECIAL_PETS.indexOf(pet) !== -1) {
|
||||
return 'Pet-Dragon-Fungi';
|
||||
return `Pet-Dragon-${prank}`;
|
||||
}
|
||||
const species = pet.slice(0, pet.indexOf('-'));
|
||||
if (includes(BASE_PETS, species)) {
|
||||
return `Pet-${species}-Fungi`;
|
||||
return `Pet-${species}-${prank}`;
|
||||
}
|
||||
return 'Pet-BearCub-Fungi';
|
||||
return `Pet-BearCub-${prank}`;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
+620
-417
File diff suppressed because it is too large
Load Diff
+3
-3
@@ -62,7 +62,7 @@
|
||||
|
||||
<script>
|
||||
import moment from 'moment';
|
||||
import userLabel from '../userLabel';
|
||||
import userLabel from '../../components/userLabel.vue';
|
||||
|
||||
import dots from '@/assets/svg/dots.svg';
|
||||
import block from '@/assets/svg/block.svg';
|
||||
@@ -117,7 +117,7 @@ export default {
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
@import '~@/assets/scss/colors';
|
||||
|
||||
.action-padding {
|
||||
height: 24px !important;
|
||||
@@ -153,7 +153,7 @@ export default {
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
@import '~@/assets/scss/colors';
|
||||
|
||||
.conversation {
|
||||
padding: 1rem 1.5rem;
|
||||
@@ -0,0 +1,64 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="filtersConversations.length > 0"
|
||||
class="conversations"
|
||||
>
|
||||
<conversation-item
|
||||
v-for="conversation in filtersConversations"
|
||||
:key="conversation.key"
|
||||
:active-key="selectedConversation?.key"
|
||||
:contributor="conversation.contributor"
|
||||
:backer="conversation.backer"
|
||||
:uuid="conversation.key"
|
||||
:display-name="conversation.name"
|
||||
:username="conversation.username"
|
||||
:last-message-date="conversation.date"
|
||||
:last-message-text="conversation.lastMessageText
|
||||
? removeTags(parseMarkdown(conversation.lastMessageText)) : ''"
|
||||
@click="selectConversation(conversation.key)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.conversations {
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
|
||||
import { defineComponent } from 'vue';
|
||||
import habiticaMarkdown from 'habitica-markdown';
|
||||
import conversationItem from '@/pages/private-messages/pm-conversation-item.vue';
|
||||
|
||||
export default defineComponent({
|
||||
components: { conversationItem },
|
||||
props: {
|
||||
filtersConversations: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
selectedConversation: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
removeTags (html) {
|
||||
const tmp = document.createElement('DIV');
|
||||
tmp.innerHTML = html;
|
||||
return tmp.textContent || tmp.innerText || '';
|
||||
},
|
||||
parseMarkdown (text) {
|
||||
if (!text) return null;
|
||||
return habiticaMarkdown.render(String(text));
|
||||
},
|
||||
selectConversation (conversationKey) {
|
||||
this.$emit('selectConversation', conversationKey);
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,37 @@
|
||||
<template>
|
||||
<div
|
||||
class="pm-disabled-caption text-center"
|
||||
>
|
||||
<h4>{{ disabledTexts.title }}</h4>
|
||||
<p>{{ disabledTexts.description }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
|
||||
.pm-disabled-caption {
|
||||
padding-top: 1.5em;
|
||||
z-index: 2;
|
||||
|
||||
h4, p {
|
||||
color: $gray-200;
|
||||
}
|
||||
|
||||
h4 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0.4em;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 12px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: ['disabledTexts'],
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,71 @@
|
||||
<template>
|
||||
<div
|
||||
class="empty-messages m-auto text-center empty-sidebar"
|
||||
>
|
||||
<div class="no-messages-box">
|
||||
<div
|
||||
v-once
|
||||
class="svg-icon envelope mb-4"
|
||||
v-html="icons.mailIcon"
|
||||
></div>
|
||||
<strong
|
||||
v-once
|
||||
class="mb-1"
|
||||
>
|
||||
{{ $t('emptyMessagesLine1') }}
|
||||
</strong>
|
||||
<p v-if="!chatRevoked">
|
||||
{{ $t('emptyMessagesLine2') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="btn btn-primary mt-4 d-flex align-items-center"
|
||||
@click="$emit('newMessageClicked')"
|
||||
>
|
||||
<div
|
||||
class="svg-icon icon-10 color mr-2"
|
||||
v-html="icons.positive"
|
||||
></div>
|
||||
{{ $t('newMessage') }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
|
||||
strong {
|
||||
line-height: 1.71;
|
||||
color: $gray-100;
|
||||
}
|
||||
|
||||
.svg-icon.icon-10 {
|
||||
margin: 3px;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 24px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import mailIcon from '@/assets/svg/mail.svg';
|
||||
import positiveIcon from '@/assets/svg/positive.svg';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
chatRevoked: Boolean,
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
icons: Object.freeze({
|
||||
mailIcon,
|
||||
positive: positiveIcon,
|
||||
}),
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,69 @@
|
||||
<template>
|
||||
<div
|
||||
v-once
|
||||
class="centered empty-messages m-auto text-center"
|
||||
>
|
||||
<avatar
|
||||
v-if="memberObj"
|
||||
:member="memberObj"
|
||||
:avatar-only="true"
|
||||
:show-weapon="false"
|
||||
:hide-class-badge="true"
|
||||
:override-top-padding="'0px'"
|
||||
:sprites-margin="'0 0 0 -30px'"
|
||||
:debug-mode="false"
|
||||
:center-avatar="true"
|
||||
class="mb-3"
|
||||
/>
|
||||
|
||||
<strong>{{ memberObj.profile.name }}</strong>
|
||||
<div class="username mb-3">
|
||||
@{{ memberObj.auth.local.username }}
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="kind-text"
|
||||
v-html="$t('rememberToBeKind')"
|
||||
></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
|
||||
.centered {
|
||||
align-content: center;
|
||||
}
|
||||
|
||||
.center-avatar {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
strong {
|
||||
line-height: 1.71;
|
||||
}
|
||||
|
||||
.username {
|
||||
font-size: 12px;
|
||||
line-height: 1.33;
|
||||
color: $gray-100;
|
||||
margin-top: -4px;
|
||||
}
|
||||
|
||||
.kind-text {
|
||||
width: 330px;
|
||||
line-height: 1.71;
|
||||
color: $gray-100;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import Avatar from '@/components/avatar.vue';
|
||||
|
||||
export default {
|
||||
components: { Avatar },
|
||||
props: {
|
||||
memberObj: null,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,44 @@
|
||||
export namespace PrivateMessages {
|
||||
// Shared properties between message types
|
||||
interface SharedMessageProps {
|
||||
username: string;
|
||||
contributor: Record<string, unknown>;
|
||||
userStyles: Record<string, unknown>;
|
||||
canReceive: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the Type we get from our API
|
||||
*/
|
||||
interface ConversationSummaryMessageEntry extends SharedMessageProps {
|
||||
uuid: string;
|
||||
user: string;
|
||||
|
||||
timestamp: string;
|
||||
text: string;
|
||||
count: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* The Visual (Sidebar) Entry
|
||||
*/
|
||||
interface ConversationEntry extends SharedMessageProps {
|
||||
/**
|
||||
* UUID
|
||||
*/
|
||||
key: string;
|
||||
name: string;
|
||||
|
||||
|
||||
lastMessageText: '',
|
||||
canLoadMore: boolean;
|
||||
page: 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Loaded Private Messages, partial type
|
||||
*/
|
||||
interface PrivateMessageEntry extends SharedMessageProps {
|
||||
text: string;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
<template>
|
||||
<div class="ml-4">
|
||||
<strong
|
||||
v-once
|
||||
v-html="$t('to')"
|
||||
></strong>
|
||||
<validated-text-input
|
||||
id="selectUser"
|
||||
ref="targetUserInput"
|
||||
v-model="targetUserInputValue"
|
||||
class="mx-2"
|
||||
:is-valid="foundUser._id"
|
||||
:only-show-invalid-state="foundUser._id === undefined"
|
||||
|
||||
:hide-error-line="true"
|
||||
:placeholder="$t('usernameOrUserId')"
|
||||
:invalid-issues="userInputInvalidIssues"
|
||||
@enter="triggerNewConversation"
|
||||
/>
|
||||
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
:disabled="preventTrigger"
|
||||
@click="triggerNewConversation()"
|
||||
>
|
||||
{{ $t('confirm') }}
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="ml-2 btn btn-secondary"
|
||||
@click="$emit('cancelNewConversation')"
|
||||
>
|
||||
{{ $t('cancel') }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
|
||||
div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
div > * {
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
strong {
|
||||
line-height: 1.71;
|
||||
align-content: center;
|
||||
}
|
||||
|
||||
input {
|
||||
border-radius: 2px;
|
||||
border-width: 2px;
|
||||
|
||||
width: 420px;
|
||||
}
|
||||
|
||||
#selectUser {
|
||||
/* changing the style of validate-text-input to the same as others */
|
||||
::v-deep {
|
||||
.input-group {
|
||||
border-width: 2px;
|
||||
|
||||
input {
|
||||
width: 420px;
|
||||
height: 100%;
|
||||
color: $gray-50;
|
||||
}
|
||||
}
|
||||
|
||||
.input-group {
|
||||
&:focus, &:active, &:focus-within {
|
||||
border: solid 2px $purple-400;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<script>
|
||||
|
||||
import debounce from 'lodash/debounce';
|
||||
import isUUID from 'validator/es/lib/isUUID';
|
||||
import ValidatedTextInput from '@/components/ui/validatedTextInput.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ValidatedTextInput,
|
||||
},
|
||||
mixins: [],
|
||||
data () {
|
||||
return {
|
||||
targetUserInputValue: '',
|
||||
userNotFound: false,
|
||||
foundUser: {},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
preventTrigger () {
|
||||
return this.targetUserInputValue.length < 2;
|
||||
},
|
||||
userInputInvalidIssues () {
|
||||
return this.targetUserInputValue.length > 0 && this.userNotFound
|
||||
? [this.$t('userWithUsernameOrUserIdNotFound')]
|
||||
: [''];
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
targetUserInputValue: {
|
||||
handler () {
|
||||
this.searchUser(this.targetUserInputValue.replace('@', ''));
|
||||
},
|
||||
},
|
||||
},
|
||||
mounted () {
|
||||
this.$refs.targetUserInput.focus();
|
||||
},
|
||||
methods: {
|
||||
close () {
|
||||
this.$root.$emit('bv::hide::modal', 'select-user-modal');
|
||||
},
|
||||
searchUser: debounce(async function userSearch (searchTerm = '') {
|
||||
this.foundUser = {};
|
||||
if (searchTerm.length < 1) {
|
||||
this.userNotFound = false;
|
||||
return;
|
||||
}
|
||||
let result;
|
||||
if (isUUID(searchTerm)) {
|
||||
try {
|
||||
result = await this.$store.dispatch('members:fetchMember', {
|
||||
memberId: searchTerm,
|
||||
});
|
||||
} catch {
|
||||
result = null;
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
result = await this.$store.dispatch('members:fetchMemberByUsername', {
|
||||
username: searchTerm,
|
||||
});
|
||||
} catch {
|
||||
result = null;
|
||||
}
|
||||
}
|
||||
if (!result) {
|
||||
this.userNotFound = true;
|
||||
return;
|
||||
}
|
||||
this.userNotFound = false;
|
||||
this.foundUser = result;
|
||||
}, 500),
|
||||
triggerNewConversation () {
|
||||
const userWithoutAt = this.$refs.targetUserInput.value.replace('@', '');
|
||||
|
||||
this.$emit('startNewConversation', userWithoutAt);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -11,6 +11,7 @@
|
||||
class="balance-info"
|
||||
:currency-needed="currencyNeeded"
|
||||
:amount-needed="amountNeeded"
|
||||
:neededCurrencyOnly="true"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -66,6 +66,7 @@
|
||||
<your-balance
|
||||
:amount-needed="amountNeeded"
|
||||
currency-needed="gems"
|
||||
class="d-flex align-items-center"
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
@@ -49,7 +49,7 @@ const GroupPlanIndex = () => import(/* webpackChunkName: "group-plans" */ '@/com
|
||||
const GroupPlanTaskInformation = () => import(/* webpackChunkName: "group-plans" */ '@/components/group-plans/taskInformation');
|
||||
const GroupPlanBilling = () => import(/* webpackChunkName: "group-plans" */ '@/components/group-plans/billing');
|
||||
|
||||
const MessagesIndex = () => import(/* webpackChunkName: "private-messages" */ '@/pages/private-messages');
|
||||
const MessagesIndex = () => import(/* webpackChunkName: "private-messages" */ '@/pages/private-messages/index.vue');
|
||||
|
||||
// Challenges
|
||||
const ChallengeIndex = () => import(/* webpackChunkName: "challenges" */ '@/components/challenges/index');
|
||||
@@ -190,7 +190,6 @@ const router = new VueRouter({
|
||||
meta: {
|
||||
privilegeNeeded: [ // any one of these is enough to give access
|
||||
'userSupport',
|
||||
'newsPoster',
|
||||
],
|
||||
},
|
||||
children: [
|
||||
|
||||
@@ -5,3 +5,9 @@ export async function searchUsers (store, payload) {
|
||||
const response = await axios.get(url);
|
||||
return response.data.data;
|
||||
}
|
||||
|
||||
export async function getUserHistory (store, payload) {
|
||||
const url = `/api/v4/admin/user/${payload.userIdentifier}/history`;
|
||||
const response = await axios.get(url);
|
||||
return response.data.data;
|
||||
}
|
||||
|
||||
@@ -43,7 +43,14 @@ export async function deleteChat (store, payload) {
|
||||
}
|
||||
|
||||
export async function like (store, payload) {
|
||||
const url = `/api/v4/groups/${payload.groupId}/chat/${payload.chatId}/like`;
|
||||
let url = '';
|
||||
|
||||
if (payload.groupId === 'privateMessage') {
|
||||
url = `/api/v4/inbox/like-private-message/${payload.chatMessageId}`;
|
||||
} else {
|
||||
url = `/api/v4/groups/${payload.groupId}/chat/${payload.chatMessageId}/like`;
|
||||
}
|
||||
|
||||
const response = await axios.post(url);
|
||||
return response.data.data;
|
||||
}
|
||||
|
||||
@@ -32,3 +32,9 @@ export async function getHeroParty (store, payload) {
|
||||
const response = await axios.get(url);
|
||||
return response.data.data;
|
||||
}
|
||||
|
||||
export async function getHeroGroupPlans (store, payload) {
|
||||
const url = `/api/v4/hall/heroes/${payload.heroId}/group-plans`;
|
||||
const response = await axios.get(url);
|
||||
return response.data.data;
|
||||
}
|
||||
|
||||
@@ -72,7 +72,11 @@ describe('LevelUp', () => {
|
||||
|
||||
it('generates the right test class for level 15', () => {
|
||||
const questClass = testFunction('questClass', 15);
|
||||
expect(questClass()).to.equal('inventory_quest_scroll_atom1');
|
||||
});
|
||||
|
||||
expect(questClass()).to.equal('scroll inventory_quest_scroll_atom1');
|
||||
it('generates empty test class for level 14', () => {
|
||||
const questClass = testFunction('questClass', 14);
|
||||
expect(questClass()).to.equal('');
|
||||
});
|
||||
});
|
||||
|
||||
+5
-3
@@ -1,14 +1,16 @@
|
||||
import Vue from 'vue';
|
||||
import { shallowMount, createLocalVue } from '@vue/test-utils';
|
||||
|
||||
import ChatCard from '@/components/chat/chatCard.vue';
|
||||
import BootstrapVue from 'bootstrap-vue';
|
||||
import MessageCard from '@/components/messages/messageCard.vue';
|
||||
import Store from '@/libs/store';
|
||||
|
||||
const localVue = createLocalVue();
|
||||
localVue.use(Store);
|
||||
localVue.use(Vue.directive('b-tooltip', {}));
|
||||
localVue.use(BootstrapVue);
|
||||
|
||||
describe('ChatCard', () => {
|
||||
describe('MessageCard', () => {
|
||||
function createMessage (text) {
|
||||
return { text, likes: {} };
|
||||
}
|
||||
@@ -26,7 +28,7 @@ describe('ChatCard', () => {
|
||||
let wrapper;
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = shallowMount(ChatCard, {
|
||||
wrapper = shallowMount(MessageCard, {
|
||||
propsData: { msg: message },
|
||||
store: new Store({
|
||||
state: {
|
||||
@@ -91,7 +91,7 @@
|
||||
"conText": "Якостта намалява щетите от отрицателни навици и пропуснати ежедневни задачи.",
|
||||
"perception": "Усет",
|
||||
"perText": "Усетът увеличава спечеленото злато, а след отключването на пазара увеличава вероятността за намиране на предмети след приключване на задачи.",
|
||||
"intelligence": "Интелигентност",
|
||||
"intelligence": "Интелект",
|
||||
"intText": "Интелигентността увеличава спечеления опит, а след отключването на класовете определя максималната мана за използване за класовите умения.",
|
||||
"levelBonus": "Бонус за ниво",
|
||||
"allocatedPoints": "Разпределени точки",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"quests": "Мисии",
|
||||
"quest": "мисия",
|
||||
"petQuests": "Мисии за любимци и превози",
|
||||
"petQuests": "Мисии за домашни любимци и ездитни животни",
|
||||
"unlockableQuests": "Мисии, които могат да бъдат отключени",
|
||||
"goldQuests": "Последователности от мисии на класовите повелители",
|
||||
"questDetails": "Подробности за мисията",
|
||||
@@ -29,7 +29,7 @@
|
||||
"collected": "Събрани",
|
||||
"abort": "Прекратяване",
|
||||
"leaveQuest": "Напускане на мисията",
|
||||
"sureLeave": "Наистина ли искате да се откажете от текущата мисия? Ще загубите целия си напредък.",
|
||||
"sureLeave": "Сигурни ли сте, че искате да се откажете от мисията? Ще загубите целия си напредък.",
|
||||
"mustComplete": "Трябва първо да завършите <%= quest %>.",
|
||||
"mustLvlQuest": "Трябва да бъдете ниво <%= level %>, за да купите тази мисия!",
|
||||
"unlockByQuesting": "За да отключите тази мисия, първо завършете <%= title %>.",
|
||||
@@ -78,5 +78,8 @@
|
||||
"questAlreadyStartedFriendly": "Мисията вече е започнала, но винаги може да хванете следващата!",
|
||||
"questAlreadyStarted": "Мисията вече е започнала.",
|
||||
"questInvitationNotificationInfo": "Получихте покана за присъединяване към мисия",
|
||||
"hatchingPotionQuests": "Мисии за Магическа Излюпваща Отвара"
|
||||
"hatchingPotionQuests": "Мисии за Магическа Излюпваща Отвара",
|
||||
"bossDamage": "Нанесохте вреда на главатаря!",
|
||||
"questItemsPending": "<%= amount %> предмета ще бъдат събрани",
|
||||
"sureLeaveInactive": "Сигурни ли сте, че искате да се откажете от мисията? Няма да можете да участвате в нея."
|
||||
}
|
||||
|
||||
@@ -58,11 +58,11 @@
|
||||
"foundNewItemsCTA": "Podívej se do tvého Inventáře a zkus zkombinovat tvůj nový líhnoucí lektvar a vajíčko!",
|
||||
"foundNewItemsExplanation": "Splnění úkolů ti dá šanci najít předměty jako vajíčka, líhnoucí lektvary a jídlo pro mazlíčky.",
|
||||
"foundNewItems": "Nové předměty nalezeny!",
|
||||
"hideAchievements": "Schovat <%= kategorie %>",
|
||||
"hideAchievements": "Schovat <%= category %>",
|
||||
"onboardingCompleteDesc": "Získáváš <strong>5 úspěchů</strong> a <strong class=\"gold-amount\">100 zlaťáků</strong> za dokončení seznamu.",
|
||||
"onboardingProgress": "<%= percentage %>% postup",
|
||||
"gettingStartedDesc": "Splň tyto základní úkoly a získej <strong>5 úspěchů</strong> a <strong class=\"gold-amount\">100 zlaťáků</strong>, jakmile budeš hotový/á!",
|
||||
"showAllAchievements": "Zobrazit všechny <%= kategorie %>",
|
||||
"showAllAchievements": "Zobrazit všechny <%= category %>",
|
||||
"yourProgress": "Tvůj postup",
|
||||
"achievementBareNecessitiesModalText": "Splnil/a jsi výpravy za opicí, lenochodem a stromečkem!",
|
||||
"achievementBareNecessitiesText": "Splnil/a výpravy za opicí, lenochodem a stromečkem.",
|
||||
|
||||
@@ -152,7 +152,7 @@
|
||||
"achievementDinosaurDynastyModalText": "Du hast alle Vogel- und Dinosaurier-Haustiere gesammelt!",
|
||||
"achievementDinosaurDynasty": "Dinosaurier Dynastie",
|
||||
"achievementBonelessBoss": "Knochenloser Boss",
|
||||
"achievementBonelessBossText": "Hat alle wirbellosen Tiere ausgebrütet: Käfer, Schmetterling, Tintenfisch, Nacktschnecke, Oktopus, Schnecke und Spinnen!",
|
||||
"achievementBonelessBossText": "Hat alle wirbellosen Tiere ausgebrütet: Käfer, Schmetterling, Tintenfisch, Nacktschnecke, Oktopus, Schnecke und Spinne!",
|
||||
"achievementBonelessBossModalText": "Du hast alle wirbellosen Tiere gesammelt!",
|
||||
"achievementDuneBuddyText": "Hat alle Standardfarben der Wüstenbewohnern ausgebrütet: Gürteltier, Kaktus, Fuchs, Frosch, Schlange und Spinne!",
|
||||
"achievementRoughRider": "Harter Reiter",
|
||||
|
||||
@@ -789,7 +789,7 @@
|
||||
"backgroundBirthdayBashNotes": "Habitica feiert eine Geburtstagsparty und alle sind eingeladen!",
|
||||
"eventBackgrounds": "Ereignis-Hintergründe",
|
||||
"backgroundBirthdayBashText": "Geburtstagsparty",
|
||||
"backgroundInsideACrystalNotes": "Schau aus dem Inneren eines Kristalls heraus.",
|
||||
"backgroundInsideACrystalNotes": "Schau aus dem Inneren eines Kristalls hinaus.",
|
||||
"backgrounds072023": "SET 110: Veröffentlicht im Juli 2023",
|
||||
"backgroundOnAPaddlewheelBoatText": "Auf einem Schaufelradboot",
|
||||
"backgroundOnAPaddlewheelBoatNotes": "Fahre mit einem Schaufelradboot.",
|
||||
@@ -891,5 +891,14 @@
|
||||
"backgroundFirstSnowForestNotes": "Tritt in den ersten Schnee im Wald.",
|
||||
"backgrounds012025": "Set 128: Veröffentlicht im Januar 2025",
|
||||
"backgroundWinterLandscapeWithCabinText": "Winterlandschaft mit Hütte",
|
||||
"backgroundWinterLandscapeWithCabinNotes": "Macht es dir in einer Winterlandschaft mit einer Hütte gemütlich."
|
||||
"backgroundWinterLandscapeWithCabinNotes": "Macht es dir in einer Winterlandschaft mit einer Hütte gemütlich.",
|
||||
"backgroundOldFashionedTeaShopText": "Altmodischer Teeladen",
|
||||
"backgroundOldFashionedTeaShopNotes": "Genieße ein Getränk in einem Altmodischen Teeladen.",
|
||||
"backgrounds022025": "Set 129: Veröffentlicht im Februar 2025",
|
||||
"backgrounds032025": "SET 130: Veröffentlicht im März 2025",
|
||||
"backgroundMountainSceneWithBlossomsText": "Bergszene mit Blüten",
|
||||
"backgroundMountainSceneWithBlossomsNotes": "Erlebe den entzückenden Anblick und Geruch einer Bergszene mit Blüten.",
|
||||
"backgroundGardenWithFlowerBedsNotes": "Genieße das Blühen des Frühlings in einem Garten mit Blumenbeeten.",
|
||||
"backgrounds0420205": "SET 131: Veröffentlicht im April 2025",
|
||||
"backgroundGardenWithFlowerBedsText": "Garten mit Blumenbeeten"
|
||||
}
|
||||
|
||||
@@ -392,5 +392,19 @@
|
||||
"questEggDogText": "Welpe",
|
||||
"questEggDogMountText": "Hund",
|
||||
"questEggDogAdjective": "ein freundlicher",
|
||||
"hatchingPotionGingerbread": "Lebkuchen"
|
||||
"hatchingPotionGingerbread": "Lebkuchen",
|
||||
"questEggCatText": "Kätzchen",
|
||||
"questEggCatMountText": "Katze",
|
||||
"questEggCatAdjective": "ein schelmisches",
|
||||
"questEggOtterText": "Otter",
|
||||
"questEggOtterMountText": "Otter",
|
||||
"hatchingPotionJade": "Jade",
|
||||
"questEggOtterAdjective": "Ein perfider",
|
||||
"questEggAlpacaText": "Alpaka",
|
||||
"questEggAlpacaMountText": "Alpaka",
|
||||
"questEggAlpacaAdjective": "ein überladenes",
|
||||
"hatchingPotionBalloon": "Ballon",
|
||||
"wackyPotionAddlNotes": "Kann nicht zum Reittier großgezogen oder für Quest-Haustier Eier benutzt werden.",
|
||||
"hatchingPotionCryptid": "Kryptisch",
|
||||
"wackyPotionNotes": "Schütte dies über ein Ei und es wird als Durchgeknalltes <%= potText(locale) %> Haustier schlüpfen."
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"webFaqAnswer31": "Wenn du eine Aufgabe erfüllst und HP verlierst, obwohl du das nicht hättest tun sollen, kam es zu einer Verzögerung, während der Server die auf anderen Plattformen vorgenommenen Änderungen synchronisiert hat. Wenn du zum Beispiel Gold oder Mana verwendest oder HP in der mobilen App verlierst und dann eine Aufgabe auf der Website erledigst, bestätigt der Server lediglich, dass alles synchronisiert ist.",
|
||||
"webFaqAnswer49": "Wenn Du Habitica mit anderen erleben möchtest, aber keine anderen Spieler kennst, ist die Suche nach einer Party die beste Option! Wenn Du bereits andere Spieler kennst, die eine Party haben, kannst Du deinen @Benutzernamen mit ihnen teilen, um eingeladen zu werden. Alternativ kannst Du auch eine neue Gruppe erstellen und sie mit ihrem @Nutzernamen oder ihrer E-Mail-Adresse einladen.\n\nUm eine Party zu erstellen oder zu suchen, wähle \"Party\" im Navigationsmenü und wähle dann die Option, die Dir am besten gefällt.",
|
||||
"webFaqAnswer62": "Gruppenpläne bieten dir die einzigartige Möglichkeit, anderen Mitgliedern deines Gruppenplans gemeinsame Aufgaben zuzuweisen. Wenn eine gemeinsame Aufgabe einem Mitglied zugewiesen wird, können andere Mitglieder sie nicht mehr erledigen.\n\nDu kannst eine Aufgabe auch mehreren Mitgliedern zuweisen. Wenn sich zum Beispiel alle Mitglieder die Zähne putzen müssen, erstellst du eine Aufgabe und weist sie jedem Mitglied zu. Jedes Mitglied kann die Aufgabe erledigen und sich seine individuelle Belohnung verdienen. Die Hauptaufgabe wird als erledigt angezeigt, sobald alle Mitglieder sie erledigt haben.",
|
||||
"webFaqAnswer32": "In Habitica gibt es vier Klassen: Krieger, Magier, Schurke und Heiler. Alle Spieler beginnen in der Klasse des Kriegers, bis du Stufe 10 erreicht hast. Sobald du Stufe 10 erreichst, hast du die Wahl, eine neue Klasse zu wählen oder als Krieger weiterzuspielen.\n\nJede Klasse verfügt über unterschiedliche Ausrüstungen und Fertigkeiten. Wenn du dich nicht für eine Klasse entscheiden möchtest, kannst du \"Aussteigen\" wählen. Wenn du dich dafür entscheidest, kannst du das Klassensystem später in den Einstellungen wieder aktivieren.",
|
||||
"webFaqAnswer32": "Alle Spieler beginnen in der Klasse des Kriegers, bis sie Stufe 10 erreicht haben. Sobald du Stufe 10 erreichst, hast du die Wahl, eine neue Klasse zu wählen oder als Krieger weiterzuspielen.\n\nJede Klasse verfügt über unterschiedliche Ausrüstungen und Fertigkeiten. Wenn du dich nicht für eine Klasse entscheiden möchtest, kannst du \"Abbrechen\" wählen. Wenn du dich später doch entscheidest, kannst du das Klassensystem in den Einstellungen wieder aktivieren.\n\nWenn Du Deine Klasse nach Level 10 noch einmal ändern möchtest, kannst Du die Sphäre der Wiedergeburt hierfür nutzen. Die Sphäre der Wiedergeburt ist mit Level 50 auf demMarktplatz für 6 Edelsteine verfügbar und mit Level 100 bekommst Du sie umsonst.\n\nAlternativ kannst Du Deine Klasse jederzeit in den Einstellungen für 3 Edelsteine ändern. Dies wird Dein Level nicht wie die Sphäre der Wiedergeburt zurücksetzen, aber es erlaubt Dir, die Fertigkeits-Punkte, die Du beim Leveln gesammelt hast, Deiner neuen Klasse zuzuordnen.",
|
||||
"sunsetFaqPara14": "<strong>Linguists</strong><br />Wir freuen uns auch weiterhin über Hilfe bei der Übersetzung der Apps und der Website und werden für qualifizierte Beiträge nach wie vor Beitragsstufen vergeben. Die Methode, mit der wir Übersetzungen annehmen, wird sich jedoch ändern. Wir möchten unsere Ressourcen auf die Unterstützung einer bestimmten Auswahl von Sprachen für alle Plattformen konzentrieren. Um dies zu erreichen, werden wir die Anzahl der für Übersetzungen verfügbaren Sprachen reduzieren. Zuvor nicht fertiggestellte Sprachen werden in Github archiviert. Wir hoffen, dass diese Änderung das plattformübergreifende Habitica-Erlebnis konsistenter macht. Sie können unsere aktuellsten Richtlinien für das Übersetzungsverfahren auf unserer Website lesen <a href='https://translate.habitica.com/projects/habitica/#information'>Übersetzungswebsite</a>.",
|
||||
"webFaqAnswer34": "Haustiere mögen Futter, das zu ihrer Farbe passt. Basis-Tiere sind die Ausnahme, aber alle Basis-Tiere mögen den gleichen Gegenstand. Im Folgenden siehst du, welche Nahrungsmittel jedes Haustier mag:\n\n * Basistiere mögen Fleisch\n * Weiße Haustiere mögen Milch\n * Wüstenhaustiere mögen Kartoffeln\n * Rote Haustiere mögen Erdbeeren\n * Schattentiere mögen Schokolade\n * Skelett-Tiere mögen Fisch\n * Zombie-Tiere mögen verdorbenes Fleisch\n * Zuckerwatte rosa Haustiere mögen rosa Zuckerwatte\n * Zuckerwatte blaue Haustiere mögen blaue Zuckerwatte\n * Goldene Haustiere mögen Honig",
|
||||
"webFaqAnswer35": "Sobald du dein Haustier genug gefüttert hast, um es zu einem Reittier zu machen, musst du diese Art von Haustier erneut ausbrüten, um es in deinem Stall zu haben.\n\nUm Reittiere in den mobilen Apps zu sehen:\n\n * Wähle im Menü \"Haustiere & Reittiere\" und wechseln zur Registerkarte \"Reittiere\".\n\nSo zeigst du Reittiere auf der Website an:\n\n * Wähle im Menü \"Inventar\" die Option \"Haustiere und Reittiere\" und scrollen nach unten zum Abschnitt \"Reittiere\"",
|
||||
@@ -36,7 +36,7 @@
|
||||
"faqQuestion30": "Was passiert, wenn ich keine HP mehr habe?",
|
||||
"webFaqAnswer30": "Wenn deine HP Null erreichen, verlierst du eine Stufe, dein gesamtes Gold und ein Ausrüstungsstück, das du wieder kaufen kannst.",
|
||||
"faqQuestion31": "Warum habe ich HP verloren, wenn ich mit einer nicht-negativen Aufgabe interagiere?",
|
||||
"faqQuestion32": "Wann kann ich eine Klasse wählen?",
|
||||
"faqQuestion32": "Wie kann ich eine Klasse wählen?",
|
||||
"faqQuestion33": "Was ist der blaue Balken, der nach Level 10 erscheint?",
|
||||
"webFaqAnswer33": "Nachdem du das Klassensystem freigeschaltet hast, schaltest du auch Fertigkeiten frei, die Mana benötigen, um genutzt werden zu können. Mana wird durch deinen INT-Wert bestimmt und kann durch Fertigkeiten und Ausrüstung angepasst werden.",
|
||||
"faqQuestion34": "Welches Futter mag mein Haustier?",
|
||||
@@ -241,5 +241,7 @@
|
||||
"subscriptionDetail430": "Die Kündigung eines aktiven Abonnements wird ein Enddatum für deine Vorteile festsetzen, bis zu dem du vollen Zugang zu allen Abo-Vorteilen hast. Das bedeutet, dass du weiterhin am Start jedes Monats Mystische Sanduhren und Erhöhungen der Edelsteinobergrenze erhältst, solange du Zugang zu diesen Vorteilen hast.",
|
||||
"subscriptionDetail440": "Am Tag, an dem diese Änderungen in Kraft treten, erhalten aktive Abonnenten mit einer ungeraden Anzahl an Edelsteinen pro Monat folgende Anpassungen ihrer Edelsteinobergrenze:",
|
||||
"subscriptionDetail470": "Gruppenabonnentenvorteile verhalten sich genauso wie die eines wiederkehrenden 1-Monats-Abonnements. Du erhältst eine Mystische Sanduhr am Anfang jedes Monats und die Anzahl an Edelsteinen, die du jeden Monat auf dem Marktplatz kaufen kannst, wird sich erhöhen bis zu einem Limit von 50.",
|
||||
"subscriptionPara3": "Wir hoffen, dass dieser neue Rhythmus besser vorhersagbar ist, mehr Zugang zur fantastischen Gegenstandauswahl im Laden des Zeitreisenden ermöglicht und noch mehr Motivation bietet, jeden Monat Fortschritte an deinen Aufgaben zu machen!"
|
||||
"subscriptionPara3": "Wir hoffen, dass dieser neue Rhythmus besser vorhersagbar ist, mehr Zugang zur fantastischen Gegenstandauswahl im Laden des Zeitreisenden ermöglicht und noch mehr Motivation bietet, jeden Monat Fortschritte an deinen Aufgaben zu machen!",
|
||||
"faqQuestion67": "Was sind die Klassen in Habitica?",
|
||||
"webFaqAnswer67": "Klassen sind verschiedene Rollen, die dein Charakter spielen kann. Jede Klasse bietet ihre eigene Reihe von einzigartigen Vorteilen und Fähigkeiten beim Aufsteigen auf höhere Level. Diese Fähigkeiten können das Bearbeiten deiner Aufgaben ergänzen oder dabei helfen, deine Party beim Abschließen von Quests zu unterstützen.\n\nDeine Klasse bestimmt auch, welche Ausrüstung für dich in den Belohnungen, im Marktplatz und im Jahreszeitenmarkt zum Kauf erhältlich ist.\n\nHier ist eine Zusammenfassung jeder Klasse, um dir dabei zu helfen, diejenige zu wählen, welche am besten zu deinem Spielstil passt:\n#### **Krieger**\n* Krieger verursachen hohen Schaden bei Bossen und haben eine hohe Chance für kritische Treffer beim Abschließen von Aufgaben, was dich mit extra Erfahrung und Gold belohnt.\n* Stärke ist ihr primäres Attribut, welches den Schaden erhöht, den sie verursachen.\n* Ausdauer ist ihr sekundäres Attribut, welches den Schaden verringert, den sie erhalten.\n* Die Fähigkeiten der Krieger erhöhen die Ausdauer und Stärke der Party Kameraden.\n* Erwäge, einen Krieger zu spielen, wenn du es liebst, Bosse zu bekämpfen und auch ein wenig Schutz möchtest, wenn du gelegentlich Aufgaben versäumst.\n#### **Heiler**\n* Heiler haben eine starke Verteidigung und können sich selbst, sowie die Party Kameraden, heilen.\n* Ausdauer ist ihr primäres Attribut, welches ihre Heilungen verstärkt und den Schaden, den sie erhalten, verringert.\n* Intelligenz ist ihr sekundäres Attribut, welches ihr Mana und ihre Erfahrung erhöht.\n* Die Fähigkeiten der Heiler bewirken, dass ihre Aufgaben weniger rot werden und erhöhen die Ausdauer der Party Kameraden.\n* Erwäge, einen Heiler zu spielen, wenn du oft Aufgaben versäumst, und die Fähigkeit benötigst, dich selbst und deine Party Kameraden zu heilen. Heiler erreichen schnell neue Level.\n#### **Magier**\n* Magier gewinnen schnell neue Level und viel Mana, und verursachen Schaden bei Bossen in Quests.\n* Intelligenz ist ihr primäres Attribut, welches ihr Mana und ihre Erfahrung erhöht.\n* Wahrnehmung ist ihr sekundäres Attribut, welches ihr gefundenes Gold und ihre gefundenen Gegenstände vermehrt.\n* Die Fähigkeiten der Magier bewirken, dass ihre Aufgaben Strähnen eingefroren werden, stellen das Mana ihrer Party Kameraden wieder her, und erhöhen ihre Intelligenz.\n* Erwäge, einen Magier zu spielen, wenn du durch das schnelle Erreichen neuer Level und das Beisteuern von Schaden in Boss Quests motiviert wirst.\n#### **Schurke**\n* Schurken bekommen die meisten erbeuteten Gegenstände und das meiste Gold beim Erledigen von Aufgaben, und haben eine höhere Chance, kritische Treffer zu erzielen, was ihnen noch mehr Erfahrung und Gold beschert.\n* Wahrnehmung ist ihr primäres Attribut, welches ihr gefundenes Gold und ihre gefundenen Gegenstände vermehrt.\n* Stärke ist ihr sekundäres Attribut, welches den Schaden erhöht, den sie verursachen.\n* Die Fähigkeiten der Schurken helfen ihnen, versäumten Tagesaufgaben auszuweichen, Gold zu klauen, und die Wahrnehmung ihrer Party Kameraden zu erhöhen.\n* Erwäge, einen Schurken zu spielen, wenn du durch Belohnungen sehr motiviert wirst."
|
||||
}
|
||||
|
||||
@@ -170,7 +170,7 @@
|
||||
"joinMany": "Schließe Dich über <%= userCountInMillions %> Millionen Leuten an und habe Spaß, während Du Deine Aufgaben erfüllst!",
|
||||
"joinToday": "Tritt Habitica heute bei",
|
||||
"signup": "Registrieren",
|
||||
"getStarted": "Auf gehts",
|
||||
"getStarted": "Auf geht's",
|
||||
"mobileApps": "Mobile Apps",
|
||||
"learnMore": "Mehr erfahren",
|
||||
"communityInstagram": "Instagram",
|
||||
|
||||
@@ -2800,12 +2800,12 @@
|
||||
"armorMystery202406Text": "Phantom-Seeräuber Kleidung",
|
||||
"headMystery202406Text": "Phantom-Seeräuber Hut",
|
||||
"eyewearMystery202406Text": "Phantom-Seeräuber Maske",
|
||||
"weaponArmoirePaintbrushNotes": "Ein Ruck purer Inspiration durchdringt dich, wenn du diesen Frabpinsel aufhebst, und ermöglicht dir, alles zu malen, was du dir vorstellen kannst. Erhöht Intelligenz um <%= int %>.Verzauberter Schrank: Maler Set (Gegenstand 3 von 4).",
|
||||
"weaponArmoirePaintbrushNotes": "Ein Ruck purer Inspiration durchdringt dich, wenn du diesen Frabpinsel aufhebst, und ermöglicht dir, alles zu malen, was du dir vorstellen kannst. Erhöht Intelligenz um <%= int %>.Verzauberter Schrank: Malerset (Gegenstand 3 von 4).",
|
||||
"weaponArmoirePaintbrushText": "Farbpinsel",
|
||||
"weaponArmoireMopText": "Mopp",
|
||||
"weaponArmoireCleaningClothText": "Putzlappen",
|
||||
"weaponArmoireMopNotes": "Schritt 1: Tauche den Mopp in einen Eimer mit Wasser und Schaum. Schritt 2: Ziehe den Mopp über den Boden. Schritt 3: Tu so, als wäre das Ende des Mopp Stiels ein Mikrofon und singe mit voller Inbrunst. Schritt 4: Wiederhole Schritte 1-3, bis der Boden sauber ist. Erhöht Ausdauer und Wahrnehmung um jeweils <%= attrs %>. Putzausrüstungs-Set Zwei (Gegenstand 2 von 3)",
|
||||
"weaponArmoireCleaningClothNotes": "Nimm dieses Putzwerkzeug auf deine Abenteuer mit und sei immer bereit, eine hübsche Gedenktafel zu polieren oder eine hölzerne Fensterbank zu wischen. Erhöht Stärke und Ausdauer um jeweils <%= attrs %>. Verzauberter Schrank: Putzausrüstung Set Zwei (Gegenstand 3 von 3)",
|
||||
"weaponArmoireMopNotes": "Schritt 1: Tauche den Mopp in einen Eimer mit Wasser und Schaum. Schritt 2: Ziehe den Mopp über den Boden. Schritt 3: Tu so, als wäre das Ende des Mopp Stiels ein Mikrofon und singe mit voller Inbrunst. Schritt 4: Wiederhole Schritte 1-3, bis der Boden sauber ist. Erhöht Ausdauer und Wahrnehmung um jeweils <%= attrs %>. Reinigungs-Set Zwei (Gegenstand 2 von 3)",
|
||||
"weaponArmoireCleaningClothNotes": "Nimm dieses Putzwerkzeug auf deine Abenteuer mit und sei immer bereit, eine hübsche Gedenktafel zu polieren oder eine hölzerne Fensterbank zu wischen. Erhöht Stärke und Ausdauer um jeweils <%= attrs %>. Verzauberter Schrank: Reinigungs-Set Zwei (Gegenstand 3 von 3)",
|
||||
"weaponArmoireRidingBroomText": "Reitbesen",
|
||||
"weaponArmoireRidingBroomNotes": "Reite auf diesem feinen Besen zu all deinen magischsten Besorgungen--oder nimm ihn für eine Spritztour durch die Nachbarschaft. Wuui! Erhöht Stärke um <%= str %> und Intelligenz um <%= int %>. Verzauberter Schrank: Spukhaftes Zauberer Set (Gegenstand 1 von 3)",
|
||||
"weaponArmoireHattersShearsText": "Scharfe Scheren",
|
||||
@@ -2969,7 +2969,7 @@
|
||||
"armorArmoireBasketballUniformNotes": "Fragst du dich, was auf dem Rücken dieser Uniform aufgedruckt ist? Deine Glückszahl, natürlich! Erhöht Wahrnehmung um <%= per %>.Verzauberter Schrank: Altertümliches Basketballset (Gegenstand 1 von 2).",
|
||||
"armorArmoireShawlCollarCoatNotes": "Ein weiser Zauberer sagte einst, dass nichts besser ist als es sowohl gemütlich zu haben als auch produktiv zu sein! Trage diesen warmen und stylischen Mantel, wenn du die diesjährigen Herausforderungen meisterst. Erhöht Ausdauer um <%= con %>.",
|
||||
"armorArmoirePaintersApronText": "Schürze des Malers",
|
||||
"armorArmoirePaintersApronNotes": "Diese Schürze kann deine Kleidung vor Farbe und deine kreativen Projekte vor harschen Kritiken schützen. Erhöht Ausdauer um <%= con %>. Verzauberter Schrank: Maler-Set (Gegenstand 1 von 4).",
|
||||
"armorArmoirePaintersApronNotes": "Diese Schürze kann deine Kleidung vor Farbe und deine kreativen Projekte vor harschen Kritiken schützen. Erhöht Ausdauer um <%= con %>. Verzauberter Schrank: Malerset (Gegenstand 1 von 4).",
|
||||
"weaponSpecialWinter2025WarriorText": "Axt des Elchkriegers",
|
||||
"weaponSpecialWinter2025WarriorNotes": "Eine mächtige Axt für einen mächtigen Elch! Du wirst unaufhaltbar sein! Erhöht Stärke um <%= str %>. Limitierte Ausgabe 2024-2025 Winterausrüstung.",
|
||||
"weaponSpecialWinter2025RogueText": "Schneeflockenausbruch",
|
||||
@@ -3124,7 +3124,7 @@
|
||||
"headArmoireWhiteFloppyHatNotes": "Viele Zaubersprüche wurden in diesen einfachen Hut eingenäht und verleihen ihm eine wundersame weiße Farbe. Erhöht Stärke, Intelligenz und Ausdauer um jeweils <%= attrs %>. Verzauberter Schrank: Weißes Loungewear -Set (Gegenstand 1 von 3).",
|
||||
"headArmoireCorsairsBandanaNotes": "Egal, ob du deinen Kopf bedecken willst, falls eine Möwe über dich hinwegfliegt, oder ob du sicherstellen willst, dass deine Feinde dich nicht schwitzen sehen, dieses Tuch ist unverzichtbar. Füge einfach eine Zierperle für jedes Abenteuer hinzu, das du bestehst. Erhöht Intelligenz um <%= int %>. Verzauberter Schrank: Korsaren-Set (Gegenstand 2 von 3)",
|
||||
"headArmoireFunnyFoolCapNotes": "Die Glöckchen an diesem Hut könnten deine Gegner zum Kichern bringen, aber dir helfen sie nur, dich zu konzentrieren. Erhöht Ausdauer um <%= con %>. Verzauberter Schrank: Lustiges Narren-Set (Gegenstand 1 von 3)",
|
||||
"headArmoireDragonKnightsHelmNotes": "Mit den feurigen Elementen auf diesem Helm könnten Drachen dich für einen der ihren halten. Erhöht Intelligenz um <%= int %>. Verzauberter Schrank: Drachenritter-Set (Gegenstand 1 von 3)",
|
||||
"headArmoireDragonKnightsHelmNotes": "Mit den feurigen Elementen auf diesem Helm könnten dich Drachen für einen der ihren halten. Erhöht Intelligenz um <%= int %>. Verzauberter Schrank: Drachenritter-Set (Gegenstand 1 von 3)",
|
||||
"headArmoireStormKnightHelmText": "Sturmritterhelm",
|
||||
"headArmoireGreenTrapperHatText": "Grüne Trappermütze",
|
||||
"headArmoireGreenTrapperHatNotes": "Alle sagen, dass deine Mütze so warm aussieht! Und das ist sie tatsächlich. Achte nur darauf, dass du die Klappen von deinen Ohren ziehst, wenn die Leute mit dir reden, sonst hört sich das Ganze eher nach „dne ütze sht ss wrrm ss!“ an. Erhöht Ausdauer und Wahrnehmung um jeweils <%= attrs %> . Verzauberter Schrank: Trappermützen-Set (Gegenstand 1 von 2).",
|
||||
@@ -3253,5 +3253,65 @@
|
||||
"bodyArmoireKarateBlueBeltText": "Blauer Gürtel",
|
||||
"bodyArmoireKaratePurpleBeltText": "Violetter Gürtel",
|
||||
"eyewearMystery202303Text": "Verträumte Augen",
|
||||
"eyewearSpecialAnniversaryText": "Habiticas Heldenmaske"
|
||||
"eyewearSpecialAnniversaryText": "Habiticas Heldenmaske",
|
||||
"shieldArmoireFancyFloralFanNotes": "Beende deinen bezaubernden Look mit diesem erstklassigen Fächer aus besonders blumiger Baumwolle. Erhöht Wahrnehmung um <%= per %>. Verzauberter Schrank: Bezauberndes Blumenset (Gegenstand 2 von 2).",
|
||||
"armorMystery202502Notes": "Du bist voller gutmütiger Witze und Scherze, von deinem zerrupften Kragen bis zu deinen gigantischen Schuhen! Gewährt keinen Attributbonus. Februar 2025 Abonnentengegenstand.",
|
||||
"armorMystery202502Text": "Herzvoller Harlekin Anzug",
|
||||
"headMystery202502Text": "Herzvoller Harlekin Hut",
|
||||
"headMystery202502Notes": "Dieses fröhliche Hütchen wird bei jedem, der dich sieht, für Freude sorgen! Gewährt keinen Attributbonus. Februar 2025 Abonnentengegenstand.",
|
||||
"headArmoireFancyFloralHatText": "Bezaubernder Blumenhut",
|
||||
"shieldMystery202502Text": "Herzvolle Harlekin Ballons",
|
||||
"shieldMystery202502Notes": "Möge dein Herz an diesen Valentinstag, und auch jeden anderen Tag, so leicht sein wie diese beschwingten Luftballons. Gewährt keinen Attributbonus. Februar 2025 Abonnentengegenstand.",
|
||||
"shieldArmoireFancyFloralFanText": "Bezaubernder Blumenfächer",
|
||||
"headArmoireFancyFloralHatNotes": "Bestaune diesen bezaubernden Hut voller hinreißender Blumen und verschnörkelten Schnallen. Erhöht Intelligenz um <%= int %>. Verzauberter Schrank: Bezauberndes Blumen Zubehör Set (Gegenstand 1 von 2).",
|
||||
"weaponSpecialSpring2025WarriorNotes": "Mit einem Schnitt kannst du durch Blumenstängel säbeln, um ein Bukett zu machen, oder direkt durch Hindernisse, um deine Aufgaben zu erfüllen. Erhöht Stärke um <%= str %>. Limitierte Ausgabe Frühlingsausrüstung 2025.",
|
||||
"weaponSpecialSpring2025WarriorText": "Sonnenschein-Säbel",
|
||||
"weaponSpecialSpring2025RogueText": "Kristallspitzen-Morgenstern",
|
||||
"weaponSpecialSpring2025RogueNotes": "Mit einem Schwinger kannst du jedes Hindernis eliminieren, das deinen Zielen im Weg steht. Erhöht Stärke um <%= str %>. Limitierte Ausgabe 2025 Frühlingsausrüstung.",
|
||||
"weaponSpecialSpring2025MageNotes": "Mit einem Schwinger kannst du Elementarmagie nutzen, um deine Umgebung zu kontrollieren. Nutze den Vorteil und spring vorwärts! Erhöht Intelligenz um <%= int %> und Wahrnehmung um <%= per %>. Limitierte Ausgabe Frühlingsausrüstung 2025.",
|
||||
"weaponSpecialSpring2025MageText": "Fangschrecken Stab",
|
||||
"armorSpecialSpring2025WarriorText": "Sonnenschein Rüstung",
|
||||
"armorSpecialSpring2025WarriorNotes": "Diese atemberaubende Rüstung hat Farben, die du inmitten eines sonnigen Frühlingstages am Himmel sehen kannst. Erhöht Ausdauer um <%= con %>. Limitierte Ausgabe Frühlingsausrüstung 2025.",
|
||||
"armorSpecialSpring2025HealerText": "Plumeria Robe",
|
||||
"armorSpecialSpring2025HealerNotes": "Diese atemberaubende Robe enthält Plumeria Blütenblätter, die weich und raschelig sind. Erhöht Ausdauer um <%= con %>. Limitierte Ausgabe 2025 Frühlingsausrüstung.",
|
||||
"weaponSpecialSpring2025HealerText": "Plumeria Hirtenstab",
|
||||
"weaponSpecialSpring2025HealerNotes": "Mit einem Schwinger kannst du Bestäuber an deine Seite rufen, um dir bei deinen Abenteuern zu helfen. Erhöht Intelligenz um <%= int %>. Limitierte Ausgabe 2025 Frühlingsausrüstung.",
|
||||
"armorSpecialSpring2025RogueText": "Kristallnadel-Umhang",
|
||||
"armorSpecialSpring2025RogueNotes": "Dieser atemberaubende Umhang enthält extra Kristalle mit speziellen, geheimen Kräften, von denen nur du weißt. Erhöht Wahrnehmung um <%= per %>. Limitierte Ausgabe 2025 Frühlingsausrüstung.",
|
||||
"armorSpecialSpring2025MageText": "Fangschrecken Uniform",
|
||||
"armorSpecialSpring2025MageNotes": "Diese atemberaubende Uniform enthält auffällige Farben, lässt dich aber trotzdem heimlich an deine schwierigsten Aufgaben anpirschen. Erhöht Intelligenz um <%= int %>. Limitierte Ausgabe 2025 Frühlingsausrüstung.",
|
||||
"headSpecialSpring2025WarriorText": "Sonnenschein Helm",
|
||||
"headSpecialSpring2025WarriorNotes": "Der Kamm auf diesem Helm erinnert an den Lauf der Sonne, sieht aber auch aus wie eine Bürste, die du für den Frühjahrsputz nutzen könntest. Erhöht Stärke um <%= str %>. Limitierte Ausgabe 2025 Frühlingsausrüstung.",
|
||||
"headSpecialSpring2025RogueText": "Kristallnadel-Hut",
|
||||
"headSpecialSpring2025RogueNotes": "Die Kristalle in diesem Hut ermuntern zu produktiver Energie und leuchten außerdem hell, damit du zu jeder Tages- und Nachtzeit arbeiten kannst. Erhöht Wahrnehmung um <%= per %>. Limitierte Ausgabe 2025 Frühlingsausrüstung.",
|
||||
"headSpecialSpring2025HealerText": "Plumeria Kopfschmuck",
|
||||
"headSpecialSpring2025HealerNotes": "Diese Blume symbolisiert Geburt, Liebe und Neubeginn! Sie verbreitet auch einen schönen Duft, den du genießen kannst, während du an deinen Aufgaben arbeitest. Erhöht Intelligenz um <%= int %>. Limitierte Ausgabe 2025 Frühlingsausrüstung.",
|
||||
"headSpecialSpring2025MageText": "Fangschrecken Maske",
|
||||
"headSpecialSpring2025MageNotes": "Die Fangschrecke ist bekannt dafür, sich zu tarnen oder sich langsam zu bewegen. Wähle eine Taktik zu Hilfe für jedes einzelne Ziel, sei aber gewiss, daß du Taktiken jederzeit ändern kannst, wenn du musst. Erhöht Wahrnehmung um <%= per %>. Limitierte Ausgabe 2025 Frühlingsausrüstung.",
|
||||
"headMystery202503Text": "Jade Juggernaut Haar",
|
||||
"headMystery202503Notes": "Diese grüne Frisur passt perfekt zu einem tapferen Krieger und Verteidiger des Planeten. Gewährt keinen Attributbonus. März 2025 Abonnentengegenstand.",
|
||||
"armorArmoireSpringPetalYukataText": "Frühlingsblüten Yukata",
|
||||
"armorArmoireSpringPetalYukataNotes": "Diesen Yukata kann man perfekt zum Feiern des kommenden Frühlings anziehen. Stell sicher, daß du für ein Foto neben Kirschblüten posierst. Erhöht Ausdauer und Stärke um jeweils <%= attrs %> . Verzauberter Schrank: Frühlingsblüten Set (Gegenstand 1 von 2).",
|
||||
"shieldSpecialSpring2025RogueText": "Kristallspitzen-Flegel",
|
||||
"shieldSpecialSpring2025RogueNotes": "Du kannst den Kristall nutzen, um eine produktive Zukunft für dich weiszusagen. Nutze die Gelegenheit und spring vorwärts! Erhöht Stärke um <%= str %>. Limitierte Ausgabe 2025 Frühlingsausrüstung.",
|
||||
"shieldSpecialSpring2025HealerText": "Plumeria Schild",
|
||||
"shieldSpecialSpring2025HealerNotes": "Du kannst dieses spezielle Blütenblatt verwenden, um Güte zu sammeln oder um negative Gedanken wegzuschnipsen. Nutze die Gelegenheit und spring vorwärts! Erhöht Ausdauer um <%= con %>. Limitierte Ausgabe 2025 Ausrüstung.",
|
||||
"shieldSpecialSpring2025WarriorText": "Sonnenstrahl Schild",
|
||||
"shieldSpecialSpring2025WarriorNotes": "Du kannst deine Gegner für den Moment blenden, wenn die Sonne diesen Schild genau richtig trifft. Nutze den Vorteil und spring vorwärts! Erhöht Ausdauer um <%= con %>. Limitierte Ausgabe 2025 Frühlingsausrüstung.",
|
||||
"shieldArmoireSpringPetalUchiwaNotes": "Dieser tragbare Fächer mit schönem Blütenmuster bewirkt eine leichte Brise nur für dich, wenn das Wetter wärmer wird. Erhöht Intelligenz und Wahrnehmung um jeweils <%= attrs %>. Verzauberter Schrank: Frühlingsblüten Set (Gegenstand 2 von 2).",
|
||||
"shieldArmoireSpringPetalUchiwaText": "Frühlingsblütenfächer",
|
||||
"eyewearMystery202503Text": "Jade Juggernaut Augen",
|
||||
"eyewearMystery202503Notes": "Dieser stechende Blick wird jeden Kämpfer, der es wagt, dich herauszufordern, in Panik versetzen! Gewährt keinen Attributbonus. März 2025 Abonnentengegenstand.",
|
||||
"armorMystery202504Text": "Scheues Yeti Rüstung",
|
||||
"armorMystery202504Notes": "Abscheulich? Eher anbetungswürdig! Gewährt keinen Attributbonus. April 2025 Abonnentengegenstand.",
|
||||
"armorArmoireSillyOrangeTuxedoText": "Alberner Orangen-Smoking",
|
||||
"armorArmoireSillyOrangeTuxedoNotes": "Dein eigener persönlicher Anzug des Tages. Erhöht Ausdauer um <%= con %>. Verzauberter Schrank: Lächerlicher Smoking Set (Gegenstand 1 von 2).",
|
||||
"armorArmoireSillierBlueTuxedoNotes": "Verbreite Stimmung in diesem einzigartigen Outfit. Erhöht Stärke um <%= str %>. Verzauberter Schrank: Noch Lächerlicherer Smoking Set (Gegenstand 1 von 2).",
|
||||
"armorArmoireSillierBlueTuxedoText": "Noch Lächerlicherer Blauer Smoking",
|
||||
"headMystery202504Text": "Scheues Yeti Kutte",
|
||||
"headArmoireSillyOrangeTophatText": "Alberner Orangen-Zylinder",
|
||||
"headArmoireSillyOrangeTophatNotes": "Passt gut zu einem Kürbiskuchen-Haarschnitt. Erhöht Stärke und Ausdauer um jeweils <%= attrs %>. Verzauberter Schrank: Lächerlicher Smoking Set (Gegenstand 2 von 2).",
|
||||
"headArmoireSillierBlueTophatText": "Noch Lächerlicherer Blauer Zylinder",
|
||||
"headMystery202504Notes": "Trage diese mysteriöse Visage, um unentdeckt unter den oskursten Fabelwesen der Welt zu verweilen. Gewährt keinen Attributbonus. April 2025 Abonnentengegenstand.",
|
||||
"headArmoireSillierBlueTophatNotes": "Etwas Klasse, etwas Raffinesse. Erhöht Stärke und Ausdauer um jeweils <%= attrs %>. Verzauberter Schrank: Noch Lächerlicherer Smoking Set (Gegenstand 2 von 2)."
|
||||
}
|
||||
|
||||
@@ -189,7 +189,7 @@
|
||||
"dismissAll": "Alle entfernen",
|
||||
"messages": "Nachrichten",
|
||||
"emptyMessagesLine1": "Du hast im Moment keine Nachrichten",
|
||||
"emptyMessagesLine2": "Sende eine Nachricht, um eine Unterhaltung mit deinen Partymitgliedern oder anderen Habitica-Spieler*innen zu beginnen",
|
||||
"emptyMessagesLine2": "Du kannst anderen eine neue Nachricht schicken, indem Du ihr Profil aufrufst und auf den \"Nachrichten\"-Knopf drückst.",
|
||||
"userSentMessage": "<span class=\"notification-bold\"><%- user %></span> hat Dir eine Nachricht gesendet",
|
||||
"letsgo": "Auf geht's!",
|
||||
"selected": "Ausgewählt",
|
||||
@@ -239,7 +239,7 @@
|
||||
"skipExternalLinkModal": "Halte STRG (Windows) oder Command (Mac) beim Anklicken eines Links, um dieses Modal zu überspringen.",
|
||||
"shadowMute": "Unsichtbare Stummschaltung",
|
||||
"titleCustomizations": "Individualisierungen",
|
||||
"targetUserNotExist": "Zielbenutzer: '<%= userName %>' existiert nicht.",
|
||||
"newMessage": "Neue Nachricht",
|
||||
"targetUserNotExist": "Benutzer: '<%= userName %>' existiert nicht.",
|
||||
"rememberToBeKind": "Denk bitte daran, freundlich und respektvoll zu sein und die <a href='/static/community-guidelines' target='_blank'>Community-Richtlinien</a> einzuhalten."
|
||||
"rememberToBeKind": "Bitte sei freundlich, respektvoll, und folge den <a href='/static/community-guidelines' target='_blank'>Community-Richtlinien</a>."
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user