Help wanted #23

Merged
danieljsummers merged 20 commits from help-wanted into main 2021-09-01 01:16:43 +00:00
54 changed files with 2183 additions and 2155 deletions
Showing only changes of commit dd549cf5f1 - Show all commits

View File

@ -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"
}
}

View File

@ -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",

View File

@ -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",

View File

@ -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>

View File

@ -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"

View File

@ -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 */

View File

@ -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>

View File

@ -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>

View File

@ -1,57 +1,58 @@
<template lang="pug">
.form-floating
select.form-select(id='continentId' :class="{ 'is-invalid': isInvalid}" :value='continentId'
@change='continentChanged')
option(value='') &ndash; {{emptyLabel}} &ndash;
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="") &ndash; {{emptyLabel}} &ndash;
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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -1,47 +1,31 @@
<template lang="pug">
div(v-if='loading') Loading&hellip;
error-list(v-else :errors='errors')
div(v-if="loading") Loading&hellip;
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>

View File

@ -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
| &nbsp;
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>

View File

@ -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>

View File

@ -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>

View 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)
}

View File

@ -1,25 +1,16 @@
<template lang="pug">
footer: p.text-muted.
Jobs, Jobs, Jobs v{{appVersion}} &bull; #[router-link(to='/privacy-policy') Privacy Policy]
&bull; #[router-link(to='/terms-of-service') Terms of Service]
Jobs, Jobs, Jobs v{{appVersion}} &bull; #[router-link(to="/privacy-policy") Privacy Policy]
&bull; #[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>

View File

@ -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 &nbsp;
nav
template(v-if='isLoggedOn')
router-link(to='/citizen/dashboard') #[icon(icon='view-dashboard-variant')]&nbsp; Dashboard
router-link(to='/help-wanted') #[icon(icon='newspaper-variant-multiple-outline')]&nbsp; Help Wanted!
router-link(to='/profile/search') #[icon(icon='view-list-outline')]&nbsp; Employment Profiles
router-link(to='/success-story/list') #[icon(icon='thumb-up')]&nbsp; Success Stories
template(v-if="isLoggedOn")
router-link(to="/citizen/dashboard") #[icon(icon="view-dashboard-variant")]&nbsp; Dashboard
router-link(to="/help-wanted") #[icon(icon="newspaper-variant-multiple-outline")]&nbsp; Help Wanted!
router-link(to="/profile/search") #[icon(icon="view-list-outline")]&nbsp; Employment Profiles
router-link(to="/success-story/list") #[icon(icon="thumb-up")]&nbsp; Success Stories
.separator
router-link(to='/listings/mine') #[icon(icon='sign-text')]&nbsp; My Job Listings
router-link(to='/citizen/profile') #[icon(icon='pencil')]&nbsp; My Employment Profile
router-link(to="/listings/mine") #[icon(icon="sign-text")]&nbsp; My Job Listings
router-link(to="/citizen/profile") #[icon(icon="pencil")]&nbsp; My Employment Profile
.separator
router-link(to='/citizen/log-off') #[icon(icon='logout-variant')]&nbsp; Log Off
router-link(to="/citizen/log-off") #[icon(icon="logout-variant")]&nbsp; Log Off
template(v-else)
router-link(to='/') #[icon(icon='home')]&nbsp; Home
router-link(to='/profile/seeking') #[icon(icon='view-list-outline')]&nbsp; Job Seekers
router-link(to='/citizen/log-on') #[icon(icon='login-variant')]&nbsp; Log On
router-link(to='/how-it-works') #[icon(icon='help-circle-outline')]&nbsp; How It Works
router-link(to="/") #[icon(icon="home")]&nbsp; Home
router-link(to="/profile/seeking") #[icon(icon="view-list-outline")]&nbsp; Job Seekers
router-link(to="/citizen/log-on") #[icon(icon="login-variant")]&nbsp; Log On
router-link(to="/how-it-works") #[icon(icon="help-circle-outline")]&nbsp; 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>

View File

@ -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>

View File

@ -2,19 +2,11 @@
nav.navbar.navbar-light.bg-light
span &nbsp;
span.navbar-text.
(...and Jobs &ndash; #[audio-clip(clip='pelosi-jobs') Let's Vote for Jobs!])
(&hellip;and Jobs &ndash; #[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>

View File

@ -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>

View File

@ -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>

View File

@ -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')") &nbsp;&minus;&nbsp;
button.btn.btn-sm.btn-outline-danger.rounded-pill(title="Delete" @click.prevent="$emit('remove')") &nbsp;&minus;&nbsp;
.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>

View File

@ -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")

View 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 } })
}

View File

@ -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"
}
})

View File

@ -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
}

View File

@ -1,3 +0,0 @@
declare module 'vuetify'
declare module 'vuetify/lib/components'
declare module 'vuetify/lib/directives'

View File

@ -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&hellip;</em>',
logOnState: "<em>Welcome back! Verifying your No Agenda Social account&hellip;</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)
}
}
},

View File

@ -1,6 +1,6 @@
<template lang="pug">
article
page-title(title='Welcome!')
page-title(title="Welcome!")
p &nbsp;
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 &nbsp;#[audio-clip(clip='thats-true') (that&rsquo;s true!)]] and find out what you&rsquo;re missing.
#[a(href="https://noagendashow.net" target="_blank") The Best Podcast in the Universe]
#[em &nbsp;#[audio-clip(clip="thats-true") (that&rsquo;s true!)]] and find out what you&rsquo;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>

View File

@ -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 &ldquo;Professional Biography&rdquo; and &ldquo;Experience&rdquo; 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 &ldquo;a quote&rdquo;), 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>

View File

@ -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}}&rsquo;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>

View File

@ -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&rsquo;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>

View File

@ -1,47 +1,41 @@
<template lang="pug">
article
page-title(title='Logging on...')
page-title(title="Logging on...")
p &nbsp;
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 &ldquo;Cancel&rdquo;?)')
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 &ldquo;Cancel&rdquo;?)")
}
})
}
onMounted(logOn)
/** Accessor for the log on state */
const message = computed(() => store.state.logOnState)
</script>

View File

@ -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 &ldquo;Edit Profile&rdquo; 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
| &nbsp; &nbsp;
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&rsquo;s just you&hellip;
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&rsquo;s just you&hellip;
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&hellip;
.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 &nbsp;
p.
To see how this application works, check out &ldquo;How It Works&rdquo; 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>

View File

@ -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 &nbsp;#[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 &nbsp;#[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&rsquo;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')]&nbsp; 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")]&nbsp; Save
template(v-if="!isNew")
| &nbsp; &nbsp;
router-link.btn.btn-outline-secondary(:to='`/profile/${user.citizenId}/view`').
#[icon(icon='file-account-outline')]&nbsp; View Your User Profile
router-link.btn.btn-outline-secondary(:to="`/profile/${user.citizenId}/view`").
#[icon(icon="file-account-outline")]&nbsp; 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>

View File

@ -1,29 +1,22 @@
<template lang="pug">
article
page-title(title='Logging off...')
page-title(title="Logging off...")
p &nbsp;
p.fst-italic Logging off&hellip;
</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 &nbsp; | &nbsp; <strong>Have a Nice Day!</strong>')
router.push('/')
})
return { }
}
onMounted(() => {
store.commit("clearUser")
toastSuccess("Log Off Successful &nbsp; | &nbsp; <strong>Have a Nice Day!</strong>")
router.push("/")
})
</script>

View File

@ -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&hellip;
</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>

View File

@ -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 &ldquo;Search&rdquo; 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&hellip;
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&hellip;
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>

View File

@ -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')]&nbsp; 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")]&nbsp; 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>

View File

@ -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)}}]] &bull;
| 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>

View File

@ -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>

View 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")
}

View File

@ -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 &ldquo;Search&rdquo; 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&hellip;
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&hellip;
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>

View File

@ -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") &nbsp;({{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')]&nbsp; Edit Your Profile
router-link.btn.btn-primary(to="/citizen/profile") #[icon(icon="pencil")]&nbsp; 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(' &bull; ')
})
/** 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(" &bull; ")
})
/** 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>

View File

@ -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 &ldquo;Search&rdquo; 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&hellip;
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&hellip;
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>

View File

@ -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 &ndash; 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&rsquo;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&rsquo; 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 &ndash; Delete Your Account
p.
@ -18,49 +18,40 @@ article
(This will not revoke this application&rsquo;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 &times; 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>

View File

@ -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 &times; Revoke]. Otherwise, clicking &ldquo;Log On&rdquo; 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

View File

@ -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')]&nbsp; Save
p(v-if='isNew'): em (Saving this will set &ldquo;Seeking Employment&rdquo; to &ldquo;No&rdquo; 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")]&nbsp; Save
p(v-if="isNew"): em (Saving this will set &ldquo;Seeking Employment&rdquo; to &ldquo;No&rdquo; 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>

View File

@ -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>

View File

@ -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}}&rsquo;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>