Update deps; convert to "script setup" tags
This commit is contained in:
parent
2eefb44f16
commit
dd549cf5f1
@ -4,16 +4,26 @@ module.exports = {
|
||||
node: true
|
||||
},
|
||||
extends: [
|
||||
'plugin:vue/vue3-essential',
|
||||
'@vue/standard',
|
||||
'@vue/typescript/recommended'
|
||||
"plugin:vue/vue3-essential",
|
||||
"@vue/standard",
|
||||
"@vue/typescript/recommended"
|
||||
],
|
||||
parserOptions: {
|
||||
ecmaVersion: 2020
|
||||
},
|
||||
globals: {
|
||||
defineProps: "readonly",
|
||||
defineEmits: "readonly",
|
||||
defineExpose: "readonly",
|
||||
withDefaults: "readonly"
|
||||
},
|
||||
rules: {
|
||||
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
|
||||
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
|
||||
'vue/no-multiple-template-root': 'off'
|
||||
"no-console": process.env.NODE_ENV === "production" ? "warn" : "off",
|
||||
"no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off",
|
||||
"vue/no-multiple-template-root": "off",
|
||||
"vue/script-setup-uses-vars": 1,
|
||||
"quotes": ["error", "double", { avoidEscape: true }],
|
||||
"func-call-spacing": "off",
|
||||
"@typescript-eslint/no-unused-vars": "off"
|
||||
}
|
||||
}
|
||||
|
661
src/JobsJobsJobs/App/package-lock.json
generated
661
src/JobsJobsJobs/App/package-lock.json
generated
@ -264,7 +264,8 @@
|
||||
"@babel/helper-validator-identifier": {
|
||||
"version": "7.14.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.5.tgz",
|
||||
"integrity": "sha512-5lsetuxCLilmVGyiLEfoHBRX8UCFD+1m2x3Rj97WrW3V7H3u4RWRXA4evMjImCsin2J2YT0QaVDGf+z8ondbAg=="
|
||||
"integrity": "sha512-5lsetuxCLilmVGyiLEfoHBRX8UCFD+1m2x3Rj97WrW3V7H3u4RWRXA4evMjImCsin2J2YT0QaVDGf+z8ondbAg==",
|
||||
"dev": true
|
||||
},
|
||||
"@babel/helper-validator-option": {
|
||||
"version": "7.14.5",
|
||||
@ -309,7 +310,8 @@
|
||||
"@babel/parser": {
|
||||
"version": "7.14.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.7.tgz",
|
||||
"integrity": "sha512-X67Z5y+VBJuHB/RjwECp8kSl5uYi0BvRbNeWqkaJCVh+LiTPl19WBUfG627psSgp9rSf6ojuXghQM3ha6qHHdA=="
|
||||
"integrity": "sha512-X67Z5y+VBJuHB/RjwECp8kSl5uYi0BvRbNeWqkaJCVh+LiTPl19WBUfG627psSgp9rSf6ojuXghQM3ha6qHHdA==",
|
||||
"dev": true
|
||||
},
|
||||
"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": {
|
||||
"version": "7.14.5",
|
||||
@ -1096,6 +1098,7 @@
|
||||
"version": "7.14.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.5.tgz",
|
||||
"integrity": "sha512-M/NzBpEL95I5Hh4dwhin5JlE7EzO5PHMAuzjxss3tiOBD46KfQvVedN/3jEPZvdRvtsK2222XfdHogNIttFgcg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/helper-validator-identifier": "^7.14.5",
|
||||
"to-fast-properties": "^2.0.0"
|
||||
@ -1361,9 +1364,9 @@
|
||||
}
|
||||
},
|
||||
"@types/bootstrap": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/bootstrap/-/bootstrap-5.1.1.tgz",
|
||||
"integrity": "sha512-W/fEBlqwaJFh+3sCz/H88LPsLt/zLsEECFlrAOkrRPjWuo/ETl8u0JefIerCdc8+WukowQS1f60eIJOwkCBwhg==",
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/bootstrap/-/bootstrap-5.1.2.tgz",
|
||||
"integrity": "sha512-dSQvMi2dMyNwJU6LZjP0pimuBowsMUvGScYdfqqeiDUoj9TxXZCpfu0cTl94U0Zvw/tdH9j/9ToOhi4LKNLZhg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@popperjs/core": "^2.9.2",
|
||||
@ -1389,6 +1392,15 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"@types/dompurify": {
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-2.2.3.tgz",
|
||||
"integrity": "sha512-CLtc2mZK8+axmrz1JqtpklO/Kvn38arGc8o1l3UVopZaXXuer9ONdZwJ/9f226GrhRLtUmLr9WrvZsRSNpS8og==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/trusted-types": "*"
|
||||
}
|
||||
},
|
||||
"@types/estree": {
|
||||
"version": "0.0.48",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.48.tgz",
|
||||
@ -1452,10 +1464,16 @@
|
||||
"integrity": "sha512-YSBPTLTVm2e2OoQIDYx8HaeWJ5tTToLH67kXR7zYNGupXMEHa2++G8k+DczX2cFVgalypqtyZIcU19AFcmOpmg==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/json5": {
|
||||
"version": "0.0.29",
|
||||
"resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
|
||||
"integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=",
|
||||
"dev": true
|
||||
},
|
||||
"@types/marked": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/marked/-/marked-2.0.4.tgz",
|
||||
"integrity": "sha512-L9VRSe0Id8xbPL99mUo/4aKgD7ZoRwFZqUQScNKHi2pFjF9ZYSMNShUHD6VlMT6J/prQq0T1mxuU25m3R7dFzg==",
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/marked/-/marked-2.0.5.tgz",
|
||||
"integrity": "sha512-shRZ7XnYFD/8n8zSjKvFdto1QNSf4tONZIlNEZGrJe8GsOE8DL/hG1Hbl8gZlfLnjS7+f5tZGIaTgfpyW38h4w==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/mime": {
|
||||
@ -1541,6 +1559,12 @@
|
||||
"integrity": "sha512-ipixuVrh2OdNmauvtT51o3d8z12p6LtFW9in7U79der/kwejjdNchQC5UMn5u/KxNoM7VHHOs/l8KS8uHxhODQ==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/trusted-types": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.2.tgz",
|
||||
"integrity": "sha512-F5DIZ36YVLE+PN+Zwws4kJogq47hNgX3Nx6WyDJ3kcplxyke3XIzB8uK5n/Lpm1HBsbGzd6nmGehL8cPekP+Tg==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/uglify-js": {
|
||||
"version": "3.13.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/uglify-js/-/uglify-js-3.13.1.tgz",
|
||||
@ -1619,13 +1643,13 @@
|
||||
}
|
||||
},
|
||||
"@typescript-eslint/eslint-plugin": {
|
||||
"version": "4.29.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.29.1.tgz",
|
||||
"integrity": "sha512-AHqIU+SqZZgBEiWOrtN94ldR3ZUABV5dUG94j8Nms9rQnHFc8fvDOue/58K4CFz6r8OtDDc35Pw9NQPWo0Ayrw==",
|
||||
"version": "4.29.3",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.29.3.tgz",
|
||||
"integrity": "sha512-tBgfA3K/3TsZY46ROGvoRxQr1wBkclbVqRQep97MjVHJzcRBURRY3sNFqLk0/Xr//BY5hM9H2p/kp+6qim85SA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@typescript-eslint/experimental-utils": "4.29.1",
|
||||
"@typescript-eslint/scope-manager": "4.29.1",
|
||||
"@typescript-eslint/experimental-utils": "4.29.3",
|
||||
"@typescript-eslint/scope-manager": "4.29.3",
|
||||
"debug": "^4.3.1",
|
||||
"functional-red-black-tree": "^1.0.1",
|
||||
"regexpp": "^3.1.0",
|
||||
@ -1669,15 +1693,15 @@
|
||||
}
|
||||
},
|
||||
"@typescript-eslint/experimental-utils": {
|
||||
"version": "4.29.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.29.1.tgz",
|
||||
"integrity": "sha512-kl6QG6qpzZthfd2bzPNSJB2YcZpNOrP6r9jueXupcZHnL74WiuSjaft7WSu17J9+ae9zTlk0KJMXPUj0daBxMw==",
|
||||
"version": "4.29.3",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.29.3.tgz",
|
||||
"integrity": "sha512-ffIvbytTVWz+3keg+Sy94FG1QeOvmV9dP2YSdLFHw/ieLXWCa3U1TYu8IRCOpMv2/SPS8XqhM1+ou1YHsdzKrg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/json-schema": "^7.0.7",
|
||||
"@typescript-eslint/scope-manager": "4.29.1",
|
||||
"@typescript-eslint/types": "4.29.1",
|
||||
"@typescript-eslint/typescript-estree": "4.29.1",
|
||||
"@typescript-eslint/scope-manager": "4.29.3",
|
||||
"@typescript-eslint/types": "4.29.3",
|
||||
"@typescript-eslint/typescript-estree": "4.29.3",
|
||||
"eslint-scope": "^5.1.1",
|
||||
"eslint-utils": "^3.0.0"
|
||||
},
|
||||
@ -1695,41 +1719,41 @@
|
||||
}
|
||||
},
|
||||
"@typescript-eslint/parser": {
|
||||
"version": "4.29.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.29.1.tgz",
|
||||
"integrity": "sha512-3fL5iN20hzX3Q4OkG7QEPFjZV2qsVGiDhEwwh+EkmE/w7oteiOvUNzmpu5eSwGJX/anCryONltJ3WDmAzAoCMg==",
|
||||
"version": "4.29.3",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.29.3.tgz",
|
||||
"integrity": "sha512-jrHOV5g2u8ROghmspKoW7pN8T/qUzk0+DITun0MELptvngtMrwUJ1tv5zMI04CYVEUsSrN4jV7AKSv+I0y0EfQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@typescript-eslint/scope-manager": "4.29.1",
|
||||
"@typescript-eslint/types": "4.29.1",
|
||||
"@typescript-eslint/typescript-estree": "4.29.1",
|
||||
"@typescript-eslint/scope-manager": "4.29.3",
|
||||
"@typescript-eslint/types": "4.29.3",
|
||||
"@typescript-eslint/typescript-estree": "4.29.3",
|
||||
"debug": "^4.3.1"
|
||||
}
|
||||
},
|
||||
"@typescript-eslint/scope-manager": {
|
||||
"version": "4.29.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.29.1.tgz",
|
||||
"integrity": "sha512-Hzv/uZOa9zrD/W5mftZa54Jd5Fed3tL6b4HeaOpwVSabJK8CJ+2MkDasnX/XK4rqP5ZTWngK1ZDeCi6EnxPQ7A==",
|
||||
"version": "4.29.3",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.29.3.tgz",
|
||||
"integrity": "sha512-x+w8BLXO7iWPkG5mEy9bA1iFRnk36p/goVlYobVWHyDw69YmaH9q6eA+Fgl7kYHmFvWlebUTUfhtIg4zbbl8PA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@typescript-eslint/types": "4.29.1",
|
||||
"@typescript-eslint/visitor-keys": "4.29.1"
|
||||
"@typescript-eslint/types": "4.29.3",
|
||||
"@typescript-eslint/visitor-keys": "4.29.3"
|
||||
}
|
||||
},
|
||||
"@typescript-eslint/types": {
|
||||
"version": "4.29.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.29.1.tgz",
|
||||
"integrity": "sha512-Jj2yu78IRfw4nlaLtKjVaGaxh/6FhofmQ/j8v3NXmAiKafbIqtAPnKYrf0sbGjKdj0hS316J8WhnGnErbJ4RCA==",
|
||||
"version": "4.29.3",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.29.3.tgz",
|
||||
"integrity": "sha512-s1eV1lKNgoIYLAl1JUba8NhULmf+jOmmeFO1G5MN/RBCyyzg4TIOfIOICVNC06lor+Xmy4FypIIhFiJXOknhIg==",
|
||||
"dev": true
|
||||
},
|
||||
"@typescript-eslint/typescript-estree": {
|
||||
"version": "4.29.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.29.1.tgz",
|
||||
"integrity": "sha512-lIkkrR9E4lwZkzPiRDNq0xdC3f2iVCUjw/7WPJ4S2Sl6C3nRWkeE1YXCQ0+KsiaQRbpY16jNaokdWnm9aUIsfw==",
|
||||
"version": "4.29.3",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.29.3.tgz",
|
||||
"integrity": "sha512-45oQJA0bxna4O5TMwz55/TpgjX1YrAPOI/rb6kPgmdnemRZx/dB0rsx+Ku8jpDvqTxcE1C/qEbVHbS3h0hflag==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@typescript-eslint/types": "4.29.1",
|
||||
"@typescript-eslint/visitor-keys": "4.29.1",
|
||||
"@typescript-eslint/types": "4.29.3",
|
||||
"@typescript-eslint/visitor-keys": "4.29.3",
|
||||
"debug": "^4.3.1",
|
||||
"globby": "^11.0.3",
|
||||
"is-glob": "^4.0.1",
|
||||
@ -1882,12 +1906,12 @@
|
||||
}
|
||||
},
|
||||
"@typescript-eslint/visitor-keys": {
|
||||
"version": "4.29.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.29.1.tgz",
|
||||
"integrity": "sha512-zLqtjMoXvgdZY/PG6gqA73V8BjqPs4af1v2kiiETBObp+uC6gRYnJLmJHxC0QyUrrHDLJPIWNYxoBV3wbcRlag==",
|
||||
"version": "4.29.3",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.29.3.tgz",
|
||||
"integrity": "sha512-MGGfJvXT4asUTeVs0Q2m+sY63UsfnA+C/FDgBKV3itLBmM9H0u+URcneePtkd0at1YELmZK6HSolCqM4Fzs6yA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@typescript-eslint/types": "4.29.1",
|
||||
"@typescript-eslint/types": "4.29.3",
|
||||
"eslint-visitor-keys": "^2.0.0"
|
||||
}
|
||||
},
|
||||
@ -2457,17 +2481,36 @@
|
||||
}
|
||||
},
|
||||
"@vue/compiler-core": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.2.2.tgz",
|
||||
"integrity": "sha512-QhCI0ZU5nAR0LMcLgzW3v75374tIrHGp8XG5CzJS7Nsy+iuignbE4MZ2XJfh5TGIrtpuzfWA4eTIfukZf/cRdg==",
|
||||
"version": "3.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.2.6.tgz",
|
||||
"integrity": "sha512-vbwnz7+OhtLO5p5i630fTuQCL+MlUpEMTKHuX+RfetQ+3pFCkItt2JUH+9yMaBG2Hkz6av+T9mwN/acvtIwpbw==",
|
||||
"requires": {
|
||||
"@babel/parser": "^7.12.0",
|
||||
"@babel/types": "^7.12.0",
|
||||
"@vue/shared": "3.2.2",
|
||||
"estree-walker": "^2.0.1",
|
||||
"@babel/parser": "^7.15.0",
|
||||
"@babel/types": "^7.15.0",
|
||||
"@vue/shared": "3.2.6",
|
||||
"estree-walker": "^2.0.2",
|
||||
"source-map": "^0.6.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/helper-validator-identifier": {
|
||||
"version": "7.14.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.9.tgz",
|
||||
"integrity": "sha512-pQYxPY0UP6IHISRitNe8bsijHex4TWZXi2HwKVsjPiltzlhse2znVcm9Ace510VT1kxIHjGJCZZQBX2gJDbo0g=="
|
||||
},
|
||||
"@babel/parser": {
|
||||
"version": "7.15.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.15.3.tgz",
|
||||
"integrity": "sha512-O0L6v/HvqbdJawj0iBEfVQMc3/6WP+AeOsovsIgBFyJaG+W2w7eqvZB7puddATmWuARlm1SX7DwxJ/JJUnDpEA=="
|
||||
},
|
||||
"@babel/types": {
|
||||
"version": "7.15.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.15.0.tgz",
|
||||
"integrity": "sha512-OBvfqnllOIdX4ojTHpwZbpvz4j3EWyjkZEdmjH0/cgsd6QOdSgU8rLSk6ard/pcW7rlmjdVSX/AWOaORR1uNOQ==",
|
||||
"requires": {
|
||||
"@babel/helper-validator-identifier": "^7.14.9",
|
||||
"to-fast-properties": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"source-map": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||
@ -2476,29 +2519,30 @@
|
||||
}
|
||||
},
|
||||
"@vue/compiler-dom": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.2.2.tgz",
|
||||
"integrity": "sha512-ggcc+NV/ENIE0Uc3TxVE/sKrhYVpLepMAAmEiQ047332mbKOvUkowz4TTFZ+YkgOIuBOPP0XpCxmCMg7p874mA==",
|
||||
"version": "3.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.2.6.tgz",
|
||||
"integrity": "sha512-+a/3oBAzFIXhHt8L5IHJOTP4a5egzvpXYyi13jR7CUYOR1S+Zzv7vBWKYBnKyJLwnrxTZnTQVjeHCgJq743XKg==",
|
||||
"requires": {
|
||||
"@vue/compiler-core": "3.2.2",
|
||||
"@vue/shared": "3.2.2"
|
||||
"@vue/compiler-core": "3.2.6",
|
||||
"@vue/shared": "3.2.6"
|
||||
}
|
||||
},
|
||||
"@vue/compiler-sfc": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.2.2.tgz",
|
||||
"integrity": "sha512-hrtqpQ5L6IPn5v7yVRo7uvLcQxv0z1+KBjZBWMBOcrXz4t+PKUxU/SWd6Tl9T8FDmYlunzKUh6lcx+2CLo6f5A==",
|
||||
"version": "3.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.2.6.tgz",
|
||||
"integrity": "sha512-Ariz1eDsf+2fw6oWXVwnBNtfKHav72RjlWXpEgozYBLnfRPzP+7jhJRw4Nq0OjSsLx2HqjF3QX7HutTjYB0/eA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/parser": "^7.13.9",
|
||||
"@babel/types": "^7.13.0",
|
||||
"@babel/parser": "^7.15.0",
|
||||
"@babel/types": "^7.15.0",
|
||||
"@types/estree": "^0.0.48",
|
||||
"@vue/compiler-core": "3.2.2",
|
||||
"@vue/compiler-dom": "3.2.2",
|
||||
"@vue/compiler-ssr": "3.2.2",
|
||||
"@vue/shared": "3.2.2",
|
||||
"@vue/compiler-core": "3.2.6",
|
||||
"@vue/compiler-dom": "3.2.6",
|
||||
"@vue/compiler-ssr": "3.2.6",
|
||||
"@vue/ref-transform": "3.2.6",
|
||||
"@vue/shared": "3.2.6",
|
||||
"consolidate": "^0.16.0",
|
||||
"estree-walker": "^2.0.1",
|
||||
"estree-walker": "^2.0.2",
|
||||
"hash-sum": "^2.0.0",
|
||||
"lru-cache": "^5.1.1",
|
||||
"magic-string": "^0.25.7",
|
||||
@ -2509,6 +2553,28 @@
|
||||
"source-map": "^0.6.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/helper-validator-identifier": {
|
||||
"version": "7.14.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.9.tgz",
|
||||
"integrity": "sha512-pQYxPY0UP6IHISRitNe8bsijHex4TWZXi2HwKVsjPiltzlhse2znVcm9Ace510VT1kxIHjGJCZZQBX2gJDbo0g==",
|
||||
"dev": true
|
||||
},
|
||||
"@babel/parser": {
|
||||
"version": "7.15.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.15.3.tgz",
|
||||
"integrity": "sha512-O0L6v/HvqbdJawj0iBEfVQMc3/6WP+AeOsovsIgBFyJaG+W2w7eqvZB7puddATmWuARlm1SX7DwxJ/JJUnDpEA==",
|
||||
"dev": true
|
||||
},
|
||||
"@babel/types": {
|
||||
"version": "7.15.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.15.0.tgz",
|
||||
"integrity": "sha512-OBvfqnllOIdX4ojTHpwZbpvz4j3EWyjkZEdmjH0/cgsd6QOdSgU8rLSk6ard/pcW7rlmjdVSX/AWOaORR1uNOQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/helper-validator-identifier": "^7.14.9",
|
||||
"to-fast-properties": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"consolidate": {
|
||||
"version": "0.16.0",
|
||||
"resolved": "https://registry.npmjs.org/consolidate/-/consolidate-0.16.0.tgz",
|
||||
@ -2538,13 +2604,13 @@
|
||||
}
|
||||
},
|
||||
"@vue/compiler-ssr": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.2.2.tgz",
|
||||
"integrity": "sha512-rVl1agMFhdEN3Go0bCriXo+3cysxKIuRP0yh1Wd8ysRrKfAmokyDhUA8PrGSq2Ymj/LdZTh+4OKfj3p2+C+hlA==",
|
||||
"version": "3.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.2.6.tgz",
|
||||
"integrity": "sha512-A7IKRKHSyPnTC4w1FxHkjzoyjXInsXkcs/oX22nBQ+6AWlXj2Tt1le96CWPOXy5vYlsTYkF1IgfBaKIdeN/39g==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@vue/compiler-dom": "3.2.2",
|
||||
"@vue/shared": "3.2.2"
|
||||
"@vue/compiler-dom": "3.2.6",
|
||||
"@vue/shared": "3.2.6"
|
||||
}
|
||||
},
|
||||
"@vue/component-compiler-utils": {
|
||||
@ -2626,36 +2692,57 @@
|
||||
"dev": true
|
||||
},
|
||||
"@vue/reactivity": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.2.2.tgz",
|
||||
"integrity": "sha512-IHjhtmrhK6dzacj/EnLQDWOaA3HuzzVk6w84qgV8EpS4uWGIJXiRalMRg6XvGW2ykJvIl3pLsF0aBFlTMRiLOA==",
|
||||
"version": "3.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.2.6.tgz",
|
||||
"integrity": "sha512-8vIDD2wpCnYisNNZjmcIj+Rixn0uhZNY3G1vzlgdVdLygeRSuFjkmnZk6WwvGzUWpKfnG0e/NUySM3mVi59hAA==",
|
||||
"requires": {
|
||||
"@vue/shared": "3.2.2"
|
||||
"@vue/shared": "3.2.6"
|
||||
}
|
||||
},
|
||||
"@vue/ref-transform": {
|
||||
"version": "3.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@vue/ref-transform/-/ref-transform-3.2.6.tgz",
|
||||
"integrity": "sha512-ie39+Y4nbirDLvH+WEq6Eo/l3n3mFATayqR+kEMSphrtMW6Uh/eEMx1Gk2Jnf82zmj3VLRq7dnmPx72JLcBYkQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/parser": "^7.15.0",
|
||||
"@vue/compiler-core": "3.2.6",
|
||||
"@vue/shared": "3.2.6",
|
||||
"estree-walker": "^2.0.2",
|
||||
"magic-string": "^0.25.7"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/parser": {
|
||||
"version": "7.15.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.15.3.tgz",
|
||||
"integrity": "sha512-O0L6v/HvqbdJawj0iBEfVQMc3/6WP+AeOsovsIgBFyJaG+W2w7eqvZB7puddATmWuARlm1SX7DwxJ/JJUnDpEA==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"@vue/runtime-core": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.2.2.tgz",
|
||||
"integrity": "sha512-/aUk1+GO/VPX0oVxhbzSWE1zrf3/wGCsO1ALNisVokYftKqfqLDjbJHE6mrI2hx3MiuwbHrWjJClkGUVTIOPEQ==",
|
||||
"version": "3.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.2.6.tgz",
|
||||
"integrity": "sha512-3mqtgpj/YSGFxtvTufSERRApo92B16JNNxz9p+5eG6PPuqTmuRJz214MqhKBEgLEAIQ6R6YCbd83ZDtjQnyw2g==",
|
||||
"requires": {
|
||||
"@vue/reactivity": "3.2.2",
|
||||
"@vue/shared": "3.2.2"
|
||||
"@vue/reactivity": "3.2.6",
|
||||
"@vue/shared": "3.2.6"
|
||||
}
|
||||
},
|
||||
"@vue/runtime-dom": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.2.2.tgz",
|
||||
"integrity": "sha512-1Le/NpCfawCOfePfJezvWUF+oCVLU8N+IHN4oFDOxRe6/PgHNJ+yT+YdxFifBfI+TIAoXI/9PsnqzmJZV+xsmw==",
|
||||
"version": "3.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.2.6.tgz",
|
||||
"integrity": "sha512-fq33urnP0BNCGm2O3KCzkJlKIHI80C94HJ4qDZbjsTtxyOn5IHqwKSqXVN3RQvO6epcQH+sWS+JNwcNDPzoasg==",
|
||||
"requires": {
|
||||
"@vue/runtime-core": "3.2.2",
|
||||
"@vue/shared": "3.2.2",
|
||||
"@vue/runtime-core": "3.2.6",
|
||||
"@vue/shared": "3.2.6",
|
||||
"csstype": "^2.6.8"
|
||||
}
|
||||
},
|
||||
"@vue/shared": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.2.2.tgz",
|
||||
"integrity": "sha512-dvYb318tk9uOzHtSaT3WII/HscQSIRzoCZ5GyxEb3JlkEXASpAUAQwKnvSe2CudnF8XHFRTB7VITWSnWNLZUtA=="
|
||||
"version": "3.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.2.6.tgz",
|
||||
"integrity": "sha512-uwX0Qs2e6kdF+WmxwuxJxOnKs/wEkMArtYpHSm7W+VY/23Tl8syMRyjnzEeXrNCAP0/8HZxEGkHJsjPEDNRuHw=="
|
||||
},
|
||||
"@vue/web-component-wrapper": {
|
||||
"version": "1.3.0",
|
||||
@ -3086,6 +3173,12 @@
|
||||
"es-abstract": "^1.18.0-next.1"
|
||||
}
|
||||
},
|
||||
"asap": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
|
||||
"integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=",
|
||||
"dev": true
|
||||
},
|
||||
"asn1": {
|
||||
"version": "0.2.4",
|
||||
"resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz",
|
||||
@ -3142,6 +3235,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"assert-never": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/assert-never/-/assert-never-1.2.1.tgz",
|
||||
"integrity": "sha512-TaTivMB6pYI1kXwrFlEhLeGfOqoDNdTxjCdwRfFFkEA30Eu+k48W34nlok2EYWJfFFzqaEmichdNM7th6M5HNw==",
|
||||
"dev": true
|
||||
},
|
||||
"assert-plus": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
|
||||
@ -3337,6 +3436,15 @@
|
||||
"@babel/helper-define-polyfill-provider": "^0.2.2"
|
||||
}
|
||||
},
|
||||
"babel-walk": {
|
||||
"version": "3.0.0-canary-5",
|
||||
"resolved": "https://registry.npmjs.org/babel-walk/-/babel-walk-3.0.0-canary-5.tgz",
|
||||
"integrity": "sha512-GAwkz0AihzY5bkwIY5QDR+LvsRQgB/B+1foMPvi0FZPMl5fjD7ICiznUiBdLYMH1QYe6vqu4gWYytZOccLouFw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/types": "^7.9.6"
|
||||
}
|
||||
},
|
||||
"balanced-match": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
@ -3891,6 +3999,15 @@
|
||||
"supports-color": "^5.3.0"
|
||||
}
|
||||
},
|
||||
"character-parser": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/character-parser/-/character-parser-2.2.0.tgz",
|
||||
"integrity": "sha1-x84o821LzZdE5f/CxfzeHHMmH8A=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"is-regex": "^1.0.3"
|
||||
}
|
||||
},
|
||||
"chardet": {
|
||||
"version": "0.7.0",
|
||||
"resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz",
|
||||
@ -4359,6 +4476,16 @@
|
||||
"bluebird": "^3.1.1"
|
||||
}
|
||||
},
|
||||
"constantinople": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/constantinople/-/constantinople-4.0.1.tgz",
|
||||
"integrity": "sha512-vCrqcSIq4//Gx74TXXCGnHpulY1dskqLTFGDmhrGxzeXL8lF8kvXv6mpNWlJj1uD4DW23D4ljAqbY4RRaaUZIw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/parser": "^7.6.0",
|
||||
"@babel/types": "^7.6.1"
|
||||
}
|
||||
},
|
||||
"constants-browserify": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz",
|
||||
@ -4580,9 +4707,9 @@
|
||||
}
|
||||
},
|
||||
"core-js": {
|
||||
"version": "3.16.1",
|
||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.16.1.tgz",
|
||||
"integrity": "sha512-AAkP8i35EbefU+JddyWi12AWE9f2N/qr/pwnDtWz4nyUIBGMJPX99ANFFRSw6FefM374lDujdtLDyhN2A/btHw=="
|
||||
"version": "3.16.3",
|
||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.16.3.tgz",
|
||||
"integrity": "sha512-lM3GftxzHNtPNUJg0v4pC2RC6puwMd6VZA7vXUczi+SKmCWSf4JwO89VJGMqbzmB7jlK7B5hr3S64PqwFL49cA=="
|
||||
},
|
||||
"core-js-compat": {
|
||||
"version": "3.15.2",
|
||||
@ -5351,6 +5478,12 @@
|
||||
"esutils": "^2.0.2"
|
||||
}
|
||||
},
|
||||
"doctypes": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/doctypes/-/doctypes-1.1.0.tgz",
|
||||
"integrity": "sha1-6oCxBqh1OHdOijpKWv4pPeSJ4Kk=",
|
||||
"dev": true
|
||||
},
|
||||
"dom-converter": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz",
|
||||
@ -5407,6 +5540,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"dompurify": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.3.1.tgz",
|
||||
"integrity": "sha512-xGWt+NHAQS+4tpgbOAI08yxW0Pr256Gu/FNE2frZVTbgrBUn8M7tz7/ktS/LZ2MHeGqz6topj0/xY+y8R5FBFw=="
|
||||
},
|
||||
"domutils": {
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz",
|
||||
@ -6118,26 +6256,26 @@
|
||||
}
|
||||
},
|
||||
"eslint-plugin-import": {
|
||||
"version": "2.24.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.24.0.tgz",
|
||||
"integrity": "sha512-Kc6xqT9hiYi2cgybOc0I2vC9OgAYga5o/rAFinam/yF/t5uBqxQbauNPMC6fgb640T/89P0gFoO27FOilJ/Cqg==",
|
||||
"version": "2.24.2",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.24.2.tgz",
|
||||
"integrity": "sha512-hNVtyhiEtZmpsabL4neEj+6M5DCLgpYyG9nzJY8lZQeQXEn5UPW1DpUdsMHMXsq98dbNm7nt1w9ZMSVpfJdi8Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"array-includes": "^3.1.3",
|
||||
"array.prototype.flat": "^1.2.4",
|
||||
"debug": "^2.6.9",
|
||||
"doctrine": "^2.1.0",
|
||||
"eslint-import-resolver-node": "^0.3.5",
|
||||
"eslint-import-resolver-node": "^0.3.6",
|
||||
"eslint-module-utils": "^2.6.2",
|
||||
"find-up": "^2.0.0",
|
||||
"has": "^1.0.3",
|
||||
"is-core-module": "^2.4.0",
|
||||
"is-core-module": "^2.6.0",
|
||||
"minimatch": "^3.0.4",
|
||||
"object.values": "^1.1.3",
|
||||
"object.values": "^1.1.4",
|
||||
"pkg-up": "^2.0.0",
|
||||
"read-pkg-up": "^3.0.0",
|
||||
"resolve": "^1.20.0",
|
||||
"tsconfig-paths": "^3.9.0"
|
||||
"tsconfig-paths": "^3.11.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"debug": {
|
||||
@ -6158,33 +6296,6 @@
|
||||
"esutils": "^2.0.2"
|
||||
}
|
||||
},
|
||||
"eslint-import-resolver-node": {
|
||||
"version": "0.3.6",
|
||||
"resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.6.tgz",
|
||||
"integrity": "sha512-0En0w03NRVMn9Uiyn8YRPDKvWjxCWkslUEhGNTdGx15RvPJYQ+lbOlqrlNI2vEAs4pDYK4f/HN2TbDmk5TP0iw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"debug": "^3.2.7",
|
||||
"resolve": "^1.20.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"debug": {
|
||||
"version": "3.2.7",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
|
||||
"integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ms": "^2.1.1"
|
||||
}
|
||||
},
|
||||
"ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"find-up": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz",
|
||||
@ -6194,6 +6305,15 @@
|
||||
"locate-path": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"is-core-module": {
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.6.0.tgz",
|
||||
"integrity": "sha512-wShG8vs60jKfPWpF2KZRaAtvt3a20OAn7+IJ6hLPECpSABLcKtFKTTI4ZtH5QcBruBHlq+WsdHWyz0BCZW7svQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"has": "^1.0.3"
|
||||
}
|
||||
},
|
||||
"locate-path": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz",
|
||||
@ -6292,9 +6412,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"eslint-plugin-vue": {
|
||||
"version": "7.16.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-7.16.0.tgz",
|
||||
"integrity": "sha512-0E2dVvVC7I2Xm1HXyx+ZwPj9CNX4NJjs4K4r+GVsHWyt5Pew3JLD4fI7A91b2jeL0TXE7LlszrwLSTJU9eqehw==",
|
||||
"version": "7.17.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-7.17.0.tgz",
|
||||
"integrity": "sha512-Rq5R2QetDCgC+kBFQw1+aJ5B93tQ4xqZvoCUxuIzwTonngNArsdP8ChM8PowIzsJvRtWl4ltGh/bZcN3xhFWSw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"eslint-utils": "^2.1.0",
|
||||
@ -6786,9 +6906,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"fastq": {
|
||||
"version": "1.11.1",
|
||||
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.11.1.tgz",
|
||||
"integrity": "sha512-HOnr8Mc60eNYl1gzwp6r5RoUyAn5/glBolUzP/Ez6IFVPMPirxn/9phgL6zhOtaTy7ISwPvQ+wT+hfcRZh/bzw==",
|
||||
"version": "1.12.0",
|
||||
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.12.0.tgz",
|
||||
"integrity": "sha512-VNX0QkHK3RsXVKr9KrlUv/FoTa0NdbYoHHl7uXHv2rzyHSlxjdNAKug2twd9luJxpcyNeAgf5iPPMutJO67Dfg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"reusify": "^1.0.4"
|
||||
@ -8179,6 +8299,24 @@
|
||||
"integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==",
|
||||
"dev": true
|
||||
},
|
||||
"is-expression": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-expression/-/is-expression-4.0.0.tgz",
|
||||
"integrity": "sha512-zMIXX63sxzG3XrkHkrAPvm/OVZVSCPNkwMHU8oTX7/U3AL78I0QXCEICXUM13BIa8TYGZ68PiTKfQz3yaTNr4A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"acorn": "^7.1.1",
|
||||
"object-assign": "^4.1.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"acorn": {
|
||||
"version": "7.4.1",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz",
|
||||
"integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"is-extendable": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz",
|
||||
@ -8283,6 +8421,12 @@
|
||||
"isobject": "^3.0.1"
|
||||
}
|
||||
},
|
||||
"is-promise": {
|
||||
"version": "2.2.2",
|
||||
"resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz",
|
||||
"integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==",
|
||||
"dev": true
|
||||
},
|
||||
"is-regex": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.3.tgz",
|
||||
@ -8383,6 +8527,12 @@
|
||||
"easy-stack": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"js-stringify": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/js-stringify/-/js-stringify-1.0.2.tgz",
|
||||
"integrity": "sha1-Fzb939lyTyijaCrcYjCufk6Weds=",
|
||||
"dev": true
|
||||
},
|
||||
"js-tokens": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
@ -8485,6 +8635,16 @@
|
||||
"verror": "1.10.0"
|
||||
}
|
||||
},
|
||||
"jstransformer": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/jstransformer/-/jstransformer-1.0.0.tgz",
|
||||
"integrity": "sha1-7Yvwkh4vPx7U1cGkT2hwntJHIsM=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"is-promise": "^2.0.0",
|
||||
"promise": "^7.0.1"
|
||||
}
|
||||
},
|
||||
"killable": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz",
|
||||
@ -10647,6 +10807,15 @@
|
||||
"integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
|
||||
"dev": true
|
||||
},
|
||||
"promise": {
|
||||
"version": "7.3.1",
|
||||
"resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz",
|
||||
"integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"asap": "~2.0.3"
|
||||
}
|
||||
},
|
||||
"promise-inflight": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz",
|
||||
@ -10703,6 +10872,139 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"pug": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/pug/-/pug-3.0.2.tgz",
|
||||
"integrity": "sha512-bp0I/hiK1D1vChHh6EfDxtndHji55XP/ZJKwsRqrz6lRia6ZC2OZbdAymlxdVFwd1L70ebrVJw4/eZ79skrIaw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"pug-code-gen": "^3.0.2",
|
||||
"pug-filters": "^4.0.0",
|
||||
"pug-lexer": "^5.0.1",
|
||||
"pug-linker": "^4.0.0",
|
||||
"pug-load": "^3.0.0",
|
||||
"pug-parser": "^6.0.0",
|
||||
"pug-runtime": "^3.0.1",
|
||||
"pug-strip-comments": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"pug-attrs": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pug-attrs/-/pug-attrs-3.0.0.tgz",
|
||||
"integrity": "sha512-azINV9dUtzPMFQktvTXciNAfAuVh/L/JCl0vtPCwvOA21uZrC08K/UnmrL+SXGEVc1FwzjW62+xw5S/uaLj6cA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"constantinople": "^4.0.1",
|
||||
"js-stringify": "^1.0.2",
|
||||
"pug-runtime": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"pug-code-gen": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/pug-code-gen/-/pug-code-gen-3.0.2.tgz",
|
||||
"integrity": "sha512-nJMhW16MbiGRiyR4miDTQMRWDgKplnHyeLvioEJYbk1RsPI3FuA3saEP8uwnTb2nTJEKBU90NFVWJBk4OU5qyg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"constantinople": "^4.0.1",
|
||||
"doctypes": "^1.1.0",
|
||||
"js-stringify": "^1.0.2",
|
||||
"pug-attrs": "^3.0.0",
|
||||
"pug-error": "^2.0.0",
|
||||
"pug-runtime": "^3.0.0",
|
||||
"void-elements": "^3.1.0",
|
||||
"with": "^7.0.0"
|
||||
}
|
||||
},
|
||||
"pug-error": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pug-error/-/pug-error-2.0.0.tgz",
|
||||
"integrity": "sha512-sjiUsi9M4RAGHktC1drQfCr5C5eriu24Lfbt4s+7SykztEOwVZtbFk1RRq0tzLxcMxMYTBR+zMQaG07J/btayQ==",
|
||||
"dev": true
|
||||
},
|
||||
"pug-filters": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pug-filters/-/pug-filters-4.0.0.tgz",
|
||||
"integrity": "sha512-yeNFtq5Yxmfz0f9z2rMXGw/8/4i1cCFecw/Q7+D0V2DdtII5UvqE12VaZ2AY7ri6o5RNXiweGH79OCq+2RQU4A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"constantinople": "^4.0.1",
|
||||
"jstransformer": "1.0.0",
|
||||
"pug-error": "^2.0.0",
|
||||
"pug-walk": "^2.0.0",
|
||||
"resolve": "^1.15.1"
|
||||
}
|
||||
},
|
||||
"pug-lexer": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/pug-lexer/-/pug-lexer-5.0.1.tgz",
|
||||
"integrity": "sha512-0I6C62+keXlZPZkOJeVam9aBLVP2EnbeDw3An+k0/QlqdwH6rv8284nko14Na7c0TtqtogfWXcRoFE4O4Ff20w==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"character-parser": "^2.2.0",
|
||||
"is-expression": "^4.0.0",
|
||||
"pug-error": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"pug-linker": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pug-linker/-/pug-linker-4.0.0.tgz",
|
||||
"integrity": "sha512-gjD1yzp0yxbQqnzBAdlhbgoJL5qIFJw78juN1NpTLt/mfPJ5VgC4BvkoD3G23qKzJtIIXBbcCt6FioLSFLOHdw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"pug-error": "^2.0.0",
|
||||
"pug-walk": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"pug-load": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pug-load/-/pug-load-3.0.0.tgz",
|
||||
"integrity": "sha512-OCjTEnhLWZBvS4zni/WUMjH2YSUosnsmjGBB1An7CsKQarYSWQ0GCVyd4eQPMFJqZ8w9xgs01QdiZXKVjk92EQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"object-assign": "^4.1.1",
|
||||
"pug-walk": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"pug-parser": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pug-parser/-/pug-parser-6.0.0.tgz",
|
||||
"integrity": "sha512-ukiYM/9cH6Cml+AOl5kETtM9NR3WulyVP2y4HOU45DyMim1IeP/OOiyEWRr6qk5I5klpsBnbuHpwKmTx6WURnw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"pug-error": "^2.0.0",
|
||||
"token-stream": "1.0.0"
|
||||
}
|
||||
},
|
||||
"pug-plain-loader": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/pug-plain-loader/-/pug-plain-loader-1.1.0.tgz",
|
||||
"integrity": "sha512-1nYgIJLaahRuHJHhzSPODV44aZfb00bO7kiJiMkke6Hj4SVZftuvx6shZ4BOokk50dJc2RSFqNUBOlus0dniFQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"loader-utils": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"pug-runtime": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/pug-runtime/-/pug-runtime-3.0.1.tgz",
|
||||
"integrity": "sha512-L50zbvrQ35TkpHwv0G6aLSuueDRwc/97XdY8kL3tOT0FmhgG7UypU3VztfV/LATAvmUfYi4wNxSajhSAeNN+Kg==",
|
||||
"dev": true
|
||||
},
|
||||
"pug-strip-comments": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pug-strip-comments/-/pug-strip-comments-2.0.0.tgz",
|
||||
"integrity": "sha512-zo8DsDpH7eTkPHCXFeAk1xZXJbyoTfdPlNR0bK7rpOMuhBYb0f5qUVCO1xlsitYd3w5FQTK7zpNVKb3rZoUrrQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"pug-error": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"pug-walk": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pug-walk/-/pug-walk-2.0.0.tgz",
|
||||
"integrity": "sha512-yYELe9Q5q9IQhuvqsZNwA5hfPkMJ8u92bQLIMcsMxf/VADjNtEYptU+inlufAFYcWdHlwNfZOEnOOQrZrcyJCQ==",
|
||||
"dev": true
|
||||
},
|
||||
"pump": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
|
||||
@ -10825,6 +11127,40 @@
|
||||
"unpipe": "1.0.0"
|
||||
}
|
||||
},
|
||||
"raw-loader": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/raw-loader/-/raw-loader-4.0.2.tgz",
|
||||
"integrity": "sha512-ZnScIV3ag9A4wPX/ZayxL/jZH+euYb6FcUinPcgiQW0+UBtEv0O6Q3lGd3cqJ+GHH+rksEv3Pj99oxJ3u3VIKA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"loader-utils": "^2.0.0",
|
||||
"schema-utils": "^3.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"loader-utils": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz",
|
||||
"integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"big.js": "^5.2.2",
|
||||
"emojis-list": "^3.0.0",
|
||||
"json5": "^2.1.2"
|
||||
}
|
||||
},
|
||||
"schema-utils": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz",
|
||||
"integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/json-schema": "^7.0.8",
|
||||
"ajv": "^6.12.5",
|
||||
"ajv-keywords": "^3.5.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"read-pkg": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz",
|
||||
@ -12606,6 +12942,12 @@
|
||||
"integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==",
|
||||
"dev": true
|
||||
},
|
||||
"token-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/token-stream/-/token-stream-1.0.0.tgz",
|
||||
"integrity": "sha1-zCAOqyYT9BZtJ/+a/HylbUnfbrQ=",
|
||||
"dev": true
|
||||
},
|
||||
"toposort": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/toposort/-/toposort-1.0.7.tgz",
|
||||
@ -12693,14 +13035,26 @@
|
||||
"dev": true
|
||||
},
|
||||
"tsconfig-paths": {
|
||||
"version": "3.10.1",
|
||||
"resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.10.1.tgz",
|
||||
"integrity": "sha512-rETidPDgCpltxF7MjBZlAFPUHv5aHH2MymyPvh+vEyWAED4Eb/WeMbsnD/JDr4OKPOA1TssDHgIcpTN5Kh0p6Q==",
|
||||
"version": "3.11.0",
|
||||
"resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.11.0.tgz",
|
||||
"integrity": "sha512-7ecdYDnIdmv639mmDwslG6KQg1Z9STTz1j7Gcz0xa+nshh/gKDAHcPxRbWOsA3SPp0tXP2leTcY9Kw+NAkfZzA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"json5": "^2.2.0",
|
||||
"@types/json5": "^0.0.29",
|
||||
"json5": "^1.0.1",
|
||||
"minimist": "^1.2.0",
|
||||
"strip-bom": "^3.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"json5": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
|
||||
"integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"minimist": "^1.2.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tslib": {
|
||||
@ -13140,14 +13494,31 @@
|
||||
"integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==",
|
||||
"dev": true
|
||||
},
|
||||
"void-elements": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz",
|
||||
"integrity": "sha1-YU9/v42AHwu18GYfWy9XhXUOTwk=",
|
||||
"dev": true
|
||||
},
|
||||
"vue": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/vue/-/vue-3.2.2.tgz",
|
||||
"integrity": "sha512-D/LuzAV30CgNJYGyNheE/VUs5N4toL2IgmS6c9qeOxvyh0xyn4exyRqizpXIrsvfx34zG9x5gCI2tdRHCGvF9w==",
|
||||
"version": "3.2.6",
|
||||
"resolved": "https://registry.npmjs.org/vue/-/vue-3.2.6.tgz",
|
||||
"integrity": "sha512-Zlb3LMemQS3Xxa6xPsecu45bNjr1hxO8Bh5FUmE0Dr6Ot0znZBKiM47rK6O7FTcakxOnvVN+NTXWJF6u8ajpCQ==",
|
||||
"requires": {
|
||||
"@vue/compiler-dom": "3.2.2",
|
||||
"@vue/runtime-dom": "3.2.2",
|
||||
"@vue/shared": "3.2.2"
|
||||
"@vue/compiler-dom": "3.2.6",
|
||||
"@vue/runtime-dom": "3.2.6",
|
||||
"@vue/shared": "3.2.6"
|
||||
}
|
||||
},
|
||||
"vue-cli-plugin-pug": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/vue-cli-plugin-pug/-/vue-cli-plugin-pug-2.0.0.tgz",
|
||||
"integrity": "sha512-Nnl5pkHqNDMkPTvOX4iidFSUs0sn2A5JGsQBVZ70Wm2aXqbK/B3jvEFOWok5/y9U/aWeZLU05PwYVYc7nTUK2Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"pug": "^3.0.0",
|
||||
"pug-plain-loader": "^1.0.0",
|
||||
"raw-loader": "^4.0.2"
|
||||
}
|
||||
},
|
||||
"vue-demi": {
|
||||
@ -13941,6 +14312,18 @@
|
||||
"integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=",
|
||||
"dev": true
|
||||
},
|
||||
"with": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/with/-/with-7.0.2.tgz",
|
||||
"integrity": "sha512-RNGKj82nUPg3g5ygxkQl0R937xLyho1J24ItRCBTr/m1YnZkzJy1hUiHUJrc/VlsDQzsCnInEGSg3bci0Lmd4w==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/parser": "^7.9.6",
|
||||
"@babel/types": "^7.9.6",
|
||||
"assert-never": "^1.2.1",
|
||||
"babel-walk": "3.0.0-canary-5"
|
||||
}
|
||||
},
|
||||
"word-wrap": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
|
||||
|
@ -13,34 +13,36 @@
|
||||
"@vuelidate/core": "^2.0.0-alpha.24",
|
||||
"@vuelidate/validators": "^2.0.0-alpha.21",
|
||||
"bootstrap": "^5.1.0",
|
||||
"core-js": "^3.16.1",
|
||||
"core-js": "^3.16.3",
|
||||
"date-fns": "^2.23.0",
|
||||
"date-fns-tz": "^1.1.6",
|
||||
"dompurify": "^2.3.1",
|
||||
"marked": "^2.1.3",
|
||||
"vue": "^3.2.2",
|
||||
"vue": "^3.2.6",
|
||||
"vue-router": "^4.0.11",
|
||||
"vuex": "^4.0.0-0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bootstrap": "^5.1.1",
|
||||
"@types/marked": "^2.0.4",
|
||||
"@typescript-eslint/eslint-plugin": "^4.29.1",
|
||||
"@typescript-eslint/parser": "^4.29.1",
|
||||
"@types/bootstrap": "^5.1.2",
|
||||
"@types/dompurify": "^2.2.3",
|
||||
"@types/marked": "^2.0.5",
|
||||
"@typescript-eslint/eslint-plugin": "^4.29.3",
|
||||
"@typescript-eslint/parser": "^4.29.3",
|
||||
"@vue/cli-plugin-babel": "~4.5.0",
|
||||
"@vue/cli-plugin-eslint": "~4.5.0",
|
||||
"@vue/cli-plugin-router": "~4.5.0",
|
||||
"@vue/cli-plugin-typescript": "~4.5.0",
|
||||
"@vue/cli-plugin-vuex": "~4.5.0",
|
||||
"@vue/cli-service": "~4.5.0",
|
||||
"@vue/compiler-sfc": "^3.2.2",
|
||||
"@vue/compiler-sfc": "^3.2.6",
|
||||
"@vue/eslint-config-standard": "^6.1.0",
|
||||
"@vue/eslint-config-typescript": "^7.0.0",
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-plugin-import": "^2.24.0",
|
||||
"eslint-plugin-import": "^2.24.2",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-promise": "^5.0.0",
|
||||
"eslint-plugin-standard": "^5.0.0",
|
||||
"eslint-plugin-vue": "^7.16.0",
|
||||
"eslint-plugin-vue": "^7.17.0",
|
||||
"sass": "~1.37.0",
|
||||
"sass-loader": "^10.0.0",
|
||||
"typescript": "~4.3.5",
|
||||
|
@ -10,18 +10,18 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue'
|
||||
import { Citizen } from './api'
|
||||
import AppFooter from './components/layout/AppFooter.vue'
|
||||
import AppNav from './components/layout/AppNav.vue'
|
||||
import AppToaster from './components/layout/AppToaster.vue'
|
||||
import TitleBar from './components/layout/TitleBar.vue'
|
||||
import { defineComponent } from "vue"
|
||||
|
||||
import 'bootstrap/dist/css/bootstrap.min.css'
|
||||
import '@mdi/font/css/materialdesignicons.css'
|
||||
import "bootstrap/dist/css/bootstrap.min.css"
|
||||
import "@mdi/font/css/materialdesignicons.css"
|
||||
|
||||
import { Citizen } from "./api"
|
||||
import AppFooter from "./components/layout/AppFooter.vue"
|
||||
import AppNav from "./components/layout/AppNav.vue"
|
||||
import AppToaster from "./components/layout/AppToaster.vue"
|
||||
import TitleBar from "./components/layout/TitleBar.vue"
|
||||
|
||||
export default defineComponent({
|
||||
name: 'App',
|
||||
components: {
|
||||
AppFooter,
|
||||
AppNav,
|
||||
@ -37,7 +37,7 @@ export default defineComponent({
|
||||
* @returns "Yes" for true, "No" for false
|
||||
*/
|
||||
export function yesOrNo (cond : boolean) : string {
|
||||
return cond ? 'Yes' : 'No'
|
||||
return cond ? "Yes" : "No"
|
||||
}
|
||||
|
||||
/**
|
||||
@ -47,7 +47,7 @@ export function yesOrNo (cond : boolean) : string {
|
||||
* @returns The citizen's display name
|
||||
*/
|
||||
export function citizenName (cit : Citizen) : string {
|
||||
return cit.realName || cit.displayName || cit.naUser
|
||||
return cit.realName ?? cit.displayName ?? cit.naUser
|
||||
}
|
||||
</script>
|
||||
|
||||
|
@ -1,4 +1,3 @@
|
||||
import { MarkedOptions } from 'marked'
|
||||
import {
|
||||
Citizen,
|
||||
Continent,
|
||||
@ -18,7 +17,7 @@ import {
|
||||
StoryEntry,
|
||||
StoryForm,
|
||||
Success
|
||||
} from './types'
|
||||
} from "./types"
|
||||
|
||||
/**
|
||||
* Create a URL that will access the API
|
||||
@ -37,13 +36,13 @@ const apiUrl = (url : string) : string => `http://localhost:5000/api/${url}`
|
||||
// eslint-disable-next-line
|
||||
const reqInit = (method : string, user : LogOnSuccess, body : any | undefined = undefined) : RequestInit => {
|
||||
const headers = new Headers()
|
||||
headers.append('Authorization', `Bearer ${user.jwt}`)
|
||||
headers.append("Authorization", `Bearer ${user.jwt}`)
|
||||
if (body) {
|
||||
headers.append('Content-Type', 'application/json')
|
||||
headers.append("Content-Type", "application/json")
|
||||
return {
|
||||
headers,
|
||||
method,
|
||||
cache: 'no-cache',
|
||||
cache: "no-cache",
|
||||
body: JSON.stringify(body)
|
||||
}
|
||||
}
|
||||
@ -104,7 +103,7 @@ export default {
|
||||
* @returns The user result, or an error
|
||||
*/
|
||||
logOn: async (code : string) : Promise<LogOnSuccess | string> => {
|
||||
const resp = await fetch(apiUrl(`citizen/log-on/${code}`), { method: 'GET', mode: 'cors' })
|
||||
const resp = await fetch(apiUrl(`citizen/log-on/${code}`), { method: "GET", mode: "cors" })
|
||||
if (resp.status === 200) return await resp.json() as LogOnSuccess
|
||||
return `Error logging on - ${await resp.text()}`
|
||||
},
|
||||
@ -117,7 +116,7 @@ export default {
|
||||
* @returns The citizen, or an error
|
||||
*/
|
||||
retrieve: async (id : string, user : LogOnSuccess) : Promise<Citizen | string | undefined> =>
|
||||
apiResult<Citizen>(await fetch(apiUrl(`citizen/${id}`), reqInit('GET', user)), `retrieving citizen ${id}`),
|
||||
apiResult<Citizen>(await fetch(apiUrl(`citizen/${id}`), reqInit("GET", user)), `retrieving citizen ${id}`),
|
||||
|
||||
/**
|
||||
* Delete the current citizen's entire Jobs, Jobs, Jobs record
|
||||
@ -126,7 +125,7 @@ export default {
|
||||
* @returns Undefined if successful, an error if not
|
||||
*/
|
||||
delete: async (user : LogOnSuccess) : Promise<string | undefined> =>
|
||||
apiAction(await fetch(apiUrl('citizen'), reqInit('DELETE', user)), 'deleting citizen')
|
||||
apiAction(await fetch(apiUrl("citizen"), reqInit("DELETE", user)), "deleting citizen")
|
||||
},
|
||||
|
||||
/** API functions for continents */
|
||||
@ -138,7 +137,7 @@ export default {
|
||||
* @returns All continents, or an error
|
||||
*/
|
||||
all: async () : Promise<Continent[] | string | undefined> =>
|
||||
apiResult<Continent[]>(await fetch(apiUrl('continents'), { method: 'GET' }), 'retrieving continents')
|
||||
apiResult<Continent[]>(await fetch(apiUrl("continents"), { method: "GET" }), "retrieving continents")
|
||||
},
|
||||
|
||||
/** API functions for job listings */
|
||||
@ -152,7 +151,7 @@ export default {
|
||||
* @returns True if the addition was successful, an error string if not
|
||||
*/
|
||||
add: async (listing : ListingForm, user : LogOnSuccess) : Promise<boolean | string> =>
|
||||
apiSend(await fetch(apiUrl('listings'), reqInit('POST', user, listing)), 'adding job listing'),
|
||||
apiSend(await fetch(apiUrl("listings"), reqInit("POST", user, listing)), "adding job listing"),
|
||||
|
||||
/**
|
||||
* Retrieve the job listings posted by the current citizen
|
||||
@ -161,8 +160,8 @@ export default {
|
||||
* @returns The job listings the user has posted, or an error string
|
||||
*/
|
||||
mine: async (user : LogOnSuccess) : Promise<ListingForView[] | string | undefined> =>
|
||||
apiResult<ListingForView[]>(await fetch(apiUrl('listings/mine'), reqInit('GET', user)),
|
||||
'retrieving your job listings'),
|
||||
apiResult<ListingForView[]>(await fetch(apiUrl("listings/mine"), reqInit("GET", user)),
|
||||
"retrieving your job listings"),
|
||||
|
||||
/**
|
||||
* Retrieve a job listing
|
||||
@ -172,7 +171,7 @@ export default {
|
||||
* @returns The job listing (if found), undefined (if not found), or an error string
|
||||
*/
|
||||
retreive: async (id : string, user : LogOnSuccess) : Promise<Listing | undefined | string> =>
|
||||
apiResult<Listing>(await fetch(apiUrl(`listing/${id}`), reqInit('GET', user)), 'retrieving job listing'),
|
||||
apiResult<Listing>(await fetch(apiUrl(`listing/${id}`), reqInit("GET", user)), "retrieving job listing"),
|
||||
|
||||
/**
|
||||
* Retrieve a job listing for viewing (also contains continent information)
|
||||
@ -182,8 +181,8 @@ export default {
|
||||
* @returns The job listing (if found), undefined (if not found), or an error string
|
||||
*/
|
||||
retreiveForView: async (id : string, user : LogOnSuccess) : Promise<ListingForView | undefined | string> =>
|
||||
apiResult<ListingForView>(await fetch(apiUrl(`listing/${id}/view`), reqInit('GET', user)),
|
||||
'retrieving job listing'),
|
||||
apiResult<ListingForView>(await fetch(apiUrl(`listing/${id}/view`), reqInit("GET", user)),
|
||||
"retrieving job listing"),
|
||||
|
||||
/**
|
||||
* Search for job listings using the given parameters
|
||||
@ -194,12 +193,12 @@ export default {
|
||||
*/
|
||||
search: async (query : ListingSearch, user : LogOnSuccess) : Promise<ListingForView[] | string | undefined> => {
|
||||
const params = new URLSearchParams()
|
||||
if (query.continentId) params.append('continentId', query.continentId)
|
||||
if (query.region) params.append('region', query.region)
|
||||
params.append('remoteWork', query.remoteWork)
|
||||
if (query.text) params.append('text', query.text)
|
||||
if (query.continentId) params.append("continentId", query.continentId)
|
||||
if (query.region) params.append("region", query.region)
|
||||
params.append("remoteWork", query.remoteWork)
|
||||
if (query.text) params.append("text", query.text)
|
||||
return apiResult<ListingForView[]>(await fetch(apiUrl(`listing/search?${params.toString()}`),
|
||||
reqInit('GET', user)), 'searching job listings')
|
||||
reqInit("GET", user)), "searching job listings")
|
||||
},
|
||||
|
||||
/**
|
||||
@ -210,7 +209,7 @@ export default {
|
||||
* @returns True if the update was successful, an error string if not
|
||||
*/
|
||||
update: async (listing : ListingForm, user : LogOnSuccess) : Promise<boolean | string> =>
|
||||
apiSend(await fetch(apiUrl(`listing/${listing.id}`), reqInit('PUT', user, listing)), 'updating job listing')
|
||||
apiSend(await fetch(apiUrl(`listing/${listing.id}`), reqInit("PUT", user, listing)), "updating job listing")
|
||||
},
|
||||
|
||||
/** API functions for profiles */
|
||||
@ -223,7 +222,7 @@ export default {
|
||||
* @returns True if the action was successful, or an error string if not
|
||||
*/
|
||||
markEmploymentFound: async (user : LogOnSuccess) : Promise<boolean | string> => {
|
||||
const result = await fetch(apiUrl('profile/employment-found'), reqInit('PATCH', user))
|
||||
const result = await fetch(apiUrl("profile/employment-found"), reqInit("PATCH", user))
|
||||
if (result.ok) return true
|
||||
return `${result.status} - ${result.statusText} (${await result.text()})`
|
||||
},
|
||||
@ -236,13 +235,13 @@ export default {
|
||||
*/
|
||||
publicSearch: async (query : PublicSearch) : Promise<PublicSearchResult[] | string | undefined> => {
|
||||
const params = new URLSearchParams()
|
||||
if (query.continentId) params.append('continentId', query.continentId)
|
||||
if (query.region) params.append('region', query.region)
|
||||
if (query.skill) params.append('skill', query.skill)
|
||||
params.append('remoteWork', query.remoteWork)
|
||||
if (query.continentId) params.append("continentId", query.continentId)
|
||||
if (query.region) params.append("region", query.region)
|
||||
if (query.skill) params.append("skill", query.skill)
|
||||
params.append("remoteWork", query.remoteWork)
|
||||
return apiResult<PublicSearchResult[]>(
|
||||
await fetch(apiUrl(`profile/public-search?${params.toString()}`), { method: 'GET' }),
|
||||
'searching public profile data')
|
||||
await fetch(apiUrl(`profile/public-search?${params.toString()}`), { method: "GET" }),
|
||||
"searching public profile data")
|
||||
},
|
||||
|
||||
/**
|
||||
@ -253,8 +252,8 @@ export default {
|
||||
* @returns The profile (if found), undefined (if not found), or an error string
|
||||
*/
|
||||
retreive: async (id : string | undefined, user : LogOnSuccess) : Promise<Profile | undefined | string> => {
|
||||
const url = id ? `profile/${id}` : 'profile'
|
||||
const resp = await fetch(apiUrl(url), reqInit('GET', user))
|
||||
const url = id ? `profile/${id}` : "profile"
|
||||
const resp = await fetch(apiUrl(url), reqInit("GET", user))
|
||||
if (resp.status === 200) return await resp.json() as Profile
|
||||
if (resp.status !== 204) return `Error retrieving profile - ${await resp.text()}`
|
||||
},
|
||||
@ -267,7 +266,7 @@ export default {
|
||||
* @returns The profile (if found), undefined (if not found), or an error string
|
||||
*/
|
||||
retreiveForView: async (id : string, user : LogOnSuccess) : Promise<ProfileForView | string | undefined> =>
|
||||
apiResult<ProfileForView>(await fetch(apiUrl(`profile/${id}/view`), reqInit('GET', user)), 'retrieving profile'),
|
||||
apiResult<ProfileForView>(await fetch(apiUrl(`profile/${id}/view`), reqInit("GET", user)), "retrieving profile"),
|
||||
|
||||
/**
|
||||
* Save a user's profile data
|
||||
@ -277,7 +276,7 @@ export default {
|
||||
* @returns True if the save was successful, an error string if not
|
||||
*/
|
||||
save: async (data : ProfileForm, user : LogOnSuccess) : Promise<boolean | string> =>
|
||||
apiSend(await fetch(apiUrl('profile'), reqInit('POST', user, data)), 'saving profile'),
|
||||
apiSend(await fetch(apiUrl("profile"), reqInit("POST", user, data)), "saving profile"),
|
||||
|
||||
/**
|
||||
* Search for profiles using the given parameters
|
||||
@ -288,12 +287,12 @@ export default {
|
||||
*/
|
||||
search: async (query : ProfileSearch, user : LogOnSuccess) : Promise<ProfileSearchResult[] | string | undefined> => {
|
||||
const params = new URLSearchParams()
|
||||
if (query.continentId) params.append('continentId', query.continentId)
|
||||
if (query.skill) params.append('skill', query.skill)
|
||||
if (query.bioExperience) params.append('bioExperience', query.bioExperience)
|
||||
params.append('remoteWork', query.remoteWork)
|
||||
if (query.continentId) params.append("continentId", query.continentId)
|
||||
if (query.skill) params.append("skill", query.skill)
|
||||
if (query.bioExperience) params.append("bioExperience", query.bioExperience)
|
||||
params.append("remoteWork", query.remoteWork)
|
||||
return apiResult<ProfileSearchResult[]>(await fetch(apiUrl(`profile/search?${params.toString()}`),
|
||||
reqInit('GET', user)), 'searching profiles')
|
||||
reqInit("GET", user)), "searching profiles")
|
||||
},
|
||||
|
||||
/**
|
||||
@ -303,7 +302,7 @@ export default {
|
||||
* @returns A count of profiles within the entire system
|
||||
*/
|
||||
count: async (user : LogOnSuccess) : Promise<number | string> => {
|
||||
const resp = await fetch(apiUrl('profile/count'), reqInit('GET', user))
|
||||
const resp = await fetch(apiUrl("profile/count"), reqInit("GET", user))
|
||||
if (resp.status === 200) {
|
||||
const result = await resp.json() as Count
|
||||
return result.count
|
||||
@ -318,7 +317,7 @@ export default {
|
||||
* @returns Undefined if successful, an error if not
|
||||
*/
|
||||
delete: async (user : LogOnSuccess) : Promise<string | undefined> =>
|
||||
apiAction(await fetch(apiUrl('profile'), reqInit('DELETE', user)), 'deleting profile')
|
||||
apiAction(await fetch(apiUrl("profile"), reqInit("DELETE", user)), "deleting profile")
|
||||
},
|
||||
|
||||
/** API functions for success stories */
|
||||
@ -331,7 +330,7 @@ export default {
|
||||
* @returns All success stories (if any exist), undefined (if none exist), or an error
|
||||
*/
|
||||
list: async (user : LogOnSuccess) : Promise<StoryEntry[] | string | undefined> =>
|
||||
apiResult<StoryEntry[]>(await fetch(apiUrl('successes'), reqInit('GET', user)), 'retrieving success stories'),
|
||||
apiResult<StoryEntry[]>(await fetch(apiUrl("successes"), reqInit("GET", user)), "retrieving success stories"),
|
||||
|
||||
/**
|
||||
* Retrieve a success story by its ID
|
||||
@ -341,7 +340,7 @@ export default {
|
||||
* @returns The success story, or an error
|
||||
*/
|
||||
retrieve: async (id : string, user : LogOnSuccess) : Promise<Success | string | undefined> =>
|
||||
apiResult<Success>(await fetch(apiUrl(`success/${id}`), reqInit('GET', user)), `retrieving success story ${id}`),
|
||||
apiResult<Success>(await fetch(apiUrl(`success/${id}`), reqInit("GET", user)), `retrieving success story ${id}`),
|
||||
|
||||
/**
|
||||
* Save a success story
|
||||
@ -351,14 +350,8 @@ export default {
|
||||
* @returns True if successful, an error string if not
|
||||
*/
|
||||
save: async (data : StoryForm, user : LogOnSuccess) : Promise<boolean | string> =>
|
||||
apiSend(await fetch(apiUrl('success'), reqInit('POST', user, data)), 'saving success story')
|
||||
apiSend(await fetch(apiUrl("success"), reqInit("POST", user, data)), "saving success story")
|
||||
}
|
||||
}
|
||||
|
||||
/** The standard Jobs, Jobs, Jobs options for `marked` (GitHub-Flavo(u)red Markdown (GFM) with smart quotes) */
|
||||
export const markedOptions : MarkedOptions = {
|
||||
gfm: true,
|
||||
smartypants: true
|
||||
}
|
||||
|
||||
export * from './types'
|
||||
export * from "./types"
|
||||
|
@ -62,17 +62,17 @@ export interface Listing {
|
||||
/** The data required to add or edit a job listing */
|
||||
export class ListingForm {
|
||||
/** The ID of the listing */
|
||||
id = ''
|
||||
id = ""
|
||||
/** The listing title */
|
||||
title = ''
|
||||
title = ""
|
||||
/** The ID of the continent on which this opportunity exists */
|
||||
continentId = ''
|
||||
continentId = ""
|
||||
/** The region in which this opportunity exists */
|
||||
region = ''
|
||||
region = ""
|
||||
/** Whether this is a remote work opportunity */
|
||||
remoteWork = false
|
||||
/** The text of the job listing */
|
||||
text = ''
|
||||
text = ""
|
||||
/** The date by which this job listing is needed */
|
||||
neededBy : string | undefined
|
||||
}
|
||||
@ -150,17 +150,17 @@ export class ProfileForm {
|
||||
/** Whether this profile should appear in the public search */
|
||||
isPublic = false
|
||||
/** The user's real name */
|
||||
realName = ''
|
||||
realName = ""
|
||||
/** The ID of the continent on which the citizen is located */
|
||||
continentId = ''
|
||||
continentId = ""
|
||||
/** The area within that continent where the citizen is located */
|
||||
region = ''
|
||||
region = ""
|
||||
/** If the citizen is available for remote work */
|
||||
remoteWork = false
|
||||
/** If the citizen is seeking full-time employment */
|
||||
fullTime = false
|
||||
/** The user's professional biography */
|
||||
biography = ''
|
||||
biography = ""
|
||||
/** The user's past experience */
|
||||
experience : string | undefined
|
||||
/** The skills for the user */
|
||||
@ -248,11 +248,11 @@ export interface StoryEntry {
|
||||
/** The data required to provide a success story */
|
||||
export class StoryForm {
|
||||
/** The ID of this story */
|
||||
id = ''
|
||||
id = ""
|
||||
/** Whether the employment was obtained from Jobs, Jobs, Jobs */
|
||||
fromHere = false
|
||||
/** The success story */
|
||||
story = ''
|
||||
story = ""
|
||||
}
|
||||
|
||||
/** A record of success finding employment */
|
||||
|
@ -1,34 +1,20 @@
|
||||
<template lang="pug">
|
||||
span(@click='playFile') #[slot] #[audio(:id='clip'): source(:src='clipSource')]
|
||||
span(@click="playFile") #[slot] #[audio(:id="clip"): source(:src="clipSource")]
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue'
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{
|
||||
clip: string
|
||||
}>()
|
||||
|
||||
export default defineComponent({
|
||||
name: 'AudioClip',
|
||||
props: {
|
||||
clip: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
setup (props) {
|
||||
/** The full relative URL for the audio clip */
|
||||
const clipSource = `/audio/${props.clip}.mp3`
|
||||
/** The full relative URL for the audio clip */
|
||||
const clipSource = `/audio/${props.clip}.mp3`
|
||||
|
||||
/** Play the audio file */
|
||||
const playFile = () => {
|
||||
const audio = document.getElementById(props.clip) as HTMLAudioElement
|
||||
audio.play()
|
||||
}
|
||||
|
||||
return {
|
||||
clipSource,
|
||||
playFile
|
||||
}
|
||||
}
|
||||
})
|
||||
/** Play the audio file */
|
||||
const playFile = () => {
|
||||
const audio = document.getElementById(props.clip) as HTMLAudioElement
|
||||
audio.play()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="sass" scoped>
|
||||
|
@ -1,32 +1,27 @@
|
||||
<template lang="pug">
|
||||
.card: .card-body
|
||||
h6.card-title
|
||||
a(href='#' :class="{ 'cp-c': collapsed, 'cp-o': !collapsed }" @click.prevent='toggle') {{headerText}}
|
||||
slot(v-if='!collapsed')
|
||||
a(href="#" :class="{ 'cp-c': collapsed, 'cp-o': !collapsed }" @click.prevent="toggle") {{headerText}}
|
||||
slot(v-if="!collapsed")
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue'
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
headerText: string
|
||||
collapsed: boolean
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
name: 'CollapsePanel',
|
||||
emits: ['toggle'],
|
||||
props: {
|
||||
headerText: {
|
||||
type: String,
|
||||
default: 'Toggle'
|
||||
},
|
||||
collapsed: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
setup (props, { emit }) {
|
||||
return {
|
||||
toggle: () => emit('toggle', !props.collapsed)
|
||||
}
|
||||
}
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
headerText: "Toggle",
|
||||
collapsed: false
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "toggle") : void
|
||||
}>()
|
||||
|
||||
/** Emit the toggle event */
|
||||
const toggle = () => emit("toggle", !props.collapsed)
|
||||
</script>
|
||||
|
||||
<style lang="sass" scoped>
|
||||
|
@ -1,57 +1,58 @@
|
||||
<template lang="pug">
|
||||
.form-floating
|
||||
select.form-select(id='continentId' :class="{ 'is-invalid': isInvalid}" :value='continentId'
|
||||
@change='continentChanged')
|
||||
option(value='') – {{emptyLabel}} –
|
||||
option(v-for='c in continents' :key='c.id' :value='c.id') {{c.name}}
|
||||
label.jjj-required(for='continentId') Continent
|
||||
select.form-select(id="continentId" :class="{ 'is-invalid': isInvalid}" :value="continentId"
|
||||
@change="continentChanged")
|
||||
option(value="") – {{emptyLabel}} –
|
||||
option(v-for="c in continents" :key="c.id" :value="c.id") {{c.name}}
|
||||
label.jjj-required(for="continentId") Continent
|
||||
.invalid-feedback Please select a continent
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { useStore } from '@/store'
|
||||
import { computed, defineComponent, onMounted, ref } from 'vue'
|
||||
export default defineComponent({
|
||||
name: 'ContinentList',
|
||||
props: {
|
||||
modelValue: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
topLabel: { type: String },
|
||||
isInvalid: { type: Boolean }
|
||||
},
|
||||
emits: ['update:modelValue', 'touch'],
|
||||
setup (props, { emit }) {
|
||||
const store = useStore()
|
||||
<script setup lang="ts">
|
||||
import { useStore } from "@/store"
|
||||
import { computed, onMounted, ref } from "vue"
|
||||
|
||||
/** The continent ID, which this component can change */
|
||||
const continentId = ref(props.modelValue)
|
||||
interface Props {
|
||||
modelValue: string
|
||||
topLabel?: string
|
||||
isInvalid?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark the continent field as changed
|
||||
*
|
||||
* (This works around a really strange sequence where, if the "touch" call is directly wired up to the onChange
|
||||
* event, the first time a value is selected, it doesn't stick (although the field is marked as touched). On second
|
||||
* and subsequent times, it worked. The solution here is to grab the value and update the reactive source for the
|
||||
* form, then manually set the field to touched; this restores the expected behavior. This is probably why the
|
||||
* library doesn't hook into the onChange event to begin with...)
|
||||
*/
|
||||
const continentChanged = (e : Event) : boolean => {
|
||||
continentId.value = (e.target as HTMLSelectElement).value
|
||||
emit('touch')
|
||||
emit('update:modelValue', continentId.value)
|
||||
return true
|
||||
}
|
||||
|
||||
onMounted(async () => await store.dispatch('ensureContinents'))
|
||||
|
||||
return {
|
||||
continentId,
|
||||
continents: computed(() => store.state.continents),
|
||||
emptyLabel: props.topLabel || 'Select',
|
||||
continentChanged
|
||||
}
|
||||
}
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
isInvalid: false
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "update:modelValue", value : string) : void
|
||||
(e: "touch") : void
|
||||
}>()
|
||||
|
||||
const store = useStore()
|
||||
|
||||
/** The continent ID, which this component can change */
|
||||
const continentId = ref(props.modelValue)
|
||||
|
||||
/**
|
||||
* Mark the continent field as changed
|
||||
*
|
||||
* (This works around a really strange sequence where, if the "touch" call is directly wired up to the onChange event,
|
||||
* the first time a value is selected, it doesn't stick (although the field is marked as touched). On second and
|
||||
* subsequent times, it worked. The solution here is to grab the value and update the reactive source for the form, then
|
||||
* manually set the field to touched; this restores the expected behavior. This is probably why the library doesn't hook
|
||||
* into the onChange event to begin with...)
|
||||
*/
|
||||
const continentChanged = (e : Event) : boolean => {
|
||||
continentId.value = (e.target as HTMLSelectElement).value
|
||||
emit("touch")
|
||||
emit("update:modelValue", continentId.value)
|
||||
return true
|
||||
}
|
||||
|
||||
onMounted(async () => await store.dispatch("ensureContinents"))
|
||||
|
||||
/** Accessor for the continent list */
|
||||
const continents = computed(() => store.state.continents)
|
||||
|
||||
/** The label to use for the top entry in the list */
|
||||
const emptyLabel = props.topLabel ?? "Select"
|
||||
</script>
|
||||
|
@ -1,22 +1,14 @@
|
||||
<template lang="pug">
|
||||
template(v-if='errors.length > 0')
|
||||
p The following error#[template(v-if='errors.length !== 1') s] occurred:
|
||||
ul: li(v-for='(error, idx) in errors' :key='idx') {{error}}
|
||||
template(v-if="errors.length > 0")
|
||||
p The following error#[template(v-if="errors.length !== 1") s] occurred:
|
||||
ul: li(v-for="(error, idx) in errors" :key="idx") {{error}}
|
||||
slot(v-else)
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ErrorList',
|
||||
props: {
|
||||
errors: {
|
||||
type: Array,
|
||||
required: true
|
||||
}
|
||||
}
|
||||
})
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{
|
||||
errors: string[]
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<style lang="sass" scoped>
|
||||
|
@ -1,34 +1,15 @@
|
||||
<template lang="pug">
|
||||
template(v-if='true') {{formatted}}
|
||||
template(v-if="true") {{formatted}}
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue'
|
||||
import { format, parseJSON } from 'date-fns'
|
||||
import { utcToZonedTime } from 'date-fns-tz'
|
||||
<script setup lang="ts">
|
||||
import { format } from "date-fns"
|
||||
import { parseToUtc } from "./"
|
||||
|
||||
/**
|
||||
* Parse a date from its JSON representation to a UTC-aligned date
|
||||
*
|
||||
* @param date The date string in JSON from JSON
|
||||
* @returns A UTC JavaScript date
|
||||
*/
|
||||
export function parseToUtc (date : string) : Date {
|
||||
return utcToZonedTime(parseJSON(date), Intl.DateTimeFormat().resolvedOptions().timeZone)
|
||||
}
|
||||
const props = defineProps<{
|
||||
date: string
|
||||
}>()
|
||||
|
||||
export default defineComponent({
|
||||
name: 'FullDate',
|
||||
props: {
|
||||
date: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
setup (props) {
|
||||
return {
|
||||
formatted: format(parseToUtc(props.date), 'PPP')
|
||||
}
|
||||
}
|
||||
})
|
||||
/** The formatted date */
|
||||
const formatted = format(parseToUtc(props.date), "PPP")
|
||||
</script>
|
||||
|
@ -1,24 +1,15 @@
|
||||
<template lang="pug">
|
||||
template(v-if='true') {{formatted}}
|
||||
template(v-if="true") {{formatted}}
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue'
|
||||
import { format } from 'date-fns'
|
||||
import { parseToUtc } from './FullDate.vue'
|
||||
<script setup lang="ts">
|
||||
import { format } from "date-fns"
|
||||
import { parseToUtc } from "./"
|
||||
|
||||
export default defineComponent({
|
||||
name: 'FullDateTime',
|
||||
props: {
|
||||
date: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
setup (props) {
|
||||
return {
|
||||
formatted: format(parseToUtc(props.date), 'PPPp')
|
||||
}
|
||||
}
|
||||
})
|
||||
const props = defineProps<{
|
||||
date: string
|
||||
}>()
|
||||
|
||||
/** The formatted date/time */
|
||||
const formatted = format(parseToUtc(props.date), "PPPp")
|
||||
</script>
|
||||
|
@ -1,22 +1,12 @@
|
||||
<template lang="pug">
|
||||
span(:class='iconClass')
|
||||
span(:class="iconClass")
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue'
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{
|
||||
icon: string
|
||||
}>()
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Icon',
|
||||
props: {
|
||||
icon: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
setup (props) {
|
||||
return {
|
||||
iconClass: `mdi mdi-${props.icon}`
|
||||
}
|
||||
}
|
||||
})
|
||||
/** The CSS class to display the requested icon */
|
||||
const iconClass = `mdi mdi-${props.icon}`
|
||||
</script>
|
||||
|
@ -2,69 +2,62 @@
|
||||
form.container
|
||||
.row
|
||||
.col.col-xs-12.col-sm-6.col-md-4.col-lg-3
|
||||
continent-list(v-model='criteria.continentId' topLabel='Any' @update:modelValue='updateContinent')
|
||||
continent-list(v-model="criteria.continentId" topLabel="Any" @update:modelValue="updateContinent")
|
||||
.col.col-xs-12.col-sm-6.col-lg-3
|
||||
.form-floating
|
||||
input.form-control(type='text' id='regionSearch' placeholder='(free-form text)' :value='criteria.region'
|
||||
input.form-control(type="text" id="regionSearch" placeholder="(free-form text)" :value="criteria.region"
|
||||
@input="updateValue('region', $event.target.value)")
|
||||
label(for='regionSearch') Region
|
||||
label(for="regionSearch") Region
|
||||
.form-text (free-form text)
|
||||
.col.col-xs-12.col-sm-6.col-offset-md-2.col-lg-3.col-offset-lg-0
|
||||
label.jjj-label Remote Work Opportunity?
|
||||
br
|
||||
.form-check.form-check-inline
|
||||
input.form-check-input(type='radio' id='remoteNull' name='remoteWork' :checked="criteria.remoteWork === ''"
|
||||
input.form-check-input(type="radio" id="remoteNull" name="remoteWork" :checked="criteria.remoteWork === ''"
|
||||
@click="updateValue('remoteWork', '')")
|
||||
label.form-check-label(for='remoteNull') No Selection
|
||||
label.form-check-label(for="remoteNull") No Selection
|
||||
.form-check.form-check-inline
|
||||
input.form-check-input(type='radio' id='remoteYes' name='remoteWork' :checked="criteria.remoteWork === 'yes'"
|
||||
input.form-check-input(type="radio" id="remoteYes" name="remoteWork" :checked="criteria.remoteWork === 'yes'"
|
||||
@click="updateValue('remoteWork', 'yes')")
|
||||
label.form-check-label(for='remoteYes') Yes
|
||||
label.form-check-label(for="remoteYes") Yes
|
||||
.form-check.form-check-inline
|
||||
input.form-check-input(type='radio' id='remoteNo' name='remoteWork' :checked="criteria.remoteWork === 'no'"
|
||||
input.form-check-input(type="radio" id="remoteNo" name="remoteWork" :checked="criteria.remoteWork === 'no'"
|
||||
@click="updateValue('remoteWork', 'no')")
|
||||
label.form-check-label(for='remoteNo') No
|
||||
label.form-check-label(for="remoteNo") No
|
||||
.col.col-xs-12.col-sm-6.col-lg-3
|
||||
.form-floating
|
||||
input.form-control(type='text' id='textSearch' placeholder='(free-form text)' :value='criteria.text'
|
||||
input.form-control(type="text" id="textSearch" placeholder="(free-form text)" :value="criteria.text"
|
||||
@input="updateValue('text', $event.target.value)")
|
||||
label(for='textSearch') Job Listing Text
|
||||
label(for="textSearch") Job Listing Text
|
||||
.form-text (free-form text)
|
||||
.row: .col.col-xs-12
|
||||
br
|
||||
button.btn.btn-outline-primary(type='submit' @click.prevent="$emit('search')") Search
|
||||
button.btn.btn-outline-primary(type="submit" @click.prevent="$emit('search')") Search
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { ListingSearch } from '@/api'
|
||||
import { defineComponent, ref, Ref } from 'vue'
|
||||
import ContinentList from './ContinentList.vue'
|
||||
<script setup lang="ts">
|
||||
import { ListingSearch } from "@/api"
|
||||
import { ref } from "vue"
|
||||
import ContinentList from "./ContinentList.vue"
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ListingSearchForm',
|
||||
components: { ContinentList },
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
emits: ['search', 'update:modelValue'],
|
||||
setup (props, { emit }) {
|
||||
/** The initial search criteria passed; this is what we'll update and emit when data changes */
|
||||
const criteria : Ref<ListingSearch> = ref({ ...props.modelValue as ListingSearch })
|
||||
const props = defineProps<{
|
||||
modelValue: ListingSearch
|
||||
}>()
|
||||
|
||||
/** Emit a value update */
|
||||
const updateValue = (key : string, value : string) => {
|
||||
criteria.value = { ...criteria.value, [key]: value }
|
||||
emit('update:modelValue', criteria.value)
|
||||
}
|
||||
const emit = defineEmits<{
|
||||
(e: "search") : void
|
||||
(e: "update:modelValue", value : ListingSearch) : void
|
||||
}>()
|
||||
|
||||
return {
|
||||
criteria,
|
||||
updateContinent: (c : string) => updateValue('continentId', c),
|
||||
updateValue
|
||||
}
|
||||
}
|
||||
})
|
||||
/** The initial search criteria passed; this is what we'll update and emit when data changes */
|
||||
const criteria = ref({ ...props.modelValue })
|
||||
|
||||
/** Emit a value update */
|
||||
const updateValue = (key : string, value : string) => {
|
||||
criteria.value = { ...criteria.value, [key]: value }
|
||||
emit("update:modelValue", criteria.value)
|
||||
}
|
||||
|
||||
/** Update the continent ID */
|
||||
const updateContinent = (c : string) => updateValue("continentId", c)
|
||||
</script>
|
||||
|
@ -1,47 +1,31 @@
|
||||
<template lang="pug">
|
||||
div(v-if='loading') Loading…
|
||||
error-list(v-else :errors='errors')
|
||||
div(v-if="loading") Loading…
|
||||
error-list(v-else :errors="errors")
|
||||
slot
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, onMounted, ref } from 'vue'
|
||||
import ErrorList from './ErrorList.vue'
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from "vue"
|
||||
import ErrorList from "./ErrorList.vue"
|
||||
|
||||
export default defineComponent({
|
||||
name: 'LoadData',
|
||||
components: { ErrorList },
|
||||
props: {
|
||||
load: {
|
||||
type: Function,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
setup (props) {
|
||||
/** Type the input function */
|
||||
const func = props.load as (errors: string[]) => Promise<unknown>
|
||||
const props = defineProps<{
|
||||
load: (errors : string[]) => Promise<unknown>
|
||||
}>()
|
||||
|
||||
/** Errors encountered during loading */
|
||||
const errors : string[] = []
|
||||
/** Errors encountered during loading */
|
||||
const errors : string[] = []
|
||||
|
||||
/** Whether we are currently loading data */
|
||||
const loading = ref(true)
|
||||
/** Whether we are currently loading data */
|
||||
const loading = ref(true)
|
||||
|
||||
/** Call the data load function */
|
||||
const loadData = async () => {
|
||||
try {
|
||||
await func(errors)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(loadData)
|
||||
|
||||
return {
|
||||
loading,
|
||||
errors
|
||||
}
|
||||
/** Call the data load function */
|
||||
const loadData = async () => {
|
||||
try {
|
||||
await props.load(errors)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(loadData)
|
||||
</script>
|
||||
|
@ -1,74 +1,60 @@
|
||||
<template lang="pug">
|
||||
.col-12
|
||||
nav.nav.nav-pills.pb-1
|
||||
button(:class='sourceClass' @click.prevent='showMarkdown') Markdown
|
||||
button(:class="sourceClass" @click.prevent="showMarkdown") Markdown
|
||||
|
|
||||
button(:class='previewClass' @click.prevent='showPreview') Preview
|
||||
section.preview(v-if='preview' v-html='previewHtml')
|
||||
button(:class="previewClass" @click.prevent="showPreview") Preview
|
||||
section.preview(v-if="preview" v-html="previewHtml")
|
||||
.form-floating(v-else)
|
||||
textarea(:id='id' :class="{ 'form-control': true, 'md-edit': true, 'is-invalid': isInvalid }" rows='10'
|
||||
v-text='text' @input="$emit('update:text', $event.target.value)")
|
||||
textarea.form-control.md-edit(:id="id" :class="{ 'is-invalid': isInvalid }" rows="10" v-text="text"
|
||||
@input="$emit('update:text', $event.target.value)")
|
||||
.invalid-feedback Please enter some text for {{label}}
|
||||
label(:for='id') {{label}}
|
||||
label(:for="id") {{label}}
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, ref } from 'vue'
|
||||
import marked from 'marked'
|
||||
import { markedOptions } from '@/api'
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from "vue"
|
||||
import { toHtml } from "@/markdown"
|
||||
|
||||
export default defineComponent({
|
||||
name: 'MarkdownEditor',
|
||||
props: {
|
||||
id: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
text: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
isInvalid: { type: Boolean }
|
||||
},
|
||||
emits: ['update:text'],
|
||||
setup (props) {
|
||||
/** Whether to show the Markdown preview */
|
||||
const preview = ref(false)
|
||||
const props = defineProps<{
|
||||
id: string
|
||||
text: string
|
||||
label: string
|
||||
isInvalid?: boolean
|
||||
}>()
|
||||
|
||||
/** The HTML rendered for preview purposes */
|
||||
const previewHtml = ref('')
|
||||
const emit = defineEmits<{
|
||||
(e: "update:text", value : string) : void
|
||||
}>()
|
||||
|
||||
/** Show the Markdown source */
|
||||
const showMarkdown = () => {
|
||||
preview.value = false
|
||||
}
|
||||
/** Whether to show the Markdown preview */
|
||||
const preview = ref(false)
|
||||
|
||||
/** Show the Markdown preview */
|
||||
const showPreview = () => {
|
||||
previewHtml.value = marked(props.text, markedOptions)
|
||||
preview.value = true
|
||||
}
|
||||
/** The HTML rendered for preview purposes */
|
||||
const previewHtml = ref("")
|
||||
|
||||
/** Button classes for the selected button */
|
||||
const selected = 'btn btn-primary btn-sm rounded-pill'
|
||||
/** Show the Markdown source */
|
||||
const showMarkdown = () => {
|
||||
preview.value = false
|
||||
}
|
||||
|
||||
/** Button classes for the unselected button */
|
||||
const unselected = 'btn btn-outline-secondary btn-sm rounded-pill'
|
||||
/** Show the Markdown preview */
|
||||
const showPreview = () => {
|
||||
previewHtml.value = toHtml(props.text)
|
||||
preview.value = true
|
||||
}
|
||||
|
||||
return {
|
||||
preview,
|
||||
previewHtml,
|
||||
showMarkdown,
|
||||
showPreview,
|
||||
sourceClass: computed(() => preview.value ? unselected : selected),
|
||||
previewClass: computed(() => preview.value ? selected : unselected)
|
||||
}
|
||||
}
|
||||
})
|
||||
/** Button classes for the selected button */
|
||||
const selected = "btn btn-primary btn-sm rounded-pill"
|
||||
|
||||
/** Button classes for the unselected button */
|
||||
const unselected = "btn btn-outline-secondary btn-sm rounded-pill"
|
||||
|
||||
/** The CSS class for the Markdown source button */
|
||||
const sourceClass = computed(() => preview.value ? unselected : selected)
|
||||
|
||||
/** The CSS class for the Markdown preview button */
|
||||
const previewClass = computed(() => preview.value ? selected : unselected)
|
||||
</script>
|
||||
|
||||
<style lang="sass" scoped>
|
||||
|
@ -1,82 +1,67 @@
|
||||
<template lang="pug">
|
||||
.modal.fade(id='maybeSaveModal' tabindex='-1' aria-labelledby='maybeSaveLabel' aria-hidden='true'): .modal-dialog: .modal-content
|
||||
.modal-header: h5.modal-title(id='maybeSaveLabel') Unsaved Changes
|
||||
.modal.fade(id="maybeSaveModal" tabindex="-1" aria-labelledby="maybeSaveLabel" aria-hidden="true"): .modal-dialog: .modal-content
|
||||
.modal-header: h5.modal-title(id="maybeSaveLabel") Unsaved Changes
|
||||
.modal-body You have modified the data on this page since it was last saved. What would you like to do?
|
||||
.modal-footer
|
||||
button.btn.btn-secondary(type='button' @click.prevent='onStay') Stay on This Page
|
||||
button.btn.btn-primary(type='button' @click.prevent='onSave') Save Changes
|
||||
button.btn.btn-danger(type='button' @click.prevent='onDiscard') Discard Changes
|
||||
button.btn.btn-secondary(type="button" @click.prevent="onStay") Stay on This Page
|
||||
button.btn.btn-primary(type="button" @click.prevent="onSave") Save Changes
|
||||
button.btn.btn-danger(type="button" @click.prevent="onDiscard") Discard Changes
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, onMounted, ref, Ref, watch } from 'vue'
|
||||
import { RouteLocationNormalized, useRouter } from 'vue-router'
|
||||
import { Validation } from '@vuelidate/core'
|
||||
import { Modal } from 'bootstrap'
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref, Ref, watch } from "vue"
|
||||
import { RouteLocationNormalized, useRouter } from "vue-router"
|
||||
import { Validation } from "@vuelidate/core"
|
||||
import { Modal } from "bootstrap"
|
||||
|
||||
export default defineComponent({
|
||||
name: 'MaybeSave',
|
||||
props: {
|
||||
isShown: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
},
|
||||
toRoute: {
|
||||
// Can't type this because it's not filled until just before the modal is shown
|
||||
required: true
|
||||
},
|
||||
saveAction: {
|
||||
type: Function
|
||||
},
|
||||
validator: {
|
||||
type: Object
|
||||
}
|
||||
},
|
||||
emits: ['close', 'discard', 'cancel'],
|
||||
setup (props, { emit }) {
|
||||
const router = useRouter()
|
||||
const props = defineProps<{
|
||||
isShown: boolean
|
||||
toRoute: RouteLocationNormalized
|
||||
saveAction?: () => Promise<unknown>
|
||||
validator?: Validation
|
||||
}>()
|
||||
|
||||
/** The route where we tried to go */
|
||||
const newRoute = computed(() => props.toRoute as RouteLocationNormalized)
|
||||
const emit = defineEmits<{
|
||||
(e: "close") : void
|
||||
(e: "discard") : void
|
||||
(e: "cancel") : void
|
||||
}>()
|
||||
|
||||
/** Reference to the modal dialog (we can't get it until the component is rendered) */
|
||||
const modal : Ref<Modal | undefined> = ref(undefined)
|
||||
const router = useRouter()
|
||||
|
||||
/** Save changes (if required) and go to the next route */
|
||||
const onSave = async () => {
|
||||
if (props.saveAction) await Promise.resolve(props.saveAction())
|
||||
emit('close')
|
||||
router.push(newRoute.value)
|
||||
}
|
||||
/** Reference to the modal dialog (we can't get it until the component is rendered) */
|
||||
const modal : Ref<Modal | undefined> = ref(undefined)
|
||||
|
||||
/** Discard changes (if required) and go to the next route */
|
||||
const onDiscard = () => {
|
||||
if (props.validator) (props.validator as Validation).$reset()
|
||||
emit('close')
|
||||
router.push(newRoute.value)
|
||||
}
|
||||
/** Save changes (if required) and go to the next route */
|
||||
const onSave = async () => {
|
||||
if (props.saveAction) await props.saveAction()
|
||||
emit("close")
|
||||
router.push(props.toRoute)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
modal.value = new Modal(document.getElementById('maybeSaveModal') as HTMLElement,
|
||||
{ backdrop: 'static', keyboard: false })
|
||||
})
|
||||
/** Discard changes (if required) and go to the next route */
|
||||
const onDiscard = () => {
|
||||
if (props.validator) props.validator.$reset()
|
||||
emit("close")
|
||||
router.push(props.toRoute)
|
||||
}
|
||||
|
||||
/** Show or hide the modal based on the property value changing */
|
||||
watch(() => props.isShown, (toShow) => {
|
||||
if (modal.value) {
|
||||
if (toShow) {
|
||||
modal.value.show()
|
||||
} else {
|
||||
modal.value.hide()
|
||||
}
|
||||
}
|
||||
})
|
||||
onMounted(() => {
|
||||
modal.value = new Modal(document.getElementById("maybeSaveModal") as HTMLElement,
|
||||
{ backdrop: "static", keyboard: false })
|
||||
})
|
||||
|
||||
return {
|
||||
onStay: () => emit('close'),
|
||||
onSave,
|
||||
onDiscard
|
||||
/** Show or hide the modal based on the property value changing */
|
||||
watch(() => props.isShown, (toShow) => {
|
||||
if (modal.value) {
|
||||
if (toShow) {
|
||||
modal.value.show()
|
||||
} else {
|
||||
modal.value.hide()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
/** Stay on this page with no changes; just close the modal */
|
||||
const onStay = () => emit("close")
|
||||
</script>
|
||||
|
@ -1,39 +1,28 @@
|
||||
<template lang="pug">
|
||||
p(v-if='false')
|
||||
p(v-if="false")
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, onMounted } from 'vue'
|
||||
<script setup lang="ts">
|
||||
import { onMounted, watch } from "vue"
|
||||
|
||||
export default defineComponent({
|
||||
name: 'PageTitle',
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
setup (props) {
|
||||
/** The name of the application */
|
||||
const appName = 'Jobs, Jobs, Jobs'
|
||||
const props = defineProps<{
|
||||
title: string
|
||||
}>()
|
||||
|
||||
/** Set the page title based on the input title attribute */
|
||||
const setTitle = () => {
|
||||
if (props.title === '') {
|
||||
document.title = appName
|
||||
} else {
|
||||
document.title = `${props.title} | ${appName}`
|
||||
}
|
||||
}
|
||||
/** The name of the application */
|
||||
const appName = "Jobs, Jobs, Jobs"
|
||||
|
||||
onMounted(setTitle)
|
||||
|
||||
return {
|
||||
setTitle
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
title: 'setTitle'
|
||||
/** Set the page title based on the input title attribute */
|
||||
const setTitle = () => {
|
||||
if (props.title === "") {
|
||||
document.title = appName
|
||||
} else {
|
||||
document.title = `${props.title} | ${appName}`
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(setTitle)
|
||||
|
||||
/** Change the page title when the property changes */
|
||||
watch(() => props.title, setTitle)
|
||||
</script>
|
||||
|
12
src/JobsJobsJobs/App/src/components/index.ts
Normal file
12
src/JobsJobsJobs/App/src/components/index.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { parseJSON } from 'date-fns'
|
||||
import { utcToZonedTime } from 'date-fns-tz'
|
||||
|
||||
/**
|
||||
* Parse a date from its JSON representation to a UTC-aligned date
|
||||
*
|
||||
* @param date The date string in JSON from JSON
|
||||
* @returns A UTC JavaScript date
|
||||
*/
|
||||
export function parseToUtc (date : string) : Date {
|
||||
return utcToZonedTime(parseJSON(date), Intl.DateTimeFormat().resolvedOptions().timeZone)
|
||||
}
|
@ -1,25 +1,16 @@
|
||||
<template lang="pug">
|
||||
footer: p.text-muted.
|
||||
Jobs, Jobs, Jobs v{{appVersion}} • #[router-link(to='/privacy-policy') Privacy Policy]
|
||||
• #[router-link(to='/terms-of-service') Terms of Service]
|
||||
Jobs, Jobs, Jobs v{{appVersion}} • #[router-link(to="/privacy-policy") Privacy Policy]
|
||||
• #[router-link(to="/terms-of-service") Terms of Service]
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue'
|
||||
import { version } from '../../../package.json'
|
||||
<script setup lang="ts">
|
||||
import { version } from "../../../package.json"
|
||||
|
||||
export default defineComponent({
|
||||
name: 'AppFooter',
|
||||
setup () {
|
||||
let appVersion : string = version
|
||||
while (appVersion.endsWith('.0')) {
|
||||
appVersion = appVersion.substring(0, appVersion.length - 2)
|
||||
}
|
||||
return {
|
||||
appVersion
|
||||
}
|
||||
}
|
||||
})
|
||||
let appVersion : string = version
|
||||
while (appVersion.endsWith(".0")) {
|
||||
appVersion = appVersion.substring(0, appVersion.length - 2)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="sass" scoped>
|
||||
|
@ -1,40 +1,33 @@
|
||||
<template lang="pug">
|
||||
aside.collapse.show.p-3
|
||||
p.home-link.pb-3: router-link(to='/') Jobs, Jobs, Jobs
|
||||
p.home-link.pb-3: router-link(to="/") Jobs, Jobs, Jobs
|
||||
p
|
||||
nav
|
||||
template(v-if='isLoggedOn')
|
||||
router-link(to='/citizen/dashboard') #[icon(icon='view-dashboard-variant')] Dashboard
|
||||
router-link(to='/help-wanted') #[icon(icon='newspaper-variant-multiple-outline')] Help Wanted!
|
||||
router-link(to='/profile/search') #[icon(icon='view-list-outline')] Employment Profiles
|
||||
router-link(to='/success-story/list') #[icon(icon='thumb-up')] Success Stories
|
||||
template(v-if="isLoggedOn")
|
||||
router-link(to="/citizen/dashboard") #[icon(icon="view-dashboard-variant")] Dashboard
|
||||
router-link(to="/help-wanted") #[icon(icon="newspaper-variant-multiple-outline")] Help Wanted!
|
||||
router-link(to="/profile/search") #[icon(icon="view-list-outline")] Employment Profiles
|
||||
router-link(to="/success-story/list") #[icon(icon="thumb-up")] Success Stories
|
||||
.separator
|
||||
router-link(to='/listings/mine') #[icon(icon='sign-text')] My Job Listings
|
||||
router-link(to='/citizen/profile') #[icon(icon='pencil')] My Employment Profile
|
||||
router-link(to="/listings/mine") #[icon(icon="sign-text")] My Job Listings
|
||||
router-link(to="/citizen/profile") #[icon(icon="pencil")] My Employment Profile
|
||||
.separator
|
||||
router-link(to='/citizen/log-off') #[icon(icon='logout-variant')] Log Off
|
||||
router-link(to="/citizen/log-off") #[icon(icon="logout-variant")] Log Off
|
||||
template(v-else)
|
||||
router-link(to='/') #[icon(icon='home')] Home
|
||||
router-link(to='/profile/seeking') #[icon(icon='view-list-outline')] Job Seekers
|
||||
router-link(to='/citizen/log-on') #[icon(icon='login-variant')] Log On
|
||||
router-link(to='/how-it-works') #[icon(icon='help-circle-outline')] How It Works
|
||||
router-link(to="/") #[icon(icon="home")] Home
|
||||
router-link(to="/profile/seeking") #[icon(icon="view-list-outline")] Job Seekers
|
||||
router-link(to="/citizen/log-on") #[icon(icon="login-variant")] Log On
|
||||
router-link(to="/how-it-works") #[icon(icon="help-circle-outline")] How It Works
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent } from 'vue'
|
||||
import { useStore } from '@/store'
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue"
|
||||
import { useStore } from "@/store"
|
||||
|
||||
export default defineComponent({
|
||||
name: 'AppNav',
|
||||
setup () {
|
||||
const store = useStore()
|
||||
const store = useStore()
|
||||
|
||||
return {
|
||||
/** Whether a user is logged in or not */
|
||||
isLoggedOn: computed(() => store.state.user !== undefined)
|
||||
}
|
||||
}
|
||||
})
|
||||
/** Whether a user is logged in or not */
|
||||
const isLoggedOn = computed(() => store.state.user !== undefined)
|
||||
</script>
|
||||
|
||||
<style lang="sass" scoped>
|
||||
|
@ -1,50 +1,50 @@
|
||||
<template lang="pug">
|
||||
div(aria-live='polite' aria-atomic='true' id='toastHost')
|
||||
.toast-container.position-absolute.p-3.bottom-0.start-50.translate-middle-x(id='toasts')
|
||||
div(aria-live="polite" aria-atomic="true" id="toastHost")
|
||||
.toast-container.position-absolute.p-3.bottom-0.start-50.translate-middle-x(id="toasts")
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue'
|
||||
import { Toast } from 'bootstrap'
|
||||
import { defineComponent } from "vue"
|
||||
import { Toast } from "bootstrap"
|
||||
|
||||
/** Remove a toast once it's hidden */
|
||||
const removeToast = (event : Event) => (event.target as HTMLDivElement).remove()
|
||||
|
||||
/** Create a toast, add it to the DOM, and show it */
|
||||
const createToast = (level : 'success' | 'warning' | 'danger', message : string, process : string | undefined) => {
|
||||
const createToast = (level : "success" | "warning" | "danger", message : string, process : string | undefined) => {
|
||||
let header : HTMLDivElement | undefined
|
||||
if (level !== 'success') {
|
||||
if (level !== "success") {
|
||||
// Create a heading, optionally including the process that generated the message
|
||||
const heading = (typ : string) : string => {
|
||||
const proc = process ? ` (${process})` : ''
|
||||
const proc = process ? ` (${process})` : ""
|
||||
return `<span class="me-auto"><strong>${typ.toUpperCase()}</strong>${proc}</span>`
|
||||
}
|
||||
header = document.createElement('div')
|
||||
header.className = 'toast-header'
|
||||
header.innerHTML = heading(level === 'warning' ? level : 'error')
|
||||
header = document.createElement("div")
|
||||
header.className = "toast-header"
|
||||
header.innerHTML = heading(level === "warning" ? level : "error")
|
||||
// Include a close button, as these will not auto-close
|
||||
const close = document.createElement('button')
|
||||
close.type = 'button'
|
||||
close.className = 'btn-close'
|
||||
close.setAttribute('data-bs-dismiss', 'toast')
|
||||
close.setAttribute('aria-label', 'Close')
|
||||
const close = document.createElement("button")
|
||||
close.type = "button"
|
||||
close.className = "btn-close"
|
||||
close.setAttribute("data-bs-dismiss", "toast")
|
||||
close.setAttribute("aria-label", "Close")
|
||||
header.appendChild(close)
|
||||
}
|
||||
const body = document.createElement('div')
|
||||
body.className = 'toast-body'
|
||||
const body = document.createElement("div")
|
||||
body.className = "toast-body"
|
||||
body.innerHTML = message
|
||||
|
||||
const toastEl = document.createElement('div')
|
||||
const toastEl = document.createElement("div")
|
||||
toastEl.className = `toast bg-${level} text-white`
|
||||
toastEl.setAttribute('role', 'alert')
|
||||
toastEl.setAttribute('aria-live', 'assertlive')
|
||||
toastEl.setAttribute('aria-atomic', 'true')
|
||||
toastEl.addEventListener('hidden.bs.toast', removeToast)
|
||||
toastEl.setAttribute("role", "alert")
|
||||
toastEl.setAttribute("aria-live", "assertlive")
|
||||
toastEl.setAttribute("aria-atomic", "true")
|
||||
toastEl.addEventListener("hidden.bs.toast", removeToast)
|
||||
if (header) toastEl.appendChild(header)
|
||||
toastEl.appendChild(body)
|
||||
|
||||
;(document.getElementById('toasts') as HTMLDivElement).appendChild(toastEl)
|
||||
new Toast(toastEl, { autohide: level === 'success' }).show()
|
||||
;(document.getElementById("toasts") as HTMLDivElement).appendChild(toastEl)
|
||||
new Toast(toastEl, { autohide: level === "success" }).show()
|
||||
}
|
||||
|
||||
/**
|
||||
@ -53,7 +53,7 @@ const createToast = (level : 'success' | 'warning' | 'danger', message : string,
|
||||
* @param message The message to be displayed
|
||||
*/
|
||||
export function toastSuccess (message : string) : void {
|
||||
createToast('success', message, undefined)
|
||||
createToast("success", message, undefined)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -63,7 +63,7 @@ export function toastSuccess (message : string) : void {
|
||||
* @param process The process which generated the warning (optional)
|
||||
*/
|
||||
export function toastWarning (message : string, process : string | undefined) : void {
|
||||
createToast('warning', message, process)
|
||||
createToast("warning", message, process)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -73,11 +73,11 @@ export function toastWarning (message : string, process : string | undefined) :
|
||||
* @param process The process which generated the error (optional)
|
||||
*/
|
||||
export function toastError (message : string, process : string | undefined) : void {
|
||||
createToast('danger', message, process)
|
||||
createToast("danger", message, process)
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
name: 'AppToaster'
|
||||
name: "AppToaster"
|
||||
})
|
||||
</script>
|
||||
|
||||
|
@ -2,19 +2,11 @@
|
||||
nav.navbar.navbar-light.bg-light
|
||||
span
|
||||
span.navbar-text.
|
||||
(...and Jobs – #[audio-clip(clip='pelosi-jobs') Let's Vote for Jobs!])
|
||||
(…and Jobs – #[audio-clip(clip="pelosi-jobs") Let's Vote for Jobs!])
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue'
|
||||
import AudioClip from '@/components/AudioClip.vue'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'TitleBar',
|
||||
components: {
|
||||
AudioClip
|
||||
}
|
||||
})
|
||||
<script setup lang="ts">
|
||||
import AudioClip from "@/components/AudioClip.vue"
|
||||
</script>
|
||||
|
||||
<style lang="sass" scoped>
|
||||
|
@ -2,69 +2,62 @@
|
||||
form.container
|
||||
.row
|
||||
.col.col-xs-12.col-sm-6.col-md-4.col-lg-3
|
||||
continent-list(v-model='criteria.continentId' topLabel='Any' @update:modelValue='updateContinent')
|
||||
continent-list(v-model="criteria.continentId" topLabel="Any" @update:modelValue="updateContinent")
|
||||
.col.col-xs-12.col-sm-6.col-md-4.col-lg-3
|
||||
.form-floating
|
||||
input.form-control.form-control-sm(type='text' id='region' placeholder='(free-form text)'
|
||||
:value='criteria.region' @input="updateValue('region', $event.target.value)")
|
||||
label(for='region') Region
|
||||
input.form-control.form-control-sm(type="text" id="region" placeholder="(free-form text)"
|
||||
:value="criteria.region" @input="updateValue('region', $event.target.value)")
|
||||
label(for="region") Region
|
||||
.form-text (free-form text)
|
||||
.col.col-xs-12.col-sm-6.col-offset-md-2.col-lg-3.col-offset-lg-0
|
||||
label.jjj-label Seeking Remote Work?
|
||||
br
|
||||
.form-check.form-check-inline
|
||||
input.form-check-input(type='radio' id='remoteNull' name='remoteWork' :checked="criteria.remoteWork === ''"
|
||||
input.form-check-input(type="radio" id="remoteNull" name="remoteWork" :checked="criteria.remoteWork === ''"
|
||||
@click="updateValue('remoteWork', '')")
|
||||
label.form-check-label(for='remoteNull') No Selection
|
||||
label.form-check-label(for="remoteNull") No Selection
|
||||
.form-check.form-check-inline
|
||||
input.form-check-input(type='radio' id='remoteYes' name='remoteWork' :checked="criteria.remoteWork === 'yes'"
|
||||
input.form-check-input(type="radio" id="remoteYes" name="remoteWork" :checked="criteria.remoteWork === 'yes'"
|
||||
@click="updateValue('remoteWork', 'yes')")
|
||||
label.form-check-label(for='remoteYes') Yes
|
||||
label.form-check-label(for="remoteYes") Yes
|
||||
.form-check.form-check-inline
|
||||
input.form-check-input(type='radio' id='remoteNo' name='remoteWork' :checked="criteria.remoteWork === 'no'"
|
||||
input.form-check-input(type="radio" id="remoteNo" name="remoteWork" :checked="criteria.remoteWork === 'no'"
|
||||
@click="updateValue('remoteWork', 'no')")
|
||||
label.form-check-label(for='remoteNo') No
|
||||
label.form-check-label(for="remoteNo") No
|
||||
.col.col-xs-12.col-sm-6.col-lg-3
|
||||
.form-floating
|
||||
input.form-control.form-control-sm(type='text' id='skillSearch' placeholder="(free-form text)"
|
||||
input.form-control.form-control-sm(type="text" id="skillSearch" placeholder="(free-form text)"
|
||||
:value="criteria.skill" @input="updateValue('skill', $event.target.value)")
|
||||
label(for='skillSearch') Skill
|
||||
label(for="skillSearch") Skill
|
||||
.form-text (free-form text)
|
||||
.row: .col.col-xs-12
|
||||
br
|
||||
button.btn.btn-outline-primary(type='submit' @click.prevent="$emit('search')") Search
|
||||
button.btn.btn-outline-primary(type="submit" @click.prevent="$emit('search')") Search
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, Ref } from 'vue'
|
||||
import { PublicSearch } from '@/api'
|
||||
import ContinentList from '../ContinentList.vue'
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue"
|
||||
import { PublicSearch } from "@/api"
|
||||
import ContinentList from "../ContinentList.vue"
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ProfilePublicSearchForm',
|
||||
components: { ContinentList },
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
emits: ['search', 'update:modelValue'],
|
||||
setup (props, { emit }) {
|
||||
/** The initial search criteria passed; this is what we'll update and emit when data changes */
|
||||
const criteria : Ref<PublicSearch> = ref({ ...props.modelValue as PublicSearch })
|
||||
const props = defineProps<{
|
||||
modelValue: PublicSearch
|
||||
}>()
|
||||
|
||||
/** Emit a value update */
|
||||
const updateValue = (key : string, value : string) => {
|
||||
criteria.value = { ...criteria.value, [key]: value }
|
||||
emit('update:modelValue', criteria.value)
|
||||
}
|
||||
const emit = defineEmits<{
|
||||
(e: "search") : void
|
||||
(e: "update:modelValue", value : PublicSearch) : void
|
||||
}>()
|
||||
|
||||
return {
|
||||
criteria,
|
||||
updateContinent: (c : string) => updateValue('continentId', c),
|
||||
updateValue
|
||||
}
|
||||
}
|
||||
})
|
||||
/** The initial search criteria passed; this is what we'll update and emit when data changes */
|
||||
const criteria = ref({ ...props.modelValue })
|
||||
|
||||
/** Emit a value update */
|
||||
const updateValue = (key : string, value : string) => {
|
||||
criteria.value = { ...criteria.value, [key]: value }
|
||||
emit("update:modelValue", criteria.value)
|
||||
}
|
||||
|
||||
/** Update the continent ID */
|
||||
const updateContinent = (c : string) => updateValue("continentId", c)
|
||||
</script>
|
||||
|
@ -2,69 +2,62 @@
|
||||
form.container
|
||||
.row
|
||||
.col.col-xs-12.col-sm-6.col-md-4.col-lg-3
|
||||
continent-list(v-model='criteria.continentId' topLabel='Any' @update:modelValue='updateContinent')
|
||||
continent-list(v-model="criteria.continentId" topLabel="Any" @update:modelValue="updateContinent")
|
||||
.col.col-xs-12.col-sm-6.col-offset-md-2.col-lg-3.col-offset-lg-0
|
||||
label.jjj-label Seeking Remote Work?
|
||||
br
|
||||
.form-check.form-check-inline
|
||||
input.form-check-input(type='radio' id='remoteNull' name='remoteWork' :checked="criteria.remoteWork === ''"
|
||||
input.form-check-input(type="radio" id="remoteNull" name="remoteWork" :checked="criteria.remoteWork === ''"
|
||||
@click="updateValue('remoteWork', '')")
|
||||
label.form-check-label(for='remoteNull') No Selection
|
||||
label.form-check-label(for="remoteNull") No Selection
|
||||
.form-check.form-check-inline
|
||||
input.form-check-input(type='radio' id='remoteYes' name='remoteWork' :checked="criteria.remoteWork === 'yes'"
|
||||
input.form-check-input(type="radio" id="remoteYes" name="remoteWork" :checked="criteria.remoteWork === 'yes'"
|
||||
@click="updateValue('remoteWork', 'yes')")
|
||||
label.form-check-label(for='remoteYes') Yes
|
||||
label.form-check-label(for="remoteYes") Yes
|
||||
.form-check.form-check-inline
|
||||
input.form-check-input(type='radio' id='remoteNo' name='remoteWork' :checked="criteria.remoteWork === 'no'"
|
||||
input.form-check-input(type="radio" id="remoteNo" name="remoteWork" :checked="criteria.remoteWork === 'no'"
|
||||
@click="updateValue('remoteWork', 'no')")
|
||||
label.form-check-label(for='remoteNo') No
|
||||
label.form-check-label(for="remoteNo") No
|
||||
.col.col-xs-12.col-sm-6.col-lg-3
|
||||
.form-floating
|
||||
input.form-control(type='text' id='skillSearch' placeholder="(free-form text)" :value='criteria.skill'
|
||||
input.form-control(type="text" id="skillSearch" placeholder="(free-form text)" :value="criteria.skill"
|
||||
@input="updateValue('skill', $event.target.value)")
|
||||
label(for='skillSearch') Skill
|
||||
label(for="skillSearch") Skill
|
||||
.form-text (free-form text)
|
||||
.col.col-xs-12.col-sm-6.col-lg-3
|
||||
.form-floating
|
||||
input.form-control(type='text' id='bioSearch' placeholder="(free-form text)" :value='criteria.bioExperience'
|
||||
input.form-control(type="text" id="bioSearch" placeholder="(free-form text)" :value="criteria.bioExperience"
|
||||
@input="updateValue('bioExperience', $event.target.value)")
|
||||
label(for='bioSearch') Bio / Experience
|
||||
label(for="bioSearch") Bio / Experience
|
||||
.form-text (free-form text)
|
||||
.row: .col.col-xs-12
|
||||
br
|
||||
button.btn.btn-outline-primary(type='submit' @click.prevent="$emit('search')") Search
|
||||
button.btn.btn-outline-primary(type="submit" @click.prevent="$emit('search')") Search
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, Ref, ref } from 'vue'
|
||||
import { ProfileSearch } from '@/api'
|
||||
import ContinentList from '../ContinentList.vue'
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue"
|
||||
import { ProfileSearch } from "@/api"
|
||||
import ContinentList from "../ContinentList.vue"
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ProfileSearchForm',
|
||||
components: { ContinentList },
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
emits: ['search', 'update:modelValue'],
|
||||
setup (props, { emit }) {
|
||||
/** The initial search criteria passed; this is what we'll update and emit when data changes */
|
||||
const criteria : Ref<ProfileSearch> = ref({ ...props.modelValue as ProfileSearch })
|
||||
const props = defineProps<{
|
||||
modelValue: ProfileSearch
|
||||
}>()
|
||||
|
||||
/** Emit a value update */
|
||||
const updateValue = (key : string, value : string) => {
|
||||
criteria.value = { ...criteria.value, [key]: value }
|
||||
emit('update:modelValue', criteria.value)
|
||||
}
|
||||
const emit = defineEmits<{
|
||||
(e: "search") : void
|
||||
(e: "update:modelValue", value : ProfileSearch) : void
|
||||
}>()
|
||||
|
||||
return {
|
||||
criteria,
|
||||
updateContinent: (c : string) => updateValue('continentId', c),
|
||||
updateValue
|
||||
}
|
||||
}
|
||||
})
|
||||
/** The initial search criteria passed; this is what we'll update and emit when data changes */
|
||||
const criteria = ref({ ...props.modelValue })
|
||||
|
||||
/** Emit a value update */
|
||||
const updateValue = (key : string, value : string) => {
|
||||
criteria.value = { ...criteria.value, [key]: value }
|
||||
emit("update:modelValue", criteria.value)
|
||||
}
|
||||
|
||||
/** Update the continent ID */
|
||||
const updateContinent = (c : string) => updateValue("continentId", c)
|
||||
</script>
|
||||
|
@ -1,48 +1,44 @@
|
||||
<template lang="pug">
|
||||
.row.pb-3
|
||||
.col.col-xs-2.col-md-1.align-self-center
|
||||
button.btn.btn-sm.btn-outline-danger.rounded-pill(title='Delete' @click.prevent="$emit('remove')") −
|
||||
button.btn.btn-sm.btn-outline-danger.rounded-pill(title="Delete" @click.prevent="$emit('remove')") −
|
||||
.col.col-xs-10.col-md-6
|
||||
.form-floating
|
||||
input.form-control(type='text' :id='`skillDesc${skill.id}`' maxlength='100'
|
||||
placeholder='A skill (language, design technique, process, etc.)' :value='skill.description'
|
||||
input.form-control(type="text" :id="`skillDesc${skill.id}`" maxlength="100"
|
||||
placeholder="A skill (language, design technique, process, etc.)" :value="skill.description"
|
||||
@input="updateValue('description', $event.target.value)")
|
||||
label.jjj-label(:for='`skillDesc${skill.id}`') Skill
|
||||
label.jjj-label(:for="`skillDesc${skill.id}`") Skill
|
||||
.form-text A skill (language, design technique, process, etc.)
|
||||
.col.col-xs-12.col-md-5
|
||||
.form-floating
|
||||
input.form-control(type='text' :id='`skillNotes${skill.id}`' maxlength='100'
|
||||
placeholder='A further description of the skill (100 characters max)' :value='skill.notes'
|
||||
input.form-control(type="text" :id="`skillNotes${skill.id}`" maxlength="100"
|
||||
placeholder="A further description of the skill (100 characters max)" :value="skill.notes"
|
||||
@input="updateValue('notes', $event.target.value)")
|
||||
label.jjj-label(:for='`skillNotes${skill.id}`') Notes
|
||||
label.jjj-label(:for="`skillNotes${skill.id}`") Notes
|
||||
.form-text A further description of the skill (100 characters max)
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, Ref, ref } from 'vue'
|
||||
import { Skill } from '@/api'
|
||||
<script setup lang="ts">
|
||||
import { Ref, ref } from "vue"
|
||||
import { Skill } from "@/api"
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ProfileSkillEdit',
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
emits: ['input', 'remove', 'update:modelValue'],
|
||||
setup (props, { emit }) {
|
||||
/** The skill being edited */
|
||||
const skill : Ref<Skill> = ref({ ...props.modelValue as Skill })
|
||||
const props = defineProps<{
|
||||
modelValue: Skill
|
||||
}>()
|
||||
|
||||
return {
|
||||
skill,
|
||||
updateValue: (key : string, value : string) => {
|
||||
skill.value = { ...skill.value, [key]: value }
|
||||
emit('update:modelValue', skill.value)
|
||||
emit('input')
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
const emit = defineEmits<{
|
||||
(e: "input") : void
|
||||
(e: "remove") : void
|
||||
(e: "update:modelValue", value: Skill) : void
|
||||
}>()
|
||||
|
||||
/** The skill being edited */
|
||||
const skill : Ref<Skill> = ref({ ...props.modelValue as Skill })
|
||||
|
||||
/** Update a value in the model */
|
||||
const updateValue = (key : string, value : string) => {
|
||||
skill.value = { ...skill.value, [key]: value }
|
||||
emit("update:modelValue", skill.value)
|
||||
emit("input")
|
||||
}
|
||||
</script>
|
||||
|
@ -1,15 +1,15 @@
|
||||
import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import store, { key } from './store'
|
||||
import Icon from './components/Icon.vue'
|
||||
import PageTitle from './components/PageTitle.vue'
|
||||
import { createApp } from "vue"
|
||||
import App from "./App.vue"
|
||||
import router from "./router"
|
||||
import store, { key } from "./store"
|
||||
import Icon from "./components/Icon.vue"
|
||||
import PageTitle from "./components/PageTitle.vue"
|
||||
|
||||
const app = createApp(App)
|
||||
.use(router)
|
||||
.use(store, key)
|
||||
|
||||
app.component('Icon', Icon)
|
||||
app.component('PageTitle', PageTitle)
|
||||
app.component("Icon", Icon)
|
||||
app.component("PageTitle", PageTitle)
|
||||
|
||||
app.mount('#app')
|
||||
app.mount("#app")
|
||||
|
12
src/JobsJobsJobs/App/src/markdown.ts
Normal file
12
src/JobsJobsJobs/App/src/markdown.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { sanitize } from "dompurify"
|
||||
import marked from "marked"
|
||||
|
||||
/**
|
||||
* Transform Markdown to HTML (standardize option, sanitize the output)
|
||||
*
|
||||
* @param markdown The Markdown text to be rendered as HTML
|
||||
* @returns The rendered HTML
|
||||
*/
|
||||
export function toHtml (markdown : string) : string {
|
||||
return sanitize(marked(markdown, { gfm: true, smartypants: true }), { USE_PROFILES: { html: true } })
|
||||
}
|
@ -5,13 +5,13 @@ import {
|
||||
RouteLocationNormalizedLoaded,
|
||||
RouteRecordName,
|
||||
RouteRecordRaw
|
||||
} from 'vue-router'
|
||||
import store from '@/store'
|
||||
import Home from '@/views/Home.vue'
|
||||
import LogOn from '@/views/citizen/LogOn.vue'
|
||||
} from "vue-router"
|
||||
import store from "@/store"
|
||||
import Home from "@/views/Home.vue"
|
||||
import LogOn from "@/views/citizen/LogOn.vue"
|
||||
|
||||
/** The URL to which the user should be pointed once they have authorized with NAS */
|
||||
export const AFTER_LOG_ON_URL = 'jjj-after-log-on-url'
|
||||
export const AFTER_LOG_ON_URL = "jjj-after-log-on-url"
|
||||
|
||||
/**
|
||||
* Get a value from the query string
|
||||
@ -27,120 +27,120 @@ export function queryValue (route: RouteLocationNormalizedLoaded, key : string)
|
||||
|
||||
const routes: Array<RouteRecordRaw> = [
|
||||
{
|
||||
path: '/',
|
||||
name: 'Home',
|
||||
path: "/",
|
||||
name: "Home",
|
||||
component: Home
|
||||
},
|
||||
{
|
||||
path: '/how-it-works',
|
||||
name: 'HowItWorks',
|
||||
component: () => import(/* webpackChunkName: "help" */ '../views/HowItWorks.vue')
|
||||
path: "/how-it-works",
|
||||
name: "HowItWorks",
|
||||
component: () => import(/* webpackChunkName: "help" */ "../views/HowItWorks.vue")
|
||||
},
|
||||
{
|
||||
path: '/privacy-policy',
|
||||
name: 'PrivacyPolicy',
|
||||
component: () => import(/* webpackChunkName: "legal" */ '../views/PrivacyPolicy.vue')
|
||||
path: "/privacy-policy",
|
||||
name: "PrivacyPolicy",
|
||||
component: () => import(/* webpackChunkName: "legal" */ "../views/PrivacyPolicy.vue")
|
||||
},
|
||||
{
|
||||
path: '/terms-of-service',
|
||||
name: 'TermsOfService',
|
||||
component: () => import(/* webpackChunkName: "legal" */ '../views/TermsOfService.vue')
|
||||
path: "/terms-of-service",
|
||||
name: "TermsOfService",
|
||||
component: () => import(/* webpackChunkName: "legal" */ "../views/TermsOfService.vue")
|
||||
},
|
||||
// Citizen URLs
|
||||
{
|
||||
path: '/citizen/log-on',
|
||||
name: 'LogOn',
|
||||
path: "/citizen/log-on",
|
||||
name: "LogOn",
|
||||
component: LogOn
|
||||
},
|
||||
{
|
||||
path: '/citizen/authorized',
|
||||
name: 'CitizenAuthorized',
|
||||
component: () => import(/* webpackChunkName: "dashboard" */ '../views/citizen/Authorized.vue')
|
||||
path: "/citizen/authorized",
|
||||
name: "CitizenAuthorized",
|
||||
component: () => import(/* webpackChunkName: "dashboard" */ "../views/citizen/Authorized.vue")
|
||||
},
|
||||
{
|
||||
path: '/citizen/dashboard',
|
||||
name: 'Dashboard',
|
||||
component: () => import(/* webpackChunkName: "dashboard" */ '../views/citizen/Dashboard.vue')
|
||||
path: "/citizen/dashboard",
|
||||
name: "Dashboard",
|
||||
component: () => import(/* webpackChunkName: "dashboard" */ "../views/citizen/Dashboard.vue")
|
||||
},
|
||||
{
|
||||
path: '/citizen/profile',
|
||||
name: 'EditProfile',
|
||||
component: () => import(/* webpackChunkName: "profedit" */ '../views/citizen/EditProfile.vue')
|
||||
path: "/citizen/profile",
|
||||
name: "EditProfile",
|
||||
component: () => import(/* webpackChunkName: "profedit" */ "../views/citizen/EditProfile.vue")
|
||||
},
|
||||
{
|
||||
path: '/citizen/log-off',
|
||||
name: 'LogOff',
|
||||
component: () => import(/* webpackChunkName: "logoff" */ '../views/citizen/LogOff.vue')
|
||||
path: "/citizen/log-off",
|
||||
name: "LogOff",
|
||||
component: () => import(/* webpackChunkName: "logoff" */ "../views/citizen/LogOff.vue")
|
||||
},
|
||||
// Job Listing URLs
|
||||
{
|
||||
path: '/help-wanted',
|
||||
name: 'HelpWanted',
|
||||
component: () => import(/* webpackChunkName: "joblist" */ '../views/listing/HelpWanted.vue')
|
||||
path: "/help-wanted",
|
||||
name: "HelpWanted",
|
||||
component: () => import(/* webpackChunkName: "joblist" */ "../views/listing/HelpWanted.vue")
|
||||
},
|
||||
{
|
||||
path: '/listing/:id/edit',
|
||||
name: 'EditListing',
|
||||
component: () => import(/* webpackChunkName: "jobedit" */ '../views/listing/ListingEdit.vue')
|
||||
path: "/listing/:id/edit",
|
||||
name: "EditListing",
|
||||
component: () => import(/* webpackChunkName: "jobedit" */ "../views/listing/ListingEdit.vue")
|
||||
},
|
||||
{
|
||||
path: '/listing/:id/view',
|
||||
name: 'ViewListing',
|
||||
component: () => import(/* webpackChunkName: "joblist" */ '../views/listing/ListingView.vue')
|
||||
path: "/listing/:id/view",
|
||||
name: "ViewListing",
|
||||
component: () => import(/* webpackChunkName: "joblist" */ "../views/listing/ListingView.vue")
|
||||
},
|
||||
{
|
||||
path: '/listings/mine',
|
||||
name: 'MyListings',
|
||||
component: () => import(/* webpackChunkName: "joblist" */ '../views/listing/MyListings.vue')
|
||||
path: "/listings/mine",
|
||||
name: "MyListings",
|
||||
component: () => import(/* webpackChunkName: "joblist" */ "../views/listing/MyListings.vue")
|
||||
},
|
||||
// Profile URLs
|
||||
{
|
||||
path: '/profile/:id/view',
|
||||
name: 'ViewProfile',
|
||||
component: () => import(/* webpackChunkName: "profview" */ '../views/profile/ProfileView.vue')
|
||||
path: "/profile/:id/view",
|
||||
name: "ViewProfile",
|
||||
component: () => import(/* webpackChunkName: "profview" */ "../views/profile/ProfileView.vue")
|
||||
},
|
||||
{
|
||||
path: '/profile/search',
|
||||
name: 'SearchProfiles',
|
||||
component: () => import(/* webpackChunkName: "profview" */ '../views/profile/ProfileSearch.vue')
|
||||
path: "/profile/search",
|
||||
name: "SearchProfiles",
|
||||
component: () => import(/* webpackChunkName: "profview" */ "../views/profile/ProfileSearch.vue")
|
||||
},
|
||||
{
|
||||
path: '/profile/seeking',
|
||||
name: 'PublicSearchProfiles',
|
||||
component: () => import(/* webpackChunkName: "seeking" */ '../views/profile/Seeking.vue')
|
||||
path: "/profile/seeking",
|
||||
name: "PublicSearchProfiles",
|
||||
component: () => import(/* webpackChunkName: "seeking" */ "../views/profile/Seeking.vue")
|
||||
},
|
||||
// "So Long" URLs
|
||||
{
|
||||
path: '/so-long/options',
|
||||
name: 'DeletionOptions',
|
||||
component: () => import(/* webpackChunkName: "so-long" */ '../views/so-long/DeletionOptions.vue')
|
||||
path: "/so-long/options",
|
||||
name: "DeletionOptions",
|
||||
component: () => import(/* webpackChunkName: "so-long" */ "../views/so-long/DeletionOptions.vue")
|
||||
},
|
||||
{
|
||||
path: '/so-long/success',
|
||||
name: 'DeletionSuccess',
|
||||
component: () => import(/* webpackChunkName: "so-long" */ '../views/so-long/DeletionSuccess.vue')
|
||||
path: "/so-long/success",
|
||||
name: "DeletionSuccess",
|
||||
component: () => import(/* webpackChunkName: "so-long" */ "../views/so-long/DeletionSuccess.vue")
|
||||
},
|
||||
// Success Story URLs
|
||||
{
|
||||
path: '/success-story/list',
|
||||
name: 'ListStories',
|
||||
component: () => import(/* webpackChunkName: "success" */ '../views/success-story/StoryList.vue')
|
||||
path: "/success-story/list",
|
||||
name: "ListStories",
|
||||
component: () => import(/* webpackChunkName: "success" */ "../views/success-story/StoryList.vue")
|
||||
},
|
||||
{
|
||||
path: '/success-story/:id/edit',
|
||||
name: 'EditStory',
|
||||
component: () => import(/* webpackChunkName: "succedit" */ '../views/success-story/StoryEdit.vue')
|
||||
path: "/success-story/:id/edit",
|
||||
name: "EditStory",
|
||||
component: () => import(/* webpackChunkName: "succedit" */ "../views/success-story/StoryEdit.vue")
|
||||
},
|
||||
{
|
||||
path: '/success-story/:id/view',
|
||||
name: 'ViewStory',
|
||||
component: () => import(/* webpackChunkName: "success" */ '../views/success-story/StoryView.vue')
|
||||
path: "/success-story/:id/view",
|
||||
name: "ViewStory",
|
||||
component: () => import(/* webpackChunkName: "success" */ "../views/success-story/StoryView.vue")
|
||||
}
|
||||
]
|
||||
/** The routes that do not require logins */
|
||||
const publicRoutes : Array<RouteRecordName> = [
|
||||
'Home', 'HowItWorks', 'PrivacyPolicy', 'TermsOfService', 'LogOn', 'CitizenAuthorized', 'PublicSearchProfiles',
|
||||
'DeletionSuccess'
|
||||
"Home", "HowItWorks", "PrivacyPolicy", "TermsOfService", "LogOn", "CitizenAuthorized", "PublicSearchProfiles",
|
||||
"DeletionSuccess"
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
@ -154,9 +154,9 @@ const router = createRouter({
|
||||
|
||||
// eslint-disable-next-line
|
||||
router.beforeEach((to : RouteLocationNormalized, from : RouteLocationNormalized) =>{
|
||||
if (store.state.user === undefined && !publicRoutes.includes(to.name || '')) {
|
||||
if (store.state.user === undefined && !publicRoutes.includes(to.name ?? "")) {
|
||||
window.localStorage.setItem(AFTER_LOG_ON_URL, to.fullPath)
|
||||
return '/citizen/log-on'
|
||||
return "/citizen/log-on"
|
||||
}
|
||||
})
|
||||
|
||||
|
4
src/JobsJobsJobs/App/src/shims-vue.d.ts
vendored
4
src/JobsJobsJobs/App/src/shims-vue.d.ts
vendored
@ -1,6 +1,6 @@
|
||||
/* eslint-disable */
|
||||
declare module '*.vue' {
|
||||
import type { DefineComponent } from 'vue'
|
||||
declare module "*.vue" {
|
||||
import type { DefineComponent } from "vue"
|
||||
const component: DefineComponent<{}, {}, any>
|
||||
export default component
|
||||
}
|
||||
|
3
src/JobsJobsJobs/App/src/shims-vuetify.d.ts
vendored
3
src/JobsJobsJobs/App/src/shims-vuetify.d.ts
vendored
@ -1,3 +0,0 @@
|
||||
declare module 'vuetify'
|
||||
declare module 'vuetify/lib/components'
|
||||
declare module 'vuetify/lib/directives'
|
@ -1,6 +1,6 @@
|
||||
import { InjectionKey } from 'vue'
|
||||
import { createStore, Store, useStore as baseUseStore } from 'vuex'
|
||||
import api, { Continent, LogOnSuccess } from '../api'
|
||||
import { InjectionKey } from "vue"
|
||||
import { createStore, Store, useStore as baseUseStore } from "vuex"
|
||||
import api, { Continent, LogOnSuccess } from "../api"
|
||||
|
||||
/** The state tracked by the application */
|
||||
export interface State {
|
||||
@ -13,7 +13,7 @@ export interface State {
|
||||
}
|
||||
|
||||
/** An injection key to identify this state with Vue */
|
||||
export const key : InjectionKey<Store<State>> = Symbol('VueX Store')
|
||||
export const key : InjectionKey<Store<State>> = Symbol("VueX Store")
|
||||
|
||||
/** Use this store in component `setup` functions */
|
||||
export function useStore () : Store<State> {
|
||||
@ -24,7 +24,7 @@ export default createStore({
|
||||
state: () : State => {
|
||||
return {
|
||||
user: undefined,
|
||||
logOnState: '<em>Welcome back! Verifying your No Agenda Social account…</em>',
|
||||
logOnState: "<em>Welcome back! Verifying your No Agenda Social account…</em>",
|
||||
continents: []
|
||||
}
|
||||
},
|
||||
@ -45,19 +45,19 @@ export default createStore({
|
||||
actions: {
|
||||
async logOn ({ commit }, code: string) {
|
||||
const logOnResult = await api.citizen.logOn(code)
|
||||
if (typeof logOnResult === 'string') {
|
||||
commit('setLogOnState', logOnResult)
|
||||
if (typeof logOnResult === "string") {
|
||||
commit("setLogOnState", logOnResult)
|
||||
} else {
|
||||
commit('setUser', logOnResult)
|
||||
commit("setUser", logOnResult)
|
||||
}
|
||||
},
|
||||
async ensureContinents ({ state, commit }) {
|
||||
if (state.continents.length > 0) return
|
||||
const theSeven = await api.continent.all()
|
||||
if (typeof theSeven === 'string') {
|
||||
if (typeof theSeven === "string") {
|
||||
console.error(theSeven)
|
||||
} else {
|
||||
commit('setContinents', theSeven)
|
||||
commit("setContinents", theSeven)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -1,6 +1,6 @@
|
||||
<template lang="pug">
|
||||
article
|
||||
page-title(title='Welcome!')
|
||||
page-title(title="Welcome!")
|
||||
p
|
||||
p.
|
||||
Welcome to Jobs, Jobs, Jobs (AKA No Agenda Careers), where citizens of Gitmo Nation can assist one another in
|
||||
@ -8,18 +8,10 @@ article
|
||||
their work deconstructing the misinformation that passes for news on a day-to-day basis.
|
||||
p.
|
||||
Do you not understand the terms in the paragraph above? No worries; just head over to
|
||||
#[a(href='https://noagendashow.net' target='_blank') The Best Podcast in the Universe]
|
||||
#[em #[audio-clip(clip='thats-true') (that’s true!)]] and find out what you’re missing.
|
||||
#[a(href="https://noagendashow.net" target="_blank") The Best Podcast in the Universe]
|
||||
#[em #[audio-clip(clip="thats-true") (that’s true!)]] and find out what you’re missing.
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue'
|
||||
import AudioClip from '@/components/AudioClip.vue'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Home',
|
||||
components: {
|
||||
AudioClip
|
||||
}
|
||||
})
|
||||
<script setup lang="ts">
|
||||
import AudioClip from "@/components/AudioClip.vue"
|
||||
</script>
|
||||
|
@ -1,6 +1,6 @@
|
||||
<template lang="pug">
|
||||
article
|
||||
page-title(title='How It Works')
|
||||
page-title(title="How It Works")
|
||||
h3 How It Works
|
||||
h4 Completing Your Profile
|
||||
ul
|
||||
@ -12,9 +12,9 @@ article
|
||||
li.
|
||||
The “Professional Biography” and “Experience” sections support Markdown, a plain-text way
|
||||
to specify formatting quite similar to that provided by word processors. The
|
||||
#[a(href='https://daringfireball.net/projects/markdown/' target='_blank') original page] for the project is a
|
||||
#[a(href="https://daringfireball.net/projects/markdown/" target="_blank") original page] for the project is a
|
||||
good overview of its capabilities, and the pages at
|
||||
#[a(href='https://www.markdownguide.org/' target='_blank') Markdown Guide] give in-depth lessons to make the most
|
||||
#[a(href="https://www.markdownguide.org/" target="_blank") Markdown Guide] give in-depth lessons to make the most
|
||||
of this language. The version of Markdown employed here supports many popular extensions, include smart quotes
|
||||
(turning "a quote" into “a quote”), tables, super/subscripts, and more.
|
||||
li.
|
||||
@ -63,7 +63,7 @@ article
|
||||
h4 Help / Suggestions
|
||||
p.
|
||||
This is open-source software
|
||||
#[a(href='https://github.com/bit-badger/jobs-jobs-jobs' _target='_blank') developed on Github]; feel free to
|
||||
#[a(href='https://github.com/bit-badger/jobs-jobs-jobs/issues' target='_blank') create an issue there], or look up
|
||||
#[a(href="https://github.com/bit-badger/jobs-jobs-jobs" _target="_blank") developed on Github]; feel free to
|
||||
#[a(href="https://github.com/bit-badger/jobs-jobs-jobs/issues" target="_blank") create an issue there], or look up
|
||||
@danieljsummers on No Agenda Social.
|
||||
</template>
|
||||
|
@ -1,6 +1,6 @@
|
||||
<template lang="pug">
|
||||
article
|
||||
page-title(title='Privacy Policy')
|
||||
page-title(title="Privacy Policy")
|
||||
h3 Privacy Policy
|
||||
p: em (as of February 6#[sup th], 2021)
|
||||
|
||||
@ -49,7 +49,7 @@ article
|
||||
provide our content or whose products or services we think may interest you.
|
||||
li.
|
||||
Website: {{name}}’s site, which can be accessed via this URL:
|
||||
#[router-link(to='/') https://noagendacareers.com/]
|
||||
#[router-link(to="/") https://noagendacareers.com/]
|
||||
li You: a person or entity that is registered with {{name}} to use the Services.
|
||||
|
||||
h4 What Information Do We Collect?
|
||||
@ -332,18 +332,10 @@ article
|
||||
|
||||
h4 Contact Us
|
||||
p Don't hesitate to contact us if you have any questions.
|
||||
ul: li Via this Link: #[router-link(to='/how-it-works') https://noagendacareers.com/how-it-works]
|
||||
ul: li Via this Link: #[router-link(to="/how-it-works") https://noagendacareers.com/how-it-works]
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'PrivacyPolicy',
|
||||
setup () {
|
||||
return {
|
||||
name: 'Jobs, Jobs, Jobs'
|
||||
}
|
||||
}
|
||||
})
|
||||
<script setup lang="ts">
|
||||
/** The name of the application */
|
||||
const name = "Jobs, Jobs, Jobs"
|
||||
</script>
|
||||
|
@ -1,6 +1,6 @@
|
||||
<template lang="pug">
|
||||
article
|
||||
page-title(title='Terms of Service')
|
||||
page-title(title="Terms of Service")
|
||||
h3 Terms of Service
|
||||
p: em (as of February 6#[sup th], 2021)
|
||||
|
||||
@ -14,9 +14,9 @@ article
|
||||
p.
|
||||
Jobs, Jobs, Jobs is a service that allows individuals to enter and amend employment profiles, restricting access
|
||||
to the details of these profiles to other users of
|
||||
#[a(href='https://noagendasocial.com' target='_blank') No Agenda Social]. Registration is accomplished by allowing
|
||||
#[a(href="https://noagendasocial.com" target="_blank") No Agenda Social]. Registration is accomplished by allowing
|
||||
Jobs, Jobs, Jobs to read one’s No Agenda Social profile. See our
|
||||
#[router-link(to='/privacy-policy') privacy policy] for details on the personal (user) information we maintain.
|
||||
#[router-link(to="/privacy-policy") privacy policy] for details on the personal (user) information we maintain.
|
||||
|
||||
h4 Liability
|
||||
p.
|
||||
@ -32,6 +32,6 @@ article
|
||||
hr
|
||||
|
||||
p.
|
||||
You may also wish to review our #[router-link(to='/privacy-policy') privacy policy] to learn how we handle your
|
||||
You may also wish to review our #[router-link(to="/privacy-policy") privacy policy] to learn how we handle your
|
||||
data.
|
||||
</template>
|
||||
|
@ -1,47 +1,41 @@
|
||||
<template lang="pug">
|
||||
article
|
||||
page-title(title='Logging on...')
|
||||
page-title(title="Logging on...")
|
||||
p
|
||||
p(v-html='message')
|
||||
p(v-html="message")
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useStore } from '@/store'
|
||||
import { AFTER_LOG_ON_URL } from '@/router'
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted } from "vue"
|
||||
import { useRouter } from "vue-router"
|
||||
import { useStore } from "@/store"
|
||||
import { AFTER_LOG_ON_URL } from "@/router"
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Authorized',
|
||||
setup () {
|
||||
const router = useRouter()
|
||||
const store = useStore()
|
||||
const router = useRouter()
|
||||
const store = useStore()
|
||||
|
||||
/** Pass the code to the API and exchange it for a user and a JWT */
|
||||
const logOn = async () => {
|
||||
const code = router.currentRoute.value.query.code
|
||||
if (code) {
|
||||
await store.dispatch('logOn', code)
|
||||
if (store.state.user !== undefined) {
|
||||
const afterLogOnUrl = window.localStorage.getItem(AFTER_LOG_ON_URL)
|
||||
if (afterLogOnUrl) {
|
||||
window.localStorage.removeItem(AFTER_LOG_ON_URL)
|
||||
router.push(afterLogOnUrl)
|
||||
} else {
|
||||
router.push('/citizen/dashboard')
|
||||
}
|
||||
}
|
||||
/** Pass the code to the API and exchange it for a user and a JWT */
|
||||
const logOn = async () => {
|
||||
const code = router.currentRoute.value.query.code
|
||||
if (code) {
|
||||
await store.dispatch("logOn", code)
|
||||
if (store.state.user !== undefined) {
|
||||
const afterLogOnUrl = window.localStorage.getItem(AFTER_LOG_ON_URL)
|
||||
if (afterLogOnUrl) {
|
||||
window.localStorage.removeItem(AFTER_LOG_ON_URL)
|
||||
router.push(afterLogOnUrl)
|
||||
} else {
|
||||
store.commit('setLogOnState',
|
||||
'Did not receive a token from No Agenda Social (perhaps you clicked “Cancel”?)')
|
||||
router.push("/citizen/dashboard")
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(logOn)
|
||||
|
||||
return {
|
||||
message: computed(() => store.state.logOnState)
|
||||
}
|
||||
} else {
|
||||
store.commit("setLogOnState",
|
||||
"Did not receive a token from No Agenda Social (perhaps you clicked “Cancel”?)")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(logOn)
|
||||
|
||||
/** Accessor for the log on state */
|
||||
const message = computed(() => store.state.logOnState)
|
||||
</script>
|
||||
|
@ -1,93 +1,77 @@
|
||||
<template lang="pug">
|
||||
article.container
|
||||
page-title(title='Dashboard')
|
||||
page-title(title="Dashboard")
|
||||
h3.pb-4 Welcome, {{user.name}}
|
||||
load-data(:load='retrieveData'): .row.row-cols-1.row-cols-md-2
|
||||
load-data(:load="retrieveData"): .row.row-cols-1.row-cols-md-2
|
||||
.col: .card.h-100
|
||||
h5.card-header Your Profile
|
||||
.card-body
|
||||
h6.card-subtitle.mb-3.text-muted.fst-italic Last updated #[full-date-time(:date='profile.lastUpdatedOn')]
|
||||
p.card-text(v-if='profile')
|
||||
h6.card-subtitle.mb-3.text-muted.fst-italic Last updated #[full-date-time(:date="profile.lastUpdatedOn")]
|
||||
p.card-text(v-if="profile")
|
||||
| Your profile currently lists {{profile.skills.length}}
|
||||
| skill#[template(v-if='profile.skills.length !== 1') s].
|
||||
span(v-if='profile.seekingEmployment')
|
||||
| skill#[template(v-if="profile.skills.length !== 1") s].
|
||||
span(v-if="profile.seekingEmployment")
|
||||
br
|
||||
br
|
||||
| Your profile indicates that you are seeking employment. Once you find it,
|
||||
router-link(to='/success-story/add') tell your fellow citizens about it!
|
||||
router-link(to="/success-story/add") tell your fellow citizens about it!
|
||||
p.card-text(v-else).
|
||||
You do not have an employment profile established; click below (or “Edit Profile” in the menu) to
|
||||
get started!
|
||||
.card-footer
|
||||
template(v-if='profile')
|
||||
router-link.btn.btn-outline-secondary(:to='`/profile/${user.citizenId}/view`') View Profile
|
||||
template(v-if="profile")
|
||||
router-link.btn.btn-outline-secondary(:to="`/profile/${user.citizenId}/view`") View Profile
|
||||
|
|
||||
router-link.btn.btn-outline-secondary(to='/citizen/profile') Edit Profile
|
||||
router-link.btn.btn-primary(v-else to='/citizen/profile') Create Profile
|
||||
router-link.btn.btn-outline-secondary(to="/citizen/profile") Edit Profile
|
||||
router-link.btn.btn-primary(v-else to="/citizen/profile") Create Profile
|
||||
.col: .card.h-100
|
||||
h5.card-header Other Citizens
|
||||
.card-body
|
||||
h6.card-subtitle.mb-3.text-muted.fst-italic
|
||||
template(v-if='profileCount === 0') No
|
||||
template(v-if="profileCount === 0") No
|
||||
template(v-else) {{profileCount}} Total
|
||||
| Employment Profile#[template(v-if='profileCount !== 1') s]
|
||||
p.card-text(v-if='profileCount === 1 && profile') It looks like, for now, it’s just you…
|
||||
p.card-text(v-else-if='profileCount > 0') Take a look around and see if you can help them find work!
|
||||
| Employment Profile#[template(v-if="profileCount !== 1") s]
|
||||
p.card-text(v-if="profileCount === 1 && profile") It looks like, for now, it’s just you…
|
||||
p.card-text(v-else-if="profileCount > 0") Take a look around and see if you can help them find work!
|
||||
p.card-text(v-else) You can click below, but you will not find anything…
|
||||
.card-footer: router-link.btn.btn-outline-secondary(to='/profile/search') Search Profiles
|
||||
.card-footer: router-link.btn.btn-outline-secondary(to="/profile/search") Search Profiles
|
||||
p
|
||||
p.
|
||||
To see how this application works, check out “How It Works” in the sidebar (last updated June
|
||||
14#[sup th], 2021).
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, Ref, ref } from 'vue'
|
||||
import api, { LogOnSuccess, Profile } from '@/api'
|
||||
import { useStore } from '@/store'
|
||||
<script setup lang="ts">
|
||||
import { Ref, ref } from "vue"
|
||||
import api, { LogOnSuccess, Profile } from "@/api"
|
||||
import { useStore } from "@/store"
|
||||
|
||||
import FullDateTime from '@/components/FullDateTime.vue'
|
||||
import LoadData from '@/components/LoadData.vue'
|
||||
import FullDateTime from "@/components/FullDateTime.vue"
|
||||
import LoadData from "@/components/LoadData.vue"
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Dashboard',
|
||||
components: {
|
||||
FullDateTime,
|
||||
LoadData
|
||||
},
|
||||
setup () {
|
||||
const store = useStore()
|
||||
const store = useStore()
|
||||
|
||||
/** The currently logged-in user */
|
||||
const user = store.state.user as LogOnSuccess
|
||||
/** The currently logged-in user */
|
||||
const user = store.state.user as LogOnSuccess
|
||||
|
||||
/** The user's profile */
|
||||
const profile : Ref<Profile | undefined> = ref(undefined)
|
||||
/** The user's profile */
|
||||
const profile : Ref<Profile | undefined> = ref(undefined)
|
||||
|
||||
/** A count of profiles in the system */
|
||||
const profileCount = ref(0)
|
||||
/** A count of profiles in the system */
|
||||
const profileCount = ref(0)
|
||||
|
||||
const retrieveData = async (errors : string[]) => {
|
||||
const profileResult = await api.profile.retreive(undefined, user)
|
||||
if (typeof profileResult === 'string') {
|
||||
errors.push(profileResult)
|
||||
} else if (typeof profileResult !== 'undefined') {
|
||||
profile.value = profileResult
|
||||
}
|
||||
const count = await api.profile.count(user)
|
||||
if (typeof count === 'string') {
|
||||
errors.push(count)
|
||||
} else {
|
||||
profileCount.value = count
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
retrieveData,
|
||||
user,
|
||||
profile,
|
||||
profileCount
|
||||
}
|
||||
const retrieveData = async (errors : string[]) => {
|
||||
const profileResult = await api.profile.retreive(undefined, user)
|
||||
if (typeof profileResult === "string") {
|
||||
errors.push(profileResult)
|
||||
} else if (typeof profileResult !== "undefined") {
|
||||
profile.value = profileResult
|
||||
}
|
||||
})
|
||||
const count = await api.profile.count(user)
|
||||
if (typeof count === "string") {
|
||||
errors.push(count)
|
||||
} else {
|
||||
profileCount.value = count
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
@ -1,47 +1,47 @@
|
||||
<template lang="pug">
|
||||
article
|
||||
page-title(title='Edit Profile')
|
||||
page-title(title="Edit Profile")
|
||||
h3.pb-3 My Employment Profile
|
||||
load-data(:load='retrieveData'): form.row.g-3
|
||||
load-data(:load="retrieveData"): form.row.g-3
|
||||
.col-12.col-sm-10.col-md-8.col-lg-6
|
||||
.form-floating
|
||||
input.form-control(type='text' id='realName' v-model='v$.realName.$model' maxlength='255'
|
||||
placeholder='Leave blank to use your NAS display name')
|
||||
label(for='realName') Real Name
|
||||
input.form-control(type="text" id="realName" v-model="v$.realName.$model" maxlength="255"
|
||||
placeholder="Leave blank to use your NAS display name")
|
||||
label(for="realName") Real Name
|
||||
.form-text Leave blank to use your NAS display name
|
||||
.col-12
|
||||
.form-check
|
||||
input.form-check-input(type='checkbox' id='isSeeking' v-model='v$.isSeekingEmployment.$model')
|
||||
label.form-check-label(for='isSeeking') I am currently seeking employment
|
||||
p(v-if='profile.isSeekingEmployment'): em.
|
||||
input.form-check-input(type="checkbox" id="isSeeking" v-model="v$.isSeekingEmployment.$model")
|
||||
label.form-check-label(for="isSeeking") I am currently seeking employment
|
||||
p(v-if="profile.isSeekingEmployment"): em.
|
||||
If you have found employment, consider
|
||||
#[router-link(to='/success-story/new/edit') telling your fellow citizens about it!]
|
||||
#[router-link(to="/success-story/new/edit") telling your fellow citizens about it!]
|
||||
.col-12.col-sm-6.col-md-4
|
||||
continent-list(v-model='v$.continentId.$model' :isInvalid='v$.continentId.$error'
|
||||
@touch='v$.continentId.$touch() || true')
|
||||
continent-list(v-model="v$.continentId.$model" :isInvalid="v$.continentId.$error"
|
||||
@touch="v$.continentId.$touch() || true")
|
||||
.col-12.col-sm-6.col-md-8
|
||||
.form-floating
|
||||
input.form-control(type='text' id='region' :class="{ 'is-invalid': v$.region.$error }"
|
||||
v-model='v$.region.$model' maxlength='255'
|
||||
placeholder='Country, state, geographic area, etc.')
|
||||
input.form-control(type="text" id="region" :class="{ 'is-invalid': v$.region.$error }"
|
||||
v-model="v$.region.$model" maxlength="255"
|
||||
placeholder="Country, state, geographic area, etc.")
|
||||
#regionFeedback.invalid-feedback Please enter a region
|
||||
label.jjj-required(for='region') Region
|
||||
label.jjj-required(for="region") Region
|
||||
.form-text Country, state, geographic area, etc.
|
||||
markdown-editor(id='bio' label='Professional Biography' v-model:text='v$.biography.$model'
|
||||
:isInvalid='v$.biography.$error')
|
||||
markdown-editor(id="bio" label="Professional Biography" v-model:text="v$.biography.$model"
|
||||
:isInvalid="v$.biography.$error")
|
||||
.col-12.col-offset-md-2.col-md-4
|
||||
.form-check
|
||||
input.form-check-input(type='checkbox' id='isRemote' v-model='v$.remoteWork.$model')
|
||||
label.form-check-label(for='isRemote') I am looking for remote work
|
||||
input.form-check-input(type="checkbox" id="isRemote" v-model="v$.remoteWork.$model")
|
||||
label.form-check-label(for="isRemote") I am looking for remote work
|
||||
.col-12.col-md-4
|
||||
.form-check
|
||||
input.form-check-input(type='checkbox' id='isFullTime' v-model='v$.fullTime.$model')
|
||||
label.form-check-label(for='isFullTime') I am looking for full-time work
|
||||
input.form-check-input(type="checkbox" id="isFullTime" v-model="v$.fullTime.$model")
|
||||
label.form-check-label(for="isFullTime") I am looking for full-time work
|
||||
.col-12
|
||||
hr
|
||||
h4.pb-2 Skills #[button.btn.btn-sm.btn-outline-primary.rounded-pill(@click.prevent='addSkill') Add a Skill]
|
||||
profile-skill-edit(v-for='(skill, idx) in profile.skills' :key='skill.id' v-model='profile.skills[idx]'
|
||||
@remove='removeSkill(skill.id)' @input='v$.skills.$touch')
|
||||
h4.pb-2 Skills #[button.btn.btn-sm.btn-outline-primary.rounded-pill(@click.prevent="addSkill") Add a Skill]
|
||||
profile-skill-edit(v-for="(skill, idx) in profile.skills" :key="skill.id" v-model="profile.skills[idx]"
|
||||
@remove="removeSkill(skill.id)" @input="v$.skills.$touch")
|
||||
.col-12
|
||||
hr
|
||||
h4 Experience
|
||||
@ -49,178 +49,155 @@ article
|
||||
This application does not have a place to individually list your chronological job history; however, you can use
|
||||
this area to list prior jobs, their dates, and anything else you want to include that’s not already a part
|
||||
of your Professional Biography above.
|
||||
markdown-editor(id='experience' label='Experience' v-model:text='v$.experience.$model')
|
||||
markdown-editor(id="experience" label="Experience" v-model:text="v$.experience.$model")
|
||||
.col-12: .form-check
|
||||
input.form-check-input(type='checkbox' id='isPublic' v-model='v$.isPublic.$model')
|
||||
label.form-check-label(for='isPublic') Allow my profile to be searched publicly (outside NA Social)
|
||||
input.form-check-input(type="checkbox" id="isPublic" v-model="v$.isPublic.$model")
|
||||
label.form-check-label(for="isPublic") Allow my profile to be searched publicly (outside NA Social)
|
||||
.col-12
|
||||
p.text-danger(v-if='v$.$error') Please correct the errors above
|
||||
button.btn.btn-primary(@click.prevent='saveProfile') #[icon(icon='content-save-outline')] Save
|
||||
template(v-if='!isNew')
|
||||
p.text-danger(v-if="v$.$error") Please correct the errors above
|
||||
button.btn.btn-primary(@click.prevent="saveProfile") #[icon(icon="content-save-outline")] Save
|
||||
template(v-if="!isNew")
|
||||
|
|
||||
router-link.btn.btn-outline-secondary(:to='`/profile/${user.citizenId}/view`').
|
||||
#[icon(icon='file-account-outline')] View Your User Profile
|
||||
router-link.btn.btn-outline-secondary(:to="`/profile/${user.citizenId}/view`").
|
||||
#[icon(icon="file-account-outline")] View Your User Profile
|
||||
hr
|
||||
p.text-muted.fst-italic.
|
||||
(If you want to delete your profile, or your entire account,
|
||||
#[router-link(to='/so-long/options') see your deletion options here].)
|
||||
maybe-save(:isShown='confirmNavShown' :toRoute='nextRoute' :saveAction='saveProfile' :validator='v$'
|
||||
@close='confirmClose')
|
||||
#[router-link(to="/so-long/options") see your deletion options here].)
|
||||
maybe-save(:isShown="confirmNavShown" :toRoute="nextRoute" :saveAction="saveProfile" :validator="v$"
|
||||
@close="confirmClose")
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, ref, reactive, Ref } from 'vue'
|
||||
import { onBeforeRouteLeave, RouteLocationNormalized } from 'vue-router'
|
||||
import useVuelidate from '@vuelidate/core'
|
||||
import { required } from '@vuelidate/validators'
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, reactive, Ref } from "vue"
|
||||
import { onBeforeRouteLeave, RouteLocationNormalized } from "vue-router"
|
||||
import useVuelidate from "@vuelidate/core"
|
||||
import { required } from "@vuelidate/validators"
|
||||
|
||||
import api, { Citizen, LogOnSuccess, Profile, ProfileForm } from '@/api'
|
||||
import { toastError, toastSuccess } from '@/components/layout/AppToaster.vue'
|
||||
import { useStore } from '@/store'
|
||||
import api, { Citizen, LogOnSuccess, Profile, ProfileForm } from "@/api"
|
||||
import { toastError, toastSuccess } from "@/components/layout/AppToaster.vue"
|
||||
import { useStore } from "@/store"
|
||||
|
||||
import ContinentList from '@/components/ContinentList.vue'
|
||||
import LoadData from '@/components/LoadData.vue'
|
||||
import MarkdownEditor from '@/components/MarkdownEditor.vue'
|
||||
import MaybeSave from '@/components/MaybeSave.vue'
|
||||
import ProfileSkillEdit from '@/components/profile/SkillEdit.vue'
|
||||
import ContinentList from "@/components/ContinentList.vue"
|
||||
import LoadData from "@/components/LoadData.vue"
|
||||
import MarkdownEditor from "@/components/MarkdownEditor.vue"
|
||||
import MaybeSave from "@/components/MaybeSave.vue"
|
||||
import ProfileSkillEdit from "@/components/profile/SkillEdit.vue"
|
||||
|
||||
export default defineComponent({
|
||||
name: 'EditProfile',
|
||||
components: {
|
||||
ContinentList,
|
||||
LoadData,
|
||||
MarkdownEditor,
|
||||
MaybeSave,
|
||||
ProfileSkillEdit
|
||||
},
|
||||
setup () {
|
||||
const store = useStore()
|
||||
const store = useStore()
|
||||
|
||||
/** The currently logged-on user */
|
||||
const user = store.state.user as LogOnSuccess
|
||||
/** The currently logged-on user */
|
||||
const user = store.state.user as LogOnSuccess
|
||||
|
||||
/** Whether this is a new profile */
|
||||
const isNew = ref(false)
|
||||
/** Whether this is a new profile */
|
||||
const isNew = ref(false)
|
||||
|
||||
/** The starting values for a new employment profile */
|
||||
const newProfile : Profile = {
|
||||
id: user.citizenId,
|
||||
seekingEmployment: false,
|
||||
isPublic: false,
|
||||
continentId: '',
|
||||
region: '',
|
||||
remoteWork: false,
|
||||
fullTime: false,
|
||||
biography: '',
|
||||
lastUpdatedOn: '',
|
||||
experience: undefined,
|
||||
skills: []
|
||||
}
|
||||
/** The starting values for a new employment profile */
|
||||
const newProfile : Profile = {
|
||||
id: user.citizenId,
|
||||
seekingEmployment: false,
|
||||
isPublic: false,
|
||||
continentId: "",
|
||||
region: "",
|
||||
remoteWork: false,
|
||||
fullTime: false,
|
||||
biography: "",
|
||||
lastUpdatedOn: "",
|
||||
experience: undefined,
|
||||
skills: []
|
||||
}
|
||||
|
||||
/** The user's current profile (plus a few items, adapted for editing) */
|
||||
const profile = reactive(new ProfileForm())
|
||||
/** The user's current profile (plus a few items, adapted for editing) */
|
||||
const profile = reactive(new ProfileForm())
|
||||
|
||||
/** The validation rules for the form */
|
||||
const rules = computed(() => ({
|
||||
realName: { },
|
||||
isSeekingEmployment: { },
|
||||
isPublic: { },
|
||||
continentId: { required },
|
||||
region: { required },
|
||||
remoteWork: { },
|
||||
fullTime: { },
|
||||
biography: { required },
|
||||
experience: { },
|
||||
skills: { }
|
||||
}))
|
||||
/** The validation rules for the form */
|
||||
const rules = computed(() => ({
|
||||
realName: { },
|
||||
isSeekingEmployment: { },
|
||||
isPublic: { },
|
||||
continentId: { required },
|
||||
region: { required },
|
||||
remoteWork: { },
|
||||
fullTime: { },
|
||||
biography: { required },
|
||||
experience: { },
|
||||
skills: { }
|
||||
}))
|
||||
|
||||
/** Initialize form validation */
|
||||
const v$ = useVuelidate(rules, profile, { $lazy: true })
|
||||
/** Initialize form validation */
|
||||
const v$ = useVuelidate(rules, profile, { $lazy: true })
|
||||
|
||||
/** Retrieve the user's profile and their real name */
|
||||
const retrieveData = async (errors : string[]) => {
|
||||
const profileResult = await api.profile.retreive(undefined, user)
|
||||
if (typeof profileResult === 'string') {
|
||||
errors.push(profileResult)
|
||||
} else if (typeof profileResult === 'undefined') {
|
||||
isNew.value = true
|
||||
}
|
||||
const nameResult = await api.citizen.retrieve(user.citizenId, user)
|
||||
if (typeof nameResult === 'string') {
|
||||
errors.push(nameResult)
|
||||
}
|
||||
if (errors.length > 0) return
|
||||
// Update the empty form with appropriate values
|
||||
const p = isNew.value ? newProfile : profileResult as Profile
|
||||
profile.isSeekingEmployment = p.seekingEmployment
|
||||
profile.isPublic = p.isPublic
|
||||
profile.continentId = p.continentId
|
||||
profile.region = p.region
|
||||
profile.remoteWork = p.remoteWork
|
||||
profile.fullTime = p.fullTime
|
||||
profile.biography = p.biography
|
||||
profile.experience = p.experience
|
||||
profile.skills = p.skills
|
||||
profile.realName = typeof nameResult !== 'undefined' ? (nameResult as Citizen).realName || '' : ''
|
||||
}
|
||||
|
||||
/** The ID for new skills */
|
||||
let newSkillId = 0
|
||||
|
||||
/** Add a skill to the profile */
|
||||
const addSkill = () => {
|
||||
profile.skills.push({ id: `new${newSkillId++}`, description: '', notes: undefined })
|
||||
v$.value.skills.$touch()
|
||||
}
|
||||
|
||||
/** Remove the given skill from the profile */
|
||||
const removeSkill = (skillId : string) => {
|
||||
profile.skills = profile.skills.filter(s => s.id !== skillId)
|
||||
v$.value.skills.$touch()
|
||||
}
|
||||
|
||||
/** Save the current profile values */
|
||||
const saveProfile = async () => {
|
||||
v$.value.$touch()
|
||||
if (v$.value.$error) return
|
||||
// Remove any blank skills before submitting
|
||||
profile.skills = profile.skills.filter(s => !(s.description.trim() === '' && (s.notes || '').trim() === ''))
|
||||
const saveResult = await api.profile.save(profile, user)
|
||||
if (typeof saveResult === 'string') {
|
||||
toastError(saveResult, 'saving profile')
|
||||
} else {
|
||||
toastSuccess('Profile Saved Successfuly')
|
||||
v$.value.$reset()
|
||||
}
|
||||
}
|
||||
|
||||
/** Whether the navigation confirmation is shown */
|
||||
const confirmNavShown = ref(false)
|
||||
|
||||
/** The "next" route (will be navigated or cleared) */
|
||||
const nextRoute : Ref<RouteLocationNormalized | undefined> = ref(undefined)
|
||||
|
||||
/** If the user has unsaved changes, give them an opportunity to save before moving on */
|
||||
onBeforeRouteLeave(async (to, from) => { // eslint-disable-line
|
||||
if (!v$.value.$anyDirty) return true
|
||||
nextRoute.value = to
|
||||
confirmNavShown.value = true
|
||||
return false
|
||||
})
|
||||
|
||||
return {
|
||||
v$,
|
||||
retrieveData,
|
||||
user,
|
||||
isNew,
|
||||
profile,
|
||||
addSkill,
|
||||
removeSkill,
|
||||
saveProfile,
|
||||
confirmNavShown,
|
||||
nextRoute,
|
||||
confirmClose: () => { confirmNavShown.value = false }
|
||||
}
|
||||
/** Retrieve the user's profile and their real name */
|
||||
const retrieveData = async (errors : string[]) => {
|
||||
const profileResult = await api.profile.retreive(undefined, user)
|
||||
if (typeof profileResult === "string") {
|
||||
errors.push(profileResult)
|
||||
} else if (typeof profileResult === "undefined") {
|
||||
isNew.value = true
|
||||
}
|
||||
const nameResult = await api.citizen.retrieve(user.citizenId, user)
|
||||
if (typeof nameResult === "string") {
|
||||
errors.push(nameResult)
|
||||
}
|
||||
if (errors.length > 0) return
|
||||
// Update the empty form with appropriate values
|
||||
const p = isNew.value ? newProfile : profileResult as Profile
|
||||
profile.isSeekingEmployment = p.seekingEmployment
|
||||
profile.isPublic = p.isPublic
|
||||
profile.continentId = p.continentId
|
||||
profile.region = p.region
|
||||
profile.remoteWork = p.remoteWork
|
||||
profile.fullTime = p.fullTime
|
||||
profile.biography = p.biography
|
||||
profile.experience = p.experience
|
||||
profile.skills = p.skills
|
||||
profile.realName = typeof nameResult !== "undefined" ? (nameResult as Citizen).realName ?? "" : ""
|
||||
}
|
||||
|
||||
/** The ID for new skills */
|
||||
let newSkillId = 0
|
||||
|
||||
/** Add a skill to the profile */
|
||||
const addSkill = () => {
|
||||
profile.skills.push({ id: `new${newSkillId++}`, description: "", notes: undefined })
|
||||
v$.value.skills.$touch()
|
||||
}
|
||||
|
||||
/** Remove the given skill from the profile */
|
||||
const removeSkill = (skillId : string) => {
|
||||
profile.skills = profile.skills.filter(s => s.id !== skillId)
|
||||
v$.value.skills.$touch()
|
||||
}
|
||||
|
||||
/** Save the current profile values */
|
||||
const saveProfile = async () => {
|
||||
v$.value.$touch()
|
||||
if (v$.value.$error) return
|
||||
// Remove any blank skills before submitting
|
||||
profile.skills = profile.skills.filter(s => !(s.description.trim() === "" && (s.notes ?? "").trim() === ""))
|
||||
const saveResult = await api.profile.save(profile, user)
|
||||
if (typeof saveResult === "string") {
|
||||
toastError(saveResult, "saving profile")
|
||||
} else {
|
||||
toastSuccess("Profile Saved Successfuly")
|
||||
v$.value.$reset()
|
||||
}
|
||||
}
|
||||
|
||||
/** Whether the navigation confirmation is shown */
|
||||
const confirmNavShown = ref(false)
|
||||
|
||||
/** The "next" route (will be navigated or cleared) */
|
||||
const nextRoute : Ref<RouteLocationNormalized | undefined> = ref(undefined)
|
||||
|
||||
/** If the user has unsaved changes, give them an opportunity to save before moving on */
|
||||
onBeforeRouteLeave(async (to, from) => { // eslint-disable-line
|
||||
if (!v$.value.$anyDirty) return true
|
||||
nextRoute.value = to
|
||||
confirmNavShown.value = true
|
||||
return false
|
||||
})
|
||||
|
||||
/** Close the navigation confirmation modal */
|
||||
const confirmClose = () => { confirmNavShown.value = false }
|
||||
</script>
|
||||
|
@ -1,29 +1,22 @@
|
||||
<template lang="pug">
|
||||
article
|
||||
page-title(title='Logging off...')
|
||||
page-title(title="Logging off...")
|
||||
p
|
||||
p.fst-italic Logging off…
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { toastSuccess } from '@/components/layout/AppToaster.vue'
|
||||
import { useStore } from '@/store'
|
||||
<script setup lang="ts">
|
||||
import { onMounted } from "vue"
|
||||
import { useRouter } from "vue-router"
|
||||
import { toastSuccess } from "@/components/layout/AppToaster.vue"
|
||||
import { useStore } from "@/store"
|
||||
|
||||
export default defineComponent({
|
||||
name: 'LogOff',
|
||||
setup () {
|
||||
const store = useStore()
|
||||
const router = useRouter()
|
||||
const store = useStore()
|
||||
const router = useRouter()
|
||||
|
||||
onMounted(() => {
|
||||
store.commit('clearUser')
|
||||
toastSuccess('Log Off Successful | <strong>Have a Nice Day!</strong>')
|
||||
router.push('/')
|
||||
})
|
||||
|
||||
return { }
|
||||
}
|
||||
onMounted(() => {
|
||||
store.commit("clearUser")
|
||||
toastSuccess("Log Off Successful | <strong>Have a Nice Day!</strong>")
|
||||
router.push("/")
|
||||
})
|
||||
</script>
|
||||
|
@ -4,29 +4,21 @@ article
|
||||
p.fst-italic Sending you over to No Agenda Social to log on; see you back in just a second…
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue'
|
||||
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* This component simply redirects the user to the No Agenda Social authorization page; it is separate here so that it
|
||||
* can be called from two different places, and allow the app to support direct links to authorized content.
|
||||
*/
|
||||
export default defineComponent({
|
||||
name: 'LogOn',
|
||||
setup () {
|
||||
/** The authorization URL to which the user should be directed */
|
||||
const authUrl = (() => {
|
||||
/** The client ID for Jobs, Jobs, Jobs at No Agenda Social */
|
||||
const id = 'k_06zlMy0N451meL4AqlwMQzs5PYr6g3d2Q_dCT-OjU'
|
||||
const client = `client_id=${id}`
|
||||
const scope = 'scope=read:accounts'
|
||||
const redirect = `redirect_uri=${document.location.origin}/citizen/authorized`
|
||||
const respType = 'response_type=code'
|
||||
return `https://noagendasocial.com/oauth/authorize?${client}&${scope}&${redirect}&${respType}`
|
||||
})()
|
||||
document.location.assign(authUrl)
|
||||
|
||||
return { }
|
||||
}
|
||||
})
|
||||
/** The authorization URL to which the user should be directed */
|
||||
const authUrl = (() => {
|
||||
/** The client ID for Jobs, Jobs, Jobs at No Agenda Social */
|
||||
const id = 'k_06zlMy0N451meL4AqlwMQzs5PYr6g3d2Q_dCT-OjU'
|
||||
const client = `client_id=${id}`
|
||||
const scope = 'scope=read:accounts'
|
||||
const redirect = `redirect_uri=${document.location.origin}/citizen/authorized`
|
||||
const respType = 'response_type=code'
|
||||
return `https://noagendasocial.com/oauth/authorize?${client}&${scope}&${redirect}&${respType}`
|
||||
})()
|
||||
document.location.assign(authUrl)
|
||||
</script>
|
||||
|
@ -1,132 +1,117 @@
|
||||
<template lang="pug">
|
||||
article
|
||||
page-title(title='Help Wanted')
|
||||
page-title(title="Help Wanted")
|
||||
h3.pb-3 Help Wanted
|
||||
p(v-if="!searched").
|
||||
Enter relevant criteria to find results, or just click “Search” to see all current job listings.
|
||||
collapse-panel(headerText='Search Criteria' :collapsed='isCollapsed' @toggle='toggleCollapse')
|
||||
listing-search-form(v-model='criteria' @search='doSearch')
|
||||
error-list(:errors='errors')
|
||||
p.pt-3(v-if='searching') Searching job listings…
|
||||
collapse-panel(headerText="Search Criteria" :collapsed="isCollapsed" @toggle="toggleCollapse")
|
||||
listing-search-form(v-model="criteria" @search="doSearch")
|
||||
error-list(:errors="errors")
|
||||
p.pt-3(v-if="searching") Searching job listings…
|
||||
template(v-else)
|
||||
table.table.table-sm.table-hover.pt-3(v-if='results.length > 0')
|
||||
table.table.table-sm.table-hover.pt-3(v-if="results.length > 0")
|
||||
thead: tr
|
||||
th(scope='col') Listing
|
||||
th(scope='col') Title
|
||||
th(scope='col') Location
|
||||
th.text-center(scope='col') Remote?
|
||||
th.text-center(scope='col') Needed By
|
||||
tbody: tr(v-for='it in results' :key='it.listing.id')
|
||||
td: router-link(:to='`/listing/${it.listing.id}/view`') View
|
||||
th(scope="col") Listing
|
||||
th(scope="col") Title
|
||||
th(scope="col") Location
|
||||
th.text-center(scope="col") Remote?
|
||||
th.text-center(scope="col") Needed By
|
||||
tbody: tr(v-for="it in results" :key="it.listing.id")
|
||||
td: router-link(:to="`/listing/${it.listing.id}/view`") View
|
||||
td {{it.listing.title}}
|
||||
td {{it.continent.name}} / {{it.listing.region}}
|
||||
td.text-center {{yesOrNo(it.listing.remoteWork)}}
|
||||
td.text-center(v-if='it.listing.neededBy') {{formatNeededBy(it.listing.neededBy)}}
|
||||
td.text-center(v-if="it.listing.neededBy") {{formatNeededBy(it.listing.neededBy)}}
|
||||
td.text-center(v-else) N/A
|
||||
p.pt-3(v-else-if='searched') No job listings found for the specified criteria
|
||||
p.pt-3(v-else-if="searched") No job listings found for the specified criteria
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, Ref, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
<script setup lang="ts">
|
||||
import { ref, Ref, watch } from "vue"
|
||||
import { useRoute, useRouter } from "vue-router"
|
||||
|
||||
import { formatNeededBy } from './ListingView.vue'
|
||||
import { yesOrNo } from '@/App.vue'
|
||||
import api, { ListingForView, ListingSearch, LogOnSuccess } from '@/api'
|
||||
import { queryValue } from '@/router'
|
||||
import { useStore } from '@/store'
|
||||
import { formatNeededBy } from "./"
|
||||
import { yesOrNo } from "@/App.vue"
|
||||
import api, { ListingForView, ListingSearch, LogOnSuccess } from "@/api"
|
||||
import { queryValue } from "@/router"
|
||||
import { useStore } from "@/store"
|
||||
|
||||
import CollapsePanel from '@/components/CollapsePanel.vue'
|
||||
import ErrorList from '@/components/ErrorList.vue'
|
||||
import ListingSearchForm from '@/components/ListingSearchForm.vue'
|
||||
import CollapsePanel from "@/components/CollapsePanel.vue"
|
||||
import ErrorList from "@/components/ErrorList.vue"
|
||||
import ListingSearchForm from "@/components/ListingSearchForm.vue"
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
CollapsePanel,
|
||||
ErrorList,
|
||||
ListingSearchForm
|
||||
},
|
||||
setup () {
|
||||
const store = useStore()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const store = useStore()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
/** Any errors encountered while retrieving data */
|
||||
const errors : Ref<string[]> = ref([])
|
||||
/** Any errors encountered while retrieving data */
|
||||
const errors : Ref<string[]> = ref([])
|
||||
|
||||
/** Whether we are currently searching (retrieving data) */
|
||||
const searching = ref(false)
|
||||
/** Whether we are currently searching (retrieving data) */
|
||||
const searching = ref(false)
|
||||
|
||||
/** Whether a search has been performed on this page since it has been loaded */
|
||||
const searched = ref(false)
|
||||
/** Whether a search has been performed on this page since it has been loaded */
|
||||
const searched = ref(false)
|
||||
|
||||
/** An empty set of search criteria */
|
||||
const emptyCriteria = {
|
||||
continentId: '',
|
||||
region: undefined,
|
||||
remoteWork: '',
|
||||
text: undefined
|
||||
}
|
||||
/** An empty set of search criteria */
|
||||
const emptyCriteria = {
|
||||
continentId: "",
|
||||
region: undefined,
|
||||
remoteWork: "",
|
||||
text: undefined
|
||||
}
|
||||
|
||||
/** The search criteria being built from the page */
|
||||
const criteria : Ref<ListingSearch> = ref(emptyCriteria)
|
||||
/** The search criteria being built from the page */
|
||||
const criteria : Ref<ListingSearch> = ref(emptyCriteria)
|
||||
|
||||
/** The current search results */
|
||||
const results : Ref<ListingForView[]> = ref([])
|
||||
/** The current search results */
|
||||
const results : Ref<ListingForView[]> = ref([])
|
||||
|
||||
/** Whether the search criteria should be collapsed */
|
||||
const isCollapsed = ref(searched.value && results.value.length > 0)
|
||||
/** Whether the search criteria should be collapsed */
|
||||
const isCollapsed = ref(searched.value && results.value.length > 0)
|
||||
|
||||
/** Set up the page to match its requested state */
|
||||
const setUpPage = async () => {
|
||||
if (queryValue(route, 'searched') === 'true') {
|
||||
searched.value = true
|
||||
try {
|
||||
searching.value = true
|
||||
// Hold variable for ensuring continent ID is not undefined here, but excluded from search payload
|
||||
const contId = queryValue(route, 'continentId')
|
||||
const searchParams : ListingSearch = {
|
||||
continentId: contId === '' ? undefined : contId,
|
||||
region: queryValue(route, 'region'),
|
||||
remoteWork: queryValue(route, 'remoteWork') || '',
|
||||
text: queryValue(route, 'text')
|
||||
}
|
||||
const searchResult = await api.listings.search(searchParams, store.state.user as LogOnSuccess)
|
||||
if (typeof searchResult === 'string') {
|
||||
errors.value.push(searchResult)
|
||||
} else if (searchResult === undefined) {
|
||||
errors.value.push('The server returned a "Not Found" response (this should not happen)')
|
||||
} else {
|
||||
results.value = searchResult
|
||||
searchParams.continentId = searchParams.continentId || ''
|
||||
criteria.value = searchParams
|
||||
}
|
||||
} finally {
|
||||
searching.value = false
|
||||
}
|
||||
isCollapsed.value = searched.value && results.value.length > 0
|
||||
} else {
|
||||
searched.value = false
|
||||
criteria.value = emptyCriteria
|
||||
errors.value = []
|
||||
results.value = []
|
||||
/** Set up the page to match its requested state */
|
||||
const setUpPage = async () => {
|
||||
if (queryValue(route, "searched") === "true") {
|
||||
searched.value = true
|
||||
try {
|
||||
searching.value = true
|
||||
// Hold variable for ensuring continent ID is not undefined here, but excluded from search payload
|
||||
const contId = queryValue(route, "continentId")
|
||||
const searchParams : ListingSearch = {
|
||||
continentId: contId === "" ? undefined : contId,
|
||||
region: queryValue(route, "region"),
|
||||
remoteWork: queryValue(route, "remoteWork") ?? "",
|
||||
text: queryValue(route, "text")
|
||||
}
|
||||
const searchResult = await api.listings.search(searchParams, store.state.user as LogOnSuccess)
|
||||
if (typeof searchResult === "string") {
|
||||
errors.value.push(searchResult)
|
||||
} else if (searchResult === undefined) {
|
||||
errors.value.push(`The server returned a "Not Found" response (this should not happen)`)
|
||||
} else {
|
||||
results.value = searchResult
|
||||
searchParams.continentId = searchParams.continentId ?? ""
|
||||
criteria.value = searchParams
|
||||
}
|
||||
} finally {
|
||||
searching.value = false
|
||||
}
|
||||
|
||||
watch(() => route.query, setUpPage, { immediate: true })
|
||||
|
||||
return {
|
||||
errors,
|
||||
criteria,
|
||||
isCollapsed,
|
||||
toggleCollapse: (it : boolean) => { isCollapsed.value = it },
|
||||
doSearch: () => router.push({ query: { searched: 'true', ...criteria.value } }),
|
||||
searching,
|
||||
searched,
|
||||
results,
|
||||
yesOrNo,
|
||||
formatNeededBy
|
||||
}
|
||||
isCollapsed.value = searched.value && results.value.length > 0
|
||||
} else {
|
||||
searched.value = false
|
||||
criteria.value = emptyCriteria
|
||||
errors.value = []
|
||||
results.value = []
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/** Refresh the page when the query string changes */
|
||||
watch(() => route.query, setUpPage, { immediate: true })
|
||||
|
||||
/** Show or hide the search parameter panel */
|
||||
const toggleCollapse = (it : boolean) => { isCollapsed.value = it }
|
||||
|
||||
/** Execute a search */
|
||||
const doSearch = () => router.push({ query: { searched: "true", ...criteria.value } })
|
||||
</script>
|
||||
|
@ -3,166 +3,150 @@ article
|
||||
page-title(:title="isNew ? 'Add a Job Listing' : 'Edit Job Listing'")
|
||||
h3.pb-3(v-if="isNew") Add a Job Listing
|
||||
h3.pb-3(v-else) Edit Job Listing
|
||||
load-data(:load='retrieveData'): form.row.g-3
|
||||
load-data(:load="retrieveData"): form.row.g-3
|
||||
.col-12.col-sm-10.col-md-8.col-lg-6
|
||||
.form-floating
|
||||
input.form-control(type='text' id='title' :class="{ 'is-invalid': v$.title.$error }" maxlength='255'
|
||||
v-model='v$.title.$model' placeholder='The title for the job listing')
|
||||
input.form-control(type="text" id="title" :class="{ 'is-invalid': v$.title.$error }" maxlength="255"
|
||||
v-model="v$.title.$model" placeholder="The title for the job listing")
|
||||
#titleFeedback.invalid-feedback Please enter a title for the job listing
|
||||
label.jjj-required(for='title') Title
|
||||
label.jjj-required(for="title") Title
|
||||
.form-text No need to put location here; it will always be show to seekers with continent and region
|
||||
.col-12.col-sm-6.col-md-4
|
||||
continent-list(v-model='v$.continentId.$model' :isInvalid='v$.continentId.$error'
|
||||
@touch='v$.continentId.$touch() || true')
|
||||
continent-list(v-model="v$.continentId.$model" :isInvalid="v$.continentId.$error"
|
||||
@touch="v$.continentId.$touch() || true")
|
||||
.col-12.col-sm-6.col-md-8
|
||||
.form-floating
|
||||
input.form-control(type='text' id='region' :class="{ 'is-invalid': v$.region.$error }" maxlength='255'
|
||||
v-model='v$.region.$model' placeholder='Country, state, geographic area, etc.')
|
||||
input.form-control(type="text" id="region" :class="{ 'is-invalid': v$.region.$error }" maxlength="255"
|
||||
v-model="v$.region.$model" placeholder="Country, state, geographic area, etc.")
|
||||
#regionFeedback.invalid-feedback Please enter a region
|
||||
label.jjj-required(for='region') Region
|
||||
label.jjj-required(for="region") Region
|
||||
.form-text Country, state, geographic area, etc.
|
||||
.col-12: .form-check
|
||||
input.form-check-input(type='checkbox' id='isRemote' v-model='v$.remoteWork.$model')
|
||||
label.form-check-label(for='isRemote') This opportunity is for remote work
|
||||
markdown-editor(id='description' label='Job Description' v-model:text='v$.text.$model' :isInvalid='v$.text.$error')
|
||||
input.form-check-input(type="checkbox" id="isRemote" v-model="v$.remoteWork.$model")
|
||||
label.form-check-label(for="isRemote") This opportunity is for remote work
|
||||
markdown-editor(id="description" label="Job Description" v-model:text="v$.text.$model" :isInvalid="v$.text.$error")
|
||||
.col-12.col-md-4: .form-floating
|
||||
input.form-control(type='date' id='neededBy' v-model='v$.neededBy.$model'
|
||||
placeholder='Date by which this position needs to be filled')
|
||||
label(for='neededBy') Needed By
|
||||
input.form-control(type="date" id="neededBy" v-model="v$.neededBy.$model"
|
||||
placeholder="Date by which this position needs to be filled")
|
||||
label(for="neededBy") Needed By
|
||||
.col-12
|
||||
p.text-danger(v-if='v$.$error') Please correct the errors above
|
||||
button.btn.btn-primary(@click.prevent='saveListing(true)') #[icon(icon='content-save-outline')] Save
|
||||
maybe-save(:isShown='confirmNavShown' :toRoute='nextRoute' :saveAction='doSave' :validator='v$' @close='confirmClose')
|
||||
p.text-danger(v-if="v$.$error") Please correct the errors above
|
||||
button.btn.btn-primary(@click.prevent="saveListing(true)") #[icon(icon="content-save-outline")] Save
|
||||
maybe-save(:isShown="confirmNavShown" :toRoute="nextRoute" :saveAction="doSave" :validator="v$" @close="confirmClose")
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, reactive, ref, Ref } from 'vue'
|
||||
import { onBeforeRouteLeave, RouteLocationNormalized, useRoute, useRouter } from 'vue-router'
|
||||
import useVuelidate from '@vuelidate/core'
|
||||
import { required } from '@vuelidate/validators'
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive, ref, Ref } from "vue"
|
||||
import { onBeforeRouteLeave, RouteLocationNormalized, useRoute, useRouter } from "vue-router"
|
||||
import useVuelidate from "@vuelidate/core"
|
||||
import { required } from "@vuelidate/validators"
|
||||
|
||||
import api, { Listing, ListingForm, LogOnSuccess } from '@/api'
|
||||
import { toastError, toastSuccess } from '@/components/layout/AppToaster.vue'
|
||||
import { useStore } from '@/store'
|
||||
import api, { Listing, ListingForm, LogOnSuccess } from "@/api"
|
||||
import { toastError, toastSuccess } from "@/components/layout/AppToaster.vue"
|
||||
import { useStore } from "@/store"
|
||||
|
||||
import ContinentList from '@/components/ContinentList.vue'
|
||||
import LoadData from '@/components/LoadData.vue'
|
||||
import MarkdownEditor from '@/components/MarkdownEditor.vue'
|
||||
import MaybeSave from '@/components/MaybeSave.vue'
|
||||
import ContinentList from "@/components/ContinentList.vue"
|
||||
import LoadData from "@/components/LoadData.vue"
|
||||
import MarkdownEditor from "@/components/MarkdownEditor.vue"
|
||||
import MaybeSave from "@/components/MaybeSave.vue"
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ListingEdit',
|
||||
components: {
|
||||
ContinentList,
|
||||
LoadData,
|
||||
MarkdownEditor,
|
||||
MaybeSave
|
||||
},
|
||||
setup () {
|
||||
const store = useStore()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const store = useStore()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
/** The currently logged-on user */
|
||||
const user = store.state.user as LogOnSuccess
|
||||
/** The currently logged-on user */
|
||||
const user = store.state.user as LogOnSuccess
|
||||
|
||||
/** A new job listing */
|
||||
const newListing : Listing = {
|
||||
id: '',
|
||||
citizenId: user.citizenId,
|
||||
createdOn: '',
|
||||
title: '',
|
||||
continentId: '',
|
||||
region: '',
|
||||
remoteWork: false,
|
||||
isExpired: false,
|
||||
updatedOn: '',
|
||||
text: '',
|
||||
neededBy: undefined,
|
||||
wasFilledHere: undefined
|
||||
}
|
||||
/** A new job listing */
|
||||
const newListing : Listing = {
|
||||
id: '',
|
||||
citizenId: user.citizenId,
|
||||
createdOn: '',
|
||||
title: '',
|
||||
continentId: '',
|
||||
region: '',
|
||||
remoteWork: false,
|
||||
isExpired: false,
|
||||
updatedOn: '',
|
||||
text: '',
|
||||
neededBy: undefined,
|
||||
wasFilledHere: undefined
|
||||
}
|
||||
|
||||
/** The backing object for the form */
|
||||
const listing = reactive(new ListingForm())
|
||||
/** The backing object for the form */
|
||||
const listing = reactive(new ListingForm())
|
||||
|
||||
/** The ID of the listing requested */
|
||||
const id = route.params.id as string
|
||||
/** The ID of the listing requested */
|
||||
const id = route.params.id as string
|
||||
|
||||
/** Is this a new job listing? */
|
||||
const isNew = computed(() => id === 'new')
|
||||
/** Is this a new job listing? */
|
||||
const isNew = computed(() => id === "new")
|
||||
|
||||
/** Validation rules for the form */
|
||||
const rules = computed(() => ({
|
||||
id: { },
|
||||
title: { required },
|
||||
continentId: { required },
|
||||
region: { required },
|
||||
remoteWork: { },
|
||||
text: { required },
|
||||
neededBy: { }
|
||||
}))
|
||||
/** Validation rules for the form */
|
||||
const rules = computed(() => ({
|
||||
id: { },
|
||||
title: { required },
|
||||
continentId: { required },
|
||||
region: { required },
|
||||
remoteWork: { },
|
||||
text: { required },
|
||||
neededBy: { }
|
||||
}))
|
||||
|
||||
/** Initialize form validation */
|
||||
const v$ = useVuelidate(rules, listing, { $lazy: true })
|
||||
/** Initialize form validation */
|
||||
const v$ = useVuelidate(rules, listing, { $lazy: true })
|
||||
|
||||
/** Retrieve the listing being edited (or set up the form for a new listing) */
|
||||
const retrieveData = async (errors : string[]) => {
|
||||
const listResult = isNew.value ? newListing : await api.listings.retreive(id, user)
|
||||
if (typeof listResult === 'string') {
|
||||
errors.push(listResult)
|
||||
} else if (typeof listResult === 'undefined') {
|
||||
errors.push('Job listing not found')
|
||||
} else {
|
||||
listing.id = listResult.id
|
||||
listing.title = listResult.title
|
||||
listing.continentId = listResult.continentId
|
||||
listing.region = listResult.region
|
||||
listing.remoteWork = listResult.remoteWork
|
||||
listing.text = listResult.text
|
||||
listing.neededBy = listResult.neededBy
|
||||
}
|
||||
}
|
||||
|
||||
/** Save the job listing */
|
||||
const saveListing = async (navigate : boolean) => {
|
||||
v$.value.$touch()
|
||||
if (v$.value.$error) return
|
||||
const apiFunc = isNew.value ? api.listings.add : api.listings.update
|
||||
if (listing.neededBy === '') listing.neededBy = undefined
|
||||
const result = await apiFunc(listing, user)
|
||||
if (typeof result === 'string') {
|
||||
toastError(result, 'saving job listing')
|
||||
} else {
|
||||
toastSuccess(`Job Listing ${isNew.value ? 'Add' : 'Updat'}ed Successfully`)
|
||||
v$.value.$reset()
|
||||
if (navigate) router.push('/listings/mine')
|
||||
}
|
||||
}
|
||||
|
||||
/** Whether the navigation confirmation is shown */
|
||||
const confirmNavShown = ref(false)
|
||||
|
||||
/** The "next" route (will be navigated or cleared) */
|
||||
const nextRoute : Ref<RouteLocationNormalized | undefined> = ref(undefined)
|
||||
|
||||
/** If the user has unsaved changes, give them an opportunity to save before moving on */
|
||||
onBeforeRouteLeave(async (to, from) => { // eslint-disable-line
|
||||
if (!v$.value.$anyDirty) return true
|
||||
nextRoute.value = to
|
||||
confirmNavShown.value = true
|
||||
return false
|
||||
})
|
||||
|
||||
return {
|
||||
isNew,
|
||||
v$,
|
||||
retrieveData,
|
||||
saveListing,
|
||||
confirmNavShown,
|
||||
nextRoute,
|
||||
doSave: async () => await saveListing(false),
|
||||
confirmClose: () => { confirmNavShown.value = false }
|
||||
}
|
||||
/** Retrieve the listing being edited (or set up the form for a new listing) */
|
||||
const retrieveData = async (errors : string[]) => {
|
||||
const listResult = isNew.value ? newListing : await api.listings.retreive(id, user)
|
||||
if (typeof listResult === "string") {
|
||||
errors.push(listResult)
|
||||
} else if (typeof listResult === "undefined") {
|
||||
errors.push("Job listing not found")
|
||||
} else {
|
||||
listing.id = listResult.id
|
||||
listing.title = listResult.title
|
||||
listing.continentId = listResult.continentId
|
||||
listing.region = listResult.region
|
||||
listing.remoteWork = listResult.remoteWork
|
||||
listing.text = listResult.text
|
||||
listing.neededBy = listResult.neededBy
|
||||
}
|
||||
}
|
||||
|
||||
/** Save the job listing */
|
||||
const saveListing = async (navigate : boolean) => {
|
||||
v$.value.$touch()
|
||||
if (v$.value.$error) return
|
||||
const apiFunc = isNew.value ? api.listings.add : api.listings.update
|
||||
if (listing.neededBy === "") listing.neededBy = undefined
|
||||
const result = await apiFunc(listing, user)
|
||||
if (typeof result === "string") {
|
||||
toastError(result, "saving job listing")
|
||||
} else {
|
||||
toastSuccess(`Job Listing ${isNew.value ? "Add" : "Updat"}ed Successfully`)
|
||||
v$.value.$reset()
|
||||
if (navigate) router.push("/listings/mine")
|
||||
}
|
||||
}
|
||||
|
||||
/** Whether the navigation confirmation is shown */
|
||||
const confirmNavShown = ref(false)
|
||||
|
||||
/** The "next" route (will be navigated or cleared) */
|
||||
const nextRoute : Ref<RouteLocationNormalized | undefined> = ref(undefined)
|
||||
|
||||
/** If the user has unsaved changes, give them an opportunity to save before moving on */
|
||||
onBeforeRouteLeave(async (to, from) => { // eslint-disable-line
|
||||
if (!v$.value.$anyDirty) return true
|
||||
nextRoute.value = to
|
||||
confirmNavShown.value = true
|
||||
return false
|
||||
})
|
||||
|
||||
/** Parameterless save function (used to save when navigating away) */
|
||||
const doSave = async () => await saveListing(false)
|
||||
|
||||
/** Close the navigation confirmation modal */
|
||||
const confirmClose = () => { confirmNavShown.value = false }
|
||||
</script>
|
||||
|
@ -1,84 +1,69 @@
|
||||
<template lang="pug">
|
||||
article
|
||||
page-title(:title='pageTitle')
|
||||
load-data(:load='retrieveListing')
|
||||
page-title(:title="title")
|
||||
load-data(:load="retrieveListing")
|
||||
h3 {{it.listing.title}}
|
||||
h4.pb-3.text-muted {{it.continent.name}} / {{it.listing.region}}
|
||||
p
|
||||
template(v-if='it.listing.neededBy').
|
||||
template(v-if="it.listing.neededBy").
|
||||
#[strong #[em NEEDED BY {{neededBy(it.listing.neededBy)}}]] •
|
||||
| Listed by #[a(:href='profileUrl' target='_blank') {{citizenName(citizen)}}]
|
||||
| Listed by #[a(:href="profileUrl" target="_blank") {{citizenName(citizen)}}]
|
||||
hr
|
||||
div(v-html='details')
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, ref, Ref } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { format } from 'date-fns'
|
||||
import marked from 'marked'
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, Ref } from "vue"
|
||||
import { useRoute } from "vue-router"
|
||||
|
||||
import api, { Citizen, ListingForView, LogOnSuccess, markedOptions } from '@/api'
|
||||
import { citizenName } from '@/App.vue'
|
||||
import { useStore } from '@/store'
|
||||
import LoadData from '@/components/LoadData.vue'
|
||||
import { formatNeededBy } from "./"
|
||||
import api, { Citizen, ListingForView, LogOnSuccess } from "@/api"
|
||||
import { citizenName } from "@/App.vue"
|
||||
import { toHtml } from "@/markdown"
|
||||
import { useStore } from "@/store"
|
||||
import LoadData from "@/components/LoadData.vue"
|
||||
|
||||
/**
|
||||
* Format the needed by date for display
|
||||
*
|
||||
* @param neededBy The defined needed by date
|
||||
* @returns The date to display
|
||||
*/
|
||||
export function formatNeededBy (neededBy : string) : string {
|
||||
return format(Date.parse(neededBy), 'PPP')
|
||||
}
|
||||
const store = useStore()
|
||||
const route = useRoute()
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ListingView',
|
||||
components: { LoadData },
|
||||
setup () {
|
||||
const store = useStore()
|
||||
const route = useRoute()
|
||||
/** The currently logged-on user */
|
||||
const user = store.state.user as LogOnSuccess
|
||||
|
||||
/** The currently logged-on user */
|
||||
const user = store.state.user as LogOnSuccess
|
||||
/** The requested job listing */
|
||||
const it : Ref<ListingForView | undefined> = ref(undefined)
|
||||
|
||||
/** The requested job listing */
|
||||
const it : Ref<ListingForView | undefined> = ref(undefined)
|
||||
/** The citizen who posted this job listing */
|
||||
const citizen : Ref<Citizen | undefined> = ref(undefined)
|
||||
|
||||
/** The citizen who posted this job listing */
|
||||
const citizen : Ref<Citizen | undefined> = ref(undefined)
|
||||
|
||||
/** Retrieve the job listing and supporting data */
|
||||
const retrieveListing = async (errors : string[]) => {
|
||||
const listingResp = await api.listings.retreiveForView(route.params.id as string, user)
|
||||
if (typeof listingResp === 'string') {
|
||||
errors.push(listingResp)
|
||||
} else if (typeof listingResp === 'undefined') {
|
||||
errors.push('Job Listing not found')
|
||||
} else {
|
||||
it.value = listingResp
|
||||
const citizenResp = await api.citizen.retrieve(listingResp.listing.citizenId, user)
|
||||
if (typeof citizenResp === 'string') {
|
||||
errors.push(citizenResp)
|
||||
} else if (typeof citizenResp === 'undefined') {
|
||||
errors.push('Listing Citizen not found (this should not happen)')
|
||||
} else {
|
||||
citizen.value = citizenResp
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
pageTitle: computed(() => it.value ? `${it.value.listing.title} | Job Listing` : 'Loading Job Listing...'),
|
||||
retrieveListing,
|
||||
it,
|
||||
details: computed(() => marked(it.value?.listing.text || '', markedOptions)),
|
||||
citizen,
|
||||
profileUrl: computed(() => citizen.value ? `https://noagendasocial.com/@${citizen.value.naUser}` : ''),
|
||||
citizenName,
|
||||
neededBy: (nb : string) => formatNeededBy(nb).toUpperCase()
|
||||
/** Retrieve the job listing and supporting data */
|
||||
const retrieveListing = async (errors : string[]) => {
|
||||
const listingResp = await api.listings.retreiveForView(route.params.id as string, user)
|
||||
if (typeof listingResp === "string") {
|
||||
errors.push(listingResp)
|
||||
} else if (typeof listingResp === "undefined") {
|
||||
errors.push("Job Listing not found")
|
||||
} else {
|
||||
it.value = listingResp
|
||||
const citizenResp = await api.citizen.retrieve(listingResp.listing.citizenId, user)
|
||||
if (typeof citizenResp === "string") {
|
||||
errors.push(citizenResp)
|
||||
} else if (typeof citizenResp === "undefined") {
|
||||
errors.push("Listing Citizen not found (this should not happen)")
|
||||
} else {
|
||||
citizen.value = citizenResp
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/** The page title (changes once the listing is loaded) */
|
||||
const title = computed(() => it.value ? `${it.value.listing.title} | Job Listing` : "Loading Job Listing...")
|
||||
|
||||
/** The HTML details of the job listing */
|
||||
const details = computed(() => toHtml(it.value?.listing.text ?? ""))
|
||||
|
||||
/** The NAS profile URL for the citizen who posted this job listing */
|
||||
const profileUrl = computed(() => citizen.value ? `https://noagendasocial.com/@${citizen.value.naUser}` : "")
|
||||
|
||||
/** The needed by date, formatted in SHOUTING MODE */
|
||||
const neededBy = (nb : string) => formatNeededBy(nb).toUpperCase()
|
||||
</script>
|
||||
|
@ -1,18 +1,18 @@
|
||||
<template lang="pug">
|
||||
article
|
||||
page-title(title='My Job Listings')
|
||||
page-title(title="My Job Listings")
|
||||
h3.pb-3 My Job Listings
|
||||
p: router-link.btn.btn-outline-primary(to='/listing/new/edit') Add a New Job Listing
|
||||
p: router-link.btn.btn-outline-primary(to="/listing/new/edit") Add a New Job Listing
|
||||
load-data(:load="getListings")
|
||||
table.table.table-sm.table-hover.pt-3(v-if='listings.length > 0')
|
||||
thead: tr
|
||||
th(scope='col') Action
|
||||
th(scope='col') Title
|
||||
th(scope='col') Continent / Region
|
||||
th(scope='col') Created
|
||||
th(scope='col') Updated
|
||||
th(scope="col") Action
|
||||
th(scope="col") Title
|
||||
th(scope="col") Continent / Region
|
||||
th(scope="col") Created
|
||||
th(scope="col") Updated
|
||||
tbody: tr(v-for='it in listings' :key='it.listing.id')
|
||||
td: router-link(:to='`/listing/${it.listing.id}/edit`') Edit
|
||||
td: router-link(:to="`/listing/${it.listing.id}/edit`") Edit
|
||||
td {{it.listing.title}}
|
||||
td {{it.continent.name}} / {{it.listing.region}}
|
||||
td: full-date-time(:date='it.listing.createdOn')
|
||||
@ -20,42 +20,28 @@ article
|
||||
p.fst-italic(v-else) No job listings found
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, Ref, ref } from 'vue'
|
||||
import api, { ListingForView, LogOnSuccess } from '@/api'
|
||||
import { useStore } from '@/store'
|
||||
<script setup lang="ts">
|
||||
import { Ref, ref } from "vue"
|
||||
import api, { ListingForView, LogOnSuccess } from "@/api"
|
||||
import { useStore } from "@/store"
|
||||
|
||||
import FullDateTime from '@/components/FullDateTime.vue'
|
||||
import LoadData from '@/components/LoadData.vue'
|
||||
import FullDateTime from "@/components/FullDateTime.vue"
|
||||
import LoadData from "@/components/LoadData.vue"
|
||||
|
||||
export default defineComponent({
|
||||
name: 'MyListings',
|
||||
components: {
|
||||
FullDateTime,
|
||||
LoadData
|
||||
},
|
||||
setup () {
|
||||
const store = useStore()
|
||||
const store = useStore()
|
||||
|
||||
/** The listings for the user */
|
||||
const listings : Ref<ListingForView[]> = ref([])
|
||||
/** The listings for the user */
|
||||
const listings : Ref<ListingForView[]> = ref([])
|
||||
|
||||
/** Retrieve the job listing posted by the current citizen */
|
||||
const getListings = async (errors : string[]) => {
|
||||
const listResult = await api.listings.mine(store.state.user as LogOnSuccess)
|
||||
if (typeof listResult === 'string') {
|
||||
errors.push(listResult)
|
||||
} else if (typeof listResult === 'undefined') {
|
||||
errors.push('API call returned 404 (this should not happen)')
|
||||
} else {
|
||||
listings.value = listResult
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
getListings,
|
||||
listings
|
||||
}
|
||||
/** Retrieve the job listing posted by the current citizen */
|
||||
const getListings = async (errors : string[]) => {
|
||||
const listResult = await api.listings.mine(store.state.user as LogOnSuccess)
|
||||
if (typeof listResult === "string") {
|
||||
errors.push(listResult)
|
||||
} else if (typeof listResult === "undefined") {
|
||||
errors.push("API call returned 404 (this should not happen)")
|
||||
} else {
|
||||
listings.value = listResult
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
11
src/JobsJobsJobs/App/src/views/listing/index.ts
Normal file
11
src/JobsJobsJobs/App/src/views/listing/index.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { format } from "date-fns"
|
||||
|
||||
/**
|
||||
* Format the needed by date for display
|
||||
*
|
||||
* @param neededBy The defined needed by date
|
||||
* @returns The date to display
|
||||
*/
|
||||
export function formatNeededBy (neededBy : string) : string {
|
||||
return format(Date.parse(neededBy), "PPP")
|
||||
}
|
@ -1,133 +1,117 @@
|
||||
<template lang="pug">
|
||||
article
|
||||
page-title(title='Search Profiles')
|
||||
page-title(title="Search Profiles")
|
||||
h3.pb-3 Search Profiles
|
||||
p(v-if='!searched').
|
||||
p(v-if="!searched").
|
||||
Enter one or more criteria to filter results, or just click “Search” to list all profiles.
|
||||
collapse-panel(headerText='Search Criteria' :collapsed='isCollapsed' @toggle='toggleCollapse')
|
||||
profile-search-form(v-model='criteria' @search='doSearch')
|
||||
error-list(:errors='errors')
|
||||
p.pt-3(v-if='searching') Searching profiles…
|
||||
collapse-panel(headerText="Search Criteria" :collapsed="isCollapsed" @toggle="toggleCollapse")
|
||||
profile-search-form(v-model="criteria" @search="doSearch")
|
||||
error-list(:errors="errors")
|
||||
p.pt-3(v-if="searching") Searching profiles…
|
||||
template(v-else)
|
||||
table.table.table-sm.table-hover.pt-3(v-if='results.length > 0')
|
||||
table.table.table-sm.table-hover.pt-3(v-if="results.length > 0")
|
||||
thead: tr
|
||||
th(scope='col') Profile
|
||||
th(scope='col') Name
|
||||
th.text-center(scope='col') Seeking?
|
||||
th.text-center(scope='col') Remote?
|
||||
th.text-center(scope='col') Full-Time?
|
||||
th(scope='col') Last Updated
|
||||
tbody: tr(v-for='profile in results' :key='profile.citzenId')
|
||||
td: router-link(:to='`/profile/${profile.citizenId}/view`') View
|
||||
th(scope="col") Profile
|
||||
th(scope="col") Name
|
||||
th.text-center(scope="col") Seeking?
|
||||
th.text-center(scope="col") Remote?
|
||||
th.text-center(scope="col") Full-Time?
|
||||
th(scope="col") Last Updated
|
||||
tbody: tr(v-for="profile in results" :key="profile.citzenId")
|
||||
td: router-link(:to="`/profile/${profile.citizenId}/view`") View
|
||||
td(:class="{ 'font-weight-bold' : profile.seekingEmployment }") {{profile.displayName}}
|
||||
td.text-center {{yesOrNo(profile.seekingEmployment)}}
|
||||
td.text-center {{yesOrNo(profile.remoteWork)}}
|
||||
td.text-center {{yesOrNo(profile.fullTime)}}
|
||||
td: full-date(:date='profile.lastUpdatedOn')
|
||||
p.pt-3(v-else-if='searched') No results found for the specified criteria
|
||||
td: full-date(:date="profile.lastUpdatedOn")
|
||||
p.pt-3(v-else-if="searched") No results found for the specified criteria
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, Ref, ref, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { yesOrNo } from '@/App.vue'
|
||||
import api, { LogOnSuccess, ProfileSearch, ProfileSearchResult } from '@/api'
|
||||
import { queryValue } from '@/router'
|
||||
import { useStore } from '@/store'
|
||||
<script setup lang="ts">
|
||||
import { defineComponent, Ref, ref, watch } from "vue"
|
||||
import { useRoute, useRouter } from "vue-router"
|
||||
import { yesOrNo } from "@/App.vue"
|
||||
import api, { LogOnSuccess, ProfileSearch, ProfileSearchResult } from "@/api"
|
||||
import { queryValue } from "@/router"
|
||||
import { useStore } from "@/store"
|
||||
|
||||
import CollapsePanel from '@/components/CollapsePanel.vue'
|
||||
import ErrorList from '@/components/ErrorList.vue'
|
||||
import FullDate from '@/components/FullDate.vue'
|
||||
import ProfileSearchForm from '@/components/profile/SearchForm.vue'
|
||||
import CollapsePanel from "@/components/CollapsePanel.vue"
|
||||
import ErrorList from "@/components/ErrorList.vue"
|
||||
import FullDate from "@/components/FullDate.vue"
|
||||
import ProfileSearchForm from "@/components/profile/SearchForm.vue"
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ProfileSearch',
|
||||
components: {
|
||||
CollapsePanel,
|
||||
ErrorList,
|
||||
FullDate,
|
||||
ProfileSearchForm
|
||||
},
|
||||
setup () {
|
||||
const store = useStore()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const store = useStore()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
/** Any errors encountered while retrieving data */
|
||||
const errors : Ref<string[]> = ref([])
|
||||
/** Any errors encountered while retrieving data */
|
||||
const errors : Ref<string[]> = ref([])
|
||||
|
||||
/** Whether we are currently searching (retrieving data) */
|
||||
const searching = ref(false)
|
||||
/** Whether we are currently searching (retrieving data) */
|
||||
const searching = ref(false)
|
||||
|
||||
/** Whether a search has been performed on this page since it has been loaded */
|
||||
const searched = ref(false)
|
||||
/** Whether a search has been performed on this page since it has been loaded */
|
||||
const searched = ref(false)
|
||||
|
||||
/** An empty set of search criteria */
|
||||
const emptyCriteria = {
|
||||
continentId: '',
|
||||
skill: undefined,
|
||||
bioExperience: undefined,
|
||||
remoteWork: ''
|
||||
}
|
||||
/** An empty set of search criteria */
|
||||
const emptyCriteria = {
|
||||
continentId: '',
|
||||
skill: undefined,
|
||||
bioExperience: undefined,
|
||||
remoteWork: ''
|
||||
}
|
||||
|
||||
/** The search criteria being built from the page */
|
||||
const criteria : Ref<ProfileSearch> = ref(emptyCriteria)
|
||||
/** The search criteria being built from the page */
|
||||
const criteria : Ref<ProfileSearch> = ref(emptyCriteria)
|
||||
|
||||
/** The current search results */
|
||||
const results : Ref<ProfileSearchResult[]> = ref([])
|
||||
/** The current search results */
|
||||
const results : Ref<ProfileSearchResult[]> = ref([])
|
||||
|
||||
/** Whether the search criteria should be collapsed */
|
||||
const isCollapsed = ref(searched.value && results.value.length > 0)
|
||||
/** Whether the search criteria should be collapsed */
|
||||
const isCollapsed = ref(searched.value && results.value.length > 0)
|
||||
|
||||
/** Set up the page to match its requested state */
|
||||
const setUpPage = async () => {
|
||||
if (queryValue(route, 'searched') === 'true') {
|
||||
searched.value = true
|
||||
try {
|
||||
searching.value = true
|
||||
// Hold variable for ensuring continent ID is not undefined here, but excluded from search payload
|
||||
const contId = queryValue(route, 'continentId')
|
||||
const searchParams : ProfileSearch = {
|
||||
continentId: contId === '' ? undefined : contId,
|
||||
skill: queryValue(route, 'skill'),
|
||||
bioExperience: queryValue(route, 'bioExperience'),
|
||||
remoteWork: queryValue(route, 'remoteWork') || ''
|
||||
}
|
||||
const searchResult = await api.profile.search(searchParams, store.state.user as LogOnSuccess)
|
||||
if (typeof searchResult === 'string') {
|
||||
errors.value.push(searchResult)
|
||||
} else if (searchResult === undefined) {
|
||||
errors.value.push('The server returned a "Not Found" response (this should not happen)')
|
||||
} else {
|
||||
results.value = searchResult
|
||||
searchParams.continentId = searchParams.continentId || ''
|
||||
criteria.value = searchParams
|
||||
}
|
||||
} finally {
|
||||
searching.value = false
|
||||
}
|
||||
isCollapsed.value = searched.value && results.value.length > 0
|
||||
} else {
|
||||
searched.value = false
|
||||
criteria.value = emptyCriteria
|
||||
errors.value = []
|
||||
results.value = []
|
||||
/** Set up the page to match its requested state */
|
||||
const setUpPage = async () => {
|
||||
if (queryValue(route, "searched") === "true") {
|
||||
searched.value = true
|
||||
try {
|
||||
searching.value = true
|
||||
// Hold variable for ensuring continent ID is not undefined here, but excluded from search payload
|
||||
const contId = queryValue(route, "continentId")
|
||||
const searchParams : ProfileSearch = {
|
||||
continentId: contId === "" ? undefined : contId,
|
||||
skill: queryValue(route, "skill"),
|
||||
bioExperience: queryValue(route, "bioExperience"),
|
||||
remoteWork: queryValue(route, "remoteWork") ?? ""
|
||||
}
|
||||
const searchResult = await api.profile.search(searchParams, store.state.user as LogOnSuccess)
|
||||
if (typeof searchResult === "string") {
|
||||
errors.value.push(searchResult)
|
||||
} else if (searchResult === undefined) {
|
||||
errors.value.push(`The server returned a "Not Found" response (this should not happen)`)
|
||||
} else {
|
||||
results.value = searchResult
|
||||
searchParams.continentId = searchParams.continentId ?? ""
|
||||
criteria.value = searchParams
|
||||
}
|
||||
} finally {
|
||||
searching.value = false
|
||||
}
|
||||
|
||||
watch(() => route.query, setUpPage, { immediate: true })
|
||||
|
||||
return {
|
||||
errors,
|
||||
criteria,
|
||||
isCollapsed,
|
||||
toggleCollapse: (it : boolean) => { isCollapsed.value = it },
|
||||
doSearch: () => router.push({ query: { searched: 'true', ...criteria.value } }),
|
||||
searching,
|
||||
searched,
|
||||
results,
|
||||
yesOrNo
|
||||
}
|
||||
isCollapsed.value = searched.value && results.value.length > 0
|
||||
} else {
|
||||
searched.value = false
|
||||
criteria.value = emptyCriteria
|
||||
errors.value = []
|
||||
results.value = []
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/** Refresh the page when the query string changes */
|
||||
watch(() => route.query, setUpPage, { immediate: true })
|
||||
|
||||
/** Show and hide the search parameter panel */
|
||||
const toggleCollapse = (it : boolean) => { isCollapsed.value = it }
|
||||
|
||||
/** Execute a search */
|
||||
const doSearch = () => router.push({ query: { searched: 'true', ...criteria.value } })
|
||||
</script>
|
||||
|
@ -1,90 +1,83 @@
|
||||
<template lang="pug">
|
||||
article
|
||||
page-title(:title='pageTitle')
|
||||
load-data(:load='retrieveProfile')
|
||||
h2: a(:href='it.citizen.profileUrl' target='_blank') {{citizenName(it.citizen)}}
|
||||
page-title(:title="title")
|
||||
load-data(:load="retrieveProfile")
|
||||
h2: a(:href="it.citizen.profileUrl" target="_blank") {{citizenName(it.citizen)}}
|
||||
h4.pb-3 {{it.continent.name}}, {{it.profile.region}}
|
||||
p(v-html='workTypes')
|
||||
p(v-html="workTypes")
|
||||
hr
|
||||
div(v-html='bioHtml')
|
||||
template(v-if='it.profile.skills.length > 0')
|
||||
div(v-html="bioHtml")
|
||||
template(v-if="it.profile.skills.length > 0")
|
||||
hr
|
||||
h4.pb-3 Skills
|
||||
ul
|
||||
li(v-for='(skill, idx) in it.profile.skills' :key='idx').
|
||||
{{skill.description}}#[template(v-if='skill.notes') ({{skill.notes}})]
|
||||
template(v-if='it.profile.experience')
|
||||
li(v-for="(skill, idx) in it.profile.skills" :key="idx").
|
||||
{{skill.description}}#[template(v-if="skill.notes") ({{skill.notes}})]
|
||||
template(v-if="it.profile.experience")
|
||||
hr
|
||||
h4.pb-3 Experience / Employment History
|
||||
div(v-html='expHtml')
|
||||
template(v-if='user.citizenId === it.citizen.id')
|
||||
div(v-html="expHtml")
|
||||
template(v-if="user.citizenId === it.citizen.id")
|
||||
br
|
||||
br
|
||||
router-link.btn.btn-primary(to='/citizen/profile') #[icon(icon='pencil')] Edit Your Profile
|
||||
router-link.btn.btn-primary(to="/citizen/profile") #[icon(icon="pencil")] Edit Your Profile
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, ref, Ref } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import marked from 'marked'
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, Ref } from "vue"
|
||||
import { useRoute } from "vue-router"
|
||||
|
||||
import api, { LogOnSuccess, markedOptions, ProfileForView } from '@/api'
|
||||
import { citizenName } from '@/App.vue'
|
||||
import { useStore } from '@/store'
|
||||
import LoadData from '@/components/LoadData.vue'
|
||||
import api, { LogOnSuccess, ProfileForView } from "@/api"
|
||||
import { citizenName } from "@/App.vue"
|
||||
import { toHtml } from "@/markdown"
|
||||
import { useStore } from "@/store"
|
||||
import LoadData from "@/components/LoadData.vue"
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ProfileView',
|
||||
components: { LoadData },
|
||||
setup () {
|
||||
const store = useStore()
|
||||
const route = useRoute()
|
||||
const store = useStore()
|
||||
const route = useRoute()
|
||||
|
||||
/** The currently logged-on user */
|
||||
const user = store.state.user as LogOnSuccess
|
||||
/** The currently logged-on user */
|
||||
const user = store.state.user as LogOnSuccess
|
||||
|
||||
/** The requested profile */
|
||||
const it : Ref<ProfileForView | undefined> = ref(undefined)
|
||||
/** The requested profile */
|
||||
const it : Ref<ProfileForView | undefined> = ref(undefined)
|
||||
|
||||
/** The work types for the top of the page */
|
||||
const workTypes = computed(() => {
|
||||
const parts : string[] = []
|
||||
if (it.value) {
|
||||
const p = it.value.profile
|
||||
if (p.seekingEmployment) {
|
||||
parts.push('<strong><em>CURRENTLY SEEKING EMPLOYMENT</em></strong>')
|
||||
} else {
|
||||
parts.push('Not actively seeking employment')
|
||||
}
|
||||
parts.push(`${p.fullTime ? 'I' : 'Not i'}nterested in full-time employment`)
|
||||
parts.push(`${p.remoteWork ? 'I' : 'Not i'}nterested in remote opportunities`)
|
||||
}
|
||||
return parts.join(' • ')
|
||||
})
|
||||
|
||||
/** Retrieve the profile and supporting data */
|
||||
const retrieveProfile = async (errors : string[]) => {
|
||||
const profileResp = await api.profile.retreiveForView(route.params.id as string, user)
|
||||
if (typeof profileResp === 'string') {
|
||||
errors.push(profileResp)
|
||||
} else if (typeof profileResp === 'undefined') {
|
||||
errors.push('Profile not found')
|
||||
} else {
|
||||
it.value = profileResp
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
pageTitle: computed(() =>
|
||||
it.value ? `Employment profile for ${citizenName(it.value.citizen)}` : 'Loading Profile...'),
|
||||
user,
|
||||
retrieveProfile,
|
||||
citizenName,
|
||||
it,
|
||||
workTypes,
|
||||
bioHtml: computed(() => marked(it.value?.profile.biography || '', markedOptions)),
|
||||
expHtml: computed(() => marked(it.value?.profile.experience || '', markedOptions))
|
||||
/** The work types for the top of the page */
|
||||
const workTypes = computed(() => {
|
||||
const parts : string[] = []
|
||||
if (it.value) {
|
||||
const p = it.value.profile
|
||||
if (p.seekingEmployment) {
|
||||
parts.push("<strong><em>CURRENTLY SEEKING EMPLOYMENT</em></strong>")
|
||||
} else {
|
||||
parts.push("Not actively seeking employment")
|
||||
}
|
||||
parts.push(`${p.fullTime ? "I" : "Not i"}nterested in full-time employment`)
|
||||
parts.push(`${p.remoteWork ? "I" : "Not i"}nterested in remote opportunities`)
|
||||
}
|
||||
return parts.join(" • ")
|
||||
})
|
||||
|
||||
/** Retrieve the profile and supporting data */
|
||||
const retrieveProfile = async (errors : string[]) => {
|
||||
const profileResp = await api.profile.retreiveForView(route.params.id as string, user)
|
||||
if (typeof profileResp === "string") {
|
||||
errors.push(profileResp)
|
||||
} else if (typeof profileResp === "undefined") {
|
||||
errors.push("Profile not found")
|
||||
} else {
|
||||
it.value = profileResp
|
||||
}
|
||||
}
|
||||
|
||||
/** The title of the page (changes once the profile is loaded) */
|
||||
const title = computed(() => it.value
|
||||
? `Employment profile for ${citizenName(it.value.citizen)}`
|
||||
: "Loading Profile...")
|
||||
|
||||
/** The HTML version of the citizen's professional biography */
|
||||
const bioHtml = computed(() => toHtml(it.value?.profile.biography ?? ""))
|
||||
|
||||
/** The HTML version of the citizens Experience section */
|
||||
const expHtml = computed(() => toHtml(it.value?.profile.experience ?? ""))
|
||||
</script>
|
||||
|
@ -1,127 +1,113 @@
|
||||
<template lang="pug">
|
||||
article
|
||||
page-title(title='People Seeking Work')
|
||||
page-title(title="People Seeking Work")
|
||||
h3.pb-3 People Seeking Work
|
||||
p(v-if='!searched').
|
||||
p(v-if="!searched").
|
||||
Enter one or more criteria to filter results, or just click “Search” to list all profiles.
|
||||
collapse-panel(headerText='Search Criteria' :collapsed='isCollapsed' @toggle='toggleCollapse')
|
||||
profile-public-search-form(v-model='criteria' @search='doSearch')
|
||||
error-list(:errors='errors')
|
||||
p(v-if='searching') Searching profiles…
|
||||
collapse-panel(headerText="Search Criteria" :collapsed="isCollapsed" @toggle="toggleCollapse")
|
||||
profile-public-search-form(v-model="criteria" @search="doSearch")
|
||||
error-list(:errors="errors")
|
||||
p(v-if="searching") Searching profiles…
|
||||
template(v-else)
|
||||
template(v-if='results.length > 0')
|
||||
template(v-if="results.length > 0")
|
||||
p.pb-3.pt-3.
|
||||
These profiles match your search criteria. To learn more about these people, join the merry band of human
|
||||
resources in the #[a(href='https://noagendashow.net' target='_blank') No Agenda] tribe!
|
||||
resources in the #[a(href="https://noagendashow.net" target="_blank") No Agenda] tribe!
|
||||
table.table.table-sm.table-hover
|
||||
thead: tr
|
||||
th(scope='col') Continent
|
||||
th.text-center(scope='col') Region
|
||||
th.text-center(scope='col') Remote?
|
||||
th.text-center(scope='col') Skills
|
||||
tbody: tr(v-for='(profile, idx) in results' :key='idx')
|
||||
th(scope="col") Continent
|
||||
th.text-center(scope="col") Region
|
||||
th.text-center(scope="col") Remote?
|
||||
th.text-center(scope="col") Skills
|
||||
tbody: tr(v-for="(profile, idx) in results" :key="idx")
|
||||
td {{profile.continent}}
|
||||
td {{profile.region}}
|
||||
td.text-center {{yesOrNo(profile.remoteWork)}}
|
||||
td: template(v-for='(skill, idx) in profile.skills' :key='idx') {{skill}}#[br]
|
||||
p.pt-3(v-else-if='searched') No results found for the specified criteria
|
||||
td: template(v-for="(skill, idx) in profile.skills" :key="idx") {{skill}}#[br]
|
||||
p.pt-3(v-else-if="searched") No results found for the specified criteria
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, Ref, ref, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { yesOrNo } from '@/App.vue'
|
||||
import api, { PublicSearch, PublicSearchResult } from '@/api'
|
||||
import { queryValue } from '@/router'
|
||||
<script setup lang="ts">
|
||||
import { Ref, ref, watch } from "vue"
|
||||
import { useRoute, useRouter } from "vue-router"
|
||||
import { yesOrNo } from "@/App.vue"
|
||||
import api, { PublicSearch, PublicSearchResult } from "@/api"
|
||||
import { queryValue } from "@/router"
|
||||
|
||||
import CollapsePanel from '@/components/CollapsePanel.vue'
|
||||
import ErrorList from '@/components/ErrorList.vue'
|
||||
import ProfilePublicSearchForm from '@/components/profile/PublicSearchForm.vue'
|
||||
import CollapsePanel from "@/components/CollapsePanel.vue"
|
||||
import ErrorList from "@/components/ErrorList.vue"
|
||||
import ProfilePublicSearchForm from "@/components/profile/PublicSearchForm.vue"
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
CollapsePanel,
|
||||
ErrorList,
|
||||
ProfilePublicSearchForm
|
||||
},
|
||||
setup () {
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
/** Whether a search has been performed */
|
||||
const searched = ref(false)
|
||||
/** Whether a search has been performed */
|
||||
const searched = ref(false)
|
||||
|
||||
/** Indicates whether a request for matching profiles is in progress */
|
||||
const searching = ref(false)
|
||||
/** Indicates whether a request for matching profiles is in progress */
|
||||
const searching = ref(false)
|
||||
|
||||
/** Error messages encountered while searching for profiles */
|
||||
const errors : Ref<string[]> = ref([])
|
||||
/** Error messages encountered while searching for profiles */
|
||||
const errors : Ref<string[]> = ref([])
|
||||
|
||||
/** An empty set of search criteria */
|
||||
const emptyCriteria = {
|
||||
continentId: '',
|
||||
region: undefined,
|
||||
skill: undefined,
|
||||
remoteWork: ''
|
||||
}
|
||||
/** An empty set of search criteria */
|
||||
const emptyCriteria = {
|
||||
continentId: '',
|
||||
region: undefined,
|
||||
skill: undefined,
|
||||
remoteWork: ''
|
||||
}
|
||||
|
||||
/** The search criteria being built from the page */
|
||||
const criteria : Ref<PublicSearch> = ref(emptyCriteria)
|
||||
/** The search criteria being built from the page */
|
||||
const criteria : Ref<PublicSearch> = ref(emptyCriteria)
|
||||
|
||||
/** The search results */
|
||||
const results : Ref<PublicSearchResult[]> = ref([])
|
||||
/** The search results */
|
||||
const results : Ref<PublicSearchResult[]> = ref([])
|
||||
|
||||
/** Whether the search results are collapsed */
|
||||
const isCollapsed = ref(searched.value && results.value.length > 0)
|
||||
/** Whether the search results are collapsed */
|
||||
const isCollapsed = ref(searched.value && results.value.length > 0)
|
||||
|
||||
/** Set up the page to match its requested state */
|
||||
const setUpPage = async () => {
|
||||
if (queryValue(route, 'searched') === 'true') {
|
||||
searched.value = true
|
||||
try {
|
||||
searching.value = true
|
||||
const contId = queryValue(route, 'continentId')
|
||||
const searchParams : PublicSearch = {
|
||||
continentId: contId === '' ? undefined : contId,
|
||||
region: queryValue(route, 'region'),
|
||||
skill: queryValue(route, 'skill'),
|
||||
remoteWork: queryValue(route, 'remoteWork') || ''
|
||||
}
|
||||
const searchResult = await api.profile.publicSearch(searchParams)
|
||||
if (typeof searchResult === 'string') {
|
||||
errors.value.push(searchResult)
|
||||
} else if (searchResult === undefined) {
|
||||
errors.value.push('The server returned a "Not Found" response (this should not happen)')
|
||||
} else {
|
||||
results.value = searchResult
|
||||
searchParams.continentId = searchParams.continentId || ''
|
||||
criteria.value = searchParams
|
||||
}
|
||||
} finally {
|
||||
searching.value = false
|
||||
}
|
||||
isCollapsed.value = searched.value && results.value.length > 0
|
||||
} else {
|
||||
searched.value = false
|
||||
criteria.value = emptyCriteria
|
||||
errors.value = []
|
||||
results.value = []
|
||||
/** Set up the page to match its requested state */
|
||||
const setUpPage = async () => {
|
||||
if (queryValue(route, "searched") === "true") {
|
||||
searched.value = true
|
||||
try {
|
||||
searching.value = true
|
||||
const contId = queryValue(route, "continentId")
|
||||
const searchParams : PublicSearch = {
|
||||
continentId: contId === "" ? undefined : contId,
|
||||
region: queryValue(route, "region"),
|
||||
skill: queryValue(route, "skill"),
|
||||
remoteWork: queryValue(route, "remoteWork") ?? ""
|
||||
}
|
||||
const searchResult = await api.profile.publicSearch(searchParams)
|
||||
if (typeof searchResult === "string") {
|
||||
errors.value.push(searchResult)
|
||||
} else if (searchResult === undefined) {
|
||||
errors.value.push(`The server returned a "Not Found" response (this should not happen)`)
|
||||
} else {
|
||||
results.value = searchResult
|
||||
searchParams.continentId = searchParams.continentId ?? ""
|
||||
criteria.value = searchParams
|
||||
}
|
||||
} finally {
|
||||
searching.value = false
|
||||
}
|
||||
|
||||
watch(() => route.query, setUpPage, { immediate: true })
|
||||
|
||||
return {
|
||||
errors,
|
||||
criteria,
|
||||
isCollapsed,
|
||||
toggleCollapse: (it : boolean) => { isCollapsed.value = it },
|
||||
doSearch: () => router.push({ query: { searched: 'true', ...criteria.value } }),
|
||||
searching,
|
||||
searched,
|
||||
results,
|
||||
yesOrNo
|
||||
}
|
||||
isCollapsed.value = searched.value && results.value.length > 0
|
||||
} else {
|
||||
searched.value = false
|
||||
criteria.value = emptyCriteria
|
||||
errors.value = []
|
||||
results.value = []
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/** Refresh the page when the query string changes */
|
||||
watch(() => route.query, setUpPage, { immediate: true })
|
||||
|
||||
/** Open and closed the search parameter panel */
|
||||
const toggleCollapse = (it : boolean) => { isCollapsed.value = it }
|
||||
|
||||
/** Execute a search */
|
||||
const doSearch = () => router.push({ query: { searched: 'true', ...criteria.value } })
|
||||
</script>
|
||||
|
@ -1,13 +1,13 @@
|
||||
<template lang="pug">
|
||||
article
|
||||
page-title(title='Account Deletion Options')
|
||||
page-title(title="Account Deletion Options")
|
||||
h3.pb-3 Account Deletion Options
|
||||
h4.pb-3 Option 1 – Delete Your Profile
|
||||
p.
|
||||
Utilizing this option will remove your current employment profile and skills. This will preserve any success stories
|
||||
you may have written, and preserves this application’s knowledge of you. This is what you want to use if you
|
||||
want to clear out your profile and start again (and remove the current one from others’ view).
|
||||
p.text-center: button.btn.btn-danger(@click.prevent='deleteProfile') Delete Your Profile
|
||||
p.text-center: button.btn.btn-danger(@click.prevent="deleteProfile") Delete Your Profile
|
||||
hr
|
||||
h4.pb-3 Option 2 – Delete Your Account
|
||||
p.
|
||||
@ -18,49 +18,40 @@ article
|
||||
(This will not revoke this application’s permissions on No Agenda Social; you will have to remove this
|
||||
yourself. The confirmation message has a link where you can do this; once the page loads, find the
|
||||
#[strong Jobs, Jobs, Jobs] entry, and click the #[strong × Revoke] link for that entry.)
|
||||
p.text-center: button.btn.btn-danger(@click.prevent='deleteAccount') Delete Your Entire Account
|
||||
p.text-center: button.btn.btn-danger(@click.prevent="deleteAccount") Delete Your Entire Account
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import api, { LogOnSuccess } from '@/api'
|
||||
import { toastError, toastSuccess } from '@/components/layout/AppToaster.vue'
|
||||
import { useStore } from '@/store'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'DeletionOptions',
|
||||
setup () {
|
||||
const store = useStore()
|
||||
const router = useRouter()
|
||||
|
||||
/** Delete the profile only; redirect to home page on success */
|
||||
const deleteProfile = async () => {
|
||||
const resp = await api.profile.delete(store.state.user as LogOnSuccess)
|
||||
if (typeof resp === 'string') {
|
||||
toastError(resp, 'Deleting Profile')
|
||||
} else {
|
||||
toastSuccess('Profile Deleted Successfully')
|
||||
router.push('/citizen/dashboard')
|
||||
}
|
||||
}
|
||||
|
||||
/** Delete everything pertaining to the user's account */
|
||||
const deleteAccount = async () => {
|
||||
const resp = await api.citizen.delete(store.state.user as LogOnSuccess)
|
||||
if (typeof resp === 'string') {
|
||||
toastError(resp, 'Deleting Account')
|
||||
} else {
|
||||
store.commit('clearUser')
|
||||
toastSuccess('Account Deleted Successfully')
|
||||
router.push('/so-long/success')
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
deleteProfile,
|
||||
deleteAccount
|
||||
}
|
||||
}
|
||||
})
|
||||
import { useRouter } from "vue-router"
|
||||
import api, { LogOnSuccess } from "@/api"
|
||||
import { toastError, toastSuccess } from "@/components/layout/AppToaster.vue"
|
||||
import { useStore } from "@/store"
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
const store = useStore()
|
||||
const router = useRouter()
|
||||
|
||||
/** Delete the profile only; redirect to home page on success */
|
||||
const deleteProfile = async () => {
|
||||
const resp = await api.profile.delete(store.state.user as LogOnSuccess)
|
||||
if (typeof resp === "string") {
|
||||
toastError(resp, "Deleting Profile")
|
||||
} else {
|
||||
toastSuccess("Profile Deleted Successfully")
|
||||
router.push("/citizen/dashboard")
|
||||
}
|
||||
}
|
||||
|
||||
/** Delete everything pertaining to the user's account */
|
||||
const deleteAccount = async () => {
|
||||
const resp = await api.citizen.delete(store.state.user as LogOnSuccess)
|
||||
if (typeof resp === "string") {
|
||||
toastError(resp, "Deleting Account")
|
||||
} else {
|
||||
store.commit("clearUser")
|
||||
toastSuccess("Account Deleted Successfully")
|
||||
router.push("/so-long/success")
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
@ -1,10 +1,10 @@
|
||||
<template lang="pug">
|
||||
article
|
||||
page-title(title='Account Deletion Success')
|
||||
page-title(title="Account Deletion Success")
|
||||
h3.pb-3 Account Deletion Success
|
||||
p.
|
||||
Your account has been successfully deleted. To revoke the permissions you have previously granted to this
|
||||
application, find it in #[a(href='https://noagendasocial.com/oauth/authorized_applications') this list] and click
|
||||
application, find it in #[a(href="https://noagendasocial.com/oauth/authorized_applications") this list] and click
|
||||
#[strong × Revoke]. Otherwise, clicking “Log On” in the left-hand menu will create a new, empty
|
||||
account without prompting you further.
|
||||
p Thank you for participating, and thank you for your courage. #GitmoNation
|
||||
|
@ -1,143 +1,128 @@
|
||||
<template lang="pug">
|
||||
article
|
||||
page-title(:title='title')
|
||||
page-title(:title="title")
|
||||
h3.pb-3 {{title}}
|
||||
load-data(:load='retrieveStory')
|
||||
p(v-if='isNew').
|
||||
load-data(:load="retrieveStory")
|
||||
p(v-if="isNew").
|
||||
Congratulations on your employment! Your fellow citizens would enjoy hearing how it all came about; tell us
|
||||
about it below! #[em (These will be visible to other users, but not to the general public.)]
|
||||
form.row.g-3
|
||||
.col-12: .form-check
|
||||
input.form-check-input(type='checkbox' id='fromHere' v-model='v$.fromHere.$model')
|
||||
label.form-check-label(for='fromHere') I found my employment here
|
||||
markdown-editor(id='story' label='The Success Story' v-model:text='v$.story.$model')
|
||||
input.form-check-input(type="checkbox" id="fromHere" v-model="v$.fromHere.$model")
|
||||
label.form-check-label(for="fromHere") I found my employment here
|
||||
markdown-editor(id="story" label="The Success Story" v-model:text="v$.story.$model")
|
||||
.col-12
|
||||
button.btn.btn-primary(type='submit' @click.prevent='saveStory(true)').
|
||||
#[icon(icon='content-save-outline')] Save
|
||||
p(v-if='isNew'): em (Saving this will set “Seeking Employment” to “No” on your profile.)
|
||||
maybe-save(:isShown='confirmNavShown' :toRoute='nextRoute' :saveAction='doSave' :validator='v$' @close='confirmClose')
|
||||
button.btn.btn-primary(type="submit" @click.prevent="saveStory(true)").
|
||||
#[icon(icon="content-save-outline")] Save
|
||||
p(v-if="isNew"): em (Saving this will set “Seeking Employment” to “No” on your profile.)
|
||||
maybe-save(:isShown="confirmNavShown" :toRoute="nextRoute" :saveAction="doSave" :validator="v$" @close="confirmClose")
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, reactive, ref, Ref } from 'vue'
|
||||
import { onBeforeRouteLeave, RouteLocationNormalized, useRoute, useRouter } from 'vue-router'
|
||||
import useVuelidate from '@vuelidate/core'
|
||||
import api, { LogOnSuccess, StoryForm } from '@/api'
|
||||
import { toastError, toastSuccess } from '@/components/layout/AppToaster.vue'
|
||||
import { useStore } from '@/store'
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive, ref, Ref } from "vue"
|
||||
import { onBeforeRouteLeave, RouteLocationNormalized, useRoute, useRouter } from "vue-router"
|
||||
import useVuelidate from "@vuelidate/core"
|
||||
|
||||
import LoadData from '@/components/LoadData.vue'
|
||||
import MarkdownEditor from '@/components/MarkdownEditor.vue'
|
||||
import MaybeSave from '@/components/MaybeSave.vue'
|
||||
import api, { LogOnSuccess, StoryForm } from "@/api"
|
||||
import { toastError, toastSuccess } from "@/components/layout/AppToaster.vue"
|
||||
import { useStore } from "@/store"
|
||||
|
||||
export default defineComponent({
|
||||
name: 'StoryEdit',
|
||||
components: {
|
||||
LoadData,
|
||||
MarkdownEditor,
|
||||
MaybeSave
|
||||
},
|
||||
setup () {
|
||||
const store = useStore()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
import LoadData from "@/components/LoadData.vue"
|
||||
import MarkdownEditor from "@/components/MarkdownEditor.vue"
|
||||
import MaybeSave from "@/components/MaybeSave.vue"
|
||||
|
||||
/** The currently logged-on user */
|
||||
const user = store.state.user as LogOnSuccess
|
||||
const store = useStore()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
/** The ID of the story being edited */
|
||||
const id = route.params.id as string
|
||||
/** The currently logged-on user */
|
||||
const user = store.state.user as LogOnSuccess
|
||||
|
||||
/** Whether this is a new story */
|
||||
const isNew = computed(() => id === 'new')
|
||||
/** The ID of the story being edited */
|
||||
const id = route.params.id as string
|
||||
|
||||
/** The page title */
|
||||
const title = computed(() => isNew.value ? 'Tell Your Success Story' : 'Edit Success Story')
|
||||
/** Whether this is a new story */
|
||||
const isNew = computed(() => id === "new")
|
||||
|
||||
/** The form for editing the story */
|
||||
const story = reactive(new StoryForm())
|
||||
/** The page title */
|
||||
const title = computed(() => isNew.value ? "Tell Your Success Story" : "Edit Success Story")
|
||||
|
||||
/** Validator rules */
|
||||
const rules = computed(() => ({
|
||||
fromHere: { },
|
||||
story: { }
|
||||
}))
|
||||
/** The form for editing the story */
|
||||
const story = reactive(new StoryForm())
|
||||
|
||||
/** The validator */
|
||||
const v$ = useVuelidate(rules, story, { $lazy: true })
|
||||
/** Validator rules */
|
||||
const rules = computed(() => ({
|
||||
fromHere: { },
|
||||
story: { }
|
||||
}))
|
||||
|
||||
/** Retrieve the specified story */
|
||||
const retrieveStory = async (errors : string[]) => {
|
||||
if (isNew.value) {
|
||||
story.id = 'new'
|
||||
} else {
|
||||
const storyResult = await api.success.retrieve(id, user)
|
||||
if (typeof storyResult === 'string') {
|
||||
errors.push(storyResult)
|
||||
} else if (typeof storyResult === 'undefined') {
|
||||
errors.push('Story not found')
|
||||
} else if (storyResult.citizenId !== user.citizenId) {
|
||||
errors.push('Quit messing around')
|
||||
} else {
|
||||
story.id = storyResult.id
|
||||
story.fromHere = storyResult.fromHere
|
||||
story.story = storyResult.story || ''
|
||||
}
|
||||
}
|
||||
}
|
||||
/** The validator */
|
||||
const v$ = useVuelidate(rules, story, { $lazy: true })
|
||||
|
||||
/** Save the success story */
|
||||
const saveStory = async (navigate : boolean) => {
|
||||
const saveResult = await api.success.save(story, user)
|
||||
if (typeof saveResult === 'string') {
|
||||
toastError(saveResult, 'saving success story')
|
||||
} else {
|
||||
if (isNew.value) {
|
||||
const foundResult = await api.profile.markEmploymentFound(user)
|
||||
if (typeof foundResult === 'string') {
|
||||
toastError(foundResult, 'clearing employment flag')
|
||||
} else {
|
||||
toastSuccess('Success Story saved and Seeking Employment flag cleared successfully')
|
||||
v$.value.$reset()
|
||||
if (navigate) {
|
||||
router.push('/success-story/list')
|
||||
}
|
||||
}
|
||||
} else {
|
||||
toastSuccess('Success Story saved successfully')
|
||||
v$.value.$reset()
|
||||
if (navigate) {
|
||||
router.push('/success-story/list')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Whether the navigation confirmation is shown */
|
||||
const confirmNavShown = ref(false)
|
||||
|
||||
/** The "next" route (will be navigated or cleared) */
|
||||
const nextRoute : Ref<RouteLocationNormalized | undefined> = ref(undefined)
|
||||
|
||||
/** Prompt for save if the user navigates away with unsaved changes */
|
||||
onBeforeRouteLeave(async (to, from) => { // eslint-disable-line
|
||||
if (!v$.value.$anyDirty) return true
|
||||
nextRoute.value = to
|
||||
confirmNavShown.value = true
|
||||
return false
|
||||
})
|
||||
|
||||
return {
|
||||
title,
|
||||
isNew,
|
||||
retrieveStory,
|
||||
v$,
|
||||
saveStory,
|
||||
confirmNavShown,
|
||||
nextRoute,
|
||||
doSave: async () => await saveStory(false),
|
||||
confirmClose: () => { confirmNavShown.value = false }
|
||||
/** Retrieve the specified story */
|
||||
const retrieveStory = async (errors : string[]) => {
|
||||
if (isNew.value) {
|
||||
story.id = "new"
|
||||
} else {
|
||||
const storyResult = await api.success.retrieve(id, user)
|
||||
if (typeof storyResult === "string") {
|
||||
errors.push(storyResult)
|
||||
} else if (typeof storyResult === "undefined") {
|
||||
errors.push("Story not found")
|
||||
} else if (storyResult.citizenId !== user.citizenId) {
|
||||
errors.push("Quit messing around")
|
||||
} else {
|
||||
story.id = storyResult.id
|
||||
story.fromHere = storyResult.fromHere
|
||||
story.story = storyResult.story ?? ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Save the success story */
|
||||
const saveStory = async (navigate : boolean) => {
|
||||
const saveResult = await api.success.save(story, user)
|
||||
if (typeof saveResult === "string") {
|
||||
toastError(saveResult, "saving success story")
|
||||
} else {
|
||||
if (isNew.value) {
|
||||
const foundResult = await api.profile.markEmploymentFound(user)
|
||||
if (typeof foundResult === "string") {
|
||||
toastError(foundResult, "clearing employment flag")
|
||||
} else {
|
||||
toastSuccess("Success Story saved and Seeking Employment flag cleared successfully")
|
||||
v$.value.$reset()
|
||||
if (navigate) {
|
||||
router.push("/success-story/list")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
toastSuccess("Success Story saved successfully")
|
||||
v$.value.$reset()
|
||||
if (navigate) {
|
||||
router.push("/success-story/list")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Whether the navigation confirmation is shown */
|
||||
const confirmNavShown = ref(false)
|
||||
|
||||
/** The "next" route (will be navigated or cleared) */
|
||||
const nextRoute : Ref<RouteLocationNormalized | undefined> = ref(undefined)
|
||||
|
||||
/** Prompt for save if the user navigates away with unsaved changes */
|
||||
onBeforeRouteLeave(async (to, from) => { // eslint-disable-line
|
||||
if (!v$.value.$anyDirty) return true
|
||||
nextRoute.value = to
|
||||
confirmNavShown.value = true
|
||||
return false
|
||||
})
|
||||
|
||||
/** No-parameter save function (used for save-on-navigate) */
|
||||
const doSave = async () => await saveStory(false)
|
||||
|
||||
/** Close the confirm navigation modal */
|
||||
const confirmClose = () => { confirmNavShown.value = false }
|
||||
</script>
|
||||
|
@ -1,68 +1,53 @@
|
||||
<template lang="pug">
|
||||
article
|
||||
page-title(title='Success Stories')
|
||||
page-title(title="Success Stories")
|
||||
h3.pb-3 Success Stories
|
||||
load-data(:load='retrieveStories')
|
||||
table.table.table-sm.table-hover(v-if='stories?.length > 0')
|
||||
load-data(:load="retrieveStories")
|
||||
table.table.table-sm.table-hover(v-if="stories?.length > 0")
|
||||
thead: tr
|
||||
th(scope='col') Story
|
||||
th(scope='col') From
|
||||
th(scope='col') Found Here?
|
||||
th(scope='col') Recorded On
|
||||
tbody: tr(v-for='story in stories' :key='story.id')
|
||||
th(scope="col") Story
|
||||
th(scope="col") From
|
||||
th(scope="col") Found Here?
|
||||
th(scope="col") Recorded On
|
||||
tbody: tr(v-for="story in stories" :key="story.id")
|
||||
td
|
||||
router-link(v-if='story.hasStory' :to='`/success-story/${story.id}/view`') View
|
||||
router-link(v-if="story.hasStory" :to="`/success-story/${story.id}/view`") View
|
||||
em(v-else) None
|
||||
template(v-if='story.citizenId === user.citizenId')
|
||||
| ~ #[router-link(:to='`/success-story/${story.id}/edit`') Edit]
|
||||
template(v-if="story.citizenId === user.citizenId")
|
||||
| ~ #[router-link(:to="`/success-story/${story.id}/edit`") Edit]
|
||||
td {{story.citizenName}}
|
||||
td
|
||||
strong(v-if='story.fromHere') Yes
|
||||
strong(v-if="story.fromHere") Yes
|
||||
template(v-else) No
|
||||
td: full-date(:date='story.recordedOn')
|
||||
td: full-date(:date="story.recordedOn")
|
||||
p(v-else) There are no success stories recorded #[em (yet)]
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, Ref } from 'vue'
|
||||
import api, { LogOnSuccess, StoryEntry } from '@/api'
|
||||
import { useStore } from '@/store'
|
||||
<script setup lang="ts">
|
||||
import { ref, Ref } from "vue"
|
||||
import api, { LogOnSuccess, StoryEntry } from "@/api"
|
||||
import { useStore } from "@/store"
|
||||
|
||||
import FullDate from '@/components/FullDate.vue'
|
||||
import LoadData from '@/components/LoadData.vue'
|
||||
import FullDate from "@/components/FullDate.vue"
|
||||
import LoadData from "@/components/LoadData.vue"
|
||||
|
||||
export default defineComponent({
|
||||
name: 'StoryList',
|
||||
components: {
|
||||
FullDate,
|
||||
LoadData
|
||||
},
|
||||
setup () {
|
||||
const store = useStore()
|
||||
const store = useStore()
|
||||
|
||||
/** The currently logged-on user */
|
||||
const user = store.state.user as LogOnSuccess
|
||||
/** The currently logged-on user */
|
||||
const user = store.state.user as LogOnSuccess
|
||||
|
||||
/** The success stories to be displayed */
|
||||
const stories : Ref<StoryEntry[] | undefined> = ref(undefined)
|
||||
/** The success stories to be displayed */
|
||||
const stories : Ref<StoryEntry[] | undefined> = ref(undefined)
|
||||
|
||||
/** Get all currently recorded stories */
|
||||
const retrieveStories = async (errors : string[]) => {
|
||||
const listResult = await api.success.list(user)
|
||||
if (typeof listResult === 'string') {
|
||||
errors.push(listResult)
|
||||
} else if (typeof listResult === 'undefined') {
|
||||
stories.value = []
|
||||
} else {
|
||||
stories.value = listResult
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
retrieveStories,
|
||||
stories,
|
||||
user
|
||||
}
|
||||
/** Get all currently recorded stories */
|
||||
const retrieveStories = async (errors : string[]) => {
|
||||
const listResult = await api.success.list(user)
|
||||
if (typeof listResult === "string") {
|
||||
errors.push(listResult)
|
||||
} else if (typeof listResult === "undefined") {
|
||||
stories.value = []
|
||||
} else {
|
||||
stories.value = listResult
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
@ -1,71 +1,60 @@
|
||||
<template lang="pug">
|
||||
article
|
||||
page-title(title='Success Story')
|
||||
load-data(:load='retrieveStory')
|
||||
page-title(title="Success Story")
|
||||
load-data(:load="retrieveStory")
|
||||
h3.pb-3 {{citizenName}}’s Success Story
|
||||
h4.text-muted: full-date-time(:date='story.recordedOn')
|
||||
p.fst-italic(v-if='story.fromHere'): strong Found via Jobs, Jobs, Jobs
|
||||
h4.text-muted: full-date-time(:date="story.recordedOn")
|
||||
p.fst-italic(v-if="story.fromHere"): strong Found via Jobs, Jobs, Jobs
|
||||
hr
|
||||
div(v-if='story.story' v-html='successStory')
|
||||
div(v-if="story.story" v-html="successStory")
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, Ref, ref } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import marked from 'marked'
|
||||
import api, { LogOnSuccess, markedOptions, Success } from '@/api'
|
||||
import { useStore } from '@/store'
|
||||
<script setup lang="ts">
|
||||
import { computed, Ref, ref } from "vue"
|
||||
import { useRoute } from "vue-router"
|
||||
|
||||
import FullDateTime from '@/components/FullDateTime.vue'
|
||||
import LoadData from '@/components/LoadData.vue'
|
||||
import api, { LogOnSuccess, Success } from "@/api"
|
||||
import { citizenName as citName } from "@/App.vue"
|
||||
import { toHtml } from '@/markdown'
|
||||
import { useStore } from "@/store"
|
||||
|
||||
export default defineComponent({
|
||||
name: 'StoryView',
|
||||
components: {
|
||||
FullDateTime,
|
||||
LoadData
|
||||
},
|
||||
setup () {
|
||||
const store = useStore()
|
||||
const route = useRoute()
|
||||
import FullDateTime from "@/components/FullDateTime.vue"
|
||||
import LoadData from "@/components/LoadData.vue"
|
||||
|
||||
/** The currently logged-on user */
|
||||
const user = store.state.user as LogOnSuccess
|
||||
const store = useStore()
|
||||
const route = useRoute()
|
||||
|
||||
/** The story to be displayed */
|
||||
const story : Ref<Success | undefined> = ref(undefined)
|
||||
/** The currently logged-on user */
|
||||
const user = store.state.user as LogOnSuccess
|
||||
|
||||
/** The citizen's name (real, display, or NAS, whichever is found first) */
|
||||
const citizenName = ref('')
|
||||
/** The story to be displayed */
|
||||
const story : Ref<Success | undefined> = ref(undefined)
|
||||
|
||||
/** Retrieve the success story */
|
||||
const retrieveStory = async (errors : string []) => {
|
||||
const storyResponse = await api.success.retrieve(route.params.id as string, user)
|
||||
if (typeof storyResponse === 'string') {
|
||||
errors.push(storyResponse)
|
||||
return
|
||||
}
|
||||
if (typeof storyResponse === 'undefined') {
|
||||
errors.push('Success story not found')
|
||||
return
|
||||
}
|
||||
story.value = storyResponse
|
||||
const citResponse = await api.citizen.retrieve(story.value.citizenId, user)
|
||||
if (typeof citResponse === 'string') {
|
||||
errors.push(citResponse)
|
||||
} else if (typeof citResponse === 'undefined') {
|
||||
errors.push('Citizen not found')
|
||||
} else {
|
||||
citizenName.value = citResponse.realName || citResponse.displayName || citResponse.naUser
|
||||
}
|
||||
}
|
||||
/** The citizen's name (real, display, or NAS, whichever is found first) */
|
||||
const citizenName = ref("")
|
||||
|
||||
return {
|
||||
story,
|
||||
retrieveStory,
|
||||
citizenName,
|
||||
successStory: computed(() => marked(story.value?.story || '', markedOptions))
|
||||
}
|
||||
/** Retrieve the success story */
|
||||
const retrieveStory = async (errors : string []) => {
|
||||
const storyResponse = await api.success.retrieve(route.params.id as string, user)
|
||||
if (typeof storyResponse === "string") {
|
||||
errors.push(storyResponse)
|
||||
return
|
||||
}
|
||||
})
|
||||
if (typeof storyResponse === "undefined") {
|
||||
errors.push("Success story not found")
|
||||
return
|
||||
}
|
||||
story.value = storyResponse
|
||||
const citResponse = await api.citizen.retrieve(story.value.citizenId, user)
|
||||
if (typeof citResponse === "string") {
|
||||
errors.push(citResponse)
|
||||
} else if (typeof citResponse === "undefined") {
|
||||
errors.push("Citizen not found")
|
||||
} else {
|
||||
citizenName.value = citName(citResponse)
|
||||
}
|
||||
}
|
||||
|
||||
/** The HTML success story */
|
||||
const successStory = computed(() => toHtml(story.value?.story ?? ""))
|
||||
</script>
|
||||
|
Loading…
Reference in New Issue
Block a user