update
3
.env
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
VUE_APP_BASE_URL=http://localhost:8000
|
||||||
|
VUE_APP_CLIENT_ID=zR2I6g5hLfCN6a1WO2PgGOlbUWckjn7yx1Y9bJhj
|
||||||
|
VUE_APP_CLIENT_SECRET=3Ki1DjszDOab9dtKdDaViijYRckTaTzwbKSstftxNOGkYNlnOo4Ez4OtHJjoPbjRyl4Pbvq04h9lXzUHNMZlrKN5WvqGJy3FCZ2DthmZbqwttHCFglJmcIEMUOb5ZA51
|
||||||
306
package-lock.json
generated
@ -8,9 +8,12 @@
|
|||||||
"name": "furikake-web",
|
"name": "furikake-web",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"axios": "^1.12.2",
|
||||||
"core-js": "^3.8.3",
|
"core-js": "^3.8.3",
|
||||||
|
"pinia": "^3.0.3",
|
||||||
"primeicons": "^7.0.0",
|
"primeicons": "^7.0.0",
|
||||||
"vue": "^3.2.13",
|
"vue": "^3.2.13",
|
||||||
|
"vue-qrcode-reader": "^5.7.3",
|
||||||
"vue-router": "^4.5.1"
|
"vue-router": "^4.5.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@ -2526,6 +2529,18 @@
|
|||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/dom-webcodecs": {
|
||||||
|
"version": "0.1.16",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/dom-webcodecs/-/dom-webcodecs-0.1.16.tgz",
|
||||||
|
"integrity": "sha512-gRNWaC3YW5EzhPRjVYy7BnxCbtLGqsgu+uTkmV/IxOF1bllFD+FAJ1KBdsDFsuJB+F+CE+nWmMlWt8vaZ3yYXA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/emscripten": {
|
||||||
|
"version": "1.41.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/emscripten/-/emscripten-1.41.4.tgz",
|
||||||
|
"integrity": "sha512-ECf0qTibhAi2Z0K6FIY96CvBTVkVIuVunOfbTUgbaAmGmbwsc33dbK9KZPROWsmzHotddy6C5pIqYqOmsBoJEw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/eslint": {
|
"node_modules/@types/eslint": {
|
||||||
"version": "8.56.12",
|
"version": "8.56.12",
|
||||||
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.12.tgz",
|
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.12.tgz",
|
||||||
@ -3350,6 +3365,30 @@
|
|||||||
"integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==",
|
"integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@vue/devtools-kit": {
|
||||||
|
"version": "7.7.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.7.7.tgz",
|
||||||
|
"integrity": "sha512-wgoZtxcTta65cnZ1Q6MbAfePVFxfM+gq0saaeytoph7nEa7yMXoi6sCPy4ufO111B9msnw0VOWjPEFCXuAKRHA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@vue/devtools-shared": "^7.7.7",
|
||||||
|
"birpc": "^2.3.0",
|
||||||
|
"hookable": "^5.5.3",
|
||||||
|
"mitt": "^3.0.1",
|
||||||
|
"perfect-debounce": "^1.0.0",
|
||||||
|
"speakingurl": "^14.0.1",
|
||||||
|
"superjson": "^2.2.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vue/devtools-shared": {
|
||||||
|
"version": "7.7.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.7.7.tgz",
|
||||||
|
"integrity": "sha512-+udSj47aRl5aKb0memBvcUG9koarqnxNM5yjuREvqwK6T3ap4mn3Zqqc17QrBFTqSMjr3HK1cvStEZpMDpfdyw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"rfdc": "^1.4.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@vue/reactivity": {
|
"node_modules/@vue/reactivity": {
|
||||||
"version": "3.5.21",
|
"version": "3.5.21",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.21.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.21.tgz",
|
||||||
@ -3923,6 +3962,12 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/asynckit": {
|
||||||
|
"version": "0.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||||
|
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/at-least-node": {
|
"node_modules/at-least-node": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz",
|
||||||
@ -3971,6 +4016,17 @@
|
|||||||
"postcss": "^8.1.0"
|
"postcss": "^8.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/axios": {
|
||||||
|
"version": "1.12.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz",
|
||||||
|
"integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"follow-redirects": "^1.15.6",
|
||||||
|
"form-data": "^4.0.4",
|
||||||
|
"proxy-from-env": "^1.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/babel-loader": {
|
"node_modules/babel-loader": {
|
||||||
"version": "8.4.1",
|
"version": "8.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.4.1.tgz",
|
||||||
@ -4065,6 +4121,16 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/barcode-detector": {
|
||||||
|
"version": "2.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/barcode-detector/-/barcode-detector-2.2.2.tgz",
|
||||||
|
"integrity": "sha512-JcSekql+EV93evfzF9zBr+Y6aRfkR+QFvgyzbwQ0dbymZXoAI9+WgT7H1E429f+3RKNncHz2CW98VQtaaKpmfQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/dom-webcodecs": "^0.1.11",
|
||||||
|
"zxing-wasm": "1.1.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/base64-js": {
|
"node_modules/base64-js": {
|
||||||
"version": "1.5.1",
|
"version": "1.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||||
@ -4126,6 +4192,15 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/birpc": {
|
||||||
|
"version": "2.6.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/birpc/-/birpc-2.6.1.tgz",
|
||||||
|
"integrity": "sha512-LPnFhlDpdSH6FJhJyn4M0kFO7vtQ5iPw24FnG0y21q09xC7e8+1LeR31S1MAIrDAHp4m7aas4bEkTDTvMAtebQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/antfu"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/bl": {
|
"node_modules/bl": {
|
||||||
"version": "4.1.0",
|
"version": "4.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
|
||||||
@ -4328,7 +4403,6 @@
|
|||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||||
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"es-errors": "^1.3.0",
|
"es-errors": "^1.3.0",
|
||||||
@ -4683,6 +4757,18 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/combined-stream": {
|
||||||
|
"version": "1.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||||
|
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"delayed-stream": "~1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/commander": {
|
"node_modules/commander": {
|
||||||
"version": "8.3.0",
|
"version": "8.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz",
|
||||||
@ -4827,6 +4913,21 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/copy-anything": {
|
||||||
|
"version": "3.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-3.0.5.tgz",
|
||||||
|
"integrity": "sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"is-what": "^4.1.8"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.13"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/mesqueeb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/copy-webpack-plugin": {
|
"node_modules/copy-webpack-plugin": {
|
||||||
"version": "9.1.0",
|
"version": "9.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-9.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-9.1.0.tgz",
|
||||||
@ -5501,6 +5602,15 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/delayed-stream": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/depd": {
|
"node_modules/depd": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
||||||
@ -5703,7 +5813,6 @@
|
|||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||||
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"call-bind-apply-helpers": "^1.0.1",
|
"call-bind-apply-helpers": "^1.0.1",
|
||||||
@ -5853,7 +5962,6 @@
|
|||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||||
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
@ -5863,7 +5971,6 @@
|
|||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||||
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
@ -5880,7 +5987,6 @@
|
|||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
||||||
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
|
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"es-errors": "^1.3.0"
|
"es-errors": "^1.3.0"
|
||||||
@ -5889,6 +5995,21 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/es-set-tostringtag": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"get-intrinsic": "^1.2.6",
|
||||||
|
"has-tostringtag": "^1.0.2",
|
||||||
|
"hasown": "^2.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/escalade": {
|
"node_modules/escalade": {
|
||||||
"version": "3.2.0",
|
"version": "3.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
|
||||||
@ -6809,7 +6930,6 @@
|
|||||||
"version": "1.15.11",
|
"version": "1.15.11",
|
||||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
|
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
|
||||||
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
|
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
|
||||||
"dev": true,
|
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "individual",
|
"type": "individual",
|
||||||
@ -6920,6 +7040,22 @@
|
|||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/form-data": {
|
||||||
|
"version": "4.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
|
||||||
|
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"asynckit": "^0.4.0",
|
||||||
|
"combined-stream": "^1.0.8",
|
||||||
|
"es-set-tostringtag": "^2.1.0",
|
||||||
|
"hasown": "^2.0.2",
|
||||||
|
"mime-types": "^2.1.12"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/forwarded": {
|
"node_modules/forwarded": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
||||||
@ -7003,7 +7139,6 @@
|
|||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||||
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
@ -7040,7 +7175,6 @@
|
|||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||||
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"call-bind-apply-helpers": "^1.0.2",
|
"call-bind-apply-helpers": "^1.0.2",
|
||||||
@ -7065,7 +7199,6 @@
|
|||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
||||||
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"dunder-proto": "^1.0.1",
|
"dunder-proto": "^1.0.1",
|
||||||
@ -7184,7 +7317,6 @@
|
|||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||||
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
@ -7250,7 +7382,6 @@
|
|||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||||
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
@ -7259,6 +7390,21 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/has-tostringtag": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"has-symbols": "^1.0.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/hash-sum": {
|
"node_modules/hash-sum": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/hash-sum/-/hash-sum-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/hash-sum/-/hash-sum-2.0.0.tgz",
|
||||||
@ -7270,7 +7416,6 @@
|
|||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||||
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"function-bind": "^1.1.2"
|
"function-bind": "^1.1.2"
|
||||||
@ -7299,6 +7444,12 @@
|
|||||||
"node": "*"
|
"node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/hookable": {
|
||||||
|
"version": "5.5.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz",
|
||||||
|
"integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/hosted-git-info": {
|
"node_modules/hosted-git-info": {
|
||||||
"version": "2.8.9",
|
"version": "2.8.9",
|
||||||
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz",
|
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz",
|
||||||
@ -7842,6 +7993,18 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/is-what": {
|
||||||
|
"version": "4.1.16",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-what/-/is-what-4.1.16.tgz",
|
||||||
|
"integrity": "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.13"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/mesqueeb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/is-wsl": {
|
"node_modules/is-wsl": {
|
||||||
"version": "2.2.0",
|
"version": "2.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
|
||||||
@ -8706,7 +8869,6 @@
|
|||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||||
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
@ -8820,7 +8982,6 @@
|
|||||||
"version": "1.52.0",
|
"version": "1.52.0",
|
||||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
@ -8830,7 +8991,6 @@
|
|||||||
"version": "2.1.35",
|
"version": "2.1.35",
|
||||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"mime-db": "1.52.0"
|
"mime-db": "1.52.0"
|
||||||
@ -9000,6 +9160,12 @@
|
|||||||
"node": ">=16 || 14 >=14.17"
|
"node": ">=16 || 14 >=14.17"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/mitt": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/mkdirp": {
|
"node_modules/mkdirp": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz",
|
||||||
@ -9699,6 +9865,12 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/perfect-debounce": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/picocolors": {
|
"node_modules/picocolors": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||||
@ -9728,6 +9900,36 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/pinia": {
|
||||||
|
"version": "3.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/pinia/-/pinia-3.0.3.tgz",
|
||||||
|
"integrity": "sha512-ttXO/InUULUXkMHpTdp9Fj4hLpD/2AoJdmAbAeW2yu1iy1k+pkFekQXw5VpC0/5p51IOR/jDaDRfRWRnMMsGOA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@vue/devtools-api": "^7.7.2"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/posva"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"typescript": ">=4.4.4",
|
||||||
|
"vue": "^2.7.0 || ^3.5.11"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"typescript": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/pinia/node_modules/@vue/devtools-api": {
|
||||||
|
"version": "7.7.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-7.7.7.tgz",
|
||||||
|
"integrity": "sha512-lwOnNBH2e7x1fIIbVT7yF5D+YWhqELm55/4ZKf45R9T8r9dE2AIOy8HKjfqzGsoTHFbWbr337O4E0A0QADnjBg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@vue/devtools-kit": "^7.7.7"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/pirates": {
|
"node_modules/pirates": {
|
||||||
"version": "4.0.7",
|
"version": "4.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz",
|
||||||
@ -10676,6 +10878,12 @@
|
|||||||
"node": ">= 0.10"
|
"node": ">= 0.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/proxy-from-env": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/pseudomap": {
|
"node_modules/pseudomap": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz",
|
||||||
@ -11060,6 +11268,12 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/rfdc": {
|
||||||
|
"version": "1.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz",
|
||||||
|
"integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/rimraf": {
|
"node_modules/rimraf": {
|
||||||
"version": "3.0.2",
|
"version": "3.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
|
||||||
@ -11148,6 +11362,12 @@
|
|||||||
"url": "https://opencollective.com/webpack"
|
"url": "https://opencollective.com/webpack"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/sdp": {
|
||||||
|
"version": "3.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/sdp/-/sdp-3.2.1.tgz",
|
||||||
|
"integrity": "sha512-lwsAIzOPlH8/7IIjjz3K0zYBk7aBVVcvjMwt3M4fLxpjMYyy7i3I97SLHebgn4YBjirkzfp3RvRDWSKsh/+WFw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/select-hose": {
|
"node_modules/select-hose": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz",
|
||||||
@ -11653,6 +11873,15 @@
|
|||||||
"wbuf": "^1.7.3"
|
"wbuf": "^1.7.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/speakingurl": {
|
||||||
|
"version": "14.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/speakingurl/-/speakingurl-14.0.1.tgz",
|
||||||
|
"integrity": "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/sprintf-js": {
|
"node_modules/sprintf-js": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
|
||||||
@ -11916,6 +12145,18 @@
|
|||||||
"node": ">=16 || 14 >=14.17"
|
"node": ">=16 || 14 >=14.17"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/superjson": {
|
||||||
|
"version": "2.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.2.tgz",
|
||||||
|
"integrity": "sha512-5JRxVqC8I8NuOUjzBbvVJAKNM8qoVuH0O77h4WInc/qC2q5IreqKxYwgkga3PfA22OayK2ikceb/B26dztPl+Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"copy-anything": "^3.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/supports-color": {
|
"node_modules/supports-color": {
|
||||||
"version": "7.2.0",
|
"version": "7.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||||
@ -12776,6 +13017,19 @@
|
|||||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/vue-qrcode-reader": {
|
||||||
|
"version": "5.7.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/vue-qrcode-reader/-/vue-qrcode-reader-5.7.3.tgz",
|
||||||
|
"integrity": "sha512-iSGko42FsEvdHyizBMBs/X+HMO9Z5ONDxjW+mQdoraOR5emRNedmjC5SEJdYzGz8ZP5ME3lwB4iHy3S7MOt5Qw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"barcode-detector": "2.2.2",
|
||||||
|
"webrtc-adapter": "8.2.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/vue-router": {
|
"node_modules/vue-router": {
|
||||||
"version": "4.5.1",
|
"version": "4.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.5.1.tgz",
|
||||||
@ -13280,6 +13534,19 @@
|
|||||||
"url": "https://opencollective.com/webpack"
|
"url": "https://opencollective.com/webpack"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/webrtc-adapter": {
|
||||||
|
"version": "8.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/webrtc-adapter/-/webrtc-adapter-8.2.3.tgz",
|
||||||
|
"integrity": "sha512-gnmRz++suzmvxtp3ehQts6s2JtAGPuDPjA1F3a9ckNpG1kYdYuHWYpazoAnL9FS5/B21tKlhkorbdCXat0+4xQ==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"sdp": "^3.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0.0",
|
||||||
|
"npm": ">=3.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/websocket-driver": {
|
"node_modules/websocket-driver": {
|
||||||
"version": "0.7.4",
|
"version": "0.7.4",
|
||||||
"resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz",
|
"resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz",
|
||||||
@ -13560,6 +13827,15 @@
|
|||||||
"integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==",
|
"integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
|
},
|
||||||
|
"node_modules/zxing-wasm": {
|
||||||
|
"version": "1.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/zxing-wasm/-/zxing-wasm-1.1.3.tgz",
|
||||||
|
"integrity": "sha512-MYm9k/5YVs4ZOTIFwlRjfFKD0crhefgbnt1+6TEpmKUDFp3E2uwqGSKwQOd2hOIsta/7Usq4hnpNRYTLoljnfA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/emscripten": "^1.39.10"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,9 +9,12 @@
|
|||||||
"clean": "rimraf dist node_modules/.cache node_modules/.vite"
|
"clean": "rimraf dist node_modules/.cache node_modules/.vite"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"axios": "^1.12.2",
|
||||||
"core-js": "^3.8.3",
|
"core-js": "^3.8.3",
|
||||||
|
"pinia": "^3.0.3",
|
||||||
"primeicons": "^7.0.0",
|
"primeicons": "^7.0.0",
|
||||||
"vue": "^3.2.13",
|
"vue": "^3.2.13",
|
||||||
|
"vue-qrcode-reader": "^5.7.3",
|
||||||
"vue-router": "^4.5.1"
|
"vue-router": "^4.5.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
BIN
public/images/manga.png
Normal file
|
After Width: | Height: | Size: 375 KiB |
BIN
public/images/manga/attackot.png
Normal file
|
After Width: | Height: | Size: 120 KiB |
BIN
public/images/manga/bleach.png
Normal file
|
After Width: | Height: | Size: 150 KiB |
BIN
public/images/manga/deathnote.png
Normal file
|
After Width: | Height: | Size: 114 KiB |
BIN
public/images/manga/demonslayer.png
Normal file
|
After Width: | Height: | Size: 408 KiB |
BIN
public/images/manga/dragonball.png
Normal file
|
After Width: | Height: | Size: 485 KiB |
BIN
public/images/manga/fullmetal.png
Normal file
|
After Width: | Height: | Size: 134 KiB |
BIN
public/images/manga/jujutsu.png
Normal file
|
After Width: | Height: | Size: 364 KiB |
BIN
public/images/manga/naruto.png
Normal file
|
After Width: | Height: | Size: 73 KiB |
BIN
public/images/manga/onepiece.png
Normal file
|
After Width: | Height: | Size: 426 KiB |
BIN
public/images/manga/tokyoghoul.png
Normal file
|
After Width: | Height: | Size: 214 KiB |
BIN
public/images/manga1.png
Normal file
|
After Width: | Height: | Size: 354 KiB |
BIN
public/images/manga2.png
Normal file
|
After Width: | Height: | Size: 418 KiB |
@ -29,7 +29,7 @@
|
|||||||
</router-link>
|
</router-link>
|
||||||
|
|
||||||
<router-link
|
<router-link
|
||||||
to="/hiburan"
|
to="/entertainment"
|
||||||
class="flex flex-col items-center text-gray-600 hover:text-green-500 p-2"
|
class="flex flex-col items-center text-gray-600 hover:text-green-500 p-2"
|
||||||
active-class="text-green-600 font-bold"
|
active-class="text-green-600 font-bold"
|
||||||
>
|
>
|
||||||
|
|||||||
@ -2,7 +2,8 @@ import { createApp } from 'vue'
|
|||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
import './assets/tailwind.css'
|
import './assets/tailwind.css'
|
||||||
import router from './router'
|
import router from './router'
|
||||||
|
import { createPinia } from 'pinia'
|
||||||
|
|
||||||
|
const pinia = createPinia()
|
||||||
createApp(App).use(router).mount('#app')
|
createApp(App).use(pinia).use(router).mount('#app')
|
||||||
|
|
||||||
|
|||||||
148
src/pages/ChapterList.vue
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="min-h-screen flex flex-col bg-gradient-to-b from-emerald-100 via-lime-50 to-white
|
||||||
|
text-gray-800 relative overflow-hidden"
|
||||||
|
@scroll.passive="handleScroll"
|
||||||
|
>
|
||||||
|
<!-- Header -->
|
||||||
|
<div
|
||||||
|
class="w-full px-6 py-4 flex items-center justify-between
|
||||||
|
backdrop-blur-md bg-white/30 border-b border-emerald-100 z-20 sticky top-0"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
@click="goBack"
|
||||||
|
class="flex items-center gap-2 text-emerald-600 hover:text-emerald-800 transition-colors"
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
|
||||||
|
</svg>
|
||||||
|
<span class="font-semibold">Kembali</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Breadcrumb -->
|
||||||
|
<div class="text-sm text-gray-600">
|
||||||
|
<span class="text-emerald-600 font-medium cursor-pointer hover:underline" @click="goHome">Beranda</span>
|
||||||
|
<span class="mx-2">›</span>
|
||||||
|
<span class="text-gray-700 font-semibold cursor-pointer hover:underline" @click="toMangas">Manga</span>
|
||||||
|
<span class="mx-2">›</span>
|
||||||
|
<span class="text-gray-700 font-semibold">Baca Manga</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Judul -->
|
||||||
|
<div class="text-center mt-8">
|
||||||
|
<h1 class="text-3xl font-extrabold bg-gradient-to-r from-emerald-500 to-lime-500 bg-clip-text text-transparent">
|
||||||
|
{{ mangaTitle }}
|
||||||
|
</h1>
|
||||||
|
<p class="text-gray-500 mt-2">Chapter {{ chapter_id }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Manga Scroll Viewer -->
|
||||||
|
<div ref="scrollContainer" class="flex flex-col items-center justify-start w-full mt-6 px-4 pb-20 overflow-y-auto">
|
||||||
|
<transition-group name="fade" tag="div" class="w-full max-w-2xl space-y-8">
|
||||||
|
<div
|
||||||
|
v-for="(page, index) in pages"
|
||||||
|
:key="index"
|
||||||
|
class="flex flex-col items-center"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
:src="page.image"
|
||||||
|
:alt="'Halaman ' + (index + 1)"
|
||||||
|
class="w-full rounded-xl shadow-lg border border-white/40 object-contain"
|
||||||
|
/>
|
||||||
|
<p class="text-sm text-gray-500 mt-2">Halaman {{ index + 1 }}</p>
|
||||||
|
</div>
|
||||||
|
</transition-group>
|
||||||
|
|
||||||
|
<!-- Loading Spinner -->
|
||||||
|
<div v-if="isLoadingNext" class="flex justify-center mt-6 mb-10">
|
||||||
|
<svg class="animate-spin h-6 w-6 text-emerald-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||||
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||||
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import router from '@/router'
|
||||||
|
import api from '@/util/api'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "MangaPageScrollReader",
|
||||||
|
props: ["manga_id", "chapter_id"],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
pages: [],
|
||||||
|
mangaTitle: "Loading...",
|
||||||
|
nextUrl: null,
|
||||||
|
isLoadingNext: false,
|
||||||
|
scrollThreshold: 400, // jarak px dari bawah sebelum trigger load berikutnya
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
goBack() {
|
||||||
|
router.back()
|
||||||
|
},
|
||||||
|
goHome() {
|
||||||
|
router.push('/')
|
||||||
|
},
|
||||||
|
toMangas() {
|
||||||
|
router.push('/entertainment/mangas')
|
||||||
|
},
|
||||||
|
|
||||||
|
async loadPages(url = null) {
|
||||||
|
try {
|
||||||
|
this.isLoadingNext = true
|
||||||
|
const endpoint = url || `/entertainment/manga/${this.manga_id}/chapters/${this.chapter_id}/pages/`
|
||||||
|
const res = await api.get(endpoint)
|
||||||
|
const data = res.data
|
||||||
|
|
||||||
|
// kalau backend pakai pagination
|
||||||
|
if (data.results) {
|
||||||
|
this.pages.push(...data.results)
|
||||||
|
this.nextUrl = data.next
|
||||||
|
} else {
|
||||||
|
this.pages.push(...data)
|
||||||
|
this.nextUrl = null
|
||||||
|
}
|
||||||
|
|
||||||
|
// ambil nama manga
|
||||||
|
if (this.pages.length > 0 && this.pages[0].chapter?.manga?.title) {
|
||||||
|
this.mangaTitle = this.pages[0].chapter.manga.title
|
||||||
|
}
|
||||||
|
console.log(this.pages)
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Gagal memuat halaman:", err)
|
||||||
|
} finally {
|
||||||
|
this.isLoadingNext = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
handleScroll() {
|
||||||
|
const container = document.documentElement
|
||||||
|
const scrollBottom = container.scrollHeight - (window.scrollY + window.innerHeight)
|
||||||
|
if (scrollBottom < this.scrollThreshold && this.nextUrl && !this.isLoadingNext) {
|
||||||
|
this.loadPages(this.nextUrl)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.loadPages()
|
||||||
|
window.addEventListener('scroll', this.handleScroll)
|
||||||
|
},
|
||||||
|
beforeUnmount() {
|
||||||
|
window.removeEventListener('scroll', this.handleScroll)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.fade-enter-active, .fade-leave-active {
|
||||||
|
transition: opacity 0.5s;
|
||||||
|
}
|
||||||
|
.fade-enter-from, .fade-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
137
src/pages/Chapters.vue
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="min-h-screen flex flex-col bg-gradient-to-b from-emerald-100 via-lime-50 to-white
|
||||||
|
text-gray-800 relative overflow-hidden mb-[-10px]"
|
||||||
|
>
|
||||||
|
<div class="w-full px-6 py-4 flex items-center justify-between backdrop-blur-md bg-white/30 border-b border-emerald-100 z-20">
|
||||||
|
<button
|
||||||
|
@click="goBack"
|
||||||
|
class="flex items-center gap-2 text-emerald-600 hover:text-emerald-800 transition-colors"
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
|
||||||
|
</svg>
|
||||||
|
<span class="font-semibold">Kembali</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Breadcrumb / Path -->
|
||||||
|
<div class="text-sm text-gray-600">
|
||||||
|
<span class="text-emerald-600 font-medium cursor-pointer hover:underline" @click="goHome">Beranda</span>
|
||||||
|
<span class="mx-2">›</span>
|
||||||
|
<span class="text-gray-700 font-semibold">Manga</span>
|
||||||
|
<span class="mx-2">›</span>
|
||||||
|
<span class="text-gray-700 font-semibold">Chapters</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Title -->
|
||||||
|
<div class="text-center mt-8">
|
||||||
|
<h1 class="text-4xl font-extrabold bg-gradient-to-r from-emerald-500 to-lime-500 bg-clip-text text-transparent">
|
||||||
|
Daftar Manga
|
||||||
|
</h1>
|
||||||
|
<p class="text-gray-500 mt-2">Temukan bacaan favoritmu di sini 📖</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Manga Grid -->
|
||||||
|
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-6 mt-10 px-6 pb-20">
|
||||||
|
<div
|
||||||
|
v-for="chapter in chapters"
|
||||||
|
:key="chapter.chapter"
|
||||||
|
class="group cursor-pointer rounded-2xl bg-white/60 backdrop-blur-lg p-4 shadow-md border border-white/40
|
||||||
|
hover:shadow-2xl hover:-translate-y-2 transition-all duration-300 relative overflow-hidden"
|
||||||
|
>
|
||||||
|
<div class="mt-0 text-center">
|
||||||
|
<h3 class="font-semibold text-lg text-gray-800 truncate">Chapter {{ chapter.chapter }}</h3>
|
||||||
|
</div>
|
||||||
|
<!-- Manga Cover -->
|
||||||
|
<div class="relative" @click="toPage(chapter.id,chapter.chapter)">
|
||||||
|
<img
|
||||||
|
:src="images.find(image => image.id === chapter.id).image"
|
||||||
|
:alt="chapter.status"
|
||||||
|
class="w-full h-48 object-cover rounded-xl group-hover:scale-105 transition-transform duration-500"
|
||||||
|
/>
|
||||||
|
<div class="absolute inset-0 bg-gradient-to-t from-black/40 to-transparent rounded-xl opacity-0 group-hover:opacity-100 transition-opacity"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Manga Info -->
|
||||||
|
<div class="mt-3 text-center">
|
||||||
|
<!-- <h3 class="font-semibold text-lg text-gray-800 truncate">Chapter {{ chapter.chapter }}</h3> -->
|
||||||
|
<p class="text-sm text-gray-500">
|
||||||
|
<button @click="toSynopsis(chapter.manga)"
|
||||||
|
class="text-gray-900 bg-white hover:bg-gray-100 border border-gray-200 focus:ring-4 focus:outline-none focus:ring-gray-100 font-medium rounded-lg text-sm px-5 py-2.5 text-center inline-flex items-center dark:focus:ring-gray-600 dark:bg-gray-800 dark:border-gray-700 dark:text-white dark:hover:bg-gray-700 me-2 mb-2">
|
||||||
|
<h3 class="font-semibold text-lg text-gray-800 truncate">📖 Synopsis</h3>
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- <button @click="toSynopsis(chapter.manga)">
|
||||||
|
Synopsis
|
||||||
|
</button> -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import router from '@/router';
|
||||||
|
import { getChapters } from '@/services/manga';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "manga-chapter",
|
||||||
|
props: ["id"],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
genres : [],
|
||||||
|
chapters: [],
|
||||||
|
images: [
|
||||||
|
{id:1, image:'/images/manga.png'},
|
||||||
|
{id:2, image:'/images/manga1.png'},
|
||||||
|
{id:3, image:'/images/manga2.png'}
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
goBack() {
|
||||||
|
router.back();
|
||||||
|
},
|
||||||
|
toPage(manga_id,chapter_id) {
|
||||||
|
router.push(`/entertainment/manga/${manga_id}/chapters/${chapter_id}/`);
|
||||||
|
},
|
||||||
|
async loadContent() {
|
||||||
|
const res = await getChapters(this.id);
|
||||||
|
this.chapters = res.results;
|
||||||
|
|
||||||
|
console.log(this.id)
|
||||||
|
console.log(this.props)
|
||||||
|
console.log(res.results);
|
||||||
|
},
|
||||||
|
toSynopsis( manga) {
|
||||||
|
console.log(manga)
|
||||||
|
router.push({
|
||||||
|
name: 'manga-sinopsis',
|
||||||
|
params: { id: manga.id },
|
||||||
|
query: { synopsis: manga.synopsis }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.loadContent();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
@keyframes pulse {
|
||||||
|
0%, 100% { opacity: 0.5; transform: scale(1); }
|
||||||
|
50% { opacity: 0.8; transform: scale(1.05); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tambahan efek glow lembut di hover */
|
||||||
|
.group:hover img {
|
||||||
|
filter: drop-shadow(0 0 10px rgba(72, 187, 120, 0.5));
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
|
||||||
|
</script>
|
||||||
@ -1,6 +1,13 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="min-h-screen flex flex-col items-center justify-center bg-gradient-to-b from-green-100 to-green-300 pb-28 pr-2 pl-2">
|
<div class="min-h-screen flex flex-col items-center justify-center bg-gradient-to-b from-green-100
|
||||||
<div class="w-full max-w-md flex flex-col items-center z-60 " >
|
to-green-300 pb-28 pr-2 pl-2 mb-[-10px]"
|
||||||
|
style="background-image:url('/images/footer.png');
|
||||||
|
background-repeat:no-repeat;
|
||||||
|
background-position:bottom;
|
||||||
|
background-size: 100% 200px;"
|
||||||
|
margin-bottom=-10px;
|
||||||
|
>
|
||||||
|
<div class="w-full max-w-md flex flex-col items-center z-60 mb-8" >
|
||||||
<div class="relative flex items-end justify-center z-30 px-[10px] mt-[40px]">
|
<div class="relative flex items-end justify-center z-30 px-[10px] mt-[40px]">
|
||||||
<img
|
<img
|
||||||
src="/images/logo.png"
|
src="/images/logo.png"
|
||||||
@ -8,7 +15,7 @@
|
|||||||
class="object-contain relative z-30 pb-0"
|
class="object-contain relative z-30 pb-0"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<h2 class="mt-6 text-xl font-bold text-gray-700">
|
<!-- <h2 class="mt-6 text-xl font-bold text-gray-700">
|
||||||
{{ selectedCharacter.name }}
|
{{ selectedCharacter.name }}
|
||||||
</h2>
|
</h2>
|
||||||
<div class="relative w-40 h-50 flex items-end justify-center z-30 px-[10px]">
|
<div class="relative w-40 h-50 flex items-end justify-center z-30 px-[10px]">
|
||||||
@ -27,8 +34,7 @@
|
|||||||
"
|
"
|
||||||
@click="changeAva('profile')"
|
@click="changeAva('profile')"
|
||||||
></div>
|
></div>
|
||||||
|
</div> -->
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full max-w-md flex flex-col items-center z-60">
|
<div class="w-full max-w-md flex flex-col items-center z-60">
|
||||||
<div class="bg-lime-200 shadow-xl rounded-2xl p-6 flex flex-col items-center
|
<div class="bg-lime-200 shadow-xl rounded-2xl p-6 flex flex-col items-center
|
||||||
@ -64,17 +70,18 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Character Selection -->
|
<!-- Character Selection -->
|
||||||
<div class="w-full max-w-md flex flex-col items-center z-60 " >
|
<div class="w-full max-w-md flex flex-col items-center z-60" >
|
||||||
<div class="grid grid-cols-2 sm:grid-cols-4 gap-4 mt-6 w-full max-w-2xl">
|
<div class="grid grid-cols-2 sm:grid-cols-4 gap-4 mt-6 w-full max-w-2xl">
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-for="char in characters"
|
v-for="char in characters"
|
||||||
:key="char.name"
|
:key="char.name"
|
||||||
@click="selectedCharacter = char"
|
@click="selectedCharacter = char"
|
||||||
class="cursor-pointer flex flex-col items-center p-4 rounded-xl shadow-md hover:shadow-lg transition bg-white"
|
class="cursor-pointer flex flex-col items-center p-4 rounded-xl
|
||||||
|
shadow-md hover:shadow-lg transition bg-white duration-300 hover:-translate-y-2
|
||||||
|
"
|
||||||
:class="{'ring-4 ring-green-400': selectedCharacter.name === char.name}"
|
:class="{'ring-4 ring-green-400': selectedCharacter.name === char.name}"
|
||||||
>
|
>
|
||||||
<img :src="char.img" :alt="char.name" class="w-20 h-20 object-contain" />
|
<img :src="char.img" :alt="char.name" class="w-20 h-20 object-contain img-hover:scale-100 transition-transform duration-300"/>
|
||||||
<!-- <p class="mt-2 text-gray-600 font-medium">{{ char.name }}</p> -->
|
<!-- <p class="mt-2 text-gray-600 font-medium">{{ char.name }}</p> -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -135,3 +142,22 @@ export default {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scope>
|
||||||
|
@keyframes zoom {
|
||||||
|
0%, 100% {
|
||||||
|
opacity: 0.5;
|
||||||
|
transform: scale(1);
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0.8;
|
||||||
|
transform: scale(1.05);
|
||||||
|
color: green;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.img-hover {
|
||||||
|
filter: drop-shadow(0 0 10px rgba(72, 187, 120, 0.5));
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
182
src/pages/Entertainment.vue
Normal file
@ -0,0 +1,182 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="min-h-screen flex flex-col items-center justify-center bg-gradient-to-b from-emerald-200 via-teal-100 to-lime-200
|
||||||
|
relative overflow-hidden text-gray-800 mb-[-10px]"
|
||||||
|
style="background-image:url('/images/footer.png');
|
||||||
|
background-repeat:no-repeat;
|
||||||
|
background-position:bottom;
|
||||||
|
background-size: 100% 200px;"
|
||||||
|
margin-bottom=-10px;
|
||||||
|
>
|
||||||
|
<!-- Background Glow & Floating Elements -->
|
||||||
|
<div class="absolute inset-0 z-0">
|
||||||
|
<div class="absolute top-10 left-10 w-72 h-72 bg-emerald-300/30 blur-3xl rounded-full animate-pulse"></div>
|
||||||
|
<div class="absolute bottom-10 right-10 w-96 h-96 bg-lime-400/30 blur-3xl rounded-full animate-pulse"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Logo Section -->
|
||||||
|
<div class="w-full max-w-md flex flex-col items-center z-20 mt-10">
|
||||||
|
<img
|
||||||
|
src="/images/logo.png"
|
||||||
|
alt="Logo"
|
||||||
|
class="w-48 h-auto drop-shadow-xl hover:scale-105 transition-transform duration-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Main Card -->
|
||||||
|
<div
|
||||||
|
class="backdrop-blur-xl bg-white/30 border border-white/40 rounded-3xl shadow-2xl p-6 mt-10 w-[90%] max-w-md
|
||||||
|
flex flex-col items-center text-center relative overflow-hidden"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-lime-400 via-emerald-300 to-cyan-300 rounded-t-3xl animate-[pulse_3s_infinite]"
|
||||||
|
></div>
|
||||||
|
|
||||||
|
<img
|
||||||
|
src="/images/but-nav-hiburan.png"
|
||||||
|
alt="Hiburan"
|
||||||
|
class="w-40 h-auto mt-6 transition-transform duration-500 hover:scale-110"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<h2 class="text-3xl font-extrabold mt-4 bg-gradient-to-r from-emerald-500 to-lime-500 bg-clip-text text-transparent tracking-wide">
|
||||||
|
Hiburan
|
||||||
|
</h2>
|
||||||
|
<p class="text-gray-600 mt-2 text-sm">Pilih hiburan favoritmu dan nikmati keseruannya</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Character Grid -->
|
||||||
|
<!-- <div class="w-[90%] max-w-md grid grid-cols-2 gap-6 mt-10 px-2 z-20 pb-[40px]"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="hiburan in hiburans"
|
||||||
|
:key="hiburan.name"
|
||||||
|
@click="toMangas(hiburan.name)"
|
||||||
|
class="group cursor-pointer flex flex-col items-center p-1 rounded-2xl
|
||||||
|
backdrop-blur-md bg-white/40 border border-white/30 shadow-lg
|
||||||
|
hover:shadow-2xl transition-all duration-300 hover:-translate-y-2"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
:src="hiburan.img"
|
||||||
|
:alt="hiburan.name"
|
||||||
|
class="w-20 h-20 object-contain group-hover:scale-110 transition-transform duration-300"
|
||||||
|
/>
|
||||||
|
<p class="mt-3 text-emerald-700 font-semibold group-hover:text-emerald-500 transition-colors duration-300">
|
||||||
|
{{ hiburan.name }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div> -->
|
||||||
|
<!-- <div class="w-[90%] max-w-5xl grid grid-cols-2 sm:grid-cols-3
|
||||||
|
lg:grid-cols-4 gap-6 mt-10 px-4 z-20 pb-[40px] mx-auto"> -->
|
||||||
|
<div class="w-[90%] max-w-md grid grid-cols-2 gap-6 mt-10 px-2 z-20 pb-[40px]"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="hiburan in hiburans"
|
||||||
|
:key="hiburan.name"
|
||||||
|
@click="toMangas(hiburan.name)"
|
||||||
|
class="group cursor-pointer relative flex flex-col items-center justify-center p-4 rounded-2xl
|
||||||
|
backdrop-blur-xl bg-white/30 border border-white/40 shadow-lg
|
||||||
|
hover:shadow-[0_0_25px_rgba(34,197,94,0.4)] hover:bg-white/50
|
||||||
|
transform-gpu transition-all duration-300 hover:-translate-y-2 hover:scale-105"
|
||||||
|
@mousemove="onMouseMove($event, hiburan.name)"
|
||||||
|
@mouseleave="resetTilt(hiburan.name)"
|
||||||
|
:style="{ transform: tiltTransforms[hiburan.name] }"
|
||||||
|
>
|
||||||
|
<div v-if="hiburan.badge"
|
||||||
|
class="absolute top-2 right-2 bg-emerald-500 text-white text-[10px] font-bold px-2 py-1 rounded-full shadow">
|
||||||
|
{{ hiburan.badge }}
|
||||||
|
</div>
|
||||||
|
<div class="relative w-24 h-24 flex items-center justify-center">
|
||||||
|
<img
|
||||||
|
:src="hiburan.img"
|
||||||
|
:alt="hiburan.name"
|
||||||
|
class="w-full h-full object-contain drop-shadow-lg group-hover:scale-110 transition-transform duration-500"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="absolute inset-0 bg-gradient-to-t from-emerald-400/20 to-transparent rounded-2xl opacity-0 group-hover:opacity-100 transition-opacity duration-500"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
<p class="mt-3 text-emerald-800 font-bold tracking-wide group-hover:text-emerald-600 transition-colors duration-300">
|
||||||
|
{{ hiburan.name }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import router from '@/router';
|
||||||
|
import { getContent } from '@/services/content';
|
||||||
|
import { getGenre } from '@/services/genre';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "Entertain-ment",
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
genres : [],
|
||||||
|
hiburans: [
|
||||||
|
{ name: "Manga", img: "/images/manga.png" , badge:"Popular"},
|
||||||
|
{ name: "Mini Game", img: "/images/char/fox.png",badge:"Popular" },
|
||||||
|
{ name: "Videos", img: "/images/char/bear.png",badge:"Popular" },
|
||||||
|
{ name: "Cerita Rakyat", img: "/images/char/fox.png",badge:"Popular" },
|
||||||
|
],
|
||||||
|
contents : getContent(),
|
||||||
|
tiltTransforms: {}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
toMangas(title) {
|
||||||
|
if(title == 'Manga'){
|
||||||
|
router.push('/entertainment/mangas')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async loadGenres() {
|
||||||
|
const res = await getGenre();
|
||||||
|
this.genres = res.results;
|
||||||
|
console.log(res);
|
||||||
|
},
|
||||||
|
onMouseMove(event, name) {
|
||||||
|
const card = event.currentTarget;
|
||||||
|
const rect = card.getBoundingClientRect();
|
||||||
|
const x = event.clientX - rect.left;
|
||||||
|
const y = event.clientY - rect.top;
|
||||||
|
const rotateY = ((x / rect.width) - 0.5) * 15;
|
||||||
|
const rotateX = ((y / rect.height) - 0.5) * -15;
|
||||||
|
// this.$set(this.tiltTransforms, name, `rotateY(${rotateY}deg) rotateX(${rotateX}deg)`);
|
||||||
|
this.tiltTransforms = {
|
||||||
|
...this.tiltTransforms,
|
||||||
|
[name]: `rotateY(${rotateY}deg) rotateX(${rotateX}deg)`
|
||||||
|
};
|
||||||
|
},
|
||||||
|
resetTilt(name) {
|
||||||
|
// this.$set(this.tiltTransforms, name, `rotateY(0deg) rotateX(0deg)`);
|
||||||
|
this.tiltTransforms = {
|
||||||
|
...this.tiltTransforms,
|
||||||
|
[name]: `rotateY(0deg) rotateX(0deg)`
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.loadGenres();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
@keyframes pulse {
|
||||||
|
0%, 100% { opacity: 0.5; transform: scale(1); }
|
||||||
|
50% { opacity: 0.8; transform: scale(1.05); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tambahan efek glow lembut di hover */
|
||||||
|
.group:hover img {
|
||||||
|
filter: drop-shadow(0 0 10px rgba(72, 187, 120, 0.5));
|
||||||
|
}
|
||||||
|
.group {
|
||||||
|
perspective: 1000px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
|
||||||
|
</script>
|
||||||
@ -1,13 +1,14 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="min-h-screen flex flex-col items-center justify-center bg-gradient-to-b from-green-100 to-green-300 pb-28 pr-2 pl-2 mb-[-10px]"
|
<div class="min-h-screen flex flex-col items-center justify-center bg-gradient-to-b
|
||||||
style="background-image:url('/images/footer.png');
|
from-green-100 to-green-300 pb-28 pr-2 pl-2 mb-[-10px]"
|
||||||
background-repeat:no-repeat;
|
style="background-image:url('/images/footer.png');
|
||||||
background-position:bottom;
|
background-repeat:no-repeat;
|
||||||
background-size: 100% 200px;"
|
background-position:bottom;
|
||||||
margin-bottom=-10px;
|
background-size: 100% 200px;"
|
||||||
|
margin-bottom=-10px;
|
||||||
>
|
>
|
||||||
<!-- Character Preview -->
|
<!-- Character Preview -->
|
||||||
<div class="w-full max-w-md flex flex-col items-center z-60 " >
|
<div class="w-full max-w-md flex flex-col items-center z-60 mb-8">
|
||||||
<div class="relative flex items-end justify-center z-30 px-[10px] mt-[40px]">
|
<div class="relative flex items-end justify-center z-30 px-[10px] mt-[40px]">
|
||||||
<img
|
<img
|
||||||
src="/images/logo.png"
|
src="/images/logo.png"
|
||||||
@ -15,24 +16,6 @@
|
|||||||
class="object-contain relative z-30 pb-0"
|
class="object-contain relative z-30 pb-0"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="relative w-40 h-50 flex items-end justify-center z-30 px-[10px]">
|
|
||||||
<img
|
|
||||||
:src="selectedProfile.img"
|
|
||||||
:alt="selectedCharacter.name"
|
|
||||||
class="w-40 h-60 object-contain relative z-30 pb-12"
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
class="absolute w-[30px] h-[30px] right-0 top-[100px] z-40 bg-green-600 hover:bg-green-400 transition"
|
|
||||||
style="
|
|
||||||
-webkit-mask: url('/images/icon-edit.svg') no-repeat center;
|
|
||||||
-webkit-mask-size: contain;
|
|
||||||
mask: url('/images/icon-edit.svg') no-repeat center;
|
|
||||||
mask-size: contain;
|
|
||||||
"
|
|
||||||
@click="changeAva('profile')"
|
|
||||||
></div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full max-w-md flex flex-col items-center z-60">
|
<div class="w-full max-w-md flex flex-col items-center z-60">
|
||||||
<div class="bg-lime-200 shadow-xl rounded-2xl p-6 flex flex-col items-center
|
<div class="bg-lime-200 shadow-xl rounded-2xl p-6 flex flex-col items-center
|
||||||
@ -60,7 +43,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center justify-center w-[30px] h-[30px] rounded-full border-4 border-green-500 absolute top-2 right-2">
|
<div class="flex items-center justify-center w-[30px] h-[30px] rounded-full border-4 border-green-500 hover:bg-green-400 absolute top-2 right-2">
|
||||||
<img src="/images/but-nav-koleksi.png" alt="img" class="w-[20px] h-[20px] "
|
<img src="/images/but-nav-koleksi.png" alt="img" class="w-[20px] h-[20px] "
|
||||||
@click="changeAva('character')">
|
@click="changeAva('character')">
|
||||||
</div>
|
</div>
|
||||||
@ -75,7 +58,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Character Selection -->
|
<!-- Character Selection -->
|
||||||
<div class="w-full max-w-md flex flex-col items-center bg-green-grey py-[0px] mx-[10px] md:mx-0
|
<!-- <div class="w-full max-w-md flex flex-col items-center bg-green-grey py-[0px] mx-[10px] md:mx-0
|
||||||
transform transition-transform duration-300 hover:scale-105
|
transform transition-transform duration-300 hover:scale-105
|
||||||
">
|
">
|
||||||
<div class="rounded-3xl p-6 shadow-2xl-soft flex items-start gap-4 w-full py-[10px] my-[20px]"
|
<div class="rounded-3xl p-6 shadow-2xl-soft flex items-start gap-4 w-full py-[10px] my-[20px]"
|
||||||
@ -107,27 +90,39 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div> -->
|
||||||
<div class="w-full max-w-md flex flex-col items-center bg-white-grey py-[0px] mx-2.5 md:mx-5
|
<div
|
||||||
transform transition-transform duration-300 hover:scale-105
|
v-for="mission in missions"
|
||||||
">
|
:key="mission.id"
|
||||||
<div class="bg-white rounded-3xl p-6 shadow-2xl-soft flex items-start gap-4 w-full py-[10px] my-[20px]">
|
|
||||||
|
class="w-full max-w-md flex flex-col items-center bg-white-grey py-[0px] mx-2.5 md:mx-5
|
||||||
|
transform transition-transform duration-300 hover:scale-105"
|
||||||
|
>
|
||||||
|
<!-- <div class="bg-white rounded-3xl p-6 shadow-2xl-soft flex items-start gap-4 w-full py-[10px] my-[20px]">
|
||||||
<div class="flex-shrink-0 mt-1">
|
<div class="flex-shrink-0 mt-1">
|
||||||
<img src="/images/qicon2.png" alt="Quest Icon" class="w-12 h-12" />
|
<img src="/images/qicon2.png" alt="Quest Icon" class="w-12 h-12" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<h2 class="text-xl font-bold text-gray-800">{{ 'MAKAN SIANG' }}</h2>
|
<h2 class="text-xl font-bold text-gray-800">{{ mission.name }}</h2>
|
||||||
<p class="text-sm text-gray-600 mb-2">{{ 'Waktunya makan siang dan istirahat.' }}</p>
|
<p class="text-sm text-gray-600 mb-2">{{ mission.description }}</p>
|
||||||
|
<p class="text-xs font-semibold"
|
||||||
|
:class="{
|
||||||
|
'text-green-600': mission.userStatus === 'completed',
|
||||||
|
'text-yellow-600': mission.userStatus === 'in_progress',
|
||||||
|
'text-gray-500': mission.userStatus === 'not_started'
|
||||||
|
}">
|
||||||
|
Status: {{ mission.userStatus }}
|
||||||
|
</p>
|
||||||
<div class="flex items-center gap-4 text-xs text-gray-700">
|
<div class="flex items-center gap-4 text-xs text-gray-700">
|
||||||
<span class="flex items-center gap-1">
|
<span class="flex items-center gap-1">
|
||||||
<svg class="w-4 h-4 text-gray-500" fill="currentColor" viewBox="0 0 20 20"><path d="M10 2a8 8 0 100 16 8 8 0 000-16zM9 11V5a1 1 0 012 0v6a1 1 0 01-2 0zM10 13a1 1 0 100 2 1 1 0 000-2z"/></svg>
|
<svg class="w-4 h-4 text-gray-500" fill="currentColor" viewBox="0 0 20 20">
|
||||||
{{ '12:20' }}
|
<path d="M10 2a8 8 0 100 16 8 8 0 000-16zM9 11V5a1 1 0 012 0v6a1 1 0 01-2 0zM10 13a1 1 0 100 2 1 1 0 000-2z"/>
|
||||||
</span>
|
</svg>
|
||||||
|
{{ timeLeft(mission.date_valid, mission.time_to_valid) }}
|
||||||
|
</span>
|
||||||
<span class="flex items-center gap-1">
|
<span class="flex items-center gap-1">
|
||||||
<svg class="w-4 h-4 text-yellow-500" fill="currentColor" viewBox="0 0 20 20"><path d="M10 15a5 5 0 100-10 5 5 0 000 10z"/><path fill-rule="evenodd" d="M10 0a10 10 0 100 20 10 10 0 000-20zm0 18a8 8 0 100-16 8 8 0 000 16z" clip-rule="evenodd"/></svg>
|
<svg class="w-4 h-4 text-yellow-500" fill="currentColor" viewBox="0 0 20 20"><path d="M10 15a5 5 0 100-10 5 5 0 000 10z"/><path fill-rule="evenodd" d="M10 0a10 10 0 100 20 10 10 0 000-20zm0 18a8 8 0 100-16 8 8 0 000 16z" clip-rule="evenodd"/></svg>
|
||||||
+{{ '1000' }} koin
|
+{{ mission.coin }} koin
|
||||||
</span>
|
</span>
|
||||||
<span class="flex items-center gap-1">
|
<span class="flex items-center gap-1">
|
||||||
<svg class="w-4 h-4 text-green-600 transform rotate-180" fill="currentColor" viewBox="0 0 20 20"><path d="M10 14a1 1 0 01-.707-.293l-4-4a1 1 0 011.414-1.414L10 11.586l3.293-3.293a1 1 0 011.414 1.414l-4 4A1 1 0 0110 14z"/></svg>
|
<svg class="w-4 h-4 text-green-600 transform rotate-180" fill="currentColor" viewBox="0 0 20 20"><path d="M10 14a1 1 0 01-.707-.293l-4-4a1 1 0 011.414-1.414L10 11.586l3.293-3.293a1 1 0 011.414 1.414l-4 4A1 1 0 0110 14z"/></svg>
|
||||||
@ -135,12 +130,112 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-4 text-xs text-gray-700">
|
<div class="flex items-center gap-4 text-xs text-gray-700">
|
||||||
<button class="w-full mt-4 py-2 px-4 rounded-lg bg-red-600 text-white font-bold hover:bg-green-700 transition">MULAI QUEST</button>
|
<button v-if="!mission.userStatus ==='completed'" class="w-full mt-4 py-2 px-4 rounded-lg bg-red-600 text-white font-bold
|
||||||
|
hover:bg-green-700 transition"
|
||||||
|
@click="handleMissionClick(mission)"
|
||||||
|
>
|
||||||
|
{{ mission.userStatus === 'completed' ? 'SELESAI' :
|
||||||
|
mission.userStatus === 'in_progress' ? 'LANJUTKAN' :
|
||||||
|
'MULAI QUEST' }}
|
||||||
|
</button>
|
||||||
|
<span v-else >Selesai</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div> -->
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="relative rounded-3xl p-6 shadow-2xl-soft flex items-start gap-4 w-full py-[10px] my-[20px] overflow-hidden"
|
||||||
|
:class="{
|
||||||
|
'bg-green-100 pointer-events-none opacity-95': mission.userStatus === 'completed',
|
||||||
|
'bg-yellow-100': mission.userStatus === 'in_progress',
|
||||||
|
'bg-white hover:scale-105': mission.userStatus === 'not_started'
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<!-- ✅ Stamp layer BEHIND content -->
|
||||||
|
<div
|
||||||
|
v-if="mission.userStatus === 'completed'"
|
||||||
|
class="absolute inset-0 flex justify-end items-center pr-6 z-0"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="text-[64px] font-extrabold text-green-600/15 rotate-[-20deg] select-none pointer-events-none"
|
||||||
|
style="mix-blend-mode: multiply;"
|
||||||
|
>
|
||||||
|
SELESAI
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ✅ Content layer ABOVE stamp -->
|
||||||
|
<div class="flex-shrink-0 mt-1 relative z-10">
|
||||||
|
<img src="/images/qicon2.png" alt="Quest Icon" class="w-12 h-12" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex-1 relative z-10">
|
||||||
|
<h2 class="text-xl font-bold text-gray-800">{{ mission.name }}</h2>
|
||||||
|
<p class="text-sm text-gray-600 mb-2">{{ mission.description }}</p>
|
||||||
|
|
||||||
|
<p
|
||||||
|
class="text-xs font-semibold"
|
||||||
|
:class="{
|
||||||
|
'text-green-600': mission.userStatus === 'completed',
|
||||||
|
'text-yellow-600': mission.userStatus === 'in_progress',
|
||||||
|
'text-gray-500': mission.userStatus === 'not_started'
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
Status: {{ mission.userStatus }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-4 text-xs text-gray-700">
|
||||||
|
<span class="flex items-center gap-1">
|
||||||
|
<svg class="w-4 h-4 text-gray-500" fill="currentColor" viewBox="0 0 20 20">
|
||||||
|
<path
|
||||||
|
d="M10 2a8 8 0 100 16 8 8 0 000-16zM9 11V5a1 1 0 012 0v6a1 1 0 01-2 0zM10 13a1 1 0 100 2 1 1 0 000-2z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
{{ timeLeft(mission.date_valid, mission.time_to_valid) }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span class="flex items-center gap-1">
|
||||||
|
<svg class="w-4 h-4 text-yellow-500" fill="currentColor" viewBox="0 0 20 20">
|
||||||
|
<path d="M10 15a5 5 0 100-10 5 5 0 000 10z" />
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M10 0a10 10 0 100 20 10 10 0 000-20zm0 18a8 8 0 100-16 8 8 0 000 16z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
+{{ mission.coin }} koin
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span class="flex items-center gap-1">
|
||||||
|
<svg
|
||||||
|
class="w-4 h-4 text-green-600 transform rotate-180"
|
||||||
|
fill="currentColor"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M10 14a1 1 0 01-.707-.293l-4-4a1 1 0 011.414-1.414L10 11.586l3.293-3.293a1 1 0 011.414 1.414l-4 4A1 1 0 0110 14z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
+100mm
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ✅ Button or empty block -->
|
||||||
|
<div class="flex items-center gap-4 text-xs text-gray-700">
|
||||||
|
<button
|
||||||
|
v-if="mission.userStatus !== 'completed'"
|
||||||
|
class="w-full mt-4 py-2 px-4 rounded-lg bg-red-600 text-white font-bold hover:bg-green-700 transition"
|
||||||
|
@click="handleMissionClick(mission)"
|
||||||
|
>
|
||||||
|
{{ mission.userStatus === 'in_progress' ? 'LANJUTKAN' : 'MULAI QUEST' }}
|
||||||
|
</button>
|
||||||
|
<div v-else class="w-full mt-4 py-4 px-4"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full max-w-md flex flex-col items-center bg-green-grey py-[0px] mx-2.5 md:mx-5
|
<!-- <div class="w-full max-w-md flex flex-col items-center bg-green-grey py-[0px] mx-2.5 md:mx-5
|
||||||
transform transition-transform duration-300 hover:scale-105
|
transform transition-transform duration-300 hover:scale-105
|
||||||
">
|
">
|
||||||
<div class="bg-white rounded-3xl p-6 shadow-2xl-soft flex items-start gap-4 w-full py-[10px] my-[20px]">
|
<div class="bg-white rounded-3xl p-6 shadow-2xl-soft flex items-start gap-4 w-full py-[10px] my-[20px]">
|
||||||
@ -171,7 +266,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div> -->
|
||||||
<!-- Modal -->
|
<!-- Modal -->
|
||||||
<div
|
<div
|
||||||
v-if="showModal"
|
v-if="showModal"
|
||||||
@ -199,6 +294,23 @@
|
|||||||
<img :src="prof.img" :alt="prof.name" class="w-16 h-16 rounded"/>
|
<img :src="prof.img" :alt="prof.name" class="w-16 h-16 rounded"/>
|
||||||
<span class="text-sm mt-1">{{ prof.name }}</span>
|
<span class="text-sm mt-1">{{ prof.name }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Full-width logout area -->
|
||||||
|
<div class="col-span-3 flex justify-center mt-6">
|
||||||
|
<button
|
||||||
|
@click="handleLogout"
|
||||||
|
class="flex items-center gap-2 bg-gradient-to-r from-red-500 to-rose-600
|
||||||
|
hover:from-red-600 hover:to-rose-700 text-white font-semibold py-2 px-6
|
||||||
|
rounded-full shadow-md hover:shadow-lg transform transition-all duration-200
|
||||||
|
hover:scale-105 active:scale-95 focus:outline-none focus:ring-2 focus:ring-red-400"
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none"
|
||||||
|
viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
|
d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a2 2 0 01-2 2H5a2 2 0 01-2-2V7a2 2 0 012-2h6a2 2 0 012 2v1" />
|
||||||
|
</svg>
|
||||||
|
<span>Logout</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-3 gap-4" v-if="modalType === 'character'">
|
<div class="grid grid-cols-3 gap-4" v-if="modalType === 'character'">
|
||||||
<div
|
<div
|
||||||
@ -218,7 +330,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { getContent } from '@/services/content';
|
||||||
|
import { getMissions,getMissionLogs, createMissionLog } from '@/services/missions';
|
||||||
export default {
|
export default {
|
||||||
name: "App",
|
name: "App",
|
||||||
data() {
|
data() {
|
||||||
@ -245,6 +358,12 @@ export default {
|
|||||||
selectedProfile : {name : "profile", img: "/images/propic-001.svg"},
|
selectedProfile : {name : "profile", img: "/images/propic-001.svg"},
|
||||||
modalType : "profile",
|
modalType : "profile",
|
||||||
isDone : true,
|
isDone : true,
|
||||||
|
contents : getContent(),
|
||||||
|
missions : [],
|
||||||
|
now : new Date(),
|
||||||
|
timer: null,
|
||||||
|
misi:[],
|
||||||
|
logs:[]
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@ -259,7 +378,142 @@ export default {
|
|||||||
profileListSelected(profile) {
|
profileListSelected(profile) {
|
||||||
this.selectedProfile = profile;
|
this.selectedProfile = profile;
|
||||||
this.showModal = false;
|
this.showModal = false;
|
||||||
|
},
|
||||||
|
async getMission() {
|
||||||
|
const res = await getMissions();
|
||||||
|
this.missions = res.results;
|
||||||
|
console.log(this.missions);
|
||||||
|
},
|
||||||
|
async getLogs(){
|
||||||
|
const resLog = await getMissionLogs();
|
||||||
|
this.logs = resLog.results;
|
||||||
|
},
|
||||||
|
timeLeft2(dateValid, timeToValid) {
|
||||||
|
if (!dateValid || !timeToValid) return "—";
|
||||||
|
try {
|
||||||
|
const now = new Date(this.now);
|
||||||
|
const target = new Date(`${dateValid}T${timeToValid}`);
|
||||||
|
const diffMs = target - now;
|
||||||
|
if (diffMs <= 0) return "Waktu habis";
|
||||||
|
|
||||||
|
const totalMinutes = Math.floor(diffMs / (1000 * 60));
|
||||||
|
const hours = Math.floor(totalMinutes / 60);
|
||||||
|
const minutes = totalMinutes % 60;
|
||||||
|
return `${hours} jam ${minutes} menit`;
|
||||||
|
} catch (e) {
|
||||||
|
return "—";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
timeLeft(dateValid, timeToValid) {
|
||||||
|
if (!dateValid || !timeToValid) return "—";
|
||||||
|
|
||||||
|
const now = new Date(this.now);
|
||||||
|
const target = new Date(`${dateValid}T${timeToValid}`);
|
||||||
|
const diffMs = target - now;
|
||||||
|
|
||||||
|
if (diffMs <= 0) return "Waktu habis";
|
||||||
|
|
||||||
|
const totalSeconds = Math.floor(diffMs / 1000);
|
||||||
|
const hours = Math.floor(totalSeconds / 3600);
|
||||||
|
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
||||||
|
const seconds = totalSeconds % 60;
|
||||||
|
|
||||||
|
const pad = (num) => String(num).padStart(2, '0');
|
||||||
|
|
||||||
|
return `${pad(hours)}:${pad(minutes)}:${pad(seconds)}`;
|
||||||
|
},
|
||||||
|
downtimeHours(from, to) {
|
||||||
|
if (!from || !to) return '-';
|
||||||
|
const [fh, fm] = from.split(':').map(Number);
|
||||||
|
const [th, tm] = to.split(':').map(Number);
|
||||||
|
const start = fh * 60 + fm;
|
||||||
|
const end = th * 60 + tm;
|
||||||
|
const diff = end - start;
|
||||||
|
const hours = (diff / 60).toFixed(2);
|
||||||
|
return hours;
|
||||||
|
},
|
||||||
|
async getMissions() {
|
||||||
|
const [resMissions, resLogs] = await Promise.all([
|
||||||
|
getMissions(),
|
||||||
|
getMissionLogs()
|
||||||
|
]);
|
||||||
|
|
||||||
|
const logs = resLogs.results || [];
|
||||||
|
this.misi = resMissions.results.map(mission => {
|
||||||
|
const log = logs.find(l => l.mission === mission.id) || {};
|
||||||
|
return {
|
||||||
|
...mission,
|
||||||
|
user_id: log.user_id || null,
|
||||||
|
userStatus: log.status || 'not_started', // status default
|
||||||
|
claimed_at: log.claimed_at || null,
|
||||||
|
completed_at: log.completed_at || null,
|
||||||
|
coin_earned: log.coin || mission.coin,
|
||||||
|
point_earned: log.point || mission.point,
|
||||||
|
image_log: log.image_log || null,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
this.missions = this.misi
|
||||||
|
},
|
||||||
|
handleMissionClick(mission) {
|
||||||
|
//convert to array task
|
||||||
|
const missionWithTask = {
|
||||||
|
...mission,
|
||||||
|
tasks:[
|
||||||
|
{id: mission.id,name:mission.name, type: mission.task, completed:false}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
if (mission.userStatus === 'completed') return;
|
||||||
|
if (mission.userStatus === 'not_started') {
|
||||||
|
this.startMission(mission);
|
||||||
|
}
|
||||||
|
if (mission.task === 'scan-qr') {
|
||||||
|
console.log(mission)
|
||||||
|
this.$router.push({
|
||||||
|
name: 'quest-missions',
|
||||||
|
params: { id: mission.id } ,
|
||||||
|
query: { mission: JSON.stringify(missionWithTask) }
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.$router.push({
|
||||||
|
name: 'quest-missions',
|
||||||
|
params: { id: mission.id } ,
|
||||||
|
query: { mission: JSON.stringify(missionWithTask) }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async startMission(mission) {
|
||||||
|
await createMissionLog({
|
||||||
|
mission: mission.id,
|
||||||
|
user_id: this.currentUser.id,
|
||||||
|
status: 'in_progress',
|
||||||
|
coin: mission.coin,
|
||||||
|
point: mission.point
|
||||||
|
});
|
||||||
|
|
||||||
|
mission.userStatus = 'in_progress';
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
mounted() {
|
||||||
|
this.getMission();
|
||||||
|
this.getMissions();
|
||||||
|
this.timer = setInterval(() => {
|
||||||
|
this.now = Date.now();
|
||||||
|
}, 1000);
|
||||||
|
},
|
||||||
|
|
||||||
|
beforeUnmount() {
|
||||||
|
clearInterval(this.timer);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
<script setup>
|
||||||
|
import { useAuthStore } from '@/stores/auth'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
const router = useRouter()
|
||||||
|
const auth = useAuthStore()
|
||||||
|
const handleLogout = () => {
|
||||||
|
auth.logout()
|
||||||
|
router.push('/login')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@ -1,50 +1,71 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col items-center justify-center bg-gradient-to-b from-green-100 to-green-300">
|
<div class="min-h-screen flex flex-col items-center justify-center bg-gradient-to-b from-green-100 to-green-300 p-4
|
||||||
<div class="w-full max-w-md flex flex-col items-center z-60 mt-10">
|
mb-[-100px]"
|
||||||
<div class="relative flex items-end justify-center z-30">
|
style="background-image:url('/images/footer.png');
|
||||||
<img
|
background-repeat:no-repeat;
|
||||||
src="/images/logo.png"
|
background-position:bottom;
|
||||||
:alt="selectedCharacter.name"
|
background-size: 100% 200px;"
|
||||||
class="object-contain relative z-30 pb-0"
|
margin-bottom=-10px;
|
||||||
|
>
|
||||||
|
<!-- Container utama -->
|
||||||
|
<div class="w-full max-w-sm sm:max-w-md bg-white rounded-3xl shadow-lg p-6 sm:p-8 flex flex-col items-center">
|
||||||
|
<!-- Logo -->
|
||||||
|
<div class="flex flex-col items-center">
|
||||||
|
<img src="/images/logo.png" alt="Logo" class="w-24 h-auto mb-4" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Avatar/Profile -->
|
||||||
|
<div class="relative w-32 h-32 flex items-center justify-center mb-6">
|
||||||
|
<img
|
||||||
|
:src="selectedProfile.img"
|
||||||
|
:alt="selectedCharacter.name"
|
||||||
|
class="w-32 h-32 object-contain rounded-full border-4 border-lime-400 shadow-md"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Form -->
|
||||||
|
<div class="w-full">
|
||||||
|
<div class="mb-4">
|
||||||
|
<input
|
||||||
|
v-model="username"
|
||||||
|
placeholder="Username"
|
||||||
|
type="text"
|
||||||
|
class="w-full h-12 px-4 py-2 text-gray-700 bg-gray-50 border border-gray-300 rounded-2xl
|
||||||
|
focus:outline-none focus:ring-2 focus:ring-lime-400 focus:border-lime-400"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="relative w-40 h-50 flex items-end justify-center z-30 px-[10px]">
|
|
||||||
<img
|
<div class="mb-6">
|
||||||
:src="selectedProfile.img"
|
<input
|
||||||
:alt="selectedCharacter.name"
|
v-model="password"
|
||||||
class="w-40 h-60 object-contain relative z-30"
|
type="password"
|
||||||
|
placeholder="Password"
|
||||||
|
class="w-full h-12 px-4 py-2 text-gray-700 bg-gray-50 border border-gray-300 rounded-2xl
|
||||||
|
focus:outline-none focus:ring-2 focus:ring-lime-400 focus:border-lime-400"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<!-- Character Selection -->
|
<button
|
||||||
<div class="w-full flex flex-col items-center z-60">
|
@click="login"
|
||||||
<div class="w-full p-4">
|
class="w-full h-12 bg-lime-500 text-white font-semibold rounded-2xl
|
||||||
<div class="mb-4">
|
hover:bg-lime-400 transition-all focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-lime-500"
|
||||||
<input type="text" placeholder="email"
|
>
|
||||||
class="w-full h-[50px] px-4 py-2 text-grey-700 bg-white border border-grey-300 rounded-2xl
|
LOGIN
|
||||||
focus:outline-none focus:ring focus:ring-grey-100 focus:border-grey-300"
|
</button>
|
||||||
/>
|
|
||||||
</div>
|
<div class="w-full flex items-center justify-center mt-4">
|
||||||
<div class="mb-4">
|
<router-link to="/register" class="text-blue-600 font-semibold hover:underline">
|
||||||
<input type="text" placeholder="Password"
|
REGISTER
|
||||||
class="w-full h-[50px] px-4 py-2 text-grey-700 bg-white border border-grey-300 rounded-2xl focus:outline-none focus:ring focus:ring-grey-100 focus:border-grey-300"
|
</router-link>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button class="w-full mt-6 h-[60px] bg-lime-500 border border-transparent rounded-lg px-4 py-2 text-sm font-medium text-white hover:bg-lime-400 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-lime-500"> R E G I S T E R </button>
|
|
||||||
<div class="w-full flex items-center justify-center mt-2">
|
|
||||||
<router-link to="/register">
|
|
||||||
<span class="cursor-pointer justify-center text-center text-sm mt-2 text-blue-600 font-bold">R E G I S T E R</span>
|
|
||||||
</router-link>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
name: "App",
|
name: "LoginPage",
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
characters: [
|
characters: [
|
||||||
@ -58,16 +79,12 @@ export default {
|
|||||||
{ name: "Sheep", img: "../images/char/sheep.png" },
|
{ name: "Sheep", img: "../images/char/sheep.png" },
|
||||||
{ name: "Tiger", img: "../images/char/tiger.png" },
|
{ name: "Tiger", img: "../images/char/tiger.png" },
|
||||||
],
|
],
|
||||||
|
profiles: [
|
||||||
profiles : [
|
{ name: "profile", img: "/images/propic-001.svg" },
|
||||||
{name : "profile", img: "/images/propic-001.svg"},
|
{ name: "profile", img: "/images/propic-blank.svg" },
|
||||||
{name : "profile", img: "/images/propic-blank.svg"},
|
|
||||||
],
|
],
|
||||||
|
|
||||||
selectedCharacter: { name: "Tiger", img: "../images/char/tiger.png" },
|
selectedCharacter: { name: "Tiger", img: "../images/char/tiger.png" },
|
||||||
showModal:false,
|
selectedProfile: { name: "profile", img: "/images/propic-001.svg" },
|
||||||
selectedProfile : {name : "profile", img: "/images/propic-001.svg"},
|
|
||||||
modalType : "profile",
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@ -79,10 +96,39 @@ export default {
|
|||||||
this.modalType = type;
|
this.modalType = type;
|
||||||
this.showModal = true;
|
this.showModal = true;
|
||||||
},
|
},
|
||||||
profileListSelected(profile) {
|
profileListSelected(profile) {
|
||||||
this.selectedProfile = profile;
|
this.selectedProfile = profile;
|
||||||
this.showModal = false;
|
this.showModal = false;
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { useAuthStore } from '@/stores/auth'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const auth = useAuthStore()
|
||||||
|
const username = ref('')
|
||||||
|
const password = ref('')
|
||||||
|
const error = ref('')
|
||||||
|
|
||||||
|
async function login() {
|
||||||
|
try {
|
||||||
|
await
|
||||||
|
await auth.login(username.value, password.value)
|
||||||
|
router.push('/')
|
||||||
|
} catch (err) {
|
||||||
|
error.value = err.message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* Tambahan kecil untuk tampilan */
|
||||||
|
body {
|
||||||
|
font-family: 'Inter', sans-serif;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
102
src/pages/Mangas.vue
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="min-h-screen flex flex-col bg-gradient-to-b from-emerald-100 via-lime-50 to-white
|
||||||
|
text-gray-800 relative overflow-hidden mb-[-10px]"
|
||||||
|
>
|
||||||
|
<!-- Header Section -->
|
||||||
|
<div class="w-full px-6 py-4 flex items-center justify-between backdrop-blur-md bg-white/30 border-b border-emerald-100 z-20">
|
||||||
|
<!-- Back Button -->
|
||||||
|
<button
|
||||||
|
@click="goBack"
|
||||||
|
class="flex items-center gap-2 text-emerald-600 hover:text-emerald-800 transition-colors"
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
|
||||||
|
</svg>
|
||||||
|
<span class="font-semibold">Kembali</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Breadcrumb / Path -->
|
||||||
|
<div class="text-sm text-gray-600">
|
||||||
|
<span class="text-emerald-600 font-medium cursor-pointer hover:underline" @click="goHome">Beranda</span>
|
||||||
|
<span class="mx-2">›</span>
|
||||||
|
<span class="text-gray-700 font-semibold">Manga</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Title -->
|
||||||
|
<div class="text-center mt-8">
|
||||||
|
<h1 class="text-4xl font-extrabold bg-gradient-to-r from-emerald-500 to-lime-500 bg-clip-text text-transparent">
|
||||||
|
Daftar Manga
|
||||||
|
</h1>
|
||||||
|
<p class="text-gray-500 mt-2">Temukan bacaan favoritmu di sini 📖</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Manga Grid -->
|
||||||
|
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-6 mt-10 px-6 pb-20">
|
||||||
|
<div
|
||||||
|
v-for="manga in mangas"
|
||||||
|
:key="manga.title"
|
||||||
|
@click="toMangaDetaial(manga.title)"
|
||||||
|
class="group cursor-pointer rounded-2xl bg-white/60 backdrop-blur-lg p-4 shadow-md border border-white/40
|
||||||
|
hover:shadow-2xl hover:-translate-y-2 transition-all duration-300 relative overflow-hidden"
|
||||||
|
>
|
||||||
|
<!-- Manga Cover -->
|
||||||
|
<div class="relative">
|
||||||
|
<img
|
||||||
|
:src="manga.cover"
|
||||||
|
:alt="manga.title"
|
||||||
|
class="w-full h-48 object-cover rounded-xl group-hover:scale-105 transition-transform duration-500"
|
||||||
|
/>
|
||||||
|
<div class="absolute inset-0 bg-gradient-to-t from-black/40 to-transparent rounded-xl opacity-0 group-hover:opacity-100 transition-opacity"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Manga Info -->
|
||||||
|
<div class="mt-3 text-center">
|
||||||
|
<h3 class="font-semibold text-lg text-gray-800 truncate">{{ manga.title }}</h3>
|
||||||
|
<p class="text-sm text-gray-500">{{ manga.genre }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: "MangaList",
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
mangas: [
|
||||||
|
{ title: "Naruto", genre: "Action", cover: "/images/manga/naruto.png" },
|
||||||
|
{ title: "One Piece", genre: "Adventure", cover: "/images/manga/onepiece.png" },
|
||||||
|
{ title: "Demon Slayer", genre: "Fantasy", cover: "/images/manga/demonslayer.png" },
|
||||||
|
{ title: "Attack on Titan", genre: "Drama", cover: "/images/manga/attackot.png" },
|
||||||
|
{ title: "Full Metal Alchemist", genre: "Action", cover: "/images/manga/fullmetal.png" },
|
||||||
|
{ title: "Jujutsu Kaisen", genre: "Supernatural", cover: "/images/manga/jujutsu.png" },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
goBack() {
|
||||||
|
window.history.back('/entertainment/mangas');
|
||||||
|
},
|
||||||
|
goHome() {
|
||||||
|
this.$router.push("/");
|
||||||
|
},
|
||||||
|
toMangaDetaial(title){
|
||||||
|
const check = this.mangas.filter((manga) => manga.title == title)
|
||||||
|
|
||||||
|
if(check[0].title == 'One Piece'){
|
||||||
|
return this.$router.push(`/entertainment/manga/${check[0].id ?? 1}/chapters`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* Tambahan animasi halus di hover */
|
||||||
|
.group:hover img {
|
||||||
|
filter: drop-shadow(0 0 10px rgba(72, 187, 120, 0.3));
|
||||||
|
}
|
||||||
|
</style>
|
||||||
304
src/pages/MissionPage.vue
Normal file
@ -0,0 +1,304 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="min-h-screen flex flex-col items-center justify-center bg-gradient-to-b from-teal-100 to-lime-200 p-6"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="max-w-lg w-full bg-white/80 rounded-3xl shadow-lg p-6 backdrop-blur-lg"
|
||||||
|
>
|
||||||
|
<!-- Judul -->
|
||||||
|
<h2 class="text-2xl font-bold text-emerald-700 mb-2">
|
||||||
|
{{ mission.name }}
|
||||||
|
</h2>
|
||||||
|
<p class="text-gray-700 mb-4">{{ mission.description }}</p>
|
||||||
|
|
||||||
|
<!-- Daftar Task -->
|
||||||
|
<div
|
||||||
|
v-for="task in mission.tasks"
|
||||||
|
:key="task.id"
|
||||||
|
class="flex flex-col gap-3 mb-4 border-b pb-4"
|
||||||
|
>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<span class="text-sm font-semibold text-gray-700">
|
||||||
|
{{ task.type === "scan-qr" ? "Scan QR Code" : "Upload Gambar" }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<button
|
||||||
|
v-if="!task.completed"
|
||||||
|
@click="handleTask(task)"
|
||||||
|
class="px-3 py-1 bg-emerald-500 text-white text-xs rounded-lg hover:bg-emerald-600"
|
||||||
|
>
|
||||||
|
Selesaikan
|
||||||
|
</button>
|
||||||
|
<span v-else class="text-emerald-600 font-semibold">✅ Selesai</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ✅ SCAN QR -->
|
||||||
|
<div
|
||||||
|
v-if="activeTask && activeTask.id === task.id && task.type === 'scan-qr'"
|
||||||
|
class="border rounded-lg overflow-hidden"
|
||||||
|
>
|
||||||
|
<!-- Kamera tersedia -->
|
||||||
|
<div v-if="!cameraUnavailable">
|
||||||
|
<QrcodeStream
|
||||||
|
:constraints="constraints"
|
||||||
|
@decode="onScanSuccess"
|
||||||
|
@detect="onDetect"
|
||||||
|
@init="onInit"
|
||||||
|
/>
|
||||||
|
<p class="text-xs text-gray-500 text-center mt-2">
|
||||||
|
Arahkan kamera ke QR Code...
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
@click="manualMode = !manualMode"
|
||||||
|
class="text-xs mt-2 px-2 py-1 bg-yellow-400 text-white rounded hover:bg-yellow-500"
|
||||||
|
>
|
||||||
|
{{ manualMode ? 'Kembali ke Kamera' : 'Gunakan Mode Manual (Upload QR)' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Fallback upload jika kamera tidak tersedia -->
|
||||||
|
<div v-else class="flex flex-col items-center gap-2 mt-2">
|
||||||
|
<p class="text-sm text-gray-600">📷 Kamera tidak tersedia. Unggah QR code secara manual:</p>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
@change="onFileSelected"
|
||||||
|
class="text-sm border rounded p-2"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
@click="uploadImage"
|
||||||
|
class="px-3 py-1 bg-blue-500 text-white text-xs rounded-lg hover:bg-blue-600"
|
||||||
|
>
|
||||||
|
Upload & Simpan
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ✅ UPLOAD IMAGE -->
|
||||||
|
<div
|
||||||
|
v-if="activeTask && activeTask.id === task.id && task.type === 'upload-photo'"
|
||||||
|
class="flex flex-col gap-2"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
@change="onFileSelected"
|
||||||
|
class="text-sm border rounded p-2"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
@click="uploadImage"
|
||||||
|
class="px-3 py-1 bg-blue-500 text-white text-xs rounded-lg hover:bg-blue-600"
|
||||||
|
>
|
||||||
|
Upload & Simpan
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ✅ Final Button -->
|
||||||
|
<button
|
||||||
|
v-if="isMissionComplete && mission.status !== 'claimed'"
|
||||||
|
@click="finishMission"
|
||||||
|
class="w-full mt-4 py-2 bg-yellow-500 text-white font-bold rounded-lg hover:bg-yellow-600 transition"
|
||||||
|
>
|
||||||
|
Tandai Sebagai Selesai
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed, onMounted } from "vue"
|
||||||
|
import { QrcodeStream } from "vue-qrcode-reader"
|
||||||
|
import api from "@/util/api"
|
||||||
|
import { useRoute } from "vue-router"
|
||||||
|
import { createMissionLog } from '@/services/missions';
|
||||||
|
import { useAuthStore } from '@/stores/auth'
|
||||||
|
|
||||||
|
const mission = ref({
|
||||||
|
id: 1,
|
||||||
|
name: "Cari Artefak Laut",
|
||||||
|
description: "Scan QR dan upload bukti penemuan artefak.",
|
||||||
|
status: "in_progress",
|
||||||
|
tasks: [
|
||||||
|
{ id: 1, type: "scan_qr", completed: false },
|
||||||
|
{ id: 2, type: "upload_image", completed: false },
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
const user_id = 42
|
||||||
|
const activeTask = ref(null)
|
||||||
|
const selectedFile = ref(null)
|
||||||
|
const cameraUnavailable = ref(false)
|
||||||
|
const constraints = ref({ facingMode: "environment" })
|
||||||
|
const scanning = ref(false)
|
||||||
|
const detectedOnce = ref(false)
|
||||||
|
const router = useRoute();
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
const currentUser = computed(() => authStore.currentUser)
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
if(router.query.mission){
|
||||||
|
const data = router.query.mission
|
||||||
|
console.log(data)
|
||||||
|
mission.value = JSON.parse(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const devices = await navigator.mediaDevices.enumerateDevices()
|
||||||
|
const videoDevices = devices.filter((d) => d.kind === "videoinput")
|
||||||
|
|
||||||
|
if (videoDevices.length === 0) {
|
||||||
|
cameraUnavailable.value = true
|
||||||
|
} else {
|
||||||
|
constraints.value = {
|
||||||
|
facingMode: videoDevices.length > 1 ? "environment" : "user",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.warn("Tidak bisa mendeteksi kamera:", err)
|
||||||
|
cameraUnavailable.value = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
async function onDetect(detectedCodes) {
|
||||||
|
if (!detectedCodes || detectedCodes.length === 0) return
|
||||||
|
if (detectedOnce.value) return
|
||||||
|
|
||||||
|
detectedOnce.value = true
|
||||||
|
const code = detectedCodes[0].rawValue
|
||||||
|
console.log("🎯 QR terdeteksi:", code)
|
||||||
|
|
||||||
|
try {
|
||||||
|
scanning.value = true
|
||||||
|
await api.post("task/tasks", {
|
||||||
|
mission: mission.value.id,
|
||||||
|
user_id,
|
||||||
|
type: "scan-qr",
|
||||||
|
data: code,
|
||||||
|
completed_at: new Date().toISOString(),
|
||||||
|
})
|
||||||
|
|
||||||
|
completeTask(activeTask.value)
|
||||||
|
activeTask.value = null
|
||||||
|
alert("QR berhasil disimpan ke server!")
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Gagal menyimpan QR:", error)
|
||||||
|
alert("Gagal menyimpan data ke server.")
|
||||||
|
} finally {
|
||||||
|
scanning.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const isMissionComplete = computed(() =>
|
||||||
|
mission.value.tasks?.every((t) => t.completed)
|
||||||
|
)
|
||||||
|
|
||||||
|
function handleTask(task) {
|
||||||
|
console.log(task)
|
||||||
|
activeTask.value = task
|
||||||
|
}
|
||||||
|
|
||||||
|
function onInit(promise) {
|
||||||
|
promise.catch((error) => {
|
||||||
|
console.error(error)
|
||||||
|
if (error.name === "NotAllowedError") {
|
||||||
|
alert("Akses kamera ditolak. Izinkan kamera di browser Anda.")
|
||||||
|
} else if (error.name === "NotFoundError") {
|
||||||
|
alert("Tidak ada kamera terdeteksi di perangkat ini.")
|
||||||
|
} else if (error.name === "NotReadableError") {
|
||||||
|
alert("Kamera sedang digunakan oleh aplikasi lain.")
|
||||||
|
} else {
|
||||||
|
alert("Gagal mengakses kamera: " + error.message)
|
||||||
|
}
|
||||||
|
cameraUnavailable.value = true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
async function onScanSuccess(result) {
|
||||||
|
if (!result) return
|
||||||
|
console.log("QR ditemukan:", result)
|
||||||
|
|
||||||
|
try {
|
||||||
|
// await api.post("task/tasks", {
|
||||||
|
// mission: mission.value.id,
|
||||||
|
// user_id: user_id,
|
||||||
|
// type: "scan-qr",
|
||||||
|
// data: result,
|
||||||
|
// completed_at: new Date().toISOString(),
|
||||||
|
// })
|
||||||
|
this.startMission(data={
|
||||||
|
mission: mission.value.id,
|
||||||
|
user_id: user_id,
|
||||||
|
type: "scan-qr",
|
||||||
|
data: result,
|
||||||
|
coin: mission.value.coin,
|
||||||
|
point: mission.value.point,
|
||||||
|
completed_at: new Date().toISOString(),
|
||||||
|
claimed_at: new Date().toISOString(),
|
||||||
|
})
|
||||||
|
|
||||||
|
completeTask(activeTask.value)
|
||||||
|
activeTask.value = null
|
||||||
|
alert("QR berhasil disimpan ke server.")
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
alert("Gagal menyimpan data ke server.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function onFileSelected(e) {
|
||||||
|
selectedFile.value = e.target.files[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
async function uploadImage() {
|
||||||
|
if (!selectedFile.value) return alert("Pilih gambar dulu!")
|
||||||
|
|
||||||
|
const formData = new FormData()
|
||||||
|
formData.append("mission", mission.value.id);
|
||||||
|
formData.append("user_id", user_id);
|
||||||
|
formData.append("status", "completed");
|
||||||
|
formData.append("coin", mission.value.coin);
|
||||||
|
formData.append("point", mission.value.point);
|
||||||
|
formData.append("image_log", selectedFile.value);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await api.post("mission/mission-logs/", formData, {
|
||||||
|
headers: { "Content-Type": "multipart/form-data" },
|
||||||
|
})
|
||||||
|
|
||||||
|
completeTask(activeTask.value)
|
||||||
|
activeTask.value = null
|
||||||
|
selectedFile.value = null
|
||||||
|
alert("📸 Gambar berhasil diupload!")
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
alert("Gagal upload gambar.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function completeTask(task) {
|
||||||
|
task.completed = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function finishMission() {
|
||||||
|
mission.value.status = "completed"
|
||||||
|
alert("🎉 Misi selesai! Semua task telah disimpan.")
|
||||||
|
}
|
||||||
|
|
||||||
|
async function startMission(mission) {
|
||||||
|
console.log("mission: ", mission)
|
||||||
|
await createMissionLog({
|
||||||
|
mission: mission.id,
|
||||||
|
user_id: currentUser.id,
|
||||||
|
status: 'in_progress',
|
||||||
|
coin: mission.coin,
|
||||||
|
point: mission.point
|
||||||
|
});
|
||||||
|
|
||||||
|
mission.userStatus = 'in_progress';
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
video {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -1,65 +1,76 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col items-center justify-center bg-gradient-to-b from-green-100 to-green-300 pb-28 pr-4 pl-4">
|
<div class="min-h-screen flex flex-col items-center justify-center bg-gradient-to-b from-green-100 to-green-300 p-4
|
||||||
<div class="max-w-md flex flex-col items-center z-60" >
|
mb-[-100px]"
|
||||||
<div class="relative flex items-end justify-center z-30 px-[10px] mt-[10px]">
|
style="background-image:url('/images/footer.png');
|
||||||
<img
|
background-repeat:no-repeat;
|
||||||
src="/images/logo.png"
|
background-position:bottom;
|
||||||
:alt="selectedCharacter.name"
|
background-size: 100% 200px;"
|
||||||
class="object-contain relative z-30 pb-0"
|
margin-bottom=-10px;
|
||||||
/>
|
>
|
||||||
</div>
|
<!-- CARD / CONTAINER -->
|
||||||
<h2 class="mt-6 text-xl font-bold text-gray-700">
|
<div class="w-full max-w-sm sm:max-w-md bg-white rounded-3xl shadow-xl p-6 sm:p-8 flex flex-col items-center">
|
||||||
{{ selectedCharacter.name }}
|
<!-- LOGO -->
|
||||||
</h2>
|
<div class="flex flex-col items-center mb-4">
|
||||||
<div class="relative w-40 h-50 flex items-end justify-center z-30 px-[10px]">
|
<img src="/images/logo.png" alt="Logo" class="w-24 h-auto mb-2" />
|
||||||
<img
|
<h2 class="text-xl font-bold text-gray-700">{{ selectedCharacter.name }}</h2>
|
||||||
:src="selectedProfile.img"
|
|
||||||
:alt="selectedCharacter.name"
|
|
||||||
class="w-40 h-60 object-contain relative z-30 pb-12"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- Character Selection -->
|
|
||||||
<div class="w-full flex flex-col items-center z-60" >
|
|
||||||
<div class="w-full ">
|
|
||||||
<div class="mb-4">
|
|
||||||
<input type="text" placeholder="Real Name"
|
|
||||||
class="w-full h-[50px] px-4 py-2 text-grey-700 bg-white border border-grey-300 rounded-2xl
|
|
||||||
focus:outline-none focus:ring focus:ring-grey-100 focus:border-grey-300 shadow-lg"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="mb-4">
|
|
||||||
<input type="text" placeholder="Email"
|
|
||||||
class="w-full h-[50px] px-4 py-2 text-grey-700 bg-white border border-grey-300 rounded-2xl
|
|
||||||
focus:outline-none focus:ring focus:ring-grey-100 focus:border-grey-300 shadow-lg"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="mb-4">
|
|
||||||
<input type="text" placeholder="Phone"
|
|
||||||
class="w-full h-[50px] px-4 py-2 text-grey-700 bg-white border border-grey-300 rounded-2xl
|
|
||||||
focus:outline-none focus:ring focus:ring-grey-100 focus:border-grey-300 shadow-lg"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div >
|
|
||||||
<input type="text" placeholder="Password"
|
|
||||||
class="w-full h-[50px] px-4 py-2 text-grey-700 bg-white border border-grey-300 rounded-2xl
|
|
||||||
focus:outline-none focus:ring focus:ring-grey-100 focus:border-grey-300 shadow-lg"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<button class="w-full mt-6 h-[60px] bg-lime-500 border border-transparent rounded-lg px-4 py-2 text-sm font-medium text-white hover:bg-lime-400 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-lime-500"> R E G I S T E R </button>
|
|
||||||
<div class="flex items-center justify-center mt-4">
|
|
||||||
<router-link to ="/login">
|
|
||||||
<span class="font-bold text-sm text-blue-600">L O G I N</span>
|
|
||||||
</router-link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<!-- PROFILE AVATAR -->
|
||||||
|
<div class="relative w-32 h-32 flex items-center justify-center mb-6">
|
||||||
|
<img
|
||||||
|
:src="selectedProfile.img"
|
||||||
|
:alt="selectedCharacter.name"
|
||||||
|
class="w-32 h-32 object-contain rounded-full border-4 border-lime-400 shadow-md"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<!-- INPUT FIELDS -->
|
||||||
|
<div class="w-full space-y-4">
|
||||||
|
<input
|
||||||
|
v-model="username"
|
||||||
|
type="text"
|
||||||
|
placeholder="Phone"
|
||||||
|
class="w-full h-12 px-4 text-gray-700 bg-gray-50 border border-gray-300 rounded-2xl
|
||||||
|
focus:outline-none focus:ring-2 focus:ring-lime-400 focus:border-lime-400 shadow-sm"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
v-model="password"
|
||||||
|
type="password"
|
||||||
|
placeholder="Password"
|
||||||
|
class="w-full h-12 px-4 text-gray-700 bg-gray-50 border border-gray-300 rounded-2xl
|
||||||
|
focus:outline-none focus:ring-2 focus:ring-lime-400 focus:border-lime-400 shadow-sm"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
v-model="re_password"
|
||||||
|
type="password"
|
||||||
|
placeholder="Re-password"
|
||||||
|
class="w-full h-12 px-4 text-gray-700 bg-gray-50 border border-gray-300 rounded-2xl
|
||||||
|
focus:outline-none focus:ring-2 focus:ring-lime-400 focus:border-lime-400 shadow-sm"
|
||||||
|
/>
|
||||||
|
<!-- REGISTER BUTTON -->
|
||||||
|
<button
|
||||||
|
@click="register"
|
||||||
|
class="w-full h-12 bg-lime-500 text-white font-semibold rounded-2xl
|
||||||
|
hover:bg-lime-400 transition-all focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-lime-500"
|
||||||
|
>
|
||||||
|
REGISTER
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- LOGIN LINK -->
|
||||||
|
<div class="flex items-center justify-center mt-2">
|
||||||
|
<router-link to="/login">
|
||||||
|
<span class="font-bold text-sm text-blue-600 hover:underline">L O G I N</span>
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ERROR MESSAGE -->
|
||||||
|
<p v-if="error" class="text-red-500 text-sm mt-2 text-center">{{ error }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
name: "App",
|
name: "RegisterPage",
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
characters: [
|
characters: [
|
||||||
@ -73,16 +84,14 @@ export default {
|
|||||||
{ name: "Sheep", img: "../images/char/sheep.png" },
|
{ name: "Sheep", img: "../images/char/sheep.png" },
|
||||||
{ name: "Tiger", img: "../images/char/tiger.png" },
|
{ name: "Tiger", img: "../images/char/tiger.png" },
|
||||||
],
|
],
|
||||||
|
profiles: [
|
||||||
profiles : [
|
{ name: "profile", img: "/images/propic-001.svg" },
|
||||||
{name : "profile", img: "/images/propic-001.svg"},
|
{ name: "profile", img: "/images/propic-blank.svg" },
|
||||||
{name : "profile", img: "/images/propic-blank.svg"},
|
|
||||||
],
|
],
|
||||||
|
|
||||||
selectedCharacter: { name: "Tiger", img: "../images/char/tiger.png" },
|
selectedCharacter: { name: "Tiger", img: "../images/char/tiger.png" },
|
||||||
showModal:false,
|
selectedProfile: { name: "profile", img: "/images/propic-001.svg" },
|
||||||
selectedProfile : {name : "profile", img: "/images/propic-001.svg"},
|
showModal: false,
|
||||||
modalType : "profile",
|
modalType: "profile",
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@ -94,10 +103,39 @@ export default {
|
|||||||
this.modalType = type;
|
this.modalType = type;
|
||||||
this.showModal = true;
|
this.showModal = true;
|
||||||
},
|
},
|
||||||
profileListSelected(profile) {
|
profileListSelected(profile) {
|
||||||
this.selectedProfile = profile;
|
this.selectedProfile = profile;
|
||||||
this.showModal = false;
|
this.showModal = false;
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { useAuthStore } from '@/stores/auth'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const auth = useAuthStore()
|
||||||
|
const username = ref('')
|
||||||
|
const password = ref('')
|
||||||
|
const re_password = ref('')
|
||||||
|
const error = ref('')
|
||||||
|
|
||||||
|
async function register() {
|
||||||
|
try {
|
||||||
|
await auth.register(username.value, password.value, re_password.value)
|
||||||
|
router.push('/login')
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
error.value = err.response?.data?.detail || err.message || 'Registration failed'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
body {
|
||||||
|
font-family: 'Inter', sans-serif;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
111
src/pages/Synopsis.vue
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
<template>
|
||||||
|
<div class="w-full px-6 py-4 flex items-center justify-between backdrop-blur-md bg-white/30 border-b border-emerald-100 z-20">
|
||||||
|
<button
|
||||||
|
@click="goBack"
|
||||||
|
class="flex items-center gap-2 text-emerald-600 hover:text-emerald-800 transition-colors"
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
|
||||||
|
</svg>
|
||||||
|
<span class="font-semibold">Kembali</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Breadcrumb / Path -->
|
||||||
|
<div class="text-sm text-gray-600">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<section
|
||||||
|
class="relative w-full max-w-3xl mx-auto mt-10 p-6 rounded-3xl bg-white/60
|
||||||
|
backdrop-blur-lg border border-white/40 shadow-lg overflow-hidden"
|
||||||
|
>
|
||||||
|
<!-- Decorative gradient background glow -->
|
||||||
|
<div
|
||||||
|
class="absolute inset-0 bg-gradient-to-br from-emerald-100 via-lime-50 to-transparent
|
||||||
|
opacity-70 blur-2xl -z-10"
|
||||||
|
></div>
|
||||||
|
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="flex items-center justify-between mb-4">
|
||||||
|
<h2
|
||||||
|
class="text-2xl sm:text-3xl font-extrabold bg-gradient-to-r
|
||||||
|
from-emerald-500 to-lime-500 bg-clip-text text-transparent"
|
||||||
|
>
|
||||||
|
📖 Sinopsis
|
||||||
|
</h2>
|
||||||
|
<span
|
||||||
|
class="text-xs uppercase tracking-wide bg-emerald-100 text-emerald-700
|
||||||
|
font-semibold px-3 py-1 rounded-full"
|
||||||
|
>
|
||||||
|
Manga Detail
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Synopsis Text / Empty State -->
|
||||||
|
<transition name="fade" mode="out-in">
|
||||||
|
<template v-if="synopsis">
|
||||||
|
<p
|
||||||
|
key="has-synopsis"
|
||||||
|
class="text-gray-700 leading-relaxed text-justify indent-6"
|
||||||
|
>
|
||||||
|
{{ synopsis }}
|
||||||
|
</p>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<div
|
||||||
|
key="no-synopsis"
|
||||||
|
class="flex flex-col items-center justify-center py-8 text-gray-400 italic"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
class="w-10 h-10 mb-2 opacity-60"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
>
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
|
d="M12 8c-1.657 0-3 1.343-3 3 0 .85.355 1.622.93 2.172C8.355 13.622 8 14.394 8 15.244 8 17.82 10.24 20 12 20s4-2.18 4-4.756c0-.85-.355-1.622-.93-2.172.575-.55.93-1.322.93-2.172 0-1.657-1.343-3-3-3z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<p>Sinopsis belum tersedia.</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</transition>
|
||||||
|
|
||||||
|
<!-- Gradient Border Glow -->
|
||||||
|
<div
|
||||||
|
class="absolute bottom-0 left-0 w-full h-1 bg-gradient-to-r
|
||||||
|
from-lime-400 via-emerald-300 to-cyan-300 animate-[pulse_3s_infinite]"
|
||||||
|
></div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: "MangaSynopsis",
|
||||||
|
props: {
|
||||||
|
synopsis: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
goBack() {
|
||||||
|
this.$router.go(-1);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.fade-enter-active, .fade-leave-active {
|
||||||
|
transition: opacity 0.8s ease;
|
||||||
|
}
|
||||||
|
.fade-enter-from, .fade-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
@keyframes pulse {
|
||||||
|
0%, 100% { opacity: 0.5; }
|
||||||
|
50% { opacity: 1; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -4,13 +4,32 @@ import ContentPage from '../pages/Home.vue'
|
|||||||
import CharacterPage from '../pages/Character.vue'
|
import CharacterPage from '../pages/Character.vue'
|
||||||
import RegistePage from '../pages/Register.vue'
|
import RegistePage from '../pages/Register.vue'
|
||||||
import loginPage from '../pages/Login.vue'
|
import loginPage from '../pages/Login.vue'
|
||||||
|
import Entertainemnt from '../pages/Entertainment.vue'
|
||||||
|
import { useAuthStore } from '@/stores/auth';
|
||||||
|
import MangasPage from '../pages/Mangas.vue'
|
||||||
|
import ChaptersPage from '@/pages/Chapters.vue'
|
||||||
|
import ChapterListPage from '@/pages/ChapterList.vue'
|
||||||
|
import SynopsisPage from '@/pages/Synopsis.vue'
|
||||||
|
import MissionPage from '@/pages/MissionPage.vue'
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
{ path: '/', name: 'home', component: HomePage },
|
{ path: '/', name: 'home', component: HomePage , meta:{requiresAuth:true}},
|
||||||
{ path: '/content', name: 'content', component: ContentPage },
|
{ path: '/content', name: 'content', component: ContentPage, meta:{requiresAuth:true}},
|
||||||
{ path: '/character', name: 'character', component: CharacterPage },
|
{ path: '/character', name: 'character', component: CharacterPage, meta:{requiresAuth:true}},
|
||||||
{path: '/register', name: 'register', component: RegistePage},
|
{ path: '/register', name: 'register', component: RegistePage,},
|
||||||
{path: '/login', name:'login', component:loginPage}
|
{ path: '/login', name:'login', component:loginPage},
|
||||||
|
{ path: '/entertainment', name:'entertainment', component:Entertainemnt},
|
||||||
|
{ path: '/entertainment/mangas', name:'mangas', component:MangasPage},
|
||||||
|
{ path: '/entertainment/manga/:id/chapters', name:'manga', component:ChaptersPage, props:true},
|
||||||
|
{ path: '/entertainment/manga/:id/chapters/synopsis', name:'manga-sinopsis', component:SynopsisPage,
|
||||||
|
props: route => ({
|
||||||
|
id: route.params.id,
|
||||||
|
synopsis: route.query.synopsis || ''
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{ path: '/entertainment/manga/:manga_id/chapters/:chapter_id/', name:'manga-list', component:ChapterListPage, props:true},
|
||||||
|
|
||||||
|
{ path:'/mission/quest/:id/missions', name: 'quest-missions', component:MissionPage}
|
||||||
]
|
]
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
@ -18,4 +37,22 @@ const router = createRouter({
|
|||||||
routes,
|
routes,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
router.beforeEach(async(to, from, next) => {
|
||||||
|
const auth = useAuthStore();
|
||||||
|
if (!auth.isLoggedIn) {
|
||||||
|
auth.restoreSession();
|
||||||
|
}
|
||||||
|
|
||||||
|
const isLoggedIn = auth.isLoggedIn;
|
||||||
|
console.log(isLoggedIn)
|
||||||
|
if(to.meta.requiresAuth && !isLoggedIn) {
|
||||||
|
return next('/login');
|
||||||
|
}
|
||||||
|
if (to.path === '/login' && isLoggedIn) {
|
||||||
|
return next('/')
|
||||||
|
}
|
||||||
|
|
||||||
|
return next();
|
||||||
|
});
|
||||||
|
|
||||||
export default router
|
export default router
|
||||||
|
|||||||
13
src/services/content.js
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import api from "@/util/api";
|
||||||
|
|
||||||
|
export const getContent = () =>{
|
||||||
|
return api.get('/content/contents')
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getContentById =() =>{
|
||||||
|
return api.get('content/contents/$id')
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createContent = (data) => {
|
||||||
|
return api.post('/content/contents',data)
|
||||||
|
}
|
||||||
5
src/services/genre.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import api from "@/util/api";
|
||||||
|
|
||||||
|
export const getGenre = () => {
|
||||||
|
return api.get("/entertainment/genres").then((res) => res.data);
|
||||||
|
};
|
||||||
12
src/services/manga.js
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import api from "@/util/api";
|
||||||
|
export const getManga = () => {
|
||||||
|
return api.get("/entertainment/manga");
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getChapters = async(id) => {
|
||||||
|
return await api.get(`/entertainment/manga/${id}/chapters`).then((res) => res.data);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getMangaByChapter = async(manga_id, chapter_id) => {
|
||||||
|
return await api.get(`/entertainment/manga/${manga_id}/chapters/${chapter_id}/`).then((res) => res.data);
|
||||||
|
};
|
||||||
30
src/services/missions.js
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import api from "@/util/api";
|
||||||
|
|
||||||
|
export const getMissions = async () => {
|
||||||
|
return await api.get("/mission/missions").then((res) => res.data);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getMissionLogs = async () => {
|
||||||
|
return await api.get("/mission/mission-logs/").then((res) => res.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createMissionLog = async (data = {}) => {
|
||||||
|
const timestp = new Date().toISOString();
|
||||||
|
console.log(data);
|
||||||
|
return await api.post('mission/mission-logs',{
|
||||||
|
mission: data.mission,
|
||||||
|
user_id: data.user_id,
|
||||||
|
status: 'completed',
|
||||||
|
coin: data.coin,
|
||||||
|
point: data.point,
|
||||||
|
image_log: data.image_log,
|
||||||
|
completed_at: timestp,
|
||||||
|
claimed_at: timestp,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export const updateMissionLog = async(missionId ,data = {}) =>{
|
||||||
|
return await api.patch(`missions/${missionId}/`, data, {
|
||||||
|
headers: { "Content-Type": "multipart/form-data" },
|
||||||
|
});
|
||||||
|
}
|
||||||
87
src/stores/auth.js
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
import {defineStore} from "pinia";
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
export const useAuthStore = defineStore('auth', {
|
||||||
|
state: () => ({
|
||||||
|
baseUrl : process.env.VUE_APP_BASE_URL,
|
||||||
|
clientId : process.env.VUE_APP_CLIENT_ID,
|
||||||
|
clientSecret : process.env.VUE_APP_CLIENT_SECRET,
|
||||||
|
token: null,
|
||||||
|
refresh_token: null,
|
||||||
|
user: null
|
||||||
|
}),
|
||||||
|
getters: {
|
||||||
|
isLoggedIn: (state) => !!state.token && !!state.user,
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
async register(username, password, re_password) {
|
||||||
|
const response =await axios.post(this.baseUrl + '/auth/users/', {
|
||||||
|
username: username,
|
||||||
|
password: password,
|
||||||
|
re_password: re_password
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("✅ Registration success:", response.data);
|
||||||
|
},
|
||||||
|
async login(username, password) {
|
||||||
|
// preparing data
|
||||||
|
const data = new URLSearchParams()
|
||||||
|
data.append('grant_type', 'password')
|
||||||
|
data.append('client_id', this.clientId)
|
||||||
|
data.append('client_secret', this.clientSecret)
|
||||||
|
data.append('username', username)
|
||||||
|
data.append('password', password)
|
||||||
|
|
||||||
|
const response = await axios.post(`${this.baseUrl}`+'/oauth/token/',data.toString(), {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const { access_token, refresh_token } = response.data;
|
||||||
|
const user = await axios.get(this.baseUrl + '/auth/users/me', {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${access_token}`
|
||||||
|
}
|
||||||
|
}).then(res => res.data)
|
||||||
|
|
||||||
|
this.refresh_token = refresh_token
|
||||||
|
this.token = access_token
|
||||||
|
this.user = user
|
||||||
|
|
||||||
|
console.log(JSON.stringify(user));
|
||||||
|
localStorage.setItem('token', this.token)
|
||||||
|
localStorage.setItem('user', JSON.stringify(user))
|
||||||
|
console.log("✅ Logged in as:", this.user.username);
|
||||||
|
},
|
||||||
|
async logout() {
|
||||||
|
// revoke token
|
||||||
|
if(this.token){
|
||||||
|
await axios.post(this.baseUrl + '/oauth/revoke_token/', {
|
||||||
|
token: this.token,
|
||||||
|
client_id: this.clientId,
|
||||||
|
client_secret: this.clientSecret
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||||
|
});
|
||||||
|
}else{
|
||||||
|
console.log("No token found");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.token = null;
|
||||||
|
this.user = null;
|
||||||
|
localStorage.removeItem('token')
|
||||||
|
localStorage.removeItem('user')
|
||||||
|
},
|
||||||
|
restoreSession() {
|
||||||
|
const savedToken = localStorage.getItem('token')
|
||||||
|
const savedUser = localStorage.getItem('user')
|
||||||
|
|
||||||
|
if (savedToken && savedUser) {
|
||||||
|
this.token = savedToken
|
||||||
|
this.user = JSON.parse(savedUser)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
50
src/util/api.js
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import axios from "axios"
|
||||||
|
const api = axios.create({
|
||||||
|
baseURL: process.env.VUE_APP_BASE_URL,
|
||||||
|
clientId : process.env.VUE_APP_CLIENT_ID,
|
||||||
|
clientSecret : process.env.VUE_APP_CLIENT_SECRET,
|
||||||
|
headers:{
|
||||||
|
"Content-Type":"application/json"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
api.interceptors.request.use(
|
||||||
|
config =>{
|
||||||
|
const token = localStorage.getItem('token')
|
||||||
|
console.log(token)
|
||||||
|
if(token){
|
||||||
|
config.headers.Authorization = `Bearer ${token}`
|
||||||
|
}
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
api.interceptors.response.use(
|
||||||
|
response => response,
|
||||||
|
async error => {
|
||||||
|
if(error.response && error.response.status === 401){
|
||||||
|
const refresh_token = localStorage.getItem('refresh_token')
|
||||||
|
if(refresh_token){
|
||||||
|
try {
|
||||||
|
const res = await axios.post(`${this.baseURL}` + "/oauth/token/refresh/", {
|
||||||
|
refresh: refresh_token,
|
||||||
|
});
|
||||||
|
|
||||||
|
localStorage.setItem("access_token", res.data.access);
|
||||||
|
error.config.headers.Authorization = `Bearer ${res.data.access}`;
|
||||||
|
return api.request(error.config);
|
||||||
|
} catch (refreshError) {
|
||||||
|
// jika refresh juga gagal, logout
|
||||||
|
localStorage.removeItem("access_token");
|
||||||
|
localStorage.removeItem("refresh_token");
|
||||||
|
window.location = "/login";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
localStorage.removeItem('token')
|
||||||
|
window.location.href = '/'
|
||||||
|
}
|
||||||
|
return Promise.reject(error)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export default api
|
||||||