diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index e757a909259f83ab7a49cbed5dac6b15ab115061..8dd0ebc17218f6c8273da344f9dec0ce864912a8 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -3,6 +3,7 @@ image: node:16 stages: # - setup - test + - deploy #variables: # npm_config_cache: "$CI_PROJECT_DIR/.npm" @@ -37,6 +38,21 @@ test: - npm run lint - npm run test:unit +deploy-backend: + image: ubuntu:latest + stage: deploy + script: + - chmod og= $ID_RSA + - apt update + - apt install --assume-yes rsync + - apt install --assume-yes openssh-client + - ssh -i $ID_RSA -o StrictHostKeyChecking=no $SERVERUSER@$SERVERIP "sudo /bin/systemctl restart cleanfront.service" + - rsync --archive --rsync-path=/usr/bin/rsync --delete --exclude='.git' --exclude='node_modules' -e "ssh -i $ID_RSA -o StrictHostKeyChecking=no -l $SERVERUSER -p 22" . $SERVERIP:./app/frontend + - ssh -i $ID_RSA -o StrictHostKeyChecking=no $SERVERUSER@$SERVERIP "cd app/frontend;npm install" + - ssh -i $ID_RSA -o StrictHostKeyChecking=no $SERVERUSER@$SERVERIP "sudo /bin/systemctl restart frontend.service" + only: + - main + #unit_test: # stage: test # script: diff --git a/package-lock.json b/package-lock.json index 894267d8427354bf139f24226b19df82101d6cf5..92415a10cf3e721007d669673ea0ebbbf5827a42 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,7 @@ "roboto-fontface": "*", "sockjs-client": "^1.6.0", "stompjs": "^2.3.3", + "tw-elements": "^1.0.0-alpha12", "vue": "^3.2.13", "vue-router": "^4.0.3", "vuelidate": "^0.7.7", @@ -2382,7 +2383,6 @@ }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", - "dev": true, "license": "MIT", "dependencies": { "@nodelib/fs.stat": "2.0.5", @@ -2394,7 +2394,6 @@ }, "node_modules/@nodelib/fs.stat": { "version": "2.0.5", - "dev": true, "license": "MIT", "engines": { "node": ">= 8" @@ -2402,7 +2401,6 @@ }, "node_modules/@nodelib/fs.walk": { "version": "1.2.8", - "dev": true, "license": "MIT", "dependencies": { "@nodelib/fs.scandir": "2.1.5", @@ -2417,6 +2415,15 @@ "dev": true, "license": "MIT" }, + "node_modules/@popperjs/core": { + "version": "2.11.5", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.5.tgz", + "integrity": "sha512-9X2obfABZuDVLCgPK9aX0a/x4jaOEweTTWE2+9sr0Qqqevj2Uv5XorvusThmc9XGYpS9yI+fhh8RTafBtGposw==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, "node_modules/@sideway/address": { "version": "4.1.4", "dev": true, @@ -3874,7 +3881,6 @@ }, "node_modules/acorn-node": { "version": "1.8.2", - "dev": true, "license": "Apache-2.0", "dependencies": { "acorn": "^7.0.0", @@ -3884,7 +3890,6 @@ }, "node_modules/acorn-node/node_modules/acorn": { "version": "7.4.1", - "dev": true, "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -3895,7 +3900,6 @@ }, "node_modules/acorn-node/node_modules/acorn-walk": { "version": "7.2.0", - "dev": true, "license": "MIT", "engines": { "node": ">=0.4.0" @@ -4046,7 +4050,6 @@ }, "node_modules/anymatch": { "version": "3.1.2", - "dev": true, "license": "ISC", "dependencies": { "normalize-path": "^3.0.0", @@ -4077,7 +4080,6 @@ }, "node_modules/arg": { "version": "5.0.1", - "dev": true, "license": "MIT" }, "node_modules/argparse": { @@ -4450,7 +4452,6 @@ }, "node_modules/binary-extensions": { "version": "2.2.0", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -4539,7 +4540,6 @@ }, "node_modules/braces": { "version": "3.0.2", - "dev": true, "license": "MIT", "dependencies": { "fill-range": "^7.0.1" @@ -4680,7 +4680,6 @@ }, "node_modules/camelcase-css": { "version": "2.0.1", - "dev": true, "license": "MIT", "engines": { "node": ">= 6" @@ -4749,9 +4748,42 @@ "node": ">=6" } }, + "node_modules/chart.js": { + "version": "2.9.4", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-2.9.4.tgz", + "integrity": "sha512-B07aAzxcrikjAPyV+01j7BmOpxtQETxTSlQ26BEYJ+3iUkbNKaOJ/nDbT6JjyqYxseM0ON12COHYdU2cTIjC7A==", + "dependencies": { + "chartjs-color": "^2.1.0", + "moment": "^2.10.2" + } + }, + "node_modules/chartjs-color": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/chartjs-color/-/chartjs-color-2.4.1.tgz", + "integrity": "sha512-haqOg1+Yebys/Ts/9bLo/BqUcONQOdr/hoEr2LLTRl6C5LXctUdHxsCYfvQVg5JIxITrfCNUDr4ntqmQk9+/0w==", + "dependencies": { + "chartjs-color-string": "^0.6.0", + "color-convert": "^1.9.3" + } + }, + "node_modules/chartjs-color-string": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/chartjs-color-string/-/chartjs-color-string-0.6.0.tgz", + "integrity": "sha512-TIB5OKn1hPJvO7JcteW4WY/63v6KwEdt6udfnDE9iCAZgy+V4SrbSxoIbTw/xkUIapjEI4ExGtD0+6D3KyFd7A==", + "dependencies": { + "color-name": "^1.0.0" + } + }, + "node_modules/chartjs-plugin-datalabels": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chartjs-plugin-datalabels/-/chartjs-plugin-datalabels-0.7.0.tgz", + "integrity": "sha512-PKVUX14nYhH0wcdCpgOoC39Gbzvn6cZ7O9n+bwc02yKD9FTnJ7/TSrBcfebmolFZp1Rcicr9xbT0a5HUbigS7g==", + "peerDependencies": { + "chart.js": ">= 2.7.0 < 3" + } + }, "node_modules/chokidar": { "version": "3.5.3", - "dev": true, "funding": [ { "type": "individual", @@ -4777,7 +4809,6 @@ }, "node_modules/chokidar/node_modules/glob-parent": { "version": "5.1.2", - "dev": true, "license": "ISC", "dependencies": { "is-glob": "^4.0.1" @@ -4997,7 +5028,6 @@ }, "node_modules/color-convert": { "version": "1.9.3", - "dev": true, "license": "MIT", "dependencies": { "color-name": "1.1.3" @@ -5005,7 +5035,6 @@ }, "node_modules/color-name": { "version": "1.1.3", - "dev": true, "license": "MIT" }, "node_modules/colord": { @@ -5512,7 +5541,6 @@ }, "node_modules/cssesc": { "version": "3.0.0", - "dev": true, "license": "MIT", "bin": { "cssesc": "bin/cssesc" @@ -5629,6 +5657,11 @@ "version": "2.6.20", "license": "MIT" }, + "node_modules/custom-event-polyfill": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/custom-event-polyfill/-/custom-event-polyfill-1.0.7.tgz", + "integrity": "sha512-TDDkd5DkaZxZFM8p+1I3yAlvM3rSr1wbrOliG4yJiwinMZN8z/iGL7BTlDkrJcYTmgUSb4ywVCc3ZaUtOtC76w==" + }, "node_modules/d": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/d/-/d-1.0.1.tgz", @@ -5851,7 +5884,6 @@ }, "node_modules/defined": { "version": "1.0.0", - "dev": true, "license": "MIT" }, "node_modules/delayed-stream": { @@ -5875,6 +5907,14 @@ "dev": true, "license": "MIT" }, + "node_modules/detect-autofill": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/detect-autofill/-/detect-autofill-1.1.4.tgz", + "integrity": "sha512-utCBQwCR/beSnADQmBC7C4tTueBBkYCl6WSpfGUkYKO/+MzPxqYGj6G4MvHzcKmH1gCTK+VunX2vaagvkRXPvA==", + "dependencies": { + "custom-event-polyfill": "^1.0.7" + } + }, "node_modules/detect-newline": { "version": "3.1.0", "dev": true, @@ -5890,7 +5930,6 @@ }, "node_modules/detective": { "version": "5.2.0", - "dev": true, "license": "MIT", "dependencies": { "acorn-node": "^1.6.1", @@ -5906,7 +5945,6 @@ }, "node_modules/didyoumean": { "version": "1.2.2", - "dev": true, "license": "Apache-2.0" }, "node_modules/diff-sequences": { @@ -5930,7 +5968,6 @@ }, "node_modules/dlv": { "version": "1.1.3", - "dev": true, "license": "MIT" }, "node_modules/dns-equal": { @@ -7061,7 +7098,6 @@ }, "node_modules/fast-glob": { "version": "3.2.11", - "dev": true, "license": "MIT", "dependencies": { "@nodelib/fs.stat": "^2.0.2", @@ -7076,7 +7112,6 @@ }, "node_modules/fast-glob/node_modules/glob-parent": { "version": "5.1.2", - "dev": true, "license": "ISC", "dependencies": { "is-glob": "^4.0.1" @@ -7097,7 +7132,6 @@ }, "node_modules/fastq": { "version": "1.13.0", - "dev": true, "license": "ISC", "dependencies": { "reusify": "^1.0.4" @@ -7145,7 +7179,6 @@ }, "node_modules/fill-range": { "version": "7.0.1", - "dev": true, "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" @@ -7314,7 +7347,6 @@ }, "node_modules/function-bind": { "version": "1.1.1", - "dev": true, "license": "MIT" }, "node_modules/functional-red-black-tree": { @@ -7391,7 +7423,6 @@ }, "node_modules/glob-parent": { "version": "6.0.2", - "dev": true, "license": "ISC", "dependencies": { "is-glob": "^4.0.3" @@ -7458,7 +7489,6 @@ }, "node_modules/has": { "version": "1.0.3", - "dev": true, "license": "MIT", "dependencies": { "function-bind": "^1.1.1" @@ -7872,7 +7902,6 @@ }, "node_modules/is-binary-path": { "version": "2.1.0", - "dev": true, "license": "MIT", "dependencies": { "binary-extensions": "^2.0.0" @@ -7904,7 +7933,6 @@ }, "node_modules/is-core-module": { "version": "2.9.0", - "dev": true, "license": "MIT", "dependencies": { "has": "^1.0.3" @@ -7937,7 +7965,6 @@ }, "node_modules/is-extglob": { "version": "2.1.1", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -7969,7 +7996,6 @@ }, "node_modules/is-glob": { "version": "4.0.3", - "dev": true, "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" @@ -7988,7 +8014,6 @@ }, "node_modules/is-number": { "version": "7.0.0", - "dev": true, "license": "MIT", "engines": { "node": ">=0.12.0" @@ -10336,7 +10361,6 @@ }, "node_modules/lilconfig": { "version": "2.0.5", - "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -10728,7 +10752,6 @@ }, "node_modules/merge2": { "version": "1.4.1", - "dev": true, "license": "MIT", "engines": { "node": ">= 8" @@ -10744,7 +10767,6 @@ }, "node_modules/micromatch": { "version": "4.0.5", - "dev": true, "license": "MIT", "dependencies": { "braces": "^3.0.2", @@ -10877,7 +10899,6 @@ }, "node_modules/minimist": { "version": "1.2.6", - "dev": true, "license": "MIT" }, "node_modules/minipass": { @@ -10907,6 +10928,14 @@ "dev": true, "license": "MIT" }, + "node_modules/moment": { + "version": "2.29.3", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.3.tgz", + "integrity": "sha512-c6YRvhEo//6T2Jz/vVtYzqBzwvPT95JBQ+smCytzf7c50oMZRsR/a4w88aD34I+/QVSfnoAnSBFPJHItlOMJVw==", + "engines": { + "node": "*" + } + }, "node_modules/mrmime": { "version": "1.0.0", "dev": true, @@ -11096,7 +11125,6 @@ }, "node_modules/normalize-path": { "version": "3.0.0", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -11158,7 +11186,6 @@ }, "node_modules/object-hash": { "version": "3.0.0", - "dev": true, "license": "MIT", "engines": { "node": ">= 6" @@ -11520,7 +11547,6 @@ }, "node_modules/path-parse": { "version": "1.0.7", - "dev": true, "license": "MIT" }, "node_modules/path-to-regexp": { @@ -11536,13 +11562,17 @@ "node": ">=8" } }, + "node_modules/perfect-scrollbar": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/perfect-scrollbar/-/perfect-scrollbar-1.5.5.tgz", + "integrity": "sha512-dzalfutyP3e/FOpdlhVryN4AJ5XDVauVWxybSkLZmakFE2sS3y3pc4JnSprw8tGmHvkaG5Edr5T7LBTZ+WWU2g==" + }, "node_modules/picocolors": { "version": "1.0.0", "license": "ISC" }, "node_modules/picomatch": { "version": "2.3.1", - "dev": true, "license": "MIT", "engines": { "node": ">=8.6" @@ -11570,6 +11600,16 @@ "node": ">=8" } }, + "node_modules/popper.js": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1.tgz", + "integrity": "sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ==", + "deprecated": "You can find the new Popper v2 at @popperjs/core, this package is dedicated to the legacy v1", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, "node_modules/portfinder": { "version": "1.0.28", "dev": true, @@ -11702,7 +11742,6 @@ }, "node_modules/postcss-js": { "version": "4.0.0", - "dev": true, "license": "MIT", "dependencies": { "camelcase-css": "^2.0.1" @@ -11720,7 +11759,6 @@ }, "node_modules/postcss-load-config": { "version": "3.1.4", - "dev": true, "license": "MIT", "dependencies": { "lilconfig": "^2.0.5", @@ -11930,7 +11968,6 @@ }, "node_modules/postcss-nested": { "version": "5.0.6", - "dev": true, "license": "MIT", "dependencies": { "postcss-selector-parser": "^6.0.6" @@ -12117,7 +12154,6 @@ }, "node_modules/postcss-selector-parser": { "version": "6.0.10", - "dev": true, "license": "MIT", "dependencies": { "cssesc": "^3.0.0", @@ -12158,7 +12194,6 @@ }, "node_modules/postcss-value-parser": { "version": "4.2.0", - "dev": true, "license": "MIT" }, "node_modules/prelude-ls": { @@ -12350,7 +12385,6 @@ }, "node_modules/queue-microtask": { "version": "1.2.3", - "dev": true, "funding": [ { "type": "github", @@ -12369,7 +12403,6 @@ }, "node_modules/quick-lru": { "version": "5.1.1", - "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -12482,7 +12515,6 @@ }, "node_modules/readdirp": { "version": "3.6.0", - "dev": true, "license": "MIT", "dependencies": { "picomatch": "^2.2.1" @@ -12612,7 +12644,6 @@ }, "node_modules/resolve": { "version": "1.22.0", - "dev": true, "license": "MIT", "dependencies": { "is-core-module": "^2.8.1", @@ -12680,7 +12711,6 @@ }, "node_modules/reusify": { "version": "1.0.4", - "dev": true, "license": "MIT", "engines": { "iojs": ">=1.0.0", @@ -12707,7 +12737,6 @@ }, "node_modules/run-parallel": { "version": "1.2.0", - "dev": true, "funding": [ { "type": "github", @@ -13410,7 +13439,6 @@ }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -13493,7 +13521,6 @@ }, "node_modules/tailwindcss": { "version": "3.0.24", - "dev": true, "license": "MIT", "dependencies": { "arg": "^5.0.1", @@ -13531,7 +13558,6 @@ }, "node_modules/tailwindcss/node_modules/color-name": { "version": "1.1.4", - "dev": true, "license": "MIT" }, "node_modules/tapable": { @@ -13759,7 +13785,6 @@ }, "node_modules/to-regex-range": { "version": "5.0.1", - "dev": true, "license": "MIT", "dependencies": { "is-number": "^7.0.0" @@ -13848,6 +13873,21 @@ "dev": true, "license": "0BSD" }, + "node_modules/tw-elements": { + "version": "1.0.0-alpha12", + "resolved": "https://registry.npmjs.org/tw-elements/-/tw-elements-1.0.0-alpha12.tgz", + "integrity": "sha512-AQHwXIC4kw4T0NPKAi0vD1bkHSiA4RhdXqqnXDWEHKsrN02njc/ITB3YZ92s2f2jt+wBPI2UOpeUsWL5xiMVeQ==", + "dependencies": { + "@popperjs/core": "^2.6.0", + "chart.js": "^2.9.4", + "chartjs-plugin-datalabels": "^0.7.0", + "deepmerge": "^4.2.2", + "detect-autofill": "^1.1.3", + "perfect-scrollbar": "^1.5.0", + "popper.js": "^1.16.1", + "tailwindcss": "^3.0.7" + } + }, "node_modules/type": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/type/-/type-1.2.0.tgz", @@ -13993,7 +14033,6 @@ }, "node_modules/util-deprecate": { "version": "1.0.2", - "dev": true, "license": "MIT" }, "node_modules/utila": { @@ -14956,7 +14995,6 @@ }, "node_modules/xtend": { "version": "4.0.2", - "dev": true, "license": "MIT", "engines": { "node": ">=0.4" @@ -14986,7 +15024,6 @@ }, "node_modules/yaml": { "version": "1.10.2", - "dev": true, "license": "ISC", "engines": { "node": ">= 6" @@ -16544,19 +16581,16 @@ }, "@nodelib/fs.scandir": { "version": "2.1.5", - "dev": true, "requires": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "@nodelib/fs.stat": { - "version": "2.0.5", - "dev": true + "version": "2.0.5" }, "@nodelib/fs.walk": { "version": "1.2.8", - "dev": true, "requires": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" @@ -16566,6 +16600,11 @@ "version": "1.0.0-next.21", "dev": true }, + "@popperjs/core": { + "version": "2.11.5", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.5.tgz", + "integrity": "sha512-9X2obfABZuDVLCgPK9aX0a/x4jaOEweTTWE2+9sr0Qqqevj2Uv5XorvusThmc9XGYpS9yI+fhh8RTafBtGposw==" + }, "@sideway/address": { "version": "4.1.4", "dev": true, @@ -17616,7 +17655,6 @@ }, "acorn-node": { "version": "1.8.2", - "dev": true, "requires": { "acorn": "^7.0.0", "acorn-walk": "^7.0.0", @@ -17624,12 +17662,10 @@ }, "dependencies": { "acorn": { - "version": "7.4.1", - "dev": true + "version": "7.4.1" }, "acorn-walk": { - "version": "7.2.0", - "dev": true + "version": "7.2.0" } } }, @@ -17718,7 +17754,6 @@ }, "anymatch": { "version": "3.1.2", - "dev": true, "requires": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" @@ -17729,8 +17764,7 @@ "dev": true }, "arg": { - "version": "5.0.1", - "dev": true + "version": "5.0.1" }, "argparse": { "version": "1.0.10", @@ -17962,8 +17996,7 @@ "dev": true }, "binary-extensions": { - "version": "2.2.0", - "dev": true + "version": "2.2.0" }, "bl": { "version": "4.1.0", @@ -18035,7 +18068,6 @@ }, "braces": { "version": "3.0.2", - "dev": true, "requires": { "fill-range": "^7.0.1" } @@ -18112,8 +18144,7 @@ "dev": true }, "camelcase-css": { - "version": "2.0.1", - "dev": true + "version": "2.0.1" }, "caniuse-api": { "version": "3.0.0", @@ -18150,9 +18181,40 @@ "version": "0.2.0", "dev": true }, + "chart.js": { + "version": "2.9.4", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-2.9.4.tgz", + "integrity": "sha512-B07aAzxcrikjAPyV+01j7BmOpxtQETxTSlQ26BEYJ+3iUkbNKaOJ/nDbT6JjyqYxseM0ON12COHYdU2cTIjC7A==", + "requires": { + "chartjs-color": "^2.1.0", + "moment": "^2.10.2" + } + }, + "chartjs-color": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/chartjs-color/-/chartjs-color-2.4.1.tgz", + "integrity": "sha512-haqOg1+Yebys/Ts/9bLo/BqUcONQOdr/hoEr2LLTRl6C5LXctUdHxsCYfvQVg5JIxITrfCNUDr4ntqmQk9+/0w==", + "requires": { + "chartjs-color-string": "^0.6.0", + "color-convert": "^1.9.3" + } + }, + "chartjs-color-string": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/chartjs-color-string/-/chartjs-color-string-0.6.0.tgz", + "integrity": "sha512-TIB5OKn1hPJvO7JcteW4WY/63v6KwEdt6udfnDE9iCAZgy+V4SrbSxoIbTw/xkUIapjEI4ExGtD0+6D3KyFd7A==", + "requires": { + "color-name": "^1.0.0" + } + }, + "chartjs-plugin-datalabels": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chartjs-plugin-datalabels/-/chartjs-plugin-datalabels-0.7.0.tgz", + "integrity": "sha512-PKVUX14nYhH0wcdCpgOoC39Gbzvn6cZ7O9n+bwc02yKD9FTnJ7/TSrBcfebmolFZp1Rcicr9xbT0a5HUbigS7g==", + "requires": {} + }, "chokidar": { "version": "3.5.3", - "dev": true, "requires": { "anymatch": "~3.1.2", "braces": "~3.0.2", @@ -18166,7 +18228,6 @@ "dependencies": { "glob-parent": { "version": "5.1.2", - "dev": true, "requires": { "is-glob": "^4.0.1" } @@ -18307,14 +18368,12 @@ }, "color-convert": { "version": "1.9.3", - "dev": true, "requires": { "color-name": "1.1.3" } }, "color-name": { - "version": "1.1.3", - "dev": true + "version": "1.1.3" }, "colord": { "version": "2.9.2", @@ -18631,8 +18690,7 @@ "dev": true }, "cssesc": { - "version": "3.0.0", - "dev": true + "version": "3.0.0" }, "cssnano": { "version": "5.1.7", @@ -18709,6 +18767,11 @@ "csstype": { "version": "2.6.20" }, + "custom-event-polyfill": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/custom-event-polyfill/-/custom-event-polyfill-1.0.7.tgz", + "integrity": "sha512-TDDkd5DkaZxZFM8p+1I3yAlvM3rSr1wbrOliG4yJiwinMZN8z/iGL7BTlDkrJcYTmgUSb4ywVCc3ZaUtOtC76w==" + }, "d": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/d/-/d-1.0.1.tgz", @@ -18844,8 +18907,7 @@ } }, "defined": { - "version": "1.0.0", - "dev": true + "version": "1.0.0" }, "delayed-stream": { "version": "1.0.0", @@ -18859,6 +18921,14 @@ "version": "1.0.4", "dev": true }, + "detect-autofill": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/detect-autofill/-/detect-autofill-1.1.4.tgz", + "integrity": "sha512-utCBQwCR/beSnADQmBC7C4tTueBBkYCl6WSpfGUkYKO/+MzPxqYGj6G4MvHzcKmH1gCTK+VunX2vaagvkRXPvA==", + "requires": { + "custom-event-polyfill": "^1.0.7" + } + }, "detect-newline": { "version": "3.1.0", "dev": true @@ -18869,7 +18939,6 @@ }, "detective": { "version": "5.2.0", - "dev": true, "requires": { "acorn-node": "^1.6.1", "defined": "^1.0.0", @@ -18877,8 +18946,7 @@ } }, "didyoumean": { - "version": "1.2.2", - "dev": true + "version": "1.2.2" }, "diff-sequences": { "version": "27.5.1", @@ -18892,8 +18960,7 @@ } }, "dlv": { - "version": "1.1.3", - "dev": true + "version": "1.1.3" }, "dns-equal": { "version": "1.0.0", @@ -19623,7 +19690,6 @@ }, "fast-glob": { "version": "3.2.11", - "dev": true, "requires": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", @@ -19634,7 +19700,6 @@ "dependencies": { "glob-parent": { "version": "5.1.2", - "dev": true, "requires": { "is-glob": "^4.0.1" } @@ -19651,7 +19716,6 @@ }, "fastq": { "version": "1.13.0", - "dev": true, "requires": { "reusify": "^1.0.4" } @@ -19685,7 +19749,6 @@ }, "fill-range": { "version": "7.0.1", - "dev": true, "requires": { "to-regex-range": "^5.0.1" } @@ -19788,8 +19851,7 @@ "dev": true }, "function-bind": { - "version": "1.1.1", - "dev": true + "version": "1.1.1" }, "functional-red-black-tree": { "version": "1.0.1", @@ -19837,7 +19899,6 @@ }, "glob-parent": { "version": "6.0.2", - "dev": true, "requires": { "is-glob": "^4.0.3" } @@ -19879,7 +19940,6 @@ }, "has": { "version": "1.0.3", - "dev": true, "requires": { "function-bind": "^1.1.1" } @@ -20130,7 +20190,6 @@ }, "is-binary-path": { "version": "2.1.0", - "dev": true, "requires": { "binary-extensions": "^2.0.0" } @@ -20154,7 +20213,6 @@ }, "is-core-module": { "version": "2.9.0", - "dev": true, "requires": { "has": "^1.0.3" } @@ -20168,8 +20226,7 @@ "dev": true }, "is-extglob": { - "version": "2.1.1", - "dev": true + "version": "2.1.1" }, "is-file-esm": { "version": "1.0.0", @@ -20188,7 +20245,6 @@ }, "is-glob": { "version": "4.0.3", - "dev": true, "requires": { "is-extglob": "^2.1.1" } @@ -20198,8 +20254,7 @@ "dev": true }, "is-number": { - "version": "7.0.0", - "dev": true + "version": "7.0.0" }, "is-plain-obj": { "version": "3.0.0", @@ -21694,8 +21749,7 @@ } }, "lilconfig": { - "version": "2.0.5", - "dev": true + "version": "2.0.5" }, "lines-and-columns": { "version": "1.2.4", @@ -21956,8 +22010,7 @@ "dev": true }, "merge2": { - "version": "1.4.1", - "dev": true + "version": "1.4.1" }, "methods": { "version": "1.1.2", @@ -21965,7 +22018,6 @@ }, "micromatch": { "version": "4.0.5", - "dev": true, "requires": { "braces": "^3.0.2", "picomatch": "^2.3.1" @@ -22042,8 +22094,7 @@ } }, "minimist": { - "version": "1.2.6", - "dev": true + "version": "1.2.6" }, "minipass": { "version": "3.1.6", @@ -22063,6 +22114,11 @@ "version": "2.2.2", "dev": true }, + "moment": { + "version": "2.29.3", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.3.tgz", + "integrity": "sha512-c6YRvhEo//6T2Jz/vVtYzqBzwvPT95JBQ+smCytzf7c50oMZRsR/a4w88aD34I+/QVSfnoAnSBFPJHItlOMJVw==" + }, "mrmime": { "version": "1.0.0", "dev": true @@ -22192,8 +22248,7 @@ } }, "normalize-path": { - "version": "3.0.0", - "dev": true + "version": "3.0.0" }, "normalize-range": { "version": "0.1.2", @@ -22226,8 +22281,7 @@ "dev": true }, "object-hash": { - "version": "3.0.0", - "dev": true + "version": "3.0.0" }, "object-keys": { "version": "1.1.1", @@ -22456,8 +22510,7 @@ "dev": true }, "path-parse": { - "version": "1.0.7", - "dev": true + "version": "1.0.7" }, "path-to-regexp": { "version": "0.1.7", @@ -22467,12 +22520,16 @@ "version": "4.0.0", "dev": true }, + "perfect-scrollbar": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/perfect-scrollbar/-/perfect-scrollbar-1.5.5.tgz", + "integrity": "sha512-dzalfutyP3e/FOpdlhVryN4AJ5XDVauVWxybSkLZmakFE2sS3y3pc4JnSprw8tGmHvkaG5Edr5T7LBTZ+WWU2g==" + }, "picocolors": { "version": "1.0.0" }, "picomatch": { - "version": "2.3.1", - "dev": true + "version": "2.3.1" }, "pirates": { "version": "4.0.5", @@ -22485,6 +22542,11 @@ "find-up": "^4.0.0" } }, + "popper.js": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1.tgz", + "integrity": "sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ==" + }, "portfinder": { "version": "1.0.28", "dev": true, @@ -22558,14 +22620,12 @@ }, "postcss-js": { "version": "4.0.0", - "dev": true, "requires": { "camelcase-css": "^2.0.1" } }, "postcss-load-config": { "version": "3.1.4", - "dev": true, "requires": { "lilconfig": "^2.0.5", "yaml": "^1.10.2" @@ -22669,7 +22729,6 @@ }, "postcss-nested": { "version": "5.0.6", - "dev": true, "requires": { "postcss-selector-parser": "^6.0.6" } @@ -22762,7 +22821,6 @@ }, "postcss-selector-parser": { "version": "6.0.10", - "dev": true, "requires": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -22784,8 +22842,7 @@ } }, "postcss-value-parser": { - "version": "4.2.0", - "dev": true + "version": "4.2.0" }, "prelude-ls": { "version": "1.1.2", @@ -22907,12 +22964,10 @@ "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==" }, "queue-microtask": { - "version": "1.2.3", - "dev": true + "version": "1.2.3" }, "quick-lru": { - "version": "5.1.1", - "dev": true + "version": "5.1.1" }, "randombytes": { "version": "2.1.0", @@ -22987,7 +23042,6 @@ }, "readdirp": { "version": "3.6.0", - "dev": true, "requires": { "picomatch": "^2.2.1" } @@ -23075,7 +23129,6 @@ }, "resolve": { "version": "1.22.0", - "dev": true, "requires": { "is-core-module": "^2.8.1", "path-parse": "^1.0.7", @@ -23114,8 +23167,7 @@ "dev": true }, "reusify": { - "version": "1.0.4", - "dev": true + "version": "1.0.4" }, "rimraf": { "version": "3.0.2", @@ -23129,7 +23181,6 @@ }, "run-parallel": { "version": "1.2.0", - "dev": true, "requires": { "queue-microtask": "^1.2.2" } @@ -23615,8 +23666,7 @@ } }, "supports-preserve-symlinks-flag": { - "version": "1.0.0", - "dev": true + "version": "1.0.0" }, "svg-tags": { "version": "1.0.0", @@ -23674,7 +23724,6 @@ }, "tailwindcss": { "version": "3.0.24", - "dev": true, "requires": { "arg": "^5.0.1", "chokidar": "^3.5.3", @@ -23700,8 +23749,7 @@ }, "dependencies": { "color-name": { - "version": "1.1.4", - "dev": true + "version": "1.1.4" } } }, @@ -23839,7 +23887,6 @@ }, "to-regex-range": { "version": "5.0.1", - "dev": true, "requires": { "is-number": "^7.0.0" } @@ -23898,6 +23945,21 @@ "version": "2.3.1", "dev": true }, + "tw-elements": { + "version": "1.0.0-alpha12", + "resolved": "https://registry.npmjs.org/tw-elements/-/tw-elements-1.0.0-alpha12.tgz", + "integrity": "sha512-AQHwXIC4kw4T0NPKAi0vD1bkHSiA4RhdXqqnXDWEHKsrN02njc/ITB3YZ92s2f2jt+wBPI2UOpeUsWL5xiMVeQ==", + "requires": { + "@popperjs/core": "^2.6.0", + "chart.js": "^2.9.4", + "chartjs-plugin-datalabels": "^0.7.0", + "deepmerge": "^4.2.2", + "detect-autofill": "^1.1.3", + "perfect-scrollbar": "^1.5.0", + "popper.js": "^1.16.1", + "tailwindcss": "^3.0.7" + } + }, "type": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/type/-/type-1.2.0.tgz", @@ -23992,8 +24054,7 @@ } }, "util-deprecate": { - "version": "1.0.2", - "dev": true + "version": "1.0.2" }, "utila": { "version": "0.4.0", @@ -24626,8 +24687,7 @@ "dev": true }, "xtend": { - "version": "4.0.2", - "dev": true + "version": "4.0.2" }, "y18n": { "version": "5.0.8", @@ -24644,8 +24704,7 @@ "dev": true }, "yaml": { - "version": "1.10.2", - "dev": true + "version": "1.10.2" }, "yargs": { "version": "16.2.0", diff --git a/package.json b/package.json index e02ef530fc185de6dac101e77ea5d312fc4374b0..82bef75d775f37cffd5e8bda14ea138904251888 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "roboto-fontface": "*", "sockjs-client": "^1.6.0", "stompjs": "^2.3.3", + "tw-elements": "^1.0.0-alpha12", "vue": "^3.2.13", "vue-router": "^4.0.3", "vuelidate": "^0.7.7", diff --git a/src/components/BaseComponents/IconButton.vue b/src/components/BaseComponents/IconButton.vue index 973f04c78f374cdb6f8e728c59bd416d1aca4cf8..095db5635e33e9a5116c0724f8a77a8a8c5acb93 100644 --- a/src/components/BaseComponents/IconButton.vue +++ b/src/components/BaseComponents/IconButton.vue @@ -1,17 +1,12 @@ <template> - <!-- Icon button --> <button - class="block w-fit text-white text-base bg-primary-medium hover:bg-primary-dark focus:ring-4 focus:outline-none focus:ring-primary-light font-medium rounded-lg text-center dark:bg-primary-medium dark:hover:bg-primary-dark dark:focus:ring-primary-dark" + class="flex items-center px-2 py-2 font-medium tracking-wide capitalize text-white transition-colors duration-200 transform rounded-md focus:outline-none focus:ring focus:ring-opacity-80" + :class="color" > - <div class="flex flex-row px-5 py-2.5 h-10"> - <!-- Icon slot: Default content "Ban"-icon --> - <div class="h-6 w-6"> - <slot> - <BanIcon /> - </slot> - </div> - <p>{{ text }}</p> + <div class="w-5 h-5 mx-1"> + <slot><BanIcon /></slot> </div> + <span class="mx-1">{{ text }}</span> </button> </template> @@ -22,9 +17,21 @@ export default { name: "IconButton", props: { text: String, + buttonColor: String, }, components: { BanIcon, }, + computed: { + color() { + if (this.buttonColor === "red") { + return "bg-error-medium hover:bg-error-dark focus:ring-error-light"; + } + if (this.buttonColor === "green") { + return "bg-success-medium hover:bg-success-dark focus:ring-success-light"; + } + return "bg-primary-medium hover:bg-primary-dark focus:ring-primary-light"; + }, + }, }; </script> diff --git a/src/components/CommunityComponents/CommunityHamburger.vue b/src/components/CommunityComponents/CommunityHamburger.vue index 34c82b645697dce95fc6d42ce3e6ea75db622323..c53a4d751caf362fd37a4cad34038edd1d4562cb 100644 --- a/src/components/CommunityComponents/CommunityHamburger.vue +++ b/src/components/CommunityComponents/CommunityHamburger.vue @@ -18,9 +18,9 @@ >Se Medlemmer </router-link> </li> - <li id="adminGroup"> + <li id="adminGroup" v-if="admin"> <router-link - :to="'/community/' + communityID + '/memberlist'" + :to="'/community/' + communityID + '/admin'" class="block py-2 px-4 text-sm text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white" >Administrer Gruppe</router-link > @@ -39,6 +39,7 @@ <script> import { LeaveCommunity } from "@/utils/apiutil"; +import CommunityAdminService from "@/services/community-admin.service"; export default { name: "CommunityHamburger", @@ -48,9 +49,9 @@ export default { data() { return { id: -1, + admin: false, }; }, - methods: { leaveCommunity: async function () { this.id = await this.$router.currentRoute.value.params.communityID; @@ -58,5 +59,8 @@ export default { this.$router.push("/"); }, }, + mounted() { + this.admin = CommunityAdminService.isUserAdmin(); + }, }; </script> diff --git a/src/components/CommunityComponents/CommunityHeader.vue b/src/components/CommunityComponents/CommunityHeader.vue index 9be1167a1da7b4871f785ffbc460bf6c4471f7c8..471d0b5fa2b9f71ac62388df21b9fc7dcb9d066f 100644 --- a/src/components/CommunityComponents/CommunityHeader.vue +++ b/src/components/CommunityComponents/CommunityHeader.vue @@ -51,7 +51,7 @@ <!-- If the user is member of the community, this hamburger menu will show --> <div v-if="member"> <svg - @click="toggle" + @click="toggleHamburgerMenu()" xmlns="http://www.w3.org/2000/svg" class="w-9 h-9 cursor-pointer" fill="none" @@ -70,6 +70,7 @@ v-if="hamburgerOpen" class="origin-top-right absolute right-0" :community-i-d="community.communityId" + :admin="admin" /> <!-- class="absolute" --> </div> @@ -80,11 +81,12 @@ <script> import CommunityHamburger from "@/components/CommunityComponents/CommunityHamburger"; import ColoredButton from "@/components/BaseComponents/ColoredButton"; +import CommunityService from "@/services/community.service"; +import CustomFooterModal from "@/components/BaseComponents/CustomFooterModal"; import { JoinOpenCommunity, GetIfUserAlreadyInCommunity, } from "@/utils/apiutil"; -import CustomFooterModal from "@/components/BaseComponents/CustomFooterModal"; export default { name: "CommunityHeader", @@ -98,27 +100,15 @@ export default { hamburgerOpen: false, dialogOpen: false, member: true, + community: {}, }; }, props: { - adminStatus: Boolean, - community: { - communityId: Number, - name: String, - description: String, - visibility: Number, - location: String, - picture: String, - }, + admin: Boolean, }, methods: { - //To open and close the hamburger menu - toggle: function () { - if (this.hamburgerOpen) { - this.hamburgerOpen = false; - } else { - this.hamburgerOpen = true; - } + toggleHamburgerMenu() { + this.hamburgerOpen = !this.hamburgerOpen; }, joinCommunity: async function (id) { const response = await JoinOpenCommunity(id); @@ -141,5 +131,10 @@ export default { beforeMount() { this.getIfUserInCommunity(); }, + async mounted() { + this.community = await CommunityService.getCommunity( + this.$route.params.communityID + ); + }, }; </script> diff --git a/src/components/CommunityComponents/CommunityHome.vue b/src/components/CommunityComponents/CommunityHome.vue index 477ba7fbbf0b314242af6e2762be65c67b040ee2..dc41215d91840e3e6c0b941edff2491b76453379 100644 --- a/src/components/CommunityComponents/CommunityHome.vue +++ b/src/components/CommunityComponents/CommunityHome.vue @@ -1,10 +1,6 @@ <template> <section class="w-full px-5 py-4 mx-auto rounded-md"> - <CommunityHeader - :admin-status="false" - :community="community" - class="mb-5" - /> + <CommunityHeader :admin="false" class="mb-5" /> <!-- Search field --> <div class="relative" id="searchComponent"> @@ -34,7 +30,12 @@ <div class="grid grid-flow-row-dense grid-cols-2 md:grid-cols-4 lg:grid-cols-5 w-full place-items-center" > - <ItemCard v-for="item in searchedItems" :key="item" :item="item" /> + <ItemCard + v-for="item in searchedItems" + :key="item" + :item="item" + @click="goToItemInfoPage(item.listingID)" + /> </div> </div> </section> @@ -43,15 +44,17 @@ <script> import CommunityHeader from "@/components/CommunityComponents/CommunityHeader.vue"; import ItemCard from "@/components/ItemComponents/ItemCard"; -import { GetCommunity, GetListingsInCommunity } from "@/utils/apiutil"; +import { + GetCommunity, + GetListingsInCommunity, + getItemPictures, +} from "@/utils/apiutil"; export default { name: "SearchItemListComponent", - components: { CommunityHeader, ItemCard, }, - computed: { searchedItems() { let filteredItems = []; @@ -75,6 +78,7 @@ export default { return { items: [], item: { + listingID: 0, img: "", address: "", title: "", @@ -95,6 +99,20 @@ export default { this.communityID = await this.$router.currentRoute.value.params .communityID; this.items = await GetListingsInCommunity(this.communityID); + for (var i = 0; i < this.items.length; i++) { + let images = await getItemPictures(this.items[i].listingID); + console.log(images); + if (images.length > 0) { + this.items[i].img = images[0].picture; + } + } + }, + goToItemInfoPage(item) { + this.$router.push("/itempage/" + item); + }, + getItemPictures: async function (itemid) { + let res = await getItemPictures(itemid); + return res; }, }, beforeMount() { diff --git a/src/components/CommunityComponents/CommunityRequestForm.vue b/src/components/CommunityComponents/CommunityRequestForm.vue index d43681e638c3030fdbc7642a0515ac9e8cb5be41..add42c62e1bd00cd4f33377ce3c73c788abd6e3f 100644 --- a/src/components/CommunityComponents/CommunityRequestForm.vue +++ b/src/components/CommunityComponents/CommunityRequestForm.vue @@ -6,7 +6,7 @@ <div class="text-xl md:text-2xl font-medium text-center text-gray-600 dark:text-gray-200 mt-4 mb-10" > - Bli med i gruppe: {{gruppenavn}} + Bli med i: {{gruppenavn}} </div> @@ -15,7 +15,7 @@ <label class="block mb-2 text-sm font-medium text-gray-900 dark:text-gray-400" id="descriptionLabel" - >Melding</label + > Melding til administrator av gruppa: </label > <textarea id="description" @@ -45,9 +45,9 @@ </template> <script> +import axios from "axios"; import useVuelidate from "@vuelidate/core"; import { required, helpers, maxLength } from "@vuelidate/validators"; -import { postNewgroup } from "@/utils/apiutil"; import Button from "@/components/BaseComponents/ColoredButton"; export default { @@ -79,44 +79,17 @@ export default { data() { return { group: { - name: "", description: "", - images: [], - radio: null, - place: "", - visibility: 1, + communityId: null, }, - imageThere: false, }; }, computed: { }, methods: { - checkValidation: function () { - this.v$.group.$touch(); - if (this.v$.group.$invalid) { - return false; - } - return true; - }, async saveClicked() { - if (this.checkValidation()) { - const groupInfo = { - name: this.group.name, - description: this.group.description, - visibility: this.group.visibility, - location: this.group.place, - picture: "", - }; - - await postNewgroup(groupInfo); - } - }, - - addImage: function (event) { - this.group.images.push(URL.createObjectURL(event.target.files[0])); - this.imageThere = true; + await axios.post(process.env.VUE_APP_BASEURL+ `community/${this.communityId}/private/join`); }, }, }; diff --git a/src/components/CommunityComponents/CommunitySettings.vue b/src/components/CommunityComponents/CommunitySettings.vue new file mode 100644 index 0000000000000000000000000000000000000000..9e6b3d4484c26cffbafd66a1c0bf1f516ff1cd6c --- /dev/null +++ b/src/components/CommunityComponents/CommunitySettings.vue @@ -0,0 +1,28 @@ +<template> + <div class="grid place-content-center h-48"> + <IconButton + @click="deleteCommunity" + :buttonColor="'red'" + :text="'Slett felleskap'" + /> + </div> +</template> + +<script> +// import AdminService from "@/services/community-admin.service"; +import IconButton from "@/components/BaseComponents/IconButton.vue"; + +//TODO: OPEN CONFIRMATION DIALOG WHEN DELETING + +export default { + components: { + IconButton, + }, + methods: { + deleteCommunity() { + console.log("DELETED"); + // AdminService.deleteCommunity(this.$route.params.communityID); + }, + }, +}; +</script> diff --git a/src/components/CommunityComponents/MemberList.vue b/src/components/CommunityComponents/MemberList.vue index 56c8bbe02a6683c4b5dc9bb2df22f1b3ad9809ec..847e35e6a71bb74f88f045e9ba4bd6369ed04196 100644 --- a/src/components/CommunityComponents/MemberList.vue +++ b/src/components/CommunityComponents/MemberList.vue @@ -1,50 +1,30 @@ <template> - <CommunityHeader - :admin-status="false" - :community="community" - class="mb-5 mt-5" - /> <ul> - <li v-for="member in memberlist" :key="member.userId"> - <user-list-item-card :admin="admin" :user="member" /> + <li v-for="member in members" :key="member.userId"> + <UserListItemCard :buttons="buttons" :user="member" /> </li> </ul> </template> <script> import UserListItemCard from "@/components/UserProfileComponents/UserListItemCard.vue"; -import { GetMembersOfCommunity, GetCommunity } from "@/utils/apiutil"; -import CommunityHeader from "@/components/CommunityComponents/CommunityHeader.vue"; +import { GetMembersOfCommunity } from "@/utils/apiutil"; export default { - data() { - return { - memberlist: [], - community: {}, - }; - }, + name: "MemberList", components: { - CommunityHeader, UserListItemCard, }, props: { - admin: Boolean, + buttons: Array, }, - methods: { - getAllMembersOfCommunity: async function () { - this.memberlist = await GetMembersOfCommunity( - this.$router.currentRoute.value.params.id - ); - }, - getCommunity: async function () { - this.community = await GetCommunity( - this.$router.currentRoute.value.params.id - ); - }, + data() { + return { + members: [], + }; }, - beforeMount() { - this.getAllMembersOfCommunity(); - this.getCommunity(); + async created() { + this.members = await GetMembersOfCommunity(this.$route.params.communityID); }, }; </script> diff --git a/src/components/FormComponents/NewPasswordForm.vue b/src/components/FormComponents/NewPasswordForm.vue index 7e245518cd2831124dabc37cf6b903e6bbf76c97..d13c08349360250ff4f0cf96b5f3faab00d7a841 100644 --- a/src/components/FormComponents/NewPasswordForm.vue +++ b/src/components/FormComponents/NewPasswordForm.vue @@ -2,11 +2,11 @@ <div class="md:ring-1 ring-gray-300 rounded-xl overflow-hidden mx-auto mb-auto max-w-md w-full p-4" > - <div - class="text-2xl font-medium text-center text-gray-600 dark:text-gray-200 mt-4 mb-8" + <h3 + class="text-xl font-medium text-center text-gray-600 dark:text-gray-200 mt-4 mb-8" > Endre passord - </div> + </h3> <div id="firstPasswordField" diff --git a/src/components/FormComponents/ResetPasswordForm.vue b/src/components/FormComponents/ResetPasswordForm.vue index 61a48774e9d97658981eeadf44659e2d9e73b86a..a4c2d80e1533e2c7be82804041bd49208b4ab6be 100644 --- a/src/components/FormComponents/ResetPasswordForm.vue +++ b/src/components/FormComponents/ResetPasswordForm.vue @@ -2,11 +2,11 @@ <div class="md:ring-1 ring-gray-300 rounded-xl overflow-hidden mx-auto mb-auto max-w-md w-full p-4" > - <div - class="text-2xl font-medium text-center text-gray-600 dark:text-gray-200 mt-4 mb-8" + <h3 + class="text-xl font-medium text-center text-gray-600 dark:text-gray-200 mt-4 mb-8" > Glemt passordet ditt? - </div> + </h3> <div id="emailField" diff --git a/src/components/ItemComponents/ItemCard.vue b/src/components/ItemComponents/ItemCard.vue index 089dc01a8043d3409d6806537e853cf95017cc28..ac79861f7bf9eb7bbed3592b4420d440bc98e587 100644 --- a/src/components/ItemComponents/ItemCard.vue +++ b/src/components/ItemComponents/ItemCard.vue @@ -1,6 +1,8 @@ <template> <div class="mt-5"> - <div class="w-4/5 rounded bg-gray-200"> + <div + class="w-4/5 rounded bg-gray-200 h-full overflow-hidden display:inline-block correct-size" + > <img class="w-full" :src="item.img || require('../../assets/default-product.png')" diff --git a/src/components/ItemComponents/NewItemForm.vue b/src/components/ItemComponents/NewItemForm.vue index 14208374965626387442ef8e888e4c2cda8195ca..f9f5f06e0e6a8fc5e610ee8cfc326a276d111e18 100644 --- a/src/components/ItemComponents/NewItemForm.vue +++ b/src/components/ItemComponents/NewItemForm.vue @@ -3,11 +3,11 @@ class="md:ring-1 ring-gray-300 rounded-xl overflow-hidden mx-auto mb-auto max-w-md w-full p-4" > <!-- Component heading --> - <div - class="text-xl md:text-2xl font-medium text-center text-gray-600 dark:text-gray-200 mt-4 mb-10" + <h3 + class="text-xl font-medium text-center text-gray-600 dark:text-gray-200 mt-4 mb-8" > Opprett ny utleie - </div> + </h3> <!-- Title --> <div class="mb-6" :class="{ error: v$.item.title.$errors.length }"> diff --git a/src/components/RentingComponents/ImageCarousel.vue b/src/components/RentingComponents/ImageCarousel.vue new file mode 100644 index 0000000000000000000000000000000000000000..aef0e652e6609273c2505025287c152ece3d10d4 --- /dev/null +++ b/src/components/RentingComponents/ImageCarousel.vue @@ -0,0 +1,78 @@ +<template> + <div + id="carouselIndicators" + class="carousel slide relative" + data-bs-ride="carousel" + > + <div + class="carousel-indicators absolute right-0 bottom-0 left-0 flex justify-center p-0 mb-4" + > + <button + v-for="(image, i) in images" + :key="i" + type="button" + data-bs-target="#carouselIndicators" + :data-bs-slide-to="i" + :class="{ active: i === 0 }" + :aria-current="i === 0 ? 'true' : 'false'" + :aria-label="'Slide ' + (i + 1)" + ></button> + </div> + <div class="carousel-inner relative w-full overflow-hidden"> + <div + v-for="(image, i) in images" + :key="i" + :class="'carousel-item float-left w-full' + (i == 0 ? ' active' : '')" + > + <img :src="image.src" class="block w-full" :alt="image.alt" /> + </div> + </div> + <button + class="carousel-control-prev absolute top-0 bottom-0 flex items-center justify-center p-0 text-center border-0 hover:outline-none hover:no-underline focus:outline-none focus:no-underline left-0" + type="button" + data-bs-target="#carouselIndicators" + data-bs-slide="prev" + > + <span + class="carousel-control-prev-icon inline-block bg-no-repeat" + aria-hidden="true" + ></span> + <span class="visually-hidden">Previous</span> + </button> + <button + class="carousel-control-next absolute top-0 bottom-0 flex items-center justify-center p-0 text-center border-0 hover:outline-none hover:no-underline focus:outline-none focus:no-underline right-0" + type="button" + data-bs-target="#carouselIndicators" + data-bs-slide="next" + > + <span + class="carousel-control-next-icon inline-block bg-no-repeat" + aria-hidden="true" + ></span> + <span class="visually-hidden">Next</span> + </button> + </div> +</template> + +<script> +/* +{ + src: "imageURL", + alt: "IMAGE ALT TEXT" +} +*/ +import "tw-elements"; +export default { + props: { + images: { + type: Array, + required: true, + }, + }, + data() { + return {}; + }, +}; +</script> + +<style scoped></style> diff --git a/src/components/RentingComponents/ItemInfo.vue b/src/components/RentingComponents/ItemInfo.vue new file mode 100644 index 0000000000000000000000000000000000000000..59308822724d89fc7749b0e1ed88afde4b316cea --- /dev/null +++ b/src/components/RentingComponents/ItemInfo.vue @@ -0,0 +1,219 @@ +<template> + <div> + <new-rent v-if="confirm" :newRentBox="pushItem"> </new-rent> + </div> + <div v-if="!confirm"> + <div> + <div + v-bind:class="{ + 'grid grid-flow-row-dense grid-cols-2 md:grid-cols-4 h-[600px] w-auto lg:grid-cols-5 place-items-center': + noPicture, + }" + > + <ImageCarousel :images="pictures"></ImageCarousel> + </div> + </div> + <!-- Product info --> + <div + class="max-w-2xl mx-auto pt-10 pb-16 px-4 sm:px-6 lg:max-w-7xl lg:pt-16 lg:pb-24 lg:px-8 lg:grid lg:grid-cols-3 lg:grid-rows-[auto,auto,1fr] lg:gap-x-8" + > + <div class="lg:col-span-2 lg:border-r lg:border-gray-200 lg:pr-8"> + <h1 + class="text-2xl font-extrabold tracking-tight text-gray-900 sm:text-3xl" + > + {{ item.title }} + </h1> + </div> + <!-- TODO make this component render elements differently depending on screen size --> + <div + class="py-10 lg:pt-6 lg:pb-16 lg:col-start-1 lg:col-span-2 lg:border-r lg:border-gray-200 lg:pr-8" + > + <!-- Description and details --> + <div> + <h3 class="text-base font-semibold text-gray-900">Pris per dag</h3> + + <div class="space-y-6"> + <p class="text-2xl font-medium text-gray-900"> + {{ item.pricePerDay }} kr + </p> + </div> + </div> + <div> + <div class="mt-4 space-y-6"> + <p class="text-sm text-gray-600">{{ item.description }}</p> + </div> + </div> + <div> + <div class="mt-4 space-y-6"> + <p class="text-base font-semibold text-gray-900"> + {{ item.address }} + </p> + </div> + </div> + <div class="mt-2"> + <UserListItemCard :user="userForId"></UserListItemCard> + </div> + <div class="mt-4"> + <h3 class="text-base font-base text-gray-900">Tidspunkter</h3> + + <div> + <p class="text-sm text-gray-900"> + <DatepickerRange + @value="setDate" + :messageOnDisplay="dateMessage" + ></DatepickerRange> + </p> + </div> + </div> + <div class="mt-2 md:col-span-1"> + <div class="mt-2 space-y-2"> + <p class="text-xl font-semibold text-gray-900"> + Total pris: {{ totPrice }} kr + </p> + <button + class="px-4 py-2 font-medium tracking-wide text-white capitalize transition-colors duration-200 transform bg-gray-500 rounded-md focus:outline-none focus:ring focus:ring-opacity-80" + v-bind:class="{ colorChange: allowForRent }" + @click="sendToConfirm" + > + <!-- This button should send you to the rent page --> + Rent now + </button> + </div> + </div> + </div> + </div> + </div> +</template> + +<script> +import NewRent from "@/components/RentingComponents/NewRent.vue"; +import { getItem, getItemPictures, getUser } from "@/utils/apiutil"; +import ImageCarousel from "@/components/RentingComponents/ImageCarousel.vue"; +import UserListItemCard from "@/components/UserProfileComponents/UserListItemCard.vue"; +import DatepickerRange from "@/components/TimepickerComponents/DatepickerRange/DatepickerRange.vue"; + +export default { + name: "ItemInfo", + data() { + return { + confirm: false, + item: { + listingID: 0, + title: "", + description: "", + pricePerDay: 0, + price: this.totPrice, + address: "", + userID: 0, + categoryNames: [], + communityIDs: [], + }, + pushItem: { + listingID: 157, + title: "Heii", + price: 56, + fromTime: "", + toTime: "", + }, + images: [ + { + listingID: 0, + picture: "", + }, + ], + pictures: [], + noPicture: true, + userForId: Object, + rentingStartDate: null, + rentingEndDate: null, + totPrice: 0, + dateMessage: "Venligst velg dato for leieperioden", + allowForRent: false, + }; + }, + components: { + ImageCarousel, + UserListItemCard, + DatepickerRange, + NewRent, + }, + methods: { + sendToConfirm() { + if (this.allowForRent) { + this.confirm = true; + this.createPushItem(); + } + }, + createPushItem() { + this.pushItem.listingID = this.item.listingID; + this.pushItem.fromTime = this.rentingStartDate; + this.pushItem.toTime = this.rentingEndDate; + this.pushItem.title = this.item.title; + this.pushItem.price = this.totPrice; + }, + async getItem() { + let id = this.$router.currentRoute.value.params.id; + this.item = await getItem(id); + this.item.listingID = id; + this.totPrice = this.item.pricePerDay; + }, + async getItemPictures() { + let id = this.$router.currentRoute.value.params.id; + this.images = await getItemPictures(id); + + if (this.images.length < 1) { + let noImage = { + src: require("@/assets/default-product.png"), + alt: "No image found", + }; + this.pictures.push(noImage); + } else { + this.noPicture = false; + for (let i = 0; i < this.images.length; i++) { + let oneImage = { + src: this.images[i].picture, + //How do i make this accurate to the image? + alt: "An image", + }; + this.pictures.push(oneImage); + } + } + //TODO fixs so each image get a correct alt text. + }, + async getUser(userId) { + this.userForId = await getUser(userId); + }, + setDate(dateOfsomthing) { + if (dateOfsomthing.startDate == null || dateOfsomthing.endDate == null) { + this.totPrice = this.item.pricePerDay; + this.allowForRent = false; + } else { + this.rentingStartDate = dateOfsomthing.startDate; + this.rentingEndDate = dateOfsomthing.endDate; + this.calculateTotPrice(); + this.allowForRent = true; + } + }, + calculateTotPrice() { + let amountOfDays = this.rentingEndDate - this.rentingStartDate; + amountOfDays = amountOfDays / 86400000; + this.totPrice = this.item.pricePerDay * amountOfDays; + }, + }, + async beforeMount() { + await this.getItemPictures(); + await this.getItem(); + await this.getUser(this.item.userID); + }, +}; +</script> + +<style> +.colorChange { + background-color: #004aad; +} + +.colorChange:hover { + background-color: #306ec1; +} +</style> diff --git a/src/components/RentingComponents/NewRent.vue b/src/components/RentingComponents/NewRent.vue new file mode 100644 index 0000000000000000000000000000000000000000..32073aa24a66328935b56876748e554e8dc00925 --- /dev/null +++ b/src/components/RentingComponents/NewRent.vue @@ -0,0 +1,219 @@ +<template> + <div id="confirmationBox"> + <div id="rentTitle"> + <h1> + {{ title }} + </h1> + </div> + + <div id="fromTime"> + <p>Fra: {{ fromTimeString }}</p> + </div> + <div id="toTime"> + <p>Til: {{ toTimeString }}</p> + </div> + <div id="price"> + <p>Totaltpris: {{ price }} kr</p> + </div> + <div id="message"> + <label + class="block mb-2 text-sm font-medium text-gray-900 dark:text-gray-400" + id="descriptionLabel" + >Skriv en melding til utleier:</label + > + <textarea + id="description" + rows="4" + v-model="message" + class="block p-2.5 w-full text-sm text-gray-900 bg-gray-200 rounded-lg border border-gray-300 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" + required + ></textarea> + </div> + <button id="cancelButton" @click="cancelRent" class="text-primary-medium"> + Tilbake + </button> + <div id="confirmButton"> + <colored-button @click="sendRent" :text="'Send'"></colored-button> + </div> + </div> + <div> + <notification-modal + @click="routeToHome" + :visible="confirmed" + :title="'Vellykket'" + :message="'Forespørsel sendt!'" + > + </notification-modal> + </div> +</template> + +<script> +import ColoredButton from "@/components/BaseComponents/ColoredButton.vue"; +import { postNewRent } from "@/utils/apiutil"; +import NotificationModal from "@/components/BaseComponents/NotificationModal.vue"; +export default { + name: "NewRent", + components: { + ColoredButton, + NotificationModal, + }, + data() { + return { + confirmed: false, + title: this.newRentBox.title, + fromTime: this.newRentBox.fromTime, + fromTimeString: "", + toTime: this.newRentBox.toTime, + toTimeString: "", + fromTimeMilliseconds: new Date(this.newRentBox.fromTime).valueOf(), + toTimeMilliseconds: new Date(this.newRentBox.toTime).valueOf(), + message: "", + price: this.newRentBox.price, + }; + }, + props: { + newRentBox: { + renterId: Number, + title: String, + description: String, + fromTime: Date, + toTime: Date, + listingID: Number, + isAccepted: Boolean, + price: Number, + }, + }, + created() { + this.fromTimeString = this.convertDates(this.fromTime); + this.toTimeString = this.convertDates(this.toTime); + }, + + methods: { + //Converts Date-object to a more readable string + convertDates(date) { + //Copies the chosen date + const dateCopy = new Date(date); + //Gets the day part of date (1-31) + const dateDate = dateCopy.getDate(); + //Gets the month of the date (1-12) + const dateMonth = dateCopy.getMonth(); + let monthString = ""; + //Gives the month the proper name + switch (dateMonth) { + case 1: + monthString = "Januar"; + break; + case 2: + monthString = "Februar"; + break; + case 3: + monthString = "Mars"; + break; + case 4: + monthString = "April"; + break; + case 5: + monthString = "Mai"; + break; + case 6: + monthString = "Juni"; + break; + case 7: + monthString = "Juli"; + break; + case 8: + monthString = "August"; + break; + case 9: + monthString = "September"; + break; + case 10: + monthString = "Oktober"; + break; + case 11: + monthString = "November"; + break; + case 12: + monthString = "Desember"; + break; + default: + monthString = "Noe feil"; + break; + } + //Gets the year of the date + const dateYear = dateCopy.getFullYear(); + return dateDate + ". " + monthString + " " + dateYear; + }, + cancelRent() { + this.$router.go(0); + }, + routeToHome() { + this.$router.push("/"); + }, + sendRent: async function () { + const rent = { + renterId: 0, + message: this.message, + listingId: this.newRentBox.listingID, + isAccepted: false, + toTime: this.toTimeMilliseconds, + fromTime: this.fromTimeMilliseconds, + }; + + await postNewRent(rent); + this.confirmed = true; + }, + }, +}; +</script> + +<style scoped> +#confirmationBox { + border: 1px solid silver; + margin-top: 60fr; + padding: 10%; + width: 80%; + margin: auto; + display: grid; + grid-template-columns: 1fr 3fr; + grid-template-rows: 0.5fr 1.5fr 0.7fr 0.7fr 0.7fr 3fr 1fr; + /* max-height: 100%; */ + margin-top: 10%; +} +#cancelButton { + grid-column: 1/2; + grid-row: 1/2; +} +#rentTitle { + margin-top: 10%; + text-align: center; + grid-column: 1/3; + grid-row: 2/3; + font-weight: bold; +} +#fromTime { + grid-row: 3/4; + grid-column: 1/3; + display: block; +} +#toTime { + grid-row: 4/5; + grid-column: 1/3; + display: block; +} +#price { + grid-row: 5/6; + grid-column: 1/3; +} +#message { + grid-column: 1/3; + grid-row: 6/7; +} +#confirmButton { + grid-column: 1/3; + grid-row: 7/8; + align-content: center; + margin: auto; + margin-top: 10px; +} +</style> diff --git a/src/components/TimepickerComponents/DatepickerRange/CalendarComponent.vue b/src/components/TimepickerComponents/DatepickerRange/CalendarComponent.vue new file mode 100644 index 0000000000000000000000000000000000000000..5279dc8265d9273154150ff7398a40912bcae196 --- /dev/null +++ b/src/components/TimepickerComponents/DatepickerRange/CalendarComponent.vue @@ -0,0 +1,313 @@ +<template> + <div class="calendar"> + <div + class="grid grid-cols-7 py-2 mt-0.5 border-b border-black border-opacity-10 dark:border-litepie-secondary-700 dark:border-opacity-100" + > + <div class="months" v-for="day in days" :key="day">{{ day }}</div> + </div> + <div class="grid grid-cols-7 gap-y-0.5 my-1"> + <div + class="days blocked" + v-for="index in daysBeforeStartOfMonth" + :key="index" + > + <button> + {{ daysInMonthBeforeMonth - (daysBeforeStartOfMonth - index) }} + </button> + </div> + <div + class="days relative" + v-for="index in daysInMonth" + :key="index" + :class="{ + disabled: this.isBlocked(index), + selected: this.isDayStartDate(index) && !this.endDate, + start: this.isDayStartDate(index), + end: this.isDayEndDate(index), + error: !!( + this.isBlocked(index) && this.isDayBetweenStartAndEndDate(index) + ), + between: this.isDayBetweenStartAndEndDate(index), + }" + > + <span class="overlay absolute inset-0"></span> + <button v-on:Click="selectDate(index)">{{ index }}</button> + </div> + <div + class="days blocked" + v-for="index in daysRemainingInLastWeek" + :key="index" + > + <button>{{ index }}</button> + </div> + </div> + <p v-if="doesDaysSelectedContainBlockedDays()" class="text-red-500"> + {{ errorMessage }} + </p> + </div> +</template> + +<script> +export default { + props: { + month: { + type: Date, + required: true, + }, + blockedDaysRange: { + type: Array, + default: () => [], + }, + start: { + type: Date, + default: () => null, + }, + end: { + type: Date, + default: () => null, + }, + errorMessage: { + type: String, + default: () => "You cannot select a blocked day", + }, + }, + data() { + return { + days: ["Man", "Tir", "Ons", "Tor", "Fre", "Lør", "Søn"], + }; + }, + computed: { + monthDate() { + return new Date(this.month); + }, + startDate() { + return this.start ? new Date(this.start) : null; + }, + endDate() { + return this.end ? new Date(this.end) : null; + }, + blockedDays() { + const blockedDays = []; + this.blockedDaysRange.forEach((blockedDay) => { + if (blockedDay.length === 2) { + const start = new Date(blockedDay[0]); + const end = new Date(blockedDay[1]); + + // Check if the start or end dates range ends in the current month or after the current month + if ( + start.getMonth() <= this.monthDate.getMonth() && + end.getMonth() >= this.monthDate.getMonth() + ) { + if ( + start.getMonth() < this.monthDate.getMonth() && + end.getMonth() > this.monthDate.getMonth() + ) { + // Add all days of month to the list + for (let i = 1; i <= this.daysInMonth; i++) { + blockedDays.push(i); + } + } else if (start.getMonth() === this.monthDate.getMonth()) { + // Add all days of start month to the list + for (let i = start.getDate(); i <= this.daysInMonth; i++) { + blockedDays.push(i); + } + } else if (end.getMonth() === this.monthDate.getMonth()) { + // Add all days of end month to the list + for (let i = 1; i <= end.getDate(); i++) { + blockedDays.push(i); + } + } + } + } else { + // check that day is in this month + const day = new Date(blockedDay[0]); + if (day.getMonth() !== this.monthDate.getMonth()) return; + blockedDays.push(day.getDate()); + } + }); + return blockedDays; + }, + daysInMonth() { + return new Date( + this.monthDate.getFullYear(), + this.monthDate.getMonth() + 1, + 0 + ).getDate(); + }, + daysRemainingInLastWeek() { + return ( + 7 - + new Date( + new Date(this.month).getFullYear(), + new Date(this.month).getMonth() + 1, + 0 + ).getDay() + ); + }, + daysInMonthBeforeMonth() { + let daysInMonth = new Date( + new Date(this.month).getFullYear(), + new Date(this.month).getMonth(), + 0 + ).getDate(); + return daysInMonth; + }, + daysBeforeStartOfMonth() { + let firstDayOfMonth = new Date( + new Date(this.month).getFullYear(), + new Date(this.month).getMonth(), + 1 + ); + let dayOfWeek = firstDayOfMonth.getDay(); + let daysBefore = dayOfWeek === 0 ? 6 : dayOfWeek - 1; + return daysBefore; + }, + }, + methods: { + isDateInMonth(date) { + if (!date) return null; + return ( + date.getFullYear() === this.monthDate.getFullYear() && + date.getMonth() === this.monthDate.getMonth() + ); + }, + isDayStartDate(day) { + // Check that start day is in month + if (!this.isDateInMonth(this.startDate)) return false; + return this.startDate && this.startDate.getDate() === day; + }, + isDayEndDate(day) { + // Check that end day is in month + if (!this.isDateInMonth(this.endDate)) return false; + return this.endDate && this.endDate.getDate() === day; + }, + selectDate(day) { + const date = new Date( + this.monthDate.getFullYear(), + this.monthDate.getMonth(), + day + ); + this.$emit("selectDate", date); + }, + isBlocked(day) { + return this.blockedDays.includes(day); + }, + isDayBlocked(day) { + return this.blockedDays.includes(day); + }, + isDayBetweenBlocked(day) { + return this.blockedDaysRange.some(([start, end]) => { + return start <= day && day <= end; + }); + }, + // Get date from day and month and check if it is between start and end + isDayBetweenStartAndEndDate(day) { + if (!this.startDate || !this.endDate) return false; + const date = new Date( + this.monthDate.getFullYear(), + this.monthDate.getMonth(), + day + ); + return ( + this.startDate.getTime() < date.getTime() && + date.getTime() < this.endDate.getTime() + ); + }, + doesDaysSelectedContainBlockedDays() { + return false; + }, + }, +}; +</script> + +<style scoped> +.calendar { + max-width: 400px; +} + +.months { + font-size: small; + text-align: center; +} +.days { + font-size: small; + text-align: center; +} + +.blocked button { + cursor: not-allowed; + color: gray; +} + +.disabled { + cursor: not-allowed; +} + +.disabled button { + cursor: not-allowed; +} + +button { + background-color: transparent; + width: 3rem; + height: 3rem; +} + +.disabled span { + background-color: red; + border-radius: 5px; + opacity: 0.6; +} + +.disabled button { + color: black; +} + +.selected.start span { + background-color: blue; + opacity: 0.8; + border-radius: 5px; +} + +.start span { + background-color: blue; + opacity: 0.8; + border-radius: 10px 0 0 10px; +} + +.start button { + color: white; + font-weight: bold; +} + +.end button { + color: white; + font-weight: bold; +} + +.between span { + background-color: lightblue; + opacity: 0.8; +} + +.error span { + background-color: red; + opacity: 0.7; + border-radius: 0; +} + +.end button { + color: white; + font-weight: bold; +} + +.end span { + background-color: blue; + opacity: 0.8; + border-radius: 0 10px 10px 0; +} + +.overlay { + z-index: -1; +} +</style> diff --git a/src/components/TimepickerComponents/DatepickerRange/DatepickerRange.vue b/src/components/TimepickerComponents/DatepickerRange/DatepickerRange.vue new file mode 100644 index 0000000000000000000000000000000000000000..53c92481e3318bfe86ccc699f80c6ed24ffbceb7 --- /dev/null +++ b/src/components/TimepickerComponents/DatepickerRange/DatepickerRange.vue @@ -0,0 +1,290 @@ +<template> + <div> + <div class="input" v-on:click="openCalendar()"> + <label> + <input + type="text" + v-model="value" + v-bind:placeholder="messageOnDisplay" + /> + </label> + </div> + <div class="picker" :style="style"> + <div class="calenders"> + <div> + <month-selector + @back="back" + type="month" + @forward="forward('month')" + :month="month" + ></month-selector> + <calendar-component + :month="month" + :start="startDate" + :end="endDate" + @selectDate="selectDate" + :blockedDaysRange="blockedDaysRange" + ></calendar-component> + </div> + <div class="split"></div> + <div> + <month-selector + @back="back" + type="monghM" + @forward="forward('monghM')" + :month="monghM" + ></month-selector> + <calendar-component + :month="monghM" + :start="startDate" + :end="endDate" + @selectDate="selectDate" + :blockedDaysRange="blockedDaysRange" + ></calendar-component> + </div> + </div> + <div class="footer"> + <p v-if="error" class="error">{{ errorMessage }}</p> + <div v-else></div> + <div class="buttons"> + <button class="btn btn-gray" v-on:click="reset()">RESET</button> + <button class="btn btn-blue" v-on:click="complete()">COMPLETE</button> + </div> + </div> + </div> + </div> +</template> + +<script> +import CalendarComponent from "./CalendarComponent.vue"; +import MonthSelector from "./MonthSelector.vue"; +export default { + components: { + CalendarComponent, + MonthSelector, + }, + props: { + range: { + type: Boolean, + default: true, + }, + blockedDaysRange: { + type: Array, + default: () => [], + }, + messageOnDisplay: String, + }, + data() { + return { + month: new Date(), + monghM: new Date(new Date().getFullYear(), new Date().getMonth() + 1, 1), + startDate: new Date(Number.MAX_VALUE), + endDate: null, + error: false, + errorMessage: "Produktet kan ikke utlånes i denne perioden", + open: false, + value: null, + style: { + display: "none", + }, + }; + }, + methods: { + openCalendar() { + // Edit style + this.open = true; + this.style = { + display: "flex", + }; + }, + reset() { + this.startDate = new Date(Number.MAX_VALUE); + this.endDate = null; + this.value = null; + this.error = false; + this.$emit("value", { + startDate: null, + endDate: null, + }); + }, + complete() { + if (this.error) return; + this.open = false; + this.style = { + display: "none", + }; + + // Set value to the start and end date + this.value = `${this.startDate.toLocaleDateString()} - ${this.endDate.toLocaleDateString()}`; + this.$emit("value", { + startDate: this.startDate, + endDate: this.endDate, + }); + }, + selectDate(date) { + const day = date.getDate(); + const month = date.getMonth(); + const year = date.getFullYear(); + if (this.isBlocked(day, month, year)) return; + if (date > this.startDate) { + this.endDate = date; + } else { + this.startDate = date; + } + + if (this.range) { + this.value = `${this.startDate?.toLocaleDateString()} - ${ + this.endDate?.toLocaleDateString() || "" + }`; + } else { + this.value = `${this.startDate.toLocaleDateString()}`; + } + // Check if start and end check if any days in range is blocked + for ( + let i = new Date(this.startDate.toUTCString()); + i <= this.endDate; + i.setDate(i.getDate() + 1) + ) { + if (this.isBlocked(i.getDate(), i.getMonth(), i.getFullYear())) { + this.error = true; + return; + } + } + + if (this.error) this.error = false; + }, + isBlocked(day, month, year) { + return this.blockedDaysRange.some(([start, end]) => { + // Check if start is the same day + if ( + start.getDate() === day && + start.getMonth() === month && + start.getFullYear() === year + ) { + return true; + } + return ( + start <= new Date(year, month, day) && + new Date(year, month, day) <= end + ); + }); + }, + back(type) { + switch (type) { + case "month": { + this.month.setMonth(this.month.getMonth() - 1); + this.month = new Date(this.month); + break; + } + case "monghM": + this.monghM.setMonth(this.monghM.getMonth() - 1); + this.monghM = new Date(this.monghM); + if (this.monghM.getMonth() === this.month.getMonth()) { + this.month.setMonth(this.month.getMonth() - 1); + this.month = new Date(this.month); + } + break; + } + }, + forward(type) { + switch (type) { + case "month": { + this.month.setMonth(this.month.getMonth() + 1); + this.month = new Date(this.month); + if (this.month.getMonth() == this.monghM.getMonth()) { + this.monghM.setMonth(this.monghM.getMonth() + 1); + this.monghM = new Date(this.monghM); + } + break; + } + case "monghM": + this.monghM.setMonth(this.monghM.getMonth() + 1); + this.monghM = new Date(this.monghM); + break; + } + }, + }, +}; +</script> + +<style scoped> +.input label { + display: block; + position: relative; +} + +.input input { + position: relative; + width: 100%; + overflow: hidden; + border: 1px solid #ccc; + border-radius: 10px; + padding: 10px; + padding-right: 50px; +} + +.picker { + position: absolute; + background-color: white; + margin-top: 5px; + z-index: 50; + border: 1px solid black; + border-radius: 4px; + padding: 5px; + flex-direction: column; + width: auto; + max-width: 700px; + overflow: auto; + -webkit-box-shadow: 0px 10px 13px -7px #000000, + 5px 5px 14px 5px rgba(0, 0, 0, 0); + box-shadow: 0px 10px 13px -7px #000000, 5px 5px 14px 5px rgba(0, 0, 0, 0); +} +.split { + border-left: 2px solid black; + margin-left: 10px; + margin-right: 10px; + height: auto; +} + +.calenders { + display: flex; +} + +.footer { + display: flex; + justify-content: space-between; +} + +.error { + align-self: center; + color: red; +} + +.buttons { + float: right; + display: flex; + justify-content: flex-end; +} + +@media only screen and (max-width: 800px) { + .calenders { + flex-direction: column; + } + + .picker { + max-width: 420px; + } +} +.btn { + @apply font-bold py-2 px-4 rounded; +} +.btn-blue { + @apply bg-primary-light text-white; +} +.btn-blue:hover { + @apply bg-primary-medium; +} +.btn-gray:hover { + @apply bg-gray-300; +} +</style> diff --git a/src/components/TimepickerComponents/DatepickerRange/MonthSelector.vue b/src/components/TimepickerComponents/DatepickerRange/MonthSelector.vue new file mode 100644 index 0000000000000000000000000000000000000000..a16a3b840b9efd770d970b0f7313f5e1d0198ec9 --- /dev/null +++ b/src/components/TimepickerComponents/DatepickerRange/MonthSelector.vue @@ -0,0 +1,120 @@ +<template> + <div class="container-c main"> + <div class="button" v-on:click="back"> + <button> + <span> + <svg + class="w-5 h-5" + fill="none" + stroke="currentColor" + viewBox="0 0 24 24" + xmlns="http://www.w3.org/2000/svg" + > + <path + stroke-linecap="round" + stroke-linejoin="round" + stroke-width="1.5" + d="M15 19l-7-7 7-7" + ></path> + </svg> + </span> + </button> + </div> + <div class="container-c text"> + <div class="date"> + {{ monthShort }} + </div> + <div class="date"> + {{ year }} + </div> + </div> + <div class="button" v-on:click="forward"> + <button> + <span> + <svg + class="w-5 h-5" + fill="none" + stroke="currentColor" + viewBox="0 0 24 24" + xmlns="http://www.w3.org/2000/svg" + > + <path + stroke-linecap="round" + stroke-linejoin="round" + stroke-width="1.5" + d="M9 5l7 7-7 7" + ></path> + </svg> + </span> + </button> + </div> + </div> +</template> + +<script> +export default { + props: { + month: { + type: Date, + required: true, + }, + type: { + type: String, + required: true, + }, + }, + computed: { + monthDate() { + return new Date(this.month); + }, + monthShort() { + return this.monthDate + .toLocaleString("default", { month: "short" }) + .toUpperCase(); + }, + year() { + return this.monthDate.getFullYear(); + }, + }, + methods: { + back() { + this.$emit("back", this.type); + }, + forward() { + console.log(this.type); + this.$emit("forward", this.type); + }, + }, +}; +</script> + +<style> +.container-c { + display: flex; +} + +.container-c.main { + justify-content: space-between; + border: 1px solid black; + padding: 5px; + border-radius: 5px; + max-width: 400px; +} + +.text { + justify-content: space-between; + width: 50%; + text-align: center; +} + +.date { + width: 50%; + text-align: center; +} + +.button { + display: flex; + justify-content: center; + align-items: center; +} +</style> diff --git a/src/components/UserProfileComponents/UserListItemCard.vue b/src/components/UserProfileComponents/UserListItemCard.vue index 90180d2df55f207bf11a4ebe1fab595da63bf1f4..b60ef198a9c76157694f67bfe694bfbc24ee2f70 100644 --- a/src/components/UserProfileComponents/UserListItemCard.vue +++ b/src/components/UserProfileComponents/UserListItemCard.vue @@ -2,67 +2,131 @@ <div class="bg-white shadow dark:bg-gray-800 select-none cursor-pointer hover:bg-gray-50 flex items-center p-4" > + <!-- User image --> <div class="h-10 w-10 flex flex-col justify-center items-center mr-4"> <router-link :to="'/profile/' + user.userId"> - <img alt="profil" src="../../assets/defaultUserProfileImage.jpg" /> + <img alt="Profilbilde" src="../../assets/defaultUserProfileImage.jpg" /> </router-link> </div> + + <!-- User name --> <div class="flex-1 pl-1"> <div class="font-medium dark:text-white"> {{ user.firstName }} {{ user.lastName }} </div> </div> + + <!-- User rating --> <div class="hidden md:block flex-auto"> - <rating-component :rating="rating" :ratingType="'Gjennomsnitts rating'" /> + <RatingComponent :rating="rating" :ratingType="'Gjennomsnitts rating'" /> </div> - <div class="flex flex-row justify-center"> - <button - v-if="!admin" - class="px-4 py-2 font-medium tracking-wide text-white capitalize transition-colors duration-200 transform bg-blue-600 rounded-md hover:bg-blue-500 focus:outline-none focus:ring focus:ring-blue-300 focus:ring-opacity-80" - > - Åpne chat - </button> - <button - v-if="admin" - class="px-4 py-2 font-medium tracking-wide text-white capitalize transition-colors duration-200 transform bg-blue-600 rounded-md hover:bg-blue-500 focus:outline-none focus:ring focus:ring-blue-300 focus:ring-opacity-80" + + <!-- Buttons --> + <div class="flex flex-row gap-4"> + <IconButton + v-if="buttons.includes('chat')" + @click="openChatWithUser()" + :text="'Chat'" + :buttonColor="'blue'" > - Fjern bruker - </button> + <ChatIcon + /></IconButton> + + <IconButton + v-if="buttons.includes('kick')" + @click="kickUserFromCommunity()" + :buttonColor="'red'" + :text="'Spark'" + ><BanIcon + /></IconButton> + + <IconButton + v-if="buttons.includes('accept')" + @click="acceptMemberRequest()" + :buttonColor="'green'" + :text="'Godta'" + ><CheckCircleIcon + /></IconButton> + + <IconButton + v-if="buttons.includes('reject')" + @click="rejectMemberRequest()" + :buttonColor="'red'" + :text="'Avslå'" + ><XCircleIcon + /></IconButton> </div> </div> </template> <script> -import { getAverageRating } from "@/utils/apiutil"; -import RatingComponent from "./Rating.vue"; +import RatingComponent from "@/components/UserProfileComponents/Rating.vue"; +import IconButton from "@/components/BaseComponents/IconButton.vue"; +import UserService from "@/services/user.service"; +import CommunityAdminService from "@/services/community-admin.service"; + +import { + ChatIcon, + CheckCircleIcon, + BanIcon, + XCircleIcon, +} from "@heroicons/vue/outline"; export default { name: "UserListItem", data() { return { - rating: this.getRating(), + rating: -1.0, + communityID: -1, }; }, components: { RatingComponent, + IconButton, + ChatIcon, + CheckCircleIcon, + BanIcon, + XCircleIcon, }, props: { user: Object, - admin: Boolean, + buttons: Array, }, methods: { getProfilePicture() { if (this.user.picture != "") { return this.user.picture; } - return "../assets/defaultUserProfileImage.jpg"; + return "@/assets/defaultUserProfileImage.jpg"; + }, + openChatWithUser() { + this.$router.push({ + name: "messages", + params: { userId: this.user.userId }, + }); + }, + kickUserFromCommunity() { + CommunityAdminService.removeUserFromCommunity( + this.communityID, + this.user.userId + ); + }, + acceptMemberRequest() { + CommunityAdminService.acceptUserIntoCommunity( + this.communityID, + this.user.userId + ); }, - async getRating() { - this.rating = await getAverageRating(this.user.userId); + rejectMemberRequest() { + CommunityAdminService.rejectUserFromCommunity( + this.communityID, + this.user.userId + ); }, }, - beforeMount() { - this.getRating(); + async created() { + this.rating = await UserService.getUserRatingAverage(this.user.userId); + this.communityID = this.$route.params.communityID; }, }; </script> diff --git a/src/router/index.js b/src/router/index.js index 4c4149ebb1f57e5b20bb22711fc3447d14cc746b..9266aafc655d4e58888124ce99442d3c7abd79d7 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -100,10 +100,23 @@ const routes = [ component: () => import("../views/CommunityViews/CommunityRequestView.vue"), }, { + beforeEnter: guardRoute, path: "/test", name: "test", component: () => import("../views/TestView.vue"), }, + { + path: "/community/:communityID/admin", + name: "CommunityAdminView", + component: () => import("@/views/CommunityViews/AdminView.vue"), + beforeEnter: guardRoute, + }, + { + path: "/itempage/:id", + name: "ItemInfo", + component: () => import("../views/RentingViews/ItemInfoPageView.vue"), + beforeEnter: guardRoute, + }, ]; const router = createRouter({ diff --git a/src/services/chat.service.js b/src/services/chat.service.js new file mode 100644 index 0000000000000000000000000000000000000000..908763073b06e96ff429dce23678cb3207945869 --- /dev/null +++ b/src/services/chat.service.js @@ -0,0 +1,21 @@ +import axios from "axios"; +import { tokenHeader } from "@/utils/token-utils"; + +const API_URL = process.env.VUE_APP_BASEURL; + +class ChatService { + async getConversations() { + return await axios + .get(API_URL + "chats/users", { + headers: tokenHeader(), + }) + .then((response) => { + return response.data; + }) + .catch((error) => { + console.error(error); + }); + } +} + +export default new ChatService(); diff --git a/src/services/community-admin.service.js b/src/services/community-admin.service.js new file mode 100644 index 0000000000000000000000000000000000000000..091fd94aa59808dd7919b3afc564cfc866312128 --- /dev/null +++ b/src/services/community-admin.service.js @@ -0,0 +1,69 @@ +import axios from "axios"; +import { tokenHeader } from "@/utils/token-utils"; + +const API_URL = process.env.VUE_APP_BASEURL; + +/** + * Service class acting as a middle layer between our components and the API + */ +class CommunityAdminService { + async isUserAdmin(communityID) { + return await axios + .get(API_URL + "communities/" + communityID + "/user/admin", { + headers: tokenHeader(), + }) + .then((res) => { + return res.data; + }); + } + + //TODO + async acceptUserIntoCommunity(communityID, userID) { + return await axios.post( + API_URL + "communities/" + communityID + "/requests", + { params: { userId: userID } } + ); + } + + //TODO + async rejectUserFromCommunity(communityID, userID) { + return await axios.patch( + API_URL + "communitites/" + communityID + "/requests/reject", + { + params: { userId: userID }, + } + ); + } + + /** + * Method that kicks a user from a community + * @param {int} communityID the community to remove the user from + * @param {int} userID the user to remove + * @returns TODO + */ + async removeUserFromCommunity(communityID, userID) { + return await axios.patch( + API_URL + "communities/" + communityID + "/kick", + null, + { + params: { userId: userID }, + } + ); + } + + /** + * Method to delete a community + * @param {int} communityID id of the community to delete. + * @returns TODO + */ + async deleteCommunity(communityID) { + return await axios.post( + API_URL + "communities/" + communityID + "/remove", + { + headers: tokenHeader(), + } + ); + } +} + +export default new CommunityAdminService(); diff --git a/src/services/community.service.js b/src/services/community.service.js new file mode 100644 index 0000000000000000000000000000000000000000..15ab272a15178bfb18d742dd0363bb92a4ff3617 --- /dev/null +++ b/src/services/community.service.js @@ -0,0 +1,45 @@ +import { tokenHeader } from "@/utils/token-utils"; +import axios from "axios"; + +const API_URL = process.env.VUE_APP_BASEURL; + +class CommunityService { + async getCommunity(communityId) { + return await axios + .get(API_URL + "community/" + communityId, { + headers: tokenHeader(), + }) + .then((response) => { + return response.data; + }) + .catch((error) => { + console.error(error); + }); + } + + async getPublicCommunities() { + return await axios + .get(API_URL + "communities") + .then((response) => { + return response.data; + }) + .catch((error) => { + console.error(error); + }); + } + + async getCommunityMembers(communityId) { + return await axios + .get(API_URL + "community/" + communityId + "/members", { + headers: tokenHeader(), + }) + .then((response) => { + return response.data; + }) + .catch((error) => { + console.error(error); + }); + } +} + +export default new CommunityService(); diff --git a/src/services/user.service.js b/src/services/user.service.js new file mode 100644 index 0000000000000000000000000000000000000000..6cb07ac7f610488d23d1c313577df686b19a72b9 --- /dev/null +++ b/src/services/user.service.js @@ -0,0 +1,32 @@ +// import { tokenHeader } from "@/utils/token-utils"; +import axios from "axios"; + +const API_URL = process.env.VUE_APP_BASEURL; + +class UserService { + async getUserFromId(userId) { + return await axios + .get(API_URL + "/users/" + userId + "/profile") + .then((res) => { + return res.data; + }) + .catch((err) => console.log(err)); + } + + async getUserRatingAverage(userId) { + return await axios + .get(API_URL + "rating/" + userId + "/average") + .then((res) => { + return res.data; + }) + .catch((err) => console.log(err)); + } + + //TODO + getUserRatingAsOwner() {} + + //TODO + getUserRatingAsRenter() {} +} + +export default new UserService(); diff --git a/src/utils/apiutil.js b/src/utils/apiutil.js index c4f46d7c2648a70d1e4649c16c1bec2639f42bc4..daac6a7fc495ca0716e84b93c0da6a49b7baf183 100644 --- a/src/utils/apiutil.js +++ b/src/utils/apiutil.js @@ -121,6 +121,20 @@ export function postNewgroup(groupInfo) { return error; }); } +export function postNewRent(rentInfo) { + return axios + .post(API_URL + "renting/renter/save", rentInfo, { + headers: tokenHeader(), + }) + .then((response) => { + console.log("poster: " + response.data); + return response; + }) + .catch((error) => { + console.log(error.response); + return error; + }); +} export function getMyGroups() { return axios @@ -148,8 +162,35 @@ export function getVisibleGroups() { }); } -export async function GetCommunity(communityID) { +export function getItem(itemid) { return axios + .get(API_URL + "listing/" + itemid, { + headers: tokenHeader(), + }) + .then((response) => { + return response.data; + }) + .catch((error) => { + console.error(error); + }); +} + +export async function getItemPictures(itemid) { + let res = await axios + .get(API_URL + "listing/" + itemid + "/pictures", { + headers: tokenHeader(), + }) + .then((response) => { + return response.data; + }) + .catch((error) => { + console.error(error); + }); + return res; +} + +export async function GetCommunity(communityID) { + return await axios .get(API_URL + "community/" + communityID, { headers: tokenHeader(), }) diff --git a/src/views/ChatViews/ChatView.vue b/src/views/ChatViews/ChatView.vue index a8c5eef7738240b1779ff766608661cd8bdbb953..1b7cfad3b7730e1991953a8f5340ff45669f568b 100644 --- a/src/views/ChatViews/ChatView.vue +++ b/src/views/ChatViews/ChatView.vue @@ -1,32 +1,40 @@ <template> - <div class="min-h-full"> + <div class="flex flex-col h-full overflow-hidden border-2"> + <div class="flex flex-row h-full border-2 bg-gray-50"> + <div class="basis-1/3"> + <h1 class="text-center text-l">Mine samtaler</h1> + <ul v-if="conversations" class="border-2"> + <li + v-for="conversation in conversations" + :key="conversation.recipient.userId" + > + <ChatProfile :conversation="conversation" @recipient="selectUser" /> + </li> + </ul> + </div> + <div class="basis-2/3"> + <CurrentChat v-if="selected" :recipient="selected" /> + </div> + </div> + </div> + <!-- <div class="min-h-full"> <div class="border rounded grid grid-cols-3 w-full"> <div class="border-r border-gray-300 col-span-1"> <ul class="hidden sm:block overflow-auto h-full"> <h2 class="my-2 mb-2 ml-2 text-lg text-gray-600">Chats</h2> - <li> - <ChatProfile - v-for="(conversation, i) in conversations" - :conversation="conversation" - :key="i" - @recipient="selectUser" - ></ChatProfile> + <li v-if="conversations"> </li> </ul> </div> - <CurrentChat - v-if="selected" - :recipient="selected" - :key="key" - ></CurrentChat> </div> - </div> + </div> --> </template> <script> import ChatProfile from "@/components/ChatComponents/ChatProfile.vue"; import CurrentChat from "@/components/ChatComponents/CurrentChat.vue"; import { parseCurrentUser } from "@/utils/token-utils"; +import ChatService from "@/services/chat.service"; export default { components: { @@ -40,14 +48,14 @@ export default { }; }, computed: { - userID() { - return parseCurrentUser().accountId; - }, key() { return this.selected.userId || "ERROR"; }, }, methods: { + userID() { + return parseCurrentUser().accountId; + }, selectUser(value) { const userid = value; this.conversations.find((conversation) => { @@ -60,17 +68,10 @@ export default { }, }, async created() { - const token = this.$store.state.user.token; - // Get all conversations from api with /chats/users - const response = await fetch(`${process.env.VUE_APP_BASEURL}chats/users`, { - method: "GET", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${token}`, - }, - }); // add error handling - const res = await response.json(); - this.conversations = res; + this.conversations = await ChatService.getConversations(this.userID()); + if (this.$route.params.userId !== null) { + this.selectUser(this.$route.params.userId); + } }, }; </script> diff --git a/src/views/CommunityViews/AdminView.vue b/src/views/CommunityViews/AdminView.vue new file mode 100644 index 0000000000000000000000000000000000000000..cd801ebe72f747c0d0939551eeec4606dbe5595f --- /dev/null +++ b/src/views/CommunityViews/AdminView.vue @@ -0,0 +1,53 @@ +<template> + <CommunityHeader :admin="true" class="mb-5" /> + <div + class="flex border-b border-gray-200 dark:border-gray-700 overflow-y-hidden" + > + <button + v-for="(tab, index) in tabs" + :key="tab" + @click="changeTab(index)" + class="h-10 px-4 py-2 -mb-px text-sm text-center bg-transparent border-b-2 sm:text-base whitespace-nowrap focus:outline-none" + :class="[currentTab === index ? activeClasses : inactiveClasses]" + > + {{ tab }} + </button> + </div> + <MemberList :buttons="['chat', 'kick']" v-if="currentTab === 0" /> + <MemberList :buttons="['accept', 'reject']" v-if="currentTab === 1" /> + <CommunitySettings v-if="currentTab === 2" /> +</template> + +<script> +import MemberList from "@/components/CommunityComponents/MemberList.vue"; +import CommunitySettings from "@/components/CommunityComponents/CommunitySettings.vue"; +import CommunityHeader from "@/components/CommunityComponents/CommunityHeader.vue"; + +export default { + name: "CommunityAdminView", + components: { + CommunityHeader, + MemberList, + CommunitySettings, + }, + data() { + return { + tabs: ["Medlemsliste", "Medlemsforespørsler", "Felleskap-innstillinger"], + currentTab: 0, //Currently selected tab (default 0 "Medlemsliste") + }; + }, + methods: { + changeTab(index) { + this.currentTab = index; + }, + }, + computed: { + activeClasses() { + return "text-primary-medium border-primary-medium dark:border-primary-light dark:text-primary-light"; + }, + inactiveClasses() { + return "text-gray-700 border-transparent dark:text-white cursor-base hover:border-gray-400"; + }, + }, +}; +</script> diff --git a/src/views/CommunityViews/CommunityView.vue b/src/views/CommunityViews/CommunityView.vue index b6643a00e413da9d9195fd6357c82876f19d5211..8755c39e632108ccca0f9ed1ce84f5db986e8105 100644 --- a/src/views/CommunityViews/CommunityView.vue +++ b/src/views/CommunityViews/CommunityView.vue @@ -42,11 +42,19 @@ export default { if (!this.loggedIn) return; this.myCommunities = await getMyGroups(); - - // Remove all of the user's communities from the public communities arrays - this.publicCommunities = this.publicCommunities.filter( - (val) => !this.myCommunities.includes(val) - ); + }, + mounted() { + // Double loop is bad; find a better way to do this + for (var i = 0; i < this.publicCommunities.length; i++) { + for (var j = 0; j < this.myCommunities.length; j++) { + if ( + this.publicCommunities[i].communityId === + this.myCommunities[j].communityId + ) { + this.publicCommunities.splice(i, 1); + } + } + } }, }; </script> diff --git a/src/views/CommunityViews/MemberListView.vue b/src/views/CommunityViews/MemberListView.vue index 9f63b476d52810f37e88b726803ca54b6ccc9266..e7bdcd94166f21e4dc214045bd73f5a1ead7e460 100644 --- a/src/views/CommunityViews/MemberListView.vue +++ b/src/views/CommunityViews/MemberListView.vue @@ -1,12 +1,15 @@ <template> - <MemberList /> + <CommunityHeader :admin="false" class="mb-5 mt-5" /> + <MemberList :buttons="['chat']" /> </template> <script> +import CommunityHeader from "@/components/CommunityComponents/CommunityHeader"; import MemberList from "@/components/CommunityComponents/MemberList.vue"; export default { components: { + CommunityHeader, MemberList, }, }; diff --git a/src/views/RentingViews/ItemInfoPageView.vue b/src/views/RentingViews/ItemInfoPageView.vue new file mode 100644 index 0000000000000000000000000000000000000000..c08362fb9094c59ca4b1419893314bc609648c38 --- /dev/null +++ b/src/views/RentingViews/ItemInfoPageView.vue @@ -0,0 +1,15 @@ +<template> + <ItemInfo></ItemInfo> +</template> + +<script> +import ItemInfo from "@/components/RentingComponents/ItemInfo"; +export default { + name: "ItemInfoPage.vue", + components: { + ItemInfo, + }, +}; +</script> + +<style scoped></style> diff --git a/tailwind.config.js b/tailwind.config.js index 1108079401f30bb7cfcf539149d36ff105100116..65380e429009105b923a55266f6b301464b97a88 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -1,6 +1,10 @@ module.exports = { darkMode: "class", - content: ["./index.html", "./src/**/*.{vue,js,ts,jsx,tsx}"], + content: [ + "./index.html", + "./src/**/*.{vue,js,ts,jsx,tsx}", + "./node_modules/tw-elements/dist/js/**/*.js", + ], theme: { colors: { white: "#fff", @@ -26,10 +30,22 @@ module.exports = { light: "#653273", dark: "#731050", }, - error: "#E23636", - warn: "#EDB95E", - success: "#82DD55", + error: { + light: "#EF4444", + medium: "#DC2626", + dark: "#B91C1C", + }, + warn: { + light: "#FDE047", + medium: "#FACC15", + dark: "#EAB308", + }, + success: { + light: "#22C55E", + medium: "#16A34A", + dark: "#15803D", + }, }, }, - plugins: [], + plugins: [require("tw-elements/dist/plugin")], }; diff --git a/tests/unit/apiutil-communityHome-mock.spec.js b/tests/unit/apiutil-communityHome-mock.spec.js new file mode 100644 index 0000000000000000000000000000000000000000..4ab75d6469cf79e31546054eebae45ce5976284d --- /dev/null +++ b/tests/unit/apiutil-communityHome-mock.spec.js @@ -0,0 +1,55 @@ +import { GetCommunity, GetListingsInCommunity } from "@/utils/apiutil"; +import axios from "axios"; + +jest.mock("axios"); + +describe("testing mocking of apiutil.js", () => { + it("check that existing group returns correctly", async () => { + const expectedResponse = { + communityId: 4040, + name: "Fisken i vannet", + description: "For vi som liker fjell fisk", + visibility: 1, + location: "Bergen brygge", + picture: "fish blub blub", + }; + + axios.get.mockImplementation(() => + Promise.resolve({ data: expectedResponse }) + ); + + const communityResponse = await GetCommunity(4040); + expect(communityResponse.name).toBe(expectedResponse.name); + }); + + it("check that existing group returns correct listings", async () => { + const expectedResponse = { + item1: { + title: "Fiskekurs", + description: "Fisking og sånn", + pricePerDay: 200, + address: "Vannet", + userID: 6, + categoryNames: null, + communityIDs: null, + }, + + item2: { + title: "TestFraFrontend", + description: "oslo", + pricePerDay: 500, + address: "oslo", + userID: 1, + categoryNames: null, + communityIDs: null, + }, + }; + + axios.get.mockImplementation(() => + Promise.resolve({ data: expectedResponse }) + ); + + const communityItemResponse = await GetListingsInCommunity(4040); + expect(communityItemResponse).toBe(expectedResponse); + }); +}); diff --git a/tests/unit/component-tests/community-component-tests/__snapshots__/item-card.spec.js.snap b/tests/unit/component-tests/community-component-tests/__snapshots__/item-card.spec.js.snap deleted file mode 100644 index 232516e6546b4be3f3852f589671a41a1e929911..0000000000000000000000000000000000000000 --- a/tests/unit/component-tests/community-component-tests/__snapshots__/item-card.spec.js.snap +++ /dev/null @@ -1,39 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`ItemCard component renders correctly 1`] = ` -<div - class="mt-5" -> - <div - class="w-4/5 rounded bg-gray-200" - > - <img - alt="Item image" - class="w-full" - src="String" - /> - <div - class="p-1 m-1" - > - <p - class="text-gray-700 text-xs font-bold" - id="adress" - > - String - </p> - <p - class="font-bold text-sm" - id="title" - > - String - </p> - <p - class="text-gray-700 text-xs" - id="price" - > - 0 kr - </p> - </div> - </div> -</div> -`;